# HG changeset patch # User Sebastien Jodogne # Date 1539350290 -7200 # Node ID 497a637366b4ac30ce5c7e8190d6b967dce5324c # Parent 2b91363cc1d1d30c7e2638c50050b0591836eac9# Parent 320a877a1f4016025d5fae236a1167f3eca56438 integration mainline->db-changes diff -r 2b91363cc1d1 -r 497a637366b4 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,5 @@ +syntax: glob +ThirdPartyDownloads/ +CMakeLists.txt.user +*.cpp.orig +*.h.orig diff -r 2b91363cc1d1 -r 497a637366b4 .hgtags --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgtags Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,1 @@ +a95beca72e99f3a1110cffd252bcf3abf5a2db27 dcmtk-3.6.1 diff -r 2b91363cc1d1 -r 497a637366b4 .travis.yml --- a/.travis.yml Thu Oct 29 11:25:45 2015 +0100 +++ b/.travis.yml Fri Oct 12 15:18:10 2018 +0200 @@ -31,20 +31,24 @@ -qq build-essential unzip cmake mercurial uuid-dev libcurl4-openssl-dev liblua5.1-0-dev libgtest-dev libpng-dev libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev libwrap0-dev libcharls-dev; fi + # For DCMTK 3.6.2 - Can't make it compile in static mode with MinGW32 on the + # Ubuntu Precise (12.04) that is used by Travis: + # - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw-w64 gcc-mingw-w64-i686 g++-mingw-w64-i686 wine; fi + + # For DCMTK 3.6.0: - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw32; fi - before_script: - mkdir Build - cd Build - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == OFF ]; then cmake -DCMAKE_BUILD_TYPE=Debug "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog" -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_BOOST=OFF -DUSE_SYSTEM_MONGOOSE=OFF -DUSE_SYSTEM_JSONCPP=OFF - -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON + -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON ..; fi - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then cmake -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON - -DCMAKE_TOOLCHAIN_FILE=Resources/MinGWToolchain.cmake + -DCMAKE_TOOLCHAIN_FILE=Resources/MinGWToolchain.cmake -DUSE_DCMTK_360=ON -DUSE_LEGACY_JSONCPP=ON ..; fi - if [ $TRAVIS_OS_NAME == osx ]; then cmake -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON diff -r 2b91363cc1d1 -r 497a637366b4 AUTHORS --- a/AUTHORS Thu Oct 29 11:25:45 2015 +0100 +++ b/AUTHORS Fri Oct 12 15:18:10 2018 +0200 @@ -6,28 +6,15 @@ ------------------ * Sebastien Jodogne - Department of Medical Physics - University Hospital of Liege - Belgium Overall design and lead developer. - -Client library --------------- - -The client library of Orthanc is automatically wrapped from the C++ -code using the LAAW software. LAAW is the Lightweight, Automated API -Wrapper from the Jomago team, that comes from the following authors: +* Department of Medical Physics + University Hospital of Liege + 4000 Liege + Belgium -* Sebastien Jodogne -* Alain Mazy -* Benjamin Golinvaux - -LAAW should be soon released as a separate open-source project. - - -Contributors ------------- - -See the file "THANKS" for the occasional contributors. +* Osimis S.A. + Rue du Bois Saint-Jean 15/1 + 4102 Seraing + Belgium diff -r 2b91363cc1d1 -r 497a637366b4 CMakeLists.txt --- a/CMakeLists.txt Thu Oct 29 11:25:45 2015 +0100 +++ b/CMakeLists.txt Fri Oct 12 15:18:10 2018 +0200 @@ -1,575 +1,525 @@ -cmake_minimum_required(VERSION 2.8) - -project(Orthanc) - -# Version of the build, should always be "mainline" except in release branches -set(ORTHANC_VERSION "mainline") - -# Version of the database schema. History: -# * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning -# * Orthanc 0.3.1 = version 2 -# * Orthanc 0.4.0 -> Orthanc 0.7.2 = version 3 -# * Orthanc 0.7.3 -> Orthanc 0.8.4 = version 4 -# * Orthanc 0.8.5 -> Orthanc 0.9.4 = version 5 -# * Orthanc 0.9.5 -> mainline = version 6 -set(ORTHANC_DATABASE_VERSION 6) - - -##################################################################### -## CMake parameters tunable at the command line -##################################################################### - -# Parameters of the build -SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") -SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") -SET(ENABLE_SSL ON CACHE BOOL "Include support for SSL") -SET(DCMTK_DICTIONARY_DIR "" CACHE PATH "Directory containing the DCMTK dictionaries \"dicom.dic\" and \"private.dic\" (only when using system version of DCMTK)") -SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") -SET(ENABLE_GOOGLE_LOG OFF CACHE BOOL "Enable Google Log (otherwise, an internal logger is used)") -SET(ENABLE_JPEG ON CACHE BOOL "Enable JPEG decompression") -SET(ENABLE_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression") -SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") -SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Build the ServeFolders plugin") - -# Advanced parameters to fine-tune linking against system libraries -SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") -SET(USE_SYSTEM_GOOGLE_LOG ON CACHE BOOL "Use the system version of Google Log") -SET(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test") -SET(USE_SYSTEM_SQLITE ON CACHE BOOL "Use the system version of SQLite") -SET(USE_SYSTEM_MONGOOSE ON CACHE BOOL "Use the system version of Mongoose") -SET(USE_SYSTEM_LUA ON CACHE BOOL "Use the system version of Lua") -SET(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") -SET(USE_SYSTEM_LIBPNG ON CACHE BOOL "Use the system version of libpng") -SET(USE_SYSTEM_LIBJPEG ON CACHE BOOL "Use the system version of libjpeg") -SET(USE_SYSTEM_CURL ON CACHE BOOL "Use the system version of LibCurl") -SET(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL") -SET(USE_SYSTEM_ZLIB ON CACHE BOOL "Use the system version of ZLib") -SET(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml)") - -# Experimental options -SET(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)") - -# Distribution-specific settings -SET(USE_GTEST_DEBIAN_SOURCE_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)") -SET(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)") -SET(USE_BOOST_ICONV ON CACHE BOOL "Use iconv instead of wconv (Windows only)") - -mark_as_advanced(USE_GTEST_DEBIAN_SOURCE_PACKAGE) -mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS) -mark_as_advanced(USE_BOOST_ICONV) -mark_as_advanced(USE_PUGIXML) - -# Path to the root folder of the Orthanc distribution -set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}) - -# Some basic inclusions -include(CheckIncludeFiles) -include(CheckIncludeFileCXX) -include(CheckLibraryExists) -include(FindPythonInterp) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/AutoGeneratedCode.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/DownloadPackage.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/Compiler.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/VisualStudioPrecompiledHeaders.cmake) - - - - -##################################################################### -## List of source files -##################################################################### - -set(ORTHANC_CORE_SOURCES - Core/Cache/MemoryCache.cpp - Core/Cache/SharedArchive.cpp - Core/ChunkedBuffer.cpp - Core/Compression/DeflateBaseCompressor.cpp - Core/Compression/GzipCompressor.cpp - Core/Compression/HierarchicalZipWriter.cpp - Core/Compression/ZipWriter.cpp - Core/Compression/ZlibCompressor.cpp - Core/DicomFormat/DicomArray.cpp - Core/DicomFormat/DicomMap.cpp - Core/DicomFormat/DicomTag.cpp - Core/DicomFormat/DicomImageInformation.cpp - Core/DicomFormat/DicomIntegerPixelAccessor.cpp - Core/DicomFormat/DicomInstanceHasher.cpp - Core/DicomFormat/DicomValue.cpp - Core/Enumerations.cpp - Core/FileStorage/FilesystemStorage.cpp - Core/FileStorage/StorageAccessor.cpp - Core/HttpClient.cpp - Core/HttpServer/BufferHttpSender.cpp - Core/HttpServer/EmbeddedResourceHttpHandler.cpp - Core/HttpServer/FilesystemHttpHandler.cpp - Core/HttpServer/HttpToolbox.cpp - Core/HttpServer/HttpOutput.cpp - Core/HttpServer/StringHttpOutput.cpp - Core/HttpServer/MongooseServer.cpp - Core/HttpServer/HttpFileSender.cpp - Core/HttpServer/FilesystemHttpSender.cpp - Core/HttpServer/HttpStreamTranscoder.cpp - Core/Logging.cpp - Core/RestApi/RestApiCall.cpp - Core/RestApi/RestApiGetCall.cpp - Core/RestApi/RestApiHierarchy.cpp - Core/RestApi/RestApiPath.cpp - Core/RestApi/RestApiOutput.cpp - Core/RestApi/RestApi.cpp - Core/MultiThreading/Mutex.cpp - Core/MultiThreading/ReaderWriterLock.cpp - Core/MultiThreading/RunnableWorkersPool.cpp - Core/MultiThreading/Semaphore.cpp - Core/MultiThreading/SharedMessageQueue.cpp - Core/Images/Font.cpp - Core/Images/FontRegistry.cpp - Core/Images/ImageAccessor.cpp - Core/Images/ImageBuffer.cpp - Core/Images/ImageProcessing.cpp - Core/Images/JpegErrorManager.cpp - Core/Images/JpegReader.cpp - Core/Images/JpegWriter.cpp - Core/Images/PngReader.cpp - Core/Images/PngWriter.cpp - Core/SQLite/Connection.cpp - Core/SQLite/FunctionContext.cpp - Core/SQLite/Statement.cpp - Core/SQLite/StatementId.cpp - Core/SQLite/StatementReference.cpp - Core/SQLite/Transaction.cpp - Core/Toolbox.cpp - Core/Uuid.cpp - Core/Lua/LuaContext.cpp - Core/Lua/LuaFunctionCall.cpp - ) - - -set(ORTHANC_SERVER_SOURCES - OrthancServer/DatabaseWrapper.cpp - OrthancServer/DatabaseWrapperBase.cpp - OrthancServer/DicomDirWriter.cpp - OrthancServer/DicomModification.cpp - OrthancServer/DicomProtocol/DicomFindAnswers.cpp - OrthancServer/DicomProtocol/DicomServer.cpp - OrthancServer/DicomProtocol/DicomUserConnection.cpp - OrthancServer/DicomProtocol/RemoteModalityParameters.cpp - OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp - OrthancServer/ExportedResource.cpp - OrthancServer/FromDcmtkBridge.cpp - OrthancServer/Internals/CommandDispatcher.cpp - OrthancServer/Internals/DicomImageDecoder.cpp - OrthancServer/Internals/FindScp.cpp - OrthancServer/Internals/MoveScp.cpp - OrthancServer/Internals/StoreScp.cpp - OrthancServer/LuaScripting.cpp - OrthancServer/OrthancFindRequestHandler.cpp - OrthancServer/OrthancHttpHandler.cpp - OrthancServer/OrthancInitialization.cpp - OrthancServer/OrthancMoveRequestHandler.cpp - OrthancServer/OrthancPeerParameters.cpp - OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp - OrthancServer/OrthancRestApi/OrthancRestApi.cpp - OrthancServer/OrthancRestApi/OrthancRestArchive.cpp - OrthancServer/OrthancRestApi/OrthancRestChanges.cpp - OrthancServer/OrthancRestApi/OrthancRestModalities.cpp - OrthancServer/OrthancRestApi/OrthancRestResources.cpp - OrthancServer/OrthancRestApi/OrthancRestSystem.cpp - OrthancServer/ParsedDicomFile.cpp - OrthancServer/QueryRetrieveHandler.cpp - OrthancServer/Search/LookupIdentifierQuery.cpp - OrthancServer/Search/LookupResource.cpp - OrthancServer/Search/SetOfResources.cpp - OrthancServer/Search/ListConstraint.cpp - OrthancServer/Search/RangeConstraint.cpp - OrthancServer/Search/ValueConstraint.cpp - OrthancServer/Search/WildcardConstraint.cpp - OrthancServer/ServerContext.cpp - OrthancServer/ServerEnumerations.cpp - OrthancServer/ServerIndex.cpp - OrthancServer/ServerToolbox.cpp - OrthancServer/SliceOrdering.cpp - OrthancServer/ToDcmtkBridge.cpp - - # From "lua-scripting" branch - OrthancServer/DicomInstanceToStore.cpp - OrthancServer/Scheduler/DeleteInstanceCommand.cpp - OrthancServer/Scheduler/ModifyInstanceCommand.cpp - OrthancServer/Scheduler/ServerCommandInstance.cpp - OrthancServer/Scheduler/ServerJob.cpp - OrthancServer/Scheduler/ServerScheduler.cpp - OrthancServer/Scheduler/StorePeerCommand.cpp - OrthancServer/Scheduler/StoreScuCommand.cpp - OrthancServer/Scheduler/CallSystemCommand.cpp - ) - - -set(ORTHANC_UNIT_TESTS_SOURCES - UnitTestsSources/DicomMapTests.cpp - UnitTestsSources/FileStorageTests.cpp - UnitTestsSources/FromDcmtkTests.cpp - UnitTestsSources/MemoryCacheTests.cpp - UnitTestsSources/ImageTests.cpp - UnitTestsSources/RestApiTests.cpp - UnitTestsSources/SQLiteTests.cpp - UnitTestsSources/SQLiteChromiumTests.cpp - UnitTestsSources/ServerIndexTests.cpp - UnitTestsSources/VersionsTests.cpp - UnitTestsSources/ZipTests.cpp - UnitTestsSources/LuaTests.cpp - UnitTestsSources/MultiThreadingTests.cpp - UnitTestsSources/UnitTestsMain.cpp - UnitTestsSources/ImageProcessingTests.cpp - UnitTestsSources/JpegLosslessTests.cpp - UnitTestsSources/StreamTests.cpp - ) - - -if (ENABLE_PLUGINS) - list(APPEND ORTHANC_SERVER_SOURCES - Plugins/Engine/OrthancPluginDatabase.cpp - Plugins/Engine/OrthancPlugins.cpp - Plugins/Engine/PluginsEnumerations.cpp - Plugins/Engine/PluginsErrorDictionary.cpp - Plugins/Engine/PluginsManager.cpp - Plugins/Engine/SharedLibrary.cpp - ) - - list(APPEND ORTHANC_UNIT_TESTS_SOURCES - UnitTestsSources/PluginsTests.cpp - ) -endif() - - -set(ORTHANC_EMBEDDED_FILES - PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql - CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json - ) - - - -##################################################################### -## Inclusion of third-party dependencies -##################################################################### - -if (ENABLE_GOOGLE_LOG) - include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfiguration.cmake) -endif() - -include(${CMAKE_SOURCE_DIR}/Resources/CMake/JsonCppConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibCurlConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibPngConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibJpegConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/LuaConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/MongooseConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/PugixmlConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/SQLiteConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/ZlibConfiguration.cmake) - -# These are the two most heavyweight dependencies. We put them as the -# last includes to quickly spot problems when configuring static -# builds. -include(${CMAKE_SOURCE_DIR}/Resources/CMake/BoostConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/Resources/CMake/DcmtkConfiguration.cmake) - - -if (ENABLE_SSL) - add_definitions(-DORTHANC_SSL_ENABLED=1) - include(${CMAKE_SOURCE_DIR}/Resources/CMake/OpenSslConfiguration.cmake) -else() - add_definitions(-DORTHANC_SSL_ENABLED=0) -endif() - - -if (ENABLE_JPEG) - add_definitions(-DORTHANC_JPEG_ENABLED=1) -else() - add_definitions(-DORTHANC_JPEG_ENABLED=0) -endif() - - -if (ENABLE_JPEG_LOSSLESS) - add_definitions(-DORTHANC_JPEG_LOSSLESS_ENABLED=1) -else() - add_definitions(-DORTHANC_JPEG_LOSSLESS_ENABLED=0) -endif() - - -if (ENABLE_PLUGINS) - add_definitions(-DORTHANC_PLUGINS_ENABLED=1) -else() - add_definitions(-DORTHANC_PLUGINS_ENABLED=0) -endif() - - - -##################################################################### -## Autogeneration of files -##################################################################### - -if (STANDALONE_BUILD) - # We embed all the resources in the binaries for standalone builds - add_definitions(-DORTHANC_STANDALONE=1) - EmbedResources( - ${ORTHANC_EMBEDDED_FILES} - ORTHANC_EXPLORER ${CMAKE_CURRENT_SOURCE_DIR}/OrthancExplorer - ${DCMTK_DICTIONARIES} - ) -else() - add_definitions( - -DORTHANC_STANDALONE=0 - -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\" - ) - EmbedResources( - ${ORTHANC_EMBEDDED_FILES} - ) -endif() - -if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") - execute_process( - COMMAND - ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py - ${ORTHANC_VERSION} Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging" - ERROR_VARIABLE Failure - OUTPUT_FILE ${AUTOGENERATED_DIR}/Orthanc.rc - ) - - if (Failure) - message(FATAL_ERROR "Error while computing the version information: ${Failure}") - endif() - - list(APPEND ORTHANC_RESOURCES ${AUTOGENERATED_DIR}/Orthanc.rc) -endif() - - - -##################################################################### -## Build the core of Orthanc -##################################################################### - -# Setup precompiled headers for Microsoft Visual Studio -if (MSVC) - add_definitions(-DORTHANC_USE_PRECOMPILED_HEADERS=1) - - ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( - "PrecompiledHeaders.h" "Core/PrecompiledHeaders.cpp" ORTHANC_CORE_SOURCES) - - ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( - "PrecompiledHeadersServer.h" "OrthancServer/PrecompiledHeadersServer.cpp" ORTHANC_SERVER_SOURCES) - - ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( - "PrecompiledHeadersUnitTests.h" "UnitTestsSources/PrecompiledHeadersUnitTests.cpp" ORTHANC_UNIT_TESTS_SOURCES) -endif() - - -add_definitions( - -DORTHANC_VERSION="${ORTHANC_VERSION}" - -DORTHANC_DATABASE_VERSION=${ORTHANC_DATABASE_VERSION} - -DORTHANC_ENABLE_LOGGING=1 - ) - -list(LENGTH OPENSSL_SOURCES OPENSSL_SOURCES_LENGTH) -if (${OPENSSL_SOURCES_LENGTH} GREATER 0) - add_library(OpenSSL STATIC ${OPENSSL_SOURCES}) -endif() - -add_library(CoreLibrary - STATIC - ${ORTHANC_CORE_SOURCES} - ${AUTOGENERATED_SOURCES} - - ${BOOST_SOURCES} - ${CURL_SOURCES} - ${GOOGLE_LOG_SOURCES} - ${JSONCPP_SOURCES} - ${LIBPNG_SOURCES} - ${LIBJPEG_SOURCES} - ${LUA_SOURCES} - ${MONGOOSE_SOURCES} - ${PUGIXML_SOURCES} - ${SQLITE_SOURCES} - ${ZLIB_SOURCES} - - ${CMAKE_SOURCE_DIR}/Resources/ThirdParty/md5/md5.c - ${CMAKE_SOURCE_DIR}/Resources/ThirdParty/base64/base64.cpp - - # This is the minizip distribution to create ZIP files using zlib - ${ORTHANC_ROOT}/Resources/ThirdParty/minizip/ioapi.c - ${ORTHANC_ROOT}/Resources/ThirdParty/minizip/zip.c - ) - - - -##################################################################### -## Build the Orthanc server -##################################################################### - -add_library(ServerLibrary - STATIC - ${DCMTK_SOURCES} - ${ORTHANC_SERVER_SOURCES} - ) - -# Ensure autogenerated code is built before building ServerLibrary -add_dependencies(ServerLibrary CoreLibrary) - -add_executable(Orthanc - OrthancServer/main.cpp - ${ORTHANC_RESOURCES} - ) - -target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES}) - -if (${OPENSSL_SOURCES_LENGTH} GREATER 0) - target_link_libraries(Orthanc OpenSSL) -endif() - -install( - TARGETS Orthanc - RUNTIME DESTINATION sbin - ) - - - -##################################################################### -## Build the unit tests -##################################################################### - -if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) - add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) -else() - add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) -endif() - -add_definitions( - -DORTHANC_BUILD_UNIT_TESTS=1 - ) - -include(${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleTestConfiguration.cmake) -add_executable(UnitTests - ${GTEST_SOURCES} - ${ORTHANC_UNIT_TESTS_SOURCES} - ) -target_link_libraries(UnitTests ServerLibrary CoreLibrary ${DCMTK_LIBRARIES}) - -if (${OPENSSL_SOURCES_LENGTH} GREATER 0) - target_link_libraries(UnitTests OpenSSL) -endif() - - - -##################################################################### -## Build the "ServeFolders" plugin -##################################################################### - -if (ENABLE_PLUGINS AND BUILD_SERVE_FOLDERS) - execute_process( - COMMAND - ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py - ${ORTHANC_VERSION} ServeFolders ServeFolders.dll "Orthanc plugin to serve additional folders" - ERROR_VARIABLE Failure - OUTPUT_FILE ${AUTOGENERATED_DIR}/ServeFolders.rc - ) - - if (Failure) - message(FATAL_ERROR "Error while computing the version information: ${Failure}") - endif() - - add_definitions(-DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}") - - include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include) - - add_library(ServeFolders SHARED - ${BOOST_SOURCES} - ${JSONCPP_SOURCES} - Plugins/Samples/ServeFolders/Plugin.cpp - ${AUTOGENERATED_DIR}/ServeFolders.rc - ) - - set_target_properties( - ServeFolders PROPERTIES - VERSION ${ORTHANC_VERSION} - SOVERSION ${ORTHANC_VERSION} - ) - - install( - TARGETS ServeFolders - RUNTIME DESTINATION lib # Destination for Windows - LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux - ) -endif() - - - -##################################################################### -## Generate the documentation if Doxygen is present -##################################################################### - -find_package(Doxygen) -if (DOXYGEN_FOUND) - configure_file( - ${CMAKE_SOURCE_DIR}/Resources/Orthanc.doxygen - ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen - @ONLY) - - configure_file( - ${CMAKE_SOURCE_DIR}/Resources/OrthancPlugin.doxygen - ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen - @ONLY) - - add_custom_target(doc - ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen - COMMENT "Generating internal documentation with Doxygen" VERBATIM - ) - - add_custom_command(TARGET Orthanc - POST_BUILD - COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Generating plugin documentation with Doxygen" VERBATIM - ) - - install( - DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/OrthancPluginDocumentation/doc/ - DESTINATION share/doc/orthanc/OrthancPlugin - ) -else() - message("Doxygen not found. The documentation will not be built.") -endif() - - - -##################################################################### -## Install the plugin SDK -##################################################################### - -if (ENABLE_PLUGINS) - install( - FILES - Plugins/Include/orthanc/OrthancCPlugin.h - Plugins/Include/orthanc/OrthancCDatabasePlugin.h - Plugins/Include/orthanc/OrthancCppDatabasePlugin.h - DESTINATION include/orthanc - ) -endif() - - - -##################################################################### -## Prepare the "uninstall" target -## http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F -##################################################################### - -configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/Resources/CMake/Uninstall.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" - IMMEDIATE @ONLY) - -add_custom_target(uninstall - COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) +cmake_minimum_required(VERSION 2.8) + +project(Orthanc) + + +##################################################################### +## Generic parameters of the Orthanc framework +##################################################################### + +include(${CMAKE_SOURCE_DIR}/Resources/CMake/OrthancFrameworkParameters.cmake) + +# Enable all the optional components of the Orthanc framework +set(ENABLE_CRYPTO_OPTIONS ON) +set(ENABLE_DCMTK ON) +set(ENABLE_DCMTK_NETWORKING ON) +set(ENABLE_GOOGLE_TEST ON) +set(ENABLE_JPEG ON) +set(ENABLE_LOCALE ON) +set(ENABLE_LUA ON) +set(ENABLE_PNG ON) +set(ENABLE_PUGIXML ON) +set(ENABLE_SQLITE ON) +set(ENABLE_WEB_CLIENT ON) +set(ENABLE_WEB_SERVER ON) +set(ENABLE_ZLIB ON) + +set(HAS_EMBEDDED_RESOURCES ON) + + +##################################################################### +## CMake parameters tunable at the command line to configure the +## plugins, the companion tools, and the unit tests +##################################################################### + +# Parameters of the build +SET(BUILD_MODALITY_WORKLISTS ON CACHE BOOL "Whether to build the sample plugin to serve modality worklists") +SET(BUILD_RECOVER_COMPRESSED_FILE ON CACHE BOOL "Whether to build the companion tool to recover files compressed using Orthanc") +SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Whether to build the ServeFolders plugin") +SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") +SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") + + +##################################################################### +## Configuration of the Orthanc framework +##################################################################### + +include(${CMAKE_SOURCE_DIR}/Resources/CMake/VisualStudioPrecompiledHeaders.cmake) +include(${CMAKE_SOURCE_DIR}/Resources/CMake/OrthancFrameworkConfiguration.cmake) + + +##################################################################### +## List of source files +##################################################################### + +set(ORTHANC_SERVER_SOURCES + OrthancServer/DatabaseWrapper.cpp + OrthancServer/DicomInstanceOrigin.cpp + OrthancServer/DicomInstanceToStore.cpp + OrthancServer/ExportedResource.cpp + OrthancServer/LuaScripting.cpp + OrthancServer/OrthancFindRequestHandler.cpp + OrthancServer/OrthancHttpHandler.cpp + OrthancServer/OrthancInitialization.cpp + OrthancServer/OrthancMoveRequestHandler.cpp + OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp + OrthancServer/OrthancRestApi/OrthancRestApi.cpp + OrthancServer/OrthancRestApi/OrthancRestArchive.cpp + OrthancServer/OrthancRestApi/OrthancRestChanges.cpp + OrthancServer/OrthancRestApi/OrthancRestModalities.cpp + OrthancServer/OrthancRestApi/OrthancRestResources.cpp + OrthancServer/OrthancRestApi/OrthancRestSystem.cpp + OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/HierarchicalMatcher.cpp + OrthancServer/Search/IFindConstraint.cpp + OrthancServer/Search/ListConstraint.cpp + OrthancServer/Search/LookupIdentifierQuery.cpp + OrthancServer/Search/LookupResource.cpp + OrthancServer/Search/RangeConstraint.cpp + OrthancServer/Search/SetOfResources.cpp + OrthancServer/Search/ValueConstraint.cpp + OrthancServer/Search/WildcardConstraint.cpp + OrthancServer/ServerContext.cpp + OrthancServer/ServerEnumerations.cpp + OrthancServer/ServerIndex.cpp + OrthancServer/ServerJobs/ArchiveJob.cpp + OrthancServer/ServerJobs/DicomModalityStoreJob.cpp + OrthancServer/ServerJobs/DicomMoveScuJob.cpp + OrthancServer/ServerJobs/LuaJobManager.cpp + OrthancServer/ServerJobs/MergeStudyJob.cpp + OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp + OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp + OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp + OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp + OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp + OrthancServer/ServerJobs/OrthancJobUnserializer.cpp + OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp + OrthancServer/ServerJobs/ResourceModificationJob.cpp + OrthancServer/ServerJobs/SplitStudyJob.cpp + OrthancServer/ServerToolbox.cpp + OrthancServer/SliceOrdering.cpp + ) + + +set(ORTHANC_UNIT_TESTS_SOURCES + UnitTestsSources/DicomMapTests.cpp + UnitTestsSources/FileStorageTests.cpp + UnitTestsSources/FromDcmtkTests.cpp + UnitTestsSources/MemoryCacheTests.cpp + UnitTestsSources/ImageTests.cpp + UnitTestsSources/RestApiTests.cpp + UnitTestsSources/SQLiteTests.cpp + UnitTestsSources/SQLiteChromiumTests.cpp + UnitTestsSources/ServerIndexTests.cpp + UnitTestsSources/VersionsTests.cpp + UnitTestsSources/ZipTests.cpp + UnitTestsSources/LuaTests.cpp + UnitTestsSources/MultiThreadingTests.cpp + UnitTestsSources/UnitTestsMain.cpp + UnitTestsSources/ImageProcessingTests.cpp + UnitTestsSources/JpegLosslessTests.cpp + UnitTestsSources/StreamTests.cpp + ) + + +if (ENABLE_PLUGINS) + list(APPEND ORTHANC_SERVER_SOURCES + Plugins/Engine/OrthancPluginDatabase.cpp + Plugins/Engine/OrthancPlugins.cpp + Plugins/Engine/PluginsEnumerations.cpp + Plugins/Engine/PluginsErrorDictionary.cpp + Plugins/Engine/PluginsJob.cpp + Plugins/Engine/PluginsManager.cpp + ) + + list(APPEND ORTHANC_UNIT_TESTS_SOURCES + UnitTestsSources/PluginsTests.cpp + ) +endif() + + +if (CMAKE_COMPILER_IS_GNUCXX + AND NOT CMAKE_CROSSCOMPILING + AND USE_DCMTK_360) + # Add the "-pedantic" flag only on the Orthanc sources, and only if + # cross-compiling DCMTK 3.6.0 + set(ORTHANC_ALL_SOURCES + ${ORTHANC_CORE_SOURCES_INTERNAL} + ${ORTHANC_DICOM_SOURCES_INTERNAL} + ${ORTHANC_SERVER_SOURCES} + ${ORTHANC_UNIT_TESTS_SOURCES} + Plugins/Samples/ServeFolders/Plugin.cpp + Plugins/Samples/ModalityWorklists/Plugin.cpp + OrthancServer/main.cpp + ) + + set_source_files_properties(${ORTHANC_ALL_SOURCES} + PROPERTIES COMPILE_FLAGS -pedantic + ) +endif() + + +##################################################################### +## Autogeneration of files +##################################################################### + +set(ORTHANC_EMBEDDED_FILES + PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql + CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + ) + +if (STANDALONE_BUILD) + # We embed all the resources in the binaries for standalone builds + add_definitions(-DORTHANC_STANDALONE=1) + EmbedResources( + ${ORTHANC_EMBEDDED_FILES} + ORTHANC_EXPLORER ${CMAKE_CURRENT_SOURCE_DIR}/OrthancExplorer + ${DCMTK_DICTIONARIES} + ) +else() + add_definitions( + -DORTHANC_STANDALONE=0 + -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\" + ) + EmbedResources( + ${ORTHANC_EMBEDDED_FILES} + ) +endif() + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py + ${ORTHANC_VERSION} Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/Orthanc.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND ORTHANC_RESOURCES ${AUTOGENERATED_DIR}/Orthanc.rc) +endif() + + + +##################################################################### +## Configuration of the C/C++ macros +##################################################################### + +if (ENABLE_PLUGINS) + add_definitions(-DORTHANC_ENABLE_PLUGINS=1) +else() + add_definitions(-DORTHANC_ENABLE_PLUGINS=0) +endif() + + +if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) +else() + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) +endif() + + +include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include) + +add_definitions( + -DORTHANC_BUILD_UNIT_TESTS=1 + -DORTHANC_ENABLE_LOGGING_PLUGIN=0 + + # Macros for the plugins + -DHAS_ORTHANC_EXCEPTION=0 + -DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}" + -DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}" + ) + + +# Setup precompiled headers for Microsoft Visual Studio + +# WARNING: There must be NO MORE "add_definitions()", "include()" or +# "include_directories()" below, otherwise the generated precompiled +# headers might get broken! + +if (MSVC) + add_definitions(-DORTHANC_USE_PRECOMPILED_HEADERS=1) + + set(TMP + ${ORTHANC_CORE_SOURCES_INTERNAL} + ${ORTHANC_DICOM_SOURCES_INTERNAL} + ) + + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeaders.h" "Core/PrecompiledHeaders.cpp" + TMP ORTHANC_CORE_PCH) + + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeadersServer.h" "OrthancServer/PrecompiledHeadersServer.cpp" + ORTHANC_SERVER_SOURCES ORTHANC_SERVER_PCH) + + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeadersUnitTests.h" "UnitTestsSources/PrecompiledHeadersUnitTests.cpp" + ORTHANC_UNIT_TESTS_SOURCES ORTHANC_UNIT_TESTS_PCH) +endif() + + + +##################################################################### +## Build the core of Orthanc +##################################################################### + +# "CoreLibrary" contains all the third-party dependencies and the +# content of the "Core" folder +add_library(CoreLibrary + STATIC + ${ORTHANC_CORE_PCH} + ${ORTHANC_CORE_SOURCES} + ${ORTHANC_DICOM_SOURCES} + ${AUTOGENERATED_SOURCES} + ) + + +##################################################################### +## Build the Orthanc server +##################################################################### + +add_library(ServerLibrary + STATIC + ${ORTHANC_SERVER_PCH} + ${ORTHANC_SERVER_SOURCES} + ) + +# Ensure autogenerated code is built before building ServerLibrary +add_dependencies(ServerLibrary CoreLibrary) + +add_executable(Orthanc + OrthancServer/main.cpp + ${ORTHANC_RESOURCES} + ) + +target_link_libraries(Orthanc ServerLibrary CoreLibrary ${DCMTK_LIBRARIES}) + +install( + TARGETS Orthanc + RUNTIME DESTINATION sbin + ) + + +##################################################################### +## Build the unit tests +##################################################################### + +add_executable(UnitTests + ${GOOGLE_TEST_SOURCES} + ${ORTHANC_UNIT_TESTS_PCH} + ${ORTHANC_UNIT_TESTS_SOURCES} + ) + +target_link_libraries(UnitTests + ServerLibrary + CoreLibrary + ${DCMTK_LIBRARIES} + ${GOOGLE_TEST_LIBRARIES} + ) + + +##################################################################### +## Build the "ServeFolders" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_SERVE_FOLDERS) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py + ${ORTHANC_VERSION} ServeFolders ServeFolders.dll "Orthanc plugin to serve additional folders" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/ServeFolders.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND SERVE_FOLDERS_RESOURCES ${AUTOGENERATED_DIR}/ServeFolders.rc) + endif() + + add_library(ServeFolders SHARED + ${BOOST_SOURCES} + ${JSONCPP_SOURCES} + ${LIBICONV_SOURCES} + Plugins/Samples/ServeFolders/Plugin.cpp + Plugins/Samples/Common/OrthancPluginCppWrapper.cpp + ${SERVE_FOLDERS_RESOURCES} + ) + + set_target_properties( + ServeFolders PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS ServeFolders + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + + +##################################################################### +## Build the "ModalityWorklists" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_MODALITY_WORKLISTS) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py + ${ORTHANC_VERSION} ModalityWorklists ModalityWorklists.dll "Sample Orthanc plugin to serve modality worklists" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/ModalityWorklists.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND MODALITY_WORKLISTS_RESOURCES ${AUTOGENERATED_DIR}/ModalityWorklists.rc) + endif() + + add_library(ModalityWorklists SHARED + ${BOOST_SOURCES} + ${JSONCPP_SOURCES} + ${LIBICONV_SOURCES} + Plugins/Samples/Common/OrthancPluginCppWrapper.cpp + Plugins/Samples/ModalityWorklists/Plugin.cpp + ${MODALITY_WORKLISTS_RESOURCES} + ) + + set_target_properties( + ModalityWorklists PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS ModalityWorklists + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + + +##################################################################### +## Build the companion tool to recover files compressed using Orthanc +##################################################################### + +if (BUILD_RECOVER_COMPRESSED_FILE) + set(RECOVER_COMPRESSED_SOURCES + Resources/Samples/Tools/RecoverCompressedFile.cpp + ) + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py + ${ORTHANC_VERSION} OrthancRecoverCompressedFile OrthancRecoverCompressedFile.exe + "Lightweight, RESTful DICOM server for medical imaging" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/OrthancRecoverCompressedFile.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND RECOVER_COMPRESSED_SOURCES + ${AUTOGENERATED_DIR}/OrthancRecoverCompressedFile.rc + ) + endif() + + add_executable(OrthancRecoverCompressedFile ${RECOVER_COMPRESSED_SOURCES}) + + target_link_libraries(OrthancRecoverCompressedFile CoreLibrary) + + install( + TARGETS OrthancRecoverCompressedFile + RUNTIME DESTINATION bin + ) +endif() + + + +##################################################################### +## Generate the documentation if Doxygen is present +##################################################################### + +find_package(Doxygen) +if (DOXYGEN_FOUND) + configure_file( + ${CMAKE_SOURCE_DIR}/Resources/Orthanc.doxygen + ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen + @ONLY) + + configure_file( + ${CMAKE_SOURCE_DIR}/Resources/OrthancPlugin.doxygen + ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen + @ONLY) + + add_custom_target(doc + ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen + COMMENT "Generating internal documentation with Doxygen" VERBATIM + ) + + add_custom_command(TARGET Orthanc + POST_BUILD + COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating plugin documentation with Doxygen" VERBATIM + ) + + install( + DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/OrthancPluginDocumentation/doc/ + DESTINATION share/doc/orthanc/OrthancPlugin + ) +else() + message("Doxygen not found. The documentation will not be built.") +endif() + + + +##################################################################### +## Install the plugin SDK +##################################################################### + +if (ENABLE_PLUGINS) + install( + FILES + Plugins/Include/orthanc/OrthancCPlugin.h + Plugins/Include/orthanc/OrthancCDatabasePlugin.h + DESTINATION include/orthanc + ) +endif() + + + +##################################################################### +## Prepare the "uninstall" target +## http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F +##################################################################### + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/Resources/CMake/Uninstall.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" + IMMEDIATE @ONLY) + +add_custom_target(uninstall + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) diff -r 2b91363cc1d1 -r 497a637366b4 Core/Cache/ICachePageProvider.h --- a/Core/Cache/ICachePageProvider.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Cache/ICachePageProvider.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Cache/LeastRecentlyUsedIndex.h --- a/Core/Cache/LeastRecentlyUsedIndex.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Cache/LeastRecentlyUsedIndex.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Cache/MemoryCache.cpp --- a/Core/Cache/MemoryCache.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Cache/MemoryCache.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -77,6 +78,18 @@ { } + void MemoryCache::Invalidate(const std::string& id) + { + Page* p = NULL; + if (index_.Contains(id, p)) + { + VLOG(1) << "Invalidating a cache page"; + assert(p != NULL); + delete p; + index_.Invalidate(id); + } + } + MemoryCache::~MemoryCache() { while (!index_.IsEmpty()) diff -r 2b91363cc1d1 -r 497a637366b4 Core/Cache/MemoryCache.h --- a/Core/Cache/MemoryCache.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Cache/MemoryCache.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -63,5 +64,7 @@ ~MemoryCache(); IDynamicObject& Access(const std::string& id); + + void Invalidate(const std::string& id); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Cache/SharedArchive.cpp --- a/Core/Cache/SharedArchive.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Cache/SharedArchive.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -33,8 +34,7 @@ #include "../PrecompiledHeaders.h" #include "SharedArchive.h" - -#include "../Uuid.h" +#include "../Toolbox.h" namespace Orthanc diff -r 2b91363cc1d1 -r 497a637366b4 Core/Cache/SharedArchive.h --- a/Core/Cache/SharedArchive.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Cache/SharedArchive.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,6 +33,14 @@ #pragma once +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class SharedArchive cannot be used in sandboxed environments +#endif + #include "LeastRecentlyUsedIndex.h" #include "../IDynamicObject.h" diff -r 2b91363cc1d1 -r 497a637366b4 Core/ChunkedBuffer.cpp --- a/Core/ChunkedBuffer.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/ChunkedBuffer.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -51,17 +52,19 @@ } - void ChunkedBuffer::AddChunk(const char* chunkData, + void ChunkedBuffer::AddChunk(const void* chunkData, size_t chunkSize) { if (chunkSize == 0) { return; } - - assert(chunkData != NULL); - chunks_.push_back(new std::string(chunkData, chunkSize)); - numBytes_ += chunkSize; + else + { + assert(chunkData != NULL); + chunks_.push_back(new std::string(reinterpret_cast(chunkData), chunkSize)); + numBytes_ += chunkSize; + } } @@ -95,5 +98,6 @@ } chunks_.clear(); + numBytes_ = 0; } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/ChunkedBuffer.h --- a/Core/ChunkedBuffer.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/ChunkedBuffer.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -61,7 +62,7 @@ return numBytes_; } - void AddChunk(const char* chunkData, + void AddChunk(const void* chunkData, size_t chunkSize); void AddChunk(const std::string& chunk); diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/DeflateBaseCompressor.cpp --- a/Core/Compression/DeflateBaseCompressor.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/DeflateBaseCompressor.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/DeflateBaseCompressor.h --- a/Core/Compression/DeflateBaseCompressor.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/DeflateBaseCompressor.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -34,6 +35,15 @@ #include "IBufferCompressor.h" +#if !defined(ORTHANC_ENABLE_ZLIB) +# error The macro ORTHANC_ENABLE_ZLIB must be defined +#endif + +#if ORTHANC_ENABLE_ZLIB != 1 +# error ZLIB support must be enabled to include this file +#endif + + #include namespace Orthanc diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/GzipCompressor.cpp --- a/Core/Compression/GzipCompressor.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/GzipCompressor.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/GzipCompressor.h --- a/Core/Compression/GzipCompressor.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/GzipCompressor.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/HierarchicalZipWriter.cpp --- a/Core/Compression/HierarchicalZipWriter.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/HierarchicalZipWriter.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/HierarchicalZipWriter.h --- a/Core/Compression/HierarchicalZipWriter.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/HierarchicalZipWriter.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -38,6 +39,10 @@ #include #include +#if ORTHANC_BUILD_UNIT_TESTS == 1 +# include +#endif + namespace Orthanc { class HierarchicalZipWriter diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/IBufferCompressor.h --- a/Core/Compression/IBufferCompressor.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/IBufferCompressor.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/ZipWriter.cpp --- a/Core/Compression/ZipWriter.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/ZipWriter.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/ZipWriter.h --- a/Core/Compression/ZipWriter.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/ZipWriter.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,14 +33,19 @@ #pragma once +#if !defined(ORTHANC_ENABLE_ZLIB) +# error The macro ORTHANC_ENABLE_ZLIB must be defined +#endif + +#if ORTHANC_ENABLE_ZLIB != 1 +# error ZLIB support must be enabled to include this file +#endif + + #include #include #include -#if ORTHANC_BUILD_UNIT_TESTS == 1 -#include -#endif - namespace Orthanc { class ZipWriter diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/ZlibCompressor.cpp --- a/Core/Compression/ZlibCompressor.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/ZlibCompressor.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Compression/ZlibCompressor.h --- a/Core/Compression/ZlibCompressor.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Compression/ZlibCompressor.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomArray.cpp --- a/Core/DicomFormat/DicomArray.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomArray.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomArray.h --- a/Core/DicomFormat/DicomArray.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomArray.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -47,7 +48,7 @@ Elements elements_; public: - DicomArray(const DicomMap& map); + explicit DicomArray(const DicomMap& map); ~DicomArray(); diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomElement.h --- a/Core/DicomFormat/DicomElement.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomElement.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomImageInformation.cpp --- a/Core/DicomFormat/DicomImageInformation.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomImageInformation.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -44,6 +45,7 @@ #include #include #include +#include namespace Orthanc { @@ -157,7 +159,7 @@ if (samplesPerPixel_ > 1) { // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1 - // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.3/ + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3 try { planarConfiguration = boost::lexical_cast(values.GetValue(DICOM_TAG_PLANAR_CONFIGURATION).GetContent()); @@ -177,18 +179,20 @@ throw OrthancException(ErrorCode_NotImplemented); } - try - { - numberOfFrames_ = boost::lexical_cast(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent()); - } - catch (OrthancException&) + if (values.HasTag(DICOM_TAG_NUMBER_OF_FRAMES)) { - // If the tag "NumberOfFrames" is absent, assume there is a single frame - numberOfFrames_ = 1; + try + { + numberOfFrames_ = boost::lexical_cast(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent()); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_NotImplemented); + } } - catch (boost::bad_lexical_cast&) + else { - throw OrthancException(ErrorCode_NotImplemented); + numberOfFrames_ = 1; } if ((bitsAllocated_ != 8 && bitsAllocated_ != 16 && @@ -199,13 +203,6 @@ throw OrthancException(ErrorCode_NotImplemented); } - if (bitsAllocated_ > 32 || - bitsStored_ >= 32) - { - // Not available, as the accessor internally uses int32_t values - throw OrthancException(ErrorCode_NotImplemented); - } - if (samplesPerPixel_ == 0) { throw OrthancException(ErrorCode_NotImplemented); @@ -217,10 +214,44 @@ isSigned_ = (pixelRepresentation != 0 ? true : false); } + DicomImageInformation* DicomImageInformation::Clone() const + { + std::auto_ptr target(new DicomImageInformation); + target->width_ = width_; + target->height_ = height_; + target->samplesPerPixel_ = samplesPerPixel_; + target->numberOfFrames_ = numberOfFrames_; + target->isPlanar_ = isPlanar_; + target->isSigned_ = isSigned_; + target->bytesPerValue_ = bytesPerValue_; + target->bitsAllocated_ = bitsAllocated_; + target->bitsStored_ = bitsStored_; + target->highBit_ = highBit_; + target->photometric_ = photometric_; - bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format) const + return target.release(); + } + + bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format, + bool ignorePhotometricInterpretation) const { - if (photometric_ == PhotometricInterpretation_Monochrome1 || + if (photometric_ == PhotometricInterpretation_Palette) + { + if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned()) + { + format = PixelFormat_RGB24; + return true; + } + + if (GetBitsStored() == 16 && GetChannelCount() == 1 && !IsSigned()) + { + format = PixelFormat_RGB48; + return true; + } + } + + if (ignorePhotometricInterpretation || + photometric_ == PhotometricInterpretation_Monochrome1 || photometric_ == PhotometricInterpretation_Monochrome2) { if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned()) @@ -240,12 +271,18 @@ format = PixelFormat_SignedGrayscale16; return true; } + + if (GetBitsAllocated() == 32 && GetChannelCount() == 1 && !IsSigned()) + { + format = PixelFormat_Grayscale32; + return true; + } } if (GetBitsStored() == 8 && GetChannelCount() == 3 && !IsSigned() && - photometric_ == PhotometricInterpretation_RGB) + (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB)) { format = PixelFormat_RGB24; return true; @@ -253,4 +290,13 @@ return false; } + + + size_t DicomImageInformation::GetFrameSize() const + { + return (GetHeight() * + GetWidth() * + GetBytesPerValue() * + GetChannelCount()); + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomImageInformation.h --- a/Core/DicomFormat/DicomImageInformation.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomImageInformation.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -56,8 +57,15 @@ PhotometricInterpretation photometric_; + protected: + explicit DicomImageInformation() + { + } + public: - DicomImageInformation(const DicomMap& values); + explicit DicomImageInformation(const DicomMap& values); + + DicomImageInformation* Clone() const; unsigned int GetWidth() const { @@ -119,6 +127,9 @@ return photometric_; } - bool ExtractPixelFormat(PixelFormat& format) const; + bool ExtractPixelFormat(PixelFormat& format, + bool ignorePhotometricInterpretation) const; + + size_t GetFrameSize() const; }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomInstanceHasher.cpp --- a/Core/DicomFormat/DicomInstanceHasher.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomInstanceHasher.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomInstanceHasher.h --- a/Core/DicomFormat/DicomInstanceHasher.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomInstanceHasher.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomIntegerPixelAccessor.cpp --- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -53,9 +54,15 @@ pixelData_(pixelData), size_(size) { + if (information_.GetBitsAllocated() > 32 || + information_.GetBitsStored() >= 32) + { + // Not available, as the accessor internally uses int32_t values + throw OrthancException(ErrorCode_NotImplemented); + } + frame_ = 0; - frameOffset_ = (information_.GetHeight() * information_.GetWidth() * - information_.GetBytesPerValue() * information_.GetChannelCount()); + frameOffset_ = information_.GetFrameSize(); if (information_.GetNumberOfFrames() * frameOffset_ > size) { @@ -137,7 +144,7 @@ const uint8_t* pixel = reinterpret_cast(pixelData_) + y * rowOffset_ + frame_ * frameOffset_; - // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.3/ + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3 if (information_.IsPlanar()) { /** diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomIntegerPixelAccessor.h --- a/Core/DicomFormat/DicomIntegerPixelAccessor.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomIntegerPixelAccessor.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomMap.cpp --- a/Core/DicomFormat/DicomMap.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomMap.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -35,7 +36,9 @@ #include #include -#include "DicomArray.h" + +#include "../Endianness.h" +#include "../Logging.h" #include "../OrthancException.h" @@ -61,7 +64,11 @@ DicomTag(0x0020, 0x0010), // StudyID DICOM_TAG_STUDY_DESCRIPTION, DICOM_TAG_ACCESSION_NUMBER, - DICOM_TAG_STUDY_INSTANCE_UID + DICOM_TAG_STUDY_INSTANCE_UID, + DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION, // New in db v6 + DICOM_TAG_INSTITUTION_NAME, // New in db v6 + DICOM_TAG_REQUESTING_PHYSICIAN, // New in db v6 + DICOM_TAG_REFERRING_PHYSICIAN_NAME // New in db v6 }; static DicomTag seriesTags[] = @@ -83,7 +90,12 @@ DICOM_TAG_NUMBER_OF_SLICES, DICOM_TAG_NUMBER_OF_TIME_SLICES, DICOM_TAG_SERIES_INSTANCE_UID, - DICOM_TAG_IMAGE_ORIENTATION_PATIENT // New in db v6 + DICOM_TAG_IMAGE_ORIENTATION_PATIENT, // New in db v6 + DICOM_TAG_SERIES_TYPE, // New in db v6 + DICOM_TAG_OPERATOR_NAME, // New in db v6 + DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, // New in db v6 + DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION, // New in db v6 + DICOM_TAG_CONTRAST_BOLUS_AGENT // New in db v6 }; static DicomTag instanceTags[] = @@ -96,7 +108,17 @@ DICOM_TAG_NUMBER_OF_FRAMES, DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, DICOM_TAG_SOP_INSTANCE_UID, - DICOM_TAG_IMAGE_POSITION_PATIENT // New in db v6 + DICOM_TAG_IMAGE_POSITION_PATIENT, // New in db v6 + DICOM_TAG_IMAGE_COMMENTS, // New in db v6 + + /** + * Main DICOM tags that are not part of any release of the + * database schema yet, and that will be part of future db v7. In + * the meantime, the user must call "/tools/reconstruct" once to + * access these tags if the corresponding DICOM files where + * indexed in the database by an older version of Orthanc. + **/ + DICOM_TAG_IMAGE_ORIENTATION_PATIENT // New in Orthanc 1.4.2 }; @@ -157,12 +179,11 @@ } - - void DicomMap::Clear() { for (Map::iterator it = map_.begin(); it != map_.end(); ++it) { + assert(it->second != NULL); delete it->second; } @@ -282,7 +303,7 @@ for (size_t i = 0; i < count; i++) { - result.SetValue(tags[i], ""); + result.SetValue(tags[i], "", false); } } @@ -294,16 +315,22 @@ void DicomMap::SetupFindStudyTemplate(DicomMap& result) { SetupFindTemplate(result, studyTags, sizeof(studyTags) / sizeof(DicomTag)); - result.SetValue(DICOM_TAG_ACCESSION_NUMBER, ""); - result.SetValue(DICOM_TAG_PATIENT_ID, ""); + result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false); + result.SetValue(DICOM_TAG_PATIENT_ID, "", false); + + // These main DICOM tags are only indirectly related to the + // General Study Module, remove them + result.Remove(DICOM_TAG_INSTITUTION_NAME); + result.Remove(DICOM_TAG_REQUESTING_PHYSICIAN); + result.Remove(DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION); } void DicomMap::SetupFindSeriesTemplate(DicomMap& result) { SetupFindTemplate(result, seriesTags, sizeof(seriesTags) / sizeof(DicomTag)); - result.SetValue(DICOM_TAG_ACCESSION_NUMBER, ""); - result.SetValue(DICOM_TAG_PATIENT_ID, ""); - result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, ""); + result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false); + result.SetValue(DICOM_TAG_PATIENT_ID, "", false); + result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false); // These tags are considered as "main" by Orthanc, but are not in the Series module result.Remove(DicomTag(0x0008, 0x0070)); // Manufacturer @@ -315,15 +342,18 @@ result.Remove(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS); result.Remove(DICOM_TAG_NUMBER_OF_TIME_SLICES); result.Remove(DICOM_TAG_IMAGE_ORIENTATION_PATIENT); + result.Remove(DICOM_TAG_SERIES_TYPE); + result.Remove(DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION); + result.Remove(DICOM_TAG_CONTRAST_BOLUS_AGENT); } void DicomMap::SetupFindInstanceTemplate(DicomMap& result) { SetupFindTemplate(result, instanceTags, sizeof(instanceTags) / sizeof(DicomTag)); - result.SetValue(DICOM_TAG_ACCESSION_NUMBER, ""); - result.SetValue(DICOM_TAG_PATIENT_ID, ""); - result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, ""); - result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, ""); + result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false); + result.SetValue(DICOM_TAG_PATIENT_ID, "", false); + result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false); + result.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "", false); } @@ -443,13 +473,6 @@ } - void DicomMap::Print(FILE* fp) const - { - DicomArray a(*this); - a.Print(fp); - } - - void DicomMap::GetTags(std::set& tags) const { tags.clear(); @@ -460,4 +483,548 @@ tags.insert(it->first); } } + + + static uint16_t ReadUnsignedInteger16(const char* dicom) + { + return le16toh(*reinterpret_cast(dicom)); + } + + + static uint32_t ReadUnsignedInteger32(const char* dicom) + { + return le32toh(*reinterpret_cast(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(1) << 32) - 8; + + case ValueRepresentation_OtherFloat: + return value.size() <= (static_cast(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(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(1) << 32) - 2; + + case ValueRepresentation_UnsignedShort: + return value.size() == 2; + + case ValueRepresentation_UnlimitedText: + return value.size() <= (static_cast(1) << 32) - 2; + + default: + // Assume unsupported tags are OK + return true; + } + } + + + static void RemoveTagPadding(std::string& value, + const ValueRepresentation& vr) + { + /** + * Remove padding from character strings, if need be. For the time + * being, only the UI VR is supported. + * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html + **/ + + switch (vr) + { + case ValueRepresentation_UniqueIdentifier: + { + /** + * "Values with a VR of UI shall be padded with a single + * trailing NULL (00H) character when necessary to achieve even + * length." + **/ + + if (!value.empty() && + value[value.size() - 1] == '\0') + { + value.resize(value.size() - 1); + } + + break; + } + + /** + * TODO implement other VR + **/ + + default: + // No padding is applicable to this VR + break; + } + } + + + static bool ReadNextTag(DicomTag& tag, + ValueRepresentation& vr, + std::string& value, + const char* dicom, + size_t size, + size_t& position) + { + /** + * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_7.html#sect_7.1.2 + * This function reads a data element with Explicit VR encoded using Little-Endian. + **/ + + if (position + 6 > size) + { + return false; + } + + tag = DicomTag(ReadUnsignedInteger16(dicom + position), + ReadUnsignedInteger16(dicom + position + 2)); + + vr = StringToValueRepresentation(std::string(dicom + position + 4, 2), true); + if (vr == ValueRepresentation_NotSupported) + { + return false; + } + + if (vr == ValueRepresentation_OtherByte || + vr == ValueRepresentation_OtherDouble || + vr == ValueRepresentation_OtherFloat || + vr == ValueRepresentation_OtherLong || + vr == ValueRepresentation_OtherWord || + vr == ValueRepresentation_Sequence || + vr == ValueRepresentation_UnlimitedCharacters || + vr == ValueRepresentation_UniversalResource || + vr == ValueRepresentation_UnlimitedText || + vr == ValueRepresentation_Unknown) // Note that "UN" should never appear in the Meta Information + { + if (position + 12 > size) + { + return false; + } + + uint32_t length = ReadUnsignedInteger32(dicom + position + 8); + + if (position + 12 + length > size) + { + return false; + } + + value.assign(dicom + position + 12, length); + position += (12 + length); + } + else + { + if (position + 8 > size) + { + return false; + } + + uint16_t length = ReadUnsignedInteger16(dicom + position + 6); + + if (position + 8 + length > size) + { + return false; + } + + value.assign(dicom + position + 8, length); + position += (8 + length); + } + + if (!ValidateTag(vr, value)) + { + return false; + } + + RemoveTagPadding(value, vr); + + return true; + } + + + bool DicomMap::ParseDicomMetaInformation(DicomMap& result, + const char* dicom, + size_t size) + { + /** + * http://dicom.nema.org/medical/dicom/current/output/chtml/part10/chapter_7.html + * According to Table 7.1-1, besides the "DICM" DICOM prefix, the + * file preamble (i.e. dicom[0..127]) should not be taken into + * account to determine whether the file is or is not a DICOM file. + **/ + + if (size < 132 || + dicom[128] != 'D' || + dicom[129] != 'I' || + dicom[130] != 'C' || + dicom[131] != 'M') + { + return false; + } + + + /** + * The DICOM File Meta Information must be encoded using the + * Explicit VR Little Endian Transfer Syntax + * (UID=1.2.840.10008.1.2.1). + **/ + + result.Clear(); + + // First, we read the "File Meta Information Group Length" tag + // (0002,0000) to know where to stop reading the meta header + size_t position = 132; + + DicomTag tag(0x0000, 0x0000); // Dummy initialization + ValueRepresentation vr; + std::string value; + if (!ReadNextTag(tag, vr, value, dicom, size, position) || + tag.GetGroup() != 0x0002 || + tag.GetElement() != 0x0000 || + vr != ValueRepresentation_UnsignedLong || + value.size() != 4) + { + return false; + } + + size_t stopPosition = position + ReadUnsignedInteger32(value.c_str()); + if (stopPosition > size) + { + return false; + } + + while (position < stopPosition) + { + if (ReadNextTag(tag, vr, value, dicom, size, position)) + { + result.SetValue(tag, value, IsBinaryValueRepresentation(vr)); + } + else + { + return false; + } + } + + return true; + } + + + static std::string ValueAsString(const DicomMap& summary, + const DicomTag& tag) + { + const DicomValue& value = summary.GetValue(tag); + if (value.IsNull()) + { + return "(null)"; + } + else + { + return value.GetContent(); + } + } + + + void DicomMap::LogMissingTagsForStore() const + { + std::string s, t; + + if (HasTag(DICOM_TAG_PATIENT_ID)) + { + if (t.size() > 0) + t += ", "; + t += "PatientID=" + ValueAsString(*this, DICOM_TAG_PATIENT_ID); + } + else + { + if (s.size() > 0) + s += ", "; + s += "PatientID"; + } + + if (HasTag(DICOM_TAG_STUDY_INSTANCE_UID)) + { + if (t.size() > 0) + t += ", "; + t += "StudyInstanceUID=" + ValueAsString(*this, DICOM_TAG_STUDY_INSTANCE_UID); + } + else + { + if (s.size() > 0) + s += ", "; + s += "StudyInstanceUID"; + } + + if (HasTag(DICOM_TAG_SERIES_INSTANCE_UID)) + { + if (t.size() > 0) + t += ", "; + t += "SeriesInstanceUID=" + ValueAsString(*this, DICOM_TAG_SERIES_INSTANCE_UID); + } + else + { + if (s.size() > 0) + s += ", "; + s += "SeriesInstanceUID"; + } + + if (HasTag(DICOM_TAG_SOP_INSTANCE_UID)) + { + if (t.size() > 0) + t += ", "; + t += "SOPInstanceUID=" + ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID); + } + else + { + if (s.size() > 0) + s += ", "; + s += "SOPInstanceUID"; + } + + if (t.size() == 0) + { + LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)"; + } + else + { + LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t; + } + } + + + bool DicomMap::CopyToString(std::string& result, + const DicomTag& tag, + bool allowBinary) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->CopyToString(result, allowBinary); + } + } + + bool DicomMap::ParseInteger32(int32_t& result, + const DicomTag& tag) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->ParseInteger32(result); + } + } + + bool DicomMap::ParseInteger64(int64_t& result, + const DicomTag& tag) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->ParseInteger64(result); + } + } + + bool DicomMap::ParseUnsignedInteger32(uint32_t& result, + const DicomTag& tag) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->ParseUnsignedInteger32(result); + } + } + + bool DicomMap::ParseUnsignedInteger64(uint64_t& result, + const DicomTag& tag) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->ParseUnsignedInteger64(result); + } + } + + bool DicomMap::ParseFloat(float& result, + const DicomTag& tag) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->ParseFloat(result); + } + } + + bool DicomMap::ParseDouble(double& result, + const DicomTag& tag) const + { + const DicomValue* value = TestAndGetValue(tag); + + if (value == NULL) + { + return false; + } + else + { + return value->ParseDouble(result); + } + } + + + void DicomMap::Serialize(Json::Value& target) const + { + target = Json::objectValue; + + for (Map::const_iterator it = map_.begin(); it != map_.end(); ++it) + { + assert(it->second != NULL); + + std::string tag = it->first.Format(); + + Json::Value value; + it->second->Serialize(value); + + target[tag] = value; + } + } + + + void DicomMap::Unserialize(const Json::Value& source) + { + Clear(); + + if (source.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value::Members tags = source.getMemberNames(); + + for (size_t i = 0; i < tags.size(); i++) + { + DicomTag tag(0, 0); + + if (!DicomTag::ParseHexadecimal(tag, tags[i].c_str()) || + map_.find(tag) != map_.end()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + std::auto_ptr value(new DicomValue); + value->Unserialize(source[tags[i]]); + + map_[tag] = value.release(); + } + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomMap.h --- a/Core/DicomFormat/DicomMap.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomMap.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -47,7 +48,7 @@ private: friend class DicomArray; friend class FromDcmtkBridge; - friend class ToDcmtkBridge; + friend class ParsedDicomFile; typedef std::map Map; @@ -88,6 +89,17 @@ void Clear(); + void SetNullValue(uint16_t group, + uint16_t element) + { + SetValue(group, element, new DicomValue); + } + + void SetNullValue(const DicomTag& tag) + { + SetValue(tag, new DicomValue); + } + void SetValue(uint16_t group, uint16_t element, const DicomValue& value) @@ -102,16 +114,18 @@ } void SetValue(const DicomTag& tag, - const std::string& str) + const std::string& str, + bool isBinary) { - SetValue(tag, new DicomValue(str, false)); + SetValue(tag, new DicomValue(str, isBinary)); } void SetValue(uint16_t group, uint16_t element, - const std::string& str) + const std::string& str, + bool isBinary) { - SetValue(group, element, new DicomValue(str, false)); + SetValue(group, element, new DicomValue(str, isBinary)); } bool HasTag(uint16_t group, uint16_t element) const @@ -169,12 +183,42 @@ static void GetMainDicomTags(std::set& result); - void Print(FILE* fp) const; - void GetTags(std::set& tags) const; static void LoadMainDicomTags(const DicomTag*& tags, size_t& size, ResourceType level); + + static bool ParseDicomMetaInformation(DicomMap& result, + const char* dicom, + size_t size); + + void LogMissingTagsForStore() const; + + bool CopyToString(std::string& result, + const DicomTag& tag, + bool allowBinary) const; + + bool ParseInteger32(int32_t& result, + const DicomTag& tag) const; + + bool ParseInteger64(int64_t& result, + const DicomTag& tag) const; + + bool ParseUnsignedInteger32(uint32_t& result, + const DicomTag& tag) const; + + bool ParseUnsignedInteger64(uint64_t& result, + const DicomTag& tag) const; + + bool ParseFloat(float& result, + const DicomTag& tag) const; + + bool ParseDouble(double& result, + const DicomTag& tag) const; + + void Serialize(Json::Value& target) const; + + void Unserialize(const Json::Value& source); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomTag.cpp --- a/Core/DicomFormat/DicomTag.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomTag.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -38,9 +39,32 @@ #include #include #include +#include namespace Orthanc -{ +{ + static inline uint16_t GetCharValue(char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else + return 0; + } + + + static inline uint16_t GetTagValue(const char* c) + { + return ((GetCharValue(c[0]) << 12) + + (GetCharValue(c[1]) << 8) + + (GetCharValue(c[2]) << 4) + + GetCharValue(c[3])); + } + + bool DicomTag::operator< (const DicomTag& other) const { if (group_ < other.group_) @@ -73,6 +97,49 @@ } + bool DicomTag::ParseHexadecimal(DicomTag& tag, + const char* value) + { + size_t length = strlen(value); + + if (length == 9 && + isxdigit(value[0]) && + isxdigit(value[1]) && + isxdigit(value[2]) && + isxdigit(value[3]) && + (value[4] == '-' || value[4] == ',') && + isxdigit(value[5]) && + isxdigit(value[6]) && + isxdigit(value[7]) && + isxdigit(value[8])) + { + uint16_t group = GetTagValue(value); + uint16_t element = GetTagValue(value + 5); + tag = DicomTag(group, element); + return true; + } + else if (length == 8 && + isxdigit(value[0]) && + isxdigit(value[1]) && + isxdigit(value[2]) && + isxdigit(value[3]) && + isxdigit(value[4]) && + isxdigit(value[5]) && + isxdigit(value[6]) && + isxdigit(value[7])) + { + uint16_t group = GetTagValue(value); + uint16_t element = GetTagValue(value + 4); + tag = DicomTag(group, element); + return true; + } + else + { + return false; + } + } + + const char* DicomTag::GetMainTagsName() const { if (*this == DICOM_TAG_ACCESSION_NUMBER) diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomTag.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -66,6 +67,11 @@ return element_; } + bool IsPrivate() const + { + return group_ % 2 == 1; + } + const char* GetMainTagsName() const; bool operator< (const DicomTag& other) const; @@ -82,6 +88,9 @@ std::string Format() const; + static bool ParseHexadecimal(DicomTag& tag, + const char* value); + friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag); static void AddTagsForModule(std::set& target, @@ -95,6 +104,7 @@ static const DicomTag DICOM_TAG_SERIES_INSTANCE_UID(0x0020, 0x000e); static const DicomTag DICOM_TAG_STUDY_INSTANCE_UID(0x0020, 0x000d); static const DicomTag DICOM_TAG_PIXEL_DATA(0x7fe0, 0x0010); + static const DicomTag DICOM_TAG_TRANSFER_SYNTAX_UID(0x0002, 0x0010); static const DicomTag DICOM_TAG_IMAGE_INDEX(0x0054, 0x1330); static const DicomTag DICOM_TAG_INSTANCE_NUMBER(0x0020, 0x0013); @@ -122,6 +132,7 @@ static const DicomTag DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER(0x0020, 0x0100); // Tags for C-FIND and C-MOVE + static const DicomTag DICOM_TAG_MESSAGE_ID(0x0000, 0x0110); static const DicomTag DICOM_TAG_SPECIFIC_CHARACTER_SET(0x0008, 0x0005); static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052); static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061); @@ -152,4 +163,47 @@ static const DicomTag DICOM_TAG_SERIES_TIME(0x0008, 0x0031); static const DicomTag DICOM_TAG_STUDY_DATE(0x0008, 0x0020); static const DicomTag DICOM_TAG_STUDY_TIME(0x0008, 0x0030); + + // Various tags + static const DicomTag DICOM_TAG_SERIES_TYPE(0x0054, 0x1000); + static const DicomTag DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION(0x0032, 0x1060); + static const DicomTag DICOM_TAG_INSTITUTION_NAME(0x0008, 0x0080); + static const DicomTag DICOM_TAG_REQUESTING_PHYSICIAN(0x0032, 0x1032); + static const DicomTag DICOM_TAG_REFERRING_PHYSICIAN_NAME(0x0008, 0x0090); + static const DicomTag DICOM_TAG_OPERATOR_NAME(0x0008, 0x1070); + static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254); + static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000); + static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400); + static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010); + + // Tags used within the Stone of Orthanc + static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009); + static const DicomTag DICOM_TAG_GRID_FRAME_OFFSET_VECTOR(0x3004, 0x000c); + static const DicomTag DICOM_TAG_PIXEL_SPACING(0x0028, 0x0030); + static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052); + static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053); + static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050); + static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050); + static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051); + static const DicomTag DICOM_TAG_DOSE_GRID_SCALING(0x3004, 0x000e); + + // Counting patients, studies and series + // https://www.medicalconnections.co.uk/kb/Counting_Studies_Series_and_Instances + static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES(0x0020, 0x1200); + static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES(0x0020, 0x1202); + static const DicomTag DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES(0x0020, 0x1204); + static const DicomTag DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES(0x0020, 0x1206); + static const DicomTag DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES(0x0020, 0x1208); + static const DicomTag DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES(0x0020, 0x1209); + static const DicomTag DICOM_TAG_SOP_CLASSES_IN_STUDY(0x0008, 0x0062); + + // Tags to preserve relationships during anonymization + static const DicomTag DICOM_TAG_REFERENCED_IMAGE_SEQUENCE(0x0008, 0x1140); + static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID(0x0008, 0x1155); + static const DicomTag DICOM_TAG_SOURCE_IMAGE_SEQUENCE(0x0008, 0x2112); + static const DicomTag DICOM_TAG_FRAME_OF_REFERENCE_UID(0x0020, 0x0052); + static const DicomTag DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_UID(0x3006, 0x0024); + static const DicomTag DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID(0x3006, 0x00c2); + static const DicomTag DICOM_TAG_CURRENT_REQUESTED_PROCEDURE_EVIDENCE_SEQUENCE(0x0040, 0xa375); + static const DicomTag DICOM_TAG_REFERENCED_SERIES_SEQUENCE(0x0008, 0x1115); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomValue.cpp --- a/Core/DicomFormat/DicomValue.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomValue.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -34,8 +35,11 @@ #include "DicomValue.h" #include "../OrthancException.h" +#include "../SerializationToolbox.h" #include "../Toolbox.h" +#include + namespace Orthanc { DicomValue::DicomValue(const DicomValue& other) : @@ -81,7 +85,7 @@ } -#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 +#if ORTHANC_ENABLE_BASE64 == 1 void DicomValue::FormatDataUriScheme(std::string& target, const std::string& mime) const { @@ -90,4 +94,165 @@ } #endif + + template + 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(value); + return true; + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } + + bool DicomValue::ParseInteger32(int32_t& result) const + { + int64_t tmp; + if (ParseValue(tmp, *this)) + { + result = static_cast(tmp); + return (tmp == static_cast(result)); // Check no overflow occurs + } + else + { + return false; + } + } + + bool DicomValue::ParseInteger64(int64_t& result) const + { + return ParseValue(result, *this); + } + + bool DicomValue::ParseUnsignedInteger32(uint32_t& result) const + { + uint64_t tmp; + if (ParseValue(tmp, *this)) + { + result = static_cast(tmp); + return (tmp == static_cast(result)); // Check no overflow occurs + } + else + { + return false; + } + } + + bool DicomValue::ParseUnsignedInteger64(uint64_t& result) const + { + return ParseValue(result, *this); + } + + bool DicomValue::ParseFloat(float& result) const + { + return ParseValue(result, *this); + } + + bool DicomValue::ParseDouble(double& result) const + { + return ParseValue(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); + } + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomFormat/DicomValue.h --- a/Core/DicomFormat/DicomValue.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/DicomFormat/DicomValue.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,8 +33,15 @@ #pragma once +#include #include #include +#include + +#if !defined(ORTHANC_ENABLE_BASE64) +# error The macro ORTHANC_ENABLE_BASE64 must be defined +#endif + namespace Orthanc { @@ -78,7 +86,7 @@ DicomValue* Clone() const; -#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 +#if ORTHANC_ENABLE_BASE64 == 1 void FormatDataUriScheme(std::string& target, const std::string& mime) const; @@ -87,5 +95,24 @@ FormatDataUriScheme(target, "application/octet-stream"); } #endif + + bool CopyToString(std::string& result, + bool allowBinary) const; + + bool ParseInteger32(int32_t& result) const; + + bool ParseInteger64(int64_t& result) const; + + bool ParseUnsignedInteger32(uint32_t& result) const; + + bool ParseUnsignedInteger64(uint64_t& result) const; + + bool ParseFloat(float& result) const; + + bool ParseDouble(double& result) const; + + void Serialize(Json::Value& target) const; + + void Unserialize(const Json::Value& source); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/DicomFindAnswers.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomFindAnswers.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,178 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomFindAnswers.h" + +#include "../DicomParsing/FromDcmtkBridge.h" +#include "../OrthancException.h" + +#include +#include +#include + + +namespace Orthanc +{ + void DicomFindAnswers::AddAnswerInternal(ParsedDicomFile* answer) + { + std::auto_ptr protection(answer); + + if (isWorklist_) + { + // These lines are necessary when serving worklists, otherwise + // Orthanc does not behave as "wlmscpfs" + protection->Remove(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID); + protection->Remove(DICOM_TAG_SOP_INSTANCE_UID); + } + + protection->ChangeEncoding(encoding_); + + answers_.push_back(protection.release()); + } + + + DicomFindAnswers::DicomFindAnswers(bool isWorklist) : + encoding_(GetDefaultDicomEncoding()), + isWorklist_(isWorklist), + complete_(true) + { + } + + + void DicomFindAnswers::SetEncoding(Encoding encoding) + { + for (size_t i = 0; i < answers_.size(); i++) + { + assert(answers_[i] != NULL); + answers_[i]->ChangeEncoding(encoding); + } + + encoding_ = encoding; + } + + + void DicomFindAnswers::SetWorklist(bool isWorklist) + { + if (answers_.empty()) + { + isWorklist_ = isWorklist; + } + else + { + // This set of answers is not empty anymore, cannot change its type + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomFindAnswers::Clear() + { + for (size_t i = 0; i < answers_.size(); i++) + { + assert(answers_[i] != NULL); + delete answers_[i]; + } + + answers_.clear(); + } + + + void DicomFindAnswers::Reserve(size_t size) + { + if (size > answers_.size()) + { + answers_.reserve(size); + } + } + + + void DicomFindAnswers::Add(const DicomMap& map) + { + AddAnswerInternal(new ParsedDicomFile(map, encoding_)); + } + + + void DicomFindAnswers::Add(ParsedDicomFile& dicom) + { + AddAnswerInternal(dicom.Clone(true)); + } + + void DicomFindAnswers::Add(const void* dicom, + size_t size) + { + AddAnswerInternal(new ParsedDicomFile(dicom, size)); + } + + + ParsedDicomFile& DicomFindAnswers::GetAnswer(size_t index) const + { + if (index < answers_.size()) + { + return *answers_[index]; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const + { + return new DcmDataset(*GetAnswer(index).GetDcmtkObject().getDataset()); + } + + + void DicomFindAnswers::ToJson(Json::Value& target, + size_t index, + bool simplify) const + { + DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Human : DicomToJsonFormat_Full); + GetAnswer(index).DatasetToJson(target, format, DicomToJsonFlags_None, 0); + } + + + void DicomFindAnswers::ToJson(Json::Value& target, + bool simplify) const + { + target = Json::arrayValue; + + for (size_t i = 0; i < GetSize(); i++) + { + Json::Value answer; + ToJson(answer, i, simplify); + target.append(answer); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/DicomFindAnswers.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomFindAnswers.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,109 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../DicomParsing/ParsedDicomFile.h" + +namespace Orthanc +{ + class DicomFindAnswers : public boost::noncopyable + { + private: + Encoding encoding_; + bool isWorklist_; + std::vector 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; + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/DicomServer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomServer.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,382 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomServer.h" + +#include "../../Core/Logging.h" +#include "../../Core/MultiThreading/RunnableWorkersPool.h" +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.h" +#include "Internals/CommandDispatcher.h" + +#include + +#if defined(__linux__) +#include +#endif + + +namespace Orthanc +{ + struct DicomServer::PImpl + { + boost::thread thread_; + T_ASC_Network *network_; + std::auto_ptr workers_; + }; + + + void DicomServer::ServerThread(DicomServer* server) + { + LOG(INFO) << "DICOM server started"; + + while (server->continue_) + { + /* receive an association and acknowledge or reject it. If the association was */ + /* acknowledged, offer corresponding services and invoke one or more if required. */ + std::auto_ptr dispatcher(Internals::AcceptAssociation(*server, server->pimpl_->network_)); + + try + { + if (dispatcher.get() != NULL) + { + server->pimpl_->workers_->Add(dispatcher.release()); + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception in the DICOM server thread: " << e.What(); + } + } + + LOG(INFO) << "DICOM server stopping"; + } + + + DicomServer::DicomServer() : + pimpl_(new PImpl), + aet_("ANY-SCP") + { + port_ = 104; + modalities_ = NULL; + findRequestHandlerFactory_ = NULL; + moveRequestHandlerFactory_ = NULL; + storeRequestHandlerFactory_ = NULL; + worklistRequestHandlerFactory_ = NULL; + applicationEntityFilter_ = NULL; + checkCalledAet_ = true; + associationTimeout_ = 30; + continue_ = false; + } + + DicomServer::~DicomServer() + { + if (continue_) + { + LOG(ERROR) << "INTERNAL ERROR: DicomServer::Stop() should be invoked manually to avoid mess in the destruction order!"; + Stop(); + } + } + + void DicomServer::SetPortNumber(uint16_t port) + { + Stop(); + port_ = port; + } + + uint16_t DicomServer::GetPortNumber() const + { + return port_; + } + + void DicomServer::SetAssociationTimeout(uint32_t seconds) + { + LOG(INFO) << "Setting timeout for DICOM connections if Orthanc acts as SCP (server): " + << seconds << " seconds (0 = no timeout)"; + + Stop(); + associationTimeout_ = seconds; + } + + uint32_t DicomServer::GetAssociationTimeout() const + { + return associationTimeout_; + } + + + void DicomServer::SetCalledApplicationEntityTitleCheck(bool check) + { + Stop(); + checkCalledAet_ = check; + } + + bool DicomServer::HasCalledApplicationEntityTitleCheck() const + { + return checkCalledAet_; + } + + void DicomServer::SetApplicationEntityTitle(const std::string& aet) + { + if (aet.size() == 0) + { + throw OrthancException(ErrorCode_BadApplicationEntityTitle); + } + + if (aet.size() > 16) + { + throw OrthancException(ErrorCode_BadApplicationEntityTitle); + } + + for (size_t i = 0; i < aet.size(); i++) + { + if (!(aet[i] == '-' || + aet[i] == '_' || + isdigit(aet[i]) || + (aet[i] >= 'A' && aet[i] <= 'Z'))) + { + LOG(WARNING) << "For best interoperability, only upper case, alphanumeric characters should be present in AET: \"" << aet << "\""; + break; + } + } + + Stop(); + aet_ = aet; + } + + const std::string& DicomServer::GetApplicationEntityTitle() const + { + return aet_; + } + + void DicomServer::SetRemoteModalities(IRemoteModalities& modalities) + { + Stop(); + modalities_ = &modalities; + } + + DicomServer::IRemoteModalities& DicomServer::GetRemoteModalities() const + { + if (modalities_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *modalities_; + } + } + + void DicomServer::SetFindRequestHandlerFactory(IFindRequestHandlerFactory& factory) + { + Stop(); + findRequestHandlerFactory_ = &factory; + } + + bool DicomServer::HasFindRequestHandlerFactory() const + { + return (findRequestHandlerFactory_ != NULL); + } + + IFindRequestHandlerFactory& DicomServer::GetFindRequestHandlerFactory() const + { + if (HasFindRequestHandlerFactory()) + { + return *findRequestHandlerFactory_; + } + else + { + throw OrthancException(ErrorCode_NoCFindHandler); + } + } + + void DicomServer::SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& factory) + { + Stop(); + moveRequestHandlerFactory_ = &factory; + } + + bool DicomServer::HasMoveRequestHandlerFactory() const + { + return (moveRequestHandlerFactory_ != NULL); + } + + IMoveRequestHandlerFactory& DicomServer::GetMoveRequestHandlerFactory() const + { + if (HasMoveRequestHandlerFactory()) + { + return *moveRequestHandlerFactory_; + } + else + { + throw OrthancException(ErrorCode_NoCMoveHandler); + } + } + + void DicomServer::SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& factory) + { + Stop(); + storeRequestHandlerFactory_ = &factory; + } + + bool DicomServer::HasStoreRequestHandlerFactory() const + { + return (storeRequestHandlerFactory_ != NULL); + } + + IStoreRequestHandlerFactory& DicomServer::GetStoreRequestHandlerFactory() const + { + if (HasStoreRequestHandlerFactory()) + { + return *storeRequestHandlerFactory_; + } + else + { + throw OrthancException(ErrorCode_NoCStoreHandler); + } + } + + void DicomServer::SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& factory) + { + Stop(); + worklistRequestHandlerFactory_ = &factory; + } + + bool DicomServer::HasWorklistRequestHandlerFactory() const + { + return (worklistRequestHandlerFactory_ != NULL); + } + + IWorklistRequestHandlerFactory& DicomServer::GetWorklistRequestHandlerFactory() const + { + if (HasWorklistRequestHandlerFactory()) + { + return *worklistRequestHandlerFactory_; + } + else + { + throw OrthancException(ErrorCode_NoWorklistHandler); + } + } + + void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory) + { + Stop(); + applicationEntityFilter_ = &factory; + } + + bool DicomServer::HasApplicationEntityFilter() const + { + return (applicationEntityFilter_ != NULL); + } + + IApplicationEntityFilter& DicomServer::GetApplicationEntityFilter() const + { + if (HasApplicationEntityFilter()) + { + return *applicationEntityFilter_; + } + else + { + throw OrthancException(ErrorCode_NoApplicationEntityFilter); + } + } + + void DicomServer::Start() + { + if (modalities_ == NULL) + { + LOG(ERROR) << "No list of modalities was provided to the DICOM server"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + Stop(); + + /* initialize network, i.e. create an instance of T_ASC_Network*. */ + OFCondition cond = ASC_initializeNetwork + (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_); + if (cond.bad()) + { + LOG(ERROR) << "cannot create network: " << cond.text(); + throw OrthancException(ErrorCode_DicomPortInUse); + } + + continue_ = true; + pimpl_->workers_.reset(new RunnableWorkersPool(4)); // Use 4 workers - TODO as a parameter? + pimpl_->thread_ = boost::thread(ServerThread, this); + } + + + void DicomServer::Stop() + { + if (continue_) + { + continue_ = false; + + if (pimpl_->thread_.joinable()) + { + pimpl_->thread_.join(); + } + + pimpl_->workers_.reset(NULL); + + /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */ + /* is the counterpart of ASC_initializeNetwork(...) which was called above. */ + OFCondition cond = ASC_dropNetwork(&pimpl_->network_); + if (cond.bad()) + { + LOG(ERROR) << "Error while dropping the network: " << cond.text(); + } + } + } + + + bool DicomServer::IsMyAETitle(const std::string& aet) const + { + if (modalities_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (!HasCalledApplicationEntityTitleCheck()) + { + // OK, no check on the AET. + return true; + } + else + { + return modalities_->IsSameAETitle(aet, GetApplicationEntityTitle()); + } + } + +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/DicomServer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomServer.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,136 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1 +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1 +#endif + +#include "IFindRequestHandlerFactory.h" +#include "IMoveRequestHandlerFactory.h" +#include "IStoreRequestHandlerFactory.h" +#include "IWorklistRequestHandlerFactory.h" +#include "IApplicationEntityFilter.h" +#include "RemoteModalityParameters.h" + +#include +#include + + +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_; + + bool checkCalledAet_; + std::string aet_; + uint16_t port_; + bool continue_; + uint32_t associationTimeout_; + IRemoteModalities* modalities_; + IFindRequestHandlerFactory* findRequestHandlerFactory_; + IMoveRequestHandlerFactory* moveRequestHandlerFactory_; + IStoreRequestHandlerFactory* storeRequestHandlerFactory_; + IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; + IApplicationEntityFilter* applicationEntityFilter_; + + static void ServerThread(DicomServer* server); + + public: + DicomServer(); + + ~DicomServer(); + + void SetPortNumber(uint16_t port); + uint16_t GetPortNumber() const; + + void SetAssociationTimeout(uint32_t seconds); + uint32_t GetAssociationTimeout() const; + + void SetCalledApplicationEntityTitleCheck(bool check); + bool HasCalledApplicationEntityTitleCheck() const; + + void SetApplicationEntityTitle(const std::string& aet); + const std::string& GetApplicationEntityTitle() const; + + void SetRemoteModalities(IRemoteModalities& modalities); + IRemoteModalities& GetRemoteModalities() const; + + void SetFindRequestHandlerFactory(IFindRequestHandlerFactory& handler); + bool HasFindRequestHandlerFactory() const; + IFindRequestHandlerFactory& GetFindRequestHandlerFactory() const; + + void SetMoveRequestHandlerFactory(IMoveRequestHandlerFactory& handler); + bool HasMoveRequestHandlerFactory() const; + IMoveRequestHandlerFactory& GetMoveRequestHandlerFactory() const; + + void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler); + bool HasStoreRequestHandlerFactory() const; + IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const; + + void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler); + bool HasWorklistRequestHandlerFactory() const; + IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const; + + void SetApplicationEntityFilter(IApplicationEntityFilter& handler); + bool HasApplicationEntityFilter() const; + IApplicationEntityFilter& GetApplicationEntityFilter() const; + + void Start(); + + void Stop(); + + bool IsMyAETitle(const std::string& aet) const; + }; + +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/DicomUserConnection.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,1258 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + +#include "../PrecompiledHeaders.h" +#include "DicomUserConnection.h" + +#include "../DicomFormat/DicomArray.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "../DicomParsing/FromDcmtkBridge.h" +#include "../DicomParsing/ToDcmtkBridge.h" + +#include +#include +#include +#include +#include + +#include + + +#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 +#endif + + +#if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX) +/** + * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that + * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an + * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect + * that the result will fit." + * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html + **/ +#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX +#endif + + +static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax; + +/** + * "If we have more than 64 storage SOP classes, tools such as + * storescu will fail because they attempt to negotiate two + * presentation contexts for each SOP class, and there is a total + * limit of 128 contexts for one association." + **/ +static const unsigned int MAXIMUM_STORAGE_SOP_CLASSES = 64; + + +namespace Orthanc +{ + // By default, the timeout for DICOM SCU (client) connections is set to 10 seconds + static uint32_t defaultTimeout_ = 10; + + struct DicomUserConnection::PImpl + { + // Connection state + uint32_t dimseTimeout_; + uint32_t acseTimeout_; + T_ASC_Network* net_; + T_ASC_Parameters* params_; + T_ASC_Association* assoc_; + + bool IsOpen() const + { + return assoc_ != NULL; + } + + void CheckIsOpen() const; + + void Store(DcmInputStream& is, + DicomUserConnection& connection, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + }; + + + static void Check(const OFCondition& cond) + { + if (cond.bad()) + { + LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text()); + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + void DicomUserConnection::PImpl::CheckIsOpen() const + { + if (!IsOpen()) + { + LOG(ERROR) << "DicomUserConnection: First open the connection"; + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + + void DicomUserConnection::CheckIsOpen() const + { + pimpl_->CheckIsOpen(); + } + + + static void RegisterStorageSOPClass(T_ASC_Parameters* params, + unsigned int& presentationContextId, + const std::string& sopClass, + const char* asPreferred[], + std::vector& asFallback) + { + Check(ASC_addPresentationContext(params, presentationContextId, + sopClass.c_str(), asPreferred, 1)); + presentationContextId += 2; + + if (asFallback.size() > 0) + { + Check(ASC_addPresentationContext(params, presentationContextId, + sopClass.c_str(), &asFallback[0], asFallback.size())); + presentationContextId += 2; + } + } + + + void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax) + { + // Flatten an array with the preferred transfer syntax + const char* asPreferred[1] = { preferredTransferSyntax.c_str() }; + + // Setup the fallback transfer syntaxes + std::set fallbackSyntaxes; + fallbackSyntaxes.insert(UID_LittleEndianExplicitTransferSyntax); + fallbackSyntaxes.insert(UID_BigEndianExplicitTransferSyntax); + fallbackSyntaxes.insert(UID_LittleEndianImplicitTransferSyntax); + fallbackSyntaxes.erase(preferredTransferSyntax); + + // Flatten an array with the fallback transfer syntaxes + std::vector asFallback; + asFallback.reserve(fallbackSyntaxes.size()); + for (std::set::const_iterator + it = fallbackSyntaxes.begin(); it != fallbackSyntaxes.end(); ++it) + { + asFallback.push_back(it->c_str()); + } + + CheckStorageSOPClassesInvariant(); + unsigned int presentationContextId = 1; + + for (std::list::const_iterator it = reservedStorageSOPClasses_.begin(); + it != reservedStorageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback); + } + + for (std::set::const_iterator it = storageSOPClasses_.begin(); + it != storageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback); + } + + for (std::set::const_iterator it = defaultStorageSOPClasses_.begin(); + it != defaultStorageSOPClasses_.end(); ++it) + { + RegisterStorageSOPClass(pimpl_->params_, presentationContextId, + *it, asPreferred, asFallback); + } + } + + + static bool IsGenericTransferSyntax(const std::string& syntax) + { + return (syntax == UID_LittleEndianExplicitTransferSyntax || + syntax == UID_BigEndianExplicitTransferSyntax || + syntax == UID_LittleEndianImplicitTransferSyntax); + } + + + void DicomUserConnection::PImpl::Store(DcmInputStream& is, + DicomUserConnection& connection, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + DcmFileFormat dcmff; + Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength)); + + // Determine the storage SOP class UID for this instance + static const DcmTagKey DCM_SOP_CLASS_UID(0x0008, 0x0016); + OFString sopClassUid; + if (dcmff.getDataset()->findAndGetOFString(DCM_SOP_CLASS_UID, sopClassUid).good()) + { + connection.AddStorageSOPClass(sopClassUid.c_str()); + } + + // Determine whether a new presentation context must be + // negotiated, depending on the transfer syntax of this instance + DcmXfer xfer(dcmff.getDataset()->getOriginalXfer()); + const std::string syntax(xfer.getXferID()); + bool isGeneric = IsGenericTransferSyntax(syntax); + + bool renegotiate; + + if (!IsOpen()) + { + renegotiate = true; + } + else if (isGeneric) + { + // Are we making a generic-to-specific or specific-to-generic change of + // the transfer syntax? If this is the case, renegotiate the connection. + renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax()); + + if (renegotiate) + { + LOG(INFO) << "Use of non-generic transfer syntax: the C-Store associated must be renegotiated"; + } + } + else + { + // We are using a specific transfer syntax. Renegotiate if the + // current connection does not match this transfer syntax. + renegotiate = (syntax != connection.GetPreferredTransferSyntax()); + + if (renegotiate) + { + LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated"; + } + } + + if (renegotiate) + { + if (isGeneric) + { + connection.ResetPreferredTransferSyntax(); + } + else + { + connection.SetPreferredTransferSyntax(syntax); + } + } + + if (!connection.IsOpen()) + { + connection.Open(); + } + + // Figure out which SOP class and SOP instance is encapsulated in the file + DIC_UI sopClass; + DIC_UI sopInstance; + if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance)) + { + throw OrthancException(ErrorCode_NoSopClassOrInstance); + } + + // Figure out which of the accepted presentation contexts should be used + int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass); + if (presID == 0) + { + const char *modalityName = dcmSOPClassUIDToModality(sopClass); + if (!modalityName) modalityName = dcmFindNameOfUID(sopClass); + if (!modalityName) modalityName = "unknown SOP class"; + throw OrthancException(ErrorCode_NoPresentationContext); + } + + // Prepare the transmission of data + T_DIMSE_C_StoreRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = assoc_->nextMsgID++; + strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); + request.Priority = DIMSE_PRIORITY_MEDIUM; + request.DataSetType = DIMSE_DATASET_PRESENT; + strncpy(request.AffectedSOPInstanceUID, sopInstance, DIC_UI_LEN); + + if (!moveOriginatorAET.empty()) + { + strncpy(request.MoveOriginatorApplicationEntityTitle, + moveOriginatorAET.c_str(), DIC_AE_LEN); + request.opts = O_STORE_MOVEORIGINATORAETITLE; + + request.MoveOriginatorID = moveOriginatorID; // The type DIC_US is an alias for uint16_t + request.opts |= O_STORE_MOVEORIGINATORID; + } + + // Finally conduct transmission of data + T_DIMSE_C_StoreRSP rsp; + DcmDataset* statusDetail = NULL; + Check(DIMSE_storeUser(assoc_, presID, &request, + NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL, + /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_, + &rsp, &statusDetail, NULL)); + + if (statusDetail != NULL) + { + delete statusDetail; + } + } + + + namespace + { + struct FindPayload + { + DicomFindAnswers* answers; + const char* level; + bool isWorklist; + }; + } + + + static void FindCallback( + /* in */ + void *callbackData, + T_DIMSE_C_FindRQ *request, /* original find request */ + int responseCount, + T_DIMSE_C_FindRSP *response, /* pending response received */ + DcmDataset *responseIdentifiers /* pending response identifiers */ + ) + { + FindPayload& payload = *reinterpret_cast(callbackData); + + if (responseIdentifiers != NULL) + { + if (payload.isWorklist) + { + ParsedDicomFile answer(*responseIdentifiers); + payload.answers->Add(answer); + } + else + { + DicomMap m; + FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers); + + if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + { + m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false); + } + + payload.answers->Add(m); + } + } + } + + + static void FixFindQuery(DicomMap& fixedQuery, + ResourceType level, + const DicomMap& fields) + { + std::set allowedTags; + + // WARNING: Do not add "break" or reorder items in this switch-case! + switch (level) + { + case ResourceType_Instance: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance); + + case ResourceType_Series: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Series); + + case ResourceType_Study: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Study); + + case ResourceType_Patient: + DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + switch (level) + { + case ResourceType_Patient: + allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); + allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); + allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); + break; + + case ResourceType_Study: + allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); + allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); + allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY); + break; + + case ResourceType_Series: + allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); + break; + + default: + break; + } + + allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); + + DicomArray query(fields); + for (size_t i = 0; i < query.GetSize(); i++) + { + const DicomTag& tag = query.GetElement(i).GetTag(); + if (allowedTags.find(tag) == allowedTags.end()) + { + LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag; + } + else + { + fixedQuery.SetValue(tag, query.GetElement(i).GetValue()); + } + } + } + + + static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields, + ModalityManufacturer manufacturer) + { + // Fix outgoing C-Find requests issue for Syngo.Via and its + // solution was reported by Emsy Chan by private mail on + // 2015-06-17. According to Robert van Ommen (2015-11-30), the + // same fix is required for Agfa Impax. This was generalized for + // generic manufacturer since it seems to affect PhilipsADW, + // GEWAServer as well: + // https://bitbucket.org/sjodogne/orthanc/issues/31/ + + switch (manufacturer) + { + case ModalityManufacturer_GenericNoWildcardInDates: + case ModalityManufacturer_GenericNoUniversalWildcard: + { + std::auto_ptr fix(fields.Clone()); + + std::set tags; + fix->GetTags(tags); + + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + // Replace a "*" wildcard query by an empty query ("") for + // "date" or "all" value representations depending on the + // type of manufacturer. + if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard || + (manufacturer == ModalityManufacturer_GenericNoWildcardInDates && + FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date)) + { + const DicomValue* value = fix->TestAndGetValue(*it); + + if (value != NULL && + !value->IsNull() && + value->GetContent() == "*") + { + fix->SetValue(*it, "", false); + } + } + } + + return new ParsedDicomFile(*fix); + } + + default: + return new ParsedDicomFile(fields); + } + } + + + static void ExecuteFind(DicomFindAnswers& answers, + T_ASC_Association* association, + DcmDataset* dataset, + const char* sopClass, + bool isWorklist, + const char* level, + uint32_t dimseTimeout) + { + assert(isWorklist ^ (level != NULL)); + + FindPayload payload; + payload.answers = &answers; + payload.level = level; + payload.isWorklist = isWorklist; + + // Figure out which of the accepted presentation contexts should be used + int presID = ASC_findAcceptedPresentationContextID(association, sopClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_DicomFindUnavailable); + } + + T_DIMSE_C_FindRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = association->nextMsgID++; + strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); + request.Priority = DIMSE_PRIORITY_MEDIUM; + request.DataSetType = DIMSE_DATASET_PRESENT; + + T_DIMSE_C_FindRSP response; + DcmDataset* statusDetail = NULL; + OFCondition cond = DIMSE_findUser(association, presID, &request, dataset, + FindCallback, &payload, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ dimseTimeout, + &response, &statusDetail); + + if (statusDetail) + { + delete statusDetail; + } + + Check(cond); + } + + + void DicomUserConnection::Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& originalFields) + { + DicomMap fields; + FixFindQuery(fields, level, originalFields); + + CheckIsOpen(); + + std::auto_ptr query(ConvertQueryFields(fields, manufacturer_)); + DcmDataset* dataset = query->GetDcmtkObject().getDataset(); + + const char* clevel = NULL; + const char* sopClass = NULL; + + switch (level) + { + case ResourceType_Patient: + clevel = "PATIENT"; + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT"); + sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; + break; + + case ResourceType_Study: + clevel = "STUDY"; + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY"); + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + case ResourceType_Series: + clevel = "SERIES"; + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES"); + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + case ResourceType_Instance: + clevel = "INSTANCE"; + if (manufacturer_ == ModalityManufacturer_ClearCanvas || + manufacturer_ == ModalityManufacturer_Dcm4Chee) + { + // This is a particular case for ClearCanvas, thanks to Peter Somlo . + // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J + // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE"); + } + else + { + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE"); + } + + sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Add the expected tags for this query level. + // WARNING: Do not reorder or add "break" in this switch-case! + switch (level) + { + case ResourceType_Instance: + // SOP Instance UID + if (!fields.HasTag(0x0008, 0x0018)) + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0018), ""); + + case ResourceType_Series: + // Series instance UID + if (!fields.HasTag(0x0020, 0x000e)) + DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000e), ""); + + case ResourceType_Study: + // Accession number + if (!fields.HasTag(0x0008, 0x0050)) + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0050), ""); + + // Study instance UID + if (!fields.HasTag(0x0020, 0x000d)) + DU_putStringDOElement(dataset, DcmTagKey(0x0020, 0x000d), ""); + + case ResourceType_Patient: + // Patient ID + if (!fields.HasTag(0x0010, 0x0020)) + DU_putStringDOElement(dataset, DcmTagKey(0x0010, 0x0020), ""); + + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(clevel != NULL && sopClass != NULL); + ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, pimpl_->dimseTimeout_); + } + + + void DicomUserConnection::MoveInternal(const std::string& targetAet, + ResourceType level, + const DicomMap& fields) + { + CheckIsOpen(); + + std::auto_ptr query(ConvertQueryFields(fields, manufacturer_)); + DcmDataset* dataset = query->GetDcmtkObject().getDataset(); + + const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; + switch (level) + { + case ResourceType_Patient: + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "PATIENT"); + break; + + case ResourceType_Study: + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "STUDY"); + break; + + case ResourceType_Series: + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "SERIES"); + break; + + case ResourceType_Instance: + if (manufacturer_ == ModalityManufacturer_ClearCanvas || + manufacturer_ == ModalityManufacturer_Dcm4Chee) + { + // This is a particular case for ClearCanvas, thanks to Peter Somlo . + // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J + // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "IMAGE"); + } + else + { + DU_putStringDOElement(dataset, DcmTagKey(0x0008, 0x0052), "INSTANCE"); + } + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Figure out which of the accepted presentation contexts should be used + int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_DicomMoveUnavailable); + } + + T_DIMSE_C_MoveRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = pimpl_->assoc_->nextMsgID++; + strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); + request.Priority = DIMSE_PRIORITY_MEDIUM; + request.DataSetType = DIMSE_DATASET_PRESENT; + strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN); + + T_DIMSE_C_MoveRSP response; + DcmDataset* statusDetail = NULL; + DcmDataset* responseIdentifiers = NULL; + OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset, + NULL, NULL, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, + pimpl_->net_, NULL, NULL, + &response, &statusDetail, &responseIdentifiers); + + if (statusDetail) + { + delete statusDetail; + } + + if (responseIdentifiers) + { + delete responseIdentifiers; + } + + Check(cond); + } + + + void DicomUserConnection::ResetStorageSOPClasses() + { + CheckStorageSOPClassesInvariant(); + + storageSOPClasses_.clear(); + defaultStorageSOPClasses_.clear(); + + // Copy the short list of storage SOP classes from DCMTK, making + // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**). + + std::set uncommon; + uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage); + uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage); + uncommon.insert(UID_ColorSoftcopyPresentationStateStorage); + uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage); + uncommon.insert(UID_XAXRFGrayscaleSoftcopyPresentationStateStorage); + + // Add the storage syntaxes for C-STORE + for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++) + { + if (uncommon.find(dcmShortSCUStorageSOPClassUIDs[i]) == uncommon.end()) + { + defaultStorageSOPClasses_.insert(dcmShortSCUStorageSOPClassUIDs[i]); + } + } + + CheckStorageSOPClassesInvariant(); + } + + + void DicomUserConnection::DefaultSetup() + { + preferredTransferSyntax_ = DEFAULT_PREFERRED_TRANSFER_SYNTAX; + localAet_ = "STORESCU"; + remoteAet_ = "ANY-SCP"; + remoteHost_ = "127.0.0.1"; + remotePort_ = 104; + manufacturer_ = ModalityManufacturer_Generic; + + SetTimeout(defaultTimeout_); + pimpl_->net_ = NULL; + pimpl_->params_ = NULL; + pimpl_->assoc_ = NULL; + + // SOP classes for C-ECHO, C-FIND and C-MOVE (**) + reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass); + reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); + reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); + reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); + reservedStorageSOPClasses_.push_back(UID_FINDModalityWorklistInformationModel); + + ResetStorageSOPClasses(); + } + + + DicomUserConnection::DicomUserConnection() : + pimpl_(new PImpl) + { + DefaultSetup(); + } + + + DicomUserConnection::DicomUserConnection(const std::string& localAet, + const RemoteModalityParameters& remote) : + pimpl_(new PImpl) + { + DefaultSetup(); + SetLocalApplicationEntityTitle(localAet); + SetRemoteModality(remote); + } + + + DicomUserConnection::~DicomUserConnection() + { + Close(); + } + + + void DicomUserConnection::SetRemoteModality(const RemoteModalityParameters& parameters) + { + SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); + SetRemoteHost(parameters.GetHost()); + SetRemotePort(parameters.GetPortNumber()); + SetRemoteManufacturer(parameters.GetManufacturer()); + } + + + void DicomUserConnection::SetLocalApplicationEntityTitle(const std::string& aet) + { + if (localAet_ != aet) + { + Close(); + localAet_ = aet; + } + } + + void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet) + { + if (remoteAet_ != aet) + { + Close(); + remoteAet_ = aet; + } + } + + void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer) + { + if (manufacturer_ != manufacturer) + { + Close(); + manufacturer_ = manufacturer; + } + } + + void DicomUserConnection::ResetPreferredTransferSyntax() + { + SetPreferredTransferSyntax(DEFAULT_PREFERRED_TRANSFER_SYNTAX); + } + + void DicomUserConnection::SetPreferredTransferSyntax(const std::string& preferredTransferSyntax) + { + if (preferredTransferSyntax_ != preferredTransferSyntax) + { + Close(); + preferredTransferSyntax_ = preferredTransferSyntax; + } + } + + + void DicomUserConnection::SetRemoteHost(const std::string& host) + { + if (remoteHost_ != host) + { + if (host.size() > HOST_NAME_MAX - 10) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Close(); + remoteHost_ = host; + } + } + + void DicomUserConnection::SetRemotePort(uint16_t port) + { + if (remotePort_ != port) + { + Close(); + remotePort_ = port; + } + } + + void DicomUserConnection::Open() + { + if (IsOpen()) + { + // Don't reopen the connection + return; + } + + LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() + << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host " + << GetRemoteHost() << ":" << GetRemotePort() + << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")"; + + Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_)); + Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); + + // Set this application's title and the called application's title in the params + Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL)); + + // Set the network addresses of the local and remote entities + char localHost[HOST_NAME_MAX]; + gethostname(localHost, HOST_NAME_MAX - 1); + + char remoteHostAndPort[HOST_NAME_MAX]; + +#ifdef _MSC_VER + _snprintf +#else + snprintf +#endif + (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_); + + Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort)); + + // Set various options + Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false)); + + SetupPresentationContexts(preferredTransferSyntax_); + + // Do the association + Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_)); + + if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0) + { + throw OrthancException(ErrorCode_NoPresentationContext); + } + } + + void DicomUserConnection::Close() + { + if (pimpl_->assoc_ != NULL) + { + ASC_releaseAssociation(pimpl_->assoc_); + ASC_destroyAssociation(&pimpl_->assoc_); + pimpl_->assoc_ = NULL; + pimpl_->params_ = NULL; + } + else + { + if (pimpl_->params_ != NULL) + { + ASC_destroyAssociationParameters(&pimpl_->params_); + pimpl_->params_ = NULL; + } + } + + if (pimpl_->net_ != NULL) + { + ASC_dropNetwork(&pimpl_->net_); + pimpl_->net_ = NULL; + } + } + + bool DicomUserConnection::IsOpen() const + { + return pimpl_->IsOpen(); + } + + void DicomUserConnection::Store(const char* buffer, + size_t size, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + // Prepare an input stream for the memory buffer + DcmInputBufferStream is; + if (size > 0) + is.setBuffer(buffer, size); + is.setEos(); + + pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + } + + void DicomUserConnection::Store(const std::string& buffer, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + if (buffer.size() > 0) + Store(reinterpret_cast(&buffer[0]), buffer.size(), moveOriginatorAET, moveOriginatorID); + else + Store(NULL, 0, moveOriginatorAET, moveOriginatorID); + } + + void DicomUserConnection::StoreFile(const std::string& path, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + // Prepare an input stream for the file + DcmInputFileStream is(path.c_str()); + pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + } + + bool DicomUserConnection::Echo() + { + CheckIsOpen(); + DIC_US status; + Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, + &status, NULL)); + return status == STATUS_Success; + } + + + static void TestAndCopyTag(DicomMap& result, + const DicomMap& source, + const DicomTag& tag) + { + if (!source.HasTag(tag)) + { + throw OrthancException(ErrorCode_BadRequest); + } + else + { + result.SetValue(tag, source.GetValue(tag)); + } + } + + + void DicomUserConnection::Move(const std::string& targetAet, + ResourceType level, + const DicomMap& findResult) + { + DicomMap move; + switch (level) + { + case ResourceType_Patient: + TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); + break; + + case ResourceType_Study: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); + break; + + case ResourceType_Instance: + TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); + TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + MoveInternal(targetAet, level, move); + } + + + void DicomUserConnection::Move(const std::string& targetAet, + const DicomMap& findResult) + { + if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + { + throw OrthancException(ErrorCode_InternalError); + } + + const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); + ResourceType level = StringToResourceType(tmp.c_str()); + + Move(targetAet, level, findResult); + } + + + void DicomUserConnection::MovePatient(const std::string& targetAet, + const std::string& patientId) + { + DicomMap query; + query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false); + MoveInternal(targetAet, ResourceType_Patient, query); + } + + void DicomUserConnection::MoveStudy(const std::string& targetAet, + const std::string& studyUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); + MoveInternal(targetAet, ResourceType_Study, query); + } + + void DicomUserConnection::MoveSeries(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); + query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); + MoveInternal(targetAet, ResourceType_Series, query); + } + + void DicomUserConnection::MoveInstance(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid, + const std::string& instanceUid) + { + DicomMap query; + query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); + query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); + query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false); + MoveInternal(targetAet, ResourceType_Instance, query); + } + + + void DicomUserConnection::SetTimeout(uint32_t seconds) + { + if (seconds == 0) + { + DisableTimeout(); + } + else + { + dcmConnectionTimeout.set(seconds); + pimpl_->dimseTimeout_ = seconds; + pimpl_->acseTimeout_ = 10; // Timeout used during association negociation + } + } + + + void DicomUserConnection::DisableTimeout() + { + /** + * Global timeout (seconds) for connecting to remote hosts. + * Default value is -1 which selects infinite timeout, i.e. blocking connect(). + */ + dcmConnectionTimeout.set(-1); + pimpl_->dimseTimeout_ = 0; + pimpl_->acseTimeout_ = 10; // Timeout used during association negociation + } + + + void DicomUserConnection::CheckStorageSOPClassesInvariant() const + { + assert(storageSOPClasses_.size() + + defaultStorageSOPClasses_.size() + + reservedStorageSOPClasses_.size() <= MAXIMUM_STORAGE_SOP_CLASSES); + } + + void DicomUserConnection::AddStorageSOPClass(const char* sop) + { + CheckStorageSOPClassesInvariant(); + + if (storageSOPClasses_.find(sop) != storageSOPClasses_.end()) + { + // This storage SOP class is already explicitly registered. Do + // nothing. + return; + } + + if (defaultStorageSOPClasses_.find(sop) != defaultStorageSOPClasses_.end()) + { + // This storage SOP class is not explicitly registered, but is + // used by default. Just register it explicitly. + defaultStorageSOPClasses_.erase(sop); + storageSOPClasses_.insert(sop); + + CheckStorageSOPClassesInvariant(); + return; + } + + // This storage SOP class is neither explicitly, nor implicitly + // registered. Close the connection and register it explicitly. + + Close(); + + if (reservedStorageSOPClasses_.size() + + storageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) // (*) + { + // The maximum number of SOP classes is reached + ResetStorageSOPClasses(); + defaultStorageSOPClasses_.erase(sop); + } + else if (reservedStorageSOPClasses_.size() + storageSOPClasses_.size() + + defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) + { + // Make room in the default storage syntaxes + assert(!defaultStorageSOPClasses_.empty()); // Necessarily true because condition (*) is false + defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin()); + } + + // Explicitly register the new storage syntax + storageSOPClasses_.insert(sop); + + CheckStorageSOPClassesInvariant(); + } + + + void DicomUserConnection::FindWorklist(DicomFindAnswers& result, + ParsedDicomFile& query) + { + CheckIsOpen(); + + DcmDataset* dataset = query.GetDcmtkObject().getDataset(); + const char* sopClass = UID_FINDModalityWorklistInformationModel; + + ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, NULL, pimpl_->dimseTimeout_); + } + + + void DicomUserConnection::SetDefaultTimeout(uint32_t seconds) + { + LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " + << seconds << " seconds (0 = no timeout)"; + defaultTimeout_ = seconds; + } + + + bool DicomUserConnection::IsSameAssociation(const std::string& localAet, + const RemoteModalityParameters& remote) const + { + return (localAet_ == localAet && + remoteAet_ == remote.GetApplicationEntityTitle() && + remoteHost_ == remote.GetHost() && + remotePort_ == remote.GetPortNumber() && + manufacturer_ == remote.GetManufacturer()); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/DicomUserConnection.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/DicomUserConnection.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,215 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1 +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1 +#endif + +#include "DicomFindAnswers.h" +#include "../Enumerations.h" +#include "RemoteModalityParameters.h" + +#include +#include +#include +#include + +namespace Orthanc +{ + class DicomUserConnection : public boost::noncopyable + { + private: + struct PImpl; + boost::shared_ptr pimpl_; + + // Connection parameters + std::string preferredTransferSyntax_; + std::string localAet_; + std::string remoteAet_; + std::string remoteHost_; + uint16_t remotePort_; + ModalityManufacturer manufacturer_; + std::set storageSOPClasses_; + std::list reservedStorageSOPClasses_; + std::set defaultStorageSOPClasses_; + + void CheckIsOpen() const; + + void SetupPresentationContexts(const std::string& preferredTransferSyntax); + + void MoveInternal(const std::string& targetAet, + ResourceType level, + const DicomMap& fields); + + void ResetStorageSOPClasses(); + + void CheckStorageSOPClassesInvariant() const; + + void DefaultSetup(); + + public: + DicomUserConnection(); + + ~DicomUserConnection(); + + // This constructor corresponds to behavior of the old class + // "ReusableDicomUserConnection", without the call to "Open()" + DicomUserConnection(const std::string& localAet, + const RemoteModalityParameters& remote); + + void SetRemoteModality(const RemoteModalityParameters& parameters); + + void SetLocalApplicationEntityTitle(const std::string& aet); + + const std::string& GetLocalApplicationEntityTitle() const + { + return localAet_; + } + + void SetRemoteApplicationEntityTitle(const std::string& aet); + + const std::string& GetRemoteApplicationEntityTitle() const + { + return remoteAet_; + } + + void SetRemoteHost(const std::string& host); + + const std::string& GetRemoteHost() const + { + return remoteHost_; + } + + void SetRemotePort(uint16_t port); + + uint16_t GetRemotePort() const + { + return remotePort_; + } + + void SetRemoteManufacturer(ModalityManufacturer manufacturer); + + ModalityManufacturer GetRemoteManufacturer() const + { + return manufacturer_; + } + + void ResetPreferredTransferSyntax(); + + void SetPreferredTransferSyntax(const std::string& preferredTransferSyntax); + + const std::string& GetPreferredTransferSyntax() const + { + return preferredTransferSyntax_; + } + + void AddStorageSOPClass(const char* sop); + + void Open(); + + void Close(); + + bool IsOpen() const; + + bool Echo(); + + void Store(const char* buffer, + size_t size, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + + void Store(const char* buffer, + size_t size) + { + Store(buffer, size, "", 0); // Not a C-Move + } + + void Store(const std::string& buffer, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + + void Store(const std::string& buffer) + { + Store(buffer, "", 0); // Not a C-Move + } + + void StoreFile(const std::string& path, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); + + void StoreFile(const std::string& path) + { + StoreFile(path, "", 0); // Not a C-Move + } + + void Find(DicomFindAnswers& result, + ResourceType level, + const DicomMap& fields); + + void Move(const std::string& targetAet, + ResourceType level, + const DicomMap& findResult); + + void Move(const std::string& targetAet, + const DicomMap& findResult); + + void MovePatient(const std::string& targetAet, + const std::string& patientId); + + void MoveStudy(const std::string& targetAet, + const std::string& studyUid); + + void MoveSeries(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid); + + void MoveInstance(const std::string& targetAet, + const std::string& studyUid, + const std::string& seriesUid, + const std::string& instanceUid); + + void SetTimeout(uint32_t seconds); + + void DisableTimeout(); + + void FindWorklist(DicomFindAnswers& result, + ParsedDicomFile& query); + + static void SetDefaultTimeout(uint32_t seconds); + + bool IsSameAssociation(const std::string& localAet, + const RemoteModalityParameters& remote) const; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IApplicationEntityFilter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IApplicationEntityFilter.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,67 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Enumerations.h" + +#include + +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; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IDicomConnectionManager.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IDicomConnectionManager.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING) +# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0 + +namespace Orthanc +{ + // DICOM networking is disabled, this is just a void class + class IDicomConnectionManager : public boost::noncopyable + { + public: + virtual ~IDicomConnectionManager() + { + } + }; +} + +#else + +#include "DicomUserConnection.h" + +namespace Orthanc +{ + class IDicomConnectionManager : public boost::noncopyable + { + public: + virtual ~IDicomConnectionManager() + { + } + + class IResource : public boost::noncopyable + { + public: + virtual ~IResource() + { + } + + virtual DicomUserConnection& GetConnection() = 0; + }; + + virtual IResource* AcquireConnection(const std::string& localAet, + const RemoteModalityParameters& remote) = 0; + }; +} + +#endif diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IFindRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IFindRequestHandler.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,57 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "DicomFindAnswers.h" + +#include + +namespace Orthanc +{ + class IFindRequestHandler : public boost::noncopyable + { + public: + virtual ~IFindRequestHandler() + { + } + + virtual void Handle(DicomFindAnswers& answers, + const DicomMap& input, + const std::list& sequencesToReturn, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, + ModalityManufacturer manufacturer) = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IFindRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IFindRequestHandlerFactory.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindRequestHandler.h" + +namespace Orthanc +{ + class IFindRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IFindRequestHandlerFactory() + { + } + + virtual IFindRequestHandler* ConstructFindRequestHandler() = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IMoveRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IMoveRequestHandler.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,79 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Core/DicomFormat/DicomMap.h" + +#include +#include + + +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; + }; + +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IMoveRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IMoveRequestHandlerFactory.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IMoveRequestHandler.h" + +namespace Orthanc +{ + class IMoveRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IMoveRequestHandlerFactory() + { + } + + virtual IMoveRequestHandler* ConstructMoveRequestHandler() = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IStoreRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStoreRequestHandler.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,58 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Core/DicomFormat/DicomMap.h" + +#include +#include +#include + +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; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IStoreRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStoreRequestHandlerFactory.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IStoreRequestHandler.h" + +namespace Orthanc +{ + class IStoreRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IStoreRequestHandlerFactory() + { + } + + virtual IStoreRequestHandler* ConstructStoreRequestHandler() = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IWorklistRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IWorklistRequestHandler.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/IWorklistRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IWorklistRequestHandlerFactory.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IWorklistRequestHandler.h" + +namespace Orthanc +{ + class IWorklistRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IWorklistRequestHandlerFactory() + { + } + + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/CommandDispatcher.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,934 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + +#include "../../PrecompiledHeaders.h" +#include "CommandDispatcher.h" + +#include "FindScp.h" +#include "StoreScp.h" +#include "MoveScp.h" +#include "../../Toolbox.h" +#include "../../Logging.h" + +#include /* for class DcmAssociationConfiguration */ +#include + +static OFBool opt_rejectWithoutImplementationUID = OFFalse; + + + +static DUL_PRESENTATIONCONTEXT * +findPresentationContextID(LST_HEAD * head, + T_ASC_PresentationContextID presentationContextID) +{ + DUL_PRESENTATIONCONTEXT *pc; + LST_HEAD **l; + OFBool found = OFFalse; + + if (head == NULL) + return NULL; + + l = &head; + if (*l == NULL) + return NULL; + + pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Head(l)); + (void)LST_Position(l, OFstatic_cast(LST_NODE *, pc)); + + while (pc && !found) { + if (pc->presentationContextID == presentationContextID) { + found = OFTrue; + } else { + pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Next(l)); + } + } + return pc; +} + + +/** accept all presenstation contexts for unknown SOP classes, + * i.e. UIDs appearing in the list of abstract syntaxes + * where no corresponding name is defined in the UID dictionary. + * @param params pointer to association parameters structure + * @param transferSyntax transfer syntax to accept + * @param acceptedRole SCU/SCP role to accept + */ +static OFCondition acceptUnknownContextsWithTransferSyntax( + T_ASC_Parameters * params, + const char* transferSyntax, + T_ASC_SC_ROLE acceptedRole) +{ + OFCondition cond = EC_Normal; + int n, i, k; + DUL_PRESENTATIONCONTEXT *dpc; + T_ASC_PresentationContext pc; + OFBool accepted = OFFalse; + OFBool abstractOK = OFFalse; + + n = ASC_countPresentationContexts(params); + for (i = 0; i < n; i++) + { + cond = ASC_getPresentationContext(params, i, &pc); + if (cond.bad()) return cond; + abstractOK = OFFalse; + accepted = OFFalse; + + if (dcmFindNameOfUID(pc.abstractSyntax) == NULL) + { + abstractOK = OFTrue; + + /* check the transfer syntax */ + for (k = 0; (k < OFstatic_cast(int, pc.transferSyntaxCount)) && !accepted; k++) + { + if (strcmp(pc.proposedTransferSyntaxes[k], transferSyntax) == 0) + { + accepted = OFTrue; + } + } + } + + if (accepted) + { + cond = ASC_acceptPresentationContext( + params, pc.presentationContextID, + transferSyntax, acceptedRole); + if (cond.bad()) return cond; + } else { + T_ASC_P_ResultReason reason; + + /* do not refuse if already accepted */ + dpc = findPresentationContextID(params->DULparams.acceptedPresentationContext, + pc.presentationContextID); + if ((dpc == NULL) || ((dpc != NULL) && (dpc->result != ASC_P_ACCEPTANCE))) + { + + if (abstractOK) { + reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED; + } else { + reason = ASC_P_ABSTRACTSYNTAXNOTSUPPORTED; + } + /* + * If previously this presentation context was refused + * because of bad transfer syntax let it stay that way. + */ + if ((dpc != NULL) && (dpc->result == ASC_P_TRANSFERSYNTAXESNOTSUPPORTED)) + reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED; + + cond = ASC_refusePresentationContext(params, pc.presentationContextID, reason); + if (cond.bad()) return cond; + } + } + } + return EC_Normal; +} + + +/** accept all presenstation contexts for unknown SOP classes, + * i.e. UIDs appearing in the list of abstract syntaxes + * where no corresponding name is defined in the UID dictionary. + * This method is passed a list of "preferred" transfer syntaxes. + * @param params pointer to association parameters structure + * @param transferSyntax transfer syntax to accept + * @param acceptedRole SCU/SCP role to accept + */ +static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes( + T_ASC_Parameters * params, + const char* transferSyntaxes[], int transferSyntaxCount, + T_ASC_SC_ROLE acceptedRole = ASC_SC_ROLE_DEFAULT) +{ + OFCondition cond = EC_Normal; + /* + ** Accept in the order "least wanted" to "most wanted" transfer + ** syntax. Accepting a transfer syntax will override previously + ** accepted transfer syntaxes. + */ + for (int i = transferSyntaxCount - 1; i >= 0; i--) + { + cond = acceptUnknownContextsWithTransferSyntax(params, transferSyntaxes[i], acceptedRole); + if (cond.bad()) return cond; + } + return cond; +} + + + +namespace Orthanc +{ + namespace Internals + { + /** + * EXTRACT OF FILE "dcmdata/libsrc/dcuid.cc" FROM DCMTK 3.6.0 + * (dcmAllStorageSOPClassUIDs). + * + * an array of const strings containing all known Storage SOP + * Classes that fit into the conventional + * PATIENT-STUDY-SERIES-INSTANCE information model, + * i.e. everything a Storage SCP might want to store in a PACS. + * Special cases such as hanging protocol storage or the Storage + * SOP Class are not included in this list. + * + * THIS LIST CONTAINS ALL STORAGE SOP CLASSES INCLUDING RETIRED + * ONES AND IS LARGER THAN 64 ENTRIES. + */ + + const char* orthancStorageSOPClassUIDs[] = + { + UID_AmbulatoryECGWaveformStorage, + UID_ArterialPulseWaveformStorage, + UID_AutorefractionMeasurementsStorage, + UID_BasicStructuredDisplayStorage, + UID_BasicTextSRStorage, + UID_BasicVoiceAudioWaveformStorage, + UID_BlendingSoftcopyPresentationStateStorage, + UID_BreastTomosynthesisImageStorage, + UID_CardiacElectrophysiologyWaveformStorage, + UID_ChestCADSRStorage, + UID_ColonCADSRStorage, + UID_ColorSoftcopyPresentationStateStorage, + UID_ComprehensiveSRStorage, + UID_ComputedRadiographyImageStorage, + UID_CTImageStorage, + UID_DeformableSpatialRegistrationStorage, + UID_DigitalIntraOralXRayImageStorageForPresentation, + UID_DigitalIntraOralXRayImageStorageForProcessing, + UID_DigitalMammographyXRayImageStorageForPresentation, + UID_DigitalMammographyXRayImageStorageForProcessing, + UID_DigitalXRayImageStorageForPresentation, + UID_DigitalXRayImageStorageForProcessing, + UID_EncapsulatedCDAStorage, + UID_EncapsulatedPDFStorage, + UID_EnhancedCTImageStorage, + UID_EnhancedMRColorImageStorage, + UID_EnhancedMRImageStorage, + UID_EnhancedPETImageStorage, + UID_EnhancedSRStorage, + UID_EnhancedUSVolumeStorage, + UID_EnhancedXAImageStorage, + UID_EnhancedXRFImageStorage, + UID_GeneralAudioWaveformStorage, + UID_GeneralECGWaveformStorage, + UID_GenericImplantTemplateStorage, + UID_GrayscaleSoftcopyPresentationStateStorage, + UID_HemodynamicWaveformStorage, + UID_ImplantAssemblyTemplateStorage, + UID_ImplantationPlanSRDocumentStorage, + UID_ImplantTemplateGroupStorage, + UID_IntraocularLensCalculationsStorage, + UID_KeratometryMeasurementsStorage, + UID_KeyObjectSelectionDocumentStorage, + UID_LensometryMeasurementsStorage, + UID_MacularGridThicknessAndVolumeReportStorage, + UID_MammographyCADSRStorage, + UID_MRImageStorage, + UID_MRSpectroscopyStorage, + UID_MultiframeGrayscaleByteSecondaryCaptureImageStorage, + UID_MultiframeGrayscaleWordSecondaryCaptureImageStorage, + UID_MultiframeSingleBitSecondaryCaptureImageStorage, + UID_MultiframeTrueColorSecondaryCaptureImageStorage, + UID_NuclearMedicineImageStorage, + UID_OphthalmicAxialMeasurementsStorage, + UID_OphthalmicPhotography16BitImageStorage, + UID_OphthalmicPhotography8BitImageStorage, + UID_OphthalmicTomographyImageStorage, + UID_OphthalmicVisualFieldStaticPerimetryMeasurementsStorage, + UID_PositronEmissionTomographyImageStorage, + UID_ProcedureLogStorage, + UID_PseudoColorSoftcopyPresentationStateStorage, + UID_RawDataStorage, + UID_RealWorldValueMappingStorage, + UID_RespiratoryWaveformStorage, + UID_RTBeamsTreatmentRecordStorage, + UID_RTBrachyTreatmentRecordStorage, + UID_RTDoseStorage, + UID_RTImageStorage, + UID_RTIonBeamsTreatmentRecordStorage, + UID_RTIonPlanStorage, + UID_RTPlanStorage, + UID_RTStructureSetStorage, + UID_RTTreatmentSummaryRecordStorage, + UID_SecondaryCaptureImageStorage, + UID_SegmentationStorage, + UID_SpatialFiducialsStorage, + UID_SpatialRegistrationStorage, + UID_SpectaclePrescriptionReportStorage, + UID_StereometricRelationshipStorage, + UID_SubjectiveRefractionMeasurementsStorage, + UID_SurfaceSegmentationStorage, + UID_TwelveLeadECGWaveformStorage, + UID_UltrasoundImageStorage, + UID_UltrasoundMultiframeImageStorage, + UID_VideoEndoscopicImageStorage, + UID_VideoMicroscopicImageStorage, + UID_VideoPhotographicImageStorage, + UID_VisualAcuityMeasurementsStorage, + UID_VLEndoscopicImageStorage, + UID_VLMicroscopicImageStorage, + UID_VLPhotographicImageStorage, + UID_VLSlideCoordinatesMicroscopicImageStorage, + UID_VLWholeSlideMicroscopyImageStorage, + UID_XAXRFGrayscaleSoftcopyPresentationStateStorage, + UID_XRay3DAngiographicImageStorage, + UID_XRay3DCraniofacialImageStorage, + UID_XRayAngiographicImageStorage, + UID_XRayRadiationDoseSRStorage, + UID_XRayRadiofluoroscopicImageStorage, + // retired + UID_RETIRED_HardcopyColorImageStorage, + UID_RETIRED_HardcopyGrayscaleImageStorage, + UID_RETIRED_NuclearMedicineImageStorage, + UID_RETIRED_StandaloneCurveStorage, + UID_RETIRED_StandaloneModalityLUTStorage, + UID_RETIRED_StandaloneOverlayStorage, + UID_RETIRED_StandalonePETCurveStorage, + UID_RETIRED_StandaloneVOILUTStorage, + UID_RETIRED_StoredPrintStorage, + UID_RETIRED_UltrasoundImageStorage, + UID_RETIRED_UltrasoundMultiframeImageStorage, + UID_RETIRED_VLImageStorage, + UID_RETIRED_VLMultiFrameImageStorage, + UID_RETIRED_XRayAngiographicBiPlaneImageStorage, + // draft + UID_DRAFT_SRAudioStorage, + UID_DRAFT_SRComprehensiveStorage, + UID_DRAFT_SRDetailStorage, + UID_DRAFT_SRTextStorage, + UID_DRAFT_WaveformStorage, + UID_DRAFT_RTBeamsDeliveryInstructionStorage, + NULL + }; + + const int orthancStorageSOPClassUIDsCount = (sizeof(orthancStorageSOPClassUIDs) / sizeof(const char*)) - 1; + + + + OFCondition AssociationCleanup(T_ASC_Association *assoc) + { + OFCondition cond = ASC_dropSCPAssociation(assoc); + if (cond.bad()) + { + LOG(FATAL) << cond.text(); + return cond; + } + + cond = ASC_destroyAssociation(&assoc); + if (cond.bad()) + { + LOG(FATAL) << cond.text(); + return cond; + } + + return cond; + } + + + + CommandDispatcher* AcceptAssociation(const DicomServer& server, T_ASC_Network *net) + { + DcmAssociationConfiguration asccfg; + char buf[BUFSIZ]; + T_ASC_Association *assoc; + OFCondition cond; + OFString sprofile; + OFString temp_str; + + std::vector knownAbstractSyntaxes; + + // For C-STORE + if (server.HasStoreRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_VerificationSOPClass); + } + + // For C-FIND + if (server.HasFindRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); + } + + if (server.HasWorklistRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); + } + + // For C-MOVE + if (server.HasMoveRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); + } + + cond = ASC_receiveAssociation(net, &assoc, + /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, + NULL, NULL, + /*opt_secureConnection*/ OFFalse, + DUL_NOBLOCK, 1); + + if (cond == DUL_NOASSOCIATIONREQUEST) + { + // Timeout + AssociationCleanup(assoc); + return NULL; + } + + // if some kind of error occured, take care of it + if (cond.bad()) + { + LOG(ERROR) << "Receiving Association failed: " << cond.text(); + // no matter what kind of error occurred, we need to do a cleanup + AssociationCleanup(assoc); + return NULL; + } + + // Retrieve the AET and the IP address of the remote modality + std::string remoteAet; + std::string remoteIp; + std::string calledAet; + + { + DIC_AE remoteAet_C; + DIC_AE calledAet_C; + DIC_AE remoteIp_C; + DIC_AE calledIP_C; + if (ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() || + ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()) + { + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_NOREASON + }; + ASC_rejectAssociation(assoc, &rej); + AssociationCleanup(assoc); + return NULL; + } + + remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C)); + remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C)); + calledAet = (/*OFSTRING_GUARD*/(calledAet_C)); + } + + LOG(INFO) << "Association Received from AET " << remoteAet + << " on IP " << remoteIp; + + + std::vector transferSyntaxes; + + // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1 + transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); + + // New transfer syntaxes supported since Orthanc 0.7.2 + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated)) + { + transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg)) + { + transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax); + transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000)) + { + transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax); + transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless)) + { + transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax); + transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) + { + transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); + transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2)) + { + transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); + transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) + { + transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); + } + + /* accept the Verification SOP Class if presented */ + cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), &transferSyntaxes[0], transferSyntaxes.size()); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + + /* the array of Storage SOP Class UIDs comes from dcuid.h */ + cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, orthancStorageSOPClassUIDs, orthancStorageSOPClassUIDsCount, &transferSyntaxes[0], transferSyntaxes.size()); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) + { + /* + * Promiscous mode is enabled: Accept everything not known not + * to be a storage SOP class. + **/ + cond = acceptUnknownContextsWithPreferredTransferSyntaxes( + assoc->params, &transferSyntaxes[0], transferSyntaxes.size()); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + } + + /* set our app title */ + ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str()); + + /* acknowledge or reject this association */ + cond = ASC_getApplicationContextName(assoc->params, buf); + if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0) + { + /* reject: the application context name is not supported */ + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED + }; + + LOG(INFO) << "Association Rejected: Bad Application Context Name: " << buf; + cond = ASC_rejectAssociation(assoc, &rej); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + } + AssociationCleanup(assoc); + return NULL; + } + + /* check the AETs */ + if (!server.IsMyAETitle(calledAet)) + { + LOG(WARNING) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")"; + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED + }; + ASC_rejectAssociation(assoc, &rej); + AssociationCleanup(assoc); + return NULL; + } + + if (server.HasApplicationEntityFilter() && + !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet)) + { + LOG(WARNING) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp; + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED + }; + ASC_rejectAssociation(assoc, &rej); + AssociationCleanup(assoc); + return NULL; + } + + if (opt_rejectWithoutImplementationUID && + strlen(assoc->params->theirImplementationClassUID) == 0) + { + /* reject: the no implementation Class UID provided */ + T_ASC_RejectParameters rej = + { + ASC_RESULT_REJECTEDPERMANENT, + ASC_SOURCE_SERVICEUSER, + ASC_REASON_SU_NOREASON + }; + + LOG(INFO) << "Association Rejected: No Implementation Class UID provided"; + cond = ASC_rejectAssociation(assoc, &rej); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + } + AssociationCleanup(assoc); + return NULL; + } + + { + cond = ASC_acknowledgeAssociation(assoc); + if (cond.bad()) + { + LOG(ERROR) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + LOG(INFO) << "Association Acknowledged (Max Send PDV: " << assoc->sendPDVLength << ")"; + if (ASC_countAcceptedPresentationContexts(assoc->params) == 0) + LOG(INFO) << " (but no valid presentation contexts)"; + } + + IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL; + return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, filter); + } + + + CommandDispatcher::CommandDispatcher(const DicomServer& server, + T_ASC_Association* assoc, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, + IApplicationEntityFilter* filter) : + server_(server), + assoc_(assoc), + remoteIp_(remoteIp), + remoteAet_(remoteAet), + calledAet_(calledAet), + filter_(filter) + { + associationTimeout_ = server.GetAssociationTimeout(); + elapsedTimeSinceLastCommand_ = 0; + } + + + CommandDispatcher::~CommandDispatcher() + { + try + { + AssociationCleanup(assoc_); + } + catch (...) + { + LOG(ERROR) << "Some association was not cleanly aborted"; + } + } + + + bool CommandDispatcher::Step() + /* + * This function receives DIMSE commmands over the network connection + * and handles these commands correspondingly. Note that in case of + * storscp only C-ECHO-RQ and C-STORE-RQ commands can be processed. + */ + { + bool finished = false; + + // receive a DIMSE command over the network, with a timeout of 1 second + DcmDataset *statusDetail = NULL; + T_ASC_PresentationContextID presID = 0; + T_DIMSE_Message msg; + + OFCondition cond = DIMSE_receiveCommand(assoc_, DIMSE_NONBLOCKING, 1, &presID, &msg, &statusDetail); + elapsedTimeSinceLastCommand_++; + + // if the command which was received has extra status + // detail information, dump this information + if (statusDetail != NULL) + { + //LOG4CPP_WARN(Internals::GetLogger(), "Status Detail:" << OFendl << DcmObject::PrintHelper(*statusDetail)); + delete statusDetail; + } + + if (cond == DIMSE_OUTOFRESOURCES) + { + finished = true; + } + else if (cond == DIMSE_NODATAAVAILABLE) + { + // Timeout due to DIMSE_NONBLOCKING + if (associationTimeout_ != 0 && + elapsedTimeSinceLastCommand_ >= associationTimeout_) + { + // This timeout is actually a association timeout + finished = true; + } + } + else if (cond == EC_Normal) + { + // Reset the association timeout counter + elapsedTimeSinceLastCommand_ = 0; + + // Convert the type of request to Orthanc's internal type + bool supported = false; + DicomRequestType request; + switch (msg.CommandField) + { + case DIMSE_C_ECHO_RQ: + request = DicomRequestType_Echo; + supported = true; + break; + + case DIMSE_C_STORE_RQ: + request = DicomRequestType_Store; + supported = true; + break; + + case DIMSE_C_MOVE_RQ: + request = DicomRequestType_Move; + supported = true; + break; + + case DIMSE_C_FIND_RQ: + request = DicomRequestType_Find; + supported = true; + break; + + default: + // we cannot handle this kind of message + cond = DIMSE_BADCOMMANDTYPE; + LOG(ERROR) << "cannot handle command: 0x" << std::hex << msg.CommandField; + break; + } + + + // Check whether this request is allowed by the security filter + if (supported && + filter_ != NULL && + !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request)) + { + LOG(WARNING) << "Rejected " << EnumerationToString(request) + << " request from remote DICOM modality with AET \"" + << remoteAet_ << "\" and hostname \"" << remoteIp_ << "\""; + cond = DIMSE_ILLEGALASSOCIATION; + supported = false; + finished = true; + } + + // in case we received a supported message, process this command + if (supported) + { + // If anything goes wrong, there will be a "BADCOMMANDTYPE" answer + cond = DIMSE_BADCOMMANDTYPE; + + switch (request) + { + case DicomRequestType_Echo: + cond = EchoScp(assoc_, &msg, presID); + break; + + case DicomRequestType_Store: + if (server_.HasStoreRequestHandlerFactory()) // Should always be true + { + std::auto_ptr handler + (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler()); + + if (handler.get() != NULL) + { + cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_); + } + } + break; + + case DicomRequestType_Move: + if (server_.HasMoveRequestHandlerFactory()) // Should always be true + { + std::auto_ptr handler + (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler()); + + if (handler.get() != NULL) + { + cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_); + } + } + break; + + case DicomRequestType_Find: + if (server_.HasFindRequestHandlerFactory() || // Should always be true + server_.HasWorklistRequestHandlerFactory()) + { + std::auto_ptr findHandler; + if (server_.HasFindRequestHandlerFactory()) + { + findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler()); + } + + std::auto_ptr worklistHandler; + if (server_.HasWorklistRequestHandlerFactory()) + { + worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler()); + } + + cond = Internals::findScp(assoc_, &msg, presID, server_.GetRemoteModalities(), + findHandler.get(), worklistHandler.get(), + remoteIp_, remoteAet_, calledAet_); + } + break; + + default: + // Should never happen + break; + } + } + } + else + { + // Bad status, which indicates the closing of the connection by + // the peer or a network error + finished = true; + + LOG(INFO) << cond.text(); + } + + if (finished) + { + if (cond == DUL_PEERREQUESTEDRELEASE) + { + LOG(INFO) << "Association Release"; + ASC_acknowledgeRelease(assoc_); + } + else if (cond == DUL_PEERABORTEDASSOCIATION) + { + LOG(INFO) << "Association Aborted"; + } + else + { + OFString temp_str; + LOG(INFO) << "DIMSE failure (aborting association): " << cond.text(); + /* some kind of error so abort the association */ + ASC_abortAssociation(assoc_); + } + } + + return !finished; + } + + + OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID) + { + OFString temp_str; + LOG(INFO) << "Received Echo Request"; + //LOG(DEBUG) << DIMSE_dumpMessage(temp_str, msg->msg.CEchoRQ, DIMSE_INCOMING, NULL, presID)); + + /* the echo succeeded !! */ + OFCondition cond = DIMSE_sendEchoResponse(assoc, presID, &msg->msg.CEchoRQ, STATUS_Success, NULL); + if (cond.bad()) + { + LOG(ERROR) << "Echo SCP Failed: " << cond.text(); + } + return cond; + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/CommandDispatcher.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,79 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../DicomServer.h" +#include "../../MultiThreading/IRunnableBySteps.h" + +#include + +namespace Orthanc +{ + namespace Internals + { + OFCondition AssociationCleanup(T_ASC_Association *assoc); + + class CommandDispatcher : public IRunnableBySteps + { + private: + uint32_t associationTimeout_; + uint32_t elapsedTimeSinceLastCommand_; + const DicomServer& server_; + T_ASC_Association* assoc_; + std::string remoteIp_; + std::string remoteAet_; + std::string calledAet_; + IApplicationEntityFilter* filter_; + + public: + CommandDispatcher(const DicomServer& server, + T_ASC_Association* assoc, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, + IApplicationEntityFilter* filter); + + virtual ~CommandDispatcher(); + + virtual bool Step(); + }; + + OFCondition EchoScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID); + + CommandDispatcher* AcceptAssociation(const DicomServer& server, + T_ASC_Network *net); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/FindScp.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/FindScp.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,348 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + + +#include "../../PrecompiledHeaders.h" +#include "FindScp.h" + +#include "../../DicomParsing/FromDcmtkBridge.h" +#include "../../DicomParsing/ToDcmtkBridge.h" +#include "../../Logging.h" +#include "../../OrthancException.h" + +#include +#include + + + +/** + * The function below is extracted from DCMTK 3.6.0, cf. file + * "dcmtk-3.6.0/dcmwlm/libsrc/wldsfs.cc". + **/ + +static void HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(DcmDataset *dataset, + const DcmTagKey &sequenceTagKey) +// Date : May 3, 2005 +// Author : Thomas Wilkens +// Task : This function performs a check on a sequence attribute in the given dataset. At two different places +// in the definition of the DICOM worklist management service, a sequence attribute with a return type +// of 2 is mentioned containing two 1C attributes in its item; the condition of the two 1C attributes +// specifies that in case a sequence item is present, then these two attributes must be existent and +// must contain a value. (I am talking about ReferencedStudySequence and ReferencedPatientSequence.) +// In cases where the sequence attribute contains exactly one item with an empty ReferencedSOPClass +// and an empty ReferencedSOPInstance, we want to remove the item from the sequence. This is what +// this function does. +// Parameters : dataset - [in] Dataset in which the consistency of the sequence attribute shall be checked. +// sequenceTagKey - [in] DcmTagKey of the sequence attribute which shall be checked. +// Return Value : none. +{ + DcmElement *sequenceAttribute = NULL, *referencedSOPClassUIDAttribute = NULL, *referencedSOPInstanceUIDAttribute = NULL; + + // in case the sequence attribute contains exactly one item with an empty + // ReferencedSOPClassUID and an empty ReferencedSOPInstanceUID, remove the item + if( dataset->findAndGetElement( sequenceTagKey, sequenceAttribute ).good() && + ( (DcmSequenceOfItems*)sequenceAttribute )->card() == 1 && + ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPClassUID, referencedSOPClassUIDAttribute ).good() && + referencedSOPClassUIDAttribute->getLength() == 0 && + ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPInstanceUID, referencedSOPInstanceUIDAttribute, OFFalse ).good() && + referencedSOPInstanceUIDAttribute->getLength() == 0 ) + { + DcmItem *item = ((DcmSequenceOfItems*)sequenceAttribute)->remove( ((DcmSequenceOfItems*)sequenceAttribute)->getItem(0) ); + delete item; + } +} + + + +namespace Orthanc +{ + namespace + { + struct FindScpData + { + DicomServer::IRemoteModalities* modalities_; + IFindRequestHandler* findHandler_; + IWorklistRequestHandler* worklistHandler_; + DicomFindAnswers answers_; + DcmDataset* lastRequest_; + const std::string* remoteIp_; + const std::string* remoteAet_; + const std::string* calledAet_; + + FindScpData() : answers_(false) + { + } + }; + + + + static void FixWorklistQuery(ParsedDicomFile& query) + { + // TODO: Check out + // WlmDataSourceFileSystem::HandleExistentButEmptyDescriptionAndCodeSequenceAttributes()" + // in DCMTK 3.6.0 + + DcmDataset* dataset = query.GetDcmtkObject().getDataset(); + HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(dataset, DCM_ReferencedStudySequence); + HandleExistentButEmptyReferencedStudyOrPatientSequenceAttributes(dataset, DCM_ReferencedPatientSequence); + } + + + + void FindScpCallback( + /* in */ + void *callbackData, + OFBool cancelled, + T_DIMSE_C_FindRQ *request, + DcmDataset *requestIdentifiers, + int responseCount, + /* out */ + T_DIMSE_C_FindRSP *response, + DcmDataset **responseIdentifiers, + DcmDataset **statusDetail) + { + bzero(response, sizeof(T_DIMSE_C_FindRSP)); + *statusDetail = NULL; + + std::string sopClassUid(request->AffectedSOPClassUID); + + FindScpData& data = *reinterpret_cast(callbackData); + if (data.lastRequest_ == NULL) + { + bool ok = false; + + try + { + RemoteModalityParameters modality; + + /** + * Ensure that the remote modality is known to Orthanc for C-FIND requests. + **/ + + assert(data.modalities_ != NULL); + if (!data.modalities_->LookupAETitle(modality, *data.remoteAet_)) + { + LOG(ERROR) << "Modality with AET \"" << *data.remoteAet_ + << "\" is not defined in the \"DicomModalities\" configuration option"; + throw OrthancException(ErrorCode_UnknownModality); + } + + + if (sopClassUid == UID_FINDModalityWorklistInformationModel) + { + data.answers_.SetWorklist(true); + + if (data.worklistHandler_ != NULL) + { + ParsedDicomFile query(*requestIdentifiers); + FixWorklistQuery(query); + + data.worklistHandler_->Handle(data.answers_, query, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_, modality.GetManufacturer()); + ok = true; + } + else + { + LOG(ERROR) << "No worklist handler is installed, cannot handle this C-FIND request"; + } + } + else + { + data.answers_.SetWorklist(false); + + if (data.findHandler_ != NULL) + { + std::list 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(*element); + if (sequence.card() != 0) + { + LOG(WARNING) << "Orthanc only supports sequence matching on worklists, " + << "ignoring C-FIND SCU constraint on tag (" << tag.Format() + << ") " << FromDcmtkBridge::GetTagName(*element); + } + + sequencesToReturn.push_back(tag); + } + } + + DicomMap input; + FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers); + + data.findHandler_->Handle(data.answers_, input, sequencesToReturn, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_, modality.GetManufacturer()); + ok = true; + } + else + { + LOG(ERROR) << "No C-Find handler is installed, cannot handle this request"; + } + } + } + catch (OrthancException& e) + { + // Internal error! + LOG(ERROR) << "C-FIND request handler has failed: " << e.What(); + } + + if (!ok) + { + response->DimseStatus = STATUS_FIND_Failed_UnableToProcess; + *responseIdentifiers = NULL; + return; + } + + data.lastRequest_ = requestIdentifiers; + } + else if (data.lastRequest_ != requestIdentifiers) + { + // Internal error! + response->DimseStatus = STATUS_FIND_Failed_UnableToProcess; + *responseIdentifiers = NULL; + return; + } + + if (responseCount <= static_cast(data.answers_.GetSize())) + { + // There are pending results that are still to be sent + response->DimseStatus = STATUS_Pending; + *responseIdentifiers = data.answers_.ExtractDcmDataset(responseCount - 1); + } + else if (data.answers_.IsComplete()) + { + // Success: All the results have been sent + response->DimseStatus = STATUS_Success; + *responseIdentifiers = NULL; + } + else + { + // Success, but the results were too numerous and had to be cropped + LOG(WARNING) << "Too many results for an incoming C-FIND query"; + response->DimseStatus = STATUS_FIND_Cancel_MatchingTerminatedDueToCancelRequest; + *responseIdentifiers = NULL; + } + } + } + + + OFCondition Internals::findScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + DicomServer::IRemoteModalities& modalities, + IFindRequestHandler* findHandler, + IWorklistRequestHandler* worklistHandler, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + FindScpData data; + data.modalities_ = &modalities; + data.findHandler_ = findHandler; + data.worklistHandler_ = worklistHandler; + data.lastRequest_ = NULL; + data.remoteIp_ = &remoteIp; + data.remoteAet_ = &remoteAet; + data.calledAet_ = &calledAet; + + OFCondition cond = DIMSE_findProvider(assoc, presID, &msg->msg.CFindRQ, + FindScpCallback, &data, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ 0); + + // if some error occured, dump corresponding information and remove the outfile if necessary + if (cond.bad()) + { + OFString temp_str; + LOG(ERROR) << "Find SCP Failed: " << cond.text(); + } + + return cond; + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/FindScp.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/FindScp.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../DicomServer.h" + +#include + +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); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/MoveScp.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/MoveScp.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,299 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + + +/*========================================================================= + + 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 + +#include "../../DicomParsing/FromDcmtkBridge.h" +#include "../../DicomParsing/ToDcmtkBridge.h" +#include "../../Logging.h" +#include "../../OrthancException.h" + +#include + + +/** + * Macro specifying whether to apply the patch suggested in issue 66: + * "Orthanc responses C-MOVE with zero Move Originator Message ID" + * https://bitbucket.org/sjodogne/orthanc/issues/66/ + **/ + +#define APPLY_FIX_ISSUE_66 1 + + +namespace Orthanc +{ + namespace + { + struct MoveScpData + { + std::string target_; + IMoveRequestHandler* handler_; + DcmDataset* lastRequest_; + unsigned int subOperationCount_; + unsigned int failureCount_; + unsigned int warningCount_; + std::auto_ptr 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(value->GetContent()); + if (tmp >= 0 && tmp <= 0xffff) + { + return static_cast(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(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(data.subOperationCount_)) + { + response->DimseStatus = STATUS_Pending; + } + else + { + response->DimseStatus = STATUS_Success; + } + } + + response->NumberOfRemainingSubOperations = data.subOperationCount_ - responseCount; + response->NumberOfCompletedSubOperations = responseCount; + response->NumberOfFailedSubOperations = data.failureCount_; + response->NumberOfWarningSubOperations = data.warningCount_; + } + } + + + OFCondition Internals::moveScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + IMoveRequestHandler& handler, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + MoveScpData data; + data.target_ = std::string(msg->msg.CMoveRQ.MoveDestination); + data.lastRequest_ = NULL; + data.handler_ = &handler; + data.remoteIp_ = &remoteIp; + data.remoteAet_ = &remoteAet; + data.calledAet_ = &calledAet; + + OFCondition cond = DIMSE_moveProvider(assoc, presID, &msg->msg.CMoveRQ, + MoveScpCallback, &data, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ 0); + + // if some error occured, dump corresponding information and remove the outfile if necessary + if (cond.bad()) + { + OFString temp_str; + LOG(ERROR) << "Move SCP Failed: " << cond.text(); + } + + return cond; + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/MoveScp.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/MoveScp.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,52 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IMoveRequestHandler.h" + +#include + +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); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/StoreScp.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/StoreScp.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,300 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + +#include "../../PrecompiledHeaders.h" +#include "StoreScp.h" + +#include "../../DicomParsing/FromDcmtkBridge.h" +#include "../../DicomParsing/ToDcmtkBridge.h" +#include "../../OrthancException.h" +#include "../../Logging.h" + +#include +#include +#include +#include +#include + + +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 ignoreTagLength; + + FromDcmtkBridge::ExtractDicomSummary(summary, **imageDataSet); + FromDcmtkBridge::ExtractDicomAsJson(dicomJson, **imageDataSet, ignoreTagLength); + + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet)) + { + LOG(ERROR) << "cannot write DICOM file to memory"; + rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources; + } + } + catch (...) + { + rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources; + } + + // check the image to make sure it is consistent, i.e. that its sopClass and sopInstance correspond + // to those mentioned in the request. If not, set the status in the response message variable. + if (rsp->DimseStatus == STATUS_Success) + { + // which SOP class and SOP instance ? + if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse)) + { + //LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName); + rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand; + } + else if (strcmp(sopClass, req->AffectedSOPClassUID) != 0) + { + rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass; + } + else if (strcmp(sopInstance, req->AffectedSOPInstanceUID) != 0) + { + rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass; + } + else + { + try + { + cbdata->handler->Handle(buffer, summary, dicomJson, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET); + } + catch (OrthancException& e) + { + rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources; + + if (e.GetErrorCode() == ErrorCode_InexistentTag) + { + summary.LogMissingTagsForStore(); + } + else + { + LOG(ERROR) << "Exception while storing DICOM: " << e.What(); + } + } + } + } + } + } + } + } + +/* + * This function processes a DIMSE C-STORE-RQ commmand that was + * received over the network connection. + * + * Parameters: + * assoc - [in] The association (network connection to another DICOM application). + * msg - [in] The DIMSE C-STORE-RQ message that was received. + * presID - [in] The ID of the presentation context which was specified in the PDV which contained + * the DIMSE command. + */ + OFCondition Internals::storeScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + IStoreRequestHandler& handler, + const std::string& remoteIp) + { + OFCondition cond = EC_Normal; + T_DIMSE_C_StoreRQ *req; + + // assign the actual information of the C-STORE-RQ command to a local variable + req = &msg->msg.CStoreRQ; + + // intialize some variables + StoreCallbackData data; + data.handler = &handler; + data.remoteIp = &remoteIp; + data.modality = dcmSOPClassUIDToModality(req->AffectedSOPClassUID/*, "UNKNOWN"*/); + if (data.modality == NULL) + data.modality = "UNKNOWN"; + + data.affectedSOPInstanceUID = req->AffectedSOPInstanceUID; + data.messageID = req->MessageID; + if (assoc && assoc->params) + { + data.remoteAET = assoc->params->DULparams.callingAPTitle; + data.calledAET = assoc->params->DULparams.calledAPTitle; + } + else + { + data.remoteAET = ""; + data.calledAET = ""; + } + + DcmFileFormat dcmff; + + // store SourceApplicationEntityTitle in metaheader + if (assoc && assoc->params) + { + const char *aet = assoc->params->DULparams.callingAPTitle; + if (aet) dcmff.getMetaInfo()->putAndInsertString(DCM_SourceApplicationEntityTitle, aet); + } + + // define an address where the information which will be received over the network will be stored + DcmDataset *dset = dcmff.getDataset(); + + cond = DIMSE_storeProvider(assoc, presID, req, NULL, /*opt_useMetaheader*/OFFalse, &dset, + storeScpCallback, &data, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ 0); + + // if some error occured, dump corresponding information and remove the outfile if necessary + if (cond.bad()) + { + OFString temp_str; + LOG(ERROR) << "Store SCP Failed: " << cond.text(); + } + + // return return value + return cond; + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/Internals/StoreScp.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/StoreScp.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,50 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IStoreRequestHandler.h" + +#include + +namespace Orthanc +{ + namespace Internals + { + OFCondition storeScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + IStoreRequestHandler& handler, + const std::string& remoteIp); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/RemoteModalityParameters.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,328 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "RemoteModalityParameters.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include +#include + + +static const char* KEY_AET = "AET"; +static const char* KEY_ALLOW_ECHO = "AllowEcho"; +static const char* KEY_ALLOW_FIND = "AllowFind"; +static const char* KEY_ALLOW_GET = "AllowGet"; +static const char* KEY_ALLOW_MOVE = "AllowMove"; +static const char* KEY_ALLOW_STORE = "AllowStore"; +static const char* KEY_HOST = "Host"; +static const char* KEY_MANUFACTURER = "Manufacturer"; +static const char* KEY_PORT = "Port"; + + +namespace Orthanc +{ + void RemoteModalityParameters::Clear() + { + aet_ = "ORTHANC"; + host_ = "127.0.0.1"; + port_ = 104; + manufacturer_ = ModalityManufacturer_Generic; + allowEcho_ = true; + allowStore_ = true; + allowFind_ = true; + allowMove_ = true; + allowGet_ = true; + } + + + RemoteModalityParameters::RemoteModalityParameters(const std::string& aet, + const std::string& host, + uint16_t port, + ModalityManufacturer manufacturer) + { + Clear(); + SetApplicationEntityTitle(aet); + SetHost(host); + SetPortNumber(port); + SetManufacturer(manufacturer); + } + + + static void CheckPortNumber(int value) + { + if (value <= 0 || + value >= 65535) + { + LOG(ERROR) << "A TCP port number must be in range [1..65534], but found: " << value; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static uint16_t ReadPortNumber(const Json::Value& value) + { + int tmp; + + switch (value.type()) + { + case Json::intValue: + case Json::uintValue: + tmp = value.asInt(); + break; + + case Json::stringValue: + try + { + tmp = boost::lexical_cast(value.asString()); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + break; + + default: + throw OrthancException(ErrorCode_BadFileFormat); + } + + CheckPortNumber(tmp); + return static_cast(tmp); + } + + + void RemoteModalityParameters::SetPortNumber(uint16_t port) + { + CheckPortNumber(port); + port_ = port; + } + + + void RemoteModalityParameters::UnserializeArray(const Json::Value& serialized) + { + assert(serialized.type() == Json::arrayValue); + + if ((serialized.size() != 3 && + serialized.size() != 4) || + serialized[0].type() != Json::stringValue || + serialized[1].type() != Json::stringValue || + (serialized.size() == 4 && + serialized[3].type() != Json::stringValue)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + aet_ = serialized[0].asString(); + host_ = serialized[1].asString(); + port_ = ReadPortNumber(serialized[2]); + + if (serialized.size() == 4) + { + manufacturer_ = StringToModalityManufacturer(serialized[3].asString()); + } + else + { + manufacturer_ = ModalityManufacturer_Generic; + } + } + + + void RemoteModalityParameters::UnserializeObject(const Json::Value& serialized) + { + assert(serialized.type() == Json::objectValue); + + aet_ = SerializationToolbox::ReadString(serialized, KEY_AET); + host_ = SerializationToolbox::ReadString(serialized, KEY_HOST); + + if (serialized.isMember(KEY_PORT)) + { + port_ = ReadPortNumber(serialized[KEY_PORT]); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + if (serialized.isMember(KEY_MANUFACTURER)) + { + manufacturer_ = StringToModalityManufacturer + (SerializationToolbox::ReadString(serialized, KEY_MANUFACTURER)); + } + else + { + manufacturer_ = ModalityManufacturer_Generic; + } + + if (serialized.isMember(KEY_ALLOW_ECHO)) + { + allowEcho_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_ECHO); + } + + if (serialized.isMember(KEY_ALLOW_FIND)) + { + allowFind_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_FIND); + } + + if (serialized.isMember(KEY_ALLOW_STORE)) + { + allowStore_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORE); + } + + if (serialized.isMember(KEY_ALLOW_GET)) + { + allowGet_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_GET); + } + + if (serialized.isMember(KEY_ALLOW_MOVE)) + { + allowMove_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_MOVE); + } + } + + + bool RemoteModalityParameters::IsRequestAllowed(DicomRequestType type) const + { + switch (type) + { + case DicomRequestType_Echo: + return allowEcho_; + + case DicomRequestType_Find: + return allowFind_; + + case DicomRequestType_Get: + return allowGet_; + + case DicomRequestType_Move: + return allowMove_; + + case DicomRequestType_Store: + return allowStore_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void RemoteModalityParameters::SetRequestAllowed(DicomRequestType type, + bool allowed) + { + switch (type) + { + case DicomRequestType_Echo: + allowEcho_ = allowed; + break; + + case DicomRequestType_Find: + allowFind_ = allowed; + break; + + case DicomRequestType_Get: + allowGet_ = allowed; + break; + + case DicomRequestType_Move: + allowMove_ = allowed; + break; + + case DicomRequestType_Store: + allowStore_ = allowed; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool RemoteModalityParameters::IsAdvancedFormatNeeded() const + { + return (!allowEcho_ || + !allowStore_ || + !allowFind_ || + !allowGet_ || + !allowMove_); + } + + + void RemoteModalityParameters::Serialize(Json::Value& target, + bool forceAdvancedFormat) const + { + if (forceAdvancedFormat || + IsAdvancedFormatNeeded()) + { + target = Json::objectValue; + target[KEY_AET] = aet_; + target[KEY_HOST] = host_; + target[KEY_PORT] = port_; + target[KEY_MANUFACTURER] = EnumerationToString(manufacturer_); + target[KEY_ALLOW_ECHO] = allowEcho_; + target[KEY_ALLOW_STORE] = allowStore_; + target[KEY_ALLOW_FIND] = allowFind_; + target[KEY_ALLOW_GET] = allowGet_; + target[KEY_ALLOW_MOVE] = allowMove_; + } + else + { + target = Json::arrayValue; + target.append(GetApplicationEntityTitle()); + target.append(GetHost()); + target.append(GetPortNumber()); + target.append(EnumerationToString(GetManufacturer())); + } + } + + + void RemoteModalityParameters::Unserialize(const Json::Value& serialized) + { + Clear(); + + switch (serialized.type()) + { + case Json::objectValue: + UnserializeObject(serialized); + break; + + case Json::arrayValue: + UnserializeArray(serialized); + break; + + default: + throw OrthancException(ErrorCode_BadFileFormat); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/RemoteModalityParameters.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,133 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Enumerations.h" + +#include +#include +#include + +namespace Orthanc +{ + class RemoteModalityParameters + { + private: + std::string aet_; + std::string host_; + uint16_t port_; + ModalityManufacturer manufacturer_; + bool allowEcho_; + bool allowStore_; + bool allowFind_; + bool allowMove_; + bool allowGet_; + + void Clear(); + + void UnserializeArray(const Json::Value& serialized); + + void UnserializeObject(const Json::Value& serialized); + + public: + RemoteModalityParameters() + { + Clear(); + } + + RemoteModalityParameters(const Json::Value& serialized) + { + Unserialize(serialized); + } + + RemoteModalityParameters(const std::string& aet, + const std::string& host, + uint16_t port, + ModalityManufacturer manufacturer); + + const std::string& GetApplicationEntityTitle() const + { + return aet_; + } + + void SetApplicationEntityTitle(const std::string& aet) + { + aet_ = aet; + } + + const std::string& GetHost() const + { + return host_; + } + + void SetHost(const std::string& host) + { + host_ = host; + } + + uint16_t GetPortNumber() const + { + return port_; + } + + void SetPortNumber(uint16_t port); + + ModalityManufacturer GetManufacturer() const + { + return manufacturer_; + } + + void SetManufacturer(ModalityManufacturer manufacturer) + { + manufacturer_ = manufacturer; + } + + void SetManufacturer(const std::string& manufacturer) + { + manufacturer_ = StringToModalityManufacturer(manufacturer); + } + + bool IsRequestAllowed(DicomRequestType type) const; + + void SetRequestAllowed(DicomRequestType type, + bool allowed); + + void Unserialize(const Json::Value& modality); + + bool IsAdvancedFormatNeeded() const; + + void Serialize(Json::Value& target, + bool forceAdvancedFormat) const; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/TimeoutDicomConnectionManager.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,134 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "TimeoutDicomConnectionManager.h" + +#include "../Logging.h" +#include "../OrthancException.h" + +namespace Orthanc +{ + static boost::posix_time::ptime GetNow() + { + return boost::posix_time::microsec_clock::universal_time(); + } + + class TimeoutDicomConnectionManager::Resource : public IDicomConnectionManager::IResource + { + private: + TimeoutDicomConnectionManager& that_; + + public: + Resource(TimeoutDicomConnectionManager& that) : + that_(that) + { + if (that_.connection_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + ~Resource() + { + that_.Touch(); + } + + DicomUserConnection& GetConnection() + { + assert(that_.connection_.get() != NULL); + return *that_.connection_; + } + }; + + + void TimeoutDicomConnectionManager::Touch() + { + lastUse_ = GetNow(); + } + + + void TimeoutDicomConnectionManager::CheckTimeoutInternal() + { + if (connection_.get() != NULL && + (GetNow() - lastUse_) >= timeout_) + { + Close(); + } + } + + + void TimeoutDicomConnectionManager::SetTimeout(unsigned int timeout) + { + timeout_ = boost::posix_time::milliseconds(timeout); + CheckTimeoutInternal(); + } + + + unsigned int TimeoutDicomConnectionManager::GetTimeout() + { + return timeout_.total_milliseconds(); + } + + + void TimeoutDicomConnectionManager::Close() + { + if (connection_.get() != NULL) + { + LOG(INFO) << "Closing inactive DICOM association with modality: " + << connection_->GetRemoteApplicationEntityTitle(); + + connection_.reset(NULL); + } + } + + + void TimeoutDicomConnectionManager::CheckTimeout() + { + CheckTimeoutInternal(); + } + + + IDicomConnectionManager::IResource* + TimeoutDicomConnectionManager::AcquireConnection(const std::string& localAet, + const RemoteModalityParameters& remote) + { + if (connection_.get() == NULL || + !connection_->IsSameAssociation(localAet, remote)) + { + connection_.reset(new DicomUserConnection(localAet, remote)); + } + + return new Resource(*this); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomNetworking/TimeoutDicomConnectionManager.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,102 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IDicomConnectionManager.h" + +#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0 + +namespace Orthanc +{ + class TimeoutDicomConnectionManager : public IDicomConnectionManager + { + public: + void SetTimeout(unsigned int timeout) + { + } + + unsigned int GetTimeout() + { + return 0; + } + + void Close() + { + } + + void CheckTimeout() + { + } + }; +} + +#else + +#include + +namespace Orthanc +{ + class TimeoutDicomConnectionManager : public IDicomConnectionManager + { + private: + class Resource; + + std::auto_ptr connection_; + boost::posix_time::ptime lastUse_; + boost::posix_time::time_duration timeout_; + + void Touch(); + + void CheckTimeoutInternal(); + + public: + TimeoutDicomConnectionManager() : + timeout_(boost::posix_time::milliseconds(1000)) + { + } + + void SetTimeout(unsigned int timeout); + + unsigned int GetTimeout(); + + void Close(); + + void CheckTimeout(); + + virtual IResource* AcquireConnection(const std::string& localAet, + const RemoteModalityParameters& remote); + }; +} + +#endif diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/DicomDirWriter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DicomDirWriter.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,582 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + +Copyright (C) 1994-2011, OFFIS e.V. +All rights reserved. + +This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=========================================================================*/ + + + +/*** + + Validation: + + # sudo apt-get install dicom3tools + # dciodvfy DICOMDIR 2>&1 | less + # dcentvfy DICOMDIR 2>&1 | less + + http://www.dclunie.com/dicom3tools/dciodvfy.html + + DICOMDIR viewer working with Wine under Linux: + http://www.microdicom.com/ + + ***/ + + +#include "../PrecompiledHeaders.h" +#include "DicomDirWriter.h" + +#include "FromDcmtkBridge.h" +#include "ToDcmtkBridge.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../TemporaryFile.h" +#include "../Toolbox.h" +#include "../SystemToolbox.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "dcmtk/dcmdata/dcvrda.h" /* for class DcmDate */ +#include "dcmtk/dcmdata/dcvrtm.h" /* for class DcmTime */ + +#include + +namespace Orthanc +{ + class DicomDirWriter::PImpl + { + private: + bool utc_; + std::string fileSetId_; + bool extendedSopClass_; + TemporaryFile file_; + std::auto_ptr dir_; + + typedef std::pair IndexKey; + typedef std::map Index; + Index index_; + + + DcmDicomDir& GetDicomDir() + { + if (dir_.get() == NULL) + { + dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), + fileSetId_.c_str())); + //SetTagValue(dir_->getRootRecord(), DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(Encoding_Utf8)); + } + + return *dir_; + } + + + DcmDirectoryRecord& GetRoot() + { + return GetDicomDir().getRootRecord(); + } + + + static bool GetUtf8TagValue(std::string& result, + DcmItem& source, + Encoding encoding, + const DcmTagKey& key) + { + DcmElement* element = NULL; + result.clear(); + + if (source.findAndGetElement(key, element).good()) + { + char* s = NULL; + if (element->isLeaf() && + element->getString(s).good()) + { + if (s != NULL) + { + result = Toolbox::ConvertToUtf8(s, encoding); + } + + return true; + } + } + + return false; + } + + + static void SetTagValue(DcmDirectoryRecord& target, + const DcmTagKey& key, + const std::string& valueUtf8) + { + std::string s = Toolbox::ConvertFromUtf8(valueUtf8, Encoding_Ascii); + + if (!target.putAndInsertString(key, s.c_str()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + + + static bool CopyString(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key, + bool optional, + bool copyEmpty) + { + if (optional && + !source.tagExistsWithValue(key) && + !(copyEmpty && source.tagExists(key))) + { + return false; + } + + std::string value; + bool found = GetUtf8TagValue(value, source, encoding, key); + + if (!found) + { + // We don't raise an exception if "!optional", even if this + // results in an invalid DICOM file + value.clear(); + } + + SetTagValue(target, key, value); + return found; + } + + + static void CopyStringType1(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key) + { + CopyString(target, source, encoding, key, false, false); + } + + static void CopyStringType1C(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key) + { + CopyString(target, source, encoding, key, true, false); + } + + static void CopyStringType2(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key) + { + CopyString(target, source, encoding, key, false, true); + } + + static void CopyStringType3(DcmDirectoryRecord& target, + DcmDataset& source, + Encoding encoding, + const DcmTagKey& key) + { + CopyString(target, source, encoding, key, true, true); + } + + + public: + PImpl() : + utc_(true), // By default, use UTC (universal time, not local time) + fileSetId_("ORTHANC_MEDIA"), + extendedSopClass_(false) + { + } + + bool IsUtcUsed() const + { + return utc_; + } + + + void SetUtcUsed(bool utc) + { + utc_ = utc; + } + + void EnableExtendedSopClass(bool enable) + { + if (enable) + { + LOG(WARNING) << "Generating a DICOMDIR with type 3 attributes, " + << "which leads to an Extended SOP Class"; + } + + extendedSopClass_ = enable; + } + + bool IsExtendedSopClass() const + { + return extendedSopClass_; + } + + void FillPatient(DcmDirectoryRecord& record, + DcmDataset& dicom, + Encoding encoding) + { + // cf. "DicomDirInterface::buildPatientRecord()" + + CopyStringType1C(record, dicom, encoding, DCM_PatientID); + CopyStringType2(record, dicom, encoding, DCM_PatientName); + } + + void FillStudy(DcmDirectoryRecord& record, + DcmDataset& dicom, + Encoding encoding) + { + // cf. "DicomDirInterface::buildStudyRecord()" + + std::string nowDate, nowTime; + SystemToolbox::GetNowDicom(nowDate, nowTime, utc_); + + std::string studyDate; + if (!GetUtf8TagValue(studyDate, dicom, encoding, DCM_StudyDate) && + !GetUtf8TagValue(studyDate, dicom, encoding, DCM_SeriesDate) && + !GetUtf8TagValue(studyDate, dicom, encoding, DCM_AcquisitionDate) && + !GetUtf8TagValue(studyDate, dicom, encoding, DCM_ContentDate)) + { + studyDate = nowDate; + } + + std::string studyTime; + if (!GetUtf8TagValue(studyTime, dicom, encoding, DCM_StudyTime) && + !GetUtf8TagValue(studyTime, dicom, encoding, DCM_SeriesTime) && + !GetUtf8TagValue(studyTime, dicom, encoding, DCM_AcquisitionTime) && + !GetUtf8TagValue(studyTime, dicom, encoding, DCM_ContentTime)) + { + studyTime = nowTime; + } + + /* copy attribute values from dataset to study record */ + SetTagValue(record, DCM_StudyDate, studyDate); + SetTagValue(record, DCM_StudyTime, studyTime); + CopyStringType2(record, dicom, encoding, DCM_StudyDescription); + CopyStringType1(record, dicom, encoding, DCM_StudyInstanceUID); + /* use type 1C instead of 1 in order to avoid unwanted overwriting */ + CopyStringType1C(record, dicom, encoding, DCM_StudyID); + CopyStringType2(record, dicom, encoding, DCM_AccessionNumber); + } + + void FillSeries(DcmDirectoryRecord& record, + DcmDataset& dicom, + Encoding encoding) + { + // cf. "DicomDirInterface::buildSeriesRecord()" + + /* copy attribute values from dataset to series record */ + CopyStringType1(record, dicom, encoding, DCM_Modality); + CopyStringType1(record, dicom, encoding, DCM_SeriesInstanceUID); + /* use type 1C instead of 1 in order to avoid unwanted overwriting */ + CopyStringType1C(record, dicom, encoding, DCM_SeriesNumber); + + // Add extended (non-standard) type 3 tags, those are not generated by DCMTK + // http://dicom.nema.org/medical/Dicom/2016a/output/chtml/part02/sect_7.3.html + // https://groups.google.com/d/msg/orthanc-users/Y7LOvZMDeoc/9cp3kDgxAwAJ + if (extendedSopClass_) + { + CopyStringType3(record, dicom, encoding, DCM_SeriesDescription); + } + } + + void FillInstance(DcmDirectoryRecord& record, + DcmDataset& dicom, + Encoding encoding, + DcmMetaInfo& metaInfo, + const char* path) + { + // cf. "DicomDirInterface::buildImageRecord()" + + /* copy attribute values from dataset to image record */ + CopyStringType1(record, dicom, encoding, DCM_InstanceNumber); + //CopyElementType1C(record, dicom, encoding, DCM_ImageType); + + // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record); + + std::string sopClassUid, sopInstanceUid, transferSyntaxUid; + if (!GetUtf8TagValue(sopClassUid, dicom, encoding, DCM_SOPClassUID) || + !GetUtf8TagValue(sopInstanceUid, dicom, encoding, DCM_SOPInstanceUID) || + !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, DCM_TransferSyntaxUID)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + SetTagValue(record, DCM_ReferencedFileID, path); + SetTagValue(record, DCM_ReferencedSOPClassUIDInFile, sopClassUid); + SetTagValue(record, DCM_ReferencedSOPInstanceUIDInFile, sopInstanceUid); + SetTagValue(record, DCM_ReferencedTransferSyntaxUIDInFile, transferSyntaxUid); + } + + + + bool CreateResource(DcmDirectoryRecord*& target, + ResourceType level, + ParsedDicomFile& dicom, + const char* filename, + const char* path) + { + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + Encoding encoding = dicom.GetEncoding(); + + bool found; + std::string id; + E_DirRecType type; + + switch (level) + { + case ResourceType_Patient: + found = GetUtf8TagValue(id, dataset, encoding, DCM_PatientID); + type = ERT_Patient; + break; + + case ResourceType_Study: + found = GetUtf8TagValue(id, dataset, encoding, DCM_StudyInstanceUID); + type = ERT_Study; + break; + + case ResourceType_Series: + found = GetUtf8TagValue(id, dataset, encoding, DCM_SeriesInstanceUID); + type = ERT_Series; + break; + + case ResourceType_Instance: + found = GetUtf8TagValue(id, dataset, encoding, DCM_SOPInstanceUID); + type = ERT_Image; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (!found) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + IndexKey key = std::make_pair(level, std::string(id.c_str())); + Index::iterator it = index_.find(key); + + if (it != index_.end()) + { + target = it->second; + return false; // Already existing + } + + std::auto_ptr record(new DcmDirectoryRecord(type, NULL, filename)); + + switch (level) + { + case ResourceType_Patient: + FillPatient(*record, dataset, encoding); + break; + + case ResourceType_Study: + FillStudy(*record, dataset, encoding); + break; + + case ResourceType_Series: + FillSeries(*record, dataset, encoding); + break; + + case ResourceType_Instance: + FillInstance(*record, dataset, encoding, *dicom.GetDcmtkObject().getMetaInfo(), path); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + CopyStringType1C(*record, dataset, encoding, DCM_SpecificCharacterSet); + + target = record.get(); + GetRoot().insertSub(record.release()); + index_[key] = target; + + return true; // Newly created + } + + void Read(std::string& s) + { + if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX, + EET_UndefinedLength /*encodingType*/, + EGL_withoutGL /*groupLength*/).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + file_.Read(s); + } + + void SetFileSetId(const std::string& id) + { + dir_.reset(NULL); + fileSetId_ = id; + } + }; + + + DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl) + { + } + + void DicomDirWriter::SetUtcUsed(bool utc) + { + pimpl_->SetUtcUsed(utc); + } + + bool DicomDirWriter::IsUtcUsed() const + { + return pimpl_->IsUtcUsed(); + } + + void DicomDirWriter::SetFileSetId(const std::string& id) + { + pimpl_->SetFileSetId(id); + } + + void DicomDirWriter::Add(const std::string& directory, + const std::string& filename, + ParsedDicomFile& dicom) + { + std::string path; + if (directory.empty()) + { + path = filename; + } + else + { + if (directory[directory.length() - 1] == '/' || + directory[directory.length() - 1] == '\\') + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + path = directory + '\\' + filename; + } + + DcmDirectoryRecord* instance; + bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, dicom, filename.c_str(), path.c_str()); + if (isNewInstance) + { + DcmDirectoryRecord* series; + bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, dicom, filename.c_str(), NULL); + series->insertSub(instance); + + if (isNewSeries) + { + DcmDirectoryRecord* study; + bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, dicom, filename.c_str(), NULL); + study->insertSub(series); + + if (isNewStudy) + { + DcmDirectoryRecord* patient; + pimpl_->CreateResource(patient, ResourceType_Patient, dicom, filename.c_str(), NULL); + patient->insertSub(study); + } + } + } + } + + void DicomDirWriter::Encode(std::string& target) + { + pimpl_->Read(target); + } + + + void DicomDirWriter::EnableExtendedSopClass(bool enable) + { + pimpl_->EnableExtendedSopClass(enable); + } + + + bool DicomDirWriter::IsExtendedSopClass() const + { + return pimpl_->IsExtendedSopClass(); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/DicomDirWriter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DicomDirWriter.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,68 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ParsedDicomFile.h" + +#include + +namespace Orthanc +{ + class DicomDirWriter : public boost::noncopyable + { + private: + class PImpl; + boost::shared_ptr 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; + }; + +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/DicomModification.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DicomModification.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,1415 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomModification.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" +#include "FromDcmtkBridge.h" +#include "ITagVisitor.h" + +#include // For std::auto_ptr + + +static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2008 = + "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1"; + +static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2017c = + "Orthanc " ORTHANC_VERSION " - PS 3.15-2017c Table E.1-1 Basic Profile"; + +namespace Orthanc +{ + class DicomModification::RelationshipsVisitor : public ITagVisitor + { + private: + DicomModification& that_; + + bool IsEnabled(const DicomTag& tag) const + { + return (!that_.IsCleared(tag) && + !that_.IsRemoved(tag) && + !that_.IsReplaced(tag)); + } + + void RemoveIfEnabled(ParsedDicomFile& dicom, + const DicomTag& tag) const + { + if (IsEnabled(tag)) + { + dicom.Remove(tag); + } + } + + + public: + RelationshipsVisitor(DicomModification& that) : + that_(that) + { + } + + virtual void VisitUnknown(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr) + { + } + + virtual void VisitBinary(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const void* data, + size_t size) + { + } + + virtual void VisitInteger(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + int64_t value) + { + } + + virtual void VisitDouble(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + double value) + { + } + + virtual void VisitAttribute(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const DicomTag& value) + { + } + + virtual Action VisitString(std::string& newValue, + const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::string& value) + { + if (!IsEnabled(tag)) + { + return Action_None; + } + else if (tag == DICOM_TAG_FRAME_OF_REFERENCE_UID || + tag == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_UID || + tag == DICOM_TAG_REFERENCED_SOP_INSTANCE_UID || + tag == DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID) + { + newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Instance); + return Action_Replace; + } + else if (parentTags.size() == 1 && + parentTags[0] == DICOM_TAG_CURRENT_REQUESTED_PROCEDURE_EVIDENCE_SEQUENCE && + tag == DICOM_TAG_STUDY_INSTANCE_UID) + { + newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Study); + return Action_Replace; + } + else if (parentTags.size() == 2 && + parentTags[0] == DICOM_TAG_CURRENT_REQUESTED_PROCEDURE_EVIDENCE_SEQUENCE && + parentTags[1] == DICOM_TAG_REFERENCED_SERIES_SEQUENCE && + tag == DICOM_TAG_SERIES_INSTANCE_UID) + { + newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Series); + return Action_Replace; + } + else + { + return Action_None; + } + } + + void RemoveRelationships(ParsedDicomFile& dicom) const + { + // Sequences containing the UID relationships + RemoveIfEnabled(dicom, DICOM_TAG_REFERENCED_IMAGE_SEQUENCE); + RemoveIfEnabled(dicom, DICOM_TAG_SOURCE_IMAGE_SEQUENCE); + + // Individual tags + RemoveIfEnabled(dicom, DICOM_TAG_FRAME_OF_REFERENCE_UID); + + // The tags below should never occur at the first level of the + // hierarchy, but remove them anyway + RemoveIfEnabled(dicom, DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_UID); + RemoveIfEnabled(dicom, DICOM_TAG_REFERENCED_SOP_INSTANCE_UID); + RemoveIfEnabled(dicom, DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID); + } + }; + + + bool DicomModification::CancelReplacement(const DicomTag& tag) + { + Replacements::iterator it = replacements_.find(tag); + + if (it != replacements_.end()) + { + delete it->second; + replacements_.erase(it); + return true; + } + else + { + return false; + } + } + + + void DicomModification::ReplaceInternal(const DicomTag& tag, + const Json::Value& value) + { + Replacements::iterator it = replacements_.find(tag); + + if (it != replacements_.end()) + { + delete it->second; + it->second = NULL; // In the case of an exception during the clone + it->second = new Json::Value(value); // Clone + } + else + { + replacements_[tag] = new Json::Value(value); // Clone + } + } + + + void DicomModification::ClearReplacements() + { + for (Replacements::iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + delete it->second; + } + + replacements_.clear(); + } + + + void DicomModification::MarkNotOrthancAnonymization() + { + Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD); + + if (it != replacements_.end() && + (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 || + it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c)) + { + delete it->second; + replacements_.erase(it); + } + } + + + + std::string DicomModification::MapDicomIdentifier(const std::string& original, + ResourceType level) + { + std::string mapped; + + UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original)); + + if (previous == uidMap_.end()) + { + if (identifierGenerator_ == NULL) + { + mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level); + } + else + { + if (!identifierGenerator_->Apply(mapped, original, level, currentSource_)) + { + LOG(ERROR) << "Unable to generate an anonymized ID"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped)); + } + else + { + mapped = previous->second; + } + + return mapped; + } + + + void DicomModification::MapDicomTags(ParsedDicomFile& dicom, + ResourceType level) + { + std::auto_ptr tag; + + switch (level) + { + case ResourceType_Study: + tag.reset(new DicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + break; + + case ResourceType_Series: + tag.reset(new DicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + break; + + case ResourceType_Instance: + tag.reset(new DicomTag(DICOM_TAG_SOP_INSTANCE_UID)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string original; + if (!dicom.GetTagValue(original, *tag)) + { + original = ""; + } + + std::string mapped = MapDicomIdentifier(Toolbox::StripSpaces(original), level); + + dicom.Replace(*tag, mapped, + false /* don't try and decode data URI scheme for UIDs */, + DicomReplaceMode_InsertIfAbsent); + } + + + DicomModification::DicomModification() : + removePrivateTags_(false), + level_(ResourceType_Instance), + allowManualIdentifiers_(true), + keepStudyInstanceUid_(false), + keepSeriesInstanceUid_(false), + updateReferencedRelationships_(true), + isAnonymization_(false), + identifierGenerator_(NULL) + { + } + + DicomModification::~DicomModification() + { + ClearReplacements(); + } + + void DicomModification::Keep(const DicomTag& tag) + { + bool wasRemoved = IsRemoved(tag); + bool wasCleared = IsCleared(tag); + + removals_.erase(tag); + clearings_.erase(tag); + + bool wasReplaced = CancelReplacement(tag); + + if (tag == DICOM_TAG_STUDY_INSTANCE_UID) + { + keepStudyInstanceUid_ = true; + } + else if (tag == DICOM_TAG_SERIES_INSTANCE_UID) + { + keepSeriesInstanceUid_ = true; + } + else if (tag.IsPrivate()) + { + privateTagsToKeep_.insert(tag); + } + else if (!wasRemoved && + !wasReplaced && + !wasCleared) + { + LOG(WARNING) << "Marking this tag as to be kept has no effect: " << tag.Format(); + } + + MarkNotOrthancAnonymization(); + } + + void DicomModification::Remove(const DicomTag& tag) + { + removals_.insert(tag); + clearings_.erase(tag); + CancelReplacement(tag); + privateTagsToKeep_.erase(tag); + + MarkNotOrthancAnonymization(); + } + + void DicomModification::Clear(const DicomTag& tag) + { + removals_.erase(tag); + clearings_.insert(tag); + CancelReplacement(tag); + privateTagsToKeep_.erase(tag); + + MarkNotOrthancAnonymization(); + } + + bool DicomModification::IsRemoved(const DicomTag& tag) const + { + return removals_.find(tag) != removals_.end(); + } + + bool DicomModification::IsCleared(const DicomTag& tag) const + { + return clearings_.find(tag) != clearings_.end(); + } + + void DicomModification::Replace(const DicomTag& tag, + const Json::Value& value, + bool safeForAnonymization) + { + clearings_.erase(tag); + removals_.erase(tag); + privateTagsToKeep_.erase(tag); + ReplaceInternal(tag, value); + + if (!safeForAnonymization) + { + MarkNotOrthancAnonymization(); + } + } + + + bool DicomModification::IsReplaced(const DicomTag& tag) const + { + return replacements_.find(tag) != replacements_.end(); + } + + const Json::Value& DicomModification::GetReplacement(const DicomTag& tag) const + { + Replacements::const_iterator it = replacements_.find(tag); + + if (it == replacements_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + return *it->second; + } + } + + + std::string DicomModification::GetReplacementAsString(const DicomTag& tag) const + { + const Json::Value& json = GetReplacement(tag); + + if (json.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return json.asString(); + } + } + + + void DicomModification::SetRemovePrivateTags(bool removed) + { + removePrivateTags_ = removed; + + if (!removed) + { + MarkNotOrthancAnonymization(); + } + } + + void DicomModification::SetLevel(ResourceType level) + { + uidMap_.clear(); + level_ = level; + + if (level != ResourceType_Patient) + { + MarkNotOrthancAnonymization(); + } + } + + + void DicomModification::SetupAnonymization2008() + { + // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles + // https://raw.githubusercontent.com/jodogne/dicom-specification/master/2008/08_15pu.pdf + + removals_.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID + //removals_.insert(DicomTag(0x0008, 0x0018)); // SOP Instance UID => set in Apply() + removals_.insert(DicomTag(0x0008, 0x0050)); // Accession Number + removals_.insert(DicomTag(0x0008, 0x0080)); // Institution Name + removals_.insert(DicomTag(0x0008, 0x0081)); // Institution Address + removals_.insert(DicomTag(0x0008, 0x0090)); // Referring Physician's Name + removals_.insert(DicomTag(0x0008, 0x0092)); // Referring Physician's Address + removals_.insert(DicomTag(0x0008, 0x0094)); // Referring Physician's Telephone Numbers + removals_.insert(DicomTag(0x0008, 0x1010)); // Station Name + removals_.insert(DicomTag(0x0008, 0x1030)); // Study Description + removals_.insert(DicomTag(0x0008, 0x103e)); // Series Description + removals_.insert(DicomTag(0x0008, 0x1040)); // Institutional Department Name + removals_.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of Record + removals_.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name + removals_.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study + removals_.insert(DicomTag(0x0008, 0x1070)); // Operators' Name + removals_.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description + //removals_.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID => RelationshipsVisitor + removals_.insert(DicomTag(0x0008, 0x2111)); // Derivation Description + //removals_.insert(DicomTag(0x0010, 0x0010)); // Patient's Name => cf. below (*) + //removals_.insert(DicomTag(0x0010, 0x0020)); // Patient ID => cf. below (*) + removals_.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date + removals_.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time + removals_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex + removals_.insert(DicomTag(0x0010, 0x1000)); // Other Patient Ids + removals_.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names + removals_.insert(DicomTag(0x0010, 0x1010)); // Patient's Age + removals_.insert(DicomTag(0x0010, 0x1020)); // Patient's Size + removals_.insert(DicomTag(0x0010, 0x1030)); // Patient's Weight + removals_.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator + removals_.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group + removals_.insert(DicomTag(0x0010, 0x2180)); // Occupation + removals_.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History + removals_.insert(DicomTag(0x0010, 0x4000)); // Patient Comments + removals_.insert(DicomTag(0x0018, 0x1000)); // Device Serial Number + removals_.insert(DicomTag(0x0018, 0x1030)); // Protocol Name + //removals_.insert(DicomTag(0x0020, 0x000d)); // Study Instance UID => set in Apply() + //removals_.insert(DicomTag(0x0020, 0x000e)); // Series Instance UID => set in Apply() + removals_.insert(DicomTag(0x0020, 0x0010)); // Study ID + //removals_.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID => cf. RelationshipsVisitor + removals_.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID + removals_.insert(DicomTag(0x0020, 0x4000)); // Image Comments + removals_.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence + removals_.insert(DicomTag(0x0040, 0xa124)); // UID + removals_.insert(DicomTag(0x0040, 0xa730)); // Content Sequence + removals_.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID + //removals_.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID => RelationshipsVisitor + //removals_.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID => RelationshipsVisitor + + // Some more removals (from the experience of DICOM files at the CHU of Liege) + removals_.insert(DicomTag(0x0010, 0x1040)); // Patient's Address + removals_.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician + removals_.insert(DicomTag(0x0010, 0x2154)); // PatientTelephoneNumbers + removals_.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts + + // Set the DeidentificationMethod tag + ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2008); + } + + + void DicomModification::SetupAnonymization2017c() + { + /** + * This is Table E.1-1 from PS 3.15-2017c (DICOM Part 15: Security + * and System Management Profiles), "basic profile" column. It was + * generated automatically with the + * "../Resources/GenerateAnonymizationProfile.py" script. + * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2017c/part15.pdf + **/ + + // TODO: (50xx,xxxx) with rule X // Curve Data + // TODO: (60xx,3000) with rule X // Overlay Data + // TODO: (60xx,4000) with rule X // Overlay Comments + // Tag (0x0008, 0x0018) is set in Apply() /* U */ // SOP Instance UID + // Tag (0x0008, 0x1140) => RelationshipsVisitor /* X/Z/U* */ // Referenced Image Sequence + // Tag (0x0008, 0x1155) => RelationshipsVisitor /* U */ // Referenced SOP Instance UID + // Tag (0x0008, 0x2112) => RelationshipsVisitor /* X/Z/U* */ // Source Image Sequence + // Tag (0x0010, 0x0010) is set below (*) /* Z */ // Patient's Name + // Tag (0x0010, 0x0020) is set below (*) /* Z */ // Patient ID + // Tag (0x0020, 0x000d) is set in Apply() /* U */ // Study Instance UID + // Tag (0x0020, 0x000e) is set in Apply() /* U */ // Series Instance UID + // Tag (0x0020, 0x0052) => RelationshipsVisitor /* U */ // Frame of Reference UID + // Tag (0x3006, 0x0024) => RelationshipsVisitor /* U */ // Referenced Frame of Reference UID + // Tag (0x3006, 0x00c2) => RelationshipsVisitor /* U */ // Related Frame of Reference UID + clearings_.insert(DicomTag(0x0008, 0x0020)); // Study Date + clearings_.insert(DicomTag(0x0008, 0x0023)); /* Z/D */ // Content Date + clearings_.insert(DicomTag(0x0008, 0x0030)); // Study Time + clearings_.insert(DicomTag(0x0008, 0x0033)); /* Z/D */ // Content Time + clearings_.insert(DicomTag(0x0008, 0x0050)); // Accession Number + clearings_.insert(DicomTag(0x0008, 0x0090)); // Referring Physician's Name + clearings_.insert(DicomTag(0x0008, 0x009c)); // Consulting Physician's Name + clearings_.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date + clearings_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex + clearings_.insert(DicomTag(0x0018, 0x0010)); /* Z/D */ // Contrast Bolus Agent + clearings_.insert(DicomTag(0x0020, 0x0010)); // Study ID + clearings_.insert(DicomTag(0x0040, 0x1101)); /* D */ // Person Identification Code Sequence + clearings_.insert(DicomTag(0x0040, 0x2016)); // Placer Order Number / Imaging Service Request + clearings_.insert(DicomTag(0x0040, 0x2017)); // Filler Order Number / Imaging Service Request + clearings_.insert(DicomTag(0x0040, 0xa073)); /* D */ // Verifying Observer Sequence + clearings_.insert(DicomTag(0x0040, 0xa075)); /* D */ // Verifying Observer Name + clearings_.insert(DicomTag(0x0040, 0xa088)); // Verifying Observer Identification Code Sequence + clearings_.insert(DicomTag(0x0040, 0xa123)); /* D */ // Person Name + clearings_.insert(DicomTag(0x0070, 0x0001)); /* D */ // Graphic Annotation Sequence + clearings_.insert(DicomTag(0x0070, 0x0084)); // Content Creator's Name + removals_.insert(DicomTag(0x0000, 0x1000)); // Affected SOP Instance UID + removals_.insert(DicomTag(0x0000, 0x1001)); /* TODO UID */ // Requested SOP Instance UID + removals_.insert(DicomTag(0x0002, 0x0003)); /* TODO UID */ // Media Storage SOP Instance UID + removals_.insert(DicomTag(0x0004, 0x1511)); /* TODO UID */ // Referenced SOP Instance UID in File + removals_.insert(DicomTag(0x0008, 0x0014)); /* TODO UID */ // Instance Creator UID + removals_.insert(DicomTag(0x0008, 0x0015)); // Instance Coercion DateTime + removals_.insert(DicomTag(0x0008, 0x0021)); /* X/D */ // Series Date + removals_.insert(DicomTag(0x0008, 0x0022)); /* X/Z */ // Acquisition Date + removals_.insert(DicomTag(0x0008, 0x0024)); // Overlay Date + removals_.insert(DicomTag(0x0008, 0x0025)); // Curve Date + removals_.insert(DicomTag(0x0008, 0x002a)); /* X/D */ // Acquisition DateTime + removals_.insert(DicomTag(0x0008, 0x0031)); /* X/D */ // Series Time + removals_.insert(DicomTag(0x0008, 0x0032)); /* X/Z */ // Acquisition Time + removals_.insert(DicomTag(0x0008, 0x0034)); // Overlay Time + removals_.insert(DicomTag(0x0008, 0x0035)); // Curve Time + removals_.insert(DicomTag(0x0008, 0x0058)); /* TODO UID */ // Failed SOP Instance UID List + removals_.insert(DicomTag(0x0008, 0x0080)); /* X/Z/D */ // Institution Name + removals_.insert(DicomTag(0x0008, 0x0081)); // Institution Address + removals_.insert(DicomTag(0x0008, 0x0082)); /* X/Z/D */ // Institution Code Sequence + removals_.insert(DicomTag(0x0008, 0x0092)); // Referring Physician's Address + removals_.insert(DicomTag(0x0008, 0x0094)); // Referring Physician's Telephone Numbers + removals_.insert(DicomTag(0x0008, 0x0096)); // Referring Physician Identification Sequence + removals_.insert(DicomTag(0x0008, 0x009d)); // Consulting Physician Identification Sequence + removals_.insert(DicomTag(0x0008, 0x0201)); // Timezone Offset From UTC + removals_.insert(DicomTag(0x0008, 0x1010)); /* X/Z/D */ // Station Name + removals_.insert(DicomTag(0x0008, 0x1030)); // Study Description + removals_.insert(DicomTag(0x0008, 0x103e)); // Series Description + removals_.insert(DicomTag(0x0008, 0x1040)); // Institutional Department Name + removals_.insert(DicomTag(0x0008, 0x1048)); // Physician(s) of Record + removals_.insert(DicomTag(0x0008, 0x1049)); // Physician(s) of Record Identification Sequence + removals_.insert(DicomTag(0x0008, 0x1050)); // Performing Physicians' Name + removals_.insert(DicomTag(0x0008, 0x1052)); // Performing Physician Identification Sequence + removals_.insert(DicomTag(0x0008, 0x1060)); // Name of Physician(s) Reading Study + removals_.insert(DicomTag(0x0008, 0x1062)); // Physician(s) Reading Study Identification Sequence + removals_.insert(DicomTag(0x0008, 0x1070)); /* X/Z/D */ // Operators' Name + removals_.insert(DicomTag(0x0008, 0x1072)); /* X/D */ // Operators' Identification Sequence + removals_.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description + removals_.insert(DicomTag(0x0008, 0x1084)); // Admitting Diagnoses Code Sequence + removals_.insert(DicomTag(0x0008, 0x1110)); /* X/Z */ // Referenced Study Sequence + removals_.insert(DicomTag(0x0008, 0x1111)); /* X/Z/D */ // Referenced Performed Procedure Step Sequence + removals_.insert(DicomTag(0x0008, 0x1120)); // Referenced Patient Sequence + removals_.insert(DicomTag(0x0008, 0x1195)); /* TODO UID */ // Transaction UID + removals_.insert(DicomTag(0x0008, 0x2111)); // Derivation Description + removals_.insert(DicomTag(0x0008, 0x3010)); /* TODO UID */ // Irradiation Event UID + removals_.insert(DicomTag(0x0008, 0x4000)); // Identifying Comments + removals_.insert(DicomTag(0x0010, 0x0021)); // Issuer of Patient ID + removals_.insert(DicomTag(0x0010, 0x0032)); // Patient's Birth Time + removals_.insert(DicomTag(0x0010, 0x0050)); // Patient's Insurance Plan Code Sequence + removals_.insert(DicomTag(0x0010, 0x0101)); // Patient's Primary Language Code Sequence + removals_.insert(DicomTag(0x0010, 0x0102)); // Patient's Primary Language Modifier Code Sequence + removals_.insert(DicomTag(0x0010, 0x1000)); // Other Patient IDs + removals_.insert(DicomTag(0x0010, 0x1001)); // Other Patient Names + removals_.insert(DicomTag(0x0010, 0x1002)); // Other Patient IDs Sequence + removals_.insert(DicomTag(0x0010, 0x1005)); // Patient's Birth Name + removals_.insert(DicomTag(0x0010, 0x1010)); // Patient's Age + removals_.insert(DicomTag(0x0010, 0x1020)); // Patient's Size + removals_.insert(DicomTag(0x0010, 0x1030)); // Patient's Weight + removals_.insert(DicomTag(0x0010, 0x1040)); // Patient Address + removals_.insert(DicomTag(0x0010, 0x1050)); // Insurance Plan Identification + removals_.insert(DicomTag(0x0010, 0x1060)); // Patient's Mother's Birth Name + removals_.insert(DicomTag(0x0010, 0x1080)); // Military Rank + removals_.insert(DicomTag(0x0010, 0x1081)); // Branch of Service + removals_.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator + removals_.insert(DicomTag(0x0010, 0x1100)); // Referenced Patient Photo Sequence + removals_.insert(DicomTag(0x0010, 0x2000)); // Medical Alerts + removals_.insert(DicomTag(0x0010, 0x2110)); // Allergies + removals_.insert(DicomTag(0x0010, 0x2150)); // Country of Residence + removals_.insert(DicomTag(0x0010, 0x2152)); // Region of Residence + removals_.insert(DicomTag(0x0010, 0x2154)); // Patient's Telephone Numbers + removals_.insert(DicomTag(0x0010, 0x2155)); // Patient's Telecom Information + removals_.insert(DicomTag(0x0010, 0x2160)); // Ethnic Group + removals_.insert(DicomTag(0x0010, 0x2180)); // Occupation + removals_.insert(DicomTag(0x0010, 0x21a0)); // Smoking Status + removals_.insert(DicomTag(0x0010, 0x21b0)); // Additional Patient's History + removals_.insert(DicomTag(0x0010, 0x21c0)); // Pregnancy Status + removals_.insert(DicomTag(0x0010, 0x21d0)); // Last Menstrual Date + removals_.insert(DicomTag(0x0010, 0x21f0)); // Patient's Religious Preference + removals_.insert(DicomTag(0x0010, 0x2203)); /* X/Z */ // Patient Sex Neutered + removals_.insert(DicomTag(0x0010, 0x2297)); // Responsible Person + removals_.insert(DicomTag(0x0010, 0x2299)); // Responsible Organization + removals_.insert(DicomTag(0x0010, 0x4000)); // Patient Comments + removals_.insert(DicomTag(0x0018, 0x1000)); /* X/Z/D */ // Device Serial Number + removals_.insert(DicomTag(0x0018, 0x1002)); /* TODO UID */ // Device UID + removals_.insert(DicomTag(0x0018, 0x1004)); // Plate ID + removals_.insert(DicomTag(0x0018, 0x1005)); // Generator ID + removals_.insert(DicomTag(0x0018, 0x1007)); // Cassette ID + removals_.insert(DicomTag(0x0018, 0x1008)); // Gantry ID + removals_.insert(DicomTag(0x0018, 0x1030)); /* X/D */ // Protocol Name + removals_.insert(DicomTag(0x0018, 0x1400)); /* X/D */ // Acquisition Device Processing Description + removals_.insert(DicomTag(0x0018, 0x2042)); /* TODO UID */ // Target UID + removals_.insert(DicomTag(0x0018, 0x4000)); // Acquisition Comments + removals_.insert(DicomTag(0x0018, 0x700a)); /* X/D */ // Detector ID + removals_.insert(DicomTag(0x0018, 0x9424)); // Acquisition Protocol Description + removals_.insert(DicomTag(0x0018, 0x9516)); /* X/D */ // Start Acquisition DateTime + removals_.insert(DicomTag(0x0018, 0x9517)); /* X/D */ // End Acquisition DateTime + removals_.insert(DicomTag(0x0018, 0xa003)); // Contribution Description + removals_.insert(DicomTag(0x0020, 0x0200)); /* TODO UID */ // Synchronization Frame of Reference UID + removals_.insert(DicomTag(0x0020, 0x3401)); // Modifying Device ID + removals_.insert(DicomTag(0x0020, 0x3404)); // Modifying Device Manufacturer + removals_.insert(DicomTag(0x0020, 0x3406)); // Modified Image Description + removals_.insert(DicomTag(0x0020, 0x4000)); // Image Comments + removals_.insert(DicomTag(0x0020, 0x9158)); // Frame Comments + removals_.insert(DicomTag(0x0020, 0x9161)); /* TODO UID */ // Concatenation UID + removals_.insert(DicomTag(0x0020, 0x9164)); /* TODO UID */ // Dimension Organization UID + removals_.insert(DicomTag(0x0028, 0x1199)); /* TODO UID */ // Palette Color Lookup Table UID + removals_.insert(DicomTag(0x0028, 0x1214)); /* TODO UID */ // Large Palette Color Lookup Table UID + removals_.insert(DicomTag(0x0028, 0x4000)); // Image Presentation Comments + removals_.insert(DicomTag(0x0032, 0x0012)); // Study ID Issuer + removals_.insert(DicomTag(0x0032, 0x1020)); // Scheduled Study Location + removals_.insert(DicomTag(0x0032, 0x1021)); // Scheduled Study Location AE Title + removals_.insert(DicomTag(0x0032, 0x1030)); // Reason for Study + removals_.insert(DicomTag(0x0032, 0x1032)); // Requesting Physician + removals_.insert(DicomTag(0x0032, 0x1033)); // Requesting Service + removals_.insert(DicomTag(0x0032, 0x1060)); /* X/Z */ // Requested Procedure Description + removals_.insert(DicomTag(0x0032, 0x1070)); // Requested Contrast Agent + removals_.insert(DicomTag(0x0032, 0x4000)); // Study Comments + removals_.insert(DicomTag(0x0038, 0x0004)); // Referenced Patient Alias Sequence + removals_.insert(DicomTag(0x0038, 0x0010)); // Admission ID + removals_.insert(DicomTag(0x0038, 0x0011)); // Issuer of Admission ID + removals_.insert(DicomTag(0x0038, 0x001e)); // Scheduled Patient Institution Residence + removals_.insert(DicomTag(0x0038, 0x0020)); // Admitting Date + removals_.insert(DicomTag(0x0038, 0x0021)); // Admitting Time + removals_.insert(DicomTag(0x0038, 0x0040)); // Discharge Diagnosis Description + removals_.insert(DicomTag(0x0038, 0x0050)); // Special Needs + removals_.insert(DicomTag(0x0038, 0x0060)); // Service Episode ID + removals_.insert(DicomTag(0x0038, 0x0061)); // Issuer of Service Episode ID + removals_.insert(DicomTag(0x0038, 0x0062)); // Service Episode Description + removals_.insert(DicomTag(0x0038, 0x0300)); // Current Patient Location + removals_.insert(DicomTag(0x0038, 0x0400)); // Patient's Institution Residence + removals_.insert(DicomTag(0x0038, 0x0500)); // Patient State + removals_.insert(DicomTag(0x0038, 0x4000)); // Visit Comments + removals_.insert(DicomTag(0x0040, 0x0001)); // Scheduled Station AE Title + removals_.insert(DicomTag(0x0040, 0x0002)); // Scheduled Procedure Step Start Date + removals_.insert(DicomTag(0x0040, 0x0003)); // Scheduled Procedure Step Start Time + removals_.insert(DicomTag(0x0040, 0x0004)); // Scheduled Procedure Step End Date + removals_.insert(DicomTag(0x0040, 0x0005)); // Scheduled Procedure Step End Time + removals_.insert(DicomTag(0x0040, 0x0006)); // Scheduled Performing Physician Name + removals_.insert(DicomTag(0x0040, 0x0007)); // Scheduled Procedure Step Description + removals_.insert(DicomTag(0x0040, 0x000b)); // Scheduled Performing Physician Identification Sequence + removals_.insert(DicomTag(0x0040, 0x0010)); // Scheduled Station Name + removals_.insert(DicomTag(0x0040, 0x0011)); // Scheduled Procedure Step Location + removals_.insert(DicomTag(0x0040, 0x0012)); // Pre-Medication + removals_.insert(DicomTag(0x0040, 0x0241)); // Performed Station AE Title + removals_.insert(DicomTag(0x0040, 0x0242)); // Performed Station Name + removals_.insert(DicomTag(0x0040, 0x0243)); // Performed Location + removals_.insert(DicomTag(0x0040, 0x0244)); // Performed Procedure Step Start Date + removals_.insert(DicomTag(0x0040, 0x0245)); // Performed Procedure Step Start Time + removals_.insert(DicomTag(0x0040, 0x0250)); // Performed Procedure Step End Date + removals_.insert(DicomTag(0x0040, 0x0251)); // Performed Procedure Step End Time + removals_.insert(DicomTag(0x0040, 0x0253)); // Performed Procedure Step ID + removals_.insert(DicomTag(0x0040, 0x0254)); // Performed Procedure Step Description + removals_.insert(DicomTag(0x0040, 0x0275)); // Request Attributes Sequence + removals_.insert(DicomTag(0x0040, 0x0280)); // Comments on the Performed Procedure Step + removals_.insert(DicomTag(0x0040, 0x0555)); // Acquisition Context Sequence + removals_.insert(DicomTag(0x0040, 0x1001)); // Requested Procedure ID + removals_.insert(DicomTag(0x0040, 0x1004)); // Patient Transport Arrangements + removals_.insert(DicomTag(0x0040, 0x1005)); // Requested Procedure Location + removals_.insert(DicomTag(0x0040, 0x1010)); // Names of Intended Recipient of Results + removals_.insert(DicomTag(0x0040, 0x1011)); // Intended Recipients of Results Identification Sequence + removals_.insert(DicomTag(0x0040, 0x1102)); // Person Address + removals_.insert(DicomTag(0x0040, 0x1103)); // Person's Telephone Numbers + removals_.insert(DicomTag(0x0040, 0x1104)); // Person's Telecom Information + removals_.insert(DicomTag(0x0040, 0x1400)); // Requested Procedure Comments + removals_.insert(DicomTag(0x0040, 0x2001)); // Reason for the Imaging Service Request + removals_.insert(DicomTag(0x0040, 0x2008)); // Order Entered By + removals_.insert(DicomTag(0x0040, 0x2009)); // Order Enterer Location + removals_.insert(DicomTag(0x0040, 0x2010)); // Order Callback Phone Number + removals_.insert(DicomTag(0x0040, 0x2011)); // Order Callback Telecom Information + removals_.insert(DicomTag(0x0040, 0x2400)); // Imaging Service Request Comments + removals_.insert(DicomTag(0x0040, 0x3001)); // Confidentiality Constraint on Patient Data Description + removals_.insert(DicomTag(0x0040, 0x4005)); // Scheduled Procedure Step Start DateTime + removals_.insert(DicomTag(0x0040, 0x4010)); // Scheduled Procedure Step Modification DateTime + removals_.insert(DicomTag(0x0040, 0x4011)); // Expected Completion DateTime + removals_.insert(DicomTag(0x0040, 0x4023)); /* TODO UID */ // Referenced General Purpose Scheduled Procedure Step Transaction UID + removals_.insert(DicomTag(0x0040, 0x4025)); // Scheduled Station Name Code Sequence + removals_.insert(DicomTag(0x0040, 0x4027)); // Scheduled Station Geographic Location Code Sequence + removals_.insert(DicomTag(0x0040, 0x4028)); // Performed Station Name Code Sequence + removals_.insert(DicomTag(0x0040, 0x4030)); // Performed Station Geographic Location Code Sequence + removals_.insert(DicomTag(0x0040, 0x4034)); // Scheduled Human Performers Sequence + removals_.insert(DicomTag(0x0040, 0x4035)); // Actual Human Performers Sequence + removals_.insert(DicomTag(0x0040, 0x4036)); // Human Performers Organization + removals_.insert(DicomTag(0x0040, 0x4037)); // Human Performers Name + removals_.insert(DicomTag(0x0040, 0x4050)); // Performed Procedure Step Start DateTime + removals_.insert(DicomTag(0x0040, 0x4051)); // Performed Procedure Step End DateTime + removals_.insert(DicomTag(0x0040, 0x4052)); // Procedure Step Cancellation DateTime + removals_.insert(DicomTag(0x0040, 0xa027)); // Verifying Organization + removals_.insert(DicomTag(0x0040, 0xa078)); // Author Observer Sequence + removals_.insert(DicomTag(0x0040, 0xa07a)); // Participant Sequence + removals_.insert(DicomTag(0x0040, 0xa07c)); // Custodial Organization Sequence + removals_.insert(DicomTag(0x0040, 0xa124)); /* TODO UID */ // UID + removals_.insert(DicomTag(0x0040, 0xa171)); /* TODO UID */ // Observation UID + removals_.insert(DicomTag(0x0040, 0xa172)); /* TODO UID */ // Referenced Observation UID (Trial) + removals_.insert(DicomTag(0x0040, 0xa192)); // Observation Date (Trial) + removals_.insert(DicomTag(0x0040, 0xa193)); // Observation Time (Trial) + removals_.insert(DicomTag(0x0040, 0xa307)); // Current Observer (Trial) + removals_.insert(DicomTag(0x0040, 0xa352)); // Verbal Source (Trial) + removals_.insert(DicomTag(0x0040, 0xa353)); // Address (Trial) + removals_.insert(DicomTag(0x0040, 0xa354)); // Telephone Number (Trial) + removals_.insert(DicomTag(0x0040, 0xa358)); // Verbal Source Identifier Code Sequence (Trial) + removals_.insert(DicomTag(0x0040, 0xa402)); /* TODO UID */ // Observation Subject UID (Trial) + removals_.insert(DicomTag(0x0040, 0xa730)); // Content Sequence + removals_.insert(DicomTag(0x0040, 0xdb0c)); /* TODO UID */ // Template Extension Organization UID + removals_.insert(DicomTag(0x0040, 0xdb0d)); /* TODO UID */ // Template Extension Creator UID + removals_.insert(DicomTag(0x0062, 0x0021)); /* TODO UID */ // Tracking UID + removals_.insert(DicomTag(0x0070, 0x0086)); // Content Creator's Identification Code Sequence + removals_.insert(DicomTag(0x0070, 0x031a)); /* TODO UID */ // Fiducial UID + removals_.insert(DicomTag(0x0070, 0x1101)); /* TODO UID */ // Presentation Display Collection UID + removals_.insert(DicomTag(0x0070, 0x1102)); /* TODO UID */ // Presentation Sequence Collection UID + removals_.insert(DicomTag(0x0088, 0x0140)); /* TODO UID */ // Storage Media File-set UID + removals_.insert(DicomTag(0x0088, 0x0200)); // Icon Image Sequence(see Note 12) + removals_.insert(DicomTag(0x0088, 0x0904)); // Topic Title + removals_.insert(DicomTag(0x0088, 0x0906)); // Topic Subject + removals_.insert(DicomTag(0x0088, 0x0910)); // Topic Author + removals_.insert(DicomTag(0x0088, 0x0912)); // Topic Keywords + removals_.insert(DicomTag(0x0400, 0x0100)); // Digital Signature UID + removals_.insert(DicomTag(0x0400, 0x0402)); // Referenced Digital Signature Sequence + removals_.insert(DicomTag(0x0400, 0x0403)); // Referenced SOP Instance MAC Sequence + removals_.insert(DicomTag(0x0400, 0x0404)); // MAC + removals_.insert(DicomTag(0x0400, 0x0550)); // Modified Attributes Sequence + removals_.insert(DicomTag(0x0400, 0x0561)); // Original Attributes Sequence + removals_.insert(DicomTag(0x2030, 0x0020)); // Text String + removals_.insert(DicomTag(0x3008, 0x0105)); // Source Serial Number + removals_.insert(DicomTag(0x300a, 0x0013)); /* TODO UID */ // Dose Reference UID + removals_.insert(DicomTag(0x300c, 0x0113)); // Reason for Omission Description + removals_.insert(DicomTag(0x300e, 0x0008)); /* X/Z */ // Reviewer Name + removals_.insert(DicomTag(0x4000, 0x0010)); // Arbitrary + removals_.insert(DicomTag(0x4000, 0x4000)); // Text Comments + removals_.insert(DicomTag(0x4008, 0x0042)); // Results ID Issuer + removals_.insert(DicomTag(0x4008, 0x0102)); // Interpretation Recorder + removals_.insert(DicomTag(0x4008, 0x010a)); // Interpretation Transcriber + removals_.insert(DicomTag(0x4008, 0x010b)); // Interpretation Text + removals_.insert(DicomTag(0x4008, 0x010c)); // Interpretation Author + removals_.insert(DicomTag(0x4008, 0x0111)); // Interpretation Approver Sequence + removals_.insert(DicomTag(0x4008, 0x0114)); // Physician Approving Interpretation + removals_.insert(DicomTag(0x4008, 0x0115)); // Interpretation Diagnosis Description + removals_.insert(DicomTag(0x4008, 0x0118)); // Results Distribution List Sequence + removals_.insert(DicomTag(0x4008, 0x0119)); // Distribution Name + removals_.insert(DicomTag(0x4008, 0x011a)); // Distribution Address + removals_.insert(DicomTag(0x4008, 0x0202)); // Interpretation ID Issuer + removals_.insert(DicomTag(0x4008, 0x0300)); // Impressions + removals_.insert(DicomTag(0x4008, 0x4000)); // Results Comments + removals_.insert(DicomTag(0xfffa, 0xfffa)); // Digital Signatures Sequence + removals_.insert(DicomTag(0xfffc, 0xfffc)); // Data Set Trailing Padding + + // Set the DeidentificationMethod tag + ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2017c); + } + + + void DicomModification::SetupAnonymization(DicomVersion version) + { + isAnonymization_ = true; + + removals_.clear(); + clearings_.clear(); + ClearReplacements(); + removePrivateTags_ = true; + level_ = ResourceType_Patient; + uidMap_.clear(); + privateTagsToKeep_.clear(); + + switch (version) + { + case DicomVersion_2008: + SetupAnonymization2008(); + break; + + case DicomVersion_2017c: + SetupAnonymization2017c(); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Set the PatientIdentityRemoved tag + ReplaceInternal(DicomTag(0x0012, 0x0062), "YES"); + + // (*) Choose a random patient name and ID + std::string patientId = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient); + ReplaceInternal(DICOM_TAG_PATIENT_ID, patientId); + ReplaceInternal(DICOM_TAG_PATIENT_NAME, patientId); + } + + void DicomModification::Apply(ParsedDicomFile& toModify) + { + // Check the request + assert(ResourceType_Patient + 1 == ResourceType_Study && + ResourceType_Study + 1 == ResourceType_Series && + ResourceType_Series + 1 == ResourceType_Instance); + + if (IsRemoved(DICOM_TAG_PATIENT_ID) || + IsRemoved(DICOM_TAG_STUDY_INSTANCE_UID) || + IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) || + IsRemoved(DICOM_TAG_SOP_INSTANCE_UID)) + { + throw OrthancException(ErrorCode_BadRequest); + } + + + // Sanity checks at the patient level + if (level_ == ResourceType_Patient && !IsReplaced(DICOM_TAG_PATIENT_ID)) + { + LOG(ERROR) << "When modifying a patient, her PatientID is required to be modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (!allowManualIdentifiers_) + { + if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a patient, the StudyInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a patient, the SeriesInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a patient, the SopInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + + // Sanity checks at the study level + if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID)) + { + LOG(ERROR) << "When modifying a study, the parent PatientID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (!allowManualIdentifiers_) + { + if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a study, the SeriesInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a study, the SopInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + + // Sanity checks at the series level + if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID)) + { + LOG(ERROR) << "When modifying a series, the parent PatientID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a series, the parent StudyInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (!allowManualIdentifiers_) + { + if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying a series, the SopInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + } + + + // Sanity checks at the instance level + if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID)) + { + LOG(ERROR) << "When modifying an instance, the parent PatientID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying an instance, the parent StudyInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + LOG(ERROR) << "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified"; + throw OrthancException(ErrorCode_BadRequest); + } + + + // (0) Create a summary of the source file, if a custom generator + // is provided + if (identifierGenerator_ != NULL) + { + toModify.ExtractDicomSummary(currentSource_); + } + + + // (1) Remove the private tags, if need be + if (removePrivateTags_) + { + toModify.RemovePrivateTags(privateTagsToKeep_); + } + + // (2) Clear the tags specified by the user + for (SetOfTags::const_iterator it = clearings_.begin(); + it != clearings_.end(); ++it) + { + toModify.Clear(*it, true /* only clear if the tag exists in the original file */); + } + + // (3) Remove the tags specified by the user + for (SetOfTags::const_iterator it = removals_.begin(); + it != removals_.end(); ++it) + { + toModify.Remove(*it); + } + + // (4) Replace the tags + for (Replacements::const_iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + toModify.Replace(it->first, *it->second, true /* decode data URI scheme */, DicomReplaceMode_InsertIfAbsent); + } + + // (5) Update the DICOM identifiers + if (level_ <= ResourceType_Study && + !IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) + { + if (keepStudyInstanceUid_) + { + LOG(WARNING) << "Modifying a study while keeping its original StudyInstanceUID: This should be avoided!"; + } + else + { + MapDicomTags(toModify, ResourceType_Study); + } + } + + if (level_ <= ResourceType_Series && + !IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) + { + if (keepSeriesInstanceUid_) + { + LOG(WARNING) << "Modifying a series while keeping its original SeriesInstanceUID: This should be avoided!"; + } + else + { + MapDicomTags(toModify, ResourceType_Series); + } + } + + if (level_ <= ResourceType_Instance && // Always true + !IsReplaced(DICOM_TAG_SOP_INSTANCE_UID)) + { + MapDicomTags(toModify, ResourceType_Instance); + } + + // (6) Update the "referenced" relationships in the case of an anonymization + if (isAnonymization_) + { + RelationshipsVisitor visitor(*this); + + if (updateReferencedRelationships_) + { + toModify.Apply(visitor); + } + else + { + visitor.RemoveRelationships(toModify); + } + } + } + + + static bool IsDatabaseKey(const DicomTag& tag) + { + return (tag == DICOM_TAG_PATIENT_ID || + tag == DICOM_TAG_STUDY_INSTANCE_UID || + tag == DICOM_TAG_SERIES_INSTANCE_UID || + tag == DICOM_TAG_SOP_INSTANCE_UID); + } + + + static void ParseListOfTags(DicomModification& target, + const Json::Value& query, + DicomModification::TagOperation operation, + bool force) + { + if (!query.isArray()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + for (Json::Value::ArrayIndex i = 0; i < query.size(); i++) + { + if (query[i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest); + } + + std::string name = query[i].asString(); + + DicomTag tag = FromDcmtkBridge::ParseTag(name); + + if (!force && IsDatabaseKey(tag)) + { + LOG(ERROR) << "Marking tag \"" << name << "\" as to be " + << (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") + << " requires the \"Force\" option to be set to true"; + throw OrthancException(ErrorCode_BadRequest); + } + + switch (operation) + { + case DicomModification::TagOperation_Keep: + target.Keep(tag); + VLOG(1) << "Keep: " << name << " " << tag; + break; + + case DicomModification::TagOperation_Remove: + target.Remove(tag); + VLOG(1) << "Remove: " << name << " " << tag; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + static void ParseReplacements(DicomModification& target, + const Json::Value& replacements, + bool force) + { + if (!replacements.isObject()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + Json::Value::Members members = replacements.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + const Json::Value& value = replacements[name]; + + DicomTag tag = FromDcmtkBridge::ParseTag(name); + + if (!force && IsDatabaseKey(tag)) + { + LOG(ERROR) << "Marking tag \"" << name << "\" as to be replaced " + << "requires the \"Force\" option to be set to true"; + throw OrthancException(ErrorCode_BadRequest); + } + + target.Replace(tag, value, false); + + VLOG(1) << "Replace: " << name << " " << tag + << " == " << value.toStyledString(); + } + } + + + static bool GetBooleanValue(const std::string& member, + const Json::Value& json, + bool defaultValue) + { + if (!json.isMember(member)) + { + return defaultValue; + } + else if (json[member].type() == Json::booleanValue) + { + return json[member].asBool(); + } + else + { + LOG(ERROR) << "Member \"" << member << "\" should be a Boolean value"; + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void DicomModification::ParseModifyRequest(const Json::Value& request) + { + if (!request.isObject()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + bool force = GetBooleanValue("Force", request, false); + + if (GetBooleanValue("RemovePrivateTags", request, false)) + { + SetRemovePrivateTags(true); + } + + if (request.isMember("Remove")) + { + ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force); + } + + if (request.isMember("Replace")) + { + ParseReplacements(*this, request["Replace"], force); + } + + // The "Keep" operation only makes sense for the tags + // StudyInstanceUID, SeriesInstanceUID and SOPInstanceUID. Avoid + // this feature as much as possible, as this breaks the DICOM + // model of the real world, except if you know exactly what + // you're doing! + if (request.isMember("Keep")) + { + ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force); + } + } + + + void DicomModification::ParseAnonymizationRequest(bool& patientNameReplaced, + const Json::Value& request) + { + if (!request.isObject()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + bool force = GetBooleanValue("Force", request, false); + + // As of Orthanc 1.3.0, the default anonymization is done + // according to PS 3.15-2017c Table E.1-1 (basic profile) + DicomVersion version = DicomVersion_2017c; + if (request.isMember("DicomVersion")) + { + if (request["DicomVersion"].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + version = StringToDicomVersion(request["DicomVersion"].asString()); + } + } + + SetupAnonymization(version); + + std::string patientName = GetReplacementAsString(DICOM_TAG_PATIENT_NAME); + + if (GetBooleanValue("KeepPrivateTags", request, false)) + { + SetRemovePrivateTags(false); + } + + if (request.isMember("Remove")) + { + ParseListOfTags(*this, request["Remove"], TagOperation_Remove, force); + } + + if (request.isMember("Replace")) + { + ParseReplacements(*this, request["Replace"], force); + } + + if (request.isMember("Keep")) + { + ParseListOfTags(*this, request["Keep"], TagOperation_Keep, force); + } + + patientNameReplaced = (IsReplaced(DICOM_TAG_PATIENT_NAME) && + GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName); + } + + + + + static const char* REMOVE_PRIVATE_TAGS = "RemovePrivateTags"; + static const char* LEVEL = "Level"; + static const char* ALLOW_MANUAL_IDENTIFIERS = "AllowManualIdentifiers"; + static const char* KEEP_STUDY_INSTANCE_UID = "KeepStudyInstanceUID"; + static const char* KEEP_SERIES_INSTANCE_UID = "KeepSeriesInstanceUID"; + static const char* UPDATE_REFERENCED_RELATIONSHIPS = "UpdateReferencedRelationships"; + static const char* IS_ANONYMIZATION = "IsAnonymization"; + static const char* REMOVALS = "Removals"; + static const char* CLEARINGS = "Clearings"; + static const char* PRIVATE_TAGS_TO_KEEP = "PrivateTagsToKeep"; + static const char* REPLACEMENTS = "Replacements"; + static const char* MAP_PATIENTS = "MapPatients"; + static const char* MAP_STUDIES = "MapStudies"; + static const char* MAP_SERIES = "MapSeries"; + static const char* MAP_INSTANCES = "MapInstances"; + + void DicomModification::Serialize(Json::Value& value) const + { + if (identifierGenerator_ != NULL) + { + LOG(ERROR) << "Cannot serialize a DicomModification with a custom identifier generator"; + throw OrthancException(ErrorCode_InternalError); + } + + value = Json::objectValue; + value[REMOVE_PRIVATE_TAGS] = removePrivateTags_; + value[LEVEL] = EnumerationToString(level_); + value[ALLOW_MANUAL_IDENTIFIERS] = allowManualIdentifiers_; + value[KEEP_STUDY_INSTANCE_UID] = keepStudyInstanceUid_; + value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_; + value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_; + value[IS_ANONYMIZATION] = isAnonymization_; + + SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS); + SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS); + SerializationToolbox::WriteSetOfTags(value, privateTagsToKeep_, PRIVATE_TAGS_TO_KEEP); + + Json::Value& tmp = value[REPLACEMENTS]; + + tmp = Json::objectValue; + + for (Replacements::const_iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + assert(it->second != NULL); + tmp[it->first.Format()] = *it->second; + } + + Json::Value& mapPatients = value[MAP_PATIENTS]; + Json::Value& mapStudies = value[MAP_STUDIES]; + Json::Value& mapSeries = value[MAP_SERIES]; + Json::Value& mapInstances = value[MAP_INSTANCES]; + + mapPatients = Json::objectValue; + mapStudies = Json::objectValue; + mapSeries = Json::objectValue; + mapInstances = Json::objectValue; + + for (UidMap::const_iterator it = uidMap_.begin(); it != uidMap_.end(); ++it) + { + Json::Value* tmp = NULL; + + switch (it->first.first) + { + case ResourceType_Patient: + tmp = &mapPatients; + break; + + case ResourceType_Study: + tmp = &mapStudies; + break; + + case ResourceType_Series: + tmp = &mapSeries; + break; + + case ResourceType_Instance: + tmp = &mapInstances; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + assert(tmp != NULL); + (*tmp) [it->first.second] = it->second; + } + } + + + void DicomModification::UnserializeUidMap(ResourceType level, + const Json::Value& serialized, + const char* field) + { + if (!serialized.isMember(field) || + serialized[field].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value::Members names = serialized[field].getMemberNames(); + + for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it) + { + const Json::Value& value = serialized[field][*it]; + + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + uidMap_[std::make_pair(level, *it)] = value.asString(); + } + } + } + + + DicomModification::DicomModification(const Json::Value& serialized) : + identifierGenerator_(NULL) + { + removePrivateTags_ = SerializationToolbox::ReadBoolean(serialized, REMOVE_PRIVATE_TAGS); + level_ = StringToResourceType(SerializationToolbox::ReadString(serialized, LEVEL).c_str()); + allowManualIdentifiers_ = SerializationToolbox::ReadBoolean(serialized, ALLOW_MANUAL_IDENTIFIERS); + keepStudyInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_STUDY_INSTANCE_UID); + keepSeriesInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SERIES_INSTANCE_UID); + updateReferencedRelationships_ = SerializationToolbox::ReadBoolean + (serialized, UPDATE_REFERENCED_RELATIONSHIPS); + isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION); + + SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS); + SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS); + SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP); + + if (!serialized.isMember(REPLACEMENTS) || + serialized[REPLACEMENTS].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value::Members names = serialized[REPLACEMENTS].getMemberNames(); + + for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it) + { + DicomTag tag(0, 0); + if (!DicomTag::ParseHexadecimal(tag, it->c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + const Json::Value& value = serialized[REPLACEMENTS][*it]; + replacements_.insert(std::make_pair(tag, new Json::Value(value))); + } + } + + UnserializeUidMap(ResourceType_Patient, serialized, MAP_PATIENTS); + UnserializeUidMap(ResourceType_Study, serialized, MAP_STUDIES); + UnserializeUidMap(ResourceType_Series, serialized, MAP_SERIES); + UnserializeUidMap(ResourceType_Instance, serialized, MAP_INSTANCES); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/DicomModification.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DicomModification.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,185 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 SetOfTags; + typedef std::map Replacements; + typedef std::map< std::pair, std::string> UidMap; + + SetOfTags removals_; + SetOfTags clearings_; + Replacements replacements_; + bool removePrivateTags_; + ResourceType level_; + UidMap uidMap_; + SetOfTags privateTagsToKeep_; + bool allowManualIdentifiers_; + bool keepStudyInstanceUid_; + bool keepSeriesInstanceUid_; + bool updateReferencedRelationships_; + bool isAnonymization_; + DicomMap currentSource_; + + IDicomIdentifierGenerator* identifierGenerator_; + + std::string MapDicomIdentifier(const std::string& original, + ResourceType level); + + void MapDicomTags(ParsedDicomFile& dicom, + ResourceType level); + + void MarkNotOrthancAnonymization(); + + void ClearReplacements(); + + bool CancelReplacement(const DicomTag& tag); + + void ReplaceInternal(const DicomTag& tag, + const Json::Value& value); + + void SetupAnonymization2008(); + + void SetupAnonymization2017c(); + + void UnserializeUidMap(ResourceType level, + const Json::Value& serialized, + const char* field); + + public: + DicomModification(); + + DicomModification(const Json::Value& serialized); + + ~DicomModification(); + + void Keep(const DicomTag& tag); + + void Remove(const DicomTag& tag); + + // Replace the DICOM tag as a NULL/empty value (e.g. for anonymization) + void Clear(const DicomTag& tag); + + bool IsRemoved(const DicomTag& tag) const; + + bool IsCleared(const DicomTag& tag) const; + + // "safeForAnonymization" tells Orthanc that this replacement does + // not break the anonymization process it implements (for internal use only) + void Replace(const DicomTag& tag, + const Json::Value& value, // Encoded using UTF-8 + bool safeForAnonymization); + + bool IsReplaced(const DicomTag& tag) const; + + const Json::Value& GetReplacement(const DicomTag& tag) const; + + std::string GetReplacementAsString(const DicomTag& tag) const; + + void SetRemovePrivateTags(bool removed); + + bool ArePrivateTagsRemoved() const + { + return removePrivateTags_; + } + + void SetLevel(ResourceType level); + + ResourceType GetLevel() const + { + return level_; + } + + void SetupAnonymization(DicomVersion version); + + void Apply(ParsedDicomFile& toModify); + + void SetAllowManualIdentifiers(bool check) + { + allowManualIdentifiers_ = check; + } + + bool AreAllowManualIdentifiers() const + { + return allowManualIdentifiers_; + } + + void ParseModifyRequest(const Json::Value& request); + + void ParseAnonymizationRequest(bool& patientNameReplaced, + const Json::Value& request); + + void SetDicomIdentifierGenerator(IDicomIdentifierGenerator& generator) + { + identifierGenerator_ = &generator; + } + + void Serialize(Json::Value& value) const; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/FromDcmtkBridge.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,2434 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#include "FromDcmtkBridge.h" +#include "ToDcmtkBridge.h" +#include "../Logging.h" +#include "../Toolbox.h" +#include "../OrthancException.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../TemporaryFile.h" +#endif + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 +# include +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 +# include +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 +# include +#endif + + +namespace Orthanc +{ +#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 + static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary, + EmbeddedResources::FileResourceId resource) + { + std::string content; + EmbeddedResources::GetFileResource(content, resource); + +#if ORTHANC_SANDBOXED == 0 + TemporaryFile tmp; + tmp.Write(content); + + if (!dictionary.loadDictionary(tmp.GetPath().c_str())) + { + LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " + << "your TEMP directory does not contain special characters."; + throw OrthancException(ErrorCode_InternalError); + } +#else + if (!dictionary.loadFromMemory(content)) + { + LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " + << "your TEMP directory does not contain special characters."; + throw OrthancException(ErrorCode_InternalError); + } +#endif + } + +#else + static void LoadExternalDictionary(DcmDataDictionary& dictionary, + const std::string& directory, + const std::string& filename) + { + boost::filesystem::path p = directory; + p = p / filename; + + LOG(WARNING) << "Loading the external DICOM dictionary " << p; + + if (!dictionary.loadDictionary(p.string().c_str())) + { + throw OrthancException(ErrorCode_InternalError); + } + } +#endif + + + namespace + { + class DictionaryLocker + { + private: + DcmDataDictionary& dictionary_; + + public: + DictionaryLocker() : dictionary_(dcmDataDict.wrlock()) + { + } + + ~DictionaryLocker() + { + dcmDataDict.unlock(); + } + + DcmDataDictionary& operator*() + { + return dictionary_; + } + + DcmDataDictionary* operator->() + { + return &dictionary_; + } + }; + + +#define DCMTK_TO_CTYPE_CONVERTER(converter, cType, dcmtkType, getter) \ + \ + struct converter \ + { \ + typedef cType CType; \ + \ + static bool Apply(CType& result, \ + DcmElement& element, \ + size_t i) \ + { \ + return dynamic_cast(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 + 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 strings; + for (size_t i = 0; i < count; i++) { + if (f.Apply(value, element, i)) { + strings.push_back(boost::lexical_cast(value)); + } + } + return new DicomValue(boost::algorithm::join(strings, "\\"), false); + } + else if (f.Apply(value, element, 0)) { + return new DicomValue(boost::lexical_cast(value), false); + } + else { + return new DicomValue; + } + } + + } + + + void FromDcmtkBridge::InitializeDictionary(bool loadPrivateDictionary) + { + LOG(INFO) << "Using DCTMK version: " << DCMTK_VERSION_NUMBER; + + { + DictionaryLocker locker; + + locker->clear(); + +#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 + LOG(INFO) << "Loading the embedded dictionaries"; + /** + * Do not load DICONDE dictionary, it breaks the other tags. The + * command "strace storescu 2>&1 |grep dic" shows that DICONDE + * dictionary is not loaded by storescu. + **/ + //LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICONDE); + + LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_DICOM); + + if (loadPrivateDictionary) + { + LOG(INFO) << "Loading the embedded dictionary of private tags"; + LoadEmbeddedDictionary(*locker, EmbeddedResources::DICTIONARY_PRIVATE); + } + else + { + LOG(INFO) << "The dictionary of private tags has not been loaded"; + } + +#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) + std::string path = DCMTK_DICTIONARY_DIR; + + const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE); + if (env != NULL) + { + path = std::string(env); + } + + LoadExternalDictionary(*locker, path, "dicom.dic"); + + if (loadPrivateDictionary) + { + LoadExternalDictionary(*locker, path, "private.dic"); + } + else + { + LOG(INFO) << "The dictionary of private tags has not been loaded"; + } + +#else +#error Support your platform here +#endif + } + + /* make sure data dictionary is loaded */ + if (!dcmDataDict.isDictionaryLoaded()) + { + LOG(ERROR) << "No DICOM dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE; + throw OrthancException(ErrorCode_InternalError); + } + + { + // Test the dictionary with a simple DICOM tag + DcmTag key(0x0010, 0x1030); // This is PatientWeight + if (key.getEVR() != EVR_DS) + { + LOG(ERROR) << "The DICOM dictionary has not been correctly read"; + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + void FromDcmtkBridge::RegisterDictionaryTag(const DicomTag& tag, + ValueRepresentation vr, + const std::string& name, + unsigned int minMultiplicity, + unsigned int maxMultiplicity, + const std::string& privateCreator) + { + if (minMultiplicity < 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + bool arbitrary = false; + if (maxMultiplicity == 0) + { + maxMultiplicity = DcmVariableVM; + arbitrary = true; + } + else if (maxMultiplicity < minMultiplicity) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + DcmEVR evr = ToDcmtkBridge::Convert(vr); + + LOG(INFO) << "Registering tag in dictionary: " << tag << " " << (DcmVR(evr).getValidVRName()) << " " + << name << " (multiplicity: " << minMultiplicity << "-" + << (arbitrary ? "n" : boost::lexical_cast(maxMultiplicity)) << ")"; + + std::auto_ptr 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(minMultiplicity), + static_cast(maxMultiplicity), + NULL /* version */, + OFTrue /* doCopyString */, + NULL /* private creator */)); + } + else + { + // "Private Data Elements have an odd Group Number that is not + // (0001,eeee), (0003,eeee), (0005,eeee), (0007,eeee), or + // (FFFF,eeee)." + if (tag.GetGroup() % 2 == 0 /* even */ || + tag.GetGroup() == 0x0001 || + tag.GetGroup() == 0x0003 || + tag.GetGroup() == 0x0005 || + tag.GetGroup() == 0x0007 || + tag.GetGroup() == 0xffff) + { + char buf[128]; + sprintf(buf, "Trying to register private tag (%04x,%04x), but it must have an odd group >= 0x0009", + tag.GetGroup(), tag.GetElement()); + LOG(ERROR) << buf; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + entry.reset(new DcmDictEntry(tag.GetGroup(), + tag.GetElement(), + evr, name.c_str(), + static_cast(minMultiplicity), + static_cast(maxMultiplicity), + "private" /* version */, + OFTrue /* doCopyString */, + privateCreator.c_str())); + } + + entry->setGroupRangeRestriction(DcmDictRange_Unspecified); + entry->setElementRangeRestriction(DcmDictRange_Unspecified); + + { + DictionaryLocker locker; + + if (locker->findEntry(name.c_str())) + { + LOG(ERROR) << "Cannot register two tags with the same symbolic name \"" << name << "\""; + throw OrthancException(ErrorCode_AlreadyExistingTag); + } + + locker->addEntry(entry.release()); + } + } + + + Encoding FromDcmtkBridge::DetectEncoding(DcmItem& dataset, + Encoding defaultEncoding) + { + Encoding encoding = defaultEncoding; + + OFString tmp; + if (dataset.findAndGetOFString(DCM_SpecificCharacterSet, tmp).good()) + { + std::string characterSet = Toolbox::StripSpaces(std::string(tmp.c_str())); + + if (characterSet.empty()) + { + // Empty specific character set tag: Use the default encoding + } + else if (GetDicomEncoding(encoding, characterSet.c_str())) + { + // The specific character set is supported by the Orthanc core + } + else + { + LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet + << ", fallback to ASCII (remove all special characters)"; + encoding = Encoding_Ascii; + } + } + else + { + // No specific character set tag: Use the default encoding + } + + return encoding; + } + + + void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, + DcmItem& dataset, + unsigned int maxStringLength, + Encoding defaultEncoding) + { + std::set ignoreTagLength; + + Encoding encoding = DetectEncoding(dataset, defaultEncoding); + + target.Clear(); + for (unsigned long i = 0; i < dataset.card(); i++) + { + DcmElement* element = dataset.getElement(i); + if (element && element->isLeaf()) + { + target.SetValue(element->getTag().getGTag(), + element->getTag().getETag(), + ConvertLeafElement(*element, DicomToJsonFlags_Default, + maxStringLength, encoding, ignoreTagLength)); + } + } + } + + + DicomTag FromDcmtkBridge::Convert(const DcmTag& tag) + { + return DicomTag(tag.getGTag(), tag.getETag()); + } + + + DicomTag FromDcmtkBridge::GetTag(const DcmElement& element) + { + return DicomTag(element.getGTag(), element.getETag()); + } + + + DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding, + const std::set& ignoreTagLength) + { + if (!element.isLeaf()) + { + // This function is only applicable to leaf elements + throw OrthancException(ErrorCode_BadParameterType); + } + + char *c = NULL; + if (element.isaString() && + element.getString(c).good()) + { + if (c == NULL) // This case corresponds to the empty string + { + return new DicomValue("", false); + } + else + { + std::string s(c); + std::string utf8 = Toolbox::ConvertToUtf8(s, encoding); + + if (maxStringLength != 0 && + utf8.size() > maxStringLength && + ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end()) + { + return new DicomValue; // Too long, create a NULL value + } + else + { + return new DicomValue(utf8, false); + } + } + } + + + if (element.getVR() == EVR_UN) + { + // Unknown value representation: Lookup in the dictionary. This + // is notably the case for private tags registered with the + // "Dictionary" configuration option. + DictionaryLocker locker; + + const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), + element.getTag().getPrivateCreator()); + if (entry != NULL && + entry->getVR().isaString()) + { + Uint8* data = NULL; + + // At (*), we do not try and convert to UTF-8, as nothing says + // the encoding of the private tag is the same as that of the + // remaining of the DICOM dataset. Only go for ASCII strings. + + if (element.getUint8Array(data) == EC_Normal && + Toolbox::IsAsciiString(data, element.getLength())) // (*) + { + if (data == NULL) + { + return new DicomValue("", false); // Empty string + } + else if (maxStringLength != 0 && + element.getLength() > maxStringLength && + ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end()) + { + return new DicomValue; // Too long, create a NULL value + } + else + { + std::string s(reinterpret_cast(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(data), element.getLength(), true); + } + } + + return new DicomValue; + } + + /** + * Numeric types + **/ + + case EVR_SL: // signed long + { + return ApplyDcmtkToCTypeConverter(element); + } + + case EVR_SS: // signed short + { + return ApplyDcmtkToCTypeConverter(element); + } + + case EVR_UL: // unsigned long + { + return ApplyDcmtkToCTypeConverter(element); + } + + case EVR_US: // unsigned short + { + return ApplyDcmtkToCTypeConverter(element); + } + + case EVR_FL: // float single-precision + { + return ApplyDcmtkToCTypeConverter(element); + } + + case EVR_FD: // float double-precision + { + return ApplyDcmtkToCTypeConverter(element); + } + + + /** + * Attribute tag. + **/ + + case EVR_AT: + { + DcmTagKey tag; + if (dynamic_cast(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 = ⌖ + break; + } + + case DicomToJsonFormat_Full: + { + assert(target.type() == Json::objectValue); + target["Value"] = Json::nullValue; + target["Type"] = Json::nullValue; + targetType = &target["Type"]; + targetValue = &target["Value"]; + break; + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(targetValue != NULL); + assert(targetValue->type() == Json::nullValue); + assert(targetType == NULL || targetType->type() == Json::nullValue); + + if (value.IsNull()) + { + if (targetType != NULL) + { + *targetType = "Null"; + } + } + else if (value.IsBinary()) + { + if (flags & DicomToJsonFlags_ConvertBinaryToAscii) + { + *targetValue = Toolbox::ConvertToAscii(value.GetContent()); + } + else + { + std::string s; + value.FormatDataUriScheme(s); + *targetValue = s; + } + + if (targetType != NULL) + { + *targetType = "Binary"; + } + } + else if (maxStringLength == 0 || + value.GetContent().size() <= maxStringLength) + { + *targetValue = value.GetContent(); + + if (targetType != NULL) + { + *targetType = "String"; + } + } + else + { + if (targetType != NULL) + { + *targetType = "TooLong"; + } + } + } + + + void FromDcmtkBridge::ElementToJson(Json::Value& parent, + DcmElement& element, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding, + const std::set& ignoreTagLength) + { + if (parent.type() == Json::nullValue) + { + parent = Json::objectValue; + } + + assert(parent.type() == Json::objectValue); + Json::Value& target = PrepareNode(parent, element, format); + + if (element.isLeaf()) + { + // The "0" below lets "LeafValueToJson()" take care of "TooLong" values + std::auto_ptr v(FromDcmtkBridge::ConvertLeafElement + (element, flags, 0, encoding, ignoreTagLength)); + + if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end()) + { + LeafValueToJson(target, *v, format, flags, maxStringLength); + } + else + { + LeafValueToJson(target, *v, format, flags, 0); + } + } + else + { + assert(target.type() == Json::nullValue); + target = Json::arrayValue; + + // "All subclasses of DcmElement except for DcmSequenceOfItems + // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset + // etc. are not." The following dynamic_cast is thus OK. + DcmSequenceOfItems& sequence = dynamic_cast(element); + + for (unsigned long i = 0; i < sequence.card(); i++) + { + DcmItem* child = sequence.getItem(i); + Json::Value& v = target.append(Json::objectValue); + DatasetToJson(v, *child, format, flags, maxStringLength, encoding, ignoreTagLength); + } + } + } + + + void FromDcmtkBridge::DatasetToJson(Json::Value& parent, + DcmItem& item, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding, + const std::set& ignoreTagLength) + { + assert(parent.type() == Json::objectValue); + + for (unsigned long i = 0; i < item.card(); i++) + { + DcmElement* element = item.getElement(i); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DicomTag tag(FromDcmtkBridge::Convert(element->getTag())); + + /*element->getTag().isPrivate()*/ + if (tag.IsPrivate() && + !(flags & DicomToJsonFlags_IncludePrivateTags)) + { + continue; + } + + if (!(flags & DicomToJsonFlags_IncludeUnknownTags)) + { + DictionaryLocker locker; + if (locker->findEntry(element->getTag(), NULL) == NULL) + { + continue; + } + } + + DcmEVR evr = element->getTag().getEVR(); + if (evr == EVR_OB || + evr == EVR_OF || + evr == EVR_OW || + evr == EVR_UN || + evr == EVR_ox) + { + // This is a binary tag + if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) || + (tag != DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludeBinary))) + { + continue; + } + } + + FromDcmtkBridge::ElementToJson(parent, *element, format, flags, + maxStringLength, encoding, ignoreTagLength); + } + } + + + void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target, + DcmDataset& dataset, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding defaultEncoding, + const std::set& ignoreTagLength) + { + Encoding encoding = DetectEncoding(dataset, defaultEncoding); + + target = Json::objectValue; + DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, ignoreTagLength); + } + + + void FromDcmtkBridge::ExtractHeaderAsJson(Json::Value& target, + DcmMetaInfo& dataset, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength) + { + std::set ignoreTagLength; + target = Json::objectValue; + DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii, ignoreTagLength); + } + + + + static std::string GetTagNameInternal(DcmTag& tag) + { + { + // Some patches for important tags because of different DICOM + // dictionaries between DCMTK versions + DicomTag tmp(tag.getGroup(), tag.getElement()); + std::string n = tmp.GetMainTagsName(); + if (n.size() != 0) + { + return n; + } + // End of patches + } + +#if 0 + // This version explicitly calls the dictionary + const DcmDataDictionary& dict = dcmDataDict.rdlock(); + const DcmDictEntry* entry = dict.findEntry(tag, NULL); + + std::string s(DcmTag_ERROR_TagName); + if (entry != NULL) + { + s = std::string(entry->getTagName()); + } + + dcmDataDict.unlock(); + return s; +#else + const char* name = tag.getTagName(); + if (name == NULL) + { + return DcmTag_ERROR_TagName; + } + else + { + return std::string(name); + } +#endif + } + + + std::string FromDcmtkBridge::GetTagName(const DicomTag& t, + const std::string& privateCreator) + { + DcmTag tag(t.GetGroup(), t.GetElement()); + + if (!privateCreator.empty()) + { + tag.setPrivateCreator(privateCreator.c_str()); + } + + return GetTagNameInternal(tag); + } + + + std::string FromDcmtkBridge::GetTagName(const DcmElement& element) + { + // Copy the tag to ensure const-correctness of DcmElement. Note + // that the private creator information is also copied. + DcmTag tag(element.getTag()); + + return GetTagNameInternal(tag); + } + + + + DicomTag FromDcmtkBridge::ParseTag(const char* name) + { + DicomTag parsed(0, 0); + if (DicomTag::ParseHexadecimal(parsed, name)) + { + return parsed; + } + +#if 0 + const DcmDataDictionary& dict = dcmDataDict.rdlock(); + const DcmDictEntry* entry = dict.findEntry(name); + + if (entry == NULL) + { + dcmDataDict.unlock(); + throw OrthancException(ErrorCode_UnknownDicomTag); + } + else + { + DcmTagKey key = entry->getKey(); + DicomTag tag(key.getGroup(), key.getElement()); + dcmDataDict.unlock(); + return tag; + } +#else + DcmTag tag; + if (DcmTag::findTagFromName(name, tag).good()) + { + return DicomTag(tag.getGTag(), tag.getETag()); + } + else + { + LOG(INFO) << "Unknown DICOM tag: \"" << name << "\""; + throw OrthancException(ErrorCode_UnknownDicomTag); + } +#endif + } + + + bool FromDcmtkBridge::IsUnknownTag(const DicomTag& tag) + { + DcmTag tmp(tag.GetGroup(), tag.GetElement()); + return tmp.isUnknownVR(); + } + + + void FromDcmtkBridge::ToJson(Json::Value& result, + const DicomMap& values, + bool simplify) + { + if (result.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + result.clear(); + + for (DicomMap::Map::const_iterator + it = values.map_.begin(); it != values.map_.end(); ++it) + { + // TODO Inject PrivateCreator if some is available in the DicomMap? + const std::string tagName = GetTagName(it->first, ""); + + if (simplify) + { + if (it->second->IsNull()) + { + result[tagName] = Json::nullValue; + } + else + { + // TODO IsBinary + result[tagName] = it->second->GetContent(); + } + } + else + { + Json::Value value = Json::objectValue; + + value["Name"] = tagName; + + if (it->second->IsNull()) + { + value["Type"] = "Null"; + value["Value"] = Json::nullValue; + } + else + { + // TODO IsBinary + value["Type"] = "String"; + value["Value"] = it->second->GetContent(); + } + + result[it->first.Format()] = value; + } + } + } + + + std::string FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType level) + { + char uid[100]; + + switch (level) + { + case ResourceType_Patient: + // The "PatientID" field is of type LO (Long String), 64 + // Bytes Maximum. An UUID is of length 36, thus it can be used + // as a random PatientID. + return Toolbox::GenerateUuid(); + + case ResourceType_Instance: + return dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT); + + case ResourceType_Series: + return dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT); + + case ResourceType_Study: + return dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmDataset& dataSet) + { + // Determine the transfer syntax which shall be used to write the + // information to the file. We always switch to the Little Endian + // syntax, with explicit length. + + // http://support.dcmtk.org/docs/dcxfer_8h-source.html + + + /** + * Note that up to Orthanc 0.7.1 (inclusive), the + * "EXS_LittleEndianExplicit" was always used to save the DICOM + * dataset into memory. We now keep the original transfer syntax + * (if available). + **/ + E_TransferSyntax xfer = dataSet.getOriginalXfer(); + if (xfer == EXS_Unknown) + { + // No information about the original transfer syntax: This is + // most probably a DICOM dataset that was read from memory. + xfer = EXS_LittleEndianExplicit; + } + + E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; + + // Create the meta-header information + DcmFileFormat ff(&dataSet); + ff.validateMetaInfo(xfer); + ff.removeInvalidGroups(); + + // Create a memory buffer with the proper size + { + const uint32_t estimatedSize = ff.calcElementLength(xfer, encodingType); // (*) + buffer.resize(estimatedSize); + } + + DcmOutputBufferStream ob(&buffer[0], buffer.size()); + + // Fill the memory buffer with the meta-header and the dataset + ff.transferInit(); + OFCondition c = ff.write(ob, xfer, encodingType, NULL, + /*opt_groupLength*/ EGL_recalcGL, + /*opt_paddingType*/ EPD_withoutPadding); + ff.transferEnd(); + + if (c.good()) + { + // The DICOM file is successfully written, truncate the target + // buffer if its size was overestimated by (*) + ob.flush(); + + size_t effectiveSize = static_cast(ob.tell()); + if (effectiveSize < buffer.size()) + { + buffer.resize(effectiveSize); + } + + return true; + } + else + { + // Error + buffer.clear(); + return false; + } + } + + + ValueRepresentation FromDcmtkBridge::LookupValueRepresentation(const DicomTag& tag) + { + DcmTag t(tag.GetGroup(), tag.GetElement()); + return Convert(t.getEVR()); + } + + ValueRepresentation FromDcmtkBridge::Convert(const DcmEVR vr) + { + switch (vr) + { + case EVR_AE: + return ValueRepresentation_ApplicationEntity; + + case EVR_AS: + return ValueRepresentation_AgeString; + + case EVR_AT: + return ValueRepresentation_AttributeTag; + + case EVR_CS: + return ValueRepresentation_CodeString; + + case EVR_DA: + return ValueRepresentation_Date; + + case EVR_DS: + return ValueRepresentation_DecimalString; + + case EVR_DT: + return ValueRepresentation_DateTime; + + case EVR_FL: + return ValueRepresentation_FloatingPointSingle; + + case EVR_FD: + return ValueRepresentation_FloatingPointDouble; + + case EVR_IS: + return ValueRepresentation_IntegerString; + + case EVR_LO: + return ValueRepresentation_LongString; + + case EVR_LT: + return ValueRepresentation_LongText; + + case EVR_OB: + return ValueRepresentation_OtherByte; + + // Not supported as of DCMTK 3.6.0 + /*case EVR_OD: + return ValueRepresentation_OtherDouble;*/ + + case EVR_OF: + return ValueRepresentation_OtherFloat; + + // Not supported as of DCMTK 3.6.0 + /*case EVR_OL: + return ValueRepresentation_OtherLong;*/ + + case EVR_OW: + return ValueRepresentation_OtherWord; + + case EVR_PN: + return ValueRepresentation_PersonName; + + case EVR_SH: + return ValueRepresentation_ShortString; + + case EVR_SL: + return ValueRepresentation_SignedLong; + + case EVR_SQ: + return ValueRepresentation_Sequence; + + case EVR_SS: + return ValueRepresentation_SignedShort; + + case EVR_ST: + return ValueRepresentation_ShortText; + + case EVR_TM: + return ValueRepresentation_Time; + + // Not supported as of DCMTK 3.6.0 + /*case EVR_UC: + return ValueRepresentation_UnlimitedCharacters;*/ + + case EVR_UI: + return ValueRepresentation_UniqueIdentifier; + + case EVR_UL: + return ValueRepresentation_UnsignedLong; + + case EVR_UN: + return ValueRepresentation_Unknown; + + // Not supported as of DCMTK 3.6.0 + /*case EVR_UR: + return ValueRepresentation_UniversalResource;*/ + + case EVR_US: + return ValueRepresentation_UnsignedShort; + + case EVR_UT: + return ValueRepresentation_UnlimitedText; + + default: + return ValueRepresentation_NotSupported; + } + } + + + static bool IsBinaryTag(const DcmTag& key) + { + return (key.isUnknownVR() || + key.getEVR() == EVR_OB || + key.getEVR() == EVR_OF || + key.getEVR() == EVR_OW || + key.getEVR() == EVR_UN || + key.getEVR() == EVR_ox); + } + + + DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag) + { + DcmTag key(tag.GetGroup(), tag.GetElement()); + + if (tag.IsPrivate() || + IsBinaryTag(key)) + { + return new DcmOtherByteOtherWord(key); + } + + switch (key.getEVR()) + { + // http://support.dcmtk.org/docs/dcvr_8h-source.html + + /** + * Binary types, handled above + **/ + + case EVR_OB: // other byte + case EVR_OF: // other float + case EVR_OW: // other word + case EVR_UN: // unknown value representation + case EVR_ox: // OB or OW depending on context + throw OrthancException(ErrorCode_InternalError); + + + /** + * String types. + * http://support.dcmtk.org/docs/classDcmByteString.html + **/ + + case EVR_AS: // age string + return new DcmAgeString(key); + + case EVR_AE: // application entity title + return new DcmApplicationEntity(key); + + case EVR_CS: // code string + return new DcmCodeString(key); + + case EVR_DA: // date string + return new DcmDate(key); + + case EVR_DT: // date time string + return new DcmDateTime(key); + + case EVR_DS: // decimal string + return new DcmDecimalString(key); + + case EVR_IS: // integer string + return new DcmIntegerString(key); + + case EVR_TM: // time string + return new DcmTime(key); + + case EVR_UI: // unique identifier + return new DcmUniqueIdentifier(key); + + case EVR_ST: // short text + return new DcmShortText(key); + + case EVR_LO: // long string + return new DcmLongString(key); + + case EVR_LT: // long text + return new DcmLongText(key); + + case EVR_UT: // unlimited text + return new DcmUnlimitedText(key); + + case EVR_SH: // short string + return new DcmShortString(key); + + case EVR_PN: // person name + return new DcmPersonName(key); + + + /** + * Numerical types + **/ + + case EVR_SL: // signed long + return new DcmSignedLong(key); + + case EVR_SS: // signed short + return new DcmSignedShort(key); + + case EVR_UL: // unsigned long + return new DcmUnsignedLong(key); + + case EVR_US: // unsigned short + return new DcmUnsignedShort(key); + + case EVR_FL: // float single-precision + return new DcmFloatingPointSingle(key); + + case EVR_FD: // float double-precision + return new DcmFloatingPointDouble(key); + + + /** + * Sequence types, should never occur at this point. + **/ + + case EVR_SQ: // sequence of items + throw OrthancException(ErrorCode_ParameterOutOfRange); + + + /** + * TODO + **/ + + case EVR_AT: // attribute tag + throw OrthancException(ErrorCode_NotImplemented); + + + /** + * Internal to DCMTK. + **/ + + case EVR_xs: // SS or US depending on context + case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) + case EVR_na: // na="not applicable", for data which has no VR + case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor + case EVR_item: // used internally for items + case EVR_metainfo: // used internally for meta info datasets + case EVR_dataset: // used internally for datasets + case EVR_fileFormat: // used internally for DICOM files + case EVR_dicomDir: // used internally for DICOMDIR objects + case EVR_dirRecord: // used internally for DICOMDIR records + case EVR_pixelSQ: // used internally for pixel sequences in a compressed image + case EVR_pixelItem: // used internally for pixel items in a compressed image + case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) + case EVR_PixelData: // used internally for uncompressed pixeld data + case EVR_OverlayData: // used internally for overlay data + case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR + default: + break; + } + + throw OrthancException(ErrorCode_InternalError); + } + + + + void FromDcmtkBridge::FillElementWithString(DcmElement& element, + const DicomTag& tag, + const std::string& utf8Value, + bool decodeDataUriScheme, + Encoding dicomEncoding) + { + std::string binary; + const std::string* decoded = &utf8Value; + + if (decodeDataUriScheme && + boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) + { + std::string mime; + if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + decoded = &binary; + } + else if (dicomEncoding != Encoding_Utf8) + { + binary = Toolbox::ConvertFromUtf8(utf8Value, dicomEncoding); + decoded = &binary; + } + + DcmTag key(tag.GetGroup(), tag.GetElement()); + + if (tag.IsPrivate() || + IsBinaryTag(key)) + { + if (element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good()) + { + return; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + bool ok = false; + + try + { + switch (key.getEVR()) + { + // http://support.dcmtk.org/docs/dcvr_8h-source.html + + /** + * TODO. + **/ + + case EVR_OB: // other byte + case EVR_OF: // other float + case EVR_OW: // other word + case EVR_AT: // attribute tag + throw OrthancException(ErrorCode_NotImplemented); + + case EVR_UN: // unknown value representation + throw OrthancException(ErrorCode_ParameterOutOfRange); + + + /** + * String types. + **/ + + case EVR_DS: // decimal string + case EVR_IS: // integer string + case EVR_AS: // age string + case EVR_DA: // date string + case EVR_DT: // date time string + case EVR_TM: // time string + case EVR_AE: // application entity title + case EVR_CS: // code string + case EVR_SH: // short string + case EVR_LO: // long string + case EVR_ST: // short text + case EVR_LT: // long text + case EVR_UT: // unlimited text + case EVR_PN: // person name + case EVR_UI: // unique identifier + { + ok = element.putString(decoded->c_str()).good(); + break; + } + + + /** + * Numerical types + **/ + + case EVR_SL: // signed long + { + ok = element.putSint32(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_SS: // signed short + { + ok = element.putSint16(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_UL: // unsigned long + { + ok = element.putUint32(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_US: // unsigned short + { + ok = element.putUint16(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_FL: // float single-precision + { + ok = element.putFloat32(boost::lexical_cast(*decoded)).good(); + break; + } + + case EVR_FD: // float double-precision + { + ok = element.putFloat64(boost::lexical_cast(*decoded)).good(); + break; + } + + + /** + * Sequence types, should never occur at this point. + **/ + + case EVR_SQ: // sequence of items + { + ok = false; + break; + } + + + /** + * Internal to DCMTK. + **/ + + case EVR_ox: // OB or OW depending on context + case EVR_xs: // SS or US depending on context + case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) + case EVR_na: // na="not applicable", for data which has no VR + case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor + case EVR_item: // used internally for items + case EVR_metainfo: // used internally for meta info datasets + case EVR_dataset: // used internally for datasets + case EVR_fileFormat: // used internally for DICOM files + case EVR_dicomDir: // used internally for DICOMDIR objects + case EVR_dirRecord: // used internally for DICOMDIR records + case EVR_pixelSQ: // used internally for pixel sequences in a compressed image + case EVR_pixelItem: // used internally for pixel items in a compressed image + case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) + case EVR_PixelData: // used internally for uncompressed pixeld data + case EVR_OverlayData: // used internally for overlay data + case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR + default: + break; + } + } + catch (boost::bad_lexical_cast&) + { + ok = false; + } + + if (!ok) + { + LOG(ERROR) << "While creating a DICOM instance, tag (" << tag.Format() + << ") has out-of-range value: \"" << *decoded << "\""; + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag, + const Json::Value& value, + bool decodeDataUriScheme, + Encoding dicomEncoding) + { + std::auto_ptr element; + + switch (value.type()) + { + case Json::stringValue: + element.reset(CreateElementForTag(tag)); + FillElementWithString(*element, tag, value.asString(), decodeDataUriScheme, dicomEncoding); + break; + + case Json::nullValue: + element.reset(CreateElementForTag(tag)); + FillElementWithString(*element, tag, "", decodeDataUriScheme, dicomEncoding); + break; + + case Json::arrayValue: + { + DcmTag key(tag.GetGroup(), tag.GetElement()); + if (key.getEVR() != EVR_SQ) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key); + element.reset(sequence); + + for (Json::Value::ArrayIndex i = 0; i < value.size(); i++) + { + std::auto_ptr item(new DcmItem); + + switch (value[i].type()) + { + case Json::objectValue: + { + Json::Value::Members members = value[i].getMemberNames(); + for (Json::Value::ArrayIndex j = 0; j < members.size(); j++) + { + item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding)); + } + break; + } + + case Json::arrayValue: + { + // Lua cannot disambiguate between an empty dictionary + // and an empty array + if (value[i].size() != 0) + { + throw OrthancException(ErrorCode_BadParameterType); + } + break; + } + + default: + throw OrthancException(ErrorCode_BadParameterType); + } + + sequence->append(item.release()); + } + + break; + } + + default: + throw OrthancException(ErrorCode_BadParameterType); + } + + return element.release(); + } + + + DcmPixelSequence* FromDcmtkBridge::GetPixelSequence(DcmDataset& dataset) + { + DcmElement *element = NULL; + if (!dataset.findAndGetElement(DCM_PixelData, element).good()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + DcmPixelData& pixelData = dynamic_cast(*element); + DcmPixelSequence* pixelSequence = NULL; + if (!pixelData.getEncapsulatedRepresentation + (dataset.getOriginalXfer(), NULL, pixelSequence).good()) + { + return NULL; + } + else + { + return pixelSequence; + } + } + + + Encoding FromDcmtkBridge::ExtractEncoding(const Json::Value& json, + Encoding defaultEncoding) + { + if (json.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + Encoding encoding = defaultEncoding; + + const Json::Value::Members tags = json.getMemberNames(); + + // Look for SpecificCharacterSet (0008,0005) in the JSON file + for (size_t i = 0; i < tags.size(); i++) + { + DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]); + if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + const Json::Value& value = json[tags[i]]; + if (value.type() != Json::stringValue || + (value.asString().length() != 0 && + !GetDicomEncoding(encoding, value.asCString()))) + { + LOG(ERROR) << "Unknown encoding while creating DICOM from JSON: " << value; + throw OrthancException(ErrorCode_BadRequest); + } + + if (value.asString().length() == 0) + { + return defaultEncoding; + } + } + } + + return encoding; + } + + + static void SetString(DcmDataset& target, + const DcmTag& tag, + const std::string& value) + { + if (!target.putAndInsertString(tag, value.c_str()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + + DcmDataset* FromDcmtkBridge::FromJson(const Json::Value& json, // Encoded using UTF-8 + bool generateIdentifiers, + bool decodeDataUriScheme, + Encoding defaultEncoding) + { + std::auto_ptr result(new DcmDataset); + Encoding encoding = ExtractEncoding(json, defaultEncoding); + + SetString(*result, DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(encoding)); + + const Json::Value::Members tags = json.getMemberNames(); + + bool hasPatientId = false; + bool hasStudyInstanceUid = false; + bool hasSeriesInstanceUid = false; + bool hasSopInstanceUid = false; + + for (size_t i = 0; i < tags.size(); i++) + { + DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]); + const Json::Value& value = json[tags[i]]; + + if (tag == DICOM_TAG_PATIENT_ID) + { + hasPatientId = true; + } + else if (tag == DICOM_TAG_STUDY_INSTANCE_UID) + { + hasStudyInstanceUid = true; + } + else if (tag == DICOM_TAG_SERIES_INSTANCE_UID) + { + hasSeriesInstanceUid = true; + } + else if (tag == DICOM_TAG_SOP_INSTANCE_UID) + { + hasSopInstanceUid = true; + } + + if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding)); + const DcmTagKey& tag = element->getTag(); + + result->findAndDeleteElement(tag); + + DcmElement* tmp = element.release(); + if (!result->insert(tmp, false, false).good()) + { + delete tmp; + throw OrthancException(ErrorCode_InternalError); + } + } + } + + if (!hasPatientId && + generateIdentifiers) + { + SetString(*result, DCM_PatientID, GenerateUniqueIdentifier(ResourceType_Patient)); + } + + if (!hasStudyInstanceUid && + generateIdentifiers) + { + SetString(*result, DCM_StudyInstanceUID, GenerateUniqueIdentifier(ResourceType_Study)); + } + + if (!hasSeriesInstanceUid && + generateIdentifiers) + { + SetString(*result, DCM_SeriesInstanceUID, GenerateUniqueIdentifier(ResourceType_Series)); + } + + if (!hasSopInstanceUid && + generateIdentifiers) + { + SetString(*result, DCM_SOPInstanceUID, GenerateUniqueIdentifier(ResourceType_Instance)); + } + + return result.release(); + } + + + DcmFileFormat* FromDcmtkBridge::LoadFromMemoryBuffer(const void* buffer, + size_t size) + { + DcmInputBufferStream is; + if (size > 0) + { + is.setBuffer(buffer, size); + } + is.setEos(); + + std::auto_ptr result(new DcmFileFormat); + + result->transferInit(); + if (!result->read(is).good()) + { + LOG(ERROR) << "Cannot parse an invalid DICOM file (size: " << size << " bytes)"; + throw OrthancException(ErrorCode_BadFileFormat); + } + + result->loadAllDataIntoMemory(); + result->transferEnd(); + + return result.release(); + } + + + void FromDcmtkBridge::FromJson(DicomMap& target, + const Json::Value& source) + { + if (source.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + target.Clear(); + + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& value = source[members[i]]; + + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + target.SetValue(ParseTag(members[i]), value.asString(), false); + } + } + + + void FromDcmtkBridge::ChangeStringEncoding(DcmItem& dataset, + Encoding source, + Encoding target) + { + // Recursive exploration of a dataset to change the encoding of + // each string-like element + + if (source == target) + { + return; + } + + for (unsigned long i = 0; i < dataset.card(); i++) + { + DcmElement* element = dataset.getElement(i); + if (element) + { + if (element->isLeaf()) + { + char *c = NULL; + if (element->isaString() && + element->getString(c).good() && + c != NULL) + { + std::string a = Toolbox::ConvertToUtf8(c, source); + std::string b = Toolbox::ConvertFromUtf8(a, target); + element->putString(b.c_str()); + } + } + else + { + // "All subclasses of DcmElement except for DcmSequenceOfItems + // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset + // etc. are not." The following dynamic_cast is thus OK. + DcmSequenceOfItems& sequence = dynamic_cast(*element); + + for (unsigned long j = 0; j < sequence.card(); j++) + { + ChangeStringEncoding(*sequence.getItem(j), source, target); + } + } + } + } + } + + + bool FromDcmtkBridge::LookupTransferSyntax(std::string& result, + DcmFileFormat& dicom) + { + const char* value = NULL; + + if (dicom.getMetaInfo() != NULL && + dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() && + value != NULL) + { + result.assign(value); + return true; + } + else + { + return false; + } + } + + +#if ORTHANC_ENABLE_LUA == 1 + void FromDcmtkBridge::ExecuteToDicom(DicomMap& target, + LuaFunctionCall& call) + { + Json::Value output; + call.ExecuteToJson(output, true /* keep strings */); + + target.Clear(); + + if (output.type() == Json::arrayValue && + output.size() == 0) + { + // This case happens for empty tables + return; + } + + if (output.type() != Json::objectValue) + { + LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table"; + throw OrthancException(ErrorCode_LuaBadOutput); + } + + Json::Value::Members members = output.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + if (output[members[i]].type() != Json::stringValue) + { + LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table mapping names of DICOM tags to strings"; + throw OrthancException(ErrorCode_LuaBadOutput); + } + + DicomTag tag(ParseTag(members[i])); + target.SetValue(tag, output[members[i]].asString(), false); + } + } +#endif + + + void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, + DcmItem& dataset) + { + ExtractDicomSummary(target, dataset, + ORTHANC_MAXIMUM_TAG_LENGTH, + GetDefaultDicomEncoding()); + } + + + void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target, + DcmDataset& dataset, + const std::set& ignoreTagLength) + { + ExtractDicomAsJson(target, dataset, + DicomToJsonFormat_Full, + DicomToJsonFlags_Default, + ORTHANC_MAXIMUM_TAG_LENGTH, + GetDefaultDicomEncoding(), + ignoreTagLength); + } + + + void FromDcmtkBridge::InitializeCodecs() + { +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + LOG(INFO) << "Registering JPEG Lossless codecs in DCMTK"; + DJLSDecoderRegistration::registerCodecs(); +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + LOG(INFO) << "Registering JPEG codecs in DCMTK"; + DJDecoderRegistration::registerCodecs(); +#endif + } + + + void FromDcmtkBridge::FinalizeCodecs() + { +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + // Unregister JPEG-LS codecs + DJLSDecoderRegistration::cleanup(); +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + // Unregister JPEG codecs + DJDecoderRegistration::cleanup(); +#endif + } + + + + // Forward declaration + static void ApplyVisitorToElement(DcmElement& element, + ITagVisitor& visitor, + const std::vector& parentTags, + const std::vector& parentIndexes, + Encoding encoding); + + static void ApplyVisitorToDataset(DcmItem& dataset, + ITagVisitor& visitor, + const std::vector& parentTags, + const std::vector& parentIndexes, + Encoding encoding) + { + assert(parentTags.size() == parentIndexes.size()); + + for (unsigned long i = 0; i < dataset.card(); i++) + { + DcmElement* element = dataset.getElement(i); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding); + } + } + } + + + static void ApplyVisitorToLeaf(DcmElement& element, + ITagVisitor& visitor, + const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + Encoding encoding) + { + // TODO - Merge this function with ConvertLeafElement() + + assert(element.isLeaf()); + + DcmEVR evr = element.getTag().getEVR(); + ValueRepresentation vr = FromDcmtkBridge::Convert(evr); + + char *c = NULL; + if (element.isaString() && + element.getString(c).good()) + { + std::string utf8; + + if (c != NULL) // This case corresponds to the empty string + { + std::string s(c); + utf8 = Toolbox::ConvertToUtf8(s, encoding); + } + + std::string newValue; + ITagVisitor::Action action = visitor.VisitString + (newValue, parentTags, parentIndexes, tag, vr, utf8); + + switch (action) + { + case ITagVisitor::Action_None: + break; + + case ITagVisitor::Action_Replace: + { + std::string s = Toolbox::ConvertFromUtf8(newValue, encoding); + if (element.putString(s.c_str()) != EC_Normal) + { + LOG(ERROR) << "Cannot replace value of tag: " << tag.Format(); + throw OrthancException(ErrorCode_InternalError); + } + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + return; // We're done + } + + + try + { + // http://support.dcmtk.org/docs/dcvr_8h-source.html + switch (element.getVR()) + { + + /** + * Deal with binary data (including PixelData). + **/ + + case EVR_OB: // other byte + case EVR_OF: // other float + case EVR_OW: // other word + case EVR_UN: // unknown value representation + case EVR_ox: // OB or OW depending on context + case EVR_DS: // decimal string + case EVR_IS: // integer string + case EVR_AS: // age string + case EVR_DA: // date string + case EVR_DT: // date time string + case EVR_TM: // time string + case EVR_AE: // application entity title + case EVR_CS: // code string + case EVR_SH: // short string + case EVR_LO: // long string + case EVR_ST: // short text + case EVR_LT: // long text + case EVR_UT: // unlimited text + case EVR_PN: // person name + case EVR_UI: // unique identifier + case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) + case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR + { + Uint8* data = NULL; + + if (element.getUint8Array(data) == EC_Normal) + { + visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength()); + } + else + { + visitor.VisitUnknown(parentTags, parentIndexes, tag, vr); + } + + break; + } + + /** + * Numeric types + **/ + + case EVR_SL: // signed long + { + Sint32 f; + if (dynamic_cast(element).getSint32(f).good()) + { + visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f); + } + + break; + } + + case EVR_SS: // signed short + { + Sint16 f; + if (dynamic_cast(element).getSint16(f).good()) + { + visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f); + } + + break; + } + + case EVR_UL: // unsigned long + { + Uint32 f; + if (dynamic_cast(element).getUint32(f).good()) + { + visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f); + } + + break; + } + + case EVR_US: // unsigned short + { + Uint16 f; + if (dynamic_cast(element).getUint16(f).good()) + { + visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f); + } + + break; + } + + case EVR_FL: // float single-precision + { + Float32 f; + if (dynamic_cast(element).getFloat32(f).good()) + { + visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f); + } + + break; + } + + case EVR_FD: // float double-precision + { + Float64 f; + if (dynamic_cast(element).getFloat64(f).good()) + { + visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f); + } + + break; + } + + + /** + * Attribute tag. + **/ + + case EVR_AT: + { + DcmTagKey tagKey; + if (dynamic_cast(element).getTagVal(tagKey, 0).good()) + { + DicomTag t(tagKey.getGroup(), tagKey.getElement()); + visitor.VisitAttribute(parentTags, parentIndexes, tag, vr, t); + } + + break; + } + + + /** + * Sequence types, should never occur at this point because of + * "element.isLeaf()". + **/ + + case EVR_SQ: // sequence of items + return; + + + /** + * Internal to DCMTK. + **/ + + case EVR_xs: // SS or US depending on context + case EVR_lt: // US, SS or OW depending on context, used for LUT Data (thus the name) + case EVR_na: // na="not applicable", for data which has no VR + case EVR_up: // up="unsigned pointer", used internally for DICOMDIR suppor + case EVR_item: // used internally for items + case EVR_metainfo: // used internally for meta info datasets + case EVR_dataset: // used internally for datasets + case EVR_fileFormat: // used internally for DICOM files + case EVR_dicomDir: // used internally for DICOMDIR objects + case EVR_dirRecord: // used internally for DICOMDIR records + case EVR_pixelSQ: // used internally for pixel sequences in a compressed image + case EVR_pixelItem: // used internally for pixel items in a compressed image + case EVR_PixelData: // used internally for uncompressed pixeld data + case EVR_OverlayData: // used internally for overlay data + visitor.VisitUnknown(parentTags, parentIndexes, tag, vr); + return; + + + /** + * Default case. + **/ + + default: + return; + } + } + catch (boost::bad_lexical_cast&) + { + return; + } + catch (std::bad_cast&) + { + return; + } + } + + + static void ApplyVisitorToElement(DcmElement& element, + ITagVisitor& visitor, + const std::vector& parentTags, + const std::vector& parentIndexes, + Encoding encoding) + { + assert(parentTags.size() == parentIndexes.size()); + + DicomTag tag(FromDcmtkBridge::Convert(element.getTag())); + + if (element.isLeaf()) + { + ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding); + } + else + { + // "All subclasses of DcmElement except for DcmSequenceOfItems + // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset + // etc. are not." The following dynamic_cast is thus OK. + DcmSequenceOfItems& sequence = dynamic_cast(element); + + std::vector tags = parentTags; + std::vector indexes = parentIndexes; + tags.push_back(tag); + indexes.push_back(0); + + for (unsigned long i = 0; i < sequence.card(); i++) + { + indexes.back() = static_cast(i); + DcmItem* child = sequence.getItem(i); + ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding); + } + } + } + + + void FromDcmtkBridge::Apply(DcmItem& dataset, + ITagVisitor& visitor, + Encoding defaultEncoding) + { + std::vector parentTags; + std::vector parentIndexes; + Encoding encoding = DetectEncoding(dataset, defaultEncoding); + ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/FromDcmtkBridge.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/FromDcmtkBridge.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,249 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ITagVisitor.h" +#include "../DicomFormat/DicomElement.h" +#include "../DicomFormat/DicomMap.h" + +#include +#include +#include +#include +#include + +#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 +#endif + +#if ORTHANC_ENABLE_LUA == 1 +# include "../Lua/LuaFunctionCall.h" +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG) +# error The macro ORTHANC_ENABLE_DCMTK_JPEG must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS) +# error The macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined +#endif + + +namespace Orthanc +{ + class FromDcmtkBridge : public boost::noncopyable + { +#if ORTHANC_BUILD_UNIT_TESTS == 1 + FRIEND_TEST(FromDcmtkBridge, FromJson); +#endif + + friend class ParsedDicomFile; + + private: + FromDcmtkBridge(); // Pure static class + + static void ExtractDicomSummary(DicomMap& target, + DcmItem& dataset, + unsigned int maxStringLength, + Encoding defaultEncoding); + + static void DatasetToJson(Json::Value& parent, + DcmItem& item, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding, + const std::set& ignoreTagLength); + + static void ElementToJson(Json::Value& parent, + DcmElement& element, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding dicomEncoding, + const std::set& ignoreTagLength); + + static void ExtractDicomAsJson(Json::Value& target, + DcmDataset& dataset, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding defaultEncoding, + const std::set& ignoreTagLength); + + static void ChangeStringEncoding(DcmItem& dataset, + Encoding source, + Encoding target); + + public: + static void InitializeDictionary(bool loadPrivateDictionary); + + static void RegisterDictionaryTag(const DicomTag& tag, + ValueRepresentation vr, + const std::string& name, + unsigned int minMultiplicity, + unsigned int maxMultiplicity, + const std::string& privateCreator); + + static Encoding DetectEncoding(DcmItem& dataset, + Encoding defaultEncoding); + + static DicomTag Convert(const DcmTag& tag); + + static DicomTag GetTag(const DcmElement& element); + + static bool IsUnknownTag(const DicomTag& tag); + + static DicomValue* ConvertLeafElement(DcmElement& element, + DicomToJsonFlags flags, + unsigned int maxStringLength, + Encoding encoding, + const std::set& ignoreTagLength); + + static void ExtractHeaderAsJson(Json::Value& target, + DcmMetaInfo& header, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength); + + static std::string GetTagName(const DicomTag& tag, + const std::string& privateCreator); + + static std::string GetTagName(const DcmElement& element); + + static std::string GetTagName(const DicomElement& element) + { + return GetTagName(element.GetTag(), ""); + } + + static DicomTag ParseTag(const char* name); + + static DicomTag ParseTag(const std::string& name) + { + return ParseTag(name.c_str()); + } + + static bool HasTag(const DicomMap& fields, + const std::string& tagName) + { + return fields.HasTag(ParseTag(tagName)); + } + + static const DicomValue& GetValue(const DicomMap& fields, + const std::string& tagName) + { + return fields.GetValue(ParseTag(tagName)); + } + + static void SetValue(DicomMap& target, + const std::string& tagName, + DicomValue* value) + { + target.SetValue(ParseTag(tagName), value); + } + + static void ToJson(Json::Value& result, + const DicomMap& values, + bool simplify); + + static std::string GenerateUniqueIdentifier(ResourceType level); + + static bool SaveToMemoryBuffer(std::string& buffer, + DcmDataset& dataSet); + + static ValueRepresentation Convert(DcmEVR vr); + + static ValueRepresentation LookupValueRepresentation(const DicomTag& tag); + + static DcmElement* CreateElementForTag(const DicomTag& tag); + + static void FillElementWithString(DcmElement& element, + const DicomTag& tag, + const std::string& utf8alue, // Encoded using UTF-8 + bool decodeDataUriScheme, + Encoding dicomEncoding); + + static DcmElement* FromJson(const DicomTag& tag, + const Json::Value& element, // Encoded using UTF-8 + bool decodeDataUriScheme, + Encoding dicomEncoding); + + static DcmPixelSequence* GetPixelSequence(DcmDataset& dataset); + + static Encoding ExtractEncoding(const Json::Value& json, + Encoding defaultEncoding); + + static DcmDataset* FromJson(const Json::Value& json, // Encoded using UTF-8 + bool generateIdentifiers, + bool decodeDataUriScheme, + Encoding defaultEncoding); + + static DcmFileFormat* LoadFromMemoryBuffer(const void* buffer, + size_t size); + + static void FromJson(DicomMap& values, + const Json::Value& result); + + static bool LookupTransferSyntax(std::string& result, + DcmFileFormat& dicom); + +#if ORTHANC_ENABLE_LUA == 1 + static void ExecuteToDicom(DicomMap& target, + LuaFunctionCall& call); +#endif + + static void ExtractDicomSummary(DicomMap& target, + DcmItem& dataset); + + static void ExtractDicomAsJson(Json::Value& target, + DcmDataset& dataset, + const std::set& ignoreTagLength); + + static void InitializeCodecs(); + + static void FinalizeCodecs(); + + static void Apply(DcmItem& dataset, + ITagVisitor& visitor, + Encoding defaultEncoding); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/ITagVisitor.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/ITagVisitor.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,93 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../DicomFormat/DicomTag.h" + +#include +#include + +namespace Orthanc +{ + class ITagVisitor : public boost::noncopyable + { + public: + enum Action + { + Action_Replace, + Action_None + }; + + virtual ~ITagVisitor() + { + } + + virtual void VisitUnknown(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr) = 0; + + virtual void VisitBinary(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const void* data, + size_t size) = 0; + + virtual void VisitInteger(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + int64_t value) = 0; + + virtual void VisitDouble(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + double value) = 0; + + virtual void VisitAttribute(const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const DicomTag& value) = 0; + + virtual Action VisitString(std::string& newValue, + const std::vector& parentTags, + const std::vector& parentIndexes, + const DicomTag& tag, + ValueRepresentation vr, + const std::string& value) = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/Internals/DicomFrameIndex.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/Internals/DicomFrameIndex.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,439 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "DicomFrameIndex.h" + +#include "../../OrthancException.h" +#include "../../DicomFormat/DicomImageInformation.h" +#include "../FromDcmtkBridge.h" +#include "../../Endianness.h" +#include "DicomImageDecoder.h" + +#include + +#include +#include +#include + +namespace Orthanc +{ + class DicomFrameIndex::FragmentIndex : public DicomFrameIndex::IIndex + { + private: + DcmPixelSequence* pixelSequence_; + std::vector startFragment_; + std::vector countFragments_; + std::vector frameSize_; + + void GetOffsetTable(std::vector& table) + { + DcmPixelItem* item = NULL; + if (!pixelSequence_->getItem(item, 0).good() || + item == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + uint32_t length = item->getLength(); + if (length == 0) + { + table.clear(); + return; + } + + if (length % 4 != 0) + { + // Error: Each fragment is index with 4 bytes (uint32_t) + throw OrthancException(ErrorCode_BadFileFormat); + } + + uint8_t* content = NULL; + if (!item->getUint8Array(content).good() || + content == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + table.resize(length / 4); + + // The offset table is always in little endian in the DICOM + // file. Swap it to host endianness if needed. + const uint32_t* offset = reinterpret_cast(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(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(fragment); + frameSize_[i] = fragment->getLength(); + countFragments_[i] = 1; + } + + return; + } + + // Parse the offset table + std::vector 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(fragment); + + unsigned int currentFragment = 1; + while (fragment != NULL) + { + if (currentFrame + 1 < countFrames && + offset == offsetOfFrame[currentFrame + 1]) + { + currentFrame += 1; + startFragment_[currentFrame] = dynamic_cast(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(&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(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(&pixelData_[0]) + index * frameSize_, frameSize_); + } + } + }; + + + + bool DicomFrameIndex::IsVideo(DcmFileFormat& dicom) + { + // Retrieve the transfer syntax from the DICOM header + const char* value = NULL; + if (!dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() || + value == NULL) + { + return false; + } + + const std::string transferSyntax(value); + + // Video standards supported in DICOM 2016a + // http://dicom.nema.org/medical/dicom/2016a/output/html/part05.html + if (transferSyntax == "1.2.840.10008.1.2.4.100" || // MPEG2 MP@ML option of ISO/IEC MPEG2 + transferSyntax == "1.2.840.10008.1.2.4.101" || // MPEG2 MP@HL option of ISO/IEC MPEG2 + transferSyntax == "1.2.840.10008.1.2.4.102" || // MPEG-4 AVC/H.264 High Profile / Level 4.1 of ITU-T H.264 + transferSyntax == "1.2.840.10008.1.2.4.103" || // MPEG-4 AVC/H.264 BD-compat High Profile / Level 4.1 of ITU-T H.264 + transferSyntax == "1.2.840.10008.1.2.4.104" || // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264 + transferSyntax == "1.2.840.10008.1.2.4.105" || // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264 + transferSyntax == "1.2.840.10008.1.2.4.106") // MPEG-4 AVC/H.264 Stereo High Profile / Level 4.2 of the ITU-T H.264 + { + return true; + } + + return false; + } + + + unsigned int DicomFrameIndex::GetFramesCount(DcmFileFormat& dicom) + { + // Assume 1 frame for video transfer syntaxes + if (IsVideo(dicom)) + { + return 1; + } + + const char* tmp = NULL; + if (!dicom.getDataset()->findAndGetString(DCM_NumberOfFrames, tmp).good() || + tmp == NULL) + { + return 1; + } + + int count = -1; + try + { + count = boost::lexical_cast(tmp); + } + catch (boost::bad_lexical_cast&) + { + } + + if (count < 0) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return count; + } + } + + + DicomFrameIndex::DicomFrameIndex(DcmFileFormat& dicom) + { + countFrames_ = GetFramesCount(dicom); + if (countFrames_ == 0) + { + // The image has no frame. No index is to be built. + return; + } + + DcmDataset& dataset = *dicom.getDataset(); + + // Test whether this image is composed of a sequence of fragments + DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset); + if (pixelSequence != NULL) + { + index_.reset(new FragmentIndex(pixelSequence, countFrames_)); + return; + } + + // Extract information about the image structure + DicomMap tags; + FromDcmtkBridge::ExtractDicomSummary(tags, dataset); + + DicomImageInformation information(tags); + + // Access to the raw pixel data + if (DicomImageDecoder::IsPsmctRle1(dataset)) + { + index_.reset(new PsmctRle1Index(dataset, countFrames_, information.GetFrameSize())); + } + else + { + index_.reset(new UncompressedIndex(dataset, countFrames_, information.GetFrameSize())); + } + } + + + void DicomFrameIndex::GetRawFrame(std::string& frame, + unsigned int index) const + { + if (index >= countFrames_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else if (index_.get() != NULL) + { + return index_->GetRawFrame(frame, index); + } + else + { + frame.clear(); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/Internals/DicomFrameIndex.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/Internals/DicomFrameIndex.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,83 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Enumerations.h" + +#include +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class DicomFrameIndex + { + private: + class IIndex : public boost::noncopyable + { + public: + virtual ~IIndex() + { + } + + virtual void GetRawFrame(std::string& frame, + unsigned int index) const = 0; + }; + + class FragmentIndex; + class UncompressedIndex; + class PsmctRle1Index; + + std::auto_ptr index_; + unsigned int countFrames_; + + public: + DicomFrameIndex(DcmFileFormat& dicom); + + unsigned int GetFramesCount() const + { + return countFrames_; + } + + void GetRawFrame(std::string& frame, + unsigned int index) const; + + static bool IsVideo(DcmFileFormat& dicom); + + static unsigned int GetFramesCount(DcmFileFormat& dicom); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/Internals/DicomImageDecoder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,1001 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "DicomImageDecoder.h" + + +/*========================================================================= + + This file is based on portions of the following project + (cf. function "DecodePsmctRle1()"): + + Program: GDCM (Grassroots DICOM). A DICOM library + Module: http://gdcm.sourceforge.net/Copyright.html + + Copyright (c) 2006-2011 Mathieu Malaterre + Copyright (c) 1993-2005 CREATIS + (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any + contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to + endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + =========================================================================*/ + + +#include "../../Logging.h" +#include "../../OrthancException.h" +#include "../../Images/Image.h" +#include "../../Images/ImageProcessing.h" +#include "../../DicomFormat/DicomIntegerPixelAccessor.h" +#include "../ToDcmtkBridge.h" +#include "../FromDcmtkBridge.h" +#include "../ParsedDicomFile.h" + +#if ORTHANC_ENABLE_PNG == 1 +# include "../../Images/PngWriter.h" +#endif + +#if ORTHANC_ENABLE_JPEG == 1 +# include "../../Images/JpegWriter.h" +#endif +#include "../../Images/PamWriter.h" + +#include + +#include +#include +#include +#include +#include + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 +# include +# include +# include +# include +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 +# include +# include +# include +# include +# include +# include +# include +# include +# include +#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(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 temp; + temp.reserve(length); + for (size_t i = 0; i < length; i++) + { + if (inbuffer[i] == 0xa5) + { + temp.push_back(inbuffer[i+2]); + for (uint8_t repeat = inbuffer[i + 1]; repeat != 0; repeat--) + { + temp.push_back(inbuffer[i+2]); + } + i += 2; + } + else + { + temp.push_back(inbuffer[i]); + } + } + + // Delta encoding pass + uint16_t delta = 0; + output.clear(); + output.reserve(temp.size()); + for (size_t i = 0; i < temp.size(); i++) + { + uint16_t value; + + if (temp[i] == 0x5a) + { + uint16_t v1 = temp[i + 1]; + uint16_t v2 = temp[i + 2]; + value = (v2 << 8) + v1; + i += 2; + } + else + { + value = delta + (int8_t) temp[i]; + } + + output.push_back(value & 0xff); + output.push_back(value >> 8); + delta = value; + } + + if (output.size() % 2) + { + output.resize(output.size() - 1); + } + + return true; + } + + + class DicomImageDecoder::ImageSource + { + private: + std::string psmct_; + std::auto_ptr 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(&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 + static void CopyPixels(ImageAccessor& target, + const DicomIntegerPixelAccessor& source) + { + const PixelType minValue = std::numeric_limits::min(); + const PixelType maxValue = std::numeric_limits::max(); + + for (unsigned int y = 0; y < source.GetInformation().GetHeight(); y++) + { + PixelType* pixel = reinterpret_cast(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(minValue)) + { + *pixel = minValue; + } + else if (v > static_cast(maxValue)) + { + *pixel = maxValue; + } + else + { + *pixel = static_cast(v); + } + } + } + } + } + + + static ImageAccessor* DecodeLookupTable(std::auto_ptr& 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(pixelData); + + for (unsigned int y = 0; y < target->GetHeight(); y++) + { + uint8_t* p = reinterpret_cast(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(pixelData); + + for (unsigned int y = 0; y < target->GetHeight(); y++) + { + uint16_t* p = reinterpret_cast(target->GetRow(y)); + + for (unsigned int x = 0; x < target->GetWidth(); x++) + { + p[0] = lutRed[*source]; + p[1] = lutGreen[*source]; + p[2] = lutBlue[*source]; + source++; + p += 3; + } + } + + return target.release(); + } + + default: + break; + } + + throw OrthancException(ErrorCode_InternalError); + } + + + ImageAccessor* DicomImageDecoder::DecodeUncompressedImage(DcmDataset& dataset, + unsigned int frame) + { + /** + * Create the target image. + **/ + + std::auto_ptr 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(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(*target, source.GetAccessor()); + break; + + case PixelFormat_Grayscale16: + CopyPixels(*target, source.GetAccessor()); + break; + + case PixelFormat_SignedGrayscale16: + CopyPixels(*target, source.GetAccessor()); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + return target.release(); + } + + + ImageAccessor* DicomImageDecoder::ApplyCodec + (const DcmCodec& codec, + const DcmCodecParameter& parameters, + const DcmRepresentationParameter& representationParameter, + DcmDataset& dataset, + unsigned int frame) + { + DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset); + if (pixelSequence == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + DicomMap m; + FromDcmtkBridge::ExtractDicomSummary(m, dataset); + DicomImageInformation info(m); + + std::auto_ptr 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, ¶meters, + &dataset, frame, startFragment, &uncompressed[0], + uncompressed.size(), decompressedColorModel).good()) + { + LOG(ERROR) << "Cannot decode a palette image"; + throw OrthancException(ErrorCode_BadFileFormat); + } + + return DecodeLookupTable(target, info, dataset, + reinterpret_cast(uncompressed.c_str()), + uncompressed.size()); + } + else + { + if (!codec.decodeFrame(&representationParameter, + pixelSequence, ¶meters, + &dataset, frame, startFragment, target->GetBuffer(), + target->GetSize(), decompressedColorModel).good()) + { + LOG(ERROR) << "Cannot decode a non-palette image"; + throw OrthancException(ErrorCode_BadFileFormat); + } + + return target.release(); + } + } + + + ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom, + unsigned int frame) + { + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + E_TransferSyntax syntax = dataset.getOriginalXfer(); + + /** + * Deal with uncompressed, raw images. + * http://support.dcmtk.org/docs/dcxfer_8h-source.html + **/ + if (syntax == EXS_Unknown || + syntax == EXS_LittleEndianImplicit || + syntax == EXS_BigEndianImplicit || + syntax == EXS_LittleEndianExplicit || + syntax == EXS_BigEndianExplicit) + { + return DecodeUncompressedImage(dataset, frame); + } + + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + /** + * Deal with JPEG-LS images. + **/ + + if (syntax == EXS_JPEGLSLossless || + syntax == EXS_JPEGLSLossy) + { + // The (2, OFTrue) are the default parameters as found in DCMTK 3.6.2 + // http://support.dcmtk.org/docs/classDJLSRepresentationParameter.html + DJLSRepresentationParameter representationParameter(2, OFTrue); + + DJLSCodecParameter parameters; + std::auto_ptr decoder; + + switch (syntax) + { + case EXS_JPEGLSLossless: + LOG(INFO) << "Decoding a JPEG-LS lossless DICOM image"; + decoder.reset(new DJLSLosslessDecoder); + break; + + case EXS_JPEGLSLossy: + LOG(INFO) << "Decoding a JPEG-LS near-lossless DICOM image"; + decoder.reset(new DJLSNearLosslessDecoder); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + return ApplyCodec(*decoder, parameters, representationParameter, dataset, frame); + } +#endif + + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + /** + * Deal with JPEG images. + **/ + + if (syntax == EXS_JPEGProcess1 || // DJDecoderBaseline + syntax == EXS_JPEGProcess2_4 || // DJDecoderExtended + syntax == EXS_JPEGProcess6_8 || // DJDecoderSpectralSelection (retired) + syntax == EXS_JPEGProcess10_12 || // DJDecoderProgressive (retired) + syntax == EXS_JPEGProcess14 || // DJDecoderLossless + syntax == EXS_JPEGProcess14SV1) // DJDecoderP14SV1 + { + // http://support.dcmtk.org/docs-snapshot/djutils_8h.html#a2a9695e5b6b0f5c45a64c7f072c1eb9d + DJCodecParameter parameters( + ECC_lossyYCbCr, // Mode for color conversion for compression, Unused for decompression + EDC_photometricInterpretation, // Perform color space conversion from YCbCr to RGB if DICOM photometric interpretation indicates YCbCr + EUC_default, // Mode for UID creation, unused for decompression + EPC_default); // Automatically determine whether color-by-plane is required from the SOP Class UID and decompressed photometric interpretation + DJ_RPLossy representationParameter; + std::auto_ptr decoder; + + switch (syntax) + { + case EXS_JPEGProcess1: + LOG(INFO) << "Decoding a JPEG baseline (process 1) DICOM image"; + decoder.reset(new DJDecoderBaseline); + break; + + case EXS_JPEGProcess2_4 : + LOG(INFO) << "Decoding a JPEG baseline (processes 2 and 4) DICOM image"; + decoder.reset(new DJDecoderExtended); + break; + + case EXS_JPEGProcess6_8: // Retired + LOG(INFO) << "Decoding a JPEG spectral section, nonhierarchical (processes 6 and 8) DICOM image"; + decoder.reset(new DJDecoderSpectralSelection); + break; + + case EXS_JPEGProcess10_12: // Retired + LOG(INFO) << "Decoding a JPEG full progression, nonhierarchical (processes 10 and 12) DICOM image"; + decoder.reset(new DJDecoderProgressive); + break; + + case EXS_JPEGProcess14: + LOG(INFO) << "Decoding a JPEG lossless, nonhierarchical (process 14) DICOM image"; + decoder.reset(new DJDecoderLossless); + break; + + case EXS_JPEGProcess14SV1: + LOG(INFO) << "Decoding a JPEG lossless, nonhierarchical, first-order prediction (process 14 selection value 1) DICOM image"; + decoder.reset(new DJDecoderP14SV1); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + return ApplyCodec(*decoder, parameters, representationParameter, dataset, frame); + } +#endif + + + if (syntax == EXS_RLELossless) + { + LOG(INFO) << "Decoding a RLE lossless DICOM image"; + DcmRLECodecParameter parameters; + DcmRLECodecDecoder decoder; + DcmRLERepresentationParameter representationParameter; + return ApplyCodec(decoder, parameters, representationParameter, dataset, frame); + } + + + /** + * This DICOM image format is not natively supported by + * Orthanc. As a last resort, try and decode it through DCMTK by + * converting its transfer syntax to Little Endian. This will + * result in higher memory consumption. This is actually the + * second example of the following page: + * http://support.dcmtk.org/docs/mod_dcmjpeg.html#Examples + **/ + + { + LOG(INFO) << "Decoding a compressed image by converting its transfer syntax to Little Endian"; + + std::auto_ptr converted(dynamic_cast(dataset.clone())); + converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL); + + if (converted->canWriteXfer(EXS_LittleEndianExplicit)) + { + return DecodeUncompressedImage(*converted, frame); + } + } + + LOG(ERROR) << "Cannot decode a DICOM image with the built-in decoder"; + throw OrthancException(ErrorCode_BadFileFormat); + } + + + static bool IsColorImage(PixelFormat format) + { + return (format == PixelFormat_RGB24 || + format == PixelFormat_RGBA32); + } + + + bool DicomImageDecoder::TruncateDecodedImage(std::auto_ptr& image, + PixelFormat format, + bool allowColorConversion) + { + // If specified, prevent the conversion between color and + // grayscale images + bool isSourceColor = IsColorImage(image->GetFormat()); + bool isTargetColor = IsColorImage(format); + + if (!allowColorConversion) + { + if (isSourceColor ^ isTargetColor) + { + return false; + } + } + + if (image->GetFormat() != format) + { + // A conversion is required + std::auto_ptr target + (new Image(format, image->GetWidth(), image->GetHeight(), false)); + ImageProcessing::Convert(*target, *image); + image = target; + } + + return true; + } + + + bool DicomImageDecoder::PreviewDecodedImage(std::auto_ptr& image) + { + switch (image->GetFormat()) + { + case PixelFormat_RGB24: + { + // Directly return color images without modification (RGB) + return true; + } + + case PixelFormat_RGB48: + { + std::auto_ptr target + (new Image(PixelFormat_RGB24, image->GetWidth(), image->GetHeight(), false)); + ImageProcessing::Convert(*target, *image); + image = target; + return true; + } + + case PixelFormat_Grayscale8: + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + { + // Grayscale image: Stretch its dynamics to the [0,255] range + int64_t a, b; + ImageProcessing::GetMinMaxIntegerValue(a, b, *image); + + if (a == b) + { + ImageProcessing::Set(*image, 0); + } + else + { + ImageProcessing::ShiftScale(*image, static_cast(-a), + 255.0f / static_cast(b - a), + true /* TODO - Consider using "false" to speed up */); + } + + // If the source image is not grayscale 8bpp, convert it + if (image->GetFormat() != PixelFormat_Grayscale8) + { + std::auto_ptr target + (new Image(PixelFormat_Grayscale8, image->GetWidth(), image->GetHeight(), false)); + ImageProcessing::Convert(*target, *image); + image = target; + } + + return true; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void DicomImageDecoder::ApplyExtractionMode(std::auto_ptr& image, + ImageExtractionMode mode, + bool invert) + { + if (image.get() == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + bool ok = false; + + switch (mode) + { + case ImageExtractionMode_UInt8: + ok = TruncateDecodedImage(image, PixelFormat_Grayscale8, false); + break; + + case ImageExtractionMode_UInt16: + ok = TruncateDecodedImage(image, PixelFormat_Grayscale16, false); + break; + + case ImageExtractionMode_Int16: + ok = TruncateDecodedImage(image, PixelFormat_SignedGrayscale16, false); + break; + + case ImageExtractionMode_Preview: + ok = PreviewDecodedImage(image); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (ok) + { + assert(image.get() != NULL); + + if (invert) + { + Orthanc::ImageProcessing::Invert(*image); + } + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void DicomImageDecoder::ExtractPamImage(std::string& result, + std::auto_ptr& image, + ImageExtractionMode mode, + bool invert) + { + ApplyExtractionMode(image, mode, invert); + + PamWriter writer; + writer.WriteToMemory(result, *image); + } + +#if ORTHANC_ENABLE_PNG == 1 + void DicomImageDecoder::ExtractPngImage(std::string& result, + std::auto_ptr& image, + ImageExtractionMode mode, + bool invert) + { + ApplyExtractionMode(image, mode, invert); + + PngWriter writer; + writer.WriteToMemory(result, *image); + } +#endif + + +#if ORTHANC_ENABLE_JPEG == 1 + void DicomImageDecoder::ExtractJpegImage(std::string& result, + std::auto_ptr& 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 +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/Internals/DicomImageDecoder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,124 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../ParsedDicomFile.h" + +#include + +#if !defined(ORTHANC_ENABLE_JPEG) +# error The macro ORTHANC_ENABLE_JPEG must be defined +#endif + +#if !defined(ORTHANC_ENABLE_PNG) +# error The macro ORTHANC_ENABLE_PNG must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG) +# error The macro ORTHANC_ENABLE_DCMTK_JPEG must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS) +# error The macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined +#endif + + +class DcmDataset; +class DcmCodec; +class DcmCodecParameter; +class DcmRepresentationParameter; + +namespace Orthanc +{ + class DicomImageDecoder : public boost::noncopyable + { + private: + class ImageSource; + + DicomImageDecoder() // This is a fully abstract class, no constructor + { + } + + static ImageAccessor* CreateImage(DcmDataset& dataset, + bool ignorePhotometricInterpretation); + + static ImageAccessor* DecodeUncompressedImage(DcmDataset& dataset, + unsigned int frame); + + static ImageAccessor* ApplyCodec(const DcmCodec& codec, + const DcmCodecParameter& parameters, + const DcmRepresentationParameter& representationParameter, + DcmDataset& dataset, + unsigned int frame); + + static bool TruncateDecodedImage(std::auto_ptr& image, + PixelFormat format, + bool allowColorConversion); + + static bool PreviewDecodedImage(std::auto_ptr& image); + + static void ApplyExtractionMode(std::auto_ptr& image, + ImageExtractionMode mode, + bool invert); + + public: + static bool IsPsmctRle1(DcmDataset& dataset); + + static bool DecodePsmctRle1(std::string& output, + DcmDataset& dataset); + + static ImageAccessor *Decode(ParsedDicomFile& dicom, + unsigned int frame); + + static void ExtractPamImage(std::string& result, + std::auto_ptr& image, + ImageExtractionMode mode, + bool invert); + +#if ORTHANC_ENABLE_PNG == 1 + static void ExtractPngImage(std::string& result, + std::auto_ptr& image, + ImageExtractionMode mode, + bool invert); +#endif + +#if ORTHANC_ENABLE_JPEG == 1 + static void ExtractJpegImage(std::string& result, + std::auto_ptr& image, + ImageExtractionMode mode, + bool invert, + uint8_t quality); +#endif + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/ParsedDicomFile.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,1549 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: GDCM (Grassroots DICOM). A DICOM library + Module: http://gdcm.sourceforge.net/Copyright.html + + Copyright (c) 2006-2011 Mathieu Malaterre + Copyright (c) 1993-2005 CREATIS + (CREATIS = Centre de Recherche et d'Applications en Traitement de l'Image) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither name of Mathieu Malaterre, or CREATIS, nor the names of any + contributors (CNRS, INSERM, UCB, Universite Lyon I), may be used to + endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + =========================================================================*/ + + +#include "../PrecompiledHeaders.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "ParsedDicomFile.h" + +#include "FromDcmtkBridge.h" +#include "ToDcmtkBridge.h" +#include "Internals/DicomFrameIndex.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#if ORTHANC_ENABLE_JPEG == 1 +# include "../Images/JpegReader.h" +#endif + +#if ORTHANC_ENABLE_PNG == 1 +# include "../Images/PngReader.h" +#endif + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include +#include +#include + + +#if DCMTK_VERSION_NUMBER <= 360 +# define EXS_JPEGProcess1 EXS_JPEGProcess1TransferSyntax +#endif + + + +namespace Orthanc +{ + struct ParsedDicomFile::PImpl + { + std::auto_ptr file_; + std::auto_ptr frameIndex_; + }; + + +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 + static const char* CONTENT_TYPE_OCTET_STREAM = "application/octet-stream"; + + static void ParseTagAndGroup(DcmTagKey& key, + const std::string& tag) + { + DicomTag t = FromDcmtkBridge::ParseTag(tag); + key = DcmTagKey(t.GetGroup(), t.GetElement()); + } + + + static unsigned int GetPixelDataBlockCount(DcmPixelData& pixelData, + E_TransferSyntax transferSyntax) + { + DcmPixelSequence* pixelSequence = NULL; + if (pixelData.getEncapsulatedRepresentation + (transferSyntax, NULL, pixelSequence).good() && pixelSequence) + { + return pixelSequence->card(); + } + else + { + return 1; + } + } + + + static void SendPathValueForDictionary(RestApiOutput& output, + DcmItem& dicom) + { + Json::Value v = Json::arrayValue; + + for (unsigned long i = 0; i < dicom.card(); i++) + { + DcmElement* element = dicom.getElement(i); + if (element) + { + char buf[16]; + sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag()); + v.append(buf); + } + } + + output.AnswerJson(v); + } + + + static void SendSequence(RestApiOutput& output, + DcmSequenceOfItems& sequence) + { + // This element is a sequence + Json::Value v = Json::arrayValue; + + for (unsigned long i = 0; i < sequence.card(); i++) + { + v.append(boost::lexical_cast(i)); + } + + output.AnswerJson(v); + } + + + namespace + { + class DicomFieldStream : public IHttpStreamAnswer + { + private: + DcmElement& element_; + uint32_t length_; + uint32_t offset_; + std::string chunk_; + size_t chunkSize_; + + public: + DicomFieldStream(DcmElement& element, + E_TransferSyntax transferSyntax) : + element_(element), + length_(element.getLength(transferSyntax)), + offset_(0), + chunkSize_(0) + { + static const size_t CHUNK_SIZE = 64 * 1024; // Use chunks of max 64KB + chunk_.resize(CHUNK_SIZE); + } + + virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/, + bool /*deflateAllowed*/) + { + // No support for compression + return HttpCompression_None; + } + + virtual bool HasContentFilename(std::string& filename) + { + return false; + } + + virtual std::string GetContentType() + { + return ""; + } + + virtual uint64_t GetContentLength() + { + return length_; + } + + virtual bool ReadNextChunk() + { + assert(offset_ <= length_); + + if (offset_ == length_) + { + return false; + } + else + { + if (length_ - offset_ < chunk_.size()) + { + chunkSize_ = length_ - offset_; + } + else + { + chunkSize_ = chunk_.size(); + } + + OFCondition cond = element_.getPartialValue(&chunk_[0], offset_, chunkSize_); + + offset_ += chunkSize_; + + if (!cond.good()) + { + LOG(ERROR) << "Error while sending a DICOM field: " << cond.text(); + throw OrthancException(ErrorCode_InternalError); + } + + return true; + } + } + + virtual const char *GetChunkContent() + { + return chunk_.c_str(); + } + + virtual size_t GetChunkSize() + { + return chunkSize_; + } + }; + } + + + static bool AnswerPixelData(RestApiOutput& output, + DcmItem& dicom, + E_TransferSyntax transferSyntax, + const std::string* blockUri) + { + DcmTag k(DICOM_TAG_PIXEL_DATA.GetGroup(), + DICOM_TAG_PIXEL_DATA.GetElement()); + + DcmElement *element = NULL; + if (!dicom.findAndGetElement(k, element).good() || + element == NULL) + { + return false; + } + + try + { + DcmPixelData& pixelData = dynamic_cast(*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(i)); + } + + output.AnswerJson(result); + return true; + } + + + unsigned int block = boost::lexical_cast(*blockUri); + + if (block < GetPixelDataBlockCount(pixelData, transferSyntax)) + { + DcmPixelSequence* pixelSequence = NULL; + if (pixelData.getEncapsulatedRepresentation + (transferSyntax, NULL, pixelSequence).good() && pixelSequence) + { + // This is the case for JPEG transfer syntaxes + if (block < pixelSequence->card()) + { + DcmPixelItem* pixelItem = NULL; + if (pixelSequence->getItem(pixelItem, block).good() && pixelItem) + { + if (pixelItem->getLength() == 0) + { + output.AnswerBuffer(NULL, 0, CONTENT_TYPE_OCTET_STREAM); + return true; + } + + Uint8* buffer = NULL; + if (pixelItem->getUint8Array(buffer).good() && buffer) + { + output.AnswerBuffer(buffer, pixelItem->getLength(), CONTENT_TYPE_OCTET_STREAM); + return true; + } + } + } + } + else + { + // This is the case for raw, uncompressed image buffers + assert(*blockUri == "0"); + DicomFieldStream stream(*element, transferSyntax); + output.AnswerStream(stream); + } + } + } + catch (boost::bad_lexical_cast&) + { + // The URI entered by the user is not a number + } + catch (std::bad_cast&) + { + // This should never happen + } + + return false; + } + + + static void SendPathValueForLeaf(RestApiOutput& output, + const std::string& tag, + DcmItem& dicom, + E_TransferSyntax transferSyntax) + { + DcmTagKey k; + ParseTagAndGroup(k, tag); + + DcmSequenceOfItems* sequence = NULL; + if (dicom.findAndGetSequence(k, sequence).good() && + sequence != NULL && + sequence->getVR() == EVR_SQ) + { + SendSequence(output, *sequence); + return; + } + + DcmElement* element = NULL; + if (dicom.findAndGetElement(k, element).good() && + element != NULL && + //element->getVR() != EVR_UNKNOWN && // This would forbid private tags + element->getVR() != EVR_SQ) + { + DicomFieldStream stream(*element, transferSyntax); + output.AnswerStream(stream); + } + } +#endif + + + static inline uint16_t GetCharValue(char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + else + return 0; + } + + + static inline uint16_t GetTagValue(const char* c) + { + return ((GetCharValue(c[0]) << 12) + + (GetCharValue(c[1]) << 8) + + (GetCharValue(c[2]) << 4) + + GetCharValue(c[3])); + } + + +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 + void ParsedDicomFile::SendPathValue(RestApiOutput& output, + const UriComponents& uri) + { + DcmItem* dicom = pimpl_->file_->getDataset(); + E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer(); + + // Special case: Accessing the pixel data + if (uri.size() == 1 || + uri.size() == 2) + { + DcmTagKey tag; + ParseTagAndGroup(tag, uri[0]); + + if (tag.getGroup() == DICOM_TAG_PIXEL_DATA.GetGroup() && + tag.getElement() == DICOM_TAG_PIXEL_DATA.GetElement()) + { + AnswerPixelData(output, *dicom, transferSyntax, uri.size() == 1 ? NULL : &uri[1]); + return; + } + } + + // Go down in the tag hierarchy according to the URI + for (size_t pos = 0; pos < uri.size() / 2; pos++) + { + size_t index; + try + { + index = boost::lexical_cast(uri[2 * pos + 1]); + } + catch (boost::bad_lexical_cast&) + { + return; + } + + DcmTagKey k; + DcmItem *child = NULL; + ParseTagAndGroup(k, uri[2 * pos]); + if (!dicom->findAndGetSequenceItem(k, child, index).good() || + child == NULL) + { + return; + } + + dicom = child; + } + + // We have reached the end of the URI + if (uri.size() % 2 == 0) + { + SendPathValueForDictionary(output, *dicom); + } + else + { + SendPathValueForLeaf(output, uri.back(), *dicom, transferSyntax); + } + } +#endif + + + void ParsedDicomFile::Remove(const DicomTag& tag) + { + InvalidateCache(); + + DcmTagKey key(tag.GetGroup(), tag.GetElement()); + DcmElement* element = pimpl_->file_->getDataset()->remove(key); + if (element != NULL) + { + delete element; + } + } + + + void ParsedDicomFile::Clear(const DicomTag& tag, + bool onlyIfExists) + { + if (tag.GetElement() == 0x0000) + { + // Prevent manually modifying generic group length tags: This is + // handled by DCMTK serialization + return; + } + + InvalidateCache(); + + DcmItem* dicom = pimpl_->file_->getDataset(); + DcmTagKey key(tag.GetGroup(), tag.GetElement()); + + if (onlyIfExists && + !dicom->tagExists(key)) + { + // The tag is non-existing, do not clear it + } + else + { + if (!dicom->insertEmptyElement(key, OFTrue /* replace old value */).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + void ParsedDicomFile::RemovePrivateTagsInternal(const std::set* toKeep) + { + InvalidateCache(); + + DcmDataset& dataset = *pimpl_->file_->getDataset(); + + // Loop over the dataset to detect its private tags + typedef std::list Tags; + Tags privateTags; + + for (unsigned long i = 0; i < dataset.card(); i++) + { + DcmElement* element = dataset.getElement(i); + DcmTag tag(element->getTag()); + + // Is this a private tag? + if (tag.isPrivate()) + { + bool remove = true; + + // Check whether this private tag is to be kept + if (toKeep != NULL) + { + DicomTag tmp = FromDcmtkBridge::Convert(tag); + if (toKeep->find(tmp) != toKeep->end()) + { + remove = false; // Keep it + } + } + + if (remove) + { + privateTags.push_back(element); + } + } + } + + // Loop over the detected private tags to remove them + for (Tags::iterator it = privateTags.begin(); + it != privateTags.end(); ++it) + { + DcmElement* tmp = dataset.remove(*it); + if (tmp != NULL) + { + delete tmp; + } + } + } + + + static void InsertInternal(DcmDataset& dicom, + DcmElement* element) + { + OFCondition cond = dicom.insert(element, false, false); + if (!cond.good()) + { + // This field already exists + delete element; + throw OrthancException(ErrorCode_InternalError); + } + } + + + void ParsedDicomFile::Insert(const DicomTag& tag, + const Json::Value& value, + bool decodeDataUriScheme) + { + if (tag.GetElement() == 0x0000) + { + // Prevent manually modifying generic group length tags: This is + // handled by DCMTK serialization + return; + } + + if (pimpl_->file_->getDataset()->tagExists(ToDcmtkBridge::Convert(tag))) + { + throw OrthancException(ErrorCode_AlreadyExistingTag); + } + + if (decodeDataUriScheme && + value.type() == Json::stringValue && + (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT || + tag == DICOM_TAG_PIXEL_DATA)) + { + if (EmbedContentInternal(value.asString())) + { + return; + } + } + + InvalidateCache(); + std::auto_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding())); + InsertInternal(*pimpl_->file_->getDataset(), element.release()); + } + + + static bool CanReplaceProceed(DcmDataset& dicom, + const DcmTagKey& tag, + DicomReplaceMode mode) + { + if (dicom.findAndDeleteElement(tag).good()) + { + // This tag was existing, it has been deleted + return true; + } + else + { + // This tag was absent, act wrt. the specified "mode" + switch (mode) + { + case DicomReplaceMode_InsertIfAbsent: + return true; + + case DicomReplaceMode_ThrowIfAbsent: + throw OrthancException(ErrorCode_InexistentItem); + + case DicomReplaceMode_IgnoreIfAbsent: + return false; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } + + + void ParsedDicomFile::UpdateStorageUid(const DicomTag& tag, + const std::string& utf8Value, + bool decodeDataUriScheme) + { + if (tag != DICOM_TAG_SOP_CLASS_UID && + tag != DICOM_TAG_SOP_INSTANCE_UID) + { + return; + } + + std::string binary; + const std::string* decoded = &utf8Value; + + if (decodeDataUriScheme && + boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) + { + std::string mime; + if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + decoded = &binary; + } + else + { + Encoding encoding = GetEncoding(); + if (GetEncoding() != Encoding_Utf8) + { + binary = Toolbox::ConvertFromUtf8(utf8Value, encoding); + decoded = &binary; + } + } + + /** + * dcmodify will automatically correct 'Media Storage SOP Class + * UID' and 'Media Storage SOP Instance UID' in the metaheader, if + * you make changes to the related tags in the dataset ('SOP Class + * UID' and 'SOP Instance UID') via insert or modify mode + * options. You can disable this behaviour by using the -nmu + * option. + **/ + + if (tag == DICOM_TAG_SOP_CLASS_UID) + { + ReplacePlainString(DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, *decoded); + } + + if (tag == DICOM_TAG_SOP_INSTANCE_UID) + { + ReplacePlainString(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID, *decoded); + } + } + + + void ParsedDicomFile::Replace(const DicomTag& tag, + const std::string& utf8Value, + bool decodeDataUriScheme, + DicomReplaceMode mode) + { + if (tag.GetElement() == 0x0000) + { + // Prevent manually modifying generic group length tags: This is + // handled by DCMTK serialization + return; + } + + InvalidateCache(); + + DcmDataset& dicom = *pimpl_->file_->getDataset(); + if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode)) + { + // Either the tag was previously existing (and now removed), or + // the replace mode was set to "InsertIfAbsent" + + if (decodeDataUriScheme && + (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT || + tag == DICOM_TAG_PIXEL_DATA)) + { + if (EmbedContentInternal(utf8Value)) + { + return; + } + } + + std::auto_ptr element(FromDcmtkBridge::CreateElementForTag(tag)); + FromDcmtkBridge::FillElementWithString(*element, tag, utf8Value, decodeDataUriScheme, GetEncoding()); + + InsertInternal(dicom, element.release()); + UpdateStorageUid(tag, utf8Value, false); + } + } + + + void ParsedDicomFile::Replace(const DicomTag& tag, + const Json::Value& value, + bool decodeDataUriScheme, + DicomReplaceMode mode) + { + if (tag.GetElement() == 0x0000) + { + // Prevent manually modifying generic group length tags: This is + // handled by DCMTK serialization + return; + } + + InvalidateCache(); + + DcmDataset& dicom = *pimpl_->file_->getDataset(); + if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode)) + { + // Either the tag was previously existing (and now removed), or + // the replace mode was set to "InsertIfAbsent" + + if (decodeDataUriScheme && + value.type() == Json::stringValue && + (tag == DICOM_TAG_ENCAPSULATED_DOCUMENT || + tag == DICOM_TAG_PIXEL_DATA)) + { + if (EmbedContentInternal(value.asString())) + { + return; + } + } + + InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding())); + + if (tag == DICOM_TAG_SOP_CLASS_UID || + tag == DICOM_TAG_SOP_INSTANCE_UID) + { + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + UpdateStorageUid(tag, value.asString(), decodeDataUriScheme); + } + } + } + + +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 + void ParsedDicomFile::Answer(RestApiOutput& output) + { + std::string serialized; + if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset())) + { + output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM); + } + } +#endif + + + bool ParsedDicomFile::GetTagValue(std::string& value, + const DicomTag& tag) + { + DcmTagKey k(tag.GetGroup(), tag.GetElement()); + DcmDataset& dataset = *pimpl_->file_->getDataset(); + + if (tag.IsPrivate() || + FromDcmtkBridge::IsUnknownTag(tag) || + tag == DICOM_TAG_PIXEL_DATA || + tag == DICOM_TAG_ENCAPSULATED_DOCUMENT) + { + const Uint8* data = NULL; // This is freed in the destructor of the dataset + long unsigned int count = 0; + + if (dataset.findAndGetUint8Array(k, data, &count).good()) + { + if (count > 0) + { + assert(data != NULL); + value.assign(reinterpret_cast(data), count); + } + else + { + value.clear(); + } + + return true; + } + else + { + return false; + } + } + else + { + DcmElement* element = NULL; + if (!dataset.findAndGetElement(k, element).good() || + element == NULL) + { + return false; + } + + std::set tmp; + std::auto_ptr v(FromDcmtkBridge::ConvertLeafElement + (*element, DicomToJsonFlags_Default, + 0, GetEncoding(), tmp)); + + if (v.get() == NULL || + v->IsNull()) + { + value = ""; + } + else + { + // TODO v->IsBinary() + value = v->GetContent(); + } + + return true; + } + } + + + DicomInstanceHasher ParsedDicomFile::GetHasher() + { + std::string patientId, studyUid, seriesUid, instanceUid; + + if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID) || + !GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) || + !GetTagValue(seriesUid, DICOM_TAG_SERIES_INSTANCE_UID) || + !GetTagValue(instanceUid, DICOM_TAG_SOP_INSTANCE_UID)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid); + } + + + void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) + { + FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset()); + } + + +#if ORTHANC_SANDBOXED == 0 + void ParsedDicomFile::SaveToFile(const std::string& path) + { + // TODO Avoid using a temporary memory buffer, write directly on disk + std::string content; + SaveToMemoryBuffer(content); + SystemToolbox::WriteFile(content, path); + } +#endif + + + ParsedDicomFile::ParsedDicomFile(bool createIdentifiers) : pimpl_(new PImpl) + { + pimpl_->file_.reset(new DcmFileFormat); + + if (createIdentifiers) + { + ReplacePlainString(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient)); + ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study)); + ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series)); + ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance)); + } + } + + + void ParsedDicomFile::CreateFromDicomMap(const DicomMap& source, + Encoding defaultEncoding) + { + pimpl_->file_.reset(new DcmFileFormat); + + const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET); + + if (tmp == NULL) + { + SetEncoding(defaultEncoding); + } + else if (tmp->IsBinary()) + { + LOG(ERROR) << "Invalid binary string in the SpecificCharacterSet (0008,0005) tag"; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else if (tmp->IsNull() || + tmp->GetContent().empty()) + { + SetEncoding(defaultEncoding); + } + else + { + Encoding encoding; + + if (GetDicomEncoding(encoding, tmp->GetContent().c_str())) + { + SetEncoding(encoding); + } + else + { + LOG(ERROR) << "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \"" + << tmp->GetContent() << "\""; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + for (DicomMap::Map::const_iterator + it = source.map_.begin(); it != source.map_.end(); ++it) + { + if (it->first != DICOM_TAG_SPECIFIC_CHARACTER_SET && + !it->second->IsNull()) + { + ReplacePlainString(it->first, it->second->GetContent()); + } + } + } + + + ParsedDicomFile::ParsedDicomFile(const DicomMap& map, + Encoding defaultEncoding) : + pimpl_(new PImpl) + { + CreateFromDicomMap(map, defaultEncoding); + } + + + ParsedDicomFile::ParsedDicomFile(const DicomMap& map) : + pimpl_(new PImpl) + { + CreateFromDicomMap(map, GetDefaultDicomEncoding()); + } + + + ParsedDicomFile::ParsedDicomFile(const void* content, + size_t size) : pimpl_(new PImpl) + { + pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(content, size)); + } + + ParsedDicomFile::ParsedDicomFile(const std::string& content) : pimpl_(new PImpl) + { + if (content.size() == 0) + { + pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(NULL, 0)); + } + else + { + pimpl_->file_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(&content[0], content.size())); + } + } + + + ParsedDicomFile::ParsedDicomFile(ParsedDicomFile& other, + bool keepSopInstanceUid) : + pimpl_(new PImpl) + { + pimpl_->file_.reset(dynamic_cast(other.pimpl_->file_->clone())); + + if (!keepSopInstanceUid) + { + // Create a new instance-level identifier + ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance)); + } + } + + + ParsedDicomFile::ParsedDicomFile(DcmDataset& dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(new DcmFileFormat(&dicom)); + } + + + ParsedDicomFile::ParsedDicomFile(DcmFileFormat& dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(new DcmFileFormat(dicom)); + } + + + DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const + { + return *pimpl_->file_.get(); + } + + + ParsedDicomFile* ParsedDicomFile::Clone(bool keepSopInstanceUid) + { + return new ParsedDicomFile(*this, keepSopInstanceUid); + } + + + bool ParsedDicomFile::EmbedContentInternal(const std::string& dataUriScheme) + { + std::string mime, content; + if (!Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme)) + { + return false; + } + + Toolbox::ToLowerCase(mime); + + if (mime == "image/png") + { +#if ORTHANC_ENABLE_PNG == 1 + EmbedImage(mime, content); +#else + LOG(ERROR) << "Orthanc was compiled without support of PNG"; + throw OrthancException(ErrorCode_NotImplemented); +#endif + } + else if (mime == "image/jpeg") + { +#if ORTHANC_ENABLE_JPEG == 1 + EmbedImage(mime, content); +#else + LOG(ERROR) << "Orthanc was compiled without support of JPEG"; + throw OrthancException(ErrorCode_NotImplemented); +#endif + } + else if (mime == "application/pdf") + { + EmbedPdf(content); + } + else + { + LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file: " << mime; + throw OrthancException(ErrorCode_NotImplemented); + } + + return true; + } + + + void ParsedDicomFile::EmbedContent(const std::string& dataUriScheme) + { + if (!EmbedContentInternal(dataUriScheme)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + +#if (ORTHANC_ENABLE_JPEG == 1 && \ + ORTHANC_ENABLE_PNG == 1) + void ParsedDicomFile::EmbedImage(const std::string& mime, + const std::string& content) + { + if (mime == "image/png") + { + PngReader reader; + reader.ReadFromMemory(content); + EmbedImage(reader); + } + else if (mime == "image/jpeg") + { + JpegReader reader; + reader.ReadFromMemory(content); + EmbedImage(reader); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } +#endif + + + void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor) + { + if (accessor.GetFormat() != PixelFormat_Grayscale8 && + accessor.GetFormat() != PixelFormat_Grayscale16 && + accessor.GetFormat() != PixelFormat_SignedGrayscale16 && + accessor.GetFormat() != PixelFormat_RGB24 && + accessor.GetFormat() != PixelFormat_RGBA32) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + InvalidateCache(); + + if (accessor.GetFormat() == PixelFormat_RGBA32) + { + LOG(WARNING) << "Getting rid of the alpha channel when embedding a RGBA image inside DICOM"; + } + + // http://dicomiseasy.blogspot.be/2012/08/chapter-12-pixel-data.html + + Remove(DICOM_TAG_PIXEL_DATA); + ReplacePlainString(DICOM_TAG_COLUMNS, boost::lexical_cast(accessor.GetWidth())); + ReplacePlainString(DICOM_TAG_ROWS, boost::lexical_cast(accessor.GetHeight())); + ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "1"); + ReplacePlainString(DICOM_TAG_NUMBER_OF_FRAMES, "1"); + + if (accessor.GetFormat() == PixelFormat_SignedGrayscale16) + { + ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "1"); + } + else + { + ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "0"); // Unsigned pixels + } + + ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0"); // Color channels are interleaved + ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); + + unsigned int bytesPerPixel = 0; + + switch (accessor.GetFormat()) + { + case PixelFormat_Grayscale8: + ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8"); + ReplacePlainString(DICOM_TAG_BITS_STORED, "8"); + ReplacePlainString(DICOM_TAG_HIGH_BIT, "7"); + bytesPerPixel = 1; + break; + + case PixelFormat_RGB24: + case PixelFormat_RGBA32: + ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB"); + ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "3"); + ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8"); + ReplacePlainString(DICOM_TAG_BITS_STORED, "8"); + ReplacePlainString(DICOM_TAG_HIGH_BIT, "7"); + bytesPerPixel = 3; + break; + + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "16"); + ReplacePlainString(DICOM_TAG_BITS_STORED, "16"); + ReplacePlainString(DICOM_TAG_HIGH_BIT, "15"); + bytesPerPixel = 2; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + assert(bytesPerPixel != 0); + + DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), + DICOM_TAG_PIXEL_DATA.GetElement()); + + std::auto_ptr 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(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(accessor.GetConstRow(y)); + for (unsigned int x = 0; x < accessor.GetWidth(); x++, target += 3, source += 4) + { + target[0] = source[0]; + target[1] = source[1]; + target[2] = source[2]; + } + + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + if (!pimpl_->file_->getDataset()->insert(pixels.release(), false, false).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + + Encoding ParsedDicomFile::GetEncoding() const + { + return FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset(), + GetDefaultDicomEncoding()); + } + + + void ParsedDicomFile::SetEncoding(Encoding encoding) + { + if (encoding == Encoding_Windows1251) + { + // This Cyrillic codepage is not officially supported by the + // DICOM standard. Do not set the SpecificCharacterSet tag. + return; + } + + std::string s = GetDicomSpecificCharacterSet(encoding); + ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, s); + } + + void ParsedDicomFile::DatasetToJson(Json::Value& target, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength) + { + std::set ignoreTagLength; + FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), + format, flags, maxStringLength, + GetDefaultDicomEncoding(), ignoreTagLength); + } + + + void ParsedDicomFile::DatasetToJson(Json::Value& target, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + const std::set& ignoreTagLength) + { + FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), + format, flags, maxStringLength, + GetDefaultDicomEncoding(), ignoreTagLength); + } + + + void ParsedDicomFile::DatasetToJson(Json::Value& target, + const std::set& ignoreTagLength) + { + FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength); + } + + + void ParsedDicomFile::DatasetToJson(Json::Value& target) + { + const std::set ignoreTagLength; + FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength); + } + + + void ParsedDicomFile::HeaderToJson(Json::Value& target, + DicomToJsonFormat format) + { + FromDcmtkBridge::ExtractHeaderAsJson(target, *pimpl_->file_->getMetaInfo(), format, DicomToJsonFlags_None, 0); + } + + + bool ParsedDicomFile::HasTag(const DicomTag& tag) const + { + DcmTag key(tag.GetGroup(), tag.GetElement()); + return pimpl_->file_->getDataset()->tagExists(key); + } + + + void ParsedDicomFile::EmbedPdf(const std::string& pdf) + { + if (pdf.size() < 5 || // (*) + strncmp("%PDF-", pdf.c_str(), 5) != 0) + { + LOG(ERROR) << "Not a PDF file"; + throw OrthancException(ErrorCode_BadFileFormat); + } + + InvalidateCache(); + + ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage); + ReplacePlainString(FromDcmtkBridge::Convert(DCM_Modality), "OT"); + ReplacePlainString(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD"); + ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), "application/pdf"); + //ReplacePlainString(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1"); + + std::auto_ptr element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument)); + + size_t s = pdf.size(); + if (s & 1) + { + // The size of the buffer must be even + s += 1; + } + + Uint8* bytes = NULL; + OFCondition result = element->createUint8Array(s, bytes); + if (!result.good() || bytes == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + // Blank pad byte (no access violation, as "pdf.size() >= 5" because of (*) ) + bytes[s - 1] = 0; + + memcpy(bytes, pdf.c_str(), pdf.size()); + + DcmPolymorphOBOW* obj = element.release(); + result = pimpl_->file_->getDataset()->insert(obj); + + if (!result.good()) + { + delete obj; + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + + bool ParsedDicomFile::ExtractPdf(std::string& pdf) + { + std::string sop, mime; + + if (!GetTagValue(sop, DICOM_TAG_SOP_CLASS_UID) || + !GetTagValue(mime, FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument)) || + sop != UID_EncapsulatedPDFStorage || + mime != "application/pdf") + { + return false; + } + + if (!GetTagValue(pdf, DICOM_TAG_ENCAPSULATED_DOCUMENT)) + { + return false; + } + + // Strip the possible pad byte at the end of file, because the + // encapsulated documents must always have an even length. The PDF + // format expects files to end with %%EOF followed by CR/LF. If + // the last character of the file is not a CR or LF, we assume it + // is a pad byte and remove it. + if (pdf.size() > 0) + { + char last = *pdf.rbegin(); + + if (last != 10 && last != 13) + { + pdf.resize(pdf.size() - 1); + } + } + + return true; + } + + + ParsedDicomFile* ParsedDicomFile::CreateFromJson(const Json::Value& json, + DicomFromJsonFlags flags) + { + const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers) ? true : false; + const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme) ? true : false; + + std::auto_ptr result(new ParsedDicomFile(generateIdentifiers)); + result->SetEncoding(FromDcmtkBridge::ExtractEncoding(json, GetDefaultDicomEncoding())); + + const Json::Value::Members tags = json.getMemberNames(); + + for (size_t i = 0; i < tags.size(); i++) + { + DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]); + const Json::Value& value = json[tags[i]]; + + if (tag == DICOM_TAG_PIXEL_DATA || + tag == DICOM_TAG_ENCAPSULATED_DOCUMENT) + { + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest); + } + else + { + result->EmbedContent(value.asString()); + } + } + else if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + result->Replace(tag, value, decodeDataUriScheme, DicomReplaceMode_InsertIfAbsent); + } + } + + return result.release(); + } + + + void ParsedDicomFile::GetRawFrame(std::string& target, + std::string& mime, + unsigned int frameId) + { + if (pimpl_->frameIndex_.get() == NULL) + { + pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_)); + } + + pimpl_->frameIndex_->GetRawFrame(target, frameId); + + E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer(); + switch (transferSyntax) + { + case EXS_JPEGProcess1: + mime = "image/jpeg"; + break; + + case EXS_JPEG2000LosslessOnly: + case EXS_JPEG2000: + mime = "image/jp2"; + break; + + default: + mime = "application/octet-stream"; + break; + } + } + + + void ParsedDicomFile::InvalidateCache() + { + pimpl_->frameIndex_.reset(NULL); + } + + + unsigned int ParsedDicomFile::GetFramesCount() const + { + return DicomFrameIndex::GetFramesCount(*pimpl_->file_); + } + + + void ParsedDicomFile::ChangeEncoding(Encoding target) + { + Encoding source = GetEncoding(); + + if (source != target) // Avoid unnecessary conversion + { + ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target)); + FromDcmtkBridge::ChangeStringEncoding(*pimpl_->file_->getDataset(), source, target); + } + } + + + void ParsedDicomFile::ExtractDicomSummary(DicomMap& target) const + { + FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset()); + } + + + bool ParsedDicomFile::LookupTransferSyntax(std::string& result) + { + return FromDcmtkBridge::LookupTransferSyntax(result, *pimpl_->file_); + } + + + bool ParsedDicomFile::LookupPhotometricInterpretation(PhotometricInterpretation& result) const + { + DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(), + DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement()); + + DcmDataset& dataset = *pimpl_->file_->getDataset(); + + const char *c = NULL; + if (dataset.findAndGetString(k, c).good() && + c != NULL) + { + result = StringToPhotometricInterpretation(c); + return true; + } + else + { + return false; + } + } + + + void ParsedDicomFile::Apply(ITagVisitor& visitor) + { + FromDcmtkBridge::Apply(*pimpl_->file_->getDataset(), visitor, GetDefaultDicomEncoding()); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/ParsedDicomFile.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/ParsedDicomFile.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,234 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_JPEG) +# error Macro ORTHANC_ENABLE_JPEG must be defined to use this file +#endif + +#if !defined(ORTHANC_ENABLE_PNG) +# error Macro ORTHANC_ENABLE_PNG must be defined to use this file +#endif + +#if !defined(ORTHANC_ENABLE_CIVETWEB) +# error Macro ORTHANC_ENABLE_CIVETWEB must be defined to use this file +#endif + +#if !defined(ORTHANC_ENABLE_MONGOOSE) +# error Macro ORTHANC_ENABLE_MONGOOSE must be defined to use this file +#endif + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#include "ITagVisitor.h" +#include "../DicomFormat/DicomInstanceHasher.h" +#include "../Images/ImageAccessor.h" +#include "../IDynamicObject.h" +#include "../Toolbox.h" + +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 +# include "../RestApi/RestApiOutput.h" +#endif + +#include + + +class DcmDataset; +class DcmFileFormat; + +namespace Orthanc +{ + class ParsedDicomFile : public IDynamicObject + { + private: + struct PImpl; + boost::shared_ptr pimpl_; + + ParsedDicomFile(ParsedDicomFile& other, + bool keepSopInstanceUid); + + void CreateFromDicomMap(const DicomMap& source, + Encoding defaultEncoding); + + void RemovePrivateTagsInternal(const std::set* toKeep); + + void UpdateStorageUid(const DicomTag& tag, + const std::string& value, + bool decodeDataUriScheme); + + void InvalidateCache(); + + bool EmbedContentInternal(const std::string& dataUriScheme); + + public: + ParsedDicomFile(bool createIdentifiers); // Create a minimal DICOM instance + + ParsedDicomFile(const DicomMap& map, + Encoding defaultEncoding); + + ParsedDicomFile(const DicomMap& map); + + ParsedDicomFile(const void* content, + size_t size); + + ParsedDicomFile(const std::string& content); + + ParsedDicomFile(DcmDataset& dicom); + + ParsedDicomFile(DcmFileFormat& dicom); + + DcmFileFormat& GetDcmtkObject() const; + + ParsedDicomFile* Clone(bool keepSopInstanceUid); + +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 + void SendPathValue(RestApiOutput& output, + const UriComponents& uri); + + void Answer(RestApiOutput& output); +#endif + + void Remove(const DicomTag& tag); + + // Replace the DICOM tag as a NULL/empty value (e.g. for anonymization) + void Clear(const DicomTag& tag, + bool onlyIfExists); + + void Replace(const DicomTag& tag, + const std::string& utf8Value, + bool decodeDataUriScheme, + DicomReplaceMode mode); + + void Replace(const DicomTag& tag, + const Json::Value& value, // Assumed to be encoded with UTF-8 + bool decodeDataUriScheme, + DicomReplaceMode mode); + + void Insert(const DicomTag& tag, + const Json::Value& value, // Assumed to be encoded with UTF-8 + bool decodeDataUriScheme); + + void ReplacePlainString(const DicomTag& tag, + const std::string& utf8Value) + { + Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent); + } + + void RemovePrivateTags() + { + RemovePrivateTagsInternal(NULL); + } + + void RemovePrivateTags(const std::set& toKeep) + { + RemovePrivateTagsInternal(&toKeep); + } + + // WARNING: This function handles the decoding of strings to UTF8 + bool GetTagValue(std::string& value, + const DicomTag& tag); + + DicomInstanceHasher GetHasher(); + + void SaveToMemoryBuffer(std::string& buffer); + +#if ORTHANC_SANDBOXED == 0 + void SaveToFile(const std::string& path); +#endif + + void EmbedContent(const std::string& dataUriScheme); + + void EmbedImage(const ImageAccessor& accessor); + +#if (ORTHANC_ENABLE_JPEG == 1 && \ + ORTHANC_ENABLE_PNG == 1) + void EmbedImage(const std::string& mime, + const std::string& content); +#endif + + Encoding GetEncoding() const; + + // WARNING: This function only sets the encoding, it will not + // convert the encoding of the tags. Use "ChangeEncoding()" if need be. + void SetEncoding(Encoding encoding); + + void DatasetToJson(Json::Value& target, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength); + + void DatasetToJson(Json::Value& target, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength, + const std::set& ignoreTagLength); + + // This version uses the default parameters for + // FileContentType_DicomAsJson + void DatasetToJson(Json::Value& target, + const std::set& ignoreTagLength); + + void DatasetToJson(Json::Value& target); + + void HeaderToJson(Json::Value& target, + DicomToJsonFormat format); + + bool HasTag(const DicomTag& tag) const; + + void EmbedPdf(const std::string& pdf); + + bool ExtractPdf(std::string& pdf); + + void GetRawFrame(std::string& target, // OUT + std::string& mime, // OUT + unsigned int frameId); // IN + + unsigned int GetFramesCount() const; + + static ParsedDicomFile* CreateFromJson(const Json::Value& value, + DicomFromJsonFlags flags); + + void ChangeEncoding(Encoding target); + + void ExtractDicomSummary(DicomMap& target) const; + + bool LookupTransferSyntax(std::string& result); + + bool LookupPhotometricInterpretation(PhotometricInterpretation& result) const; + + void Apply(ITagVisitor& visitor); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/ToDcmtkBridge.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/ToDcmtkBridge.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,149 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "ToDcmtkBridge.h" + +#include + +#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); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/DicomParsing/ToDcmtkBridge.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/ToDcmtkBridge.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,55 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if ORTHANC_ENABLE_DCMTK != 1 +# error The macro ORTHANC_ENABLE_DCMTK must be set to 1 +#endif + +#include "../DicomFormat/DicomMap.h" +#include + +namespace Orthanc +{ + class ToDcmtkBridge + { + public: + static DcmTagKey Convert(const DicomTag& tag) + { + return DcmTagKey(tag.GetGroup(), tag.GetElement()); + } + + static DcmEVR Convert(ValueRepresentation vr); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Endianness.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Endianness.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,220 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 +#elif defined(__linux__) || defined(__EMSCRIPTEN__) +# define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1 +# include +#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 +#endif + + +/******************************************************************** + ** OPENBSD ARCHITECTURES + ********************************************************************/ + +#if defined(__OpenBSD__) +# define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1 +# include +#endif + + +/******************************************************************** + ** APPLE ARCHITECTURES (including OS X) + ********************************************************************/ + +#if defined(__APPLE__) +# define ORTHANC_HAS_BUILTIN_BYTE_SWAP 1 +# include +# 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 + +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(&a); + return (static_cast(p[0]) << 24 | + static_cast(p[1]) << 16 | + static_cast(p[2]) << 8 | + static_cast(p[3])); +} + +static inline uint64_t __orthanc_bswap64(uint64_t a) +{ + const uint8_t* p = reinterpret_cast(&a); + return (static_cast(p[0]) << 56 | + static_cast(p[1]) << 48 | + static_cast(p[2]) << 40 | + static_cast(p[3]) << 32 | + static_cast(p[4]) << 24 | + static_cast(p[5]) << 16 | + static_cast(p[6]) << 8 | + static_cast(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 diff -r 2b91363cc1d1 -r 497a637366b4 Core/EnumerationDictionary.h --- a/Core/EnumerationDictionary.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/EnumerationDictionary.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -60,6 +61,11 @@ stringToEnumeration_.clear(); } + bool Contains(Enumeration value) const + { + return enumerationToString_.find(value) != enumerationToString_.end(); + } + void Add(Enumeration value, const std::string& str) { // Check if these values are free @@ -80,10 +86,9 @@ { try { - int value = boost::lexical_cast(str); - return static_cast(value); + return static_cast(boost::lexical_cast(str)); } - catch (boost::bad_lexical_cast) + catch (boost::bad_lexical_cast&) { } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Enumerations.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -35,8 +36,11 @@ #include "OrthancException.h" #include "Toolbox.h" +#include "Logging.h" +#include #include +#include namespace Orthanc { @@ -62,7 +66,7 @@ return "Parameter out of range"; case ErrorCode_NotEnoughMemory: - return "Not enough memory"; + return "The server hosting Orthanc is running out of memory"; case ErrorCode_BadParameterType: return "Bad type for a parameter"; @@ -151,6 +155,18 @@ case ErrorCode_EmptyRequest: return "The request is empty"; + case ErrorCode_NotAcceptable: + return "Cannot send a response which is acceptable according to the Accept HTTP header"; + + case ErrorCode_NullPointer: + return "Cannot handle a NULL pointer"; + + case ErrorCode_DatabaseUnavailable: + return "The database is currently not available (probably a transient situation)"; + + case ErrorCode_CanceledJob: + return "This job was canceled"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -209,10 +225,10 @@ return "The specified path does not point to a directory"; case ErrorCode_HttpPortInUse: - return "The TCP port of the HTTP server is already in use"; + return "The TCP port of the HTTP server is privileged or already in use"; case ErrorCode_DicomPortInUse: - return "The TCP port of the DICOM server is already in use"; + return "The TCP port of the DICOM server is privileged or already in use"; case ErrorCode_BadHttpStatusInRest: return "This HTTP status is not allowed in a REST API"; @@ -322,6 +338,12 @@ case ErrorCode_CannotOrderSlices: return "Unable to order the slices of the series"; + case ErrorCode_NoWorklistHandler: + return "No request handler factory for DICOM C-Find Modality SCP"; + + case ErrorCode_AlreadyExistingTag: + return "Cannot override the value of a tag that already exists"; + default: if (error >= ErrorCode_START_PLUGINS) { @@ -621,10 +643,10 @@ return "RGB"; case PhotometricInterpretation_Monochrome1: - return "Monochrome1"; + return "MONOCHROME1"; case PhotometricInterpretation_Monochrome2: - return "Monochrome2"; + return "MONOCHROME2"; case PhotometricInterpretation_ARGB: return "ARGB"; @@ -636,25 +658,25 @@ return "HSV"; case PhotometricInterpretation_Palette: - return "Palette color"; + return "PALETTE COLOR"; case PhotometricInterpretation_YBRFull: - return "YBR full"; + return "YBR_FULL"; case PhotometricInterpretation_YBRFull422: - return "YBR full 422"; + return "YBR_FULL_422"; case PhotometricInterpretation_YBRPartial420: - return "YBR partial 420"; + return "YBR_PARTIAL_420"; case PhotometricInterpretation_YBRPartial422: - return "YBR partial 422"; + return "YBR_PARTIAL_422"; case PhotometricInterpretation_YBR_ICT: - return "YBR ICT"; + return "YBR_ICT"; case PhotometricInterpretation_YBR_RCT: - return "YBR RCT"; + return "YBR_RCT"; case PhotometricInterpretation_Unknown: return "Unknown"; @@ -675,8 +697,8 @@ case RequestOrigin_DicomProtocol: return "DicomProtocol"; - case RequestOrigin_Http: - return "Http"; + case RequestOrigin_RestApi: + return "RestApi"; case RequestOrigin_Plugins: return "Plugins"; @@ -712,6 +734,290 @@ } + const char* EnumerationToString(PixelFormat format) + { + switch (format) + { + case PixelFormat_RGB24: + return "RGB24"; + + case PixelFormat_RGBA32: + return "RGBA32"; + + case PixelFormat_BGRA32: + return "BGRA32"; + + case PixelFormat_Grayscale8: + return "Grayscale (unsigned 8bpp)"; + + case PixelFormat_Grayscale16: + return "Grayscale (unsigned 16bpp)"; + + case PixelFormat_SignedGrayscale16: + return "Grayscale (signed 16bpp)"; + + case PixelFormat_Float32: + return "Grayscale (float 32bpp)"; + + case PixelFormat_Grayscale32: + return "Grayscale (unsigned 32bpp)"; + + case PixelFormat_Grayscale64: + return "Grayscale (unsigned 64bpp)"; + + case PixelFormat_RGB48: + return "RGB48"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* EnumerationToString(ModalityManufacturer manufacturer) + { + switch (manufacturer) + { + case ModalityManufacturer_Generic: + return "Generic"; + + case ModalityManufacturer_GenericNoWildcardInDates: + return "GenericNoWildcardInDates"; + + case ModalityManufacturer_GenericNoUniversalWildcard: + return "GenericNoUniversalWildcard"; + + case ModalityManufacturer_StoreScp: + return "StoreScp"; + + case ModalityManufacturer_ClearCanvas: + return "ClearCanvas"; + + case ModalityManufacturer_Dcm4Chee: + return "Dcm4Chee"; + + case ModalityManufacturer_Vitrea: + return "Vitrea"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* EnumerationToString(DicomRequestType type) + { + switch (type) + { + case DicomRequestType_Echo: + return "Echo"; + break; + + case DicomRequestType_Find: + return "Find"; + break; + + case DicomRequestType_Get: + return "Get"; + break; + + case DicomRequestType_Move: + return "Move"; + break; + + case DicomRequestType_Store: + return "Store"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* EnumerationToString(TransferSyntax syntax) + { + switch (syntax) + { + case TransferSyntax_Deflated: + return "Deflated"; + + case TransferSyntax_Jpeg: + return "JPEG"; + + case TransferSyntax_Jpeg2000: + return "JPEG2000"; + + case TransferSyntax_JpegLossless: + return "JPEG Lossless"; + + case TransferSyntax_Jpip: + return "JPIP"; + + case TransferSyntax_Mpeg2: + return "MPEG2"; + + case TransferSyntax_Rle: + return "RLE"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* EnumerationToString(DicomVersion version) + { + switch (version) + { + case DicomVersion_2008: + return "2008"; + break; + + case DicomVersion_2017c: + return "2017c"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* EnumerationToString(ValueRepresentation vr) + { + switch (vr) + { + case ValueRepresentation_ApplicationEntity: // AE + return "AE"; + + case ValueRepresentation_AgeString: // AS + return "AS"; + + case ValueRepresentation_AttributeTag: // AT (2 x uint16_t) + return "AT"; + + case ValueRepresentation_CodeString: // CS + return "CS"; + + case ValueRepresentation_Date: // DA + return "DA"; + + case ValueRepresentation_DecimalString: // DS + return "DS"; + + case ValueRepresentation_DateTime: // DT + return "DT"; + + case ValueRepresentation_FloatingPointSingle: // FL (float) + return "FL"; + + case ValueRepresentation_FloatingPointDouble: // FD (double) + return "FD"; + + case ValueRepresentation_IntegerString: // IS + return "IS"; + + case ValueRepresentation_LongString: // LO + return "LO"; + + case ValueRepresentation_LongText: // LT + return "LT"; + + case ValueRepresentation_OtherByte: // OB + return "OB"; + + case ValueRepresentation_OtherDouble: // OD + return "OD"; + + case ValueRepresentation_OtherFloat: // OF + return "OF"; + + case ValueRepresentation_OtherLong: // OL + return "OL"; + + case ValueRepresentation_OtherWord: // OW + return "OW"; + + case ValueRepresentation_PersonName: // PN + return "PN"; + + case ValueRepresentation_ShortString: // SH + return "SH"; + + case ValueRepresentation_SignedLong: // SL (int32_t) + return "SL"; + + case ValueRepresentation_Sequence: // SQ + return "SQ"; + + case ValueRepresentation_SignedShort: // SS (int16_t) + return "SS"; + + case ValueRepresentation_ShortText: // ST + return "ST"; + + case ValueRepresentation_Time: // TM + return "TM"; + + case ValueRepresentation_UnlimitedCharacters: // UC + return "UC"; + + case ValueRepresentation_UniqueIdentifier: // UI (UID) + return "UI"; + + case ValueRepresentation_UnsignedLong: // UL (uint32_t) + return "UL"; + + case ValueRepresentation_Unknown: // UN + return "UN"; + + case ValueRepresentation_UniversalResource: // UR (URI or URL) + return "UR"; + + case ValueRepresentation_UnsignedShort: // US (uint16_t) + return "US"; + + case ValueRepresentation_UnlimitedText: // UT + return "UT"; + + case ValueRepresentation_NotSupported: + return "Not supported"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + const char* EnumerationToString(JobState state) + { + switch (state) + { + case JobState_Pending: + return "Pending"; + + case JobState_Running: + return "Running"; + + case JobState_Success: + return "Success"; + + case JobState_Failure: + return "Failure"; + + case JobState_Paused: + return "Paused"; + + case JobState_Retry: + return "Retry"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + Encoding StringToEncoding(const char* encoding) { std::string s(encoding); @@ -862,6 +1168,368 @@ } + ValueRepresentation StringToValueRepresentation(const std::string& vr, + bool throwIfUnsupported) + { + if (vr == "AE") + { + return ValueRepresentation_ApplicationEntity; + } + else if (vr == "AS") + { + return ValueRepresentation_AgeString; + } + else if (vr == "AT") + { + return ValueRepresentation_AttributeTag; + } + else if (vr == "CS") + { + return ValueRepresentation_CodeString; + } + else if (vr == "DA") + { + return ValueRepresentation_Date; + } + else if (vr == "DS") + { + return ValueRepresentation_DecimalString; + } + else if (vr == "DT") + { + return ValueRepresentation_DateTime; + } + else if (vr == "FL") + { + return ValueRepresentation_FloatingPointSingle; + } + else if (vr == "FD") + { + return ValueRepresentation_FloatingPointDouble; + } + else if (vr == "IS") + { + return ValueRepresentation_IntegerString; + } + else if (vr == "LO") + { + return ValueRepresentation_LongString; + } + else if (vr == "LT") + { + return ValueRepresentation_LongText; + } + else if (vr == "OB") + { + return ValueRepresentation_OtherByte; + } + else if (vr == "OD") + { + return ValueRepresentation_OtherDouble; + } + else if (vr == "OF") + { + return ValueRepresentation_OtherFloat; + } + else if (vr == "OL") + { + return ValueRepresentation_OtherLong; + } + else if (vr == "OW") + { + return ValueRepresentation_OtherWord; + } + else if (vr == "PN") + { + return ValueRepresentation_PersonName; + } + else if (vr == "SH") + { + return ValueRepresentation_ShortString; + } + else if (vr == "SL") + { + return ValueRepresentation_SignedLong; + } + else if (vr == "SQ") + { + return ValueRepresentation_Sequence; + } + else if (vr == "SS") + { + return ValueRepresentation_SignedShort; + } + else if (vr == "ST") + { + return ValueRepresentation_ShortText; + } + else if (vr == "TM") + { + return ValueRepresentation_Time; + } + else if (vr == "UC") + { + return ValueRepresentation_UnlimitedCharacters; + } + else if (vr == "UI") + { + return ValueRepresentation_UniqueIdentifier; + } + else if (vr == "UL") + { + return ValueRepresentation_UnsignedLong; + } + else if (vr == "UN") + { + return ValueRepresentation_Unknown; + } + else if (vr == "UR") + { + return ValueRepresentation_UniversalResource; + } + else if (vr == "US") + { + return ValueRepresentation_UnsignedShort; + } + else if (vr == "UT") + { + return ValueRepresentation_UnlimitedText; + } + else + { + std::string s = "Unsupported value representation encountered: " + vr; + + if (throwIfUnsupported) + { + LOG(ERROR) << s; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + LOG(INFO) << s; + return ValueRepresentation_NotSupported; + } + } + } + + + PhotometricInterpretation StringToPhotometricInterpretation(const char* value) + { + // http://dicom.nema.org/medical/dicom/2017a/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2 + std::string s(value); + + if (s == "MONOCHROME1") + { + return PhotometricInterpretation_Monochrome1; + } + + if (s == "MONOCHROME2") + { + return PhotometricInterpretation_Monochrome2; + } + + if (s == "PALETTE COLOR") + { + return PhotometricInterpretation_Palette; + } + + if (s == "RGB") + { + return PhotometricInterpretation_RGB; + } + + if (s == "HSV") + { + return PhotometricInterpretation_HSV; + } + + if (s == "ARGB") + { + return PhotometricInterpretation_ARGB; + } + + if (s == "CMYK") + { + return PhotometricInterpretation_CMYK; + } + + if (s == "YBR_FULL") + { + return PhotometricInterpretation_YBRFull; + } + + if (s == "YBR_FULL_422") + { + return PhotometricInterpretation_YBRFull422; + } + + if (s == "YBR_PARTIAL_422") + { + return PhotometricInterpretation_YBRPartial422; + } + + if (s == "YBR_PARTIAL_420") + { + return PhotometricInterpretation_YBRPartial420; + } + + if (s == "YBR_ICT") + { + return PhotometricInterpretation_YBR_ICT; + } + + if (s == "YBR_RCT") + { + return PhotometricInterpretation_YBR_RCT; + } + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer) + { + ModalityManufacturer result; + bool obsolete = false; + + if (manufacturer == "Generic") + { + return ModalityManufacturer_Generic; + } + else if (manufacturer == "GenericNoWildcardInDates") + { + return ModalityManufacturer_GenericNoWildcardInDates; + } + else if (manufacturer == "GenericNoUniversalWildcard") + { + return ModalityManufacturer_GenericNoUniversalWildcard; + } + else if (manufacturer == "ClearCanvas") + { + return ModalityManufacturer_ClearCanvas; + } + else if (manufacturer == "StoreScp") + { + return ModalityManufacturer_StoreScp; + } + else if (manufacturer == "Dcm4Chee") + { + return ModalityManufacturer_Dcm4Chee; + } + else if (manufacturer == "Vitrea") + { + return ModalityManufacturer_Vitrea; + } + else if (manufacturer == "AgfaImpax" || + manufacturer == "SyngoVia") + { + result = ModalityManufacturer_GenericNoWildcardInDates; + obsolete = true; + } + else if (manufacturer == "EFilm2" || + manufacturer == "MedInria") + { + result = ModalityManufacturer_Generic; + obsolete = true; + } + else + { + LOG(ERROR) << "Unknown modality manufacturer: \"" << manufacturer << "\""; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (obsolete) + { + LOG(WARNING) << "The \"" << manufacturer << "\" manufacturer is obsolete since " + << "Orthanc 1.3.0. To guarantee compatibility with future Orthanc " + << "releases, you should replace it by \"" + << EnumerationToString(result) + << "\" in your configuration file."; + } + + return result; + } + + + DicomVersion StringToDicomVersion(const std::string& version) + { + if (version == "2008") + { + return DicomVersion_2008; + } + else if (version == "2017c") + { + return DicomVersion_2017c; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + JobState StringToJobState(const std::string& state) + { + if (state == "Pending") + { + return JobState_Pending; + } + else if (state == "Running") + { + return JobState_Running; + } + else if (state == "Success") + { + return JobState_Success; + } + else if (state == "Failure") + { + return JobState_Failure; + } + else if (state == "Paused") + { + return JobState_Paused; + } + else if (state == "Retry") + { + return JobState_Retry; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + RequestOrigin StringToRequestOrigin(const std::string& origin) + { + if (origin == "Unknown") + { + return RequestOrigin_Unknown; + } + else if (origin == "DicomProtocol") + { + return RequestOrigin_DicomProtocol; + } + else if (origin == "RestApi") + { + return RequestOrigin_RestApi; + } + else if (origin == "Plugins") + { + return RequestOrigin_Plugins; + } + else if (origin == "Lua") + { + return RequestOrigin_Lua; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + unsigned int GetBytesPerPixel(PixelFormat format) { switch (format) @@ -877,8 +1545,20 @@ return 3; case PixelFormat_RGBA32: + case PixelFormat_BGRA32: + case PixelFormat_Grayscale32: return 4; + case PixelFormat_Float32: + assert(sizeof(float) == 4); + return 4; + + case PixelFormat_RGB48: + return 6; + + case PixelFormat_Grayscale64: + return 8; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -888,15 +1568,18 @@ bool GetDicomEncoding(Encoding& encoding, const char* specificCharacterSet) { - std::string s = specificCharacterSet; + std::string s = Toolbox::StripSpaces(specificCharacterSet); Toolbox::ToUpperCase(s); - // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2 // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-core/src/main/java/org/dcm4che3/data/SpecificCharacterSet.java if (s == "ISO_IR 6" || - s == "ISO_IR 192" || s == "ISO 2022 IR 6") { + encoding = Encoding_Ascii; + } + else if (s == "ISO_IR 192") + { encoding = Encoding_Utf8; } else if (s == "ISO_IR 100" || @@ -952,8 +1635,15 @@ { encoding = Encoding_Japanese; } - else if (s == "GB18030") + else if (s == "GB18030" || s == "GBK") { + /** + * According to tumashu@163.com, "In China, many dicom file's + * 0008,0005 tag is set as "GBK", instead of "GB18030", GBK is a + * subset of GB18030, and which is used frequently in China, + * suggest support it." + * https://groups.google.com/d/msg/orthanc-users/WMM8LMbjpUc/02-1f_yFCgAJ + **/ encoding = Encoding_Chinese; } /* @@ -980,39 +1670,6 @@ } - const char* GetMimeType(FileContentType type) - { - switch (type) - { - case FileContentType_Dicom: - return "application/dicom"; - - case FileContentType_DicomAsJson: - return "application/json"; - - default: - return "application/octet-stream"; - } - } - - - const char* GetFileExtension(FileContentType type) - { - switch (type) - { - case FileContentType_Dicom: - return ".dcm"; - - case FileContentType_DicomAsJson: - return ".json"; - - default: - // Unknown file type - return ""; - } - } - - ResourceType GetChildResourceType(ResourceType type) { switch (type) @@ -1073,11 +1730,13 @@ const char* GetDicomSpecificCharacterSet(Encoding encoding) { - // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2 switch (encoding) { + case Encoding_Ascii: + return "ISO_IR 6"; + case Encoding_Utf8: - case Encoding_Ascii: return "ISO_IR 192"; case Encoding_Latin1: @@ -1164,8 +1823,100 @@ case ErrorCode_Unauthorized: return HttpStatus_401_Unauthorized; + case ErrorCode_NotAcceptable: + return HttpStatus_406_NotAcceptable; + + case ErrorCode_DatabaseUnavailable: + return HttpStatus_503_ServiceUnavailable; + default: return HttpStatus_500_InternalServerError; } } + + + bool IsUserContentType(FileContentType type) + { + return (type >= FileContentType_StartUser && + type <= FileContentType_EndUser); + } + + + bool IsBinaryValueRepresentation(ValueRepresentation vr) + { + // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html + + switch (vr) + { + case ValueRepresentation_ApplicationEntity: // AE + case ValueRepresentation_AgeString: // AS + case ValueRepresentation_CodeString: // CS + case ValueRepresentation_Date: // DA + case ValueRepresentation_DecimalString: // DS + case ValueRepresentation_DateTime: // DT + case ValueRepresentation_IntegerString: // IS + case ValueRepresentation_LongString: // LO + case ValueRepresentation_LongText: // LT + case ValueRepresentation_PersonName: // PN + case ValueRepresentation_ShortString: // SH + case ValueRepresentation_ShortText: // ST + case ValueRepresentation_Time: // TM + case ValueRepresentation_UnlimitedCharacters: // UC + case ValueRepresentation_UniqueIdentifier: // UI (UID) + case ValueRepresentation_UniversalResource: // UR (URI or URL) + case ValueRepresentation_UnlimitedText: // UT + { + return false; + } + + /** + * Below are all the VR whose character repertoire is tagged as + * "not applicable" + **/ + case ValueRepresentation_AttributeTag: // AT (2 x uint16_t) + case ValueRepresentation_FloatingPointSingle: // FL (float) + case ValueRepresentation_FloatingPointDouble: // FD (double) + case ValueRepresentation_OtherByte: // OB + case ValueRepresentation_OtherDouble: // OD + case ValueRepresentation_OtherFloat: // OF + case ValueRepresentation_OtherLong: // OL + case ValueRepresentation_OtherWord: // OW + case ValueRepresentation_SignedLong: // SL (int32_t) + case ValueRepresentation_Sequence: // SQ + case ValueRepresentation_SignedShort: // SS (int16_t) + case ValueRepresentation_UnsignedLong: // UL (uint32_t) + case ValueRepresentation_Unknown: // UN + case ValueRepresentation_UnsignedShort: // US (uint16_t) + { + return true; + } + + case ValueRepresentation_NotSupported: + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static boost::mutex defaultEncodingMutex_; // Should not be necessary + static Encoding defaultEncoding_ = ORTHANC_DEFAULT_DICOM_ENCODING; + + Encoding GetDefaultDicomEncoding() + { + boost::mutex::scoped_lock lock(defaultEncodingMutex_); + return defaultEncoding_; + } + + void SetDefaultDicomEncoding(Encoding encoding) + { + std::string name = EnumerationToString(encoding); + + { + boost::mutex::scoped_lock lock(defaultEncodingMutex_); + defaultEncoding_ = encoding; + } + + LOG(INFO) << "Default encoding for DICOM was changed to: " << name; + } + } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Enumerations.h --- a/Core/Enumerations.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Enumerations.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,6 +33,18 @@ #pragma once +#include + + +#if defined(_MSC_VER) +# define ORTHANC_FORCE_INLINE __forceinline +#elif defined(__GNUC__) || defined(__clang__) || defined(__EMSCRIPTEN__) +# define ORTHANC_FORCE_INLINE inline __attribute((always_inline)) +#else +# error Please support your compiler here +#endif + + namespace Orthanc { enum Endianness @@ -50,7 +63,7 @@ ErrorCode_Plugin = 1 /*!< Error encountered within the plugin engine */, ErrorCode_NotImplemented = 2 /*!< Not implemented yet */, ErrorCode_ParameterOutOfRange = 3 /*!< Parameter out of range */, - ErrorCode_NotEnoughMemory = 4 /*!< Not enough memory */, + ErrorCode_NotEnoughMemory = 4 /*!< The server hosting Orthanc is running out of memory */, ErrorCode_BadParameterType = 5 /*!< Bad type for a parameter */, ErrorCode_BadSequenceOfCalls = 6 /*!< Bad sequence of calls */, ErrorCode_InexistentItem = 7 /*!< Accessing an inexistent item */, @@ -80,6 +93,10 @@ ErrorCode_DatabasePlugin = 31 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */, ErrorCode_StorageAreaPlugin = 32 /*!< Error in the plugin implementing a custom storage area */, ErrorCode_EmptyRequest = 33 /*!< The request is empty */, + ErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, + ErrorCode_NullPointer = 35 /*!< Cannot handle a NULL pointer */, + ErrorCode_DatabaseUnavailable = 36 /*!< The database is currently not available (probably a transient situation) */, + ErrorCode_CanceledJob = 37 /*!< This job was canceled */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, @@ -99,8 +116,8 @@ ErrorCode_DirectoryOverFile = 2000 /*!< The directory to be created is already occupied by a regular file */, ErrorCode_FileStorageCannotWrite = 2001 /*!< Unable to create a subdirectory or a file in the file storage */, ErrorCode_DirectoryExpected = 2002 /*!< The specified path does not point to a directory */, - ErrorCode_HttpPortInUse = 2003 /*!< The TCP port of the HTTP server is already in use */, - ErrorCode_DicomPortInUse = 2004 /*!< The TCP port of the DICOM server is already in use */, + ErrorCode_HttpPortInUse = 2003 /*!< The TCP port of the HTTP server is privileged or already in use */, + ErrorCode_DicomPortInUse = 2004 /*!< The TCP port of the DICOM server is privileged or already in use */, ErrorCode_BadHttpStatusInRest = 2005 /*!< This HTTP status is not allowed in a REST API */, ErrorCode_RegularFileExpected = 2006 /*!< The specified path does not point to a regular file */, ErrorCode_PathToExecutable = 2007 /*!< Unable to get the path to the executable */, @@ -137,6 +154,8 @@ ErrorCode_DatabaseNotInitialized = 2038 /*!< Plugin trying to call the database during its initialization */, ErrorCode_SslDisabled = 2039 /*!< Orthanc has been built without SSL support */, ErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, + ErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, + ErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, ErrorCode_START_PLUGINS = 1000000 }; @@ -184,7 +203,35 @@ * {summary}{Graylevel, signed 16bpp image.} * {description}{The image is graylevel. Each pixel is signed and stored in two bytes.} **/ - PixelFormat_SignedGrayscale16 = 5 + PixelFormat_SignedGrayscale16 = 5, + + /** + * {summary}{Graylevel, floating-point image.} + * {description}{The image is graylevel. Each pixel is floating-point and stored in 4 bytes.} + **/ + PixelFormat_Float32 = 6, + + // This is the memory layout for Cairo (for internal use in Stone of Orthanc) + PixelFormat_BGRA32 = 7, + + /** + * {summary}{Graylevel, unsigned 32bpp image.} + * {description}{The image is graylevel. Each pixel is unsigned and stored in 4 bytes.} + **/ + PixelFormat_Grayscale32 = 8, + + /** + * {summary}{Color image in RGB48 format.} + * {description}{This format describes a color image. The pixels are stored in 6 + * consecutive bytes. The memory layout is RGB.} + **/ + PixelFormat_RGB48 = 9, + + /** + * {summary}{Graylevel, unsigned 64bpp image.} + * {description}{The image is graylevel. Each pixel is unsigned and stored in 4 bytes.} + **/ + PixelFormat_Grayscale64 = 10 }; @@ -312,7 +359,8 @@ }; - // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ + // Specific Character Sets + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2 enum Encoding { Encoding_Ascii, @@ -336,7 +384,7 @@ }; - // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.2/ + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.2 enum PhotometricInterpretation { PhotometricInterpretation_ARGB, // Retired @@ -368,11 +416,169 @@ { RequestOrigin_Unknown, RequestOrigin_DicomProtocol, - RequestOrigin_Http, + RequestOrigin_RestApi, RequestOrigin_Plugins, RequestOrigin_Lua }; + enum ServerBarrierEvent + { + ServerBarrierEvent_Stop, + ServerBarrierEvent_Reload // SIGHUP signal: reload configuration file + }; + + enum FileMode + { + FileMode_ReadBinary, + FileMode_WriteBinary + }; + + /** + * The value representations Orthanc knows about. They correspond to + * the DICOM 2016b version of the standard. + * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html + **/ + enum ValueRepresentation + { + ValueRepresentation_ApplicationEntity = 1, // AE + ValueRepresentation_AgeString = 2, // AS + ValueRepresentation_AttributeTag = 3, // AT (2 x uint16_t) + ValueRepresentation_CodeString = 4, // CS + ValueRepresentation_Date = 5, // DA + ValueRepresentation_DecimalString = 6, // DS + ValueRepresentation_DateTime = 7, // DT + ValueRepresentation_FloatingPointSingle = 8, // FL (float) + ValueRepresentation_FloatingPointDouble = 9, // FD (double) + ValueRepresentation_IntegerString = 10, // IS + ValueRepresentation_LongString = 11, // LO + ValueRepresentation_LongText = 12, // LT + ValueRepresentation_OtherByte = 13, // OB + ValueRepresentation_OtherDouble = 14, // OD + ValueRepresentation_OtherFloat = 15, // OF + ValueRepresentation_OtherLong = 16, // OL + ValueRepresentation_OtherWord = 17, // OW + ValueRepresentation_PersonName = 18, // PN + ValueRepresentation_ShortString = 19, // SH + ValueRepresentation_SignedLong = 20, // SL (int32_t) + ValueRepresentation_Sequence = 21, // SQ + ValueRepresentation_SignedShort = 22, // SS (int16_t) + ValueRepresentation_ShortText = 23, // ST + ValueRepresentation_Time = 24, // TM + ValueRepresentation_UnlimitedCharacters = 25, // UC + ValueRepresentation_UniqueIdentifier = 26, // UI (UID) + ValueRepresentation_UnsignedLong = 27, // UL (uint32_t) + ValueRepresentation_Unknown = 28, // UN + ValueRepresentation_UniversalResource = 29, // UR (URI or URL) + ValueRepresentation_UnsignedShort = 30, // US (uint16_t) + ValueRepresentation_UnlimitedText = 31, // UT + ValueRepresentation_NotSupported // Not supported by Orthanc, or tag not in dictionary + }; + + enum DicomReplaceMode + { + DicomReplaceMode_InsertIfAbsent, + DicomReplaceMode_ThrowIfAbsent, + DicomReplaceMode_IgnoreIfAbsent + }; + + enum DicomToJsonFormat + { + DicomToJsonFormat_Full, + DicomToJsonFormat_Short, + DicomToJsonFormat_Human + }; + + enum DicomToJsonFlags + { + DicomToJsonFlags_IncludeBinary = (1 << 0), + DicomToJsonFlags_IncludePrivateTags = (1 << 1), + DicomToJsonFlags_IncludeUnknownTags = (1 << 2), + DicomToJsonFlags_IncludePixelData = (1 << 3), + DicomToJsonFlags_ConvertBinaryToAscii = (1 << 4), + DicomToJsonFlags_ConvertBinaryToNull = (1 << 5), + + // Some predefined combinations + DicomToJsonFlags_None = 0, + DicomToJsonFlags_Default = (DicomToJsonFlags_IncludeBinary | + DicomToJsonFlags_IncludePixelData | + DicomToJsonFlags_IncludePrivateTags | + DicomToJsonFlags_IncludeUnknownTags | + DicomToJsonFlags_ConvertBinaryToNull) + }; + + enum DicomFromJsonFlags + { + DicomFromJsonFlags_DecodeDataUriScheme = (1 << 0), + DicomFromJsonFlags_GenerateIdentifiers = (1 << 1), + + // Some predefined combinations + DicomFromJsonFlags_None = 0 + }; + + enum DicomVersion + { + DicomVersion_2008, + DicomVersion_2017c + }; + + enum ModalityManufacturer + { + ModalityManufacturer_Generic, + ModalityManufacturer_GenericNoWildcardInDates, + ModalityManufacturer_GenericNoUniversalWildcard, + ModalityManufacturer_StoreScp, + ModalityManufacturer_ClearCanvas, + ModalityManufacturer_Dcm4Chee, + ModalityManufacturer_Vitrea + }; + + enum DicomRequestType + { + DicomRequestType_Echo, + DicomRequestType_Find, + DicomRequestType_Get, + DicomRequestType_Move, + DicomRequestType_Store + }; + + enum TransferSyntax + { + TransferSyntax_Deflated, + TransferSyntax_Jpeg, + TransferSyntax_Jpeg2000, + TransferSyntax_JpegLossless, + TransferSyntax_Jpip, + TransferSyntax_Mpeg2, + TransferSyntax_Rle + }; + + enum JobState + { + JobState_Pending, + JobState_Running, + JobState_Success, + JobState_Failure, + JobState_Paused, + JobState_Retry + }; + + enum JobStepCode + { + JobStepCode_Success, + JobStepCode_Failure, + JobStepCode_Continue, + JobStepCode_Retry + }; + + enum JobStopReason + { + JobStopReason_Paused, + JobStopReason_Canceled, + JobStopReason_Success, + JobStopReason_Failure, + JobStopReason_Retry + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -441,23 +647,46 @@ const char* EnumerationToString(RequestOrigin origin); + const char* EnumerationToString(PixelFormat format); + + const char* EnumerationToString(ModalityManufacturer manufacturer); + + const char* EnumerationToString(DicomRequestType type); + + const char* EnumerationToString(TransferSyntax syntax); + + const char* EnumerationToString(DicomVersion version); + + const char* EnumerationToString(ValueRepresentation vr); + + const char* EnumerationToString(JobState state); + Encoding StringToEncoding(const char* encoding); ResourceType StringToResourceType(const char* type); ImageFormat StringToImageFormat(const char* format); - LogLevel StringToLogLevel(const char* format); + LogLevel StringToLogLevel(const char* level); + + ValueRepresentation StringToValueRepresentation(const std::string& vr, + bool throwIfUnsupported); + + PhotometricInterpretation StringToPhotometricInterpretation(const char* value); + ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer); + + DicomVersion StringToDicomVersion(const std::string& version); + + JobState StringToJobState(const std::string& state); + + RequestOrigin StringToRequestOrigin(const std::string& origin); + unsigned int GetBytesPerPixel(PixelFormat format); bool GetDicomEncoding(Encoding& encoding, const char* specificCharacterSet); - const char* GetMimeType(FileContentType type); - - const char* GetFileExtension(FileContentType type); - ResourceType GetChildResourceType(ResourceType type); ResourceType GetParentResourceType(ResourceType type); @@ -467,4 +696,12 @@ const char* GetDicomSpecificCharacterSet(Encoding encoding); HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error); + + bool IsUserContentType(FileContentType type); + + bool IsBinaryValueRepresentation(ValueRepresentation vr); + + Encoding GetDefaultDicomEncoding(); + + void SetDefaultDicomEncoding(Encoding encoding); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/FileInfo.h --- a/Core/FileStorage/FileInfo.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/FileStorage/FileInfo.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/FilesystemStorage.cpp --- a/Core/FileStorage/FilesystemStorage.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/FileStorage/FilesystemStorage.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -39,7 +40,7 @@ #include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" -#include "../Uuid.h" +#include "../SystemToolbox.h" #include @@ -83,14 +84,40 @@ //root_ = boost::filesystem::absolute(root).string(); root_ = root; - Toolbox::MakeDirectory(root); + SystemToolbox::MakeDirectory(root); } + + + static const char* GetDescriptionInternal(FileContentType content) + { + // This function is for logging only (internal use), a more + // fully-featured version is available in ServerEnumerations.cpp + switch (content) + { + case FileContentType_Unknown: + return "Unknown"; + + case FileContentType_Dicom: + return "DICOM"; + + case FileContentType_DicomAsJson: + return "JSON summary of DICOM"; + + default: + return "User-defined"; + } + } + + void FilesystemStorage::Create(const std::string& uuid, const void* content, size_t size, - FileContentType /*type*/) + FileContentType type) { + LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) + << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)"; + boost::filesystem::path path; path = GetPath(uuid); @@ -117,33 +144,19 @@ } } - boost::filesystem::ofstream f; - f.open(path, std::ofstream::out | std::ios::binary); - if (!f.good()) - { - throw OrthancException(ErrorCode_FileStorageCannotWrite); - } - - if (size != 0) - { - f.write(static_cast(content), size); - if (!f.good()) - { - f.close(); - throw OrthancException(ErrorCode_FileStorageCannotWrite); - } - } - - f.close(); + SystemToolbox::WriteFile(content, size, path.string()); } void FilesystemStorage::Read(std::string& content, const std::string& uuid, - FileContentType /*type*/) + FileContentType type) { + LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) + << "\" content type"; + content.clear(); - Toolbox::ReadFile(content, GetPath(uuid).string()); + SystemToolbox::ReadFile(content, GetPath(uuid).string()); } @@ -165,7 +178,7 @@ { for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current) { - if (fs::is_regular_file(current->status())) + if (SystemToolbox::IsRegularFile(current->path().string())) { try { @@ -186,7 +199,7 @@ } } } - catch (fs::filesystem_error) + catch (fs::filesystem_error&) { } } @@ -211,11 +224,9 @@ void FilesystemStorage::Remove(const std::string& uuid, - FileContentType /*type*/) + FileContentType type) { -#if ORTHANC_ENABLE_GOOGLE_LOG == 1 - LOG(INFO) << "Deleting file " << uuid; -#endif + LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast(type); namespace fs = boost::filesystem; diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/FilesystemStorage.h --- a/Core/FileStorage/FilesystemStorage.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/FileStorage/FilesystemStorage.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,6 +33,14 @@ #pragma once +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class FilesystemStorage cannot be used in sandboxed environments +#endif + #include "IStorageArea.h" #include @@ -52,7 +61,7 @@ boost::filesystem::path GetPath(const std::string& uuid) const; public: - FilesystemStorage(std::string root); + explicit FilesystemStorage(std::string root); virtual void Create(const std::string& uuid, const void* content, diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/IStorageArea.h --- a/Core/FileStorage/IStorageArea.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/FileStorage/IStorageArea.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/MemoryStorageArea.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/MemoryStorageArea.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,128 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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(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(content), size); + } + } + + + void MemoryStorageArea::Read(std::string& content, + const std::string& uuid, + FileContentType type) + { + LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" + << static_cast(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(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); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/MemoryStorageArea.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/FileStorage/MemoryStorageArea.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IStorageArea.h" + +#include +#include + +namespace Orthanc +{ + class MemoryStorageArea : public IStorageArea + { + private: + typedef std::map 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); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/StorageAccessor.cpp --- a/Core/FileStorage/StorageAccessor.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/FileStorage/StorageAccessor.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -35,7 +36,12 @@ #include "../Compression/ZlibCompressor.h" #include "../OrthancException.h" -#include "../HttpServer/HttpStreamTranscoder.h" +#include "../Toolbox.h" +#include "../Toolbox.h" + +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 +# include "../HttpServer/HttpStreamTranscoder.h" +#endif namespace Orthanc { @@ -140,33 +146,59 @@ } +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::SetupSender(BufferHttpSender& sender, - const FileInfo& info) + const FileInfo& info, + const std::string& mime) { area_.Read(sender.GetBuffer(), info.GetUuid(), info.GetContentType()); - sender.SetContentType(GetMimeType(info.GetContentType())); - sender.SetContentFilename(info.GetUuid() + std::string(GetFileExtension(info.GetContentType()))); + sender.SetContentType(mime); + + const char* extension; + switch (info.GetContentType()) + { + case FileContentType_Dicom: + extension = ".dcm"; + break; + + case FileContentType_DicomAsJson: + extension = ".json"; + break; + + default: + // Non-standard content type + extension = ""; + } + + sender.SetContentFilename(info.GetUuid() + std::string(extension)); } +#endif +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::AnswerFile(HttpOutput& output, - const FileInfo& info) + const FileInfo& info, + const std::string& mime) { BufferHttpSender sender; - SetupSender(sender, info); + SetupSender(sender, info, mime); HttpStreamTranscoder transcoder(sender, info.GetCompressionType()); output.Answer(transcoder); } +#endif +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::AnswerFile(RestApiOutput& output, - const FileInfo& info) + const FileInfo& info, + const std::string& mime) { BufferHttpSender sender; - SetupSender(sender, info); + SetupSender(sender, info, mime); HttpStreamTranscoder transcoder(sender, info.GetCompressionType()); output.AnswerStream(transcoder); } +#endif } diff -r 2b91363cc1d1 -r 497a637366b4 Core/FileStorage/StorageAccessor.h --- a/Core/FileStorage/StorageAccessor.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/FileStorage/StorageAccessor.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,10 +33,29 @@ #pragma once +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class StorageAccessor cannot be used in sandboxed environments +#endif + +#if !defined(ORTHANC_ENABLE_CIVETWEB) +# error Macro ORTHANC_ENABLE_CIVETWEB must be defined to use this file +#endif + +#if !defined(ORTHANC_ENABLE_MONGOOSE) +# error Macro ORTHANC_ENABLE_MONGOOSE must be defined to use this file +#endif + #include "IStorageArea.h" #include "FileInfo.h" -#include "../HttpServer/BufferHttpSender.h" -#include "../RestApi/RestApiOutput.h" + +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 +# include "../HttpServer/BufferHttpSender.h" +# include "../RestApi/RestApiOutput.h" +#endif #include #include @@ -50,8 +70,11 @@ private: IStorageArea& area_; +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void SetupSender(BufferHttpSender& sender, - const FileInfo& info); + const FileInfo& info, + const std::string& mime); +#endif public: StorageAccessor(IStorageArea& area) : area_(area) @@ -84,10 +107,14 @@ area_.Remove(info.GetUuid(), info.GetContentType()); } +#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void AnswerFile(HttpOutput& output, - const FileInfo& info); + const FileInfo& info, + const std::string& mime); void AnswerFile(RestApiOutput& output, - const FileInfo& info); + const FileInfo& info, + const std::string& mime); +#endif }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpClient.cpp --- a/Core/HttpClient.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpClient.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -36,15 +37,19 @@ #include "Toolbox.h" #include "OrthancException.h" #include "Logging.h" +#include "ChunkedBuffer.h" +#include "SystemToolbox.h" #include #include #include +#include -static std::string globalCACertificates_; -static bool globalVerifyPeers_ = true; -static long globalTimeout_ = 0; +#if ORTHANC_ENABLE_PKCS11 == 1 +# include "Pkcs11.h" +#endif + extern "C" { @@ -69,7 +74,12 @@ #endif static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status) { +#if ORTHANC_ENABLE_SSL == 1 return GetHttpStatus(curl_easy_perform(curl), curl, status); +#else + LOG(ERROR) << "Orthanc was compiled without SSL support, cannot make HTTPS request"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); +#endif } } @@ -77,14 +87,116 @@ namespace Orthanc { + class HttpClient::GlobalParameters + { + private: + boost::mutex mutex_; + bool httpsVerifyPeers_; + std::string httpsCACertificates_; + std::string proxy_; + long timeout_; + bool verbose_; + + GlobalParameters() : + httpsVerifyPeers_(true), + timeout_(0), + verbose_(false) + { + } + + public: + // Singleton pattern + static GlobalParameters& GetInstance() + { + static GlobalParameters parameters; + return parameters; + } + + void ConfigureSsl(bool httpsVerifyPeers, + const std::string& httpsCACertificates) + { + boost::mutex::scoped_lock lock(mutex_); + httpsVerifyPeers_ = httpsVerifyPeers; + httpsCACertificates_ = httpsCACertificates; + } + + void GetSslConfiguration(bool& httpsVerifyPeers, + std::string& httpsCACertificates) + { + boost::mutex::scoped_lock lock(mutex_); + httpsVerifyPeers = httpsVerifyPeers_; + httpsCACertificates = httpsCACertificates_; + } + + void SetDefaultProxy(const std::string& proxy) + { + LOG(INFO) << "Setting the default proxy for HTTP client connections: " << proxy; + + { + boost::mutex::scoped_lock lock(mutex_); + proxy_ = proxy; + } + } + + void GetDefaultProxy(std::string& target) + { + boost::mutex::scoped_lock lock(mutex_); + target = proxy_; + } + + void SetDefaultTimeout(long seconds) + { + LOG(INFO) << "Setting the default timeout for HTTP client connections: " << seconds << " seconds"; + + { + boost::mutex::scoped_lock lock(mutex_); + timeout_ = seconds; + } + } + + long GetDefaultTimeout() + { + boost::mutex::scoped_lock lock(mutex_); + return timeout_; + } + +#if ORTHANC_ENABLE_PKCS11 == 1 + bool IsPkcs11Initialized() + { + boost::mutex::scoped_lock lock(mutex_); + return Pkcs11::IsInitialized(); + } + + void InitializePkcs11(const std::string& module, + const std::string& pin, + bool verbose) + { + boost::mutex::scoped_lock lock(mutex_); + Pkcs11::Initialize(module, pin, verbose); + } +#endif + + bool IsDefaultVerbose() const + { + return verbose_; + } + + void SetDefaultVerbose(bool verbose) + { + verbose_ = verbose; + } + }; + + struct HttpClient::PImpl { CURL* curl_; - struct curl_slist *postHeaders_; + struct curl_slist *defaultPostHeaders_; + struct curl_slist *userHeaders_; }; - static void ThrowException(HttpStatus status) + void HttpClient::ThrowException(HttpStatus status) { switch (status) { @@ -92,10 +204,11 @@ throw OrthancException(ErrorCode_BadRequest); case HttpStatus_401_Unauthorized: + case HttpStatus_403_Forbidden: throw OrthancException(ErrorCode_Unauthorized); case HttpStatus_404_NotFound: - throw OrthancException(ErrorCode_InexistentItem); + throw OrthancException(ErrorCode_UnknownResource); default: throw OrthancException(ErrorCode_NetworkProtocol); @@ -103,9 +216,15 @@ } - static CURLcode CheckCode(CURLcode code) { + if (code == CURLE_NOT_BUILT_IN) + { + LOG(ERROR) << "Your libcurl does not contain a required feature, " + << "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF"; + throw OrthancException(ErrorCode_InternalError); + } + if (code != CURLE_OK) { LOG(ERROR) << "libCURL error: " + std::string(curl_easy_strerror(code)); @@ -116,27 +235,104 @@ } - static size_t CurlCallback(void *buffer, size_t size, size_t nmemb, void *payload) + static size_t CurlBodyCallback(void *buffer, size_t size, size_t nmemb, void *payload) { - std::string& target = *(static_cast(payload)); + ChunkedBuffer& target = *(static_cast(payload)); size_t length = size * nmemb; if (length == 0) + { return 0; + } + else + { + target.AddChunk(buffer, length); + return length; + } + } + - size_t pos = target.size(); + /*static int CurlDebugCallback(CURL *handle, + curl_infotype type, + char *data, + size_t size, + void *userptr) + { + switch (type) + { + case CURLINFO_TEXT: + case CURLINFO_HEADER_IN: + case CURLINFO_HEADER_OUT: + case CURLINFO_SSL_DATA_IN: + case CURLINFO_SSL_DATA_OUT: + case CURLINFO_END: + case CURLINFO_DATA_IN: + case CURLINFO_DATA_OUT: + { + std::string s(data, size); + LOG(INFO) << "libcurl: " << s; + break; + } + + default: + break; + } + + return 0; + }*/ + - target.resize(pos + length); - memcpy(&target.at(pos), buffer, length); + struct CurlHeaderParameters + { + bool lowerCase_; + HttpClient::HttpHeaders* headers_; + }; + + + static size_t CurlHeaderCallback(void *buffer, size_t size, size_t nmemb, void *payload) + { + CurlHeaderParameters& parameters = *(static_cast(payload)); + assert(parameters.headers_ != NULL); - return length; + size_t length = size * nmemb; + if (length == 0) + { + return 0; + } + else + { + std::string s(reinterpret_cast(buffer), length); + std::size_t colon = s.find(':'); + std::size_t eol = s.find("\r\n"); + if (colon != std::string::npos && + eol != std::string::npos) + { + std::string tmp(s.substr(0, colon)); + + if (parameters.lowerCase_) + { + Toolbox::ToLowerCase(tmp); + } + + std::string key = Toolbox::StripSpaces(tmp); + + if (!key.empty()) + { + std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol)); + (*parameters.headers_) [key] = value; + } + } + + return length; + } } void HttpClient::Setup() { - pimpl_->postHeaders_ = NULL; - if ((pimpl_->postHeaders_ = curl_slist_append(pimpl_->postHeaders_, "Expect:")) == NULL) + pimpl_->userHeaders_ = NULL; + pimpl_->defaultPostHeaders_ = NULL; + if ((pimpl_->defaultPostHeaders_ = curl_slist_append(pimpl_->defaultPostHeaders_, "Expect:")) == NULL) { throw OrthancException(ErrorCode_NotEnoughMemory); } @@ -144,11 +340,11 @@ pimpl_->curl_ = curl_easy_init(); if (!pimpl_->curl_) { - curl_slist_free_all(pimpl_->postHeaders_); + curl_slist_free_all(pimpl_->defaultPostHeaders_); throw OrthancException(ErrorCode_NotEnoughMemory); } - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlCallback)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlBodyCallback)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1)); @@ -160,30 +356,56 @@ url_ = ""; method_ = HttpMethod_Get; lastStatus_ = HttpStatus_200_Ok; - isVerbose_ = false; - timeout_ = globalTimeout_; - verifyPeers_ = globalVerifyPeers_; + SetVerbose(GlobalParameters::GetInstance().IsDefaultVerbose()); + timeout_ = GlobalParameters::GetInstance().GetDefaultTimeout(); + GlobalParameters::GetInstance().GetDefaultProxy(proxy_); + GlobalParameters::GetInstance().GetSslConfiguration(verifyPeers_, caCertificates_); } - HttpClient::HttpClient() : pimpl_(new PImpl) + HttpClient::HttpClient() : + pimpl_(new PImpl), + verifyPeers_(true), + pkcs11Enabled_(false), + headersToLowerCase_(true), + redirectionFollowed_(true) { Setup(); } - HttpClient::HttpClient(const HttpClient& other) : pimpl_(new PImpl) + HttpClient::HttpClient(const WebServiceParameters& service, + const std::string& uri) : + pimpl_(new PImpl), + verifyPeers_(true), + headersToLowerCase_(true), + redirectionFollowed_(true) { Setup(); - if (other.IsVerbose()) + if (service.GetUsername().size() != 0 && + service.GetPassword().size() != 0) { - SetVerbose(true); + SetCredentials(service.GetUsername().c_str(), + service.GetPassword().c_str()); } - if (other.credentials_.size() != 0) + if (!service.GetCertificateFile().empty()) { - credentials_ = other.credentials_; + SetClientCertificate(service.GetCertificateFile(), + service.GetCertificateKeyFile(), + service.GetCertificateKeyPassword()); + } + + SetPkcs11Enabled(service.IsPkcs11Enabled()); + + SetUrl(service.GetUrl() + uri); + + for (WebServiceParameters::Dictionary::const_iterator + it = service.GetHttpHeaders().begin(); + it != service.GetHttpHeaders().end(); ++it) + { + AddHeader(it->first, it->second); } } @@ -191,7 +413,8 @@ HttpClient::~HttpClient() { curl_easy_cleanup(pimpl_->curl_); - curl_slist_free_all(pimpl_->postHeaders_); + curl_slist_free_all(pimpl_->defaultPostHeaders_); + ClearHeaders(); } @@ -202,6 +425,7 @@ if (isVerbose_) { CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 1)); + //CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_DEBUGFUNCTION, &CurlDebugCallback)); } else { @@ -210,35 +434,140 @@ } - bool HttpClient::Apply(std::string& answer) + void HttpClient::AddHeader(const std::string& key, + const std::string& value) { - answer.clear(); - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str())); - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer)); + if (key.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + std::string s = key + ": " + value; + + if ((pimpl_->userHeaders_ = curl_slist_append(pimpl_->userHeaders_, s.c_str())) == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + + void HttpClient::ClearHeaders() + { + if (pimpl_->userHeaders_ != NULL) + { + curl_slist_free_all(pimpl_->userHeaders_); + pimpl_->userHeaders_ = NULL; + } + } + + bool HttpClient::ApplyInternal(std::string& answerBody, + HttpHeaders* answerHeaders) + { + answerBody.clear(); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str())); + + CurlHeaderParameters headerParameters; + + if (answerHeaders == NULL) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, NULL)); + } + else + { + headerParameters.lowerCase_ = headersToLowerCase_; + headerParameters.headers_ = answerHeaders; + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlHeaderCallback)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &headerParameters)); + } + +#if ORTHANC_ENABLE_SSL == 1 // Setup HTTPS-related options -#if ORTHANC_SSL_ENABLED == 1 - if (IsHttpsVerifyPeers()) + + if (verifyPeers_) { - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, GetHttpsCACertificates().c_str())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, caCertificates_.c_str())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 2)); // libcurl default is strict verifyhost CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1)); } else { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYHOST, 0)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 0)); } #endif + // Setup the HTTPS client certificate + if (!clientCertificateFile_.empty() && + pkcs11Enabled_) + { + LOG(ERROR) << "Cannot enable both client certificates and PKCS#11 authentication"; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (pkcs11Enabled_) + { +#if ORTHANC_ENABLE_PKCS11 == 1 + if (GlobalParameters::GetInstance().IsPkcs11Initialized()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLENGINE, Pkcs11::GetEngineIdentifier())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "ENG")); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "ENG")); + } + else + { + LOG(ERROR) << "Cannot use PKCS#11 for a HTTPS request, because it has not been initialized"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } +#else + LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11"; + throw OrthancException(ErrorCode_InternalError); +#endif + } + else if (!clientCertificateFile_.empty()) + { +#if ORTHANC_ENABLE_SSL == 1 + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "PEM")); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERT, clientCertificateFile_.c_str())); + + if (!clientCertificateKeyPassword_.empty()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_KEYPASSWD, clientCertificateKeyPassword_.c_str())); + } + + // NB: If no "clientKeyFile_" is provided, the key must be + // prepended to the certificate file + if (!clientCertificateKeyFile_.empty()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "PEM")); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str())); + } +#else + LOG(ERROR) << "This version of Orthanc is compiled without OpenSSL support, cannot use HTTPS client authentication"; + throw OrthancException(ErrorCode_InternalError); +#endif + } + // Reset the parameters from previous calls to Apply() - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, NULL)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->userHeaders_)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 0L)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 0L)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, NULL)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL)); - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0)); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0L)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, NULL)); + if (redirectionFollowed_) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1L)); + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 0L)); + } + // Set timeouts if (timeout_ <= 0) { @@ -269,7 +598,12 @@ case HttpMethod_Post: CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L)); - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_)); + + if (pimpl_->userHeaders_ == NULL) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->defaultPostHeaders_)); + } + break; case HttpMethod_Delete: @@ -284,7 +618,12 @@ // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L)); curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */ - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_)); + + if (pimpl_->userHeaders_ == NULL) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->defaultPostHeaders_)); + } + break; default: @@ -312,6 +651,9 @@ CURLcode code; long status = 0; + ChunkedBuffer buffer; + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &buffer)); + if (boost::starts_with(url_, "https://")) { code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status); @@ -321,6 +663,14 @@ code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status); } + LOG(INFO) << "HTTP status code " << status << " after " + << EnumerationToString(method_) << " request on: " << url_; + + if (isVerbose_) + { + LOG(INFO) << "cURL status code: " << code; + } + CheckCode(code); if (status == 0) @@ -333,17 +683,31 @@ lastStatus_ = static_cast(status); } - return (status >= 200 && status < 300); + bool success = (status >= 200 && status < 300); + + if (success) + { + buffer.Flatten(answerBody); + } + else + { + answerBody.clear(); + LOG(INFO) << "Error in HTTP request, received HTTP status " << status + << " (" << EnumerationToString(lastStatus_) << ")"; + } + + return success; } - bool HttpClient::Apply(Json::Value& answer) + bool HttpClient::ApplyInternal(Json::Value& answerBody, + HttpClient::HttpHeaders* answerHeaders) { std::string s; - if (Apply(s)) + if (ApplyInternal(s, answerHeaders)) { Json::Reader reader; - return reader.parse(s, answer); + return reader.parse(s, answerBody); } else { @@ -358,75 +722,148 @@ credentials_ = std::string(username) + ":" + std::string(password); } - - const std::string& HttpClient::GetHttpsCACertificates() const + + void HttpClient::ConfigureSsl(bool httpsVerifyPeers, + const std::string& httpsVerifyCertificates) { - if (caCertificates_.empty()) - { - return globalCACertificates_; - } - else - { - return caCertificates_; - } - } - - - void HttpClient::GlobalInitialize(bool httpsVerifyPeers, - const std::string& httpsVerifyCertificates) - { - globalVerifyPeers_ = httpsVerifyPeers; - globalCACertificates_ = httpsVerifyCertificates; - -#if ORTHANC_SSL_ENABLED == 1 +#if ORTHANC_ENABLE_SSL == 1 if (httpsVerifyPeers) { - if (globalCACertificates_.empty()) + if (httpsVerifyCertificates.empty()) { LOG(WARNING) << "No certificates are provided to validate peers, " << "set \"HttpsCACertificates\" if you need to do HTTPS requests"; } else { - LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << globalCACertificates_; + LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << httpsVerifyCertificates; } } else { - LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled!"; + LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled"; } #endif - CheckCode(curl_global_init(CURL_GLOBAL_DEFAULT)); + GlobalParameters::GetInstance().ConfigureSsl(httpsVerifyPeers, httpsVerifyCertificates); } + void HttpClient::GlobalInitialize() + { +#if ORTHANC_ENABLE_SSL == 1 + CheckCode(curl_global_init(CURL_GLOBAL_ALL)); +#else + CheckCode(curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_SSL)); +#endif + } + + void HttpClient::GlobalFinalize() { curl_global_cleanup(); + +#if ORTHANC_ENABLE_PKCS11 == 1 + Pkcs11::Finalize(); +#endif + } + + + void HttpClient::SetDefaultVerbose(bool verbose) + { + GlobalParameters::GetInstance().SetDefaultVerbose(verbose); + } + + + void HttpClient::SetDefaultProxy(const std::string& proxy) + { + GlobalParameters::GetInstance().SetDefaultProxy(proxy); + } + + + void HttpClient::SetDefaultTimeout(long timeout) + { + GlobalParameters::GetInstance().SetDefaultTimeout(timeout); + } + + + void HttpClient::ApplyAndThrowException(std::string& answerBody) + { + if (!Apply(answerBody)) + { + ThrowException(GetLastStatus()); + } } - void HttpClient::SetDefaultTimeout(long timeout) + void HttpClient::ApplyAndThrowException(Json::Value& answerBody) { - LOG(INFO) << "Setting the default timeout for HTTP client connections: " << timeout << " seconds"; - globalTimeout_ = timeout; + if (!Apply(answerBody)) + { + ThrowException(GetLastStatus()); + } } - void HttpClient::ApplyAndThrowException(std::string& answer) + void HttpClient::ApplyAndThrowException(std::string& answerBody, + HttpHeaders& answerHeaders) { - if (!Apply(answer)) + if (!Apply(answerBody, answerHeaders)) { ThrowException(GetLastStatus()); } } - void HttpClient::ApplyAndThrowException(Json::Value& answer) + + void HttpClient::ApplyAndThrowException(Json::Value& answerBody, + HttpHeaders& answerHeaders) { - if (!Apply(answer)) + if (!Apply(answerBody, answerHeaders)) { ThrowException(GetLastStatus()); } } + + + void HttpClient::SetClientCertificate(const std::string& certificateFile, + const std::string& certificateKeyFile, + const std::string& certificateKeyPassword) + { + if (certificateFile.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (!SystemToolbox::IsRegularFile(certificateFile)) + { + LOG(ERROR) << "Cannot open certificate file: " << certificateFile; + throw OrthancException(ErrorCode_InexistentFile); + } + + if (!certificateKeyFile.empty() && + !SystemToolbox::IsRegularFile(certificateKeyFile)) + { + LOG(ERROR) << "Cannot open key file: " << certificateKeyFile; + throw OrthancException(ErrorCode_InexistentFile); + } + + clientCertificateFile_ = certificateFile; + clientCertificateKeyFile_ = certificateKeyFile; + clientCertificateKeyPassword_ = certificateKeyPassword; + } + + + void HttpClient::InitializePkcs11(const std::string& module, + const std::string& pin, + bool verbose) + { +#if ORTHANC_ENABLE_PKCS11 == 1 + LOG(INFO) << "Initializing PKCS#11 using " << module + << (pin.empty() ? " (no PIN provided)" : " (PIN is provided)"); + GlobalParameters::GetInstance().InitializePkcs11(module, pin, verbose); +#else + LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11"; + throw OrthancException(ErrorCode_InternalError); +#endif + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpClient.h --- a/Core/HttpClient.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpClient.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -33,16 +34,31 @@ #pragma once #include "Enumerations.h" +#include "WebServiceParameters.h" #include #include #include +#if !defined(ORTHANC_ENABLE_SSL) +# error The macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if !defined(ORTHANC_ENABLE_PKCS11) +# error The macro ORTHANC_ENABLE_PKCS11 must be defined +#endif + + namespace Orthanc { class HttpClient { + public: + typedef std::map HttpHeaders; + private: + class GlobalParameters; + struct PImpl; boost::shared_ptr pimpl_; @@ -56,15 +72,29 @@ std::string proxy_; bool verifyPeers_; std::string caCertificates_; + std::string clientCertificateFile_; + std::string clientCertificateKeyFile_; + std::string clientCertificateKeyPassword_; + bool pkcs11Enabled_; + bool headersToLowerCase_; + bool redirectionFollowed_; void Setup(); - void operator= (const HttpClient&); // Forbidden + void operator= (const HttpClient&); // Assignment forbidden + HttpClient(const HttpClient& base); // Copy forbidden + + bool ApplyInternal(std::string& answerBody, + HttpHeaders* answerHeaders); + + bool ApplyInternal(Json::Value& answerBody, + HttpHeaders* answerHeaders); public: - HttpClient(const HttpClient& base); + HttpClient(); - HttpClient(); + HttpClient(const WebServiceParameters& service, + const std::string& uri); ~HttpClient(); @@ -125,9 +155,32 @@ return isVerbose_; } - bool Apply(std::string& answer); + void AddHeader(const std::string& key, + const std::string& value); + + void ClearHeaders(); + + bool Apply(std::string& answerBody) + { + return ApplyInternal(answerBody, NULL); + } - bool Apply(Json::Value& answer); + bool Apply(Json::Value& answerBody) + { + return ApplyInternal(answerBody, NULL); + } + + bool Apply(std::string& answerBody, + HttpHeaders& answerHeaders) + { + return ApplyInternal(answerBody, &answerHeaders); + } + + bool Apply(Json::Value& answerBody, + HttpHeaders& answerHeaders) + { + return ApplyInternal(answerBody, &answerHeaders); + } HttpStatus GetLastStatus() const { @@ -157,17 +210,87 @@ caCertificates_ = certificates; } - const std::string& GetHttpsCACertificates() const; + const std::string& GetHttpsCACertificates() const + { + return caCertificates_; + } + + void SetClientCertificate(const std::string& certificateFile, + const std::string& certificateKeyFile, + const std::string& certificateKeyPassword); + + void SetPkcs11Enabled(bool enabled) + { + pkcs11Enabled_ = enabled; + } + + bool IsPkcs11Enabled() const + { + return pkcs11Enabled_; + } + + const std::string& GetClientCertificateFile() const + { + return clientCertificateFile_; + } - static void GlobalInitialize(bool httpsVerifyPeers, - const std::string& httpsCACertificates); + const std::string& GetClientCertificateKeyFile() const + { + return clientCertificateKeyFile_; + } + + const std::string& GetClientCertificateKeyPassword() const + { + return clientCertificateKeyPassword_; + } + + void SetConvertHeadersToLowerCase(bool lowerCase) + { + headersToLowerCase_ = lowerCase; + } + + bool IsConvertHeadersToLowerCase() const + { + return headersToLowerCase_; + } + + void SetRedirectionFollowed(bool follow) + { + redirectionFollowed_ = follow; + } + + bool IsRedirectionFollowed() const + { + return redirectionFollowed_; + } + + static void GlobalInitialize(); static void GlobalFinalize(); + static void InitializePkcs11(const std::string& module, + const std::string& pin, + bool verbose); + + static void ConfigureSsl(bool httpsVerifyPeers, + const std::string& httpsCACertificates); + + static void SetDefaultVerbose(bool verbose); + + static void SetDefaultProxy(const std::string& proxy); + static void SetDefaultTimeout(long timeout); - void ApplyAndThrowException(std::string& answer); + void ApplyAndThrowException(std::string& answerBody); + + void ApplyAndThrowException(Json::Value& answerBody); - void ApplyAndThrowException(Json::Value& answer); + void ApplyAndThrowException(std::string& answerBody, + HttpHeaders& answerHeaders); + + void ApplyAndThrowException(Json::Value& answerBody, + HttpHeaders& answerHeaders); + + static void ThrowException(HttpStatus status); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/BufferHttpSender.cpp --- a/Core/HttpServer/BufferHttpSender.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/BufferHttpSender.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/BufferHttpSender.h --- a/Core/HttpServer/BufferHttpSender.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/BufferHttpSender.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/EmbeddedResourceHttpHandler.cpp --- a/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/EmbeddedResourceHttpHandler.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/EmbeddedResourceHttpHandler.h --- a/Core/HttpServer/EmbeddedResourceHttpHandler.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/EmbeddedResourceHttpHandler.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/FilesystemHttpHandler.cpp --- a/Core/HttpServer/FilesystemHttpHandler.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/FilesystemHttpHandler.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -34,6 +35,7 @@ #include "FilesystemHttpHandler.h" #include "../OrthancException.h" +#include "../SystemToolbox.h" #include "FilesystemHttpSender.h" #include @@ -95,8 +97,10 @@ #endif std::string h = Toolbox::FlattenUri(uri) + "/" + f; - if (fs::is_regular_file(it->status())) + if (SystemToolbox::IsRegularFile(it->path().string())) + { s += "
  • " + f + "
  • "; + } } s += " "; @@ -156,7 +160,7 @@ p /= uri[i]; } - if (fs::exists(p) && fs::is_regular_file(p)) + if (SystemToolbox::IsRegularFile(p.string())) { FilesystemHttpSender sender(p); output.Answer(sender); // TODO COMPRESSION diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/FilesystemHttpHandler.h --- a/Core/HttpServer/FilesystemHttpHandler.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/FilesystemHttpHandler.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/FilesystemHttpSender.cpp --- a/Core/HttpServer/FilesystemHttpSender.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/FilesystemHttpSender.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -69,7 +70,7 @@ throw OrthancException(ErrorCode_CorruptedFile); } - chunkSize_ = file_.gcount(); + chunkSize_ = static_cast(file_.gcount()); return chunkSize_ > 0; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/FilesystemHttpSender.h --- a/Core/HttpServer/FilesystemHttpSender.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/FilesystemHttpSender.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -50,12 +51,12 @@ void Initialize(const boost::filesystem::path& path); public: - FilesystemHttpSender(const std::string& path) + explicit FilesystemHttpSender(const std::string& path) { Initialize(path); } - FilesystemHttpSender(const boost::filesystem::path& path) + explicit FilesystemHttpSender(const boost::filesystem::path& path) { Initialize(path); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpContentNegociation.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpContentNegociation.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,266 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "HttpContentNegociation.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + +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(value); + ok = (quality >= 0.0f && quality <= 1.0f); + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + return quality; + } + else + { + LOG(ERROR) << "Quality parameter out of range in a HTTP request (must be between 0 and 1): " << value; + throw OrthancException(ErrorCode_BadRequest); + } + } + } + + return 1.0f; // Default quality + } + + + void HttpContentNegociation::SelectBestMatch(std::auto_ptr& best, + const Handler& handler, + const std::string& type, + const std::string& subtype, + float quality) + { + std::auto_ptr match(new Reference(handler, type, subtype, quality)); + + if (best.get() == NULL || + *best < *match) + { + best = match; + } + } + + + void HttpContentNegociation::Register(const std::string& mime, + IHandler& handler) + { + std::string type, subtype; + + if (SplitPair(type, subtype, mime, '/') && + type != "*" && + subtype != "*") + { + handlers_.push_back(Handler(type, subtype, handler)); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool HttpContentNegociation::Apply(const HttpHeaders& headers) + { + HttpHeaders::const_iterator accept = headers.find("accept"); + if (accept != headers.end()) + { + return Apply(accept->second); + } + else + { + return Apply("*/*"); + } + } + + + bool HttpContentNegociation::Apply(const std::string& accept) + { + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + // https://en.wikipedia.org/wiki/Content_negotiation + // http://www.newmediacampaigns.com/blog/browser-rest-http-accept-headers + + Tokens mediaRanges; + Toolbox::TokenizeString(mediaRanges, accept, ','); + + std::auto_ptr 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; + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpContentNegociation.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpContentNegociation.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,112 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + + +namespace Orthanc +{ + class HttpContentNegociation : public boost::noncopyable + { + public: + typedef std::map 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 Tokens; + typedef std::list Handlers; + + Handlers handlers_; + + + static bool SplitPair(std::string& first /* out */, + std::string& second /* out */, + const std::string& source, + char separator); + + static float GetQuality(const Tokens& parameters); + + static void SelectBestMatch(std::auto_ptr& 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); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpFileSender.cpp --- a/Core/HttpServer/HttpFileSender.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpFileSender.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpFileSender.h --- a/Core/HttpServer/HttpFileSender.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpFileSender.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpOutput.cpp --- a/Core/HttpServer/HttpOutput.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpOutput.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -291,8 +292,7 @@ const char* message, size_t messageSize) { - if (status == HttpStatus_200_Ok || - status == HttpStatus_301_MovedPermanently || + if (status == HttpStatus_301_MovedPermanently || status == HttpStatus_401_Unauthorized || status == HttpStatus_405_MethodNotAllowed) { @@ -300,7 +300,6 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - stateMachine_.ClearHeaders(); stateMachine_.SetHttpStatus(status); stateMachine_.SendBody(message, messageSize); } @@ -433,28 +432,103 @@ { if (!Toolbox::StartsWith(*it, "Set-Cookie: ")) { - LOG(ERROR) << "The only headers that can be set in multipart answers are Set-Cookie (here: " << *it << " is set)"; + LOG(ERROR) << "The only headers that can be set in multipart answers " + << "are Set-Cookie (here: " << *it << " is set)"; throw OrthancException(ErrorCode_BadSequenceOfCalls); } header += *it; } + /** + * Fix for issue 54 ("Decide what to do wrt. quoting of multipart + * answers"). The "type" parameter in the "Content-Type" HTTP + * header must be quoted if it contains a forward slash "/". This + * is necessary for DICOMweb compatibility with OsiriX, but breaks + * compatibility with old releases of the client in the Orthanc + * DICOMweb plugin <= 0.3 (releases >= 0.4 work fine). + * + * Full history is available at the following locations: + * - In changeset 2248:69b0f4e8a49b: + * # hg history -v -r 2248 + * - https://bitbucket.org/sjodogne/orthanc/issues/54/ + * - https://groups.google.com/d/msg/orthanc-users/65zhIM5xbKI/TU5Q1_LhAwAJ + **/ + std::string tmp; + if (contentType.find('/') == std::string::npos) + { + // No forward slash in the content type + tmp = contentType; + } + else + { + // Quote the content type because of the forward slash + tmp = "\"" + contentType + "\""; + } + multipartBoundary_ = Toolbox::GenerateUuid(); multipartContentType_ = contentType; - header += "Content-Type: multipart/related; type=multipart/" + subType + "; boundary=" + multipartBoundary_ + "\r\n\r\n"; + header += ("Content-Type: multipart/" + subType + "; type=" + + tmp + "; boundary=" + multipartBoundary_ + "\r\n\r\n"); stream_.Send(true, header.c_str(), header.size()); state_ = State_WritingMultipart; } - void HttpOutput::StateMachine::SendMultipartItem(const void* item, size_t length) + void HttpOutput::StateMachine::SendMultipartItem(const void* item, + size_t length, + const std::map& headers) { - std::string header = "--" + multipartBoundary_ + "\n"; - header += "Content-Type: " + multipartContentType_ + "\n"; - header += "Content-Length: " + boost::lexical_cast(length) + "\n"; - header += "MIME-Version: 1.0\n\n"; + if (state_ != State_WritingMultipart) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + std::string header = "--" + multipartBoundary_ + "\r\n"; + + bool hasContentType = false; + bool hasContentLength = false; + bool hasMimeVersion = false; + + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + header += it->first + ": " + it->second + "\r\n"; + + std::string tmp; + Toolbox::ToLowerCase(tmp, it->first); + + if (tmp == "content-type") + { + hasContentType = true; + } + + if (tmp == "content-length") + { + hasContentLength = true; + } + + if (tmp == "mime-version") + { + hasMimeVersion = true; + } + } + + if (!hasContentType) + { + header += "Content-Type: " + multipartContentType_ + "\r\n"; + } + + if (!hasContentLength) + { + header += "Content-Length: " + boost::lexical_cast(length) + "\r\n"; + } + + if (!hasMimeVersion) + { + header += "MIME-Version: 1.0\r\n\r\n"; + } stream_.Send(false, header.c_str(), header.size()); @@ -463,7 +537,7 @@ stream_.Send(false, item, length); } - stream_.Send(false, "\n", 1); + stream_.Send(false, "\r\n", 2); } @@ -478,7 +552,7 @@ // closed the connection. Such an error is ignored. try { - std::string header = "--" + multipartBoundary_ + "--\n"; + std::string header = "--" + multipartBoundary_ + "--\r\n"; stream_.Send(false, header.c_str(), header.size()); } catch (OrthancException&) @@ -489,19 +563,6 @@ } - void HttpOutput::SendMultipartItem(const std::string& item) - { - if (item.size() > 0) - { - stateMachine_.SendMultipartItem(item.c_str(), item.size()); - } - else - { - stateMachine_.SendMultipartItem(NULL, 0); - } - } - - void HttpOutput::Answer(IHttpStreamAnswer& stream) { HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_); diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpOutput.h --- a/Core/HttpServer/HttpOutput.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpOutput.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,13 +33,14 @@ #pragma once +#include "../Enumerations.h" +#include "IHttpOutputStream.h" +#include "IHttpStreamAnswer.h" + #include #include #include -#include "../Enumerations.h" -#include "IHttpOutputStream.h" -#include "IHttpStreamAnswer.h" -#include "../Uuid.h" +#include namespace Orthanc { @@ -99,7 +101,9 @@ void StartMultipart(const std::string& subType, const std::string& contentType); - void SendMultipartItem(const void* item, size_t length); + void SendMultipartItem(const void* item, + size_t length, + const std::map& headers); void CloseMultipart(); @@ -202,11 +206,11 @@ stateMachine_.StartMultipart(subType, contentType); } - void SendMultipartItem(const std::string& item); - - void SendMultipartItem(const void* item, size_t size) + void SendMultipartItem(const void* item, + size_t size, + const std::map& headers) { - stateMachine_.SendMultipartItem(item, size); + stateMachine_.SendMultipartItem(item, size, headers); } void CloseMultipart() diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpStreamTranscoder.cpp --- a/Core/HttpServer/HttpStreamTranscoder.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpStreamTranscoder.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpStreamTranscoder.h --- a/Core/HttpServer/HttpStreamTranscoder.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpStreamTranscoder.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -61,6 +62,7 @@ sourceCompression_(compression), bytesToSkip_(0), skipped_(0), + currentChunkOffset_(0), ready_(false) { } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpToolbox.cpp --- a/Core/HttpServer/HttpToolbox.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpToolbox.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -41,7 +42,7 @@ #include "StringHttpOutput.h" -static const char* LOCALHOST = "localhost"; +static const char* LOCALHOST = "127.0.0.1"; @@ -201,10 +202,9 @@ bool HttpToolbox::SimpleGet(std::string& result, IHttpHandler& handler, RequestOrigin origin, - const std::string& uri) + const std::string& uri, + const IHttpHandler::Arguments& httpHeaders) { - IHttpHandler::Arguments headers; // No HTTP header - UriComponents curi; IHttpHandler::GetArguments getArguments; ParseGetQuery(curi, getArguments, uri.c_str()); @@ -213,7 +213,7 @@ HttpOutput http(stream, false /* no keep alive */); if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, - headers, getArguments, NULL /* no body for GET */, 0)) + httpHeaders, getArguments, NULL /* no body for GET */, 0)) { stream.GetOutput(result); return true; @@ -225,6 +225,16 @@ } + bool HttpToolbox::SimpleGet(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri) + { + IHttpHandler::Arguments headers; // No HTTP header + return SimpleGet(result, handler, origin, uri, headers); + } + + static bool SimplePostOrPut(std::string& result, IHttpHandler& handler, RequestOrigin origin, diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/HttpToolbox.h --- a/Core/HttpServer/HttpToolbox.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/HttpToolbox.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -65,6 +66,12 @@ RequestOrigin origin, const std::string& uri); + static bool SimpleGet(std::string& result, + IHttpHandler& handler, + RequestOrigin origin, + const std::string& uri, + const IHttpHandler::Arguments& httpHeaders); + static bool SimplePost(std::string& result, IHttpHandler& handler, RequestOrigin origin, diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/IHttpHandler.h --- a/Core/HttpServer/IHttpHandler.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/IHttpHandler.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,7 +33,7 @@ #pragma once -#include "../Enumerations.h" +#include "../Toolbox.h" #include "HttpOutput.h" #include diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/IHttpOutputStream.h --- a/Core/HttpServer/IHttpOutputStream.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/IHttpOutputStream.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/IHttpStreamAnswer.h --- a/Core/HttpServer/IHttpStreamAnswer.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/IHttpStreamAnswer.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/IIncomingHttpRequestFilter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/IIncomingHttpRequestFilter.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/MongooseServer.cpp --- a/Core/HttpServer/MongooseServer.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/MongooseServer.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -38,7 +39,17 @@ #include "../Logging.h" #include "../ChunkedBuffer.h" #include "HttpToolbox.h" -#include "mongoose.h" + +#if ORTHANC_ENABLE_MONGOOSE == 1 +# include "mongoose.h" + +#elif ORTHANC_ENABLE_CIVETWEB == 1 +# include "civetweb.h" +# define MONGOOSE_USE_CALLBACKS 1 + +#else +# error "Either Mongoose or Civetweb must be enabled to compile this file" +#endif #include #include @@ -49,14 +60,16 @@ #include #include -#if ORTHANC_SSL_ENABLED == 1 +#if !defined(ORTHANC_ENABLE_SSL) +# error The macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if ORTHANC_ENABLE_SSL == 1 #include #endif #define ORTHANC_REALM "Orthanc Secure Area" -static const long LOCALHOST = (127ll << 24) + 1ll; - namespace Orthanc { @@ -279,7 +292,7 @@ { length = boost::lexical_cast(cs->second); } - catch (boost::bad_lexical_cast) + catch (boost::bad_lexical_cast&) { return PostDataStatus_NoLength; } @@ -351,7 +364,7 @@ { fileSize = boost::lexical_cast(fileSizeStr->second); } - catch (boost::bad_lexical_cast) + catch (boost::bad_lexical_cast&) { return PostDataStatus_Failure; } @@ -404,7 +417,7 @@ last = it; } } - catch (std::length_error) + catch (std::length_error&) { return PostDataStatus_Failure; } @@ -575,19 +588,30 @@ } - static void InternalCallback(struct mg_connection *connection, + static void InternalCallback(HttpOutput& output /* out */, + HttpMethod& method /* out */, + MongooseServer& server, + struct mg_connection *connection, const struct mg_request_info *request) { - MongooseServer* that = reinterpret_cast(request->user_data); - - MongooseOutputStream stream(connection); - HttpOutput output(stream, that->IsKeepAliveEnabled()); + bool localhost; +#if ORTHANC_ENABLE_MONGOOSE == 1 + static const long LOCALHOST = (127ll << 24) + 1ll; + localhost = (request->remote_ip == LOCALHOST); +#elif ORTHANC_ENABLE_CIVETWEB == 1 + // The "remote_ip" field of "struct mg_request_info" is tagged as + // deprecated in Civetweb, using "remote_addr" instead. + localhost = (std::string(request->remote_addr) == "127.0.0.1"); +#else +#error +#endif + // Check remote calls - if (!that->IsRemoteAccessAllowed() && - request->remote_ip != LOCALHOST) + if (!server.IsRemoteAccessAllowed() && + !localhost) { - output.SendUnauthorized(ORTHANC_REALM); + output.SendUnauthorized(server.GetRealm()); return; } @@ -597,11 +621,14 @@ for (int i = 0; i < request->num_headers; i++) { std::string name = request->http_headers[i].name; + std::string value = request->http_headers[i].value; + std::transform(name.begin(), name.end(), name.begin(), ::tolower); - headers.insert(std::make_pair(name, request->http_headers[i].value)); + headers.insert(std::make_pair(name, value)); + VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]"; } - if (that->IsHttpCompressionEnabled()) + if (server.IsHttpCompressionEnabled()) { ConfigureHttpCompression(output, headers); } @@ -616,7 +643,7 @@ // Compute the HTTP method, taking method faking into consideration - HttpMethod method = HttpMethod_Get; + method = HttpMethod_Get; if (!ExtractMethod(method, request, headers, argumentsGET)) { output.SendStatus(HttpStatus_400_BadRequest); @@ -625,13 +652,15 @@ // Authenticate this connection - if (that->IsAuthenticationEnabled() && !IsAccessGranted(*that, headers)) + if (server.IsAuthenticationEnabled() && + !IsAccessGranted(server, headers)) { - output.SendUnauthorized(ORTHANC_REALM); + output.SendUnauthorized(server.GetRealm()); return; } - + +#if ORTHANC_ENABLE_MONGOOSE == 1 // Apply the filter, if it is installed char remoteIp[24]; sprintf(remoteIp, "%d.%d.%d.%d", @@ -639,15 +668,22 @@ reinterpret_cast(&request->remote_ip) [2], reinterpret_cast(&request->remote_ip) [1], reinterpret_cast(&request->remote_ip) [0]); +#elif ORTHANC_ENABLE_CIVETWEB == 1 + const char* remoteIp = request->remote_addr; +#else +#error +#endif std::string username = GetAuthenticatedUsername(headers); - const IIncomingHttpRequestFilter *filter = that->GetIncomingHttpRequestFilter(); + IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); if (filter != NULL) { - if (!filter->IsAllowed(method, request->uri, remoteIp, username.c_str())) + if (!filter->IsAllowed(method, request->uri, remoteIp, + username.c_str(), headers, argumentsGET)) { - output.SendUnauthorized(ORTHANC_REALM); + //output.SendUnauthorized(server.GetRealm()); + output.SendStatus(HttpStatus_403_Forbidden); return; } } @@ -675,7 +711,7 @@ if (contentType.size() >= multipartLength && !memcmp(contentType.c_str(), multipart, multipartLength)) { - status = ParseMultipartPost(body, connection, headers, contentType, that->GetChunkStore()); + status = ParseMultipartPost(body, connection, headers, contentType, server.GetChunkStore()); } else { @@ -709,7 +745,7 @@ { Toolbox::SplitUriComponents(uri, request->uri); } - catch (OrthancException) + catch (OrthancException&) { output.SendStatus(HttpStatus_400_BadRequest); return; @@ -718,56 +754,111 @@ LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); + bool found = false; + if (server.HasHandler()) + { + found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), + method, uri, headers, argumentsGET, body.c_str(), body.size()); + } + + if (!found) + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + static void ProtectedCallback(struct mg_connection *connection, + const struct mg_request_info *request) + { try { - bool found = false; + void* that = NULL; + +#if ORTHANC_ENABLE_MONGOOSE == 1 + that = request->user_data; +#elif ORTHANC_ENABLE_CIVETWEB == 1 + // https://github.com/civetweb/civetweb/issues/409 + that = mg_get_user_data(mg_get_context(connection)); +#else +#error +#endif + + MongooseServer* server = reinterpret_cast(that); + + if (server == NULL) + { + MongooseOutputStream stream(connection); + HttpOutput output(stream, false /* assume no keep-alive */); + output.SendStatus(HttpStatus_500_InternalServerError); + return; + } + + MongooseOutputStream stream(connection); + HttpOutput output(stream, server->IsKeepAliveEnabled()); + HttpMethod method = HttpMethod_Get; try { - if (that->HasHandler()) + try + { + InternalCallback(output, method, *server, connection, request); + } + catch (OrthancException&) + { + throw; // Pass the exception to the main handler below + } + // Now convert native exceptions as OrthancException + catch (boost::bad_lexical_cast&) { - found = that->GetHandler().Handle(output, RequestOrigin_Http, remoteIp, username.c_str(), - method, uri, headers, argumentsGET, body.c_str(), body.size()); + LOG(ERROR) << "Syntax error in some user-supplied data"; + throw OrthancException(ErrorCode_BadParameterType); + } + catch (std::runtime_error&) + { + // Presumably an error while parsing the JSON body + throw OrthancException(ErrorCode_BadRequest); + } + catch (std::bad_alloc&) + { + LOG(ERROR) << "The server hosting Orthanc is running out of memory"; + throw OrthancException(ErrorCode_NotEnoughMemory); + } + catch (...) + { + LOG(ERROR) << "An unhandled exception was generated inside the HTTP server"; + throw OrthancException(ErrorCode_InternalError); } } - catch (boost::bad_lexical_cast&) - { - throw OrthancException(ErrorCode_BadParameterType); - } - catch (std::runtime_error&) + catch (OrthancException& e) { - // Presumably an error while parsing the JSON body - throw OrthancException(ErrorCode_BadRequest); - } + assert(server != NULL); - if (!found) - { - throw OrthancException(ErrorCode_UnknownResource); + // Using this candidate handler results in an exception + try + { + if (server->GetExceptionFormatter() == NULL) + { + LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); + output.SendStatus(e.GetHttpStatus()); + } + else + { + server->GetExceptionFormatter()->Format(output, e, method, request->uri); + } + } + catch (OrthancException&) + { + // An exception here reflects the fact that the status code + // was already set by the HTTP handler. + } } } - catch (OrthancException& e) + catch (...) { - // Using this candidate handler results in an exception - try - { - if (that->GetExceptionFormatter() == NULL) - { - LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); - output.SendStatus(e.GetHttpStatus()); - } - else - { - that->GetExceptionFormatter()->Format(output, e, method, request->uri); - } - } - catch (OrthancException&) - { - // An exception here reflects the fact that the status code - // was already set by the HTTP handler. - } - - return; + // We should never arrive at this point, where it is even impossible to send an answer + LOG(ERROR) << "Catastrophic error inside the HTTP server, giving up"; } } @@ -779,7 +870,7 @@ { if (event == MG_NEW_REQUEST) { - InternalCallback(connection, request); + ProtectedCallback(connection, request); // Mark as processed return (void*) ""; @@ -793,9 +884,9 @@ #elif MONGOOSE_USE_CALLBACKS == 1 static int Callback(struct mg_connection *connection) { - struct mg_request_info *request = mg_get_request_info(connection); + const struct mg_request_info *request = mg_get_request_info(connection); - InternalCallback(connection, request); + ProtectedCallback(connection, request); return 1; // Do not let Mongoose handle the request by itself } @@ -826,8 +917,9 @@ keepAlive_ = false; httpCompression_ = true; exceptionFormatter_ = NULL; + realm_ = ORTHANC_REALM; -#if ORTHANC_SSL_ENABLED == 1 +#if ORTHANC_ENABLE_SSL == 1 // Check for the Heartbleed exploit // https://en.wikipedia.org/wiki/OpenSSL#Heartbleed_bug if (OPENSSL_VERSION_NUMBER < 0x1000107fL /* openssl-1.0.1g */ && @@ -853,6 +945,14 @@ void MongooseServer::Start() { +#if ORTHANC_ENABLE_MONGOOSE == 1 + LOG(INFO) << "Starting embedded Web server using Mongoose"; +#elif ORTHANC_ENABLE_CIVETWEB == 1 + LOG(INFO) << "Starting embedded Web server using Civetweb"; +#else +#error +#endif + if (!IsRunning()) { std::string port = boost::lexical_cast(port_); @@ -870,6 +970,11 @@ // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ "enable_keep_alive", (keepAlive_ ? "yes" : "no"), +#if ORTHANC_ENABLE_CIVETWEB == 1 + // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no + "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"), +#endif + // Set the SSL certificate, if any. This must be the last option. ssl_ ? "ssl_certificate" : NULL, certificate_.c_str(), @@ -893,6 +998,13 @@ { throw OrthancException(ErrorCode_HttpPortInUse); } + + LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber() + << " (HTTPS encryption is " + << (IsSslEnabled() ? "enabled" : "disabled") + << ", remote access is " + << (IsRemoteAccessAllowed() ? "" : "not ") + << "allowed)"; } } @@ -928,7 +1040,7 @@ { Stop(); -#if ORTHANC_SSL_ENABLED == 0 +#if ORTHANC_ENABLE_SSL == 0 if (enabled) { throw OrthancException(ErrorCode_SslDisabled); @@ -947,7 +1059,7 @@ { Stop(); keepAlive_ = enabled; - LOG(WARNING) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled"); + LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled"); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/MongooseServer.h --- a/Core/HttpServer/MongooseServer.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/MongooseServer.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,7 +33,21 @@ #pragma once -#include "IHttpHandler.h" +#if !defined(ORTHANC_ENABLE_MONGOOSE) +# error Macro ORTHANC_ENABLE_MONGOOSE must be defined to include this file +#endif + +#if !defined(ORTHANC_ENABLE_CIVETWEB) +# error Macro ORTHANC_ENABLE_CIVETWEB must be defined to include this file +#endif + +#if (ORTHANC_ENABLE_MONGOOSE == 0 && \ + ORTHANC_ENABLE_CIVETWEB == 0) +# error Either ORTHANC_ENABLE_MONGOOSE or ORTHANC_ENABLE_CIVETWEB must be set to 1 +#endif + + +#include "IIncomingHttpRequestFilter.h" #include "../OrthancException.h" @@ -46,21 +61,7 @@ { class ChunkStore; - class IIncomingHttpRequestFilter - { - public: - virtual ~IIncomingHttpRequestFilter() - { - } - - virtual bool IsAllowed(HttpMethod method, - const char* uri, - const char* ip, - const char* username) const = 0; - }; - - - class IHttpExceptionFormatter + class IHttpExceptionFormatter : public boost::noncopyable { public: virtual ~IHttpExceptionFormatter() @@ -95,6 +96,7 @@ bool keepAlive_; bool httpCompression_; IHttpExceptionFormatter* exceptionFormatter_; + std::string realm_; bool IsRunning() const; @@ -161,7 +163,7 @@ void SetHttpCompressionEnabled(bool enabled); - const IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const + IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const { return filter_; } @@ -187,5 +189,15 @@ { return exceptionFormatter_; } + + const std::string& GetRealm() const + { + return realm_; + } + + void SetRealm(const std::string& realm) + { + realm_ = realm; + } }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/StringHttpOutput.cpp --- a/Core/HttpServer/StringHttpOutput.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/StringHttpOutput.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -39,9 +40,18 @@ { void StringHttpOutput::OnHttpStatusReceived(HttpStatus status) { - if (status != HttpStatus_200_Ok) + switch (status) { - throw OrthancException(ErrorCode_BadRequest); + case HttpStatus_200_Ok: + found_ = true; + break; + + case HttpStatus_404_NotFound: + found_ = false; + break; + + default: + throw OrthancException(ErrorCode_BadRequest); } } @@ -52,4 +62,16 @@ buffer_.AddChunk(reinterpret_cast(buffer), length); } } + + void StringHttpOutput::GetOutput(std::string& output) + { + if (found_) + { + buffer_.Flatten(output); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/HttpServer/StringHttpOutput.h --- a/Core/HttpServer/StringHttpOutput.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/HttpServer/StringHttpOutput.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -41,16 +42,18 @@ class StringHttpOutput : public IHttpOutputStream { private: + bool found_; ChunkedBuffer buffer_; public: + StringHttpOutput() : found_(false) + { + } + virtual void OnHttpStatusReceived(HttpStatus status); virtual void Send(bool isHeader, const void* buffer, size_t length); - void GetOutput(std::string& output) - { - buffer_.Flatten(output); - } + void GetOutput(std::string& output); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/ICommand.h --- a/Core/ICommand.h Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#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; - }; -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/IDynamicObject.h --- a/Core/IDynamicObject.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/IDynamicObject.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -49,4 +50,27 @@ { } }; + + /** + * This class is a simple implementation of a IDynamicObject that stores a single typed value + */ + template + class SingleValueObject : public Orthanc::IDynamicObject + { + private: + T value_; + public: + SingleValueObject(const T& value) : + value_(value) + { + } + virtual ~SingleValueObject() + { + } + + const T& GetValue() const + { + return value_; + } + }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/Font.cpp --- a/Core/Images/Font.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/Font.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -33,6 +34,14 @@ #include "../PrecompiledHeaders.h" #include "Font.h" +#if !defined(ORTHANC_ENABLE_LOCALE) +# error ORTHANC_ENABLE_LOCALE must be defined to use this file +#endif + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + #include "../Toolbox.h" #include "../OrthancException.h" @@ -132,12 +141,14 @@ } +#if ORTHANC_SANDBOXED == 0 void Font::LoadFromFile(const std::string& path) { std::string font; - Toolbox::ReadFile(font, path); + SystemToolbox::ReadFile(font, path); LoadFromMemory(font); } +#endif static unsigned int MyMin(unsigned int a, @@ -208,6 +219,7 @@ } case PixelFormat_RGBA32: + case PixelFormat_BGRA32: { assert(bpp == 4); @@ -245,14 +257,21 @@ { if (target.GetFormat() != PixelFormat_Grayscale8 && target.GetFormat() != PixelFormat_RGB24 && - target.GetFormat() != PixelFormat_RGBA32) + target.GetFormat() != PixelFormat_RGBA32 && + target.GetFormat() != PixelFormat_BGRA32) { throw OrthancException(ErrorCode_NotImplemented); } int a = x; +#if ORTHANC_ENABLE_LOCALE == 1 std::string s = Toolbox::ConvertFromUtf8(utf8, Encoding_Latin1); +#else + // If the locale support is disabled, simply drop non-ASCII + // characters from the source UTF-8 string + std::string s = Toolbox::ConvertToAscii(utf8); +#endif for (size_t i = 0; i < s.size(); i++) { @@ -294,7 +313,25 @@ uint8_t g, uint8_t b) const { - uint8_t color[4] = { r, g, b, 255 }; + uint8_t color[4]; + + switch (target.GetFormat()) + { + case PixelFormat_BGRA32: + color[0] = b; + color[1] = g; + color[2] = r; + color[3] = 255; + break; + + default: + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = 255; + break; + } + DrawInternal(target, utf8, x, y, color); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/Font.h --- a/Core/Images/Font.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/Font.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -83,7 +84,9 @@ void LoadFromMemory(const std::string& font); +#if ORTHANC_SANDBOXED == 0 void LoadFromFile(const std::string& path); +#endif const std::string& GetName() const { diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/FontRegistry.cpp --- a/Core/Images/FontRegistry.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/FontRegistry.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -56,20 +57,24 @@ } +#if ORTHANC_SANDBOXED == 0 void FontRegistry::AddFromFile(const std::string& path) { std::auto_ptr f(new Font); f->LoadFromFile(path); fonts_.push_back(f.release()); } +#endif +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 void FontRegistry::AddFromResource(EmbeddedResources::FileResourceId resource) { std::string content; EmbeddedResources::GetFileResource(content, resource); AddFromMemory(content); } +#endif const Font& FontRegistry::GetFont(size_t i) const diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/FontRegistry.h --- a/Core/Images/FontRegistry.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/FontRegistry.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -34,7 +35,13 @@ #include "Font.h" -#include // Autogenerated file +#if !defined(ORTHANC_HAS_EMBEDDED_RESOURCES) +# error Macro ORTHANC_HAS_EMBEDDED_RESOURCES must be defined +#endif + +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 +# include // Autogenerated file +#endif namespace Orthanc { @@ -50,9 +57,13 @@ void AddFromMemory(const std::string& font); +#if ORTHANC_SANDBOXED == 0 void AddFromFile(const std::string& path); +#endif +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 void AddFromResource(EmbeddedResources::FileResourceId resource); +#endif size_t GetSize() const { diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/IImageWriter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/IImageWriter.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,55 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/IImageWriter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/IImageWriter.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,86 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ImageAccessor.h" + +#include + +#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 + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/Image.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/Image.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,61 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "Image.h" + +#include "ImageProcessing.h" + +#include + +namespace Orthanc +{ + Image::Image(PixelFormat format, + unsigned int width, + unsigned int height, + bool forceMinimalPitch) : + image_(format, width, height, forceMinimalPitch) + { + ImageAccessor accessor; + image_.GetWriteableAccessor(accessor); + + AssignWritable(format, width, height, accessor.GetPitch(), accessor.GetBuffer()); + } + + + Image* Image::Clone(const ImageAccessor& source) + { + std::auto_ptr target(new Image(source.GetFormat(), source.GetWidth(), source.GetHeight(), false)); + ImageProcessing::Copy(*target, source); + return target.release(); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/Image.h --- a/Core/Images/Image.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/Image.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -45,11 +46,9 @@ public: Image(PixelFormat format, unsigned int width, - unsigned int height) : - image_(format, width, height) - { - ImageAccessor accessor = image_.GetAccessor(); - AssignWritable(format, width, height, accessor.GetPitch(), accessor.GetBuffer()); - } + unsigned int height, + bool forceMinimalPitch); + + static Image* Clone(const ImageAccessor& source); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/ImageAccessor.cpp --- a/Core/Images/ImageAccessor.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/ImageAccessor.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -65,7 +66,7 @@ for (unsigned int x = 0; x < source.GetWidth(); x++, p++) { - s += boost::lexical_cast(static_cast(*p)) + " "; + s += boost::lexical_cast(static_cast(*p)) + " "; } target.AddChunk(s); @@ -121,7 +122,7 @@ { if (buffer_ != NULL) { - return reinterpret_cast(buffer_) + y * pitch_; + return buffer_ + y * pitch_; } else { @@ -143,7 +144,7 @@ if (buffer_ != NULL) { - return reinterpret_cast(buffer_) + y * pitch_; + return buffer_ + y * pitch_; } else { @@ -174,9 +175,12 @@ width_ = width; height_ = height; pitch_ = pitch; - buffer_ = const_cast(buffer); + buffer_ = reinterpret_cast(const_cast(buffer)); - assert(GetBytesPerPixel() * width_ <= pitch_); + if (GetBytesPerPixel() * width_ > pitch_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } } @@ -191,9 +195,25 @@ width_ = width; height_ = height; pitch_ = pitch; - buffer_ = buffer; + buffer_ = reinterpret_cast(buffer); + + if (GetBytesPerPixel() * width_ > pitch_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + - assert(GetBytesPerPixel() * width_ <= pitch_); + void ImageAccessor::GetWriteableAccessor(ImageAccessor& target) const + { + if (readOnly_) + { + throw OrthancException(ErrorCode_ReadOnly); + } + else + { + target.AssignWritable(format_, width_, height_, pitch_, buffer_); + } } @@ -211,10 +231,22 @@ ToMatlabStringInternal(buffer, *this); break; + case PixelFormat_Grayscale32: + ToMatlabStringInternal(buffer, *this); + break; + + case PixelFormat_Grayscale64: + ToMatlabStringInternal(buffer, *this); + break; + case PixelFormat_SignedGrayscale16: ToMatlabStringInternal(buffer, *this); break; + case PixelFormat_Float32: + ToMatlabStringInternal(buffer, *this); + break; + case PixelFormat_RGB24: RGB24ToMatlabString(buffer, *this); break; @@ -226,4 +258,58 @@ buffer.Flatten(target); } + + + void ImageAccessor::GetRegion(ImageAccessor& accessor, + unsigned int x, + unsigned int y, + unsigned int width, + unsigned int height) const + { + if (x + width > width_ || + y + height > height_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (width == 0 || + height == 0) + { + accessor.AssignWritable(format_, 0, 0, 0, NULL); + } + else + { + uint8_t* p = (buffer_ + + y * pitch_ + + x * GetBytesPerPixel()); + + if (readOnly_) + { + accessor.AssignReadOnly(format_, width, height, pitch_, p); + } + else + { + accessor.AssignWritable(format_, width, height, pitch_, p); + } + } + } + + + void ImageAccessor::SetFormat(PixelFormat format) + { + if (readOnly_) + { +#if ORTHANC_ENABLE_LOGGING == 1 + LOG(ERROR) << "Trying to modify the format of a read-only image"; +#endif + throw OrthancException(ErrorCode_ReadOnly); + } + + if (::Orthanc::GetBytesPerPixel(format) != ::Orthanc::GetBytesPerPixel(format_)) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + format_ = format; + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/ImageAccessor.h --- a/Core/Images/ImageAccessor.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/ImageAccessor.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -35,18 +36,40 @@ #include "../Enumerations.h" #include +#include +#include namespace Orthanc { - class ImageAccessor + class ImageAccessor : public boost::noncopyable { private: + template + friend struct ImageTraits; + bool readOnly_; PixelFormat format_; unsigned int width_; unsigned int height_; unsigned int pitch_; - void *buffer_; + uint8_t *buffer_; + + template + const T& GetPixelUnchecked(unsigned int x, + unsigned int y) const + { + const uint8_t* row = reinterpret_cast(buffer_) + y * pitch_; + return reinterpret_cast(row) [x]; + } + + + template + T& GetPixelUnchecked(unsigned int x, + unsigned int y) + { + uint8_t* row = reinterpret_cast(buffer_) + y * pitch_; + return reinterpret_cast(row) [x]; + } public: ImageAccessor() @@ -112,12 +135,27 @@ unsigned int pitch, const void *buffer); + void GetReadOnlyAccessor(ImageAccessor& target) const + { + target.AssignReadOnly(format_, width_, height_, pitch_, buffer_); + } + void AssignWritable(PixelFormat format, unsigned int width, unsigned int height, unsigned int pitch, void *buffer); + void GetWriteableAccessor(ImageAccessor& target) const; + void ToMatlabString(std::string& target) const; + + void GetRegion(ImageAccessor& accessor, + unsigned int x, + unsigned int y, + unsigned int width, + unsigned int height) const; + + void SetFormat(PixelFormat format); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/ImageBuffer.cpp --- a/Core/Images/ImageBuffer.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/ImageBuffer.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -87,7 +88,9 @@ ImageBuffer::ImageBuffer(PixelFormat format, unsigned int width, - unsigned int height) + unsigned int height, + bool forceMinimalPitch) : + forceMinimalPitch_(forceMinimalPitch) { Initialize(); SetWidth(width); @@ -137,34 +140,18 @@ } } - - ImageAccessor ImageBuffer::GetAccessor() - { - Allocate(); - - ImageAccessor accessor; - accessor.AssignWritable(format_, width_, height_, pitch_, buffer_); - return accessor; - } - - - ImageAccessor ImageBuffer::GetConstAccessor() + + void ImageBuffer::GetReadOnlyAccessor(ImageAccessor& accessor) { Allocate(); - - ImageAccessor accessor; accessor.AssignReadOnly(format_, width_, height_, pitch_, buffer_); - return accessor; } - + - void ImageBuffer::SetMinimalPitchForced(bool force) + void ImageBuffer::GetWriteableAccessor(ImageAccessor& accessor) { - if (force != forceMinimalPitch_) - { - changed_ = true; - forceMinimalPitch_ = force; - } + Allocate(); + accessor.AssignWritable(format_, width_, height_, pitch_, buffer_); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/ImageBuffer.h --- a/Core/Images/ImageBuffer.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/ImageBuffer.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -61,7 +62,8 @@ public: ImageBuffer(PixelFormat format, unsigned int width, - unsigned int height); + unsigned int height, + bool forceMinimalPitch); ImageBuffer() { @@ -99,17 +101,15 @@ return ::Orthanc::GetBytesPerPixel(format_); } - ImageAccessor GetAccessor(); + void GetReadOnlyAccessor(ImageAccessor& accessor); - ImageAccessor GetConstAccessor(); + void GetWriteableAccessor(ImageAccessor& accessor); bool IsMinimalPitchForced() const { return forceMinimalPitch_; } - void SetMinimalPitchForced(bool force); - void AcquireOwnership(ImageBuffer& other); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/ImageProcessing.cpp --- a/Core/Images/ImageProcessing.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/ImageProcessing.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -33,7 +34,7 @@ #include "../PrecompiledHeaders.h" #include "ImageProcessing.h" -#include "../OrthancException.h" +#include "PixelTraits.h" #include @@ -75,9 +76,28 @@ } + template + static void ConvertGrayscaleToFloat(ImageAccessor& target, + const ImageAccessor& source) + { + assert(sizeof(float) == 4); + + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + float* t = reinterpret_cast(target.GetRow(y)); + const SourceType* s = reinterpret_cast(source.GetConstRow(y)); + + for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s++) + { + *t = static_cast(*s); + } + } + } + + template static void ConvertColorToGrayscale(ImageAccessor& target, - const ImageAccessor& source) + const ImageAccessor& source) { assert(source.GetFormat() == PixelFormat_RGB24); @@ -94,7 +114,7 @@ // Y = 0.2126 R + 0.7152 G + 0.0722 B int32_t v = (2126 * static_cast(s[0]) + 7152 * static_cast(s[1]) + - 0722 * static_cast(s[2])) / 1000; + 0722 * static_cast(s[2])) / 10000; if (static_cast(v) < static_cast(minValue)) { @@ -146,11 +166,13 @@ minValue = std::numeric_limits::max(); maxValue = std::numeric_limits::min(); + const unsigned int width = source.GetWidth(); + for (unsigned int y = 0; y < source.GetHeight(); y++) { const PixelType* p = reinterpret_cast(source.GetConstRow(y)); - for (unsigned int x = 0; x < source.GetWidth(); x++, p++) + for (unsigned int x = 0; x < width; x++, p++) { if (*p < minValue) { @@ -205,9 +227,10 @@ - template - void MultiplyConstantInternal(ImageAccessor& image, - float factor) + template + static void MultiplyConstantInternal(ImageAccessor& image, + float factor) { if (std::abs(factor - 1.0f) <= std::numeric_limits::epsilon()) { @@ -216,14 +239,24 @@ const int64_t minValue = std::numeric_limits::min(); const int64_t maxValue = std::numeric_limits::max(); + const unsigned int width = image.GetWidth(); for (unsigned int y = 0; y < image.GetHeight(); y++) { PixelType* p = reinterpret_cast(image.GetRow(y)); - for (unsigned int x = 0; x < image.GetWidth(); x++, p++) + for (unsigned int x = 0; x < width; x++, p++) { - int64_t v = boost::math::llround(static_cast(*p) * factor); + int64_t v; + if (UseRound) + { + // The "round" operation is very costly + v = boost::math::llround(static_cast(*p) * factor); + } + else + { + v = static_cast(static_cast(*p) * factor); + } if (v > maxValue) { @@ -242,33 +275,44 @@ } - template - void ShiftScaleInternal(ImageAccessor& image, - float offset, - float scaling) + template + static void ShiftScaleInternal(ImageAccessor& image, + float offset, + float scaling) { - const float minValue = static_cast(std::numeric_limits::min()); - const float maxValue = static_cast(std::numeric_limits::max()); + const float minFloatValue = static_cast(std::numeric_limits::min()); + const float maxFloatValue = static_cast(std::numeric_limits::max()); + const PixelType minPixelValue = std::numeric_limits::min(); + const PixelType maxPixelValue = std::numeric_limits::max(); - for (unsigned int y = 0; y < image.GetHeight(); y++) + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + + for (unsigned int y = 0; y < height; y++) { PixelType* p = reinterpret_cast(image.GetRow(y)); - for (unsigned int x = 0; x < image.GetWidth(); x++, p++) + for (unsigned int x = 0; x < width; x++, p++) { float v = (static_cast(*p) + offset) * scaling; - if (v > maxValue) + if (v > maxFloatValue) { - *p = std::numeric_limits::max(); + *p = maxPixelValue; } - else if (v < minValue) + else if (v < minFloatValue) { - *p = std::numeric_limits::min(); + *p = minPixelValue; + } + else if (UseRound) + { + // The "round" operation is very costly + *p = static_cast(boost::math::iround(v)); } else { - *p = static_cast(boost::math::iround(v)); + *p = static_cast(v); } } } @@ -378,6 +422,34 @@ return; } + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_Grayscale8) + { + ConvertGrayscaleToFloat(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_Grayscale16) + { + ConvertGrayscaleToFloat(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_Grayscale32) + { + ConvertGrayscaleToFloat(target, source); + return; + } + + if (target.GetFormat() == PixelFormat_Float32 && + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + ConvertGrayscaleToFloat(target, source); + return; + } + if (target.GetFormat() == PixelFormat_Grayscale8 && source.GetFormat() == PixelFormat_RGBA32) { @@ -397,6 +469,25 @@ return; } + if (target.GetFormat() == PixelFormat_Grayscale8 && + source.GetFormat() == PixelFormat_BGRA32) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++, q++) + { + *q = static_cast((2126 * static_cast(p[2]) + + 7152 * static_cast(p[1]) + + 0722 * static_cast(p[0])) / 10000); + p += 4; + } + } + + return; + } + if (target.GetFormat() == PixelFormat_RGB24 && source.GetFormat() == PixelFormat_RGBA32) { @@ -417,6 +508,26 @@ return; } + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_BGRA32) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++) + { + q[0] = p[2]; + q[1] = p[1]; + q[2] = p[0]; + p += 4; + q += 3; + } + } + + return; + } + if (target.GetFormat() == PixelFormat_RGBA32 && source.GetFormat() == PixelFormat_RGB24) { @@ -438,6 +549,146 @@ return; } + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_Grayscale8) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++) + { + q[0] = *p; + q[1] = *p; + q[2] = *p; + p += 1; + q += 3; + } + } + + return; + } + + if ((target.GetFormat() == PixelFormat_RGBA32 || + target.GetFormat() == PixelFormat_BGRA32) && + source.GetFormat() == PixelFormat_Grayscale8) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++) + { + q[0] = *p; + q[1] = *p; + q[2] = *p; + q[3] = 255; + p += 1; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_BGRA32 && + source.GetFormat() == PixelFormat_Grayscale16) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const uint16_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++) + { + uint8_t value = (*p < 256 ? *p : 255); + q[0] = value; + q[1] = value; + q[2] = value; + q[3] = 255; + p += 1; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_BGRA32 && + source.GetFormat() == PixelFormat_SignedGrayscale16) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const int16_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++) + { + uint8_t value; + if (*p < 0) + { + value = 0; + } + else if (*p > 255) + { + value = 255; + } + else + { + value = static_cast(*p); + } + + q[0] = value; + q[1] = value; + q[2] = value; + q[3] = 255; + p += 1; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_BGRA32 && + source.GetFormat() == PixelFormat_RGB24) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const uint8_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++) + { + q[0] = p[2]; + q[1] = p[1]; + q[2] = p[0]; + q[3] = 255; + p += 3; + q += 4; + } + } + + return; + } + + if (target.GetFormat() == PixelFormat_RGB24 && + source.GetFormat() == PixelFormat_RGB48) + { + for (unsigned int y = 0; y < source.GetHeight(); y++) + { + const uint16_t* p = reinterpret_cast(source.GetConstRow(y)); + uint8_t* q = reinterpret_cast(target.GetRow(y)); + for (unsigned int x = 0; x < source.GetWidth(); x++) + { + q[0] = p[0] >> 8; + q[1] = p[1] >> 8; + q[2] = p[2] >> 8; + p += 3; + q += 3; + } + } + + return; + } + throw OrthancException(ErrorCode_NotImplemented); } @@ -449,15 +700,56 @@ switch (image.GetFormat()) { case PixelFormat_Grayscale8: - SetInternal(image, value); + memset(image.GetBuffer(), static_cast(value), image.GetPitch() * image.GetHeight()); return; case PixelFormat_Grayscale16: - SetInternal(image, value); + if (value == 0) + { + memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); + } + else + { + SetInternal(image, value); + } + return; + + case PixelFormat_Grayscale32: + if (value == 0) + { + memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); + } + else + { + SetInternal(image, value); + } + return; + + case PixelFormat_Grayscale64: + if (value == 0) + { + memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); + } + else + { + SetInternal(image, value); + } return; case PixelFormat_SignedGrayscale16: - SetInternal(image, value); + if (value == 0) + { + memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight()); + } + else + { + SetInternal(image, value); + } + return; + + case PixelFormat_Float32: + assert(sizeof(float) == 4); + SetInternal(image, value); return; default: @@ -466,6 +758,61 @@ } + void ImageProcessing::Set(ImageAccessor& image, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t alpha) + { + uint8_t p[4]; + unsigned int size; + + switch (image.GetFormat()) + { + case PixelFormat_RGBA32: + p[0] = red; + p[1] = green; + p[2] = blue; + p[3] = alpha; + size = 4; + break; + + case PixelFormat_BGRA32: + p[0] = blue; + p[1] = green; + p[2] = red; + p[3] = alpha; + size = 4; + break; + + case PixelFormat_RGB24: + p[0] = red; + p[1] = green; + p[2] = blue; + size = 3; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + uint8_t* q = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < image.GetWidth(); x++) + { + for (unsigned int i = 0; i < size; i++) + { + q[i] = p[i]; + } + + q += size; + } + } + } + + void ImageProcessing::ShiftRight(ImageAccessor& image, unsigned int shift) { @@ -481,9 +828,9 @@ } - void ImageProcessing::GetMinMaxValue(int64_t& minValue, - int64_t& maxValue, - const ImageAccessor& image) + void ImageProcessing::GetMinMaxIntegerValue(int64_t& minValue, + int64_t& maxValue, + const ImageAccessor& image) { switch (image.GetFormat()) { @@ -505,6 +852,15 @@ break; } + case PixelFormat_Grayscale32: + { + uint32_t a, b; + GetMinMaxValueInternal(a, b, image); + minValue = a; + maxValue = b; + break; + } + case PixelFormat_SignedGrayscale16: { int16_t a, b; @@ -520,6 +876,28 @@ } + void ImageProcessing::GetMinMaxFloatValue(float& minValue, + float& maxValue, + const ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Float32: + { + assert(sizeof(float) == 32); + float a, b; + GetMinMaxValueInternal(a, b, image); + minValue = a; + maxValue = b; + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + void ImageProcessing::AddConstant(ImageAccessor& image, int64_t value) @@ -545,20 +923,42 @@ void ImageProcessing::MultiplyConstant(ImageAccessor& image, - float factor) + float factor, + bool useRound) { switch (image.GetFormat()) { case PixelFormat_Grayscale8: - MultiplyConstantInternal(image, factor); + if (useRound) + { + MultiplyConstantInternal(image, factor); + } + else + { + MultiplyConstantInternal(image, factor); + } return; case PixelFormat_Grayscale16: - MultiplyConstantInternal(image, factor); + if (useRound) + { + MultiplyConstantInternal(image, factor); + } + else + { + MultiplyConstantInternal(image, factor); + } return; case PixelFormat_SignedGrayscale16: - MultiplyConstantInternal(image, factor); + if (useRound) + { + MultiplyConstantInternal(image, factor); + } + else + { + MultiplyConstantInternal(image, factor); + } return; default: @@ -569,24 +969,302 @@ void ImageProcessing::ShiftScale(ImageAccessor& image, float offset, - float scaling) + float scaling, + bool useRound) { switch (image.GetFormat()) { case PixelFormat_Grayscale8: - ShiftScaleInternal(image, offset, scaling); + if (useRound) + { + ShiftScaleInternal(image, offset, scaling); + } + else + { + ShiftScaleInternal(image, offset, scaling); + } return; case PixelFormat_Grayscale16: - ShiftScaleInternal(image, offset, scaling); + if (useRound) + { + ShiftScaleInternal(image, offset, scaling); + } + else + { + ShiftScaleInternal(image, offset, scaling); + } return; case PixelFormat_SignedGrayscale16: - ShiftScaleInternal(image, offset, scaling); + if (useRound) + { + ShiftScaleInternal(image, offset, scaling); + } + else + { + ShiftScaleInternal(image, offset, scaling); + } return; default: throw OrthancException(ErrorCode_NotImplemented); } } + + + void ImageProcessing::Invert(ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + { + for (unsigned int y = 0; y < image.GetHeight(); y++) + { + uint8_t* p = reinterpret_cast(image.GetRow(y)); + + for (unsigned int x = 0; x < image.GetWidth(); x++, p++) + { + *p = 255 - (*p); + } + } + + return; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + + namespace + { + template + class BresenhamPixelWriter + { + private: + typedef typename PixelTraits::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::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(x) < image_.GetWidth() && + static_cast(y) < image_.GetHeight()) + { + PixelType* p = reinterpret_cast(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 writer(image, value); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case Orthanc::PixelFormat_Grayscale16: + { + BresenhamPixelWriter writer(image, value); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case Orthanc::PixelFormat_SignedGrayscale16: + { + BresenhamPixelWriter 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::PixelType pixel; + pixel.red_ = red; + pixel.green_ = green; + pixel.blue_ = blue; + pixel.alpha_ = alpha; + + BresenhamPixelWriter writer(image, pixel); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case Orthanc::PixelFormat_RGBA32: + { + PixelTraits::PixelType pixel; + pixel.red_ = red; + pixel.green_ = green; + pixel.blue_ = blue; + pixel.alpha_ = alpha; + + BresenhamPixelWriter writer(image, pixel); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + case Orthanc::PixelFormat_RGB24: + { + PixelTraits::PixelType pixel; + pixel.red_ = red; + pixel.green_ = green; + pixel.blue_ = blue; + + BresenhamPixelWriter writer(image, pixel); + writer.DrawSegment(x0, y0, x1, y1); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/ImageProcessing.h --- a/Core/Images/ImageProcessing.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/ImageProcessing.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -38,33 +39,65 @@ namespace Orthanc { - class ImageProcessing + namespace ImageProcessing { - public: - static void Copy(ImageAccessor& target, - const ImageAccessor& source); + void Copy(ImageAccessor& target, + const ImageAccessor& source); + + void Convert(ImageAccessor& target, + const ImageAccessor& source); + + void Set(ImageAccessor& image, + int64_t value); - static void Convert(ImageAccessor& target, - const ImageAccessor& source); + void Set(ImageAccessor& image, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t alpha); - static void Set(ImageAccessor& image, - int64_t value); + void ShiftRight(ImageAccessor& target, + unsigned int shift); - static void ShiftRight(ImageAccessor& target, - unsigned int shift); - - static void GetMinMaxValue(int64_t& minValue, + void GetMinMaxIntegerValue(int64_t& minValue, int64_t& maxValue, const ImageAccessor& image); - static void AddConstant(ImageAccessor& image, - int64_t value); + void GetMinMaxFloatValue(float& minValue, + float& maxValue, + const ImageAccessor& image); + + void AddConstant(ImageAccessor& image, + int64_t value); - static void MultiplyConstant(ImageAccessor& image, - float factor); + // "useRound" is expensive + void MultiplyConstant(ImageAccessor& image, + float factor, + bool useRound); + + // "useRound" is expensive + void ShiftScale(ImageAccessor& image, + float offset, + float scaling, + bool useRound); - static void ShiftScale(ImageAccessor& image, - float offset, - float scaling); - }; + void Invert(ImageAccessor& image); + + void DrawLineSegment(ImageAccessor& image, + int x0, + int y0, + int x1, + int y1, + int64_t value); + + void DrawLineSegment(ImageAccessor& image, + int x0, + int y0, + int x1, + int y1, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t alpha); + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/ImageTraits.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/ImageTraits.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,89 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ImageAccessor.h" +#include "PixelTraits.h" + +#include + +namespace Orthanc +{ + template + struct ImageTraits + { + typedef ::Orthanc::PixelTraits 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(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(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(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(x, y), value); + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/JpegErrorManager.cpp --- a/Core/Images/JpegErrorManager.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/JpegErrorManager.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/JpegErrorManager.h --- a/Core/Images/JpegErrorManager.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/JpegErrorManager.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -31,6 +32,14 @@ #pragma once +#if !defined(ORTHANC_ENABLE_JPEG) +# error The macro ORTHANC_ENABLE_JPEG must be defined +#endif + +#if ORTHANC_ENABLE_JPEG != 1 +# error JPEG support must be enabled to include this file +#endif + #include #include #include diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/JpegReader.cpp --- a/Core/Images/JpegReader.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/JpegReader.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -37,13 +38,21 @@ #include "../OrthancException.h" #include "../Logging.h" +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + + namespace Orthanc { static void Uncompress(struct jpeg_decompress_struct& cinfo, std::string& content, ImageAccessor& accessor) { - jpeg_read_header(&cinfo, TRUE); + // The "static_cast" is necessary on OS X: + // https://github.com/simonfuhrmann/mve/issues/371 + jpeg_read_header(&cinfo, static_cast(true)); + jpeg_start_decompress(&cinfo); PixelFormat format; @@ -93,9 +102,10 @@ } - void JpegReader::ReadFromFile(const char* filename) +#if ORTHANC_SANDBOXED == 0 + void JpegReader::ReadFromFile(const std::string& filename) { - FILE* fp = fopen(filename, "rb"); + FILE* fp = SystemToolbox::OpenFile(filename, FileMode_ReadBinary); if (!fp) { throw OrthancException(ErrorCode_InexistentFile); @@ -111,7 +121,7 @@ { jpeg_destroy_decompress(&cinfo); fclose(fp); - LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage(); + LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage(); throw OrthancException(ErrorCode_InternalError); } @@ -134,6 +144,7 @@ jpeg_destroy_decompress(&cinfo); fclose(fp); } +#endif void JpegReader::ReadFromMemory(const void* buffer, @@ -148,7 +159,7 @@ if (setjmp(jerr.GetJumpBuffer())) { jpeg_destroy_decompress(&cinfo); - LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage(); + LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage(); throw OrthancException(ErrorCode_InternalError); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/JpegReader.h --- a/Core/Images/JpegReader.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/JpegReader.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,6 +33,18 @@ #pragma once +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if !defined(ORTHANC_ENABLE_JPEG) +# error The macro ORTHANC_ENABLE_JPEG must be defined +#endif + +#if ORTHANC_ENABLE_JPEG != 1 +# error JPEG support must be enabled to include this file +#endif + #include "ImageAccessor.h" #include @@ -44,12 +57,9 @@ std::string content_; public: - void ReadFromFile(const char* filename); - - void ReadFromFile(const std::string& filename) - { - ReadFromFile(filename.c_str()); - } +#if ORTHANC_SANDBOXED == 0 + void ReadFromFile(const std::string& filename); +#endif void ReadFromMemory(const void* buffer, size_t size); diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/JpegWriter.cpp --- a/Core/Images/JpegWriter.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/JpegWriter.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -35,9 +36,13 @@ #include "../OrthancException.h" #include "../Logging.h" - #include "JpegErrorManager.h" +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include #include namespace Orthanc @@ -91,8 +96,12 @@ } jpeg_set_defaults(&cinfo); - jpeg_set_quality(&cinfo, quality, TRUE); - jpeg_start_compress(&cinfo, TRUE); + + // The "static_cast" is necessary on OS X: + // https://github.com/simonfuhrmann/mve/issues/371 + jpeg_set_quality(&cinfo, quality, static_cast(true)); + jpeg_start_compress(&cinfo, static_cast(true)); + jpeg_write_scanlines(&cinfo, &lines[0], height); jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); @@ -101,7 +110,7 @@ void JpegWriter::SetQuality(uint8_t quality) { - if (quality <= 0 || quality > 100) + if (quality == 0 || quality > 100) { throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -110,17 +119,18 @@ } - void JpegWriter::WriteToFile(const char* filename, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer) +#if ORTHANC_SANDBOXED == 0 + void JpegWriter::WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) { - FILE* fp = fopen(filename, "wb"); + FILE* fp = SystemToolbox::OpenFile(filename, FileMode_WriteBinary); if (fp == NULL) { - throw OrthancException(ErrorCode_FullStorage); + throw OrthancException(ErrorCode_CannotWriteFile); } std::vector lines; @@ -153,14 +163,15 @@ fclose(fp); } +#endif - void JpegWriter::WriteToMemory(std::string& jpeg, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer) + void JpegWriter::WriteToMemoryInternal(std::string& jpeg, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) { std::vector lines; GetLines(lines, height, pitch, format, buffer); diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/JpegWriter.h --- a/Core/Images/JpegWriter.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/JpegWriter.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,15 +33,37 @@ #pragma once -#include "ImageAccessor.h" +#if !defined(ORTHANC_ENABLE_JPEG) +# error The macro ORTHANC_ENABLE_JPEG must be defined +#endif -#include -#include +#if ORTHANC_ENABLE_JPEG != 1 +# error JPEG support must be enabled to include this file +#endif + +#include "IImageWriter.h" namespace Orthanc { - class JpegWriter + class JpegWriter : public IImageWriter { + protected: +#if ORTHANC_SANDBOXED == 0 + virtual void WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer); +#endif + + virtual void WriteToMemoryInternal(std::string& jpeg, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer); + private: uint8_t quality_; @@ -55,33 +78,5 @@ { return quality_; } - - void WriteToFile(const char* filename, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer); - - void WriteToMemory(std::string& jpeg, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer); - - void WriteToFile(const char* filename, - const ImageAccessor& accessor) - { - WriteToFile(filename, accessor.GetWidth(), accessor.GetHeight(), - accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer()); - } - - void WriteToMemory(std::string& jpeg, - const ImageAccessor& accessor) - { - WriteToMemory(jpeg, accessor.GetWidth(), accessor.GetHeight(), - accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer()); - } }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PamReader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/PamReader.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,247 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "PamReader.h" + +#include "../Endianness.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + +#include +#include + + +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 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(LookupStringParameter(parameters, key)); + + if (value < 0) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return static_cast(value); + } + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void PamReader::ParseContent() + { + static const std::string headerDelimiter = "ENDHDR\n"; + + boost::iterator_range headerRange = + boost::algorithm::find_first(content_, headerDelimiter); + + if (!headerRange) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + std::string header(static_cast(content_).begin(), headerRange.begin()); + + std::vector 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 tokens; + Toolbox::TokenizeString(tokens, lines[i], ' '); + + if (tokens.size() != 2) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + parameters[tokens[0]] = tokens[1]; + } + } + + const unsigned int width = LookupIntegerParameter(parameters, "WIDTH"); + const unsigned int height = LookupIntegerParameter(parameters, "HEIGHT"); + const unsigned int channelCount = LookupIntegerParameter(parameters, "DEPTH"); + const unsigned int maxValue = LookupIntegerParameter(parameters, "MAXVAL"); + const std::string tupleType = LookupStringParameter(parameters, "TUPLTYPE"); + + unsigned int bytesPerChannel; + PixelFormat format; + GetPixelFormat(format, bytesPerChannel, maxValue, channelCount, tupleType.c_str()); + + unsigned int pitch = width * channelCount * bytesPerChannel; + + if (content_.size() != header.size() + headerDelimiter.size() + pitch * height) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + size_t offset = content_.size() - pitch * height; + AssignWritable(format, width, height, pitch, &content_[offset]); + + // Byte swapping if needed + if (bytesPerChannel != 1 && + bytesPerChannel != 2) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + if (Toolbox::DetectEndianness() == Endianness_Little && + bytesPerChannel == 2) + { + for (unsigned int h = 0; h < height; ++h) + { + uint16_t* pixel = reinterpret_cast(GetRow(h)); + + for (unsigned int w = 0; w < GetWidth(); ++w, ++pixel) + { + // memcpy() is necessary to avoid segmentation fault if the + // "pixel" pointer is not 16-bit aligned (which is the case + // if "offset" is an odd number). Check out issue #99: + // https://bitbucket.org/sjodogne/orthanc/issues/99 + uint16_t v = htobe16(*pixel); + memcpy(pixel, &v, sizeof(v)); + } + } + } + } + + +#if ORTHANC_SANDBOXED == 0 + void PamReader::ReadFromFile(const std::string& filename) + { + SystemToolbox::ReadFile(content_, filename); + ParseContent(); + } +#endif + + + void PamReader::ReadFromMemory(const std::string& buffer) + { + content_ = buffer; + ParseContent(); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PamReader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/PamReader.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,58 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ImageAccessor.h" + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +namespace Orthanc +{ + class PamReader : public ImageAccessor + { + private: + void ParseContent(); + + std::string content_; + + public: +#if ORTHANC_SANDBOXED == 0 + void ReadFromFile(const std::string& filename); +#endif + + void ReadFromMemory(const std::string& buffer); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PamWriter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/PamWriter.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,159 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "PamWriter.h" + +#include "../Endianness.h" +#include "../OrthancException.h" +#include "../Toolbox.h" + +#include + + +namespace Orthanc +{ + static void GetPixelFormatInfo(const PixelFormat& format, + unsigned int& maxValue, + unsigned int& channelCount, + unsigned int& bytesPerChannel, + std::string& tupleType) + { + switch (format) + { + case PixelFormat_Grayscale8: + maxValue = 255; + channelCount = 1; + bytesPerChannel = 1; + tupleType = "GRAYSCALE"; + break; + + case PixelFormat_Grayscale16: + maxValue = 65535; + channelCount = 1; + bytesPerChannel = 2; + tupleType = "GRAYSCALE"; + break; + + case PixelFormat_RGB24: + maxValue = 255; + channelCount = 3; + bytesPerChannel = 1; + tupleType = "RGB"; + break; + + case PixelFormat_RGB48: + maxValue = 255; + channelCount = 3; + bytesPerChannel = 2; + tupleType = "RGB"; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void PamWriter::WriteToMemoryInternal(std::string& target, + unsigned int width, + unsigned int height, + unsigned int sourcePitch, + PixelFormat format, + const void* buffer) + { + unsigned int maxValue, channelCount, bytesPerChannel; + std::string tupleType; + GetPixelFormatInfo(format, maxValue, channelCount, bytesPerChannel, tupleType); + + target = (std::string("P7") + + std::string("\nWIDTH ") + boost::lexical_cast(width) + + std::string("\nHEIGHT ") + boost::lexical_cast(height) + + std::string("\nDEPTH ") + boost::lexical_cast(channelCount) + + std::string("\nMAXVAL ") + boost::lexical_cast(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 + (reinterpret_cast(buffer) + h * sourcePitch); + uint16_t* q = reinterpret_cast + (reinterpret_cast(&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(buffer) + h * sourcePitch; + void* q = reinterpret_cast(&target[offset]) + h * targetPitch; + memcpy(q, p, targetPitch); + } + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PamWriter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/PamWriter.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,51 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PixelTraits.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Images/PixelTraits.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,395 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Enumerations.h" +#include "../OrthancException.h" + +#include + +namespace Orthanc +{ + template + 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(std::numeric_limits::min()) || + value > static_cast(std::numeric_limits::max())) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return static_cast(value); + } + } + + ORTHANC_FORCE_INLINE + static void SetZero(PixelType& target) + { + target = 0; + } + + ORTHANC_FORCE_INLINE + static void SetMinValue(PixelType& target) + { + target = std::numeric_limits::min(); + } + + ORTHANC_FORCE_INLINE + static void SetMaxValue(PixelType& target) + { + target = std::numeric_limits::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(source); + } + + ORTHANC_FORCE_INLINE + static void FloatToPixel(PixelType& target, + float value) + { + if (value < static_cast(std::numeric_limits::min())) + { + target = std::numeric_limits::min(); + } + else if (value > static_cast(std::numeric_limits::max())) + { + target = std::numeric_limits::max(); + } + else + { + target = static_cast(value); + } + } + + ORTHANC_FORCE_INLINE + static bool IsEqual(const PixelType& a, + const PixelType& b) + { + return a == b; + } + }; + + + template + struct PixelTraits; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits : + public IntegerPixelTraits + { + }; + + + template <> + struct PixelTraits + { + 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::FloatToPixel(v, value); + + target.red_ = v; + target.green_ = v; + target.blue_ = v; + } + }; + + + template <> + struct PixelTraits + { + 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::FloatToPixel(v, value); + + target.blue_ = v; + target.green_ = v; + target.red_ = v; + target.alpha_ = 255; + } + }; + + + template <> + struct PixelTraits + { + 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::FloatToPixel(v, value); + + target.red_ = v; + target.green_ = v; + target.blue_ = v; + target.alpha_ = 255; + } + }; + + + template <> + struct PixelTraits + { + 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::epsilon(); + } + + ORTHANC_FORCE_INLINE + static void FloatToPixel(PixelType& target, + float value) + { + target = value; + } + + ORTHANC_FORCE_INLINE + static float PixelToFloat(const PixelType& source) + { + return source; + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PngReader.cpp --- a/Core/Images/PngReader.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/PngReader.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -36,11 +37,16 @@ #include "../OrthancException.h" #include "../Toolbox.h" +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + #include #include // For memcpy() namespace Orthanc { +#if ORTHANC_SANDBOXED == 0 namespace { struct FileRabi @@ -49,7 +55,7 @@ FileRabi(const char* filename) { - fp_ = fopen(filename, "rb"); + fp_ = SystemToolbox::OpenFile(filename, FileMode_ReadBinary); if (!fp_) { throw OrthancException(ErrorCode_InexistentFile); @@ -59,10 +65,13 @@ ~FileRabi() { if (fp_) + { fclose(fp_); + } } }; } +#endif struct PngReader::PngRabi @@ -204,9 +213,11 @@ AssignWritable(format, width, height, pitch, &data_[0]); } - void PngReader::ReadFromFile(const char* filename) + +#if ORTHANC_SANDBOXED == 0 + void PngReader::ReadFromFile(const std::string& filename) { - FileRabi f(filename); + FileRabi f(filename.c_str()); char header[8]; if (fread(header, 1, 8, f.fp_) != 8) @@ -228,6 +239,7 @@ Read(rabi); } +#endif namespace diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PngReader.h --- a/Core/Images/PngReader.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/PngReader.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,6 +33,14 @@ #pragma once +#if !defined(ORTHANC_ENABLE_PNG) +# error The macro ORTHANC_ENABLE_PNG must be defined +#endif + +#if ORTHANC_ENABLE_PNG != 1 +# error PNG support must be enabled to include this file +#endif + #include "ImageAccessor.h" #include "../Enumerations.h" @@ -40,6 +49,10 @@ #include #include +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + namespace Orthanc { class PngReader : public ImageAccessor @@ -56,12 +69,9 @@ public: PngReader(); - void ReadFromFile(const char* filename); - - void ReadFromFile(const std::string& filename) - { - ReadFromFile(filename.c_str()); - } +#if ORTHANC_SANDBOXED == 0 + void ReadFromFile(const std::string& filename); +#endif void ReadFromMemory(const void* buffer, size_t size); diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PngWriter.cpp --- a/Core/Images/PngWriter.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/PngWriter.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -40,6 +41,10 @@ #include "../ChunkedBuffer.h" #include "../Toolbox.h" +#if ORTHANC_SANDBOXED == 0 +# include "../SystemToolbox.h" +#endif + // http://www.libpng.org/pub/png/libpng-1.2.5-manual.html#section-4 // http://zarb.org/~gc/html/libpng.html @@ -202,16 +207,17 @@ } - void PngWriter::WriteToFile(const char* filename, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer) +#if ORTHANC_SANDBOXED == 0 + void PngWriter::WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) { Prepare(width, height, pitch, format, buffer); - FILE* fp = fopen(filename, "wb"); + FILE* fp = SystemToolbox::OpenFile(filename, FileMode_WriteBinary); if (!fp) { throw OrthancException(ErrorCode_CannotWriteFile); @@ -229,8 +235,7 @@ fclose(fp); } - - +#endif static void MemoryCallback(png_structp png_ptr, @@ -243,12 +248,12 @@ - void PngWriter::WriteToMemory(std::string& png, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer) + void PngWriter::WriteToMemoryInternal(std::string& png, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer) { ChunkedBuffer chunks; diff -r 2b91363cc1d1 -r 497a637366b4 Core/Images/PngWriter.h --- a/Core/Images/PngWriter.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Images/PngWriter.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,15 +33,39 @@ #pragma once -#include "ImageAccessor.h" +#if !defined(ORTHANC_ENABLE_PNG) +# error The macro ORTHANC_ENABLE_PNG must be defined +#endif + +#if ORTHANC_ENABLE_PNG != 1 +# error PNG support must be enabled to include this file +#endif + +#include "IImageWriter.h" #include -#include namespace Orthanc { - class PngWriter + class PngWriter : public IImageWriter { + protected: +#if ORTHANC_SANDBOXED == 0 + virtual void WriteToFileInternal(const std::string& filename, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer); +#endif + + virtual void WriteToMemoryInternal(std::string& png, + unsigned int width, + unsigned int height, + unsigned int pitch, + PixelFormat format, + const void* buffer); + private: struct PImpl; boost::shared_ptr pimpl_; @@ -60,33 +85,5 @@ PngWriter(); ~PngWriter(); - - void WriteToFile(const char* filename, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer); - - void WriteToMemory(std::string& png, - unsigned int width, - unsigned int height, - unsigned int pitch, - PixelFormat format, - const void* buffer); - - void WriteToFile(const char* filename, - const ImageAccessor& accessor) - { - WriteToFile(filename, accessor.GetWidth(), accessor.GetHeight(), - accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer()); - } - - void WriteToMemory(std::string& png, - const ImageAccessor& accessor) - { - WriteToMemory(png, accessor.GetWidth(), accessor.GetHeight(), - accessor.GetPitch(), accessor.GetFormat(), accessor.GetConstBuffer()); - } }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/GenericJobUnserializer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/GenericJobUnserializer.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,99 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "GenericJobUnserializer.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include "Operations/LogJobOperation.h" +#include "Operations/NullOperationValue.h" +#include "Operations/SequenceOfOperationsJob.h" +#include "Operations/StringOperationValue.h" + +namespace Orthanc +{ + IJob* GenericJobUnserializer::UnserializeJob(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "SequenceOfOperations") + { + return new SequenceOfOperationsJob(*this, source); + } + else + { + LOG(ERROR) << "Cannot unserialize job of type: " << type; + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "Log") + { + return new LogJobOperation; + } + else + { + LOG(ERROR) << "Cannot unserialize operation of type: " << type; + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + JobOperationValue* GenericJobUnserializer::UnserializeValue(const Json::Value& source) + { + const std::string type = SerializationToolbox::ReadString(source, "Type"); + + if (type == "String") + { + return new StringOperationValue(SerializationToolbox::ReadString(source, "Content")); + } + else if (type == "Null") + { + return new NullOperationValue; + } + else + { + LOG(ERROR) << "Cannot unserialize value of type: " << type; + throw OrthancException(ErrorCode_BadFileFormat); + } + } +} + diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/GenericJobUnserializer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/GenericJobUnserializer.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/IJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/IJob.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "JobStepResult.h" + +#include +#include + +namespace Orthanc +{ + class IJob : public boost::noncopyable + { + public: + virtual ~IJob() + { + } + + // Method called once the job enters the jobs engine + virtual void Start() = 0; + + virtual JobStepResult Step() = 0; + + // Method called once the job is resubmitted after a failure + virtual void Reset() = 0; + + // For pausing/canceling/ending jobs: This method must release allocated resources + virtual void Stop(JobStopReason reason) = 0; + + virtual float GetProgress() = 0; + + virtual void GetJobType(std::string& target) = 0; + + virtual void GetPublicContent(Json::Value& value) = 0; + + virtual bool Serialize(Json::Value& value) = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/IJobUnserializer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/IJobUnserializer.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,57 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IJob.h" +#include "Operations/JobOperationValue.h" +#include "Operations/IJobOperation.h" + +#include + +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; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobInfo.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobInfo.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,148 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobInfo.h" + +#include "../OrthancException.h" + +// This "include" is mandatory for Release builds using Linux Standard Base +#include + +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(runtime_.total_milliseconds()); + + if (status_.GetProgress() > 0.01f && + ms > 0.01f) + { + float ratio = static_cast(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(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(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()); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobInfo.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobInfo.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,120 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "JobStatus.h" + +#include + +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; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobStatus.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStatus.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobStatus.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + JobStatus::JobStatus() : + errorCode_(ErrorCode_InternalError), + progress_(0), + jobType_("Invalid"), + publicContent_(Json::objectValue), + hasSerialized_(false) + { + } + + + JobStatus::JobStatus(ErrorCode code, + IJob& job) : + errorCode_(code), + progress_(job.GetProgress()), + publicContent_(Json::objectValue) + { + if (progress_ < 0) + { + progress_ = 0; + } + + if (progress_ > 1) + { + progress_ = 1; + } + + job.GetJobType(jobType_); + job.GetPublicContent(publicContent_); + + hasSerialized_ = job.Serialize(serialized_); + } + + + const Json::Value& JobStatus::GetSerialized() const + { + if (!hasSerialized_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return serialized_; + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobStatus.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStatus.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,88 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IJob.h" + +namespace Orthanc +{ + class JobStatus + { + private: + ErrorCode errorCode_; + float progress_; + std::string jobType_; + Json::Value publicContent_; + Json::Value serialized_; + bool hasSerialized_; + + public: + JobStatus(); + + JobStatus(ErrorCode code, + IJob& job); + + ErrorCode GetErrorCode() const + { + return errorCode_; + } + + void SetErrorCode(ErrorCode error) + { + errorCode_ = error; + } + + float GetProgress() const + { + return progress_; + } + + const std::string& GetJobType() const + { + return jobType_; + } + + const Json::Value& GetPublicContent() const + { + return publicContent_; + } + + const Json::Value& GetSerialized() const; + + bool HasSerialized() const + { + return hasSerialized_; + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobStepResult.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStepResult.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobStepResult.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + JobStepResult JobStepResult::Retry(unsigned int timeout) + { + JobStepResult result(JobStepCode_Retry); + result.timeout_ = timeout; + return result; + } + + + JobStepResult JobStepResult::Failure(const ErrorCode& error) + { + JobStepResult result(JobStepCode_Failure); + result.error_ = error; + return result; + } + + + unsigned int JobStepResult::GetRetryTimeout() const + { + if (code_ == JobStepCode_Retry) + { + return timeout_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ErrorCode JobStepResult::GetFailureCode() const + { + if (code_ == JobStepCode_Failure) + { + return error_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobStepResult.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobStepResult.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,85 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Enumerations.h" + +namespace Orthanc +{ + class JobStepResult + { + private: + JobStepCode code_; + unsigned int timeout_; + ErrorCode error_; + + explicit JobStepResult(JobStepCode code) : + code_(code), + timeout_(0), + error_(ErrorCode_Success) + { + } + + public: + explicit JobStepResult() : + code_(JobStepCode_Failure), + timeout_(0), + error_(ErrorCode_InternalError) + { + } + + static JobStepResult Success() + { + return JobStepResult(JobStepCode_Success); + } + + static JobStepResult Continue() + { + return JobStepResult(JobStepCode_Continue); + } + + static JobStepResult Retry(unsigned int timeout); + + static JobStepResult Failure(const ErrorCode& error); + + JobStepCode GetCode() const + { + return code_; + } + + unsigned int GetRetryTimeout() const; + + ErrorCode GetFailureCode() const; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobsEngine.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsEngine.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,324 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobsEngine.h" + +#include "../Logging.h" +#include "../OrthancException.h" + +#include + +namespace Orthanc +{ + bool JobsEngine::IsRunning() + { + boost::mutex::scoped_lock lock(stateMutex_); + return (state_ == State_Running); + } + + + bool JobsEngine::ExecuteStep(JobsRegistry::RunningJob& running, + size_t workerIndex) + { + assert(running.IsValid()); + + if (running.IsPauseScheduled()) + { + running.GetJob().Stop(JobStopReason_Paused); + running.MarkPause(); + return false; + } + + if (running.IsCancelScheduled()) + { + running.GetJob().Stop(JobStopReason_Canceled); + running.MarkCanceled(); + return false; + } + + JobStepResult result; + + try + { + result = running.GetJob().Step(); + } + catch (OrthancException& e) + { + result = JobStepResult::Failure(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + result = JobStepResult::Failure(ErrorCode_BadFileFormat); + } + catch (...) + { + result = JobStepResult::Failure(ErrorCode_InternalError); + } + + switch (result.GetCode()) + { + case JobStepCode_Success: + running.GetJob().Stop(JobStopReason_Success); + running.UpdateStatus(ErrorCode_Success); + running.MarkSuccess(); + return false; + + case JobStepCode_Failure: + running.GetJob().Stop(JobStopReason_Failure); + running.UpdateStatus(result.GetFailureCode()); + running.MarkFailure(); + return false; + + case JobStepCode_Retry: + running.GetJob().Stop(JobStopReason_Retry); + running.UpdateStatus(ErrorCode_Success); + running.MarkRetry(result.GetRetryTimeout()); + return false; + + case JobStepCode_Continue: + running.UpdateStatus(ErrorCode_Success); + return true; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void JobsEngine::RetryHandler(JobsEngine* engine) + { + assert(engine != NULL); + + while (engine->IsRunning()) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(engine->threadSleep_)); + engine->GetRegistry().ScheduleRetries(); + } + } + + + void JobsEngine::Worker(JobsEngine* engine, + size_t workerIndex) + { + assert(engine != NULL); + + LOG(INFO) << "Worker thread " << workerIndex << " has started"; + + while (engine->IsRunning()) + { + JobsRegistry::RunningJob running(engine->GetRegistry(), engine->threadSleep_); + + if (running.IsValid()) + { + LOG(INFO) << "Executing job with priority " << running.GetPriority() + << " in worker thread " << workerIndex << ": " << running.GetId(); + + while (engine->IsRunning()) + { + if (!engine->ExecuteStep(running, workerIndex)) + { + break; + } + } + } + } + } + + + JobsEngine::JobsEngine() : + state_(State_Setup), + registry_(new JobsRegistry), + threadSleep_(200), + workers_(1) + { + } + + + JobsEngine::~JobsEngine() + { + if (state_ != State_Setup && + state_ != State_Done) + { + LOG(ERROR) << "INTERNAL ERROR: JobsEngine::Stop() should be invoked manually to avoid mess in the destruction order!"; + Stop(); + } + } + + + JobsRegistry& JobsEngine::GetRegistry() + { + if (registry_.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + return *registry_; + } + + + void JobsEngine::LoadRegistryFromJson(IJobUnserializer& unserializer, + const Json::Value& serialized) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + registry_.reset(new JobsRegistry(unserializer, serialized)); + } + + + void JobsEngine::LoadRegistryFromString(IJobUnserializer& unserializer, + const std::string& serialized) + { + Json::Value value; + Json::Reader reader; + if (reader.parse(serialized, value)) + { + LoadRegistryFromJson(unserializer, value); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void JobsEngine::SetWorkersCount(size_t count) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + workers_.resize(count); + } + + + void JobsEngine::SetThreadSleep(unsigned int sleep) + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + // Can only be invoked before calling "Start()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + threadSleep_ = sleep; + } + + + void JobsEngine::Start() + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Setup) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + retryHandler_ = boost::thread(RetryHandler, this); + + if (workers_.size() == 0) + { + // Use all the available CPUs + size_t n = boost::thread::hardware_concurrency(); + + if (n == 0) + { + n = 1; + } + + workers_.resize(n); + } + + for (size_t i = 0; i < workers_.size(); i++) + { + assert(workers_[i] == NULL); + workers_[i] = new boost::thread(Worker, this, i); + } + + state_ = State_Running; + + LOG(WARNING) << "The jobs engine has started with " << workers_.size() << " threads"; + } + + + void JobsEngine::Stop() + { + { + boost::mutex::scoped_lock lock(stateMutex_); + + if (state_ != State_Running) + { + return; + } + + state_ = State_Stopping; + } + + LOG(INFO) << "Stopping the jobs engine"; + + if (retryHandler_.joinable()) + { + retryHandler_.join(); + } + + for (size_t i = 0; i < workers_.size(); i++) + { + assert(workers_[i] != NULL); + + if (workers_[i]->joinable()) + { + workers_[i]->join(); + } + + delete workers_[i]; + } + + { + boost::mutex::scoped_lock lock(stateMutex_); + state_ = State_Done; + } + + LOG(WARNING) << "The jobs engine has stopped"; + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobsEngine.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsEngine.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,91 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "JobsRegistry.h" + +#include + +namespace Orthanc +{ + class JobsEngine + { + private: + enum State + { + State_Setup, + State_Running, + State_Stopping, + State_Done + }; + + boost::mutex stateMutex_; + State state_; + std::auto_ptr registry_; + boost::thread retryHandler_; + unsigned int threadSleep_; + std::vector workers_; + + bool IsRunning(); + + bool ExecuteStep(JobsRegistry::RunningJob& running, + size_t workerIndex); + + static void RetryHandler(JobsEngine* engine); + + static void Worker(JobsEngine* engine, + size_t workerIndex); + + public: + JobsEngine(); + + ~JobsEngine(); + + JobsRegistry& GetRegistry(); + + void LoadRegistryFromJson(IJobUnserializer& unserializer, + const Json::Value& serialized); + + void LoadRegistryFromString(IJobUnserializer& unserializer, + const std::string& serialized); + + void SetWorkersCount(size_t count); + + void SetThreadSleep(unsigned int sleep); + + void Start(); + + void Stop(); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobsRegistry.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsRegistry.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,1335 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "JobsRegistry.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../Toolbox.h" +#include "../SerializationToolbox.h" + +namespace Orthanc +{ + static const char* STATE = "State"; + static const char* TYPE = "Type"; + static const char* PRIORITY = "Priority"; + static const char* JOB = "Job"; + static const char* JOBS = "Jobs"; + static const char* JOBS_REGISTRY = "JobsRegistry"; + static const char* MAX_COMPLETED_JOBS = "MaxCompletedJobs"; + static const char* CREATION_TIME = "CreationTime"; + static const char* LAST_CHANGE_TIME = "LastChangeTime"; + static const char* RUNTIME = "Runtime"; + + + class JobsRegistry::JobHandler : public boost::noncopyable + { + private: + std::string id_; + JobState state_; + std::string jobType_; + std::auto_ptr 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(runtime_.total_milliseconds()); + return true; + } + else + { + LOG(INFO) << "Job backup is not supported for job of type: " << jobType_; + return false; + } + } + + JobHandler(IJobUnserializer& unserializer, + const Json::Value& serialized, + const std::string& id) : + id_(id), + pauseScheduled_(false), + cancelScheduled_(false) + { + state_ = StringToJobState(SerializationToolbox::ReadString(serialized, STATE)); + priority_ = SerializationToolbox::ReadInteger(serialized, PRIORITY); + creationTime_ = boost::posix_time::from_iso_string + (SerializationToolbox::ReadString(serialized, CREATION_TIME)); + lastStateChangeTime_ = boost::posix_time::from_iso_string + (SerializationToolbox::ReadString(serialized, LAST_CHANGE_TIME)); + runtime_ = boost::posix_time::milliseconds + (SerializationToolbox::ReadInteger(serialized, RUNTIME)); + + retryTime_ = creationTime_; + + job_.reset(unserializer.UnserializeJob(serialized[JOB])); + job_->GetJobType(jobType_); + job_->Start(); + + lastStatus_ = JobStatus(ErrorCode_Success, *job_); + } + }; + + + bool JobsRegistry::PriorityComparator::operator() (JobHandler*& a, + JobHandler*& b) const + { + return a->GetPriority() < b->GetPriority(); + } + + +#if defined(NDEBUG) + void JobsRegistry::CheckInvariants() const + { + } + +#else + bool JobsRegistry::IsPendingJob(const JobHandler& job) const + { + PendingJobs copy = pendingJobs_; + while (!copy.empty()) + { + if (copy.top() == &job) + { + return true; + } + + copy.pop(); + } + + return false; + } + + bool JobsRegistry::IsCompletedJob(JobHandler& job) const + { + for (CompletedJobs::const_iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + if (*it == &job) + { + return true; + } + } + + return false; + } + + bool JobsRegistry::IsRetryJob(JobHandler& job) const + { + return retryJobs_.find(&job) != retryJobs_.end(); + } + + void JobsRegistry::CheckInvariants() const + { + { + PendingJobs copy = pendingJobs_; + while (!copy.empty()) + { + assert(copy.top()->GetState() == JobState_Pending); + copy.pop(); + } + } + + assert(completedJobs_.size() <= maxCompletedJobs_); + + for (CompletedJobs::const_iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + assert((*it)->GetState() == JobState_Success || + (*it)->GetState() == JobState_Failure); + } + + for (RetryJobs::const_iterator it = retryJobs_.begin(); + it != retryJobs_.end(); ++it) + { + assert((*it)->GetState() == JobState_Retry); + } + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + JobHandler& job = *it->second; + + assert(job.GetId() == it->first); + + switch (job.GetState()) + { + case JobState_Pending: + assert(!IsRetryJob(job) && IsPendingJob(job) && !IsCompletedJob(job)); + break; + + case JobState_Success: + case JobState_Failure: + assert(!IsRetryJob(job) && !IsPendingJob(job) && IsCompletedJob(job)); + break; + + case JobState_Retry: + assert(IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job)); + break; + + case JobState_Running: + case JobState_Paused: + assert(!IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job)); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } +#endif + + + void JobsRegistry::ForgetOldCompletedJobs() + { + if (maxCompletedJobs_ != 0) + { + while (completedJobs_.size() > maxCompletedJobs_) + { + assert(completedJobs_.front() != NULL); + + std::string id = completedJobs_.front()->GetId(); + assert(jobsIndex_.find(id) != jobsIndex_.end()); + + jobsIndex_.erase(id); + delete(completedJobs_.front()); + completedJobs_.pop_front(); + } + } + } + + + void JobsRegistry::SetCompletedJob(JobHandler& job, + bool success) + { + job.SetState(success ? JobState_Success : JobState_Failure); + + completedJobs_.push_back(&job); + ForgetOldCompletedJobs(); + + someJobComplete_.notify_all(); + } + + + void JobsRegistry::MarkRunningAsCompleted(JobHandler& job, + bool success) + { + LOG(INFO) << "Job has completed with " << (success ? "success" : "failure") + << ": " << job.GetId(); + + CheckInvariants(); + + assert(job.GetState() == JobState_Running); + SetCompletedJob(job, success); + + if (observer_ != NULL) + { + if (success) + { + observer_->SignalJobSuccess(job.GetId()); + } + else + { + observer_->SignalJobFailure(job.GetId()); + } + } + + CheckInvariants(); + } + + + void JobsRegistry::MarkRunningAsRetry(JobHandler& job, + unsigned int timeout) + { + LOG(INFO) << "Job scheduled for retry in " << timeout << "ms: " << job.GetId(); + + CheckInvariants(); + + assert(job.GetState() == JobState_Running && + retryJobs_.find(&job) == retryJobs_.end()); + + retryJobs_.insert(&job); + job.SetRetryState(timeout); + + CheckInvariants(); + } + + + void JobsRegistry::MarkRunningAsPaused(JobHandler& job) + { + LOG(INFO) << "Job paused: " << job.GetId(); + + CheckInvariants(); + assert(job.GetState() == JobState_Running); + + job.SetState(JobState_Paused); + + CheckInvariants(); + } + + + bool JobsRegistry::GetStateInternal(JobState& state, + const std::string& id) + { + CheckInvariants(); + + JobsIndex::const_iterator it = jobsIndex_.find(id); + if (it == jobsIndex_.end()) + { + return false; + } + else + { + state = it->second->GetState(); + return true; + } + } + + + JobsRegistry::~JobsRegistry() + { + for (JobsIndex::iterator it = jobsIndex_.begin(); it != jobsIndex_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void JobsRegistry::SetMaxCompletedJobs(size_t n) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + LOG(INFO) << "The size of the history of the jobs engine is set to: " << n << " job(s)"; + + maxCompletedJobs_ = n; + ForgetOldCompletedJobs(); + + CheckInvariants(); + } + + + void JobsRegistry::ListJobs(std::set& target) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + target.insert(it->first); + } + } + + + bool JobsRegistry::GetJobInfo(JobInfo& target, + const std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::const_iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + return false; + } + else + { + const JobHandler& handler = *found->second; + target = JobInfo(handler.GetId(), + handler.GetPriority(), + handler.GetState(), + handler.GetLastStatus(), + handler.GetCreationTime(), + handler.GetLastStateChangeTime(), + handler.GetRuntime()); + return true; + } + } + + + void JobsRegistry::SubmitInternal(std::string& id, + JobHandler* handlerRaw, + bool keepLastChangeTime) + { + if (handlerRaw == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + std::auto_ptr handler(handlerRaw); + + boost::posix_time::ptime lastChangeTime = handler->GetLastStateChangeTime(); + + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + id = handler->GetId(); + int priority = handler->GetPriority(); + + switch (handler->GetState()) + { + case JobState_Pending: + case JobState_Retry: + case JobState_Running: + handler->SetState(JobState_Pending); + pendingJobs_.push(handler.get()); + pendingJobAvailable_.notify_one(); + break; + + case JobState_Success: + SetCompletedJob(*handler, true); + break; + + case JobState_Failure: + SetCompletedJob(*handler, false); + break; + + case JobState_Paused: + break; + + default: + LOG(ERROR) << "A job should not be loaded from state: " + << EnumerationToString(handler->GetState()); + throw OrthancException(ErrorCode_InternalError); + } + + if (keepLastChangeTime) + { + handler->SetLastStateChangeTime(lastChangeTime); + } + + jobsIndex_.insert(std::make_pair(id, handler.release())); + + LOG(INFO) << "New job submitted with priority " << priority << ": " << id; + + if (observer_ != NULL) + { + observer_->SignalJobSubmitted(id); + } + + CheckInvariants(); + } + } + + + void JobsRegistry::Submit(std::string& id, + IJob* job, // Takes ownership + int priority) + { + SubmitInternal(id, new JobHandler(job, priority), false); + } + + + void JobsRegistry::Submit(IJob* job, // Takes ownership + int priority) + { + std::string id; + SubmitInternal(id, new JobHandler(job, priority), false); + } + + + bool JobsRegistry::SubmitAndWait(Json::Value& successContent, + IJob* job, // Takes ownership + int priority) + { + std::string id; + Submit(id, job, priority); + + JobState state = JobState_Pending; // Dummy initialization + + { + boost::mutex::scoped_lock lock(mutex_); + + for (;;) + { + if (!GetStateInternal(state, id)) + { + // Job has finished and has been lost (should not happen) + state = JobState_Failure; + break; + } + else if (state == JobState_Failure) + { + // Failure + break; + } + else if (state == JobState_Success) + { + // Success, try and retrieve the status of the job + JobsIndex::const_iterator it = jobsIndex_.find(id); + if (it == jobsIndex_.end()) + { + // Should not happen + state = JobState_Failure; + } + else + { + const JobStatus& status = it->second->GetLastStatus(); + successContent = status.GetPublicContent(); + } + + break; + } + else + { + // This job has not finished yet, wait for new completion + someJobComplete_.wait(lock); + } + } + } + + return (state == JobState_Success); + } + + + bool JobsRegistry::SetPriority(const std::string& id, + int priority) + { + LOG(INFO) << "Changing priority to " << priority << " for job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + found->second->SetPriority(priority); + + if (found->second->GetState() == JobState_Pending) + { + // If the job is pending, we need to reconstruct the + // priority queue, as the heap condition has changed + + PendingJobs copy; + std::swap(copy, pendingJobs_); + + assert(pendingJobs_.empty()); + while (!copy.empty()) + { + pendingJobs_.push(copy.top()); + copy.pop(); + } + } + + CheckInvariants(); + return true; + } + } + + + void JobsRegistry::RemovePendingJob(const std::string& id) + { + // If the job is pending, we need to reconstruct the priority + // queue to remove it + PendingJobs copy; + std::swap(copy, pendingJobs_); + + assert(pendingJobs_.empty()); + while (!copy.empty()) + { + if (copy.top()->GetId() != id) + { + pendingJobs_.push(copy.top()); + } + + copy.pop(); + } + } + + + void JobsRegistry::RemoveRetryJob(JobHandler* handler) + { + RetryJobs::iterator item = retryJobs_.find(handler); + assert(item != retryJobs_.end()); + retryJobs_.erase(item); + } + + + bool JobsRegistry::Pause(const std::string& id) + { + LOG(INFO) << "Pausing job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + switch (found->second->GetState()) + { + case JobState_Pending: + RemovePendingJob(id); + found->second->SetState(JobState_Paused); + break; + + case JobState_Retry: + RemoveRetryJob(found->second); + found->second->SetState(JobState_Paused); + break; + + case JobState_Paused: + case JobState_Success: + case JobState_Failure: + // Nothing to be done + break; + + case JobState_Running: + found->second->SchedulePause(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Cancel(const std::string& id) + { + LOG(INFO) << "Canceling job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else + { + switch (found->second->GetState()) + { + case JobState_Pending: + RemovePendingJob(id); + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Retry: + RemoveRetryJob(found->second); + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Paused: + SetCompletedJob(*found->second, false); + found->second->SetLastErrorCode(ErrorCode_CanceledJob); + break; + + case JobState_Success: + case JobState_Failure: + // Nothing to be done + break; + + case JobState_Running: + found->second->ScheduleCancel(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Resume(const std::string& id) + { + LOG(INFO) << "Resuming job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else if (found->second->GetState() != JobState_Paused) + { + LOG(WARNING) << "Cannot resume a job that is not paused: " << id; + return false; + } + else + { + found->second->SetState(JobState_Pending); + pendingJobs_.push(found->second); + pendingJobAvailable_.notify_one(); + CheckInvariants(); + return true; + } + } + + + bool JobsRegistry::Resubmit(const std::string& id) + { + LOG(INFO) << "Resubmitting failed job: " << id; + + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::iterator found = jobsIndex_.find(id); + + if (found == jobsIndex_.end()) + { + LOG(WARNING) << "Unknown job: " << id; + return false; + } + else if (found->second->GetState() != JobState_Failure) + { + LOG(WARNING) << "Cannot resubmit a job that has not failed: " << id; + return false; + } + else + { + found->second->GetJob().Reset(); + + bool ok = false; + for (CompletedJobs::iterator it = completedJobs_.begin(); + it != completedJobs_.end(); ++it) + { + if (*it == found->second) + { + ok = true; + completedJobs_.erase(it); + break; + } + } + + assert(ok); + + found->second->SetState(JobState_Pending); + pendingJobs_.push(found->second); + pendingJobAvailable_.notify_one(); + + CheckInvariants(); + return true; + } + } + + + void JobsRegistry::ScheduleRetries() + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + RetryJobs copy; + std::swap(copy, retryJobs_); + + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + + assert(retryJobs_.empty()); + for (RetryJobs::iterator it = copy.begin(); it != copy.end(); ++it) + { + if ((*it)->IsRetryReady(now)) + { + LOG(INFO) << "Retrying job: " << (*it)->GetId(); + (*it)->SetState(JobState_Pending); + pendingJobs_.push(*it); + pendingJobAvailable_.notify_one(); + } + else + { + retryJobs_.insert(*it); + } + } + + CheckInvariants(); + } + + + bool JobsRegistry::GetState(JobState& state, + const std::string& id) + { + boost::mutex::scoped_lock lock(mutex_); + return GetStateInternal(state, id); + } + + + void JobsRegistry::SetObserver(JobsRegistry::IObserver& observer) + { + boost::mutex::scoped_lock lock(mutex_); + observer_ = &observer; + } + + + void JobsRegistry::ResetObserver() + { + boost::mutex::scoped_lock lock(mutex_); + observer_ = NULL; + } + + + JobsRegistry::RunningJob::RunningJob(JobsRegistry& registry, + unsigned int timeout) : + registry_(registry), + handler_(NULL), + targetState_(JobState_Failure), + targetRetryTimeout_(0), + canceled_(false) + { + { + boost::mutex::scoped_lock lock(registry_.mutex_); + + while (registry_.pendingJobs_.empty()) + { + if (timeout == 0) + { + registry_.pendingJobAvailable_.wait(lock); + } + else + { + bool success = registry_.pendingJobAvailable_.timed_wait + (lock, boost::posix_time::milliseconds(timeout)); + if (!success) + { + // No pending job + return; + } + } + } + + handler_ = registry_.pendingJobs_.top(); + registry_.pendingJobs_.pop(); + + assert(handler_->GetState() == JobState_Pending); + handler_->SetState(JobState_Running); + handler_->SetLastErrorCode(ErrorCode_Success); + + job_ = &handler_->GetJob(); + id_ = handler_->GetId(); + priority_ = handler_->GetPriority(); + } + } + + + JobsRegistry::RunningJob::~RunningJob() + { + if (IsValid()) + { + boost::mutex::scoped_lock lock(registry_.mutex_); + + switch (targetState_) + { + case JobState_Failure: + registry_.MarkRunningAsCompleted(*handler_, false); + + if (canceled_) + { + handler_->SetLastErrorCode(ErrorCode_CanceledJob); + } + + break; + + case JobState_Success: + registry_.MarkRunningAsCompleted(*handler_, true); + break; + + case JobState_Paused: + registry_.MarkRunningAsPaused(*handler_); + break; + + case JobState_Retry: + registry_.MarkRunningAsRetry(*handler_, targetRetryTimeout_); + break; + + default: + assert(0); + } + } + } + + + bool JobsRegistry::RunningJob::IsValid() const + { + return (handler_ != NULL && + job_ != NULL); + } + + + const std::string& JobsRegistry::RunningJob::GetId() const + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return id_; + } + } + + + int JobsRegistry::RunningJob::GetPriority() const + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return priority_; + } + } + + + IJob& JobsRegistry::RunningJob::GetJob() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *job_; + } + } + + + bool JobsRegistry::RunningJob::IsPauseScheduled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + return handler_->IsPauseScheduled(); + } + } + + + bool JobsRegistry::RunningJob::IsCancelScheduled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + return handler_->IsCancelScheduled(); + } + } + + + void JobsRegistry::RunningJob::MarkSuccess() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Success; + } + } + + + void JobsRegistry::RunningJob::MarkFailure() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Failure; + } + } + + + void JobsRegistry::RunningJob::MarkCanceled() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Failure; + canceled_ = true; + } + } + + + void JobsRegistry::RunningJob::MarkPause() + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Paused; + } + } + + + void JobsRegistry::RunningJob::MarkRetry(unsigned int timeout) + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + targetState_ = JobState_Retry; + targetRetryTimeout_ = timeout; + } + } + + + void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code) + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + JobStatus status(code, *job_); + + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + handler_->SetLastStatus(status); + } + } + + + + void JobsRegistry::Serialize(Json::Value& target) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + target = Json::objectValue; + target[TYPE] = JOBS_REGISTRY; + target[MAX_COMPLETED_JOBS] = static_cast(maxCompletedJobs_); + target[JOBS] = Json::objectValue; + + for (JobsIndex::const_iterator it = jobsIndex_.begin(); + it != jobsIndex_.end(); ++it) + { + Json::Value v; + if (it->second->Serialize(v)) + { + target[JOBS][it->first] = v; + } + } + } + + + JobsRegistry::JobsRegistry(IJobUnserializer& unserializer, + const Json::Value& s) : + observer_(NULL) + { + if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY || + !s.isMember(JOBS) || + s[JOBS].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + maxCompletedJobs_ = SerializationToolbox::ReadUnsignedInteger(s, MAX_COMPLETED_JOBS); + + Json::Value::Members members = s[JOBS].getMemberNames(); + + for (Json::Value::Members::const_iterator it = members.begin(); + it != members.end(); ++it) + { + std::auto_ptr job(new JobHandler(unserializer, s[JOBS][*it], *it)); + + std::string id; + SubmitInternal(id, job.release(), true); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/JobsRegistry.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/JobsRegistry.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,235 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 +#include +#include +#include +#include + +namespace Orthanc +{ + // This class handles the state machine of the jobs engine + class JobsRegistry : public boost::noncopyable + { + public: + class IObserver : public boost::noncopyable + { + public: + virtual ~IObserver() + { + } + + virtual void SignalJobSubmitted(const std::string& jobId) = 0; + + virtual void SignalJobSuccess(const std::string& jobId) = 0; + + virtual void SignalJobFailure(const std::string& jobId) = 0; + }; + + private: + class JobHandler; + + struct PriorityComparator + { + bool operator() (JobHandler*& a, + JobHandler*& b) const; + }; + + typedef std::map JobsIndex; + typedef std::list CompletedJobs; + typedef std::set RetryJobs; + typedef std::priority_queue, // Could be a "std::deque" + PriorityComparator> PendingJobs; + + boost::mutex mutex_; + JobsIndex jobsIndex_; + PendingJobs pendingJobs_; + CompletedJobs completedJobs_; + RetryJobs retryJobs_; + + boost::condition_variable pendingJobAvailable_; + boost::condition_variable someJobComplete_; + size_t maxCompletedJobs_; + + IObserver* observer_; + + +#ifndef NDEBUG + bool IsPendingJob(const JobHandler& job) const; + + bool IsCompletedJob(JobHandler& job) const; + + bool IsRetryJob(JobHandler& job) const; +#endif + + void CheckInvariants() const; + + void ForgetOldCompletedJobs(); + + void SetCompletedJob(JobHandler& job, + bool success); + + void MarkRunningAsCompleted(JobHandler& job, + bool success); + + void MarkRunningAsRetry(JobHandler& job, + unsigned int timeout); + + void MarkRunningAsPaused(JobHandler& job); + + bool GetStateInternal(JobState& state, + const std::string& id); + + void RemovePendingJob(const std::string& id); + + void RemoveRetryJob(JobHandler* handler); + + void SubmitInternal(std::string& id, + JobHandler* handler, + bool keepLastChangeTime); + + public: + JobsRegistry() : + maxCompletedJobs_(10), + observer_(NULL) + { + } + + JobsRegistry(IJobUnserializer& unserializer, + const Json::Value& s); + + ~JobsRegistry(); + + void SetMaxCompletedJobs(size_t i); + + void ListJobs(std::set& target); + + bool GetJobInfo(JobInfo& target, + const std::string& id); + + void Serialize(Json::Value& target); + + void Submit(std::string& id, + IJob* job, // Takes ownership + int priority); + + void Submit(IJob* job, // Takes ownership + int priority); + + bool SubmitAndWait(Json::Value& successContent, + IJob* job, // Takes ownership + int priority); + + bool SetPriority(const std::string& id, + int priority); + + bool Pause(const std::string& id); + + bool Resume(const std::string& id); + + bool Resubmit(const std::string& id); + + bool Cancel(const std::string& id); + + void ScheduleRetries(); + + bool GetState(JobState& state, + const std::string& id); + + void SetObserver(IObserver& observer); + + void ResetObserver(); + + class RunningJob : public boost::noncopyable + { + private: + JobsRegistry& registry_; + JobHandler* handler_; // Can only be accessed if the + // registry mutex is locked! + IJob* job_; // Will by design be in mutual exclusion, + // because only one RunningJob can be + // executed at a time on a JobHandler + + std::string id_; + int priority_; + JobState targetState_; + unsigned int targetRetryTimeout_; + bool canceled_; + + public: + RunningJob(JobsRegistry& registry, + unsigned int timeout); + + ~RunningJob(); + + bool IsValid() const; + + const std::string& GetId() const; + + int GetPriority() const; + + IJob& GetJob(); + + bool IsPauseScheduled(); + + bool IsCancelScheduled(); + + void MarkSuccess(); + + void MarkFailure(); + + void MarkPause(); + + void MarkCanceled(); + + void MarkRetry(unsigned int timeout); + + void UpdateStatus(ErrorCode code); + }; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/IJobOperation.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/IJobOperation.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "JobOperationValues.h" +#include "../../DicomNetworking/IDicomConnectionManager.h" + +namespace Orthanc +{ + class IJobOperation : public boost::noncopyable + { + public: + virtual ~IJobOperation() + { + } + + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& dicomConnection) = 0; + + virtual void Serialize(Json::Value& result) const = 0; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/JobOperationValue.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/JobOperationValue.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,74 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +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; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/JobOperationValues.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/JobOperationValues.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,143 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "JobOperationValues.h" + +#include "../IJobUnserializer.h" +#include "../../OrthancException.h" + +#include +#include + +namespace Orthanc +{ + void JobOperationValues::Append(JobOperationValues& target, + bool clear) + { + target.Reserve(target.GetSize() + GetSize()); + + for (size_t i = 0; i < values_.size(); i++) + { + if (clear) + { + target.Append(values_[i]); + values_[i] = NULL; + } + else + { + target.Append(GetValue(i).Clone()); + } + } + + if (clear) + { + Clear(); + } + } + + + void JobOperationValues::Clear() + { + for (size_t i = 0; i < values_.size(); i++) + { + if (values_[i] != NULL) + { + delete values_[i]; + } + } + + values_.clear(); + } + + + void JobOperationValues::Append(JobOperationValue* value) // Takes ownership + { + if (value == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + values_.push_back(value); + } + } + + + JobOperationValue& JobOperationValues::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(values_[index] != NULL); + return *values_[index]; + } + } + + + void JobOperationValues::Serialize(Json::Value& target) const + { + target = Json::arrayValue; + + for (size_t i = 0; i < values_.size(); i++) + { + Json::Value tmp; + values_[i]->Serialize(tmp); + target.append(tmp); + } + } + + + JobOperationValues* JobOperationValues::Unserialize(IJobUnserializer& unserializer, + const Json::Value& source) + { + if (source.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + std::auto_ptr 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(); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/JobOperationValues.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/JobOperationValues.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,89 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "JobOperationValue.h" + +#include + +namespace Orthanc +{ + class IJobUnserializer; + + class JobOperationValues : public boost::noncopyable + { + private: + std::vector 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); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/LogJobOperation.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/LogJobOperation.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,64 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "LogJobOperation.h" + +#include "../../Logging.h" +#include "StringOperationValue.h" + +namespace Orthanc +{ + void LogJobOperation::Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager) + { + switch (input.GetType()) + { + case JobOperationValue::Type_String: + LOG(INFO) << "Job value: " + << dynamic_cast(input).GetContent(); + break; + + case JobOperationValue::Type_Null: + LOG(INFO) << "Job value: (null)"; + break; + + default: + LOG(INFO) << "Job value: (unsupport)"; + break; + } + + outputs.Append(input.Clone()); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/LogJobOperation.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/LogJobOperation.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IJobOperation.h" + +namespace Orthanc +{ + class LogJobOperation : public IJobOperation + { + public: + virtual void Apply(JobOperationValues& outputs, + const JobOperationValue& input, + IDicomConnectionManager& connectionManager); + + virtual void Serialize(Json::Value& result) const + { + result = Json::objectValue; + result["Type"] = "Log"; + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/NullOperationValue.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/NullOperationValue.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,59 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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"; + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,495 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../../PrecompiledHeaders.h" +#include "SequenceOfOperationsJob.h" + +#include "../../Logging.h" +#include "../../OrthancException.h" +#include "../../SerializationToolbox.h" +#include "../IJobUnserializer.h" + +namespace Orthanc +{ + static const char* CURRENT = "Current"; + static const char* DESCRIPTION = "Description"; + static const char* DICOM_TIMEOUT = "DicomTimeout"; + static const char* NEXT_OPERATIONS = "Next"; + static const char* OPERATION = "Operation"; + static const char* OPERATIONS = "Operations"; + static const char* ORIGINAL_INPUTS = "OriginalInputs"; + static const char* TRAILING_TIMEOUT = "TrailingTimeout"; + static const char* TYPE = "Type"; + static const char* WORK_INPUTS = "WorkInputs"; + + + class SequenceOfOperationsJob::Operation : public boost::noncopyable + { + private: + size_t index_; + std::auto_ptr operation_; + std::auto_ptr originalInputs_; + std::auto_ptr workInputs_; + std::list nextOperations_; + size_t currentInput_; + + public: + Operation(size_t index, + IJobOperation* operation) : + index_(index), + operation_(operation), + originalInputs_(new JobOperationValues), + workInputs_(new JobOperationValues), + currentInput_(0) + { + if (operation == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + } + + void AddOriginalInput(const JobOperationValue& value) + { + if (currentInput_ != 0) + { + // Cannot add input after processing has started + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + originalInputs_->Append(value.Clone()); + } + } + + const JobOperationValues& GetOriginalInputs() const + { + return *originalInputs_; + } + + void Reset() + { + workInputs_->Clear(); + currentInput_ = 0; + } + + void AddNextOperation(Operation& other, + bool unserializing) + { + if (other.index_ <= index_) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (!unserializing && + currentInput_ != 0) + { + // Cannot add input after processing has started + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + nextOperations_.push_back(&other); + } + } + + bool IsDone() const + { + return currentInput_ >= originalInputs_->GetSize() + workInputs_->GetSize(); + } + + void Step(IDicomConnectionManager& connectionManager) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const JobOperationValue* input; + + if (currentInput_ < originalInputs_->GetSize()) + { + input = &originalInputs_->GetValue(currentInput_); + } + else + { + input = &workInputs_->GetValue(currentInput_ - originalInputs_->GetSize()); + } + + JobOperationValues outputs; + operation_->Apply(outputs, *input, connectionManager); + + if (!nextOperations_.empty()) + { + std::list::iterator first = nextOperations_.begin(); + outputs.Move(*(*first)->workInputs_); + + std::list::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(currentInput_); + operation_->Serialize(target[OPERATION]); + originalInputs_->Serialize(target[ORIGINAL_INPUTS]); + workInputs_->Serialize(target[WORK_INPUTS]); + + Json::Value tmp = Json::arrayValue; + for (std::list::const_iterator it = nextOperations_.begin(); + it != nextOperations_.end(); ++it) + { + tmp.append(static_cast((*it)->index_)); + } + + target[NEXT_OPERATIONS] = tmp; + } + + Operation(IJobUnserializer& unserializer, + Json::Value::ArrayIndex index, + const Json::Value& serialized) : + index_(index) + { + if (serialized.type() != Json::objectValue || + !serialized.isMember(OPERATION) || + !serialized.isMember(ORIGINAL_INPUTS) || + !serialized.isMember(WORK_INPUTS)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + currentInput_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT); + operation_.reset(unserializer.UnserializeOperation(serialized[OPERATION])); + originalInputs_.reset(JobOperationValues::Unserialize + (unserializer, serialized[ORIGINAL_INPUTS])); + workInputs_.reset(JobOperationValues::Unserialize + (unserializer, serialized[WORK_INPUTS])); + } + }; + + + SequenceOfOperationsJob::SequenceOfOperationsJob() : + done_(false), + current_(0), + trailingTimeout_(boost::posix_time::milliseconds(1000)) + { + } + + + SequenceOfOperationsJob::~SequenceOfOperationsJob() + { + for (size_t i = 0; i < operations_.size(); i++) + { + if (operations_[i] != NULL) + { + delete operations_[i]; + } + } + } + + + void SequenceOfOperationsJob::SetDescription(const std::string& description) + { + boost::mutex::scoped_lock lock(mutex_); + description_ = description; + } + + + void SequenceOfOperationsJob::GetDescription(std::string& description) + { + boost::mutex::scoped_lock lock(mutex_); + description = description_; + } + + + void SequenceOfOperationsJob::Register(IObserver& observer) + { + boost::mutex::scoped_lock lock(mutex_); + observers_.push_back(&observer); + } + + + void SequenceOfOperationsJob::Lock::SetTrailingOperationTimeout(unsigned int timeout) + { + that_.trailingTimeout_ = boost::posix_time::milliseconds(timeout); + } + + + void SequenceOfOperationsJob::Lock::SetDicomAssociationTimeout(unsigned int timeout) + { + that_.connectionManager_.SetTimeout(timeout); + } + + + size_t SequenceOfOperationsJob::Lock::AddOperation(IJobOperation* operation) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + size_t index = that_.operations_.size(); + + that_.operations_.push_back(new Operation(index, operation)); + that_.operationAdded_.notify_one(); + + return index; + } + + + void SequenceOfOperationsJob::Lock::AddInput(size_t index, + const JobOperationValue& value) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (index >= that_.operations_.size() || + index < that_.current_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + that_.operations_[index]->AddOriginalInput(value); + } + } + + + void SequenceOfOperationsJob::Lock::Connect(size_t input, + size_t output) + { + if (IsDone()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (input >= output || + input >= that_.operations_.size() || + output >= that_.operations_.size() || + input < that_.current_ || + output < that_.current_) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + Operation& a = *that_.operations_[input]; + Operation& b = *that_.operations_[output]; + a.AddNextOperation(b, false /* not unserializing */); + } + } + + + JobStepResult SequenceOfOperationsJob::Step() + { + boost::mutex::scoped_lock lock(mutex_); + + if (current_ == operations_.size()) + { + LOG(INFO) << "Executing the trailing timeout in the sequence of operations"; + operationAdded_.timed_wait(lock, trailingTimeout_); + + if (current_ == operations_.size()) + { + // No operation was added during the trailing timeout: The + // job is over + LOG(INFO) << "The sequence of operations is over"; + done_ = true; + + for (std::list::iterator it = observers_.begin(); + it != observers_.end(); ++it) + { + (*it)->SignalDone(*this); + } + + connectionManager_.Close(); + return JobStepResult::Success(); + } + else + { + LOG(INFO) << "New operation were added to the sequence of operations"; + } + } + + assert(current_ < operations_.size()); + + while (current_ < operations_.size() && + operations_[current_]->IsDone()) + { + current_++; + } + + if (current_ < operations_.size()) + { + operations_[current_]->Step(connectionManager_); + } + + connectionManager_.CheckTimeout(); + + return JobStepResult::Continue(); + } + + + void SequenceOfOperationsJob::Reset() + { + boost::mutex::scoped_lock lock(mutex_); + + current_ = 0; + done_ = false; + + for (size_t i = 0; i < operations_.size(); i++) + { + operations_[i]->Reset(); + } + } + + + void SequenceOfOperationsJob::Stop(JobStopReason reason) + { + boost::mutex::scoped_lock lock(mutex_); + connectionManager_.Close(); + } + + + float SequenceOfOperationsJob::GetProgress() + { + boost::mutex::scoped_lock lock(mutex_); + + return (static_cast(current_) / + static_cast(operations_.size() + 1)); + } + + + void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) + { + boost::mutex::scoped_lock lock(mutex_); + + value["CountOperations"] = static_cast(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(trailingTimeout_.total_milliseconds()); + value[DICOM_TIMEOUT] = connectionManager_.GetTimeout(); + value[CURRENT] = static_cast(current_); + + Json::Value tmp = Json::arrayValue; + for (size_t i = 0; i < operations_.size(); i++) + { + Json::Value operation = Json::objectValue; + operations_[i]->Serialize(operation); + tmp.append(operation); + } + + value[OPERATIONS] = tmp; + + return true; + } + + + SequenceOfOperationsJob::SequenceOfOperationsJob(IJobUnserializer& unserializer, + const Json::Value& serialized) : + done_(false) + { + std::string jobType; + GetJobType(jobType); + + if (SerializationToolbox::ReadString(serialized, TYPE) != jobType || + !serialized.isMember(OPERATIONS) || + serialized[OPERATIONS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + description_ = SerializationToolbox::ReadString(serialized, DESCRIPTION); + trailingTimeout_ = boost::posix_time::milliseconds + (SerializationToolbox::ReadUnsignedInteger(serialized, TRAILING_TIMEOUT)); + connectionManager_.SetTimeout + (SerializationToolbox::ReadUnsignedInteger(serialized, DICOM_TIMEOUT)); + current_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT); + + const Json::Value& ops = serialized[OPERATIONS]; + + // Unserialize the individual operations + operations_.reserve(ops.size()); + for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++) + { + operations_.push_back(new Operation(unserializer, i, ops[i])); + } + + // Connect the next operations + for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++) + { + if (!ops[i].isMember(NEXT_OPERATIONS) || + ops[i][NEXT_OPERATIONS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& next = ops[i][NEXT_OPERATIONS]; + for (Json::Value::ArrayIndex j = 0; j < next.size(); j++) + { + if (next[j].type() != Json::intValue || + next[j].asInt() < 0 || + next[j].asUInt() >= operations_.size()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + operations_[i]->AddNextOperation(*operations_[next[j].asUInt()], true); + } + } + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/SequenceOfOperationsJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,153 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IJob.h" +#include "IJobOperation.h" + +#include "../../DicomNetworking/TimeoutDicomConnectionManager.h" + +#include +#include + +#include + +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 operations_; + size_t current_; + boost::condition_variable operationAdded_; + boost::posix_time::time_duration trailingTimeout_; + std::list observers_; + TimeoutDicomConnectionManager connectionManager_; + + public: + SequenceOfOperationsJob(); + + SequenceOfOperationsJob(IJobUnserializer& unserializer, + const Json::Value& serialized); + + virtual ~SequenceOfOperationsJob(); + + void SetDescription(const std::string& description); + + void GetDescription(std::string& description); + + void Register(IObserver& observer); + + // This lock allows adding new operations to the end of the job, + // from another thread than the worker thread, after the job has + // been submitted for processing + class Lock : public boost::noncopyable + { + private: + SequenceOfOperationsJob& that_; + boost::mutex::scoped_lock lock_; + + public: + Lock(SequenceOfOperationsJob& that) : + that_(that), + lock_(that.mutex_) + { + } + + bool IsDone() const + { + return that_.done_; + } + + void SetTrailingOperationTimeout(unsigned int timeout); + + void SetDicomAssociationTimeout(unsigned int timeout); + + size_t AddOperation(IJobOperation* operation); + + size_t GetOperationsCount() const + { + return that_.operations_.size(); + } + + void AddInput(size_t index, + const JobOperationValue& value); + + void Connect(size_t input, + size_t output); + }; + + virtual void Start() + { + } + + virtual JobStepResult Step(); + + virtual void Reset(); + + virtual void Stop(JobStopReason reason); + + virtual float GetProgress(); + + virtual void GetJobType(std::string& target) + { + target = "SequenceOfOperations"; + } + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& value); + + void AwakeTrailingSleep() + { + operationAdded_.notify_one(); + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/Operations/StringOperationValue.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/Operations/StringOperationValue.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,71 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "JobOperationValue.h" + +#include + +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_; + } + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/SetOfCommandsJob.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/SetOfCommandsJob.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,303 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "SetOfCommandsJob.h" + +#include "../Logging.h" +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include +#include + +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(position_) / + static_cast(commands_.size())); + } + } + + + const SetOfCommandsJob::ICommand& SetOfCommandsJob::GetCommand(size_t index) const + { + if (index >= commands_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(commands_[index] != NULL); + return *commands_[index]; + } + } + + + JobStepResult SetOfCommandsJob::Step() + { + if (!started_) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (commands_.empty() && + position_ == 0) + { + // No command to handle: We're done + position_ = 1; + return JobStepResult::Success(); + } + + if (position_ >= commands_.size()) + { + // Already done + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + try + { + // Not at the trailing step: Handle the current command + if (!commands_[position_]->Execute()) + { + // Error + if (!permissive_) + { + return JobStepResult::Failure(ErrorCode_InternalError); + } + } + } + catch (OrthancException& e) + { + if (permissive_) + { + LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What(); + } + else + { + return JobStepResult::Failure(e.GetErrorCode()); + } + } + + position_ += 1; + + if (position_ == commands_.size()) + { + // We're done + return JobStepResult::Success(); + } + else + { + return JobStepResult::Continue(); + } + } + + + + static const char* KEY_DESCRIPTION = "Description"; + static const char* KEY_PERMISSIVE = "Permissive"; + static const char* KEY_POSITION = "Position"; + static const char* KEY_TYPE = "Type"; + static const char* KEY_COMMANDS = "Commands"; + + + void SetOfCommandsJob::GetPublicContent(Json::Value& value) + { + value[KEY_DESCRIPTION] = GetDescription(); + } + + + bool SetOfCommandsJob::Serialize(Json::Value& target) + { + target = Json::objectValue; + + std::string type; + GetJobType(type); + target[KEY_TYPE] = type; + + target[KEY_PERMISSIVE] = permissive_; + target[KEY_POSITION] = static_cast(position_); + target[KEY_DESCRIPTION] = description_; + + target[KEY_COMMANDS] = Json::arrayValue; + Json::Value& tmp = target[KEY_COMMANDS]; + + for (size_t i = 0; i < commands_.size(); i++) + { + assert(commands_[i] != NULL); + + Json::Value command; + commands_[i]->Serialize(command); + tmp.append(command); + } + + return true; + } + + + SetOfCommandsJob::SetOfCommandsJob(ICommandUnserializer* unserializer, + const Json::Value& source) : + started_(false) + { + std::auto_ptr 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); + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/SetOfCommandsJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/SetOfCommandsJob.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,135 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IJob.h" + +#include + +namespace Orthanc +{ + class SetOfCommandsJob : public IJob + { + public: + class ICommand : public boost::noncopyable + { + public: + virtual ~ICommand() + { + } + + virtual bool Execute() = 0; + + virtual void Serialize(Json::Value& target) const = 0; + }; + + class ICommandUnserializer : public boost::noncopyable + { + public: + virtual ~ICommandUnserializer() + { + } + + virtual ICommand* Unserialize(const Json::Value& source) const = 0; + }; + + private: + bool started_; + std::vector commands_; + bool permissive_; + size_t position_; + std::string description_; + + public: + SetOfCommandsJob(); + + SetOfCommandsJob(ICommandUnserializer* unserializer /* takes ownership */, + const Json::Value& source); + + virtual ~SetOfCommandsJob(); + + size_t GetPosition() const + { + return position_; + } + + void SetDescription(const std::string& description) + { + description_ = description; + } + + const std::string& GetDescription() const + { + return description_; + } + + void Reserve(size_t size); + + size_t GetCommandsCount() const + { + return commands_.size(); + } + + void AddCommand(ICommand* command); // Takes ownership + + bool IsPermissive() const + { + return permissive_; + } + + void SetPermissive(bool permissive); + + virtual void Reset(); + + virtual void Start() + { + started_ = true; + } + + virtual float GetProgress(); + + bool IsStarted() const + { + return started_; + } + + const ICommand& GetCommand(size_t index) const; + + virtual JobStepResult Step(); + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& target); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/SetOfInstancesJob.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/SetOfInstancesJob.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,240 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "SetOfInstancesJob.h" + +#include "../OrthancException.h" +#include "../SerializationToolbox.h" + +#include + +namespace Orthanc +{ + class SetOfInstancesJob::InstanceCommand : public SetOfInstancesJob::ICommand + { + private: + SetOfInstancesJob& that_; + std::string instance_; + + public: + InstanceCommand(SetOfInstancesJob& that, + const std::string& instance) : + that_(that), + instance_(instance) + { + } + + const std::string& GetInstance() const + { + return instance_; + } + + virtual bool Execute() + { + if (!that_.HandleInstance(instance_)) + { + that_.failedInstances_.insert(instance_); + return false; + } + else + { + return true; + } + } + + virtual void Serialize(Json::Value& target) const + { + target = instance_; + } + }; + + + class SetOfInstancesJob::TrailingStepCommand : public SetOfInstancesJob::ICommand + { + private: + SetOfInstancesJob& that_; + + public: + TrailingStepCommand(SetOfInstancesJob& that) : + that_(that) + { + } + + virtual bool Execute() + { + return that_.HandleTrailingStep(); + } + + virtual void Serialize(Json::Value& target) const + { + target = Json::nullValue; + } + }; + + + class SetOfInstancesJob::InstanceUnserializer : + public SetOfInstancesJob::ICommandUnserializer + { + private: + SetOfInstancesJob& that_; + + public: + InstanceUnserializer(SetOfInstancesJob& that) : + that_(that) + { + } + + virtual ICommand* Unserialize(const Json::Value& source) const + { + if (source.type() == Json::nullValue) + { + return new TrailingStepCommand(that_); + } + else if (source.type() == Json::stringValue) + { + return new InstanceCommand(that_, source.asString()); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + }; + + + SetOfInstancesJob::SetOfInstancesJob() : + hasTrailingStep_(false) + { + } + + + void SetOfInstancesJob::AddInstance(const std::string& instance) + { + AddCommand(new InstanceCommand(*this, instance)); + } + + + void SetOfInstancesJob::AddTrailingStep() + { + AddCommand(new TrailingStepCommand(*this)); + hasTrailingStep_ = true; + } + + + size_t SetOfInstancesJob::GetInstancesCount() const + { + if (hasTrailingStep_) + { + assert(GetCommandsCount() > 0); + return GetCommandsCount() - 1; + } + else + { + return GetCommandsCount(); + } + } + + + const std::string& SetOfInstancesJob::GetInstance(size_t index) const + { + if (index >= GetInstancesCount()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return dynamic_cast(GetCommand(index)).GetInstance(); + } + } + + + void SetOfInstancesJob::Start() + { + SetOfCommandsJob::Start(); + } + + + void SetOfInstancesJob::Reset() + { + SetOfCommandsJob::Reset(); + + failedInstances_.clear(); + } + + + void SetOfInstancesJob::GetPublicContent(Json::Value& target) + { + SetOfCommandsJob::GetPublicContent(target); + target["InstancesCount"] = static_cast(GetInstancesCount()); + target["FailedInstancesCount"] = static_cast(failedInstances_.size()); + } + + + + static const char* KEY_TRAILING_STEP = "TrailingStep"; + static const char* KEY_FAILED_INSTANCES = "FailedInstances"; + + bool SetOfInstancesJob::Serialize(Json::Value& target) + { + if (SetOfCommandsJob::Serialize(target)) + { + target[KEY_TRAILING_STEP] = hasTrailingStep_; + SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES); + return true; + } + else + { + return false; + } + } + + + SetOfInstancesJob::SetOfInstancesJob(const Json::Value& source) : + SetOfCommandsJob(new InstanceUnserializer(*this), source) + { + SerializationToolbox::ReadSetOfStrings(failedInstances_, source, KEY_FAILED_INSTANCES); + + if (source.isMember(KEY_TRAILING_STEP)) + { + hasTrailingStep_ = SerializationToolbox::ReadBoolean(source, KEY_TRAILING_STEP); + } + else + { + // Backward compatibility with Orthanc <= 1.4.2 + hasTrailingStep_ = false; + } + } + + +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/JobsEngine/SetOfInstancesJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/JobsEngine/SetOfInstancesJob.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,97 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IJob.h" +#include "SetOfCommandsJob.h" + +#include + +namespace Orthanc +{ + class SetOfInstancesJob : public SetOfCommandsJob + { + private: + class InstanceCommand; + class TrailingStepCommand; + class InstanceUnserializer; + + bool hasTrailingStep_; + std::set failedInstances_; + + protected: + virtual bool HandleInstance(const std::string& instance) = 0; + + virtual bool HandleTrailingStep() = 0; + + // Hiding this method, use AddInstance() instead + using SetOfCommandsJob::AddCommand; + + public: + SetOfInstancesJob(); + + SetOfInstancesJob(const Json::Value& source); // Unserialization + + void AddInstance(const std::string& instance); + + void AddTrailingStep(); + + size_t GetInstancesCount() const; + + const std::string& GetInstance(size_t index) const; + + bool HasTrailingStep() const + { + return hasTrailingStep_; + } + + const std::set& 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); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Logging.cpp --- a/Core/Logging.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Logging.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -47,6 +48,14 @@ { } + void Reset() + { + } + + void Flush() + { + } + void EnableInfoLevel(bool enabled) { } @@ -55,90 +64,179 @@ { } + void SetTargetFile(const std::string& path) + { + } + void SetTargetFolder(const std::string& path) { } } } -#elif ORTHANC_ENABLE_GOOGLE_LOG == 1 + +#elif ORTHANC_ENABLE_LOGGING_PLUGIN == 1 /********************************************************* - * Wrapper around Google Log + * Logger compatible with the Orthanc plugin SDK *********************************************************/ +#include + namespace Orthanc -{ +{ namespace Logging { - void Initialize() + static OrthancPluginContext* context_ = NULL; + + void Initialize(OrthancPluginContext* context) { - // Initialize Google's logging library. - FLAGS_logtostderr = true; - FLAGS_minloglevel = 1; // Do not print LOG(INFO) by default - FLAGS_v = 0; // Do not print trace-level VLOG(1) by default + context_ = context; + } - google::InitGoogleLogging("Orthanc"); + InternalLogger::InternalLogger(InternalLevel level, + const char* file /* ignored */, + int line /* ignored */) : + level_(level) + { } - void Finalize() + InternalLogger::~InternalLogger() { - google::ShutdownGoogleLogging(); + if (context_ != NULL) + { + switch (level_) + { + case InternalLevel_ERROR: + OrthancPluginLogError(context_, message_.c_str()); + break; + + case InternalLevel_WARNING: + OrthancPluginLogWarning(context_, message_.c_str()); + break; + + case InternalLevel_INFO: + OrthancPluginLogInfo(context_, message_.c_str()); + break; + + case InternalLevel_TRACE: + // Not used by plugins + break; + + default: + { + std::string s = ("Unknown log level (" + boost::lexical_cast(level_) + + ") for message: " + message_); + OrthancPluginLogError(context_, s.c_str()); + break; + } + } + } + } + } +} + + +#elif ORTHANC_ENABLE_LOGGING_STDIO == 1 + +/********************************************************* + * Logger compatible with + *********************************************************/ + +#include +#include + +namespace Orthanc +{ + namespace Logging + { + static bool globalVerbose_ = false; + static bool globalTrace_ = false; + + InternalLogger::InternalLogger(InternalLevel level, + const char* file /* ignored */, + int line /* ignored */) : + level_(level) + { + } + + InternalLogger::~InternalLogger() + { + switch (level_) + { + case InternalLevel_ERROR: + fprintf(stderr, "E: %s\n", message_.c_str()); + break; + + case InternalLevel_WARNING: + fprintf(stdout, "W: %s\n", message_.c_str()); + break; + + case InternalLevel_INFO: + if (globalVerbose_) + { + fprintf(stdout, "I: %s\n", message_.c_str()); + } + break; + + case InternalLevel_TRACE: + if (globalTrace_) + { + fprintf(stdout, "T: %s\n", message_.c_str()); + } + break; + + default: + fprintf(stderr, "Unknown log level (%d) for message: %s\n", level_, message_.c_str()); + } } void EnableInfoLevel(bool enabled) { - FLAGS_minloglevel = (enabled ? 0 : 1); + globalVerbose_ = enabled; } void EnableTraceLevel(bool enabled) { - if (enabled) - { - FLAGS_minloglevel = 0; - FLAGS_v = 1; - } - else - { - FLAGS_v = 0; - } - } - - void SetTargetFolder(const std::string& path) - { - FLAGS_logtostderr = false; - FLAGS_log_dir = path; + globalTrace_ = enabled; } } } -#else + +#else /* ORTHANC_ENABLE_LOGGING_PLUGIN == 0 && + ORTHANC_ENABLE_LOGGING_STDIO == 0 && + ORTHANC_ENABLE_LOGGING == 1 */ /********************************************************* - * Use internal logger, not Google Log + * Internal logger of Orthanc, that mimics some + * behavior from Google Log. *********************************************************/ #include "OrthancException.h" #include "Enumerations.h" #include "Toolbox.h" +#if ORTHANC_SANDBOXED == 1 +# include +#else +# include "SystemToolbox.h" +#endif + #include #include #include - -#if BOOST_HAS_DATE_TIME == 1 -# include -#else -# error Boost::date_time is required -#endif +#include namespace { - struct LoggingState + struct LoggingContext { bool infoEnabled_; bool traceEnabled_; + std::string targetFile_; + std::string targetFolder_; std::ostream* error_; std::ostream* warning_; @@ -146,7 +244,7 @@ std::auto_ptr file_; - LoggingState() : + LoggingContext() : infoEnabled_(false), traceEnabled_(false), error_(&std::cerr), @@ -159,7 +257,7 @@ -static std::auto_ptr loggingState_; +static std::auto_ptr loggingContext_; static boost::mutex loggingMutex_; @@ -186,7 +284,7 @@ boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); boost::filesystem::path root(directory); - boost::filesystem::path exe(Toolbox::GetPathToExecutable()); + boost::filesystem::path exe(SystemToolbox::GetPathToExecutable()); if (!boost::filesystem::exists(root) || !boost::filesystem::is_directory(root)) @@ -199,10 +297,10 @@ static_cast(now.date().year()), now.date().month().as_number(), now.date().day().as_number(), - now.time_of_day().hours(), - now.time_of_day().minutes(), - now.time_of_day().seconds(), - Toolbox::GetProcessId()); + static_cast(now.time_of_day().hours()), + static_cast(now.time_of_day().minutes()), + static_cast(now.time_of_day().seconds()), + SystemToolbox::GetProcessId()); std::string programName = exe.filename().replace_extension("").string(); @@ -211,9 +309,9 @@ } - static void PrepareLogFile(std::auto_ptr& file, - const std::string& suffix, - const std::string& directory) + static void PrepareLogFolder(std::auto_ptr& file, + const std::string& suffix, + const std::string& directory) { boost::filesystem::path log, link; GetLogPath(log, link, suffix, directory); @@ -230,168 +328,247 @@ void Initialize() { boost::mutex::scoped_lock lock(loggingMutex_); - loggingState_.reset(new LoggingState); + loggingContext_.reset(new LoggingContext); } void Finalize() { boost::mutex::scoped_lock lock(loggingMutex_); - loggingState_.reset(NULL); + loggingContext_.reset(NULL); + } + + void Reset() + { + // Recover the old logging context + std::auto_ptr old; + + { + boost::mutex::scoped_lock lock(loggingMutex_); + if (loggingContext_.get() == NULL) + { + return; + } + else + { + old = loggingContext_; + + // Create a new logging context, + loggingContext_.reset(new LoggingContext); + } + } + + EnableInfoLevel(old->infoEnabled_); + EnableTraceLevel(old->traceEnabled_); + + if (!old->targetFolder_.empty()) + { + SetTargetFolder(old->targetFolder_); + } + else if (!old->targetFile_.empty()) + { + SetTargetFile(old->targetFile_); + } } void EnableInfoLevel(bool enabled) { boost::mutex::scoped_lock lock(loggingMutex_); - assert(loggingState_.get() != NULL); + assert(loggingContext_.get() != NULL); - loggingState_->infoEnabled_ = enabled; + loggingContext_->infoEnabled_ = enabled; + + if (!enabled) + { + // Also disable the "TRACE" level when info-level debugging is disabled + loggingContext_->traceEnabled_ = false; + } } void EnableTraceLevel(bool enabled) { boost::mutex::scoped_lock lock(loggingMutex_); - assert(loggingState_.get() != NULL); + assert(loggingContext_.get() != NULL); - loggingState_->traceEnabled_ = enabled; + loggingContext_->traceEnabled_ = enabled; if (enabled) { // Also enable the "INFO" level when trace-level debugging is enabled - loggingState_->infoEnabled_ = true; + loggingContext_->infoEnabled_ = true; + } + } + + + static void CheckFile(std::auto_ptr& f) + { + if (loggingContext_->file_.get() == NULL || + !loggingContext_->file_->is_open()) + { + throw OrthancException(ErrorCode_CannotWriteFile); } } void SetTargetFolder(const std::string& path) { boost::mutex::scoped_lock lock(loggingMutex_); - assert(loggingState_.get() != NULL); + assert(loggingContext_.get() != NULL); + + PrepareLogFolder(loggingContext_->file_, "" /* no suffix */, path); + CheckFile(loggingContext_->file_); - PrepareLogFile(loggingState_->file_, "" /* no suffix */, path); + loggingContext_->targetFile_.clear(); + loggingContext_->targetFolder_ = path; + loggingContext_->warning_ = loggingContext_->file_.get(); + loggingContext_->error_ = loggingContext_->file_.get(); + loggingContext_->info_ = loggingContext_->file_.get(); + } + - loggingState_->warning_ = loggingState_->file_.get(); - loggingState_->error_ = loggingState_->file_.get(); - loggingState_->info_ = loggingState_->file_.get(); + void SetTargetFile(const std::string& path) + { + boost::mutex::scoped_lock lock(loggingMutex_); + assert(loggingContext_.get() != NULL); + + loggingContext_->file_.reset(new std::ofstream(path.c_str(), std::fstream::app)); + CheckFile(loggingContext_->file_); + + loggingContext_->targetFile_ = path; + loggingContext_->targetFolder_.clear(); + loggingContext_->warning_ = loggingContext_->file_.get(); + loggingContext_->error_ = loggingContext_->file_.get(); + loggingContext_->info_ = loggingContext_->file_.get(); } + InternalLogger::InternalLogger(const char* level, const char* file, int line) : lock_(loggingMutex_), stream_(&null_) // By default, logging to "/dev/null" is simulated { - if (loggingState_.get() == NULL) + if (loggingContext_.get() == NULL) { fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n"); return; } - LogLevel l = StringToLogLevel(level); - - if ((l == LogLevel_Info && !loggingState_->infoEnabled_) || - (l == LogLevel_Trace && !loggingState_->traceEnabled_)) + try { - // This logging level is disabled, directly exit and unlock - // the mutex to speed-up things. The stream is set to "/dev/null" - lock_.unlock(); - return; - } + LogLevel l = StringToLogLevel(level); + + if ((l == LogLevel_Info && !loggingContext_->infoEnabled_) || + (l == LogLevel_Trace && !loggingContext_->traceEnabled_)) + { + // This logging level is disabled, directly exit and unlock + // the mutex to speed-up things. The stream is set to "/dev/null" + lock_.unlock(); + return; + } - // Compute the header of the line, temporary release the lock as - // this is a time-consuming operation - lock_.unlock(); - std::string header; + // Compute the header of the line, temporary release the lock as + // this is a time-consuming operation + lock_.unlock(); + std::string header; - { - boost::filesystem::path path(file); - boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time(); - boost::posix_time::time_duration duration = now.time_of_day(); + { + boost::filesystem::path path(file); + boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time(); + boost::posix_time::time_duration duration = now.time_of_day(); - /** - From Google Log documentation: + /** + From Google Log documentation: - "Log lines have this form: + "Log lines have this form: - Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg... + Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg... - where the fields are defined as follows: + where the fields are defined as follows: - L A single character, representing the log level (eg 'I' for INFO) - mm The month (zero padded; ie May is '05') - dd The day (zero padded) - hh:mm:ss.uuuuuu Time in hours, minutes and fractional seconds - threadid The space-padded thread ID as returned by GetTID() (this matches the PID on Linux) - file The file name - line The line number - msg The user-supplied message" + L A single character, representing the log level (eg 'I' for INFO) + mm The month (zero padded; ie May is '05') + dd The day (zero padded) + hh:mm:ss.uuuuuu Time in hours, minutes and fractional seconds + threadid The space-padded thread ID as returned by GetTID() (this matches the PID on Linux) + file The file name + line The line number + msg The user-supplied message" - In this implementation, "threadid" is not printed. - **/ + In this implementation, "threadid" is not printed. + **/ - char date[32]; - sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ", - level[0], - now.date().month().as_number(), - now.date().day().as_number(), - duration.hours(), - duration.minutes(), - duration.seconds(), - static_cast(duration.fractional_seconds())); + char date[40]; + sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ", + level[0], + now.date().month().as_number(), + now.date().day().as_number(), + static_cast(duration.hours()), + static_cast(duration.minutes()), + static_cast(duration.seconds()), + static_cast(duration.fractional_seconds())); - header = std::string(date) + path.filename().string() + ":" + boost::lexical_cast(line) + "] "; - } + header = std::string(date) + path.filename().string() + ":" + boost::lexical_cast(line) + "] "; + } - // The header is computed, we now re-lock the mutex to access - // the stream objects. Pay attention that "loggingState_", - // "infoEnabled_" or "traceEnabled_" might have changed while - // the mutex was unlocked. - lock_.lock(); + // The header is computed, we now re-lock the mutex to access + // the stream objects. Pay attention that "loggingContext_", + // "infoEnabled_" or "traceEnabled_" might have changed while + // the mutex was unlocked. + lock_.lock(); + + if (loggingContext_.get() == NULL) + { + fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n"); + return; + } - if (loggingState_.get() == NULL) - { - fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n"); - return; - } + switch (l) + { + case LogLevel_Error: + stream_ = loggingContext_->error_; + break; - switch (l) - { - case LogLevel_Error: - stream_ = loggingState_->error_; - break; + case LogLevel_Warning: + stream_ = loggingContext_->warning_; + break; - case LogLevel_Warning: - stream_ = loggingState_->warning_; - break; + case LogLevel_Info: + if (loggingContext_->infoEnabled_) + { + stream_ = loggingContext_->info_; + } + + break; - case LogLevel_Info: - if (loggingState_->infoEnabled_) - { - stream_ = loggingState_->info_; - } + case LogLevel_Trace: + if (loggingContext_->traceEnabled_) + { + stream_ = loggingContext_->info_; + } - break; + break; - case LogLevel_Trace: - if (loggingState_->traceEnabled_) - { - stream_ = loggingState_->info_; - } + default: + throw OrthancException(ErrorCode_InternalError); + } - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } + if (stream_ == &null_) + { + // The logging is disabled for this level. The stream is the + // "null_" member of this object, so we can release the global + // mutex. + lock_.unlock(); + } - if (stream_ == &null_) - { - // The logging is disabled for this level. The stream is the - // "null_" member of this object, so we can release the global - // mutex. - lock_.unlock(); + (*stream_) << header; } - - (*stream_) << header; + catch (...) + { + // Something is going really wrong, probably running out of + // memory. Fallback to a degraded mode. + stream_ = loggingContext_->error_; + (*stream_) << "E???? ??:??:??.?????? ] "; + } } @@ -410,6 +587,16 @@ } + void Flush() + { + boost::mutex::scoped_lock lock(loggingMutex_); + + if (loggingContext_.get() != NULL && + loggingContext_->file_.get() != NULL) + { + loggingContext_->file_->flush(); + } + } } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Logging.h --- a/Core/Logging.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Logging.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -34,18 +35,54 @@ #include +#if !defined(ORTHANC_ENABLE_LOGGING) +# error The macro ORTHANC_ENABLE_LOGGING must be defined +#endif + +#if !defined(ORTHANC_ENABLE_LOGGING_PLUGIN) +# if ORTHANC_ENABLE_LOGGING == 1 +# error The macro ORTHANC_ENABLE_LOGGING_PLUGIN must be defined +# else +# define ORTHANC_ENABLE_LOGGING_PLUGIN 0 +# endif +#endif + +#if !defined(ORTHANC_ENABLE_LOGGING_STDIO) +# if ORTHANC_ENABLE_LOGGING == 1 +# error The macro ORTHANC_ENABLE_LOGGING_STDIO must be defined +# else +# define ORTHANC_ENABLE_LOGGING_STDIO 0 +# endif +#endif + +#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1 +# include +#endif + +#include + namespace Orthanc { namespace Logging { +#if ORTHANC_ENABLE_LOGGING_PLUGIN == 1 + void Initialize(OrthancPluginContext* context); +#else void Initialize(); +#endif void Finalize(); + void Reset(); + + void Flush(); + void EnableInfoLevel(bool enabled); void EnableTraceLevel(bool enabled); + void SetTargetFile(const std::string& path); + void SetTargetFolder(const std::string& path); struct NullStream : public std::ostream @@ -56,7 +93,8 @@ { } - std::ostream& operator<< (const std::string& message) + template + std::ostream& operator<< (const T& message) { return *this; } @@ -70,18 +108,62 @@ # define LOG(level) ::Orthanc::Logging::NullStream() # define VLOG(level) ::Orthanc::Logging::NullStream() -#else /* ORTHANC_ENABLE_LOGGING == 1 */ + +#elif (ORTHANC_ENABLE_LOGGING_PLUGIN == 1 || \ + ORTHANC_ENABLE_LOGGING_STDIO == 1) + +# include +# define LOG(level) ::Orthanc::Logging::InternalLogger \ + (::Orthanc::Logging::InternalLevel_ ## level, __FILE__, __LINE__) +# define VLOG(level) ::Orthanc::Logging::InternalLogger \ + (::Orthanc::Logging::InternalLevel_TRACE, __FILE__, __LINE__) -#if ORTHANC_ENABLE_GOOGLE_LOG == 1 -# include // Including this fixes a problem in glog for recent releases of MinGW -# include -#else +namespace Orthanc +{ + namespace Logging + { + enum InternalLevel + { + InternalLevel_ERROR, + InternalLevel_WARNING, + InternalLevel_INFO, + InternalLevel_TRACE + }; + + class InternalLogger : public boost::noncopyable + { + private: + InternalLevel level_; + std::string message_; + + public: + InternalLogger(InternalLevel level, + const char* file, + int line); + + ~InternalLogger(); + + template + InternalLogger& operator<< (const T& message) + { + message_ += boost::lexical_cast(message); + return *this; + } + }; + } +} + + + + +#else /* ORTHANC_ENABLE_LOGGING_PLUGIN == 0 && + ORTHANC_ENABLE_LOGGING_STDIO == 0 && + ORTHANC_ENABLE_LOGGING == 1 */ + # include # define LOG(level) ::Orthanc::Logging::InternalLogger(#level, __FILE__, __LINE__) # define VLOG(level) ::Orthanc::Logging::InternalLogger("TRACE", __FILE__, __LINE__) -#endif -#if ORTHANC_ENABLE_GOOGLE_LOG != 1 namespace Orthanc { namespace Logging @@ -100,13 +182,13 @@ ~InternalLogger(); - std::ostream& operator<< (const std::string& message) + template + std::ostream& operator<< (const T& message) { - return (*stream_) << message; + return (*stream_) << boost::lexical_cast(message); } }; } } -#endif #endif // ORTHANC_ENABLE_LOGGING diff -r 2b91363cc1d1 -r 497a637366b4 Core/Lua/LuaContext.cpp --- a/Core/Lua/LuaContext.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Lua/LuaContext.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -61,7 +62,6 @@ return true; } - LuaContext& LuaContext::GetLuaContext(lua_State *state) { const void* value = GetGlobalVariable(state, "_LuaContext"); @@ -209,14 +209,36 @@ return true; } + void LuaContext::SetHttpHeaders(lua_State *state, int top) + { + this->httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request + + if (lua_gettop(state) >= top) + { + Json::Value headers; + this->GetJson(headers, top, true); + + Json::Value::Members members = headers.getMemberNames(); + + for (Json::Value::Members::const_iterator + it = members.begin(); it != members.end(); ++it) + { + this->httpClient_.AddHeader(*it, headers[*it].asString()); + } + } + + } + + int LuaContext::CallHttpGet(lua_State *state) { LuaContext& that = GetLuaContext(state); // Check the types of the arguments int nArgs = lua_gettop(state); - if (nArgs != 1 || !lua_isstring(state, 1)) // URL + if ((nArgs < 1 || nArgs > 2) || // check args count + !lua_isstring(state, 1)) // URL is a string { LOG(ERROR) << "Lua: Bad parameters to HttpGet()"; lua_pushnil(state); @@ -227,6 +249,8 @@ const char* url = lua_tostring(state, 1); that.httpClient_.SetMethod(HttpMethod_Get); that.httpClient_.SetUrl(url); + that.httpClient_.GetBody().clear(); + that.SetHttpHeaders(state, 2); // Do the HTTP GET request if (!that.AnswerHttpQuery(state)) @@ -246,9 +270,9 @@ // Check the types of the arguments int nArgs = lua_gettop(state); - if ((nArgs != 1 && nArgs != 2) || - !lua_isstring(state, 1) || // URL - (nArgs >= 2 && !lua_isstring(state, 2))) // Body data + if ((nArgs < 1 || nArgs > 3) || // check arg count + !lua_isstring(state, 1) || // URL is a string + (nArgs >= 2 && (!lua_isstring(state, 2) && !lua_isnil(state, 2)))) // Body data is null or is a string { LOG(ERROR) << "Lua: Bad parameters to HttpPost() or HttpPut()"; lua_pushnil(state); @@ -259,10 +283,21 @@ const char* url = lua_tostring(state, 1); that.httpClient_.SetMethod(method); that.httpClient_.SetUrl(url); + that.SetHttpHeaders(state, 3); - if (nArgs >= 2) + if (nArgs >= 2 && !lua_isnil(state, 2)) { - that.httpClient_.SetBody(lua_tostring(state, 2)); + size_t bodySize = 0; + const char* bodyData = lua_tolstring(state, 2, &bodySize); + + if (bodySize == 0) + { + that.httpClient_.GetBody().clear(); + } + else + { + that.httpClient_.GetBody().assign(bodyData, bodySize); + } } else { @@ -298,7 +333,7 @@ // Check the types of the arguments int nArgs = lua_gettop(state); - if (nArgs != 1 || !lua_isstring(state, 1)) // URL + if (nArgs < 1 || nArgs > 2 || !lua_isstring(state, 1)) // URL { LOG(ERROR) << "Lua: Bad parameters to HttpDelete()"; lua_pushnil(state); @@ -309,6 +344,8 @@ const char* url = lua_tostring(state, 1); that.httpClient_.SetMethod(HttpMethod_Delete); that.httpClient_.SetUrl(url); + that.httpClient_.GetBody().clear(); + that.SetHttpHeaders(state, 2); // Do the HTTP DELETE request std::string s; @@ -497,6 +534,10 @@ // Lua can convert most types to strings by default. result = std::string(lua_tostring(lua_, top)); } + else if (lua_isboolean(lua_, top)) + { + result = lua_toboolean(lua_, top) ? true : false; + } else { LOG(WARNING) << "Unsupported Lua type when returning Json"; @@ -557,12 +598,14 @@ } +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 void LuaContext::Execute(EmbeddedResources::FileResourceId resource) { std::string command; EmbeddedResources::GetFileResource(command, resource); ExecuteInternal(NULL, command); } +#endif bool LuaContext::IsExistingFunction(const char* name) diff -r 2b91363cc1d1 -r 497a637366b4 Core/Lua/LuaContext.h --- a/Core/Lua/LuaContext.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Lua/LuaContext.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -32,6 +33,22 @@ #pragma once +#if !defined(ORTHANC_ENABLE_LUA) +# error The macro ORTHANC_ENABLE_LUA must be defined +#endif + +#if !defined(ORTHANC_HAS_EMBEDDED_RESOURCES) +# error Macro ORTHANC_HAS_EMBEDDED_RESOURCES must be defined +#endif + +#if ORTHANC_ENABLE_LUA == 0 +# error The Lua support is disabled, cannot include this file +#endif + +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 +# include // Autogenerated file +#endif + #include "../HttpClient.h" extern "C" @@ -39,7 +56,6 @@ #include } -#include #include namespace Orthanc @@ -74,6 +90,8 @@ void GetJson(Json::Value& result, int top, bool keepStrings); + + void SetHttpHeaders(lua_State* state, int top); public: LuaContext(); @@ -94,7 +112,9 @@ void Execute(Json::Value& output, const std::string& command); +#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1 void Execute(EmbeddedResources::FileResourceId resource); +#endif bool IsExistingFunction(const char* name); @@ -104,11 +124,6 @@ httpClient_.SetCredentials(username, password); } - void SetHttpProxy(const std::string& proxy) - { - httpClient_.SetProxy(proxy); - } - void RegisterFunction(const char* name, lua_CFunction func); diff -r 2b91363cc1d1 -r 497a637366b4 Core/Lua/LuaFunctionCall.cpp --- a/Core/Lua/LuaFunctionCall.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Lua/LuaFunctionCall.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -152,4 +153,40 @@ throw OrthancException(ErrorCode_LuaReturnsNoString); } } + + + void LuaFunctionCall::PushStringMap(const std::map& value) + { + Json::Value json = Json::objectValue; + + for (std::map::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); + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/Lua/LuaFunctionCall.h --- a/Core/Lua/LuaFunctionCall.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Lua/LuaFunctionCall.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -34,6 +35,9 @@ #include "LuaContext.h" +#include "../DicomFormat/DicomArray.h" +#include "../DicomFormat/DicomMap.h" + #include namespace Orthanc @@ -46,8 +50,14 @@ void CheckAlreadyExecuted(); + protected: void ExecuteInternal(int numOutputs); + lua_State* GetState() + { + return context_.lua_; + } + public: LuaFunctionCall(LuaContext& context, const char* functionName); @@ -62,6 +72,12 @@ void PushJson(const Json::Value& value); + void PushStringMap(const std::map& value); + + void PushDicom(const DicomMap& dicom); + + void PushDicom(const DicomArray& dicom); + void Execute() { ExecuteInternal(0); diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/ILockable.h --- a/Core/MultiThreading/ILockable.h Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include - -namespace Orthanc -{ - class ILockable : public boost::noncopyable - { - friend class Locker; - - protected: - virtual void Lock() = 0; - - virtual void Unlock() = 0; - - public: - virtual ~ILockable() - { - } - }; -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/IRunnableBySteps.h --- a/Core/MultiThreading/IRunnableBySteps.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/MultiThreading/IRunnableBySteps.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/Locker.h --- a/Core/MultiThreading/Locker.h Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#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(); - } - }; -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/Mutex.cpp --- a/Core/MultiThreading/Mutex.cpp Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,121 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeaders.h" -#include "Mutex.h" - -#include "../OrthancException.h" - -#if defined(_WIN32) -#include -#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) -#include -#else -#error Support your platform here -#endif - -namespace Orthanc -{ -#if defined (_WIN32) - - struct Mutex::PImpl - { - CRITICAL_SECTION criticalSection_; - }; - - Mutex::Mutex() - { - pimpl_ = new PImpl; - ::InitializeCriticalSection(&pimpl_->criticalSection_); - } - - Mutex::~Mutex() - { - ::DeleteCriticalSection(&pimpl_->criticalSection_); - delete pimpl_; - } - - void Mutex::Lock() - { - ::EnterCriticalSection(&pimpl_->criticalSection_); - } - - void Mutex::Unlock() - { - ::LeaveCriticalSection(&pimpl_->criticalSection_); - } - - -#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) - - struct Mutex::PImpl - { - pthread_mutex_t mutex_; - }; - - Mutex::Mutex() - { - pimpl_ = new PImpl; - - if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0) - { - delete pimpl_; - throw OrthancException(ErrorCode_InternalError); - } - } - - Mutex::~Mutex() - { - pthread_mutex_destroy(&pimpl_->mutex_); - delete pimpl_; - } - - void Mutex::Lock() - { - if (pthread_mutex_lock(&pimpl_->mutex_) != 0) - { - throw OrthancException(ErrorCode_InternalError); - } - } - - void Mutex::Unlock() - { - if (pthread_mutex_unlock(&pimpl_->mutex_) != 0) - { - throw OrthancException(ErrorCode_InternalError); - } - } - -#else -#error Support your plateform here -#endif -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/Mutex.h --- a/Core/MultiThreading/Mutex.h Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#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(); - }; -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/ReaderWriterLock.cpp --- a/Core/MultiThreading/ReaderWriterLock.cpp Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,126 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeaders.h" -#include "ReaderWriterLock.h" - -#include - -namespace Orthanc -{ - namespace - { - // Anonymous namespace to avoid clashes between compilation - // modules. - - class ReaderLockable : public ILockable - { - private: - boost::shared_mutex& lock_; - - protected: - virtual void Lock() - { - lock_.lock_shared(); - } - - virtual void Unlock() - { - lock_.unlock_shared(); - } - - public: - ReaderLockable(boost::shared_mutex& lock) : lock_(lock) - { - } - }; - - - class WriterLockable : public ILockable - { - private: - boost::shared_mutex& lock_; - - protected: - virtual void Lock() - { - lock_.lock(); - } - - virtual void Unlock() - { - lock_.unlock(); - } - - public: - WriterLockable(boost::shared_mutex& lock) : lock_(lock) - { - } - - }; - } - - struct ReaderWriterLock::PImpl - { - boost::shared_mutex lock_; - ReaderLockable reader_; - WriterLockable writer_; - - PImpl() : reader_(lock_), writer_(lock_) - { - } - }; - - - ReaderWriterLock::ReaderWriterLock() - { - pimpl_ = new PImpl; - } - - - ReaderWriterLock::~ReaderWriterLock() - { - delete pimpl_; - } - - - ILockable& ReaderWriterLock::ForReader() - { - return pimpl_->reader_; - } - - - ILockable& ReaderWriterLock::ForWriter() - { - return pimpl_->writer_; - } -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/ReaderWriterLock.h --- a/Core/MultiThreading/ReaderWriterLock.h Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ILockable.h" - -#include - -namespace Orthanc -{ - class ReaderWriterLock : public boost::noncopyable - { - private: - struct PImpl; - - PImpl *pimpl_; - - public: - ReaderWriterLock(); - - virtual ~ReaderWriterLock(); - - ILockable& ForReader(); - - ILockable& ForWriter(); - }; -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/RunnableWorkersPool.cpp --- a/Core/MultiThreading/RunnableWorkersPool.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/MultiThreading/RunnableWorkersPool.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -52,25 +53,37 @@ { while (that->continue_) { - std::auto_ptr obj(that->queue_.Dequeue(100)); - if (obj.get() != NULL) + try { - try + std::auto_ptr obj(that->queue_.Dequeue(100)); + if (obj.get() != NULL) { IRunnableBySteps& runnable = *dynamic_cast(obj.get()); - + bool wishToContinue = runnable.Step(); - + if (wishToContinue) { // The runnable wishes to continue, reinsert it at the beginning of the queue that->queue_.Enqueue(obj.release()); } } - catch (OrthancException& e) - { - LOG(ERROR) << "Exception in a pool of working threads: " << e.What(); - } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Exception while handling some runnable object: " << e.What(); + } + catch (std::bad_alloc&) + { + LOG(ERROR) << "Not enough memory to handle some runnable object"; + } + catch (std::exception& e) + { + LOG(ERROR) << "std::exception while handling some runnable object: " << e.what(); + } + catch (...) + { + LOG(ERROR) << "Native exception while handling some runnable object"; } } } @@ -105,7 +118,7 @@ { pimpl_->continue_ = true; - if (countWorkers <= 0) + if (countWorkers == 0) { throw OrthancException(ErrorCode_ParameterOutOfRange); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/RunnableWorkersPool.h --- a/Core/MultiThreading/RunnableWorkersPool.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/MultiThreading/RunnableWorkersPool.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -47,7 +48,7 @@ void Stop(); public: - RunnableWorkersPool(size_t countWorkers); + explicit RunnableWorkersPool(size_t countWorkers); ~RunnableWorkersPool(); diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/Semaphore.cpp --- a/Core/MultiThreading/Semaphore.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/MultiThreading/Semaphore.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -38,9 +39,10 @@ namespace Orthanc { - Semaphore::Semaphore(unsigned int count) : count_(count) + Semaphore::Semaphore(unsigned int count) : + count_(count) { - if (count == 0) + if (count_ == 0) { throw OrthancException(ErrorCode_ParameterOutOfRange); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/Semaphore.h --- a/Core/MultiThreading/Semaphore.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/MultiThreading/Semaphore.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -43,12 +44,30 @@ unsigned int count_; boost::mutex mutex_; boost::condition_variable condition_; + + void Release(); + + void Acquire(); public: explicit Semaphore(unsigned int count); - void Release(); + class Locker : public boost::noncopyable + { + private: + Semaphore& that_; - void Acquire(); + public: + explicit Locker(Semaphore& that) : + that_(that) + { + that_.Acquire(); + } + + ~Locker() + { + that_.Release(); + } + }; }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/SharedMessageQueue.cpp --- a/Core/MultiThreading/SharedMessageQueue.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/MultiThreading/SharedMessageQueue.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -185,4 +186,24 @@ boost::mutex::scoped_lock lock(mutex_); isFifo_ = false; } + + void SharedMessageQueue::Clear() + { + boost::mutex::scoped_lock lock(mutex_); + + if (queue_.empty()) + { + return; + } + else + { + while (!queue_.empty()) + { + std::auto_ptr message(queue_.front()); + queue_.pop_front(); + } + + emptied_.notify_all(); + } + } } diff -r 2b91363cc1d1 -r 497a637366b4 Core/MultiThreading/SharedMessageQueue.h --- a/Core/MultiThreading/SharedMessageQueue.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/MultiThreading/SharedMessageQueue.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -78,5 +79,7 @@ void SetFifoPolicy(); void SetLifoPolicy(); + + void Clear(); }; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/OrthancException.h --- a/Core/OrthancException.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/OrthancException.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -45,7 +46,7 @@ HttpStatus httpStatus_; public: - OrthancException(ErrorCode errorCode) : + explicit OrthancException(ErrorCode errorCode) : errorCode_(errorCode), httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)) { diff -r 2b91363cc1d1 -r 497a637366b4 Core/Pkcs11.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Pkcs11.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,309 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 // This is P11's "engine.h" +# include +} + +#include + + +namespace Orthanc +{ + namespace Pkcs11 + { + static const char* PKCS11_ENGINE_ID = "pkcs11"; + static const char* PKCS11_ENGINE_NAME = "PKCS#11 for Orthanc"; + static const ENGINE_CMD_DEFN PKCS11_ENGINE_COMMANDS[] = + { + { + CMD_MODULE_PATH, + "MODULE_PATH", + "Specifies the path to the PKCS#11 module shared library", + ENGINE_CMD_FLAG_STRING + }, + { + CMD_PIN, + "PIN", + "Specifies the pin code", + ENGINE_CMD_FLAG_STRING + }, + { + CMD_VERBOSE, + "VERBOSE", + "Print additional details", + ENGINE_CMD_FLAG_NO_INPUT + }, + { + CMD_LOAD_CERT_CTRL, + "LOAD_CERT_CTRL", + "Get the certificate from card", + ENGINE_CMD_FLAG_INTERNAL + }, + { + 0, + NULL, + NULL, + 0 + } + }; + + + static bool pkcs11Initialized_ = false; + static ENGINE_CTX *context_ = NULL; + + static int EngineInitialize(ENGINE* engine) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_init(context_); + } + } + + + static int EngineFinalize(ENGINE* engine) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_finish(context_); + } + } + + + static int EngineDestroy(ENGINE* engine) + { + return (context_ == NULL ? 0 : 1); + } + + + static int EngineControl(ENGINE *engine, + int command, + long i, + void *p, + void (*f) ()) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_engine_ctrl(context_, command, i, p, f); + } + } + + + static EVP_PKEY *EngineLoadPublicKey(ENGINE *engine, + const char *s_key_id, + UI_METHOD *ui_method, + void *callback_data) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_load_public_key(context_, s_key_id, ui_method, callback_data); + } + } + + + static EVP_PKEY *EngineLoadPrivateKey(ENGINE *engine, + const char *s_key_id, + UI_METHOD *ui_method, + void *callback_data) + { + if (context_ == NULL) + { + return 0; + } + else + { + return pkcs11_load_private_key(context_, s_key_id, ui_method, callback_data); + } + } + + + static ENGINE* LoadEngine() + { + // This function creates an engine for PKCS#11 and inspired by + // the "ENGINE_load_dynamic" function from OpenSSL, in file + // "crypto/engine/eng_dyn.c" + + ENGINE* engine = ENGINE_new(); + if (!engine) + { + LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11"; + throw OrthancException(ErrorCode_InternalError); + } + + // Create a PKCS#11 context using libp11 + context_ = pkcs11_new(); + if (!context_) + { + LOG(ERROR) << "Cannot create a libp11 context for PKCS#11"; + ENGINE_free(engine); + throw OrthancException(ErrorCode_InternalError); + } + + if (!ENGINE_set_id(engine, PKCS11_ENGINE_ID) || + !ENGINE_set_name(engine, PKCS11_ENGINE_NAME) || + !ENGINE_set_cmd_defns(engine, PKCS11_ENGINE_COMMANDS) || + + // Register the callback functions + !ENGINE_set_init_function(engine, EngineInitialize) || + !ENGINE_set_finish_function(engine, EngineFinalize) || + !ENGINE_set_destroy_function(engine, EngineDestroy) || + !ENGINE_set_ctrl_function(engine, EngineControl) || + !ENGINE_set_load_pubkey_function(engine, EngineLoadPublicKey) || + !ENGINE_set_load_privkey_function(engine, EngineLoadPrivateKey) || + + !ENGINE_set_RSA(engine, PKCS11_get_rsa_method()) || + !ENGINE_set_ECDSA(engine, PKCS11_get_ecdsa_method()) || + !ENGINE_set_ECDH(engine, PKCS11_get_ecdh_method()) || + +#if OPENSSL_VERSION_NUMBER >= 0x10100002L + !ENGINE_set_EC(engine, PKCS11_get_ec_key_method()) || +#endif + + // Make OpenSSL know about our PKCS#11 engine + !ENGINE_add(engine)) + { + LOG(ERROR) << "Cannot initialize the OpenSSL engine for PKCS#11"; + pkcs11_finish(context_); + ENGINE_free(engine); + throw OrthancException(ErrorCode_InternalError); + } + + // If the "ENGINE_add" worked, it gets a structural + // reference. We release our just-created reference. + ENGINE_free(engine); + + return ENGINE_by_id(PKCS11_ENGINE_ID); + } + + + bool IsInitialized() + { + return pkcs11Initialized_; + } + + const char* GetEngineIdentifier() + { + return PKCS11_ENGINE_ID; + } + + void Initialize(const std::string& module, + const std::string& pin, + bool verbose) + { + if (pkcs11Initialized_) + { + LOG(ERROR) << "The PKCS#11 engine has already been initialized"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (module.empty() || + !SystemToolbox::IsRegularFile(module)) + { + LOG(ERROR) << "The PKCS#11 module must be a path to one shared library (DLL or .so)"; + throw OrthancException(ErrorCode_InexistentFile); + } + + ENGINE* engine = LoadEngine(); + if (!engine) + { + LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11"; + throw OrthancException(ErrorCode_InternalError); + } + + if (!ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", module.c_str(), 0)) + { + LOG(ERROR) << "Cannot configure the OpenSSL dynamic engine for PKCS#11"; + throw OrthancException(ErrorCode_InternalError); + } + + if (verbose) + { + ENGINE_ctrl_cmd_string(engine, "VERBOSE", NULL, 0); + } + + if (!pin.empty() && + !ENGINE_ctrl_cmd_string(engine, "PIN", pin.c_str(), 0)) + { + LOG(ERROR) << "Cannot set the PIN code for PKCS#11"; + throw OrthancException(ErrorCode_InternalError); + } + + if (!ENGINE_init(engine)) + { + LOG(ERROR) << "Cannot initialize the OpenSSL dynamic engine for PKCS#11"; + throw OrthancException(ErrorCode_InternalError); + } + + LOG(WARNING) << "The PKCS#11 engine has been successfully initialized"; + pkcs11Initialized_ = true; + } + + + void Finalize() + { + // Nothing to do, the unregistration of the engine is + // automatically done by OpenSSL + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Pkcs11.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/Pkcs11.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,73 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 + +namespace Orthanc +{ + namespace Pkcs11 + { + const char* GetEngineIdentifier(); + + bool IsInitialized(); + + void Initialize(const std::string& module, + const std::string& pin, + bool verbose); + + void Finalize(); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/PrecompiledHeaders.cpp --- a/Core/PrecompiledHeaders.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/PrecompiledHeaders.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/PrecompiledHeaders.h --- a/Core/PrecompiledHeaders.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/PrecompiledHeaders.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -48,14 +49,28 @@ #include -#if ORTHANC_PUGIXML_ENABLED == 1 -#include +#if ORTHANC_ENABLE_PUGIXML == 1 +# include #endif #include "Enumerations.h" #include "Logging.h" #include "OrthancException.h" #include "Toolbox.h" -#include "Uuid.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +// Headers from DCMTK used in Orthanc headers +# include +# include +# include +# include +#endif + +#if ORTHANC_ENABLE_DCMTK_NETWORKING == 1 +# include "DicomNetworking/DicomServer.h" + +// Headers from DCMTK used in Orthanc headers +# include +#endif #endif diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApi.cpp --- a/Core/RestApi/RestApi.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApi.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -185,7 +186,7 @@ { RestApiOutput wrappedOutput(output, method); -#if ORTHANC_PUGIXML_ENABLED == 1 +#if ORTHANC_ENABLE_PUGIXML == 1 { // Look if the client wishes XML answers instead of JSON // http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html#z3 diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApi.h --- a/Core/RestApi/RestApi.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApi.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiCall.cpp --- a/Core/RestApi/RestApiCall.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiCall.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiCall.h --- a/Core/RestApi/RestApiCall.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiCall.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiDeleteCall.h --- a/Core/RestApi/RestApiDeleteCall.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiDeleteCall.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiGetCall.cpp --- a/Core/RestApi/RestApiGetCall.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiGetCall.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiGetCall.h --- a/Core/RestApi/RestApiGetCall.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiGetCall.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiHierarchy.cpp --- a/Core/RestApi/RestApiHierarchy.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiHierarchy.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiHierarchy.h --- a/Core/RestApi/RestApiHierarchy.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiHierarchy.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiOutput.cpp --- a/Core/RestApi/RestApiOutput.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiOutput.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -35,6 +36,7 @@ #include "../Logging.h" #include "../OrthancException.h" +#include "../Toolbox.h" #include @@ -91,10 +93,10 @@ if (convertJsonToXml_) { -#if ORTHANC_PUGIXML_ENABLED == 1 +#if ORTHANC_ENABLE_PUGIXML == 1 std::string s; Toolbox::JsonToXml(s, value); - output_.SetContentType("application/xml"); + output_.SetContentType("application/xml; charset=utf-8"); output_.Answer(s); #else LOG(ERROR) << "Orthanc was compiled without XML support"; @@ -104,7 +106,7 @@ else { Json::StyledWriter writer; - output_.SetContentType("application/json"); + output_.SetContentType("application/json; charset=utf-8"); output_.Answer(writer.write(value)); } diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiOutput.h --- a/Core/RestApi/RestApiOutput.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiOutput.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiPath.cpp --- a/Core/RestApi/RestApiPath.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiPath.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiPath.h --- a/Core/RestApi/RestApiPath.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiPath.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiPostCall.h --- a/Core/RestApi/RestApiPostCall.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiPostCall.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/RestApi/RestApiPutCall.h --- a/Core/RestApi/RestApiPutCall.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/RestApi/RestApiPutCall.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/Connection.cpp --- a/Core/SQLite/Connection.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/Connection.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -163,7 +163,8 @@ if (error == SQLITE_ERROR) { #if ORTHANC_SQLITE_STANDALONE != 1 - LOG(ERROR) << "SQLite execute error: " << sqlite3_errmsg(db_); + LOG(ERROR) << "SQLite execute error: " << sqlite3_errmsg(db_) + << " (" << sqlite3_extended_errcode(db_) << ")"; #endif throw OrthancSQLiteException(ErrorCode_SQLiteExecute); diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/Connection.h --- a/Core/SQLite/Connection.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/Connection.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -39,13 +39,11 @@ #include "Statement.h" #include "IScalarFunction.h" +#include "SQLiteTypes.h" #include #include -struct sqlite3; -struct sqlite3_stmt; - #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__) namespace Orthanc diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/FunctionContext.cpp --- a/Core/SQLite/FunctionContext.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/FunctionContext.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Redistribution and use in source and binary forms, with or without @@ -49,7 +49,7 @@ { FunctionContext::FunctionContext(struct sqlite3_context* context, int argc, - struct ::Mem** argv) + Internals::SQLiteValue** argv) { assert(context != NULL); assert(argc >= 0); diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/FunctionContext.h --- a/Core/SQLite/FunctionContext.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/FunctionContext.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Redistribution and use in source and binary forms, with or without @@ -36,9 +36,6 @@ #include "Statement.h" -struct sqlite3_context; -struct Mem; // This corresponds to the opaque type "sqlite3_value" - namespace Orthanc { namespace SQLite @@ -50,14 +47,14 @@ private: struct sqlite3_context* context_; unsigned int argc_; - struct ::Mem** argv_; + Internals::SQLiteValue** argv_; void CheckIndex(unsigned int index) const; public: FunctionContext(struct sqlite3_context* context, int argc, - struct ::Mem** argv); + Internals::SQLiteValue** argv); ColumnType GetColumnType(unsigned int index) const; diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/IScalarFunction.h --- a/Core/SQLite/IScalarFunction.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/IScalarFunction.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Redistribution and use in source and binary forms, with or without diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/ITransaction.h --- a/Core/SQLite/ITransaction.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/ITransaction.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/NonCopyable.h --- a/Core/SQLite/NonCopyable.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/NonCopyable.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Redistribution and use in source and binary forms, with or without diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/OrthancSQLiteException.h --- a/Core/SQLite/OrthancSQLiteException.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/OrthancSQLiteException.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -38,6 +38,11 @@ #pragma once +#if ORTHANC_ENABLE_SQLITE != 1 +# error Macro ORTHANC_ENABLE_SQLITE must be set to 1 to use SQLite +#endif + + #if ORTHANC_SQLITE_STANDALONE == 1 #include diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/SQLiteTypes.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SQLite/SQLiteTypes.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,73 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * + * Copyright (C) 2012-2016 Sebastien Jodogne , + * 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 + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/Statement.cpp --- a/Core/SQLite/Statement.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/Statement.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -46,8 +46,21 @@ #include #include -#if ORTHANC_SQLITE_STANDALONE != 1 -#include "../Logging.h" +#if (ORTHANC_SQLITE_STANDALONE == 1) +// Trace logging is disabled if this SQLite wrapper is used +// independently of Orthanc +# define LOG_CREATE(message); +# define LOG_APPLY(message); +#elif defined(NDEBUG) +// Trace logging is disabled in release builds +# include "../Logging.h" +# define LOG_CREATE(message); +# define LOG_APPLY(message); +#else +// Trace logging is enabled in debug builds +# include "../Logging.h" +# define LOG_CREATE(message) VLOG(1) << "SQLite::Statement create: " << message; +# define LOG_APPLY(message); // VLOG(1) << "SQLite::Statement apply: " << message; #endif #include "sqlite3.h" @@ -56,6 +69,7 @@ #define snprintf _snprintf #endif + namespace Orthanc { namespace SQLite @@ -103,6 +117,7 @@ reference_(database.GetCachedStatement(id, sql.c_str())) { Reset(true); + LOG_CREATE(sql); } @@ -112,6 +127,7 @@ reference_(database.GetCachedStatement(id, sql)) { Reset(true); + LOG_CREATE(sql); } @@ -119,6 +135,7 @@ const std::string& sql) : reference_(database.GetWrappedObject(), sql.c_str()) { + LOG_CREATE(sql); } @@ -126,23 +143,20 @@ const char* sql) : reference_(database.GetWrappedObject(), sql) { + LOG_CREATE(sql); } bool Statement::Run() { -#if ORTHANC_SQLITE_STANDALONE != 1 - VLOG(1) << "SQLite::Statement::Run " << sqlite3_sql(GetStatement()); -#endif + LOG_APPLY(sqlite3_sql(GetStatement())); return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotRun) == SQLITE_DONE; } bool Statement::Step() { -#if ORTHANC_SQLITE_STANDALONE != 1 - VLOG(1) << "SQLite::Statement::Step " << sqlite3_sql(GetStatement()); -#endif + LOG_APPLY(sqlite3_sql(GetStatement())); return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotStep) == SQLITE_ROW; } diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/Statement.h --- a/Core/SQLite/Statement.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/Statement.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -46,11 +46,9 @@ #include #if ORTHANC_BUILD_UNIT_TESTS == 1 -#include +# include #endif -struct sqlite3_stmt; - namespace Orthanc { diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/StatementId.cpp --- a/Core/SQLite/StatementId.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/StatementId.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/StatementId.h --- a/Core/SQLite/StatementId.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/StatementId.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/StatementReference.cpp --- a/Core/SQLite/StatementReference.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/StatementReference.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -82,7 +82,8 @@ if (error != SQLITE_OK) { #if ORTHANC_SQLITE_STANDALONE != 1 - LOG(ERROR) << "SQLite: " << sqlite3_errmsg(database); + LOG(ERROR) << "SQLite: " << sqlite3_errmsg(database) + << " (" << sqlite3_extended_errcode(database) << ")"; #endif throw OrthancSQLiteException(ErrorCode_SQLitePrepareStatement); diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/StatementReference.h --- a/Core/SQLite/StatementReference.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/StatementReference.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -38,13 +38,12 @@ #pragma once #include "NonCopyable.h" +#include "SQLiteTypes.h" #include #include #include -struct sqlite3; -struct sqlite3_stmt; namespace Orthanc { diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/Transaction.cpp --- a/Core/SQLite/Transaction.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/Transaction.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. diff -r 2b91363cc1d1 -r 497a637366b4 Core/SQLite/Transaction.h --- a/Core/SQLite/Transaction.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/SQLite/Transaction.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,7 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store * - * Copyright (C) 2012-2015 Sebastien Jodogne , + * Copyright (C) 2012-2016 Sebastien Jodogne , * Medical Physics Department, CHU of Liege, Belgium * * Copyright (c) 2012 The Chromium Authors. All rights reserved. diff -r 2b91363cc1d1 -r 497a637366b4 Core/SerializationToolbox.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SerializationToolbox.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,375 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeaders.h" +#include "SerializationToolbox.h" + +#include "OrthancException.h" + +namespace Orthanc +{ + namespace SerializationToolbox + { + std::string ReadString(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return value[field.c_str()].asString(); + } + } + + + int ReadInteger(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + (value[field.c_str()].type() != Json::intValue && + value[field.c_str()].type() != Json::uintValue)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return value[field.c_str()].asInt(); + } + } + + + unsigned int ReadUnsignedInteger(const Json::Value& value, + const std::string& field) + { + int tmp = ReadInteger(value, field); + + if (tmp < 0) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return static_cast(tmp); + } + } + + + bool ReadBoolean(const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::booleanValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return value[field.c_str()].asBool(); + } + } + + + void ReadArrayOfStrings(std::vector& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& arr = value[field.c_str()]; + + target.resize(arr.size()); + + for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++) + { + if (arr[i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + target[i] = arr[i].asString(); + } + } + } + + + void ReadListOfStrings(std::list& target, + const Json::Value& value, + const std::string& field) + { + std::vector tmp; + ReadArrayOfStrings(tmp, value, field); + + target.clear(); + for (size_t i = 0; i < tmp.size(); i++) + { + target.push_back(tmp[i]); + } + } + + + void ReadSetOfStrings(std::set& target, + const Json::Value& value, + const std::string& field) + { + std::vector tmp; + ReadArrayOfStrings(tmp, value, field); + + target.clear(); + for (size_t i = 0; i < tmp.size(); i++) + { + target.insert(tmp[i]); + } + } + + + void ReadSetOfTags(std::set& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& arr = value[field.c_str()]; + + target.clear(); + + for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++) + { + DicomTag tag(0, 0); + + if (arr[i].type() != Json::stringValue || + !DicomTag::ParseHexadecimal(tag, arr[i].asCString())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + target.insert(tag); + } + } + } + + + void ReadMapOfStrings(std::map& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& source = value[field.c_str()]; + + target.clear(); + + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& tmp = source[members[i]]; + + if (tmp.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + target[members[i]] = tmp.asString(); + } + } + } + + + void ReadMapOfTags(std::map& target, + const Json::Value& value, + const std::string& field) + { + if (value.type() != Json::objectValue || + !value.isMember(field.c_str()) || + value[field.c_str()].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const Json::Value& source = value[field.c_str()]; + + target.clear(); + + Json::Value::Members members = source.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& tmp = source[members[i]]; + + DicomTag tag(0, 0); + + if (!DicomTag::ParseHexadecimal(tag, members[i].c_str()) || + tmp.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + target[tag] = tmp.asString(); + } + } + } + + + void WriteArrayOfStrings(Json::Value& target, + const std::vector& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + for (size_t i = 0; i < values.size(); i++) + { + value.append(values[i]); + } + } + + + void WriteSetOfStrings(Json::Value& target, + const std::set& 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::const_iterator it = values.begin(); + it != values.end(); ++it) + { + value.append(*it); + } + } + + + void WriteSetOfTags(Json::Value& target, + const std::set& 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::const_iterator it = tags.begin(); + it != tags.end(); ++it) + { + value.append(it->Format()); + } + } + + + void WriteMapOfStrings(Json::Value& target, + const std::map& 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::const_iterator + it = values.begin(); it != values.end(); ++it) + { + value[it->first] = it->second; + } + } + + + void WriteMapOfTags(Json::Value& target, + const std::map& 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::const_iterator + it = values.begin(); it != values.end(); ++it) + { + value[it->first.Format()] = it->second; + } + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/SerializationToolbox.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SerializationToolbox.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,102 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "DicomFormat/DicomTag.h" + +#include +#include +#include + +namespace Orthanc +{ + namespace SerializationToolbox + { + std::string ReadString(const Json::Value& value, + const std::string& field); + + int ReadInteger(const Json::Value& value, + const std::string& field); + + unsigned int ReadUnsignedInteger(const Json::Value& value, + const std::string& field); + + bool ReadBoolean(const Json::Value& value, + const std::string& field); + + void ReadArrayOfStrings(std::vector& target, + const Json::Value& value, + const std::string& field); + + void ReadListOfStrings(std::list& target, + const Json::Value& value, + const std::string& field); + + void ReadSetOfStrings(std::set& target, + const Json::Value& value, + const std::string& field); + + void ReadSetOfTags(std::set& target, + const Json::Value& value, + const std::string& field); + + void ReadMapOfStrings(std::map& values, + const Json::Value& target, + const std::string& field); + + void ReadMapOfTags(std::map& values, + const Json::Value& target, + const std::string& field); + + void WriteArrayOfStrings(Json::Value& target, + const std::vector& values, + const std::string& field); + + void WriteSetOfStrings(Json::Value& target, + const std::set& values, + const std::string& field); + + void WriteSetOfTags(Json::Value& target, + const std::set& tags, + const std::string& field); + + void WriteMapOfStrings(Json::Value& target, + const std::map& values, + const std::string& field); + + void WriteMapOfTags(Json::Value& target, + const std::map& values, + const std::string& field); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/SharedLibrary.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SharedLibrary.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,136 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeaders.h" +#include "SharedLibrary.h" + +#include "Logging.h" +#include "OrthancException.h" + +#include + +#if defined(_WIN32) +#include +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#include +#else +#error Support your platform here +#endif + +namespace Orthanc +{ + SharedLibrary::SharedLibrary(const std::string& path) : + path_(path), + handle_(NULL) + { +#if defined(_WIN32) + handle_ = ::LoadLibraryA(path_.c_str()); + if (handle_ == NULL) + { + LOG(ERROR) << "LoadLibrary(" << path_ << ") failed: Error " << ::GetLastError(); + throw OrthancException(ErrorCode_SharedLibrary); + } + +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) + handle_ = ::dlopen(path_.c_str(), RTLD_NOW); + if (handle_ == NULL) + { + std::string explanation; + const char *tmp = ::dlerror(); + if (tmp) + { + explanation = ": Error " + std::string(tmp); + } + + LOG(ERROR) << "dlopen(" << path_ << ") failed" << explanation; + throw OrthancException(ErrorCode_SharedLibrary); + } + +#else +#error Support your platform here +#endif + } + + SharedLibrary::~SharedLibrary() + { + if (handle_) + { +#if defined(_WIN32) + ::FreeLibrary((HMODULE)handle_); +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) + ::dlclose(handle_); +#else +#error Support your platform here +#endif + } + } + + + SharedLibrary::FunctionPointer SharedLibrary::GetFunctionInternal(const std::string& name) + { + if (!handle_) + { + throw OrthancException(ErrorCode_InternalError); + } + +#if defined(_WIN32) + return ::GetProcAddress((HMODULE)handle_, name.c_str()); +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) + return ::dlsym(handle_, name.c_str()); +#else +#error Support your platform here +#endif + } + + + SharedLibrary::FunctionPointer SharedLibrary::GetFunction(const std::string& name) + { + SharedLibrary::FunctionPointer result = GetFunctionInternal(name); + + if (result == NULL) + { + LOG(ERROR) << "Shared library does not expose function \"" << name << "\""; + throw OrthancException(ErrorCode_SharedLibrary); + } + else + { + return result; + } + } + + + bool SharedLibrary::HasFunction(const std::string& name) + { + return GetFunctionInternal(name) != NULL; + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/SharedLibrary.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SharedLibrary.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The namespace SystemToolbox cannot be used in sandboxed environments +#endif + +#if defined(_WIN32) +#include +#endif + +#include +#include + +namespace Orthanc +{ + class SharedLibrary : public boost::noncopyable + { + public: +#if defined(_WIN32) + typedef FARPROC FunctionPointer; +#else + typedef void* FunctionPointer; +#endif + + private: + std::string path_; + void *handle_; + + FunctionPointer GetFunctionInternal(const std::string& name); + + public: + explicit SharedLibrary(const std::string& path); + + ~SharedLibrary(); + + const std::string& GetPath() const + { + return path_; + } + + bool HasFunction(const std::string& name); + + FunctionPointer GetFunction(const std::string& name); + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/SystemToolbox.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SystemToolbox.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,581 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeaders.h" +#include "SystemToolbox.h" + + +#if defined(_WIN32) +# include +# include // For "_spawnvp()" and "_getpid()" +#else +# include // For "execvp()" +# include // For "waitpid()" +#endif + + +#if defined(__APPLE__) && defined(__MACH__) +# include /* _NSGetExecutablePath */ +# include /* PATH_MAX */ +#endif + + +#if defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) +# include /* PATH_MAX */ +# include +# include +#endif + + +#if defined(__OpenBSD__) +# include // For "sysctl", "CTL_KERN" and "KERN_PROC_ARGS" +#endif + + +#include "Logging.h" +#include "OrthancException.h" +#include "Toolbox.h" + +#include +#include +#include +#include + + +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(microSeconds / static_cast(1000))); +#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__native_client__) + usleep(microSeconds); +#else +#error Support your platform here +#endif + } + + + static std::streamsize GetStreamSize(std::istream& f) + { + // http://www.cplusplus.com/reference/iostream/istream/tellg/ + f.seekg(0, std::ios::end); + std::streamsize size = f.tellg(); + f.seekg(0, std::ios::beg); + + return size; + } + + + void SystemToolbox::ReadFile(std::string& content, + const std::string& path) + { + if (!IsRegularFile(path)) + { + LOG(ERROR) << "The path does not point to a regular file: " << path; + throw OrthancException(ErrorCode_RegularFileExpected); + } + + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + + std::streamsize size = GetStreamSize(f); + content.resize(static_cast(size)); + if (size != 0) + { + f.read(reinterpret_cast(&content[0]), size); + } + + f.close(); + } + + + bool SystemToolbox::ReadHeader(std::string& header, + const std::string& path, + size_t headerSize) + { + if (!IsRegularFile(path)) + { + LOG(ERROR) << "The path does not point to a regular file: " << path; + throw OrthancException(ErrorCode_RegularFileExpected); + } + + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + + bool full = true; + + { + std::streamsize size = GetStreamSize(f); + if (size <= 0) + { + headerSize = 0; + full = false; + } + else if (static_cast(size) < headerSize) + { + headerSize = static_cast(size); // Truncate to the size of the file + full = false; + } + } + + header.resize(headerSize); + if (headerSize != 0) + { + f.read(reinterpret_cast(&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(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(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 buffer(32768); + /*int bytes =*/ GetModuleFileNameA(NULL, &buffer[0], static_cast(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 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(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(&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& arguments) + { + // Convert the arguments as a C array + std::vector args(arguments.size() + 2); + + args.front() = const_cast(command.c_str()); + + for (size_t i = 0; i < arguments.size(); i++) + { + args[i + 1] = const_cast(arguments[i].c_str()); + } + + args.back() = NULL; + + int status; + +#if defined(_WIN32) + // http://msdn.microsoft.com/en-us/library/275khfab.aspx + status = static_cast(_spawnvp(_P_OVERLAY, command.c_str(), &args[0])); + +#else + int pid = fork(); + + if (pid == -1) + { + // Error in fork() +#if ORTHANC_ENABLE_LOGGING == 1 + LOG(ERROR) << "Cannot fork a child process"; +#endif + + throw OrthancException(ErrorCode_SystemCommand); + } + else if (pid == 0) + { + // Execute the system command in the child process + execvp(command.c_str(), &args[0]); + + // We should never get here + _exit(1); + } + else + { + // Wait for the system command to exit + waitpid(pid, &status, 0); + } +#endif + + if (status != 0) + { +#if ORTHANC_ENABLE_LOGGING == 1 + LOG(ERROR) << "System command failed with status code " << status; +#endif + + throw OrthancException(ErrorCode_SystemCommand); + } + } + + + int SystemToolbox::GetProcessId() + { +#if defined(_WIN32) + return static_cast(_getpid()); +#else + return static_cast(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; + } + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/SystemToolbox.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/SystemToolbox.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,104 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 +#include +#include + +namespace Orthanc +{ + namespace SystemToolbox + { + void USleep(uint64_t microSeconds); + + ServerBarrierEvent ServerBarrier(const bool& stopFlag); + + ServerBarrierEvent ServerBarrier(); + + void ReadFile(std::string& content, + const std::string& path); + + bool ReadHeader(std::string& header, + const std::string& path, + size_t headerSize); + + void WriteFile(const void* content, + size_t size, + const std::string& path); + + void WriteFile(const std::string& content, + const std::string& path); + + void RemoveFile(const std::string& path); + + uint64_t GetFileSize(const std::string& path); + + void MakeDirectory(const std::string& path); + + bool IsExistingFile(const std::string& path); + + std::string GetPathToExecutable(); + + std::string GetDirectoryOfExecutable(); + + void ExecuteSystemCommand(const std::string& command, + const std::vector& arguments); + + int GetProcessId(); + + bool IsRegularFile(const std::string& path); + + FILE* OpenFile(const std::string& path, + FileMode mode); + + std::string GetNowIsoString(bool utc); + + void GetNowDicom(std::string& date, + std::string& time, + bool utc); + + unsigned int GetHardwareConcurrency(); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/TemporaryFile.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/TemporaryFile.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,95 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeaders.h" +#include "TemporaryFile.h" + +#include "SystemToolbox.h" +#include "Toolbox.h" + +#include + +namespace Orthanc +{ + static std::string CreateTemporaryPath(const char* extension) + { +#if BOOST_HAS_FILESYSTEM_V3 == 1 + boost::filesystem::path tmpDir = boost::filesystem::temp_directory_path(); +#elif defined(__linux__) + boost::filesystem::path tmpDir("/tmp"); +#else +#error Support your platform here +#endif + + // We use UUID to create unique path to temporary files + std::string filename = "Orthanc-" + Orthanc::Toolbox::GenerateUuid(); + + if (extension != NULL) + { + filename.append(extension); + } + + tmpDir /= filename; + return tmpDir.string(); + } + + + TemporaryFile::TemporaryFile() : + path_(CreateTemporaryPath(NULL)) + { + } + + + TemporaryFile::TemporaryFile(const char* extension) : + path_(CreateTemporaryPath(extension)) + { + } + + + TemporaryFile::~TemporaryFile() + { + boost::filesystem::remove(path_); + } + + + void TemporaryFile::Write(const std::string& content) + { + SystemToolbox::WriteFile(content, path_); + } + + + void TemporaryFile::Read(std::string& content) const + { + SystemToolbox::ReadFile(content, path_); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/TemporaryFile.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/TemporaryFile.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class TemporaryFile cannot be used in sandboxed environments +#endif + +#include + +namespace Orthanc +{ + class TemporaryFile + { + private: + std::string path_; + + public: + TemporaryFile(); + + TemporaryFile(const char* extension); + + ~TemporaryFile(); + + const std::string& GetPath() const + { + return path_; + } + + void Write(const std::string& content); + + void Read(std::string& content) const; + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Toolbox.cpp --- a/Core/Toolbox.cpp Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Toolbox.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -36,57 +37,38 @@ #include "OrthancException.h" #include "Logging.h" +#include +#include +#include +#include +#include + #include #include #include -#include -#include -#include -#include #include #include -#if BOOST_HAS_DATE_TIME == 1 -#include -#endif -#if BOOST_HAS_REGEX == 1 -#include +#if ORTHANC_ENABLE_MD5 == 1 +# include "../Resources/ThirdParty/md5/md5.h" #endif -#if defined(_WIN32) -#include -#include // For "_spawnvp()" and "_getpid()" -#else -#include // For "execvp()" -#include // For "waitpid()" -#endif - -#if defined(__APPLE__) && defined(__MACH__) -#include /* _NSGetExecutablePath */ -#include /* PATH_MAX */ +#if ORTHANC_ENABLE_BASE64 == 1 +# include "../Resources/ThirdParty/base64/base64.h" #endif -#if defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) -#include /* PATH_MAX */ -#include -#include -#endif - -#if BOOST_HAS_LOCALE != 1 -#error Since version 0.7.6, Orthanc entirely relies on boost::locale +#if ORTHANC_ENABLE_LOCALE == 1 +# include #endif -#include - - -#if !defined(ORTHANC_ENABLE_MD5) || ORTHANC_ENABLE_MD5 == 1 -#include "../Resources/ThirdParty/md5/md5.h" -#endif - - -#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 -#include "../Resources/ThirdParty/base64/base64.h" +#if ORTHANC_ENABLE_SSL == 1 +// For OpenSSL initialization and finalization +# include +# include +# include +# include +# include #endif @@ -103,81 +85,105 @@ #endif -#if ORTHANC_PUGIXML_ENABLED == 1 -#include "ChunkedBuffer.h" -#include +#if defined(_WIN32) +# include // For ::Sleep +#endif + + +#if ORTHANC_ENABLE_PUGIXML == 1 +# include "ChunkedBuffer.h" +# include #endif +// Inclusions for UUID +// http://stackoverflow.com/a/1626302 + +extern "C" +{ +#if defined(_WIN32) +# include +#else +# include +#endif +} + + + namespace Orthanc { - static bool finish; - -#if defined(_WIN32) - static BOOL WINAPI ConsoleControlHandler(DWORD dwCtrlType) - { - // http://msdn.microsoft.com/en-us/library/ms683242(v=vs.85).aspx - finish = true; - return true; - } -#else - static void SignalHandler(int) + void Toolbox::LinesIterator::FindEndOfLine() { - finish = true; + lineEnd_ = lineStart_; + + while (lineEnd_ < content_.size() && + content_[lineEnd_] != '\n' && + content_[lineEnd_] != '\r') + { + lineEnd_ += 1; + } } -#endif + - void Toolbox::USleep(uint64_t microSeconds) + Toolbox::LinesIterator::LinesIterator(const std::string& content) : + content_(content), + lineStart_(0) { -#if defined(_WIN32) - ::Sleep(static_cast(microSeconds / static_cast(1000))); -#elif defined(__linux) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) - usleep(microSeconds); -#else -#error Support your platform here -#endif + FindEndOfLine(); } - - static void ServerBarrierInternal(const bool* stopFlag) + + bool Toolbox::LinesIterator::GetLine(std::string& target) const { -#if defined(_WIN32) - SetConsoleCtrlHandler(ConsoleControlHandler, true); -#else - signal(SIGINT, SignalHandler); - signal(SIGQUIT, SignalHandler); - signal(SIGTERM, SignalHandler); -#endif - - // Active loop that awakens every 100ms - finish = false; - while (!(*stopFlag || finish)) + assert(lineStart_ <= content_.size() && + lineEnd_ <= content_.size() && + lineStart_ <= lineEnd_); + + if (lineStart_ == content_.size()) { - Toolbox::USleep(100 * 1000); + return false; } - -#if defined(_WIN32) - SetConsoleCtrlHandler(ConsoleControlHandler, false); -#else - signal(SIGINT, NULL); - signal(SIGQUIT, NULL); - signal(SIGTERM, NULL); -#endif + else + { + target = content_.substr(lineStart_, lineEnd_ - lineStart_); + return true; + } } + + void Toolbox::LinesIterator::Next() + { + lineStart_ = lineEnd_; - void Toolbox::ServerBarrier(const bool& stopFlag) - { - ServerBarrierInternal(&stopFlag); + if (lineStart_ != content_.size()) + { + assert(content_[lineStart_] == '\r' || + content_[lineStart_] == '\n'); + + char second; + + if (content_[lineStart_] == '\r') + { + second = '\n'; + } + else + { + second = '\r'; + } + + lineStart_ += 1; + + if (lineStart_ < content_.size() && + content_[lineStart_] == second) + { + lineStart_ += 1; + } + + FindEndOfLine(); + } } - void Toolbox::ServerBarrier() - { - const bool stopFlag = false; - ServerBarrierInternal(&stopFlag); - } - - + void Toolbox::ToUpperCase(std::string& s) { std::transform(s.begin(), s.end(), s.begin(), toupper); @@ -205,82 +211,6 @@ } - void Toolbox::ReadFile(std::string& content, - const std::string& path) - { - if (!boost::filesystem::is_regular_file(path)) - { - LOG(ERROR) << "The path does not point to a regular file: " << path; - throw OrthancException(ErrorCode_RegularFileExpected); - } - - boost::filesystem::ifstream f; - f.open(path, std::ifstream::in | std::ifstream::binary); - if (!f.good()) - { - throw OrthancException(ErrorCode_InexistentFile); - } - - // http://www.cplusplus.com/reference/iostream/istream/tellg/ - f.seekg(0, std::ios::end); - std::streamsize size = f.tellg(); - f.seekg(0, std::ios::beg); - - content.resize(size); - if (size != 0) - { - f.read(reinterpret_cast(&content[0]), size); - } - - f.close(); - } - - - void Toolbox::WriteFile(const void* content, - size_t size, - const std::string& path) - { - boost::filesystem::ofstream f; - f.open(path, std::ofstream::binary); - if (!f.good()) - { - throw OrthancException(ErrorCode_CannotWriteFile); - } - - if (size != 0) - { - f.write(reinterpret_cast(content), size); - } - - f.close(); - } - - - void Toolbox::WriteFile(const std::string& content, - const std::string& path) - { - WriteFile(content.size() > 0 ? content.c_str() : NULL, - content.size(), path); - } - - - void Toolbox::RemoveFile(const std::string& path) - { - if (boost::filesystem::exists(path)) - { - if (boost::filesystem::is_regular_file(path)) - { - boost::filesystem::remove(path); - } - else - { - throw OrthancException(ErrorCode_RegularFileExpected); - } - } - } - - - void Toolbox::SplitUriComponents(UriComponents& components, const std::string& uri) { @@ -413,6 +343,8 @@ contentType = "application/json"; else if (!strcmp(extension, "pdf")) contentType = "application/pdf"; + else if (!strcmp(extension, "wasm")) + contentType = "application/wasm"; // Images types else if (!strcmp(extension, "jpg") || !strcmp(extension, "jpeg")) @@ -448,21 +380,7 @@ } - - uint64_t Toolbox::GetFileSize(const std::string& path) - { - try - { - return static_cast(boost::filesystem::file_size(path)); - } - catch (boost::filesystem::filesystem_error&) - { - throw OrthancException(ErrorCode_InexistentFile); - } - } - - -#if !defined(ORTHANC_ENABLE_MD5) || ORTHANC_ENABLE_MD5 == 1 +#if ORTHANC_ENABLE_MD5 == 1 static char GetHexadecimalCharacter(uint8_t value) { assert(value < 16); @@ -494,16 +412,16 @@ void Toolbox::ComputeMD5(std::string& result, const void* data, - size_t length) + size_t size) { md5_state_s state; md5_init(&state); - if (length > 0) + if (size > 0) { md5_append(&state, reinterpret_cast(data), - static_cast(length)); + static_cast(size)); } md5_byte_t actualHash[16]; @@ -519,7 +437,7 @@ #endif -#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 +#if ORTHANC_ENABLE_BASE64 == 1 void Toolbox::EncodeBase64(std::string& result, const std::string& data) { @@ -529,12 +447,23 @@ void Toolbox::DecodeBase64(std::string& result, const std::string& data) { + for (size_t i = 0; i < data.length(); i++) + { + if (!isalnum(data[i]) && + data[i] != '+' && + data[i] != '/' && + data[i] != '=') + { + // This is not a valid character for a Base64 string + throw OrthancException(ErrorCode_BadFileFormat); + } + } + result = base64_decode(data); } -# if BOOST_HAS_REGEX == 1 - void Toolbox::DecodeDataUriScheme(std::string& mime, + bool Toolbox::DecodeDataUriScheme(std::string& mime, std::string& content, const std::string& source) { @@ -546,71 +475,26 @@ { mime = what[1]; DecodeBase64(content, what[2]); + return true; } else { - throw OrthancException(ErrorCode_BadFileFormat); + return false; } } -# endif + + + void Toolbox::EncodeDataUriScheme(std::string& result, + const std::string& mime, + const std::string& content) + { + result = "data:" + mime + ";base64," + base64_encode(content); + } #endif - -#if defined(_WIN32) - static std::string GetPathToExecutableInternal() - { - // Yes, this is ugly, but there is no simple way to get the - // required buffer size, so we use a big constant - std::vector buffer(32768); - /*int bytes =*/ GetModuleFileNameA(NULL, &buffer[0], static_cast(buffer.size() - 1)); - return std::string(&buffer[0]); - } - -#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) - static std::string GetPathToExecutableInternal() - { - std::vector 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(sizeof(pathbuf)); - - _NSGetExecutablePath( pathbuf, &bufsize); - - return std::string(pathbuf); - } - -#else -#error Support your platform here -#endif - - - std::string Toolbox::GetPathToExecutable() - { - boost::filesystem::path p(GetPathToExecutableInternal()); - return boost::filesystem::absolute(p).string(); - } - - - std::string Toolbox::GetDirectoryOfExecutable() - { - boost::filesystem::path p(GetPathToExecutableInternal()); - return boost::filesystem::absolute(p.parent_path()).string(); - } - - +#if ORTHANC_ENABLE_LOCALE == 1 static const char* GetBoostLocaleEncoding(const Encoding sourceEncoding) { switch (sourceEncoding) @@ -677,8 +561,10 @@ throw OrthancException(ErrorCode_NotImplemented); } } +#endif +#if ORTHANC_ENABLE_LOCALE == 1 std::string Toolbox::ConvertToUtf8(const std::string& source, Encoding sourceEncoding) { @@ -705,8 +591,10 @@ return ConvertToAscii(source); } } +#endif + - +#if ORTHANC_ENABLE_LOCALE == 1 std::string Toolbox::ConvertFromUtf8(const std::string& source, Encoding targetEncoding) { @@ -733,8 +621,32 @@ return ConvertToAscii(source); } } +#endif + bool Toolbox::IsAsciiString(const void* data, + size_t size) + { + const uint8_t* p = reinterpret_cast(data); + + for (size_t i = 0; i < size; i++, p++) + { + if (*p > 127 || *p == 0 || iscntrl(*p)) + { + return false; + } + } + + return true; + } + + + bool Toolbox::IsAsciiString(const std::string& s) + { + return IsAsciiString(s.c_str(), s.size()); + } + + std::string Toolbox::ConvertToAscii(const std::string& source) { std::string result; @@ -751,14 +663,16 @@ return result; } + void Toolbox::ComputeSHA1(std::string& result, - const std::string& data) + const void* data, + size_t size) { boost::uuids::detail::sha1 sha1; - if (data.size() > 0) + if (size > 0) { - sha1.process_bytes(&data[0], data.size()); + sha1.process_bytes(data, size); } unsigned int digest[5]; @@ -777,6 +691,20 @@ digest[4]); } + void Toolbox::ComputeSHA1(std::string& result, + const std::string& data) + { + if (data.size() > 0) + { + ComputeSHA1(result, data.c_str(), data.size()); + } + else + { + ComputeSHA1(result, NULL, 0); + } + } + + bool Toolbox::IsSHA1(const char* str, size_t size) { @@ -855,30 +783,6 @@ } -#if BOOST_HAS_DATE_TIME == 1 - std::string Toolbox::GetNowIsoString() - { - boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); - return boost::posix_time::to_iso_string(now); - } - - void Toolbox::GetNowDicom(std::string& date, - std::string& time) - { - boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); - tm tm = boost::posix_time::to_tm(now); - - char s[32]; - sprintf(s, "%04d%02d%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); - date.assign(s); - - // TODO milliseconds - sprintf(s, "%02d%02d%02d.%06d", tm.tm_hour, tm.tm_min, tm.tm_sec, 0); - time.assign(s); - } -#endif - - std::string Toolbox::StripSpaces(const std::string& source) { size_t first = 0; @@ -979,7 +883,6 @@ } -#if BOOST_HAS_REGEX == 1 std::string Toolbox::WildcardToRegularExpression(const std::string& source) { // TODO - Speed up this with a regular expression @@ -1007,8 +910,6 @@ return result; } -#endif - void Toolbox::TokenizeString(std::vector& result, @@ -1036,32 +937,7 @@ } - void Toolbox::MakeDirectory(const std::string& path) - { - if (boost::filesystem::exists(path)) - { - if (!boost::filesystem::is_directory(path)) - { - throw OrthancException(ErrorCode_DirectoryOverFile); - } - } - else - { - if (!boost::filesystem::create_directories(path)) - { - throw OrthancException(ErrorCode_MakeDirectory); - } - } - } - - - bool Toolbox::IsExistingFile(const std::string& path) - { - return boost::filesystem::exists(path); - } - - -#if ORTHANC_PUGIXML_ENABLED == 1 +#if ORTHANC_ENABLE_PUGIXML == 1 class ChunkedBufferWriter : public pugi::xml_writer { private: @@ -1183,64 +1059,6 @@ #endif - void Toolbox::ExecuteSystemCommand(const std::string& command, - const std::vector& arguments) - { - // Convert the arguments as a C array - std::vector args(arguments.size() + 2); - - args.front() = const_cast(command.c_str()); - - for (size_t i = 0; i < arguments.size(); i++) - { - args[i + 1] = const_cast(arguments[i].c_str()); - } - - args.back() = NULL; - - int status; - -#if defined(_WIN32) - // http://msdn.microsoft.com/en-us/library/275khfab.aspx - status = static_cast(_spawnvp(_P_OVERLAY, command.c_str(), &args[0])); - -#else - int pid = fork(); - - if (pid == -1) - { - // Error in fork() -#if ORTHANC_ENABLE_LOGGING == 1 - LOG(ERROR) << "Cannot fork a child process"; -#endif - - throw OrthancException(ErrorCode_SystemCommand); - } - else if (pid == 0) - { - // Execute the system command in the child process - execvp(command.c_str(), &args[0]); - - // We should never get here - _exit(1); - } - else - { - // Wait for the system command to exit - waitpid(pid, &status, 0); - } -#endif - - if (status != 0) - { -#if ORTHANC_ENABLE_LOGGING == 1 - LOG(ERROR) << "System command failed with status code " << status; -#endif - - throw OrthancException(ErrorCode_SystemCommand); - } - } - bool Toolbox::IsInteger(const std::string& str) { @@ -1348,15 +1166,402 @@ return str.compare(0, prefix.size(), prefix) == 0; } } + + + static bool IsUnreservedCharacter(char c) + { + // This function checks whether "c" is an unserved character + // wrt. an URI percent-encoding + // https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding%5Fin%5Fa%5FURI + + return ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '_' || + c == '.' || + c == '~'); + } + + void Toolbox::UriEncode(std::string& target, + const std::string& source) + { + // Estimate the length of the percent-encoded URI + size_t length = 0; + + for (size_t i = 0; i < source.size(); i++) + { + if (IsUnreservedCharacter(source[i])) + { + length += 1; + } + else + { + // This character must be percent-encoded + length += 3; + } + } + + target.clear(); + target.reserve(length); + + for (size_t i = 0; i < source.size(); i++) + { + if (IsUnreservedCharacter(source[i])) + { + target.push_back(source[i]); + } + else + { + // This character must be percent-encoded + uint8_t byte = static_cast(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(v); + } + } + + + bool Toolbox::IsUuid(const std::string& str) + { + if (str.size() != 36) + { + return false; + } + + for (size_t i = 0; i < str.length(); i++) + { + if (i == 8 || i == 13 || i == 18 || i == 23) + { + if (str[i] != '-') + return false; + } + else + { + if (!isalnum(str[i])) + return false; + } + } + + return true; + } - int Toolbox::GetProcessId() + bool Toolbox::StartsWithUuid(const std::string& str) + { + if (str.size() < 36) + { + return false; + } + + if (str.size() == 36) + { + return IsUuid(str); + } + + assert(str.size() > 36); + if (!isspace(str[36])) + { + return false; + } + + return IsUuid(str.substr(0, 36)); + } + + +#if ORTHANC_ENABLE_LOCALE == 1 + static std::auto_ptr globalLocale_; + + static bool SetGlobalLocale(const char* locale) + { + globalLocale_.reset(NULL); + + try + { + if (locale == NULL) + { + LOG(WARNING) << "Falling back to system-wide default locale"; + globalLocale_.reset(new std::locale()); + } + else + { + LOG(INFO) << "Using locale: \"" << locale << "\" for case-insensitive comparison of strings"; + globalLocale_.reset(new std::locale(locale)); + } + } + catch (std::runtime_error&) + { + } + + return (globalLocale_.get() != NULL); + } + + void Toolbox::InitializeGlobalLocale(const char* locale) + { + // Make Orthanc use English, United States locale + // Linux: use "en_US.UTF-8" + // Windows: use "" + // Wine: use NULL + +#if defined(__MINGW32__) + // Visibly, there is no support of locales in MinGW yet + // http://mingw.5.n7.nabble.com/How-to-use-std-locale-global-with-MinGW-correct-td33048.html + static const char* DEFAULT_LOCALE = NULL; +#elif defined(_WIN32) + // For Windows: use default locale (using "en_US" does not work) + static const char* DEFAULT_LOCALE = ""; +#else + // For Linux & cie + static const char* DEFAULT_LOCALE = "en_US.UTF-8"; +#endif + + bool ok; + + if (locale == NULL) + { + ok = SetGlobalLocale(DEFAULT_LOCALE); + +#if defined(__MINGW32__) + LOG(WARNING) << "This is a MinGW build, case-insensitive comparison of " + << "strings with accents will not work outside of Wine"; +#endif + } + else + { + ok = SetGlobalLocale(locale); + } + + if (!ok && + !SetGlobalLocale(NULL)) + { + LOG(ERROR) << "Cannot initialize global locale"; + throw OrthancException(ErrorCode_InternalError); + } + + } + + + void Toolbox::FinalizeGlobalLocale() { -#if defined(_WIN32) - return static_cast(_getpid()); + globalLocale_.reset(); + } + + + std::string Toolbox::ToUpperCaseWithAccents(const std::string& source) + { + if (globalLocale_.get() == NULL) + { + LOG(ERROR) << "No global locale was set, call Toolbox::InitializeGlobalLocale()"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + /** + * A few notes about locales: + * + * (1) We don't use "case folding": + * http://www.boost.org/doc/libs/1_64_0/libs/locale/doc/html/conversions.html + * + * Characters are made uppercase one by one. This is because, in + * static builds, we are using iconv, which is visibly not + * supported correctly (TODO: Understand why). Case folding seems + * to be working correctly if using the default backend under + * Linux (ICU or POSIX?). If one wishes to use case folding, one + * would use: + * + * boost::locale::generator gen; + * std::locale::global(gen(DEFAULT_LOCALE)); + * return boost::locale::to_upper(source); + * + * (2) The function "boost::algorithm::to_upper_copy" does not + * make use of the "std::locale::global()". We therefore create a + * global variable "globalLocale_". + * + * (3) The variant of "boost::algorithm::to_upper_copy()" that + * uses std::string does not work properly. We need to apply it + * one wide strings (std::wstring). This explains the two calls to + * "utf_to_utf" in order to convert to/from std::wstring. + **/ + + std::wstring w = boost::locale::conv::utf_to_utf(source); + w = boost::algorithm::to_upper_copy(w, *globalLocale_); + return boost::locale::conv::utf_to_utf(w); + } +#endif + + + void Toolbox::InitializeOpenSsl() + { +#if ORTHANC_ENABLE_SSL == 1 + // https://wiki.openssl.org/index.php/Library_Initialization + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + ERR_load_crypto_strings(); +#endif + } + + + void Toolbox::FinalizeOpenSsl() + { +#if ORTHANC_ENABLE_SSL == 1 + // Finalize OpenSSL + // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup +#ifdef FIPS_mode_set + FIPS_mode_set(0); +#endif + ENGINE_cleanup(); + CONF_modules_unload(1); + EVP_cleanup(); + CRYPTO_cleanup_all_ex_data(); + ERR_remove_state(0); + ERR_free_strings(); +#endif + } + + + std::string Toolbox::GenerateUuid() + { +#ifdef WIN32 + UUID uuid; + UuidCreate ( &uuid ); + + unsigned char * str; + UuidToStringA ( &uuid, &str ); + + std::string s( ( char* ) str ); + + RpcStringFreeA ( &str ); #else - return static_cast(getpid()); + uuid_t uuid; + uuid_generate_random ( uuid ); + char s[37]; + uuid_unparse ( uuid, s ); #endif + return s; } } + + +OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content) +{ + return reinterpret_cast(new Orthanc::Toolbox::LinesIterator(content)); +} + + +bool OrthancLinesIterator_GetLine(std::string& target, + const OrthancLinesIterator* iterator) +{ + if (iterator != NULL) + { + return reinterpret_cast(iterator)->GetLine(target); + } + else + { + return false; + } +} + + +void OrthancLinesIterator_Next(OrthancLinesIterator* iterator) +{ + if (iterator != NULL) + { + reinterpret_cast(iterator)->Next(); + } +} + + +void OrthancLinesIterator_Free(OrthancLinesIterator* iterator) +{ + if (iterator != NULL) + { + delete reinterpret_cast(iterator); + } +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Toolbox.h --- a/Core/Toolbox.h Thu Oct 29 11:25:45 2015 +0100 +++ b/Core/Toolbox.h Fri Oct 12 15:18:10 2018 +0200 @@ -1,7 +1,8 @@ /** * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -39,6 +40,35 @@ #include #include + +#if !defined(ORTHANC_ENABLE_BASE64) +# error The macro ORTHANC_ENABLE_BASE64 must be defined +#endif + +#if !defined(ORTHANC_ENABLE_LOCALE) +# error The macro ORTHANC_ENABLE_LOCALE must be defined +#endif + +#if !defined(ORTHANC_ENABLE_MD5) +# error The macro ORTHANC_ENABLE_MD5 must be defined +#endif + +#if !defined(ORTHANC_ENABLE_PUGIXML) +# error The macro ORTHANC_ENABLE_PUGIXML must be defined +#endif + + +/** + * NOTE: GUID vs. UUID + * The simple answer is: no difference, they are the same thing. Treat + * them as a 16 byte (128 bits) value that is used as a unique + * value. In Microsoft-speak they are called GUIDs, but call them + * UUIDs when not using Microsoft-speak. + * http://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid + **/ + + + namespace Orthanc { typedef std::vector UriComponents; @@ -49,10 +79,24 @@ namespace Toolbox { - void ServerBarrier(const bool& stopFlag); + class LinesIterator + { + private: + const std::string& content_; + size_t lineStart_; + size_t lineEnd_; - void ServerBarrier(); + void FindEndOfLine(); + + public: + LinesIterator(const std::string& content); + + bool GetLine(std::string& target) const; + void Next(); + }; + + void ToUpperCase(std::string& s); // Inplace version void ToLowerCase(std::string& s); // Inplace version @@ -63,20 +107,6 @@ void ToLowerCase(std::string& result, const std::string& source); - void ReadFile(std::string& content, - const std::string& path); - - void WriteFile(const std::string& content, - const std::string& path); - - void WriteFile(const void* content, - size_t size, - const std::string& path); - - void USleep(uint64_t microSeconds); - - void RemoveFile(const std::string& path); - void SplitUriComponents(UriComponents& components, const std::string& uri); @@ -92,87 +122,78 @@ std::string FlattenUri(const UriComponents& components, size_t fromLevel = 0); - uint64_t GetFileSize(const std::string& path); - -#if !defined(ORTHANC_ENABLE_MD5) || ORTHANC_ENABLE_MD5 == 1 +#if ORTHANC_ENABLE_MD5 == 1 void ComputeMD5(std::string& result, const std::string& data); void ComputeMD5(std::string& result, const void* data, - size_t length); + size_t size); #endif void ComputeSHA1(std::string& result, const std::string& data); + void ComputeSHA1(std::string& result, + const void* data, + size_t size); + bool IsSHA1(const char* str, size_t size); bool IsSHA1(const std::string& s); -#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 +#if ORTHANC_ENABLE_BASE64 == 1 void DecodeBase64(std::string& result, const std::string& data); void EncodeBase64(std::string& result, const std::string& data); -# if BOOST_HAS_REGEX == 1 - void DecodeDataUriScheme(std::string& mime, + bool DecodeDataUriScheme(std::string& mime, std::string& content, const std::string& source); -# endif + + void EncodeDataUriScheme(std::string& result, + const std::string& mime, + const std::string& content); #endif - std::string GetPathToExecutable(); - - std::string GetDirectoryOfExecutable(); - +#if ORTHANC_ENABLE_LOCALE == 1 std::string ConvertToUtf8(const std::string& source, Encoding sourceEncoding); std::string ConvertFromUtf8(const std::string& source, Encoding targetEncoding); +#endif + + bool IsAsciiString(const void* data, + size_t size); + + bool IsAsciiString(const std::string& s); std::string ConvertToAscii(const std::string& source); std::string StripSpaces(const std::string& source); -#if BOOST_HAS_DATE_TIME == 1 - std::string GetNowIsoString(); - - void GetNowDicom(std::string& date, - std::string& time); -#endif - // In-place percent-decoding for URL void UrlDecode(std::string& s); Endianness DetectEndianness(); -#if BOOST_HAS_REGEX == 1 std::string WildcardToRegularExpression(const std::string& s); -#endif void TokenizeString(std::vector& result, const std::string& source, char separator); - void MakeDirectory(const std::string& path); - - bool IsExistingFile(const std::string& path); - -#if ORTHANC_PUGIXML_ENABLED == 1 +#if ORTHANC_ENABLE_PUGIXML == 1 void JsonToXml(std::string& target, const Json::Value& source, const std::string& rootElement = "root", const std::string& arrayElement = "item"); #endif - void ExecuteSystemCommand(const std::string& command, - const std::vector& arguments); - bool IsInteger(const std::string& str); void CopyJsonWithoutComments(Json::Value& target, @@ -181,6 +202,62 @@ bool StartsWith(const std::string& str, const std::string& prefix); - int GetProcessId(); + void UriEncode(std::string& target, + const std::string& source); + + std::string GetJsonStringField(const ::Json::Value& json, + const std::string& key, + const std::string& defaultValue); + + bool GetJsonBooleanField(const ::Json::Value& json, + const std::string& key, + bool defaultValue); + + int GetJsonIntegerField(const ::Json::Value& json, + const std::string& key, + int defaultValue); + + unsigned int GetJsonUnsignedIntegerField(const ::Json::Value& json, + const std::string& key, + unsigned int defaultValue); + + bool IsUuid(const std::string& str); + + bool StartsWithUuid(const std::string& str); + +#if ORTHANC_ENABLE_LOCALE == 1 + void InitializeGlobalLocale(const char* locale); + + void FinalizeGlobalLocale(); + + std::string ToUpperCaseWithAccents(const std::string& source); +#endif + + void InitializeOpenSsl(); + + void FinalizeOpenSsl(); + + std::string GenerateUuid(); } } + + + + +/** + * The plain C, opaque data structure "OrthancLinesIterator" is a thin + * wrapper around Orthanc::Toolbox::LinesIterator, and is only used by + * "../Resources/WebAssembly/dcdict.cc", in order to avoid code + * duplication + **/ + +struct OrthancLinesIterator; + +OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content); + +bool OrthancLinesIterator_GetLine(std::string& target, + const OrthancLinesIterator* iterator); + +void OrthancLinesIterator_Next(OrthancLinesIterator* iterator); + +void OrthancLinesIterator_Free(OrthancLinesIterator* iterator); diff -r 2b91363cc1d1 -r 497a637366b4 Core/Uuid.cpp --- a/Core/Uuid.cpp Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,162 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "PrecompiledHeaders.h" -#include "Uuid.h" - -// http://stackoverflow.com/a/1626302 - -extern "C" -{ -#ifdef WIN32 -#include -#else -#include -#endif -} - -#include - -namespace Orthanc -{ - namespace Toolbox - { - std::string GenerateUuid() - { -#ifdef WIN32 - UUID uuid; - UuidCreate ( &uuid ); - - unsigned char * str; - UuidToStringA ( &uuid, &str ); - - std::string s( ( char* ) str ); - - RpcStringFreeA ( &str ); -#else - uuid_t uuid; - uuid_generate_random ( uuid ); - char s[37]; - uuid_unparse ( uuid, s ); -#endif - return s; - } - - - bool IsUuid(const std::string& str) - { - if (str.size() != 36) - { - return false; - } - - for (size_t i = 0; i < str.length(); i++) - { - if (i == 8 || i == 13 || i == 18 || i == 23) - { - if (str[i] != '-') - return false; - } - else - { - if (!isalnum(str[i])) - return false; - } - } - - return true; - } - - - bool StartsWithUuid(const std::string& str) - { - if (str.size() < 36) - { - return false; - } - - if (str.size() == 36) - { - return IsUuid(str); - } - - assert(str.size() > 36); - if (!isspace(str[36])) - { - return false; - } - - return IsUuid(str.substr(0, 36)); - } - - - static std::string CreateTemporaryPath(const char* extension) - { -#if BOOST_HAS_FILESYSTEM_V3 == 1 - boost::filesystem::path tmpDir = boost::filesystem::temp_directory_path(); -#elif defined(__linux__) - boost::filesystem::path tmpDir("/tmp"); -#else -#error Support your platform here -#endif - - // We use UUID to create unique path to temporary files - std::string filename = "Orthanc-" + Orthanc::Toolbox::GenerateUuid(); - - if (extension != NULL) - { - filename.append(extension); - } - - tmpDir /= filename; - return tmpDir.string(); - } - - - TemporaryFile::TemporaryFile() : - path_(CreateTemporaryPath(NULL)) - { - } - - - TemporaryFile::TemporaryFile(const char* extension) : - path_(CreateTemporaryPath(extension)) - { - } - - - TemporaryFile::~TemporaryFile() - { - boost::filesystem::remove(path_); - } - } -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/Uuid.h --- a/Core/Uuid.h Thu Oct 29 11:25:45 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,86 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include - -/** - * GUID vs. UUID - * The simple answer is: no difference, they are the same thing. Treat - * them as a 16 byte (128 bits) value that is used as a unique - * value. In Microsoft-speak they are called GUIDs, but call them - * UUIDs when not using Microsoft-speak. - * http://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid - **/ - -#include "Toolbox.h" - -namespace Orthanc -{ - namespace Toolbox - { - std::string GenerateUuid(); - - bool IsUuid(const std::string& str); - - bool StartsWithUuid(const std::string& str); - - class TemporaryFile - { - private: - std::string path_; - - public: - TemporaryFile(); - - TemporaryFile(const char* extension); - - ~TemporaryFile(); - - const std::string& GetPath() const - { - return path_; - } - - void Write(const std::string& content) - { - Toolbox::WriteFile(content, path_); - } - - void Read(std::string& content) const - { - Toolbox::ReadFile(content, path_); - } - }; - } -} diff -r 2b91363cc1d1 -r 497a637366b4 Core/WebServiceParameters.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/WebServiceParameters.cpp Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,504 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#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 + +namespace Orthanc +{ + static const char* KEY_CERTIFICATE_FILE = "CertificateFile"; + static const char* KEY_CERTIFICATE_KEY_FILE = "CertificateKeyFile"; + static const char* KEY_CERTIFICATE_KEY_PASSWORD = "CertificateKeyPassword"; + static const char* KEY_HTTP_HEADERS = "HttpHeaders"; + static const char* KEY_PASSWORD = "Password"; + static const char* KEY_PKCS11 = "Pkcs11"; + static const char* KEY_URL = "Url"; + static const char* KEY_URL_2 = "URL"; + static const char* KEY_USERNAME = "Username"; + + + static bool IsReservedKey(const std::string& key) + { + return (key == KEY_CERTIFICATE_FILE || + key == KEY_CERTIFICATE_KEY_FILE || + key == KEY_CERTIFICATE_KEY_PASSWORD || + key == KEY_HTTP_HEADERS || + key == KEY_PASSWORD || + key == KEY_PKCS11 || + key == KEY_URL || + key == KEY_URL_2 || + key == KEY_USERNAME); + } + + + WebServiceParameters::WebServiceParameters() : + pkcs11Enabled_(false) + { + SetUrl("http://127.0.0.1:8042/"); + } + + + void WebServiceParameters::ClearClientCertificate() + { + certificateFile_.clear(); + certificateKeyFile_.clear(); + certificateKeyPassword_.clear(); + } + + + void WebServiceParameters::SetUrl(const std::string& url) + { + if (!Toolbox::StartsWith(url, "http://") && + !Toolbox::StartsWith(url, "https://")) + { + LOG(ERROR) << "Bad URL: " << url; + throw OrthancException(ErrorCode_BadFileFormat); + } + + // Add trailing slash if needed + if (url[url.size() - 1] == '/') + { + url_ = url; + } + else + { + url_ = url + '/'; + } + } + + + void WebServiceParameters::ClearCredentials() + { + username_.clear(); + password_.clear(); + } + + + void WebServiceParameters::SetCredentials(const std::string& username, + const std::string& password) + { + if (username.empty() && + !password.empty()) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + username_ = username; + password_ = password; + } + } + + + void WebServiceParameters::SetClientCertificate(const std::string& certificateFile, + const std::string& certificateKeyFile, + const std::string& certificateKeyPassword) + { + if (certificateFile.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (certificateKeyPassword.empty()) + { + LOG(ERROR) << "The password for the HTTPS certificate is not provided: " << certificateFile; + throw OrthancException(ErrorCode_BadFileFormat); + } + + certificateFile_ = certificateFile; + certificateKeyFile_ = certificateKeyFile; + certificateKeyPassword_ = certificateKeyPassword; + } + + + void WebServiceParameters::FromSimpleFormat(const Json::Value& peer) + { + assert(peer.isArray()); + + pkcs11Enabled_ = false; + ClearClientCertificate(); + + if (peer.size() != 1 && + peer.size() != 3) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + SetUrl(peer.get(0u, "").asString()); + + if (peer.size() == 1) + { + ClearCredentials(); + } + else if (peer.size() == 2) + { + LOG(ERROR) << "The HTTP password is not provided"; + throw OrthancException(ErrorCode_BadFileFormat); + } + else if (peer.size() == 3) + { + SetCredentials(peer.get(1u, "").asString(), + peer.get(2u, "").asString()); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + static std::string GetStringMember(const Json::Value& peer, + const std::string& key, + const std::string& defaultValue) + { + if (!peer.isMember(key)) + { + return defaultValue; + } + else if (peer[key].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + return peer[key].asString(); + } + } + + + void WebServiceParameters::FromAdvancedFormat(const Json::Value& peer) + { + assert(peer.isObject()); + + std::string url = GetStringMember(peer, KEY_URL, ""); + if (url.empty()) + { + SetUrl(GetStringMember(peer, KEY_URL_2, "")); + } + else + { + SetUrl(url); + } + + SetCredentials(GetStringMember(peer, KEY_USERNAME, ""), + GetStringMember(peer, KEY_PASSWORD, "")); + + std::string file = GetStringMember(peer, KEY_CERTIFICATE_FILE, ""); + if (!file.empty()) + { + SetClientCertificate(file, GetStringMember(peer, KEY_CERTIFICATE_KEY_FILE, ""), + GetStringMember(peer, KEY_CERTIFICATE_KEY_PASSWORD, "")); + } + else + { + ClearClientCertificate(); + } + + if (peer.isMember(KEY_PKCS11)) + { + if (peer[KEY_PKCS11].type() == Json::booleanValue) + { + pkcs11Enabled_ = peer[KEY_PKCS11].asBool(); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + else + { + pkcs11Enabled_ = false; + } + + + headers_.clear(); + + if (peer.isMember(KEY_HTTP_HEADERS)) + { + const Json::Value& h = peer[KEY_HTTP_HEADERS]; + if (h.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + Json::Value::Members keys = h.getMemberNames(); + for (size_t i = 0; i < keys.size(); i++) + { + const Json::Value& value = h[keys[i]]; + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + headers_[keys[i]] = value.asString(); + } + } + } + } + + + userProperties_.clear(); + + const Json::Value::Members members = peer.getMemberNames(); + + for (Json::Value::Members::const_iterator it = members.begin(); + it != members.end(); ++it) + { + if (!IsReservedKey(*it)) + { + if (peer[*it].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + userProperties_[*it] = peer[*it].asString(); + } + } + } + } + + + void WebServiceParameters::Unserialize(const Json::Value& peer) + { + try + { + if (peer.isArray()) + { + FromSimpleFormat(peer); + } + else if (peer.isObject()) + { + FromAdvancedFormat(peer); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + catch (OrthancException&) + { + throw; + } + catch (...) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + + void WebServiceParameters::ListHttpHeaders(std::set& target) const + { + target.clear(); + + for (Dictionary::const_iterator it = headers_.begin(); + it != headers_.end(); ++it) + { + target.insert(it->first); + } + } + + + bool WebServiceParameters::LookupHttpHeader(std::string& value, + const std::string& key) const + { + Dictionary::const_iterator found = headers_.find(key); + + if (found == headers_.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + void WebServiceParameters::AddUserProperty(const std::string& key, + const std::string& value) + { + if (IsReservedKey(key)) + { + LOG(ERROR) << "Cannot use this reserved key to name an user property: " << key; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + userProperties_[key] = value; + } + } + + + void WebServiceParameters::ListUserProperties(std::set& target) const + { + target.clear(); + + for (Dictionary::const_iterator it = userProperties_.begin(); + it != userProperties_.end(); ++it) + { + target.insert(it->first); + } + } + + + bool WebServiceParameters::LookupUserProperty(std::string& value, + const std::string& key) const + { + Dictionary::const_iterator found = userProperties_.find(key); + + if (found == userProperties_.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + bool WebServiceParameters::IsAdvancedFormatNeeded() const + { + return (!certificateFile_.empty() || + !certificateKeyFile_.empty() || + !certificateKeyPassword_.empty() || + pkcs11Enabled_ || + !headers_.empty() || + !userProperties_.empty()); + } + + + void WebServiceParameters::Serialize(Json::Value& value, + bool forceAdvancedFormat, + bool includePasswords) const + { + if (forceAdvancedFormat || + IsAdvancedFormatNeeded()) + { + value = Json::objectValue; + value[KEY_URL] = url_; + + if (!username_.empty() || + !password_.empty()) + { + value[KEY_USERNAME] = username_; + + if (includePasswords) + { + value[KEY_PASSWORD] = password_; + } + } + + if (!certificateFile_.empty()) + { + value[KEY_CERTIFICATE_FILE] = certificateFile_; + } + + if (!certificateKeyFile_.empty()) + { + value[KEY_CERTIFICATE_KEY_FILE] = certificateKeyFile_; + } + + if (!certificateKeyPassword_.empty() && + includePasswords) + { + value[KEY_CERTIFICATE_KEY_PASSWORD] = certificateKeyPassword_; + } + + value[KEY_PKCS11] = pkcs11Enabled_; + + value[KEY_HTTP_HEADERS] = Json::objectValue; + for (Dictionary::const_iterator it = headers_.begin(); + it != headers_.end(); ++it) + { + value[KEY_HTTP_HEADERS][it->first] = it->second; + } + + for (Dictionary::const_iterator it = userProperties_.begin(); + it != userProperties_.end(); ++it) + { + value[it->first] = it->second; + } + } + else + { + value = Json::arrayValue; + value.append(url_); + + if (!username_.empty() || + !password_.empty()) + { + value.append(username_); + value.append(includePasswords ? password_ : ""); + } + } + } + + +#if ORTHANC_SANDBOXED == 0 + void WebServiceParameters::CheckClientCertificate() const + { + if (!certificateFile_.empty()) + { + if (!SystemToolbox::IsRegularFile(certificateFile_)) + { + LOG(ERROR) << "Cannot open certificate file: " << certificateFile_; + throw OrthancException(ErrorCode_InexistentFile); + } + + if (!certificateKeyFile_.empty() && + !SystemToolbox::IsRegularFile(certificateKeyFile_)) + { + LOG(ERROR) << "Cannot open key file: " << certificateKeyFile_; + throw OrthancException(ErrorCode_InexistentFile); + } + } + } +#endif +} diff -r 2b91363cc1d1 -r 497a637366b4 Core/WebServiceParameters.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/WebServiceParameters.h Fri Oct 12 15:18:10 2018 +0200 @@ -0,0 +1,179 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#include +#include +#include +#include + +namespace Orthanc +{ + class WebServiceParameters + { + public: + typedef std::map 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& 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& target) const; + + bool LookupUserProperty(std::string& value, + const std::string& key) const; + + bool IsAdvancedFormatNeeded() const; + + void Unserialize(const Json::Value& peer); + + void Serialize(Json::Value& value, + bool forceAdvancedFormat, + bool includePasswords) const; + +#if ORTHANC_SANDBOXED == 0 + void CheckClientCertificate() const; +#endif + }; +} diff -r 2b91363cc1d1 -r 497a637366b4 DarwinCompilation.txt --- a/DarwinCompilation.txt Thu Oct 29 11:25:45 2015 +0100 +++ b/DarwinCompilation.txt Fri Oct 12 15:18:10 2018 +0200 @@ -31,7 +31,9 @@ # cmake -GXcode -DCMAKE_OSX_DEPLOYMENT_TARGET=10.8 -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON ~/Orthanc NB: Adapt the value of "CMAKE_OSX_DEPLOYMENT_TARGET" with respect to -your version of XCode. +your version of OS X. This version can obtained by typing: + +# sw_vers Build the Debug version of Orthanc @@ -47,6 +49,6 @@ ------------------------------------ # xcodebuild -configuration Release -# ./Debug/UnitTests +# ./Release/UnitTests The binaries of Orthanc are located at "~/OrthancBuild/Release/Orthanc". diff -r 2b91363cc1d1 -r 497a637366b4 INSTALL --- a/INSTALL Thu Oct 29 11:25:45 2015 +0100 +++ b/INSTALL Fri Oct 12 15:18:10 2018 +0200 @@ -51,14 +51,19 @@ third-party dependencies, delete the "~/Orthanc/ThirdPartyDownloads/" folder, then restart cmake. +WARNING 3: If performance is important to you, make sure to add the +option "-DCMAKE_BUILD_TYPE=Release" when invoking cmake. Indeed, by +default, run-time debug assertions are enabled, which can seriously +impact performance, especially if your Orthanc server stores a lot of +DICOM instances. -Native Linux Compilation ------------------------- + +Native GNU/Linux Compilation +---------------------------- See the file "LinuxCompilation.txt". - Native OS X Compilation ----------------------- @@ -70,36 +75,55 @@ ------------------------------------------------- # cd [...]\OrthancBuild -# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON -G "Visual Studio 8 2005" [...]\Orthanc +# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON -DUSE_LEGACY_JSONCPP=ON -G "Visual Studio 9 2008" [...]\Orthanc Then open the "[...]/OrthancBuild/Orthanc.sln" with Visual Studio. NOTES: -* More recent versions of Visual Studio than 2005 should also +* More recent versions of Visual Studio than 2008 should also work. Type "cmake" without arguments to have the list of generators that are available on your computer. * You will have to install the Platform SDK (version 6 or above) for Visual Studio 2005: http://en.wikipedia.org/wiki/Microsoft_Windows_SDK. Read the CMake FAQ: http://goo.gl/By90B +* The "-DUSE_LEGACY_JSONCPP=ON" must be set for versions of + Visual Studio that do not support C++11 -Cross-Compilation for Windows under Linux ------------------------------------------ +Cross-Compilation for Windows under GNU/Linux +--------------------------------------------- -To cross-compile Windows binaries under Linux using MinGW, please use -the following command: +Some versions of MinGW-W64 might have problems with C++11 (notably +those shipped in Ubuntu 16.04 LTS, in the "mingw-w64" package). Use +the following command to disable C++11: # cd ~/OrthancBuild -# cmake -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGWToolchain.cmake -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DCMAKE_BUILD_TYPE=Debug ~/Orthanc +# cmake ~/Orthanc \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGW-W64-Toolchain32.cmake \ + -DSTANDALONE_BUILD=ON \ + -DSTATIC_BUILD=ON \ + -DUSE_LEGACY_JSONCPP=ON # make +NB: Use the toolchain "MinGW-W64-Toolchain64.cmake" to produce 64bit +Windows binaries. + -Native Windows build with MinGW (VERY SLOW) -------------------------------------------- +Legacy MinGW32 compilers (notably those shipped in Ubuntu 14.04 LTS, +in the "mingw32" package) are incompatible with DCMTK 3.6.2 and +C++11. Use the following command to force using DCMTK 3.6.0 and +disable C++11: -# cd [...]\OrthancBuild -# cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug [...]\Orthanc -# mingw32-make +# cd ~/OrthancBuild +# cmake ~/Orthanc \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_TOOLCHAIN_FILE=~/Orthanc/Resources/MinGWToolchain.cmake \ + -DSTANDALONE_BUILD=ON \ + -DSTATIC_BUILD=ON \ + -DUSE_DCMTK_360=ON \ + -DUSE_LEGACY_JSONCPP=ON +# make diff -r 2b91363cc1d1 -r 497a637366b4 LinuxCompilation.txt --- a/LinuxCompilation.txt Thu Oct 29 11:25:45 2015 +0100 +++ b/LinuxCompilation.txt Fri Oct 12 15:18:10 2018 +0200 @@ -1,17 +1,17 @@ This file is a complement to "INSTALL", which contains instructions -that are specific to Linux. +that are specific to GNU/Linux. -Static linking for Linux -======================== +Static linking for GNU/Linux +============================ -The most simple way of building Orthanc under Linux consists in +The most simple way of building Orthanc under GNU/Linux consists in statically linking against all the third-party dependencies. In this case, the system-wide libraries will not be used. The build tool (CMake) will download the sources of all the required packages and automatically compile them. -This process should work on any Linux distribution, provided that a +This process should work on any GNU/Linux distribution, provided that a C/C++ compiler ("build-essential" in Debian-based systems), the Python interpreter, CMake, the "unzip" system tool, and the development package for libuuid ("uuid-dev" in Debian) are installed. @@ -47,14 +47,14 @@ Note 3- To build the documentation, you will have to install doxyen. -Use system-wide libraries under Linux -===================================== +Use system-wide libraries under GNU/Linux +========================================= -Under Linux, by default, Orthanc links against the shared libraries of -your system (the "STATIC_BUILD" option is set to "OFF"). This greatly -speeds up the compilation. This is also required when building -packages for Linux distributions. Because using system libraries is -the default behavior, you just have to use: +Under GNU/Linux, by default, Orthanc links against the shared +libraries of your system (the "STATIC_BUILD" option is set to +"OFF"). This greatly speeds up the compilation. This is also required +when building packages for GNU/Linux distributions. Because using +system libraries is the default behavior, you just have to use: # cd ~/OrthancBuild # cmake -DCMAKE_BUILD_TYPE=Debug ~/Orthanc @@ -62,13 +62,14 @@ Note that to build the documentation, you will have to install doxyen. -However, on some Linux distributions, it is still required to download -and static link against some third-party dependencies, e.g. when the -system-wide library is not shipped or is outdated. Because of -difference in the packaging of the various Linux distribution, it is -also sometimes required to fine-tune some options. +However, on some GNU/Linux distributions, it is still required to +download and static link against some third-party dependencies, +e.g. when the system-wide library is not shipped or is +outdated. Because of difference in the packaging of the various +GNU/Linux distribution, it is also sometimes required to fine-tune +some options. -You will find below build instructions for specific Linux +You will find below build instructions for specific GNU/Linux distributions. Distributions tagged by "SUPPORTED" are tested by Sébastien Jodogne. Distributions tagged by "CONTRIBUTED" come from Orthanc users. @@ -79,14 +80,15 @@ # sudo apt-get install build-essential unzip cmake mercurial \ uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \ - libgoogle-glog-dev libgtest-dev libpng-dev libjpeg-dev \ + libgtest-dev libpng-dev libjpeg-dev \ libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev \ libboost-all-dev libwrap0-dev libjsoncpp-dev libpugixml-dev # cmake -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_MONGOOSE=OFF \ - -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ -DDCMTK_LIBRARIES=dcmjpls \ + -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc Note: Have also a look at the official package: @@ -106,9 +108,9 @@ -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DUSE_SYSTEM_JSONCPP=OFF \ - -DUSE_SYSTEM_GOOGLE_LOG=OFF \ -DUSE_SYSTEM_PUGIXML=OFF \ - -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ + -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc @@ -123,9 +125,10 @@ libcharls-dev libjsoncpp-dev libpugixml-dev # cmake -DALLOW_DOWNLOADS=ON \ - -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \ + -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DDCMTK_LIBRARIES=dcmjpls \ + -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc @@ -134,7 +137,7 @@ ------------------------ # sudo yum install unzip make automake gcc gcc-c++ python cmake \ - boost-devel curl-devel dcmtk-devel glog-devel \ + boost-devel curl-devel dcmtk-devel \ gtest-devel libpng-devel libsqlite3x-devel libuuid-devel jpeg-devel \ mongoose-devel openssl-devel jsoncpp-devel lua-devel pugixml-devel @@ -144,6 +147,7 @@ # cmake "-DDCMTK_LIBRARIES=CharLS" \ -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc Note: Have also a look at the official package: @@ -155,11 +159,12 @@ ------------------------ # pkg install jsoncpp pugixml lua51 curl googletest dcmtk cmake jpeg \ - e2fsprogs-libuuid glog boost-libs sqlite3 python libiconv + e2fsprogs-libuuid boost-libs sqlite3 python libiconv # cmake -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_MONGOOSE=OFF \ -DDCMTK_LIBRARIES="dcmdsig;charls;dcmjpls" \ + -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc @@ -180,12 +185,13 @@ -DUSE_SYSTEM_DCMTK=OFF \ -DUSE_SYSTEM_GOOGLE_TEST=OFF \ -DUSE_SYSTEM_LIBJPEG=OFF \ + -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc -Other Linux distributions? --------------------------- +Other GNU/Linux distributions? +------------------------------ Please send us your build instructions (by a mail to s.jodogne@gmail.com)! @@ -203,7 +209,7 @@ Using ccache ============ -Under Linux, you also have the opportunity to use "ccache" to +Under GNU/Linux, you also have the opportunity to use "ccache" to dramatically decrease the compilation time when rebuilding Orthanc. This is especially useful for developers. To this end, you would use: diff -r 2b91363cc1d1 -r 497a637366b4 NEWS --- a/NEWS Thu Oct 29 11:25:45 2015 +0100 +++ b/NEWS Fri Oct 12 15:18:10 2018 +0200 @@ -1,21 +1,492 @@ Pending changes in the mainline =============================== -* Full indexation of the patient/study tags to speed up searches and C-Find -* Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive") -* "/tools/create-dicom": Support of binary tags encoded using data URI scheme -* "/tools/create-dicom": Support of hierarchical structures (creation of sequences) -* "/modify" can insert/modify sequences -* "/series/.../ordered-slices" to order the slices of a 2D+t or 3D image -* New URI "/tools/shutdown" to stop Orthanc from the REST API -* New URIs for attachments: ".../compress", ".../uncompress" and ".../is-compressed" + +General +------- + +* Possibility to restrict the allowed DICOM commands for each modality + +Orthanc Explorer +---------------- + +* The first screen of Orthanc Explorer is now a form to do studies lookups +* Support of large databases, by limiting the results to 100 patients or studies + +REST API +-------- + +* New URI: "/studies/.../merge" to merge a study +* New URI: "/studies/.../split" to split a study + +Maintenance +----------- + +* Executing a query/retrieve from the REST API now creates a job +* Fix: Closing DICOM associations after running query/retrieve from REST API + + +Version 1.4.2 (2018-09-20) +========================== + +General +------- + +* "OrthancPeers" configuration option now allows to specify HTTP headers +* New main DICOM tag: "ImageOrientationPatient" at the instance level +* New configuration options: + - "HttpVerbose" to debug outgoing HTTP connections + - "OverwriteInstances" to choose how duplicate SOPInstanceUID are handled + +Orthanc Explorer +---------------- + +* Query/retrieve: Added button for "DX" modality + +REST API +-------- + +* "/tools/reconstruct" to reconstruct the main DICOM tags, the JSON summary and + the metadata of all the instances stored in Orthanc. This is a slow operation! + +Plugins +------- + +* New primitives to access Orthanc peers from plugins +* New events in change callbacks: "UpdatedPeers" and "UpdatedModalities" +* New primitives to handle jobs from plugins: "OrthancPluginSubmitJob()" + and "OrthancPluginRegisterJobsUnserializer()" + +Lua +--- + +* IncomingWorklistRequestFilter() to filter incoming C-FIND worklist queries + +Maintenance +----------- + +* Fix "/series/.../ordered-slices" in the presence of non-parallel slices +* Fix incoming DICOM C-Store filtering for JPEG-LS transfer syntaxes +* Fix OrthancPluginHttpClient() to return the HTTP status on errors +* Fix HTTPS requests to sites using a certificate encrypted with ECDSA +* Fix handling of incoming C-FIND queries containing Generic Group Length (*, 0x0000) +* Fix issue 54 (quoting multipart answers), for OsiriX compatibility through DICOMweb +* Fix issue 98 (DCMTK configuration fails with GCC 6.4.0 on Alpine) +* Fix issue 99 (PamWriter test segfaults on alpine linux with gcc 6.4.0) + + +Version 1.4.1 (2018-07-17) +========================== + +* Fix deadlock in Lua scripting +* Simplification to the "DatabaseWrapper" class + + +Version 1.4.0 (2018-07-13) +========================== + +General +------- + +* New advanced job engine +* New configuration options: + - "ConcurrentJobs": Max number of jobs that are simultaneously running + - "SynchronousCMove": Whether to run DICOM C-Move operations synchronously + - "JobsHistorySize": Max number of completed jobs that are kept in memory +* New metadata automatically computed at the instance level: + "RemoteIp", "CalledAet" and "HttpUsername" + +Orthanc Explorer +---------------- + +* New screen listing all the available studies + +REST API +-------- + +* "/jobs/..." to manage the jobs from the REST API +* New option "?short" to list DICOM tags using their hexadecimal ID in: + - "/instances/.../tags?short" + - "/instances/.../header?short" + - "/{patients|studies|series}/.../instances-tags?short" + - "/{patients|studies|series}/.../shared-tags?short" + - "/{patients|studies|series|instances}/.../module?short" + - "/studies/.../module-patient?short" +* "/instances/.../tags" URI was returning only the first value of + DicomTags containing multiple numerical value. It now returns all + values in a string separated by \\ (i.e.: "1\\2\\3"). Note that, + for data already in Orthanc, you'll need to reconstruct the data by + sending a POST request to the ".../reconstruct" URI. This change + triggered an update of ORTHANC_API_VERSION from 1.0 to 1.1 +* "/instances/.../frame/../image-uint8 and friends now accepts a + "image/pam" MIME type to retrieve images in PAM format + (https://en.wikipedia.org/wiki/Netpbm#PAM_graphics_format) +* New option "?expand" to "/instances/.../metadata" + +Plugins +------- + +* New primitive in database SDK: "lookupIdentifierRange" to speed up range searches +* New function in the SDK: "OrthancPluginCheckVersionAdvanced()" + +Maintenance +----------- + +* Configuration option "LogExportedResources" is now "false" by default +* Header "OrthancCppDatabasePlugin.h" is now part of the "orthanc-databases" project +* Fix generation of DICOMDIR if PatientID is empty +* Fix issue 25 (Deadlock with Lua scripts): The event queue is now implemented for Lua +* Fix issue 94 (Instance modification should not modify FrameOfReferenceUID) +* Fix issue 77 (Lua access to REST-API is null terminated) +* Fix memory leak introduced by changeset #99116ed6f38c in Orthanc 1.3.2 +* Upgraded dependencies for static and Windows builds: + - boost 1.67.0 + - openssl 1.0.2o + + +Version 1.3.2 (2018-04-18) +========================== + +REST API +-------- + +* "/system" URI returns the version of the Orthanc REST API +* "/tools/now" returns the current UTC (universal) time +* "/tools/now-local" returns the curent local time. + This was the behavior of "/tools/now" until release 1.3.1. +* Added "?expand" GET argument to "/peers" and "/modalities" routes +* New URI: "/tools/create-media-extended" to generate a DICOMDIR + archive from several resources, including additional type-3 tags +* Preservation of UID relationships while anonymizing + +Lua +--- + +* New CMake option: "-DENABLE_LUA_MODULES=ON" to enable support for + loading external Lua modules if the Lua engine is statically linked + +Plugins +------- + +* New error code: DatabaseUnavailable + +Maintenance +----------- + +* Orthanc now uses UTC (universal time) instead of local time in its database +* Fix to allow creating DICOM instances with empty Specific Character Set (0008,0005) +* Support of Linux Standard Base +* Static linking against libuuid (from e2fsprogs) +* Fix static build on CentOS 6 +* Possibility of using JsonCpp 0.10.6 if the compiler does not support C++11 + with the "-DUSE_LEGACY_JSONCPP=ON" CMake option +* Upgraded dependencies for static and Windows builds: + - boost 1.66.0 + - curl 7.57.0 + - jsoncpp 1.8.4 + - zlib 1.2.11 + + +Version 1.3.1 (2017-11-29) +========================== + +General +------- + +* Built-in decoding of palette images + +REST API +-------- + +* New URI: "/instances/.../frames/.../raw.gz" to compress raw frames using gzip +* New argument "ignore-length" to force the inclusion of too long tags in JSON +* New argument "/.../media?extended" to include additional type-3 tags in DICOMDIR Plugins ------- -* New function "OrthancPluginRegisterErrorCode()" to declare custom error codes -* New function "OrthancPluginRegisterDictionaryTag()" to declare DICOM tags -* New "OrthancStarted" and "OrthancStopped" events in change callbacks +* New pixel formats exposed in SDK: BGRA32, Float32, Grayscale32, RGB48 + +Maintenance +----------- + +* Creation of ./Resources/CMake/OrthancFramework*.cmake to reuse the Orthanc + C++ framework in other projects +* New security-related options: "DicomAlwaysAllowEcho" +* Use "GBK" (frequently used in China) as an alias for "GB18030" +* Experimental support of actively maintained Civetweb to replace Mongoose 3.8 +* Fix issue 31 for good (create new modality types for Philips ADW, GE Xeleris, GE AWServer) +* Fix issue 64 (OpenBSD support) +* Fix static compilation of DCMTK 3.6.2 on Fedora +* Upgrade to Boost 1.65.1 in static builds +* Upgrade to SQLite amalgamation 3.21.0 in static builds + + +Version 1.3.0 (2017-07-19) +========================== + +General +------- + +* Orthanc now anonymizes according to Basic Profile of PS 3.15-2017c Table E.1-1 +* In the "DicomModalities" configuration: + - Manufacturer type MedInria is now obsolete + - Manufacturer types AgfaImpax and SyngoVia are obsolete too + (use GenericNoWildcardInDates instead) + - Obsolete manufacturers are still accepted but might disappear in the future + - Added new manufacturer: GenericNoUniversalWildcard to replace all '*' by '' in + outgoing C-Find requests +* New security-related options: "DicomAlwaysAllowStore" and "DicomCheckModalityHost" + +REST API +-------- + +* Argument "Since" in URI "/tools/find" (related to issue 53) +* Argument "DicomVersion" in URIs "/{...}/{...}/anonymization" + +Plugins +------- + +* New function: "OrthancPluginRegisterIncomingHttpRequestFilter2()" + +Lua +--- + +* Added HTTP headers support for Lua HttpPost/HttpGet/HttpPut/HttpDelete + +Orthanc Explorer +---------------- + +* Query/retrieve: Added button for "DR" modality + +Maintenance +----------- + +* Ability to retrieve raw frames encoded as unsigned 32-bits integers +* Fix issue 29 (more consistent handling of the "--upgrade" argument) +* Fix issue 31 (create new modality types for Philips ADW, GE Xeleris, GE AWServer) +* Fix issue 35 (AET name is not transferred to Orthanc using DCMTK 3.6.0) +* Fix issue 44 (bad interpretation of photometric interpretation MONOCHROME1) +* Fix issue 45 (crash when providing a folder to "--config" command-line option) +* Fix issue 46 (PHI remaining after anonymization) +* Fix issue 49 (worklists: accentuated characters are removed from C-Find responses) +* Fix issue 52 (DICOM level security association problems) +* Fix issue 55 (modification/anonymization of tags that might break the database + model now requires the "Force" parameter to be set to "true" in the query) +* Fix issue 56 (case-insensitive matching over accents) +* Fix Debian #865606 (orthanc FTBFS with libdcmtk-dev 3.6.1~20170228-2) +* Fix XSS inside DICOM in Orthanc Explorer (as reported by Victor Pasnkel, Morphus Labs) +* Upgrade to DCMTK 3.6.2 in static builds (released on 2017-07-17) +* Upgrade to Boost 1.64.0 in static builds +* New advanced "Locale" configuration option +* Removed configuration option "USE_DCMTK_361_PRIVATE_DIC" + + +Version 1.2.0 (2016/12/13) +========================== + +General +------- + +* Handling of private tags/creators in the "Dictionary" configuration option +* New configuration options: "LoadPrivateDictionary", "DicomScuTimeout" and "DicomScpTimeout" +* New metadata automatically computed at the instance level: "TransferSyntax" and "SopClassUid" + +REST API +-------- + +* "/tools/invalidate-tags" to invalidate the JSON summary of all the DICOM files + (useful if private tags are registered, or if changing the default encoding) +* "Permissive" flag for URI "/modalities/{...}/store" to ignore C-STORE transfer errors +* "Asynchronous" flag for URIs "/modalities/{...}/store" and "/peers/{...}/store" + to avoid waiting for the completion of image transfers +* Possibility to DELETE "dicom-as-json" attachments to reconstruct the JSON summaries + (useful if "Dictionary" has changed) +* "Keep" option for modifications to keep original DICOM identifiers (advanced feature) +* "/tools/default-encoding" to get or temporarily change the default encoding +* "/{resource}/{id}/reconstruct" to reconstruct the main DICOM tags, the JSON summary and + the metadata of a resource (useful to compute new metadata, or if using "Keep" above) + +Plugins +------- + +* New function: "OrthancPluginRegisterPrivateDictionaryTag()" to register private tags +* More control over client cache in the ServeFolders plugin +* New C++ help wrappers in "Plugins/Samples/Common/" to read DICOM datasets from REST API +* New data structure: "OrthancPluginFindMatcher" to match DICOM against C-FIND queries + +Maintenance +----------- + +* Fix handling of encodings in C-FIND requests (including for worklists) +* Use of DCMTK 3.6.1 dictionary of private tags in standalone builds +* Avoid hard crash if not enough memory (handling of std::bad_alloc) +* Improved robustness of Orthanc Explorer wrt. query/retrieve (maybe fix issue 24) +* Fix serious performance issue with C-FIND +* Fix extraction of the symbolic name of the private tags +* Performance warning if runtime debug assertions are turned on +* Improved robustness against files with no PatientID +* Upgrade to curl 7.50.3 for static and Windows builds +* Content-Type for JSON documents is now "application/json; charset=utf-8" +* Ignore "Group Length" tags in C-FIND queries +* Fix handling of worklist SCP with ReferencedStudySequence and ReferencedPatientSequence +* Fix handling of Move Originator AET and ID in C-MOVE SCP +* Fix vulnerability ZSL-2016-5379 "Unquoted Service Path Privilege Escalation" in the + Windows service +* Fix vulnerability ZSL-2016-5380 "Remote Memory Corruption Vulnerability" in DCMTK 3.6.0 + + +Version 1.1.0 (2016/06/27) +========================== + +General +------- + +* HTTPS client certificates can be associated with Orthanc peers to enhance security over Internet +* Possibility to use PKCS#11 authentication for hardware security modules with Orthanc peers +* New command-line option "--logfile" to output the Orthanc log to the given file +* Support of SIGHUP signal (restart Orthanc only if the configuration files have changed) + +REST API +-------- + +* New URI: "/instances/.../frames/.../raw" to access the raw frames (bypass image decoding) +* New URI "/modalities/.../move" to issue C-MOVE SCU requests +* "MoveOriginatorID" can be specified for "/modalities/.../store" + +Dicom protocol +-------------- + +* Support of optional tags for counting resources in C-FIND: + 0008-0061, 0008-0062, 0020-1200, 0020-1202, 0020-1204, 0020-1206, 0020-1208, 0020-1209 +* Support of Move Originator Message ID (0000,1031) in C-STORE responses driven by C-MOVE + +Plugins +------- + +* Speedup in plugins by removing unnecessary locks +* New callback to filter incoming HTTP requests: OrthancPluginRegisterIncomingHttpRequestFilter() +* New callback to handle non-worklists C-FIND requests: OrthancPluginRegisterFindCallback() +* New callback to handle C-MOVE requests: OrthancPluginRegisterMoveCallback() +* New function: "OrthancPluginHttpClient()" to do HTTP requests with full control +* New function: "OrthancPluginGenerateUuid()" to generate a UUID +* More than one custom image decoder can be installed (e.g. to handle different transfer syntaxes) + +Lua +--- + +* Possibility to dynamically fix outgoing C-FIND requests using "OutgoingFindRequestFilter()" +* Access to the HTTP headers in the "IncomingHttpRequestFilter()" callback + +Image decoding +-------------- + +* Huge speedup if decoding the family of JPEG transfer syntaxes +* Refactoring leading to speedups with custom image decoders (including Web viewer plugin) +* Support decoding of RLE Lossless transfer syntax +* Support of signed 16bpp images in ParsedDicomFile + +Maintenance +----------- + +* New logo of Orthanc +* Fix issue 11 (is_regular_file() fails for FILE_ATTRIBUTE_REPARSE_POINT) +* Fix issue 16 ("Limit" parameter error in REST API /tools/find method) +* Fix of Debian bug #818512 ("FTBFS: Please install libdcmtk*-dev") +* Fix of Debian bug #823139 ("orthanc: Please provide RecoverCompressedFile.cpp") +* Replaced "localhost" by "127.0.0.1", as it might impact performance on Windows +* Compatibility with CMake >= 3.5.0 +* Possibility to use forthcoming DCMTK 3.6.1 in static builds (instead of 3.6.0) +* Upgrade to Boost 1.60.0 for static builds +* Use of HTTP status 403 Forbidden (instead of 401) if access to a REST resource is disallowed +* Option "HttpsVerifyPeers" can be used to connect against self-signed HTTPS certificates +* New configuration option "AllowFindSopClassesInStudy" +* Macro "__linux" (now obsolete) replaced by macro "__linux__" (maybe solves Debian bug #821011) +* Modification of instances can now replace PixelData (resp. EncapsulatedDocument) with + provided a PNG/JPEG image (resp. PDF file) if it is encoded using Data URI Scheme +* Dropped support of Google Log + + +Version 1.0.0 (2015/12/15) +========================== + +* Lua: "IncomingFindRequestFilter()" to apply filters to incoming C-FIND requests +* New function in plugin SDK: "OrthancPluginSendMultipartItem2()" +* Fix of DICOMDIR generation with DCMTK 3.6.1, support of encodings +* Fix range search if the lower or upper limit is absent +* Fix modality worklists lookups if tags with UN (unknown) VR are present +* Warn about badly formatted modality/peer definitions in configuration file at startup + + +Version 0.9.6 (2015/12/08) +========================== + +* Promiscuous mode (accept unknown SOP class UID) is now turned off by default +* Fix serialization of DICOM buffers that might contain garbage trailing +* Fix modality worklists server if some fields are null +* More tolerant "/series/.../ordered-slices" with broken series +* Improved logging information if upgrade fails +* Fix formatting of multipart HTTP answers (bis) + + +Version 0.9.5 (2015/12/02) +========================== + +Major +----- + +* Experimental support of DICOM C-FIND SCP for modality worklists through plugins +* Support of DICOM C-FIND SCU for modality worklists ("/modalities/{dicom}/find-worklist") + +REST API +-------- + +* New URIs: + - "/series/.../ordered-slices" to order the slices of a 2D+t or 3D series + - "/tools/shutdown" to stop Orthanc from the REST API + - ".../compress", ".../uncompress" and ".../is-compressed" for attachments + - "/tools/create-archive" to create ZIP from a set of resources + - "/tools/create-media" to create ZIP+DICOMDIR from a set of resources + - "/instances/.../header" to get the meta information (header) of the DICOM instance +* "/tools/create-dicom": + - Support of binary tags encoded using data URI scheme + - Support of hierarchical structures (creation of sequences) + - Create tags with unknown VR +* "/modify" can insert/modify sequences +* ".../preview" and ".../image-uint8" can return JPEG images if the HTTP Accept Header asks so +* "Origin" metadata for the instances + +Minor +----- + +* New configuration options: + - "UnknownSopClassAccepted" to disable promiscuous mode (accept unknown SOP class UID) + - New configuration option: "Dictionary" to declare custom DICOM tags +* Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive") +* MIME content type can be associated to custom attachments (cf. "UserContentType") + +Plugins +------- + +* New functions: + - "OrthancPluginRegisterDecodeImageCallback()" to replace the built-in image decoder + - "OrthancPluginDicomInstanceToJson()" to convert DICOM to JSON + - "OrthancPluginDicomBufferToJson()" to convert DICOM to JSON + - "OrthancPluginRegisterErrorCode()" to declare custom error codes + - "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags + - "OrthancPluginLookupDictionary()" to get information about some DICOM tag + - "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API + - "OrthancPluginGetInstanceOrigin()" to know through which mechanism an instance was received + - "OrthancPluginCreateImage()" and "OrthancPluginCreateImageAccessor()" to create images + - "OrthancPluginDecodeDicomImage()" to decode DICOM images + - "OrthancPluginComputeMd5()" and "OrthancPluginComputeSha1()" to compute MD5/SHA-1 hash +* New events in change callbacks: + - "OrthancStarted" + - "OrthancStopped" + - "UpdatedAttachment" + - "UpdatedMetadata" +* "/system" URI gives information about the plugins used for storage area and DB back-end +* Plugin callbacks must now return explicit "OrthancPluginErrorCode" (instead of integers) Lua --- @@ -25,15 +496,17 @@ Maintenance ----------- -* Full refactoring of the searching features -* C-Move SCP for studies using AccessionNumber tag -* Fix issue 4 (C-Store Association not renegotiated on Specific-to-specific transfer syntax change) -* "/system" URI gives information about the plugins used for storage area and DB back-end -* Plugin callbacks should now return explicit "OrthancPluginErrorCode" instead of integers -* "/tools/create-dicom" can create tags with unknown VR +* Full indexation of the patient/study tags to speed up searches and C-FIND +* Many refactorings, notably of the searching features and of the image decoding +* C-MOVE SCP for studies using AccessionNumber tag +* Fix issue 4 (C-STORE Association not renegotiated on Specific-to-specific transfer syntax change) +* Fix formatting of multipart HTTP answers * "--logdir" flag creates a single log file instead of 3 separate files for errors/warnings/infos * "--errors" flag lists the error codes that could be returned by Orthanc * Under Windows, the exit status of Orthanc corresponds to the encountered error code +* New "AgfaImpax", "EFilm2" and "Vitrea" modality manufacturers +* C-FIND SCP will return tags with sequence value representation +* Upgrade to Boost 1.59.0 for static builds Version 0.9.4 (2015/09/16) @@ -77,7 +550,7 @@ * If error while calling the REST API, the answer body contains description of the error (this feature can be disabled with the "HttpDescribeErrors" option) * Upgrade to curl 7.44.0 for static and Windows builds -* Upgrade to libcurl 1.0.2d for static and Windows builds +* Upgrade to openssl 1.0.2d for static and Windows builds * Depends on libjpeg 9a * Bypass zlib uncompression if "StorageCompression" is enabled and HTTP client supports deflate @@ -108,7 +581,7 @@ ------- * The configuration can be splitted into several files stored inside the same folder -* Custom setting of the local AET during C-Store SCU (both in Lua and in the REST API) +* Custom setting of the local AET during C-STORE SCU (both in Lua and in the REST API) * Many code refactorings Lua @@ -127,9 +600,9 @@ Fixes ----- -* Fix compatibility issues for C-Find SCU to Siemens Syngo.Via modalities SCP +* Fix compatibility issues for C-FIND SCU to Siemens Syngo.Via modalities SCP * Fix issue 15 (Lua scripts making HTTP requests) -* Fix issue 35 (Characters in PatientID string are not protected for C-Find) +* Fix issue 35 (Characters in PatientID string are not protected for C-FIND) * Fix issue 37 (Hyphens trigger range query even if datatype does not support ranges) @@ -140,7 +613,7 @@ ----- * DICOM Query/Retrieve available from Orthanc Explorer -* C-Move SCU and C-Find SCU are accessible through the REST API +* C-MOVE SCU and C-FIND SCU are accessible through the REST API * "?expand" flag for URIs "/patients", "/studies" and "/series" * "/tools/find" URI to search for DICOM resources from REST * Support of FreeBSD @@ -150,15 +623,15 @@ ----- * Speed-up in Orthanc Explorer for large amount of images -* Speed-up of the C-Find SCP server of Orthanc +* Speed-up of the C-FIND SCP server of Orthanc * Allow replacing PatientID/StudyInstanceUID/SeriesInstanceUID from Lua scripts -* Option "CaseSensitivePN" to enable case-insensitive C-Find SCP +* Option "CaseSensitivePN" to enable case-insensitive C-FIND SCP Fixes ----- * Prevent freeze on C-FIND if no DICOM tag is to be returned -* Fix slow C-Store SCP on recent versions of Linux, if +* Fix slow C-STORE SCP on recent versions of GNU/Linux, if USE_SYSTEM_DCMTK is set to OFF (http://forum.dcmtk.org/viewtopic.php?f=1&t=4009) * Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052)) * Fix issue 32 (Cyrillic symbols): Introduction of the "Windows1251" encoding @@ -229,7 +702,7 @@ * "/instances-tags" to get the tags of all the child instances of a patient/study/series with a single REST call (bulk tags retrieval) -* Configuration/Lua to select the accepted C-Store SCP transfer syntaxes +* Configuration/Lua to select the accepted C-STORE SCP transfer syntaxes * Fix reporting of errors in Orthanc Explorer when sending images to peers/modalities * Installation of plugin SDK in CMake @@ -340,7 +813,7 @@ Version 0.7.5 (2014/05/08) ========================== -* Dynamic negotiation of SOP classes for C-Store SCU +* Dynamic negotiation of SOP classes for C-STORE SCU * Creation of DICOM instances using the REST API * Embedding of images within DICOM instances * Adding/removal/modification of remote modalities/peers through REST @@ -379,16 +852,16 @@ * Possibility to disable the HTTP server or the DICOM server * Automatic computation of MD5 hashes for the stored DICOM files * Maintenance tool to recover DICOM files compressed by Orthanc -* The newline characters in the configuration file are fixed for Linux -* Capture of the SIGTERM signal in Linux +* The newline characters in the configuration file are fixed for GNU/Linux +* Capture of the SIGTERM signal in GNU/Linux Version 0.7.2 (2013/11/08) ========================== * Support of Query/Retrieve from medInria -* Accept more transfer syntaxes for C-Store SCP and SCU (notably JPEG) -* Create the meta-header when receiving files through C-Store SCP +* Accept more transfer syntaxes for C-STORE SCP and SCU (notably JPEG) +* Create the meta-header when receiving files through C-STORE SCP * Fixes and improvements thanks to the static analyzer cppcheck @@ -431,7 +904,7 @@ ========================== * Detection of stable patients/studies/series -* C-Find SCU at the instance level +* C-FIND SCU at the instance level * Link from modified to original resource in Orthanc Explorer * Fix of issue #8 * Anonymization of the medical alerts tag (0010,2000) @@ -541,7 +1014,7 @@ * The patient/study/series/instances are now indexed by SHA-1 digests of their DICOM Instance IDs (and not by UUIDs anymore): The same DICOM objects are thus always identified by the same Orthanc IDs -* Log of exported instances through DICOM C-Store SCU ("/exported" URI) +* Log of exported instances through DICOM C-STORE SCU ("/exported" URI) * Full refactoring of the DB schema and of the REST API * Introduction of generic classes for REST APIs (in Core/RestApi) @@ -572,7 +1045,7 @@ Version 0.2.2 (2012/10/04) ========================== -* Switch to Google Logging +* Switch to Google Log * Fixes to Debian packaging diff -r 2b91363cc1d1 -r 497a637366b4 OrthancExplorer/explorer.html --- a/OrthancExplorer/explorer.html Thu Oct 29 11:25:45 2015 +0100 +++ b/OrthancExplorer/explorer.html Fri Oct 12 15:18:10 2018 +0200 @@ -34,25 +34,139 @@ -
    +
    -

    Find a patient

    - Plugins +

    Lookup studies

    +
    + Lookup + Plugins +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    +
    +
     
    +
    +
    +
    +
    +

    Warning:

    Your lookup led to many results! + Showing only ? studies to + avoid performance issue. Please make your query more + specific, then relaunch the lookup. +
    +
     
    +
    +
      +
    +
    +
    +
    + +
    +
    +

    All patients

    +
    + Lookup + Plugins +
    + +
    +
    +
    +
    +

    Warning:

    This is a large Orthanc server. Showing + only ? patients to avoid + performance issue. Make sure to use lookup if targeting + specific patients! +
    +
     
    +
    -
    +
    +
    +

    All studies

    +
    + Lookup + Plugins +
    + +
    +
    +
    +
    +

    Warning:

    This is a large Orthanc server. Showing + only ? studies to avoid + performance issue. Make sure to use lookup if targeting + specific studies! +
    +
     
    +
    +
      +
    +
    +
    + +

    Upload DICOM files

    - Patients +
    + Lookup + Plugins +
    @@ -78,10 +192,14 @@
    @@ -135,10 +253,14 @@ Patient » Study - Patients +
    + Lookup + Plugins +
    @@ -186,11 +308,14 @@ Study » Series - - Patients +
    + Lookup + Plugins +
    @@ -240,10 +365,14 @@ Series » Instance - Patients +
    + Lookup + Plugins +
    @@ -292,7 +421,10 @@

    Plugins

    - Patients +
    + Lookup + Plugins +