Mercurial > hg > orthanc
changeset 5350:17165a5540f6
integration Orthanc-1.12.0->mainline
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 04 Jul 2023 12:20:41 +0200 |
parents | 0223315871e8 (diff) ca6df6e6c7a6 (current diff) |
children | e2746201996a 65b4e6ae2703 |
files | |
diffstat | 97 files changed, 3198 insertions(+), 1170 deletions(-) [+] |
line wrap: on
line diff
--- a/LinuxCompilation.txt Tue May 16 07:09:06 2023 +0200 +++ b/LinuxCompilation.txt Tue Jul 04 12:20:41 2023 +0200 @@ -161,7 +161,7 @@ uuid-dev libcurl4-openssl-dev liblua5.3-dev \ libgtest-dev libpng-dev libsqlite3-dev libssl-dev libjpeg-dev \ zlib1g-dev libdcmtk-dev libboost-all-dev libwrap0-dev \ - libcharls-dev libjsoncpp-dev libpugixml-dev locales + libcharls-dev libjsoncpp-dev libpugixml-dev locales protobuf-compiler # cd ./Build # cmake -DALLOW_DOWNLOADS=ON \
--- a/NEWS Tue May 16 07:09:06 2023 +0200 +++ b/NEWS Tue Jul 04 12:20:41 2023 +0200 @@ -1,6 +1,47 @@ Pending changes in the mainline =============================== +General +------- + +* Orthanc now anonymizes according to Basic Profile of PS 3.15-2023b Table E.1-1 +* Added metrics: + - "orthanc_storage_read_bytes" + - "orthanc_storage_written_bytes" + - "orthanc_memory_trimming_duration_ms" + +REST API +-------- + +* API version upgraded to 21 +* "/tools/create-dicom" can now be used to create Encapsulated 3D + Manufacturing Model IODs (MTL, OBJ, or STL) +* Added a route to delete the output of an asynchronous job (right now + only for archive jobs): e.g. DELETE /jobs/../archive + +Plugins +------- + +* Added "OrthancPluginLoadDicomInstance()" to load DICOM instances from the database +* Added "OrthancPluginSetMetricsIntegerValue()" to track metrics with integer values + +Maintenance +----------- + +* Fix decoding of YBR_FULL RLE images for which the "Planar Configuration" + tag (0028,0006) equals 1 +* Made Orthanc more resilient to common spelling errors in SpecificCharacterSet +* Modality worklists plugin: Allow searching on private tags (exact match only) +* Fix orphan files remaining in storage when working with MaximumStorageSize + (https://discourse.orthanc-server.org/t/issue-with-deleting-incoming-dicoms-when-maximumstoragesize-is-reached/3510) +* When deleting a resource, the "LastUpdate" metadata of its parents are now updated +* Reduced the memory usage when downloading archives when "ZipLoaderThreads" > 0 +* Metrics can be stored either as floating-point numbers, or as integers +* Upgraded dependencies for static builds: + - boost 1.82.0 +* Reduce the frequency of memory trimming from 100ms to 30s to avoid high idle + CPU load (https://discourse.orthanc-server.org/t/onchange-callbacks-and-cpu-loads/3534). + Version 1.12.0 (2023-04-14) ===========================
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake Tue Jul 04 12:20:41 2023 +0200 @@ -90,10 +90,10 @@ ## Parameters for static compilation of Boost ## - set(BOOST_NAME boost_1_80_0) - set(BOOST_VERSION 1.80.0) - set(BOOST_BCP_SUFFIX bcpdigest-1.11.2) - set(BOOST_MD5 "7734e19f9a39a4411b807a9913e4a5ff") + set(BOOST_NAME boost_1_82_0) + set(BOOST_VERSION 1.82.0) + set(BOOST_BCP_SUFFIX bcpdigest-1.12.1) + set(BOOST_MD5 "9d02d026c61870b1838b53293692326f") set(BOOST_URL "https://orthanc.uclouvain.be/third-party-downloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz") set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME}) @@ -310,12 +310,14 @@ ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/date_time.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/formatting.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/generator.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/iconv_codecvt.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/ids.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/localization_backend.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/message.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/shared/mo_lambda.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/codecvt_converter.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/default_locale.cpp + ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/encoding.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/gregorian.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/info.cpp ${BOOST_SOURCES_DIR}/libs/locale/src/boost/locale/util/locale_data.cpp
--- a/OrthancFramework/Resources/CMake/BoostConfiguration.sh Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Resources/CMake/BoostConfiguration.sh Tue Jul 04 12:20:41 2023 +0200 @@ -22,10 +22,11 @@ ## - Orthanc between 1.4.0 and 1.4.2: Boost 1.67.0 ## - Orthanc between 1.5.0 and 1.5.4: Boost 1.68.0 ## - Orthanc between 1.5.5 and 1.11.1: Boost 1.69.0 -## - Orthanc >= 1.11.2: Boost 1.80.0 +## - Orthanc between 1.11.2 and 1.12.0: Boost 1.80.0 +## - Orthanc >= 1.12.1: Boost 1.82.0 -BOOST_VERSION=1_80_0 -ORTHANC_VERSION=1.11.2 +BOOST_VERSION=1_82_0 +ORTHANC_VERSION=1.12.1 rm -rf /tmp/boost_${BOOST_VERSION} rm -rf /tmp/bcp/boost_${BOOST_VERSION}
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Tue Jul 04 12:20:41 2023 +0200 @@ -154,6 +154,8 @@ set(ORTHANC_FRAMEWORK_MD5 "ede3de356493a8868545f8cb4b8bc8b5") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.3") set(ORTHANC_FRAMEWORK_MD5 "5c1b11009d782f248739919db6bf7f7a") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.0") + set(ORTHANC_FRAMEWORK_MD5 "d32a0cde03b6eb603d8dd2b33d38bf1b") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc
--- a/OrthancFramework/Resources/CMake/DownloadPackage.cmake Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Resources/CMake/DownloadPackage.cmake Tue Jul 04 12:20:41 2023 +0200 @@ -101,19 +101,26 @@ message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON") endif() - if ("${MD5}" STREQUAL "no-check") - message(WARNING "Not checking the MD5 of: ${Url}") - file(DOWNLOAD "${Url}" "${TMP_PATH}" - SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60 - STATUS Failure) - else() - file(DOWNLOAD "${Url}" "${TMP_PATH}" - SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60 - EXPECTED_MD5 "${MD5}" STATUS Failure) - endif() + foreach (retry RANGE 1 5) # Retries 5 times + if ("${MD5}" STREQUAL "no-check") + message(WARNING "Not checking the MD5 of: ${Url}") + file(DOWNLOAD "${Url}" "${TMP_PATH}" + SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10 + STATUS Failure) + else() + file(DOWNLOAD "${Url}" "${TMP_PATH}" + SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10 + EXPECTED_MD5 "${MD5}" STATUS Failure) + endif() - list(GET Failure 0 Status) + list(GET Failure 0 Status) + if (Status EQUAL 0) + break() # Successful download + endif() + endforeach() + if (NOT Status EQUAL 0) + file(REMOVE ${TMP_PATH}) message(FATAL_ERROR "Cannot download file: ${Url}") endif()
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Jul 04 12:20:41 2023 +0200 @@ -24,7 +24,7 @@ ##################################################################### # Version of the build, should always be "mainline" except in release branches -set(ORTHANC_VERSION "1.12.0") +set(ORTHANC_VERSION "mainline") # Version of the database schema. History: # * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning @@ -38,7 +38,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "20") +set(ORTHANC_API_VERSION "21") #####################################################################
--- a/OrthancFramework/Resources/Patches/boost-1.65.1-linux-standard-base.patch Tue May 16 07:09:06 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -diff -urEb boost_1_65_1.orig/boost/move/adl_move_swap.hpp boost_1_65_1/boost/move/adl_move_swap.hpp ---- boost_1_65_1.orig/boost/move/adl_move_swap.hpp 2017-11-08 17:43:20.000000000 +0100 -+++ boost_1_65_1/boost/move/adl_move_swap.hpp 2018-01-02 15:34:48.829052917 +0100 -@@ -28,6 +28,8 @@ - //Try to avoid including <algorithm>, as it's quite big - #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB) - #include <utility> //Dinkum libraries define std::swap in utility which is lighter than algorithm -+#elif defined(__LSB_VERSION__) -+# include <utility> - #elif defined(BOOST_GNU_STDLIB) - //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions - //use the good old stl_algobase header, which is quite lightweight
--- a/OrthancFramework/Resources/Patches/boost-1.66.0-linux-standard-base.patch Tue May 16 07:09:06 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -diff -urEb boost_1_66_0.orig/boost/move/adl_move_swap.hpp boost_1_66_0/boost/move/adl_move_swap.hpp ---- boost_1_66_0.orig/boost/move/adl_move_swap.hpp 2018-04-11 11:56:16.761768726 +0200 -+++ boost_1_66_0/boost/move/adl_move_swap.hpp 2018-04-11 11:57:01.073881330 +0200 -@@ -28,6 +28,8 @@ - //Try to avoid including <algorithm>, as it's quite big - #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB) - #include <utility> //Dinkum libraries define std::swap in utility which is lighter than algorithm -+#elif defined(__LSB_VERSION__) -+# include <utility> - #elif defined(BOOST_GNU_STDLIB) - //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions - //use the good old stl_algobase header, which is quite lightweight -Only in boost_1_66_0/boost/move: adl_move_swap.hpp~
--- a/OrthancFramework/Resources/Patches/boost-1.67.0-linux-standard-base.patch Tue May 16 07:09:06 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,74 +0,0 @@ -diff -urEb boost_1_67_0.orig/boost/move/adl_move_swap.hpp boost_1_67_0/boost/move/adl_move_swap.hpp ---- boost_1_67_0.orig/boost/move/adl_move_swap.hpp 2018-06-20 17:42:27.000000000 +0200 -+++ boost_1_67_0/boost/move/adl_move_swap.hpp 2018-10-12 14:27:41.368076902 +0200 -@@ -28,6 +28,8 @@ - //Try to avoid including <algorithm>, as it's quite big - #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB) - #include <utility> //Dinkum libraries define std::swap in utility which is lighter than algorithm -+#elif defined(__LSB_VERSION__) -+# include <utility> - #elif defined(BOOST_GNU_STDLIB) - //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions - //use the good old stl_algobase header, which is quite lightweight -diff -urEb boost_1_67_0.orig/boost/thread/detail/config.hpp boost_1_67_0/boost/thread/detail/config.hpp ---- boost_1_67_0.orig/boost/thread/detail/config.hpp 2018-06-20 17:42:27.000000000 +0200 -+++ boost_1_67_0/boost/thread/detail/config.hpp 2018-10-12 14:27:41.372076898 +0200 -@@ -417,6 +417,8 @@ - #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO - #elif defined(BOOST_THREAD_CHRONO_MAC_API) - #define BOOST_THREAD_HAS_MONO_CLOCK -+#elif defined(__LSB_VERSION__) || defined(__ANDROID__) -+ #define BOOST_THREAD_HAS_MONO_CLOCK - #else - #include <time.h> // check for CLOCK_MONOTONIC - #if defined(CLOCK_MONOTONIC) -diff -urEb boost_1_67_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_67_0/boost/type_traits/detail/has_postfix_operator.hpp ---- boost_1_67_0.orig/boost/type_traits/detail/has_postfix_operator.hpp 2018-06-20 17:42:27.000000000 +0200 -+++ boost_1_67_0/boost/type_traits/detail/has_postfix_operator.hpp 2018-10-12 14:31:27.539874170 +0200 -@@ -32,8 +32,11 @@ - namespace boost { - namespace detail { - -+// https://stackoverflow.com/a/15474269 -+#ifndef Q_MOC_RUN - // This namespace ensures that argument-dependent name lookup does not mess things up. - namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) { -+#endif - - // 1. a function to have an instance of type T without requiring T to be default - // constructible -@@ -181,7 +184,9 @@ - BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value)); - }; - -+#ifndef Q_MOC_RUN - } // namespace impl -+#endif - } // namespace detail - - // this is the accessible definition of the trait to end user -diff -urEb boost_1_67_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_67_0/boost/type_traits/detail/has_prefix_operator.hpp ---- boost_1_67_0.orig/boost/type_traits/detail/has_prefix_operator.hpp 2018-06-20 17:42:27.000000000 +0200 -+++ boost_1_67_0/boost/type_traits/detail/has_prefix_operator.hpp 2018-10-12 14:31:40.991862281 +0200 -@@ -45,8 +45,11 @@ - namespace boost { - namespace detail { - -+// https://stackoverflow.com/a/15474269 -+#ifndef Q_MOC_RUN - // This namespace ensures that argument-dependent name lookup does not mess things up. - namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) { -+#endif - - // 1. a function to have an instance of type T without requiring T to be default - // constructible -@@ -194,7 +197,9 @@ - BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value)); - }; - -+#ifndef Q_MOC_RUN - } // namespace impl -+#endif - } // namespace detail - - // this is the accessible definition of the trait to end user
--- a/OrthancFramework/Resources/Patches/boost-1.68.0-linux-standard-base.patch Tue May 16 07:09:06 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -diff -urEb boost_1_68_0.orig/boost/move/adl_move_swap.hpp boost_1_68_0/boost/move/adl_move_swap.hpp ---- boost_1_68_0.orig/boost/move/adl_move_swap.hpp 2018-11-13 16:08:32.214434915 +0100 -+++ boost_1_68_0/boost/move/adl_move_swap.hpp 2018-11-13 16:09:03.558399048 +0100 -@@ -28,6 +28,8 @@ - //Try to avoid including <algorithm>, as it's quite big - #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB) - #include <utility> //Dinkum libraries define std::swap in utility which is lighter than algorithm -+#elif defined(__LSB_VERSION__) -+# include <utility> - #elif defined(BOOST_GNU_STDLIB) - //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions - //use the good old stl_algobase header, which is quite lightweight -diff -urEb boost_1_68_0.orig/boost/thread/detail/config.hpp boost_1_68_0/boost/thread/detail/config.hpp ---- boost_1_68_0.orig/boost/thread/detail/config.hpp 2018-11-13 16:08:32.210434920 +0100 -+++ boost_1_68_0/boost/thread/detail/config.hpp 2018-11-13 16:10:03.386329911 +0100 -@@ -417,7 +417,7 @@ - #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO - #elif defined(BOOST_THREAD_CHRONO_MAC_API) - #define BOOST_THREAD_HAS_MONO_CLOCK --#elif defined(__ANDROID__) -+#elif defined(__LSB_VERSION__) || defined(__ANDROID__) - #define BOOST_THREAD_HAS_MONO_CLOCK - #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21 - #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO -diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp ---- boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp 2018-11-13 16:08:32.206434924 +0100 -+++ boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp 2018-11-13 16:11:08.374253901 +0100 -@@ -32,8 +32,11 @@ - namespace boost { - namespace detail { - -+// https://stackoverflow.com/a/15474269 -+#ifndef Q_MOC_RUN - // This namespace ensures that argument-dependent name lookup does not mess things up. - namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) { -+#endif - - // 1. a function to have an instance of type T without requiring T to be default - // constructible -@@ -181,7 +184,9 @@ - BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value)); - }; - -+#ifndef Q_MOC_RUN - } // namespace impl -+#endif - } // namespace detail - - // this is the accessible definition of the trait to end user -diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp ---- boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp 2018-11-13 16:08:32.206434924 +0100 -+++ boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp 2018-11-13 16:14:30.278012856 +0100 -@@ -45,8 +45,11 @@ - namespace boost { - namespace detail { - -+// https://stackoverflow.com/a/15474269 -+#ifndef Q_MOC_RUN - // This namespace ensures that argument-dependent name lookup does not mess things up. - namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) { -+#endif - - // 1. a function to have an instance of type T without requiring T to be default - // constructible -@@ -194,7 +197,10 @@ - BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value)); - }; - -+ -+#ifndef Q_MOC_RUN - } // namespace impl -+#endif - } // namespace detail - - // this is the accessible definition of the trait to end user -Only in boost_1_68_0/boost/type_traits/detail: has_prefix_operator.hpp~
--- a/OrthancFramework/Resources/Patches/civetweb-1.11.patch Tue May 16 07:09:06 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -diff -urEb civetweb-1.11.orig/include/civetweb.h civetweb-1.11/include/civetweb.h ---- civetweb-1.11.orig/include/civetweb.h 2019-01-17 21:09:41.844888908 +0100 -+++ civetweb-1.11/include/civetweb.h 2019-01-21 12:05:08.138998659 +0100 -@@ -1507,6 +1507,10 @@ - #endif - - -+// Added by SJ -+CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn); -+ -+ - #ifdef __cplusplus - } - #endif /* __cplusplus */ -diff -urEb civetweb-1.11.orig/src/civetweb.c civetweb-1.11/src/civetweb.c ---- civetweb-1.11.orig/src/civetweb.c 2019-01-17 21:09:41.852888857 +0100 -+++ civetweb-1.11/src/civetweb.c 2019-01-21 12:06:35.826868284 +0100 -@@ -59,6 +59,9 @@ - #if defined(__linux__) && !defined(_XOPEN_SOURCE) - #define _XOPEN_SOURCE 600 /* For flockfile() on Linux */ - #endif -+#if defined(__LSB_VERSION__) -+#define NEED_TIMEGM -+#endif - #if !defined(_LARGEFILE_SOURCE) - #define _LARGEFILE_SOURCE /* For fseeko(), ftello() */ - #endif -@@ -129,6 +132,12 @@ - - - /* Alternative queue is well tested and should be the new default */ -+#if defined(__LSB_VERSION__) -+/* Function "eventfd()" is not available in Linux Standard Base, can't -+ * use the alternative queue */ -+#define NO_ALTERNATIVE_QUEUE -+#endif -+ - #if defined(NO_ALTERNATIVE_QUEUE) - #if defined(ALTERNATIVE_QUEUE) - #error "Define ALTERNATIVE_QUEUE or NO_ALTERNATIVE_QUEUE or none, but not both" -@@ -536,6 +545,10 @@ - #if !defined(EWOULDBLOCK) - #define EWOULDBLOCK WSAEWOULDBLOCK - #endif /* !EWOULDBLOCK */ -+#if !defined(ECONNRESET) -+/* This macro is not defined e.g. in Visual Studio 2008 */ -+#define ECONNRESET WSAECONNRESET -+#endif /* !ECONNRESET */ - #define _POSIX_ - #define INT64_FMT "I64d" - #define UINT64_FMT "I64u" -@@ -2939,6 +2952,13 @@ - #endif - - -+#if defined(__LSB_VERSION__) -+static void -+mg_set_thread_name(const char *threadName) -+{ -+ /* prctl() does not seem to be available in Linux Standard Base */ -+} -+#else - static void - mg_set_thread_name(const char *name) - { -@@ -2980,6 +3000,7 @@ - (void)prctl(PR_SET_NAME, threadName, 0, 0, 0); - #endif - } -+#endif - #else /* !defined(NO_THREAD_NAME) */ - void - mg_set_thread_name(const char *threadName) -@@ -16919,6 +16940,10 @@ - /* Message is a valid request */ - - /* Is there a "host" ? */ -+ /* https://github.com/civetweb/civetweb/pull/675/commits/96e3e8c50acb4b8e0c946d02b5f880a3e62986e1 */ -+ if (conn->host!=NULL) { -+ mg_free((void *)conn->host); -+ } - conn->host = alloc_get_host(conn); - if (!conn->host) { - mg_snprintf(conn, -@@ -19857,4 +19882,13 @@ - } - - -+// Added by SJ -+void mg_disable_keep_alive(struct mg_connection *conn) -+{ -+ if (conn != NULL) { -+ conn->must_close = 1; -+ } -+} -+ -+ - /* End of civetweb.c */
--- a/OrthancFramework/Resources/Patches/civetweb-1.12.patch Tue May 16 07:09:06 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -diff -urEb civetweb-1.12.orig/include/civetweb.h civetweb-1.12/include/civetweb.h ---- civetweb-1.12.orig/include/civetweb.h 2020-10-06 12:39:10.634902843 +0200 -+++ civetweb-1.12/include/civetweb.h 2020-10-06 12:39:30.630872089 +0200 -@@ -1614,6 +1614,9 @@ - struct mg_error_data *error); - #endif - -+// Added by SJ -+CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn); -+ - #ifdef __cplusplus - } - #endif /* __cplusplus */ -diff -urEb civetweb-1.12.orig/src/civetweb.c civetweb-1.12/src/civetweb.c ---- civetweb-1.12.orig/src/civetweb.c 2020-10-06 12:39:10.638902837 +0200 -+++ civetweb-1.12/src/civetweb.c 2020-10-06 12:41:40.110671929 +0200 -@@ -10525,6 +10525,11 @@ - /* + MicroSoft extensions - * https://msdn.microsoft.com/en-us/library/aa142917.aspx */ - -+ /* Added by SJ, for write access to WebDAV on Windows >= 7 */ -+ {"LOCK", 1, 1, 0, 0, 0}, -+ {"UNLOCK", 1, 0, 0, 0, 0}, -+ {"PROPPATCH", 1, 1, 0, 0, 0}, -+ - /* REPORT method (RFC 3253) */ - {"REPORT", 1, 1, 1, 1, 1}, - /* REPORT method only allowed for CGI/Lua/LSP and callbacks. */ -@@ -20704,5 +20709,12 @@ - return 1; - } - -+// Added by SJ -+void mg_disable_keep_alive(struct mg_connection *conn) -+{ -+ if (conn != NULL) { -+ conn->must_close = 1; -+ } -+} - - /* End of civetweb.c */
--- a/OrthancFramework/Sources/Cache/SharedArchive.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/Cache/SharedArchive.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -102,7 +102,7 @@ std::string SharedArchive::Add(IDynamicObject* obj) { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (archive_.size() == maxSize_) { @@ -122,7 +122,7 @@ void SharedArchive::Remove(const std::string& id) { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); RemoveInternal(id); } @@ -132,7 +132,7 @@ items.clear(); { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); for (Archive::const_iterator it = archive_.begin(); it != archive_.end(); ++it)
--- a/OrthancFramework/Sources/Cache/SharedArchive.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/Cache/SharedArchive.h Tue Jul 04 12:20:41 2023 +0200 @@ -44,9 +44,9 @@ private: typedef std::map<std::string, IDynamicObject*> Archive; - size_t maxSize_; - boost::mutex mutex_; - Archive archive_; + size_t maxSize_; + boost::recursive_mutex mutex_; + Archive archive_; LeastRecentlyUsedIndex<std::string> lru_; void RemoveInternal(const std::string& id); @@ -55,8 +55,8 @@ class ORTHANC_PUBLIC Accessor : public boost::noncopyable { private: - boost::mutex::scoped_lock lock_; - IDynamicObject* item_; + boost::recursive_mutex::scoped_lock lock_; + IDynamicObject* item_; public: Accessor(SharedArchive& that,
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -86,7 +86,21 @@ { DicomTag t = elements_[i]->GetTag(); const DicomValue& v = elements_[i]->GetValue(); - std::string s = v.IsNull() ? "(null)" : v.GetContent(); + + std::string s; + if (v.IsNull()) + { + s = "(null)"; + } + else if (v.IsSequence()) + { + s = "(sequence)"; + } + else + { + s = v.GetContent(); + } + printf("0x%04x 0x%04x [%s]\n", t.GetGroup(), t.GetElement(), s.c_str()); } }
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -423,4 +423,46 @@ { return 256; } + + + ValueRepresentation DicomImageInformation::GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax, + unsigned int bitsAllocated) + { + /** + * This approach is validated in "Tests/GuessPixelDataVR.py": + * https://hg.orthanc-server.com/orthanc-tests/file/tip/Tests/GuessPixelDataVR.py + **/ + + if (transferSyntax == DicomTransferSyntax_LittleEndianExplicit || + transferSyntax == DicomTransferSyntax_BigEndianExplicit) + { + /** + * Same rules apply to Little Endian Explicit and Big Endian + * Explicit (now retired). The VR of the pixel data directly + * depends upon the "Bits Allocated (0028,0100)" tag: + * https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.2.html + * https://dicom.nema.org/medical/dicom/2016b/output/chtml/part05/sect_A.3.html + **/ + if (bitsAllocated > 8) + { + return ValueRepresentation_OtherWord; + } + else + { + return ValueRepresentation_OtherByte; + } + } + else if (transferSyntax == DicomTransferSyntax_LittleEndianImplicit) + { + // Assume "OW" for DICOM Implicit VR Little Endian Transfer Syntax + // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html#sect_A.1 + return ValueRepresentation_OtherWord; + } + else + { + // Assume "OB" for all the compressed transfer syntaxes + // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.html + return ValueRepresentation_OtherByte; + } + } }
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h Tue Jul 04 12:20:41 2023 +0200 @@ -95,5 +95,8 @@ * was implicitly used in Orthanc <= 1.7.2. **/ static unsigned int GetUsefulTagLength(); + + static ValueRepresentation GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax, + unsigned int bitsAllocated); }; }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -34,6 +34,7 @@ #include "../OrthancException.h" #include "../Toolbox.h" #include "DicomArray.h" +#include "DicomImageInformation.h" #if ORTHANC_ENABLE_DCMTK == 1 #include "../DicomParsing/FromDcmtkBridge.h" @@ -418,7 +419,7 @@ SetValueInternal(group, element, new DicomValue(str, isBinary)); } - void DicomMap::SetValue(const DicomTag& tag, const Json::Value& value) + void DicomMap::SetSequenceValue(const DicomTag& tag, const Json::Value& value) { SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue(value)); } @@ -1456,7 +1457,7 @@ } else { - SetValue(tag, value["Value"]); + SetSequenceValue(tag, value["Value"]); } } } @@ -1530,7 +1531,7 @@ { if (it->second->IsSequence()) { - result.SetValue(it->first, it->second->GetSequenceContent()); + result.SetSequenceValue(it->first, it->second->GetSequenceContent()); } } } @@ -1808,6 +1809,19 @@ } + ValueRepresentation DicomMap::GuessPixelDataValueRepresentation(DicomTransferSyntax transferSyntax) const + { + const DicomValue* value = TestAndGetValue(DICOM_TAG_BITS_ALLOCATED); + + uint32_t bitsAllocated; + if (value == NULL || + !value->ParseUnsignedInteger32(bitsAllocated)) + { + bitsAllocated = 8; + } + + return DicomImageInformation::GuessPixelDataValueRepresentation(transferSyntax, bitsAllocated); + } void DicomMap::Print(FILE* fp) const
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Tue Jul 04 12:20:41 2023 +0200 @@ -85,8 +85,8 @@ const std::string& str, bool isBinary); - void SetValue(const DicomTag& tag, - const Json::Value& value); + void SetSequenceValue(const DicomTag& tag, + const Json::Value& value); bool HasTag(uint16_t group, uint16_t element) const; @@ -230,6 +230,8 @@ void DumpMainDicomTags(Json::Value& target, ResourceType level) const; + ValueRepresentation GuessPixelDataValueRepresentation(DicomTransferSyntax transferSyntax) const; + void Print(FILE* fp) const; // For debugging only }; }
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -579,13 +579,17 @@ class DicomStreamReader::PixelDataVisitor : public DicomStreamReader::IVisitor { private: - bool hasPixelData_; - uint64_t pixelDataOffset_; + bool hasPixelData_; + uint64_t pixelDataOffset_; + ValueRepresentation pixelDataVR_; + DicomTransferSyntax transferSyntax_; public: PixelDataVisitor() : hasPixelData_(false), - pixelDataOffset_(0) + pixelDataOffset_(0), + pixelDataVR_(ValueRepresentation_Unknown), + transferSyntax_(DicomTransferSyntax_LittleEndianImplicit) // Default DICOM transfer syntax { } @@ -597,6 +601,7 @@ virtual void VisitTransferSyntax(DicomTransferSyntax transferSyntax) ORTHANC_OVERRIDE { + transferSyntax_ = transferSyntax; } virtual bool VisitDatasetTag(const DicomTag& tag, @@ -609,6 +614,23 @@ { hasPixelData_ = true; pixelDataOffset_ = fileOffset; + + if (transferSyntax_ == DicomTransferSyntax_LittleEndianImplicit) + { + // Implicit Little Endian has always "OW" VR for pixel data + // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html + pixelDataVR_ = ValueRepresentation_OtherWord; + } + else if (transferSyntax_ == DicomTransferSyntax_LittleEndianExplicit || + transferSyntax_ == DicomTransferSyntax_BigEndianExplicit) + { + pixelDataVR_ = vr; + } + else + { + // Compressed transfer syntaxes must always be OB + pixelDataVR_ = ValueRepresentation_OtherByte; + } } // Stop processing once pixel data has been passed @@ -625,7 +647,13 @@ return pixelDataOffset_; } + ValueRepresentation GetPixelDataVR() const + { + return pixelDataVR_; + } + static bool LookupPixelDataOffset(uint64_t& offset, + ValueRepresentation& vr, std::istream& stream) { PixelDataVisitor visitor; @@ -672,6 +700,7 @@ s[3] == char(0x00)) { offset = visitor.GetPixelDataOffset(); + vr = visitor.GetPixelDataVR(); return true; } else @@ -688,20 +717,22 @@ bool DicomStreamReader::LookupPixelDataOffset(uint64_t& offset, + ValueRepresentation& vr, const std::string& dicom) { std::stringstream stream(dicom); - return PixelDataVisitor::LookupPixelDataOffset(offset, stream); + return PixelDataVisitor::LookupPixelDataOffset(offset, vr, stream); } bool DicomStreamReader::LookupPixelDataOffset(uint64_t& offset, + ValueRepresentation& vr, const void* buffer, size_t size) { boost::iostreams::array_source source(reinterpret_cast<const char*>(buffer), size); boost::iostreams::stream<boost::iostreams::array_source> stream(source); - return PixelDataVisitor::LookupPixelDataOffset(offset, stream); + return PixelDataVisitor::LookupPixelDataOffset(offset, vr, stream); } }
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h Tue Jul 04 12:20:41 2023 +0200 @@ -128,10 +128,12 @@ uint64_t GetProcessedBytes() const; - static bool LookupPixelDataOffset(uint64_t& offset, + static bool LookupPixelDataOffset(uint64_t& offset /* out */, + ValueRepresentation& vr /* out */, const std::string& dicom); - static bool LookupPixelDataOffset(uint64_t& offset, + static bool LookupPixelDataOffset(uint64_t& offset /* out */, + ValueRepresentation& vr /* out */, const void* buffer, size_t size); };
--- a/OrthancFramework/Sources/DicomFormat/DicomValue.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -66,6 +66,10 @@ type_(Type_SequenceAsJson), sequenceJson_(value) { + if (value.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadParameterType); + } } const std::string& DicomValue::GetContent() const
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -43,6 +43,9 @@ static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2021b = "Orthanc " ORTHANC_VERSION " - PS 3.15-2021b Table E.1-1 Basic Profile"; +static const std::string ORTHANC_DEIDENTIFICATION_METHOD_2023b = + "Orthanc " ORTHANC_VERSION " - PS 3.15-2023b Table E.1-1 Basic Profile"; + namespace Orthanc { namespace @@ -427,7 +430,8 @@ if (it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2008 || it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2017c || - it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b) + it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2021b || + it->second->asString() == ORTHANC_DEIDENTIFICATION_METHOD_2023b) { delete it->second; replacements_.erase(it); @@ -831,9 +835,6 @@ * generated automatically by calling: * "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2021b/part15.xml" - * - * http://dicom.nema.org/medical/dicom/2021b/output/chtml/part15/chapter_E.html#table_E.1-1a - * http://dicom.nema.org/medical/dicom/2021b/output/chtml/part15/chapter_E.html#table_E.1-1 **/ #include "DicomModification_Anonymization2021b.impl.h" @@ -843,6 +844,26 @@ } + void DicomModification::SetupAnonymization2023b() + { + /** + * This is Table E.1-1 from PS 3.15-2023b (DICOM Part 15: Security + * and System Management Profiles), "basic profile" column. It was + * generated automatically by calling: + * "../../../OrthancServer/Resources/GenerateAnonymizationProfile.py + * https://raw.githubusercontent.com/jodogne/dicom-specification/master/2023b/part15.xml" + * + * http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1a + * http://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_E.html#table_E.1-1 + **/ + +#include "DicomModification_Anonymization2023b.impl.h" + + // Set the DeidentificationMethod tag + ReplaceInternal(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD_2023b); + } + + void DicomModification::SetupAnonymization(DicomVersion version) { isAnonymization_ = true; @@ -874,6 +895,10 @@ SetupAnonymization2021b(); break; + case DicomVersion_2023b: + SetupAnonymization2023b(); + break; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -1344,7 +1369,9 @@ // DicomVersion version = DicomVersion_2008; // For Orthanc <= 1.2.0 // DicomVersion version = DicomVersion_2017c; // For Orthanc between 1.3.0 and 1.9.3 - DicomVersion version = DicomVersion_2021b; // For Orthanc >= 1.9.4 + // DicomVersion version = DicomVersion_2021b; // For Orthanc >= 1.9.4 + DicomVersion version = DicomVersion_2023b; // For Orthanc >= 1.12.1 + if (request.isMember("DicomVersion")) { if (request["DicomVersion"].type() != Json::stringValue)
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h Tue Jul 04 12:20:41 2023 +0200 @@ -179,6 +179,8 @@ void SetupAnonymization2021b(); + void SetupAnonymization2023b(); + void UnserializeUidMap(ResourceType level, const Json::Value& serialized, const char* field);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2023b.impl.h Tue Jul 04 12:20:41 2023 +0200 @@ -0,0 +1,610 @@ +// RelationshipsVisitor handles (0x0008, 0x1140) /* X/Z/U* */ // Referenced Image Sequence +// RelationshipsVisitor handles (0x0008, 0x2112) /* X/Z/U* */ // Source Image Sequence +// Tag (0x0008, 0x0018) is set in Apply() /* U */ // SOP Instance UID +// 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 +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(0x0008, 0x0106)); /* D */ // Context Group Version +clearings_.insert(DicomTag(0x0008, 0x0107)); /* D */ // Context Group Local Version +clearings_.insert(DicomTag(0x0010, 0x0030)); // Patient's Birth Date +clearings_.insert(DicomTag(0x0010, 0x0040)); // Patient's Sex +clearings_.insert(DicomTag(0x0012, 0x0010)); /* D */ // Clinical Trial Sponsor Name +clearings_.insert(DicomTag(0x0012, 0x0020)); /* D */ // Clinical Trial Protocol ID +clearings_.insert(DicomTag(0x0012, 0x0021)); // Clinical Trial Protocol Name +clearings_.insert(DicomTag(0x0012, 0x0030)); // Clinical Trial Site ID +clearings_.insert(DicomTag(0x0012, 0x0031)); // Clinical Trial Site Name +clearings_.insert(DicomTag(0x0012, 0x0040)); /* D */ // Clinical Trial Subject ID +clearings_.insert(DicomTag(0x0012, 0x0042)); /* D */ // Clinical Trial Subject Reading ID +clearings_.insert(DicomTag(0x0012, 0x0050)); // Clinical Trial Time Point ID +clearings_.insert(DicomTag(0x0012, 0x0060)); // Clinical Trial Coordinating Center Name +clearings_.insert(DicomTag(0x0012, 0x0081)); /* D */ // Clinical Trial Protocol Ethics Committee Name +clearings_.insert(DicomTag(0x0018, 0x0010)); /* Z/D */ // Contrast/Bolus Agent +clearings_.insert(DicomTag(0x0018, 0x11bb)); /* D */ // Acquisition Field Of View Label +clearings_.insert(DicomTag(0x0018, 0x1203)); // Calibration DateTime +clearings_.insert(DicomTag(0x0018, 0x9074)); /* D */ // Frame Acquisition DateTime +clearings_.insert(DicomTag(0x0018, 0x9151)); /* D */ // Frame Reference DateTime +clearings_.insert(DicomTag(0x0018, 0x9367)); /* D */ // X-Ray Source ID +clearings_.insert(DicomTag(0x0018, 0x9369)); /* D */ // Source Start DateTime +clearings_.insert(DicomTag(0x0018, 0x936a)); /* D */ // Source End DateTime +clearings_.insert(DicomTag(0x0018, 0x9371)); /* D */ // X-Ray Detector ID +clearings_.insert(DicomTag(0x0018, 0x9623)); /* D */ // Functional Sync Pulse +clearings_.insert(DicomTag(0x0018, 0x9701)); /* D */ // Decay Correction DateTime +clearings_.insert(DicomTag(0x0018, 0x9804)); /* D */ // Exclusion Start DateTime +clearings_.insert(DicomTag(0x0018, 0x9919)); /* Z/D */ // Instruction Performed DateTime +clearings_.insert(DicomTag(0x0020, 0x0010)); // Study ID +clearings_.insert(DicomTag(0x0034, 0x0001)); /* D */ // Flow Identifier Sequence +clearings_.insert(DicomTag(0x0034, 0x0002)); /* D */ // Flow Identifier +clearings_.insert(DicomTag(0x0034, 0x0005)); /* D */ // Source Identifier +clearings_.insert(DicomTag(0x0034, 0x0007)); /* D */ // Frame Origin Timestamp +clearings_.insert(DicomTag(0x003a, 0x0314)); /* D */ // Impedance Measurement DateTime +clearings_.insert(DicomTag(0x0040, 0x0512)); /* D */ // Container Identifier +clearings_.insert(DicomTag(0x0040, 0x0513)); // Issuer of the Container Identifier Sequence +clearings_.insert(DicomTag(0x0040, 0x0551)); /* D */ // Specimen Identifier +clearings_.insert(DicomTag(0x0040, 0x0562)); // Issuer of the Specimen Identifier Sequence +clearings_.insert(DicomTag(0x0040, 0x0610)); // Specimen Preparation Sequence +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, 0xa027)); /* D */ // Verifying Organization +clearings_.insert(DicomTag(0x0040, 0xa030)); /* D */ // Verification DateTime +clearings_.insert(DicomTag(0x0040, 0xa073)); /* D */ // Verifying Observer Sequence +clearings_.insert(DicomTag(0x0040, 0xa075)); /* D */ // Verifying Observer Name +clearings_.insert(DicomTag(0x0040, 0xa082)); // Participation DateTime +clearings_.insert(DicomTag(0x0040, 0xa088)); // Verifying Observer Identification Code Sequence +clearings_.insert(DicomTag(0x0040, 0xa120)); /* D */ // DateTime +clearings_.insert(DicomTag(0x0040, 0xa121)); /* D */ // Date +clearings_.insert(DicomTag(0x0040, 0xa122)); /* D */ // Time +clearings_.insert(DicomTag(0x0040, 0xa123)); /* D */ // Person Name +clearings_.insert(DicomTag(0x0040, 0xa13a)); /* D */ // Referenced DateTime +clearings_.insert(DicomTag(0x0040, 0xa730)); /* D */ // Content Sequence +clearings_.insert(DicomTag(0x0042, 0x0011)); /* D */ // Encapsulated Document +clearings_.insert(DicomTag(0x0044, 0x0104)); /* D */ // Assertion DateTime +clearings_.insert(DicomTag(0x0068, 0x6226)); /* D */ // Effective DateTime +clearings_.insert(DicomTag(0x0068, 0x6270)); /* D */ // Information Issue DateTime +clearings_.insert(DicomTag(0x006a, 0x0003)); /* D */ // Annotation Group UID +clearings_.insert(DicomTag(0x006a, 0x0005)); /* D */ // Annotation Group Label +clearings_.insert(DicomTag(0x0070, 0x0001)); /* D */ // Graphic Annotation Sequence +clearings_.insert(DicomTag(0x0070, 0x0084)); /* Z/D */ // Content Creator's Name +clearings_.insert(DicomTag(0x0072, 0x000a)); /* D */ // Hanging Protocol Creation DateTime +clearings_.insert(DicomTag(0x0072, 0x005e)); /* D */ // Selector AE Value +clearings_.insert(DicomTag(0x0072, 0x005f)); /* D */ // Selector AS Value +clearings_.insert(DicomTag(0x0072, 0x0061)); /* D */ // Selector DA Value +clearings_.insert(DicomTag(0x0072, 0x0063)); /* D */ // Selector DT Value +clearings_.insert(DicomTag(0x0072, 0x0065)); /* D */ // Selector OB Value +clearings_.insert(DicomTag(0x0072, 0x0066)); /* D */ // Selector LO Value +clearings_.insert(DicomTag(0x0072, 0x0068)); /* D */ // Selector LT Value +clearings_.insert(DicomTag(0x0072, 0x006a)); /* D */ // Selector PN Value +clearings_.insert(DicomTag(0x0072, 0x006b)); /* D */ // Selector TM Value +clearings_.insert(DicomTag(0x0072, 0x006c)); /* D */ // Selector SH Value +clearings_.insert(DicomTag(0x0072, 0x006d)); /* D */ // Selector UN Value +clearings_.insert(DicomTag(0x0072, 0x006e)); /* D */ // Selector ST Value +clearings_.insert(DicomTag(0x0072, 0x0070)); /* D */ // Selector UT Value +clearings_.insert(DicomTag(0x0072, 0x0071)); /* D */ // Selector UR Value +clearings_.insert(DicomTag(0x0400, 0x0105)); /* D */ // Digital Signature DateTime +clearings_.insert(DicomTag(0x0400, 0x0115)); /* D */ // Certificate of Signer +clearings_.insert(DicomTag(0x0400, 0x0562)); /* D */ // Attribute Modification DateTime +clearings_.insert(DicomTag(0x0400, 0x0563)); /* D */ // Modifying System +clearings_.insert(DicomTag(0x0400, 0x0564)); // Source of Previous Values +clearings_.insert(DicomTag(0x0400, 0x0565)); /* D */ // Reason for the Attribute Modification +clearings_.insert(DicomTag(0x2100, 0x0140)); /* D */ // Destination AE +clearings_.insert(DicomTag(0x3006, 0x0002)); /* D */ // Structure Set Label +clearings_.insert(DicomTag(0x3006, 0x0008)); // Structure Set Date +clearings_.insert(DicomTag(0x3006, 0x0009)); // Structure Set Time +clearings_.insert(DicomTag(0x3006, 0x0026)); // ROI Name +clearings_.insert(DicomTag(0x3006, 0x00a6)); // ROI Interpreter +clearings_.insert(DicomTag(0x3008, 0x0024)); /* D */ // Treatment Control Point Date +clearings_.insert(DicomTag(0x3008, 0x0025)); /* D */ // Treatment Control Point Time +clearings_.insert(DicomTag(0x3008, 0x0162)); /* D */ // Safe Position Exit Date +clearings_.insert(DicomTag(0x3008, 0x0164)); /* D */ // Safe Position Exit Time +clearings_.insert(DicomTag(0x3008, 0x0166)); /* D */ // Safe Position Return Date +clearings_.insert(DicomTag(0x3008, 0x0168)); /* D */ // Safe Position Return Time +clearings_.insert(DicomTag(0x300a, 0x0002)); /* D */ // RT Plan Label +clearings_.insert(DicomTag(0x300a, 0x022c)); /* D */ // Source Strength Reference Date +clearings_.insert(DicomTag(0x300a, 0x022e)); /* D */ // Source Strength Reference Time +clearings_.insert(DicomTag(0x300a, 0x0608)); /* D */ // Treatment Position Group Label +clearings_.insert(DicomTag(0x300a, 0x0611)); // RT Accessory Holder Slot ID +clearings_.insert(DicomTag(0x300a, 0x0615)); // RT Accessory Device Slot ID +clearings_.insert(DicomTag(0x300a, 0x0619)); /* D */ // Radiation Dose Identification Label +clearings_.insert(DicomTag(0x300a, 0x0623)); /* D */ // Radiation Dose In-Vivo Measurement Label +clearings_.insert(DicomTag(0x300a, 0x062a)); /* D */ // RT Tolerance Set Label +clearings_.insert(DicomTag(0x300a, 0x067c)); /* D */ // Radiation Generation Mode Label +clearings_.insert(DicomTag(0x300a, 0x067d)); // Radiation Generation Mode Description +clearings_.insert(DicomTag(0x300a, 0x0734)); /* D */ // Treatment Tolerance Violation Description +clearings_.insert(DicomTag(0x300a, 0x0736)); /* D */ // Treatment Tolerance Violation DateTime +clearings_.insert(DicomTag(0x300a, 0x073a)); /* D */ // Recorded RT Control Point DateTime +clearings_.insert(DicomTag(0x300a, 0x0741)); /* D */ // Interlock DateTime +clearings_.insert(DicomTag(0x300a, 0x0742)); /* D */ // Interlock Description +clearings_.insert(DicomTag(0x300a, 0x0760)); /* D */ // Override DateTime +clearings_.insert(DicomTag(0x300a, 0x0783)); /* D */ // Interlock Origin Description +clearings_.insert(DicomTag(0x300c, 0x0127)); /* D */ // Beam Hold Transition DateTime +clearings_.insert(DicomTag(0x300e, 0x0004)); // Review Date +clearings_.insert(DicomTag(0x300e, 0x0005)); // Review Time +clearings_.insert(DicomTag(0x3010, 0x000f)); // Conceptual Volume Combination Description +clearings_.insert(DicomTag(0x3010, 0x0017)); // Conceptual Volume Description +clearings_.insert(DicomTag(0x3010, 0x001b)); // Device Alternate Identifier +clearings_.insert(DicomTag(0x3010, 0x002d)); /* D */ // Device Label +clearings_.insert(DicomTag(0x3010, 0x0033)); /* D */ // User Content Label +clearings_.insert(DicomTag(0x3010, 0x0034)); /* D */ // User Content Long Label +clearings_.insert(DicomTag(0x3010, 0x0035)); /* D */ // Entity Label +clearings_.insert(DicomTag(0x3010, 0x0038)); /* D */ // Entity Long Label +clearings_.insert(DicomTag(0x3010, 0x0043)); // Manufacturer's Device Identifier +clearings_.insert(DicomTag(0x3010, 0x0054)); /* D */ // RT Prescription Label +clearings_.insert(DicomTag(0x3010, 0x005a)); // RT Physician Intent Narrative +clearings_.insert(DicomTag(0x3010, 0x005c)); // Reason for Superseding +clearings_.insert(DicomTag(0x3010, 0x007a)); // Treatment Technique Notes +clearings_.insert(DicomTag(0x3010, 0x007b)); // Prescription Notes +clearings_.insert(DicomTag(0x3010, 0x007f)); // Fractionation Notes +clearings_.insert(DicomTag(0x3010, 0x0081)); // Prescription Notes Sequence +removals_.insert(DicomTag(0x0000, 0x1000)); // Affected SOP Instance UID +removals_.insert(DicomTag(0x0008, 0x0012)); /* X/D */ // Instance Creation Date +removals_.insert(DicomTag(0x0008, 0x0013)); /* X/Z/D */ // Instance Creation Time +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/Z/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, 0x0054)); // Retrieve AE Title +removals_.insert(DicomTag(0x0008, 0x0055)); // Station AE Title +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, 0x1000)); // Network ID +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, 0x1041)); // Institutional Department Type Code Sequence +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 Physician's 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 */ // Operator Identification Sequence +removals_.insert(DicomTag(0x0008, 0x1080)); // Admitting Diagnoses Description +removals_.insert(DicomTag(0x0008, 0x1084)); // Admitting Diagnoses Code Sequence +removals_.insert(DicomTag(0x0008, 0x1088)); // Pyramid Description +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, 0x2111)); // Derivation Description +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's Address +removals_.insert(DicomTag(0x0010, 0x1050)); // Insurance Plan Identification +removals_.insert(DicomTag(0x0010, 0x1060)); // Patient's Mother's Birth Name +removals_.insert(DicomTag(0x0010, 0x1080)); // Military Rank +removals_.insert(DicomTag(0x0010, 0x1081)); // Branch of Service +removals_.insert(DicomTag(0x0010, 0x1090)); // Medical Record Locator +removals_.insert(DicomTag(0x0010, 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 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's Sex Neutered +removals_.insert(DicomTag(0x0010, 0x2297)); // Responsible Person +removals_.insert(DicomTag(0x0010, 0x2299)); // Responsible Organization +removals_.insert(DicomTag(0x0010, 0x4000)); // Patient Comments +removals_.insert(DicomTag(0x0012, 0x0051)); // Clinical Trial Time Point Description +removals_.insert(DicomTag(0x0012, 0x0071)); // Clinical Trial Series ID +removals_.insert(DicomTag(0x0012, 0x0072)); // Clinical Trial Series Description +removals_.insert(DicomTag(0x0012, 0x0082)); // Clinical Trial Protocol Ethics Committee Approval Number +removals_.insert(DicomTag(0x0012, 0x0086)); // Ethics Committee Approval Effectiveness Start Date +removals_.insert(DicomTag(0x0012, 0x0087)); // Ethics Committee Approval Effectiveness End Date +removals_.insert(DicomTag(0x0014, 0x407c)); // Calibration Time +removals_.insert(DicomTag(0x0014, 0x407e)); // Calibration Date +removals_.insert(DicomTag(0x0016, 0x002b)); // Maker Note +removals_.insert(DicomTag(0x0016, 0x004b)); // Device Setting Description +removals_.insert(DicomTag(0x0016, 0x004d)); // Camera Owner Name +removals_.insert(DicomTag(0x0016, 0x004e)); // Lens Specification +removals_.insert(DicomTag(0x0016, 0x004f)); // Lens Make +removals_.insert(DicomTag(0x0016, 0x0050)); // Lens Model +removals_.insert(DicomTag(0x0016, 0x0051)); // Lens Serial Number +removals_.insert(DicomTag(0x0016, 0x0070)); // GPS Version ID +removals_.insert(DicomTag(0x0016, 0x0071)); // GPS Latitude Ref +removals_.insert(DicomTag(0x0016, 0x0072)); // GPS Latitude +removals_.insert(DicomTag(0x0016, 0x0073)); // GPS Longitude Ref +removals_.insert(DicomTag(0x0016, 0x0074)); // GPS Longitude +removals_.insert(DicomTag(0x0016, 0x0075)); // GPS Altitude Ref +removals_.insert(DicomTag(0x0016, 0x0076)); // GPS Altitude +removals_.insert(DicomTag(0x0016, 0x0077)); // GPS Time Stamp +removals_.insert(DicomTag(0x0016, 0x0078)); // GPS Satellites +removals_.insert(DicomTag(0x0016, 0x0079)); // GPS Status +removals_.insert(DicomTag(0x0016, 0x007a)); // GPS Measure Mode +removals_.insert(DicomTag(0x0016, 0x007b)); // GPS DOP +removals_.insert(DicomTag(0x0016, 0x007c)); // GPS Speed Ref +removals_.insert(DicomTag(0x0016, 0x007d)); // GPS Speed +removals_.insert(DicomTag(0x0016, 0x007e)); // GPS Track Ref +removals_.insert(DicomTag(0x0016, 0x007f)); // GPS Track +removals_.insert(DicomTag(0x0016, 0x0080)); // GPS Img Direction Ref +removals_.insert(DicomTag(0x0016, 0x0081)); // GPS Img Direction +removals_.insert(DicomTag(0x0016, 0x0082)); // GPS Map Datum +removals_.insert(DicomTag(0x0016, 0x0083)); // GPS Dest Latitude Ref +removals_.insert(DicomTag(0x0016, 0x0084)); // GPS Dest Latitude +removals_.insert(DicomTag(0x0016, 0x0085)); // GPS Dest Longitude Ref +removals_.insert(DicomTag(0x0016, 0x0086)); // GPS Dest Longitude +removals_.insert(DicomTag(0x0016, 0x0087)); // GPS Dest Bearing Ref +removals_.insert(DicomTag(0x0016, 0x0088)); // GPS Dest Bearing +removals_.insert(DicomTag(0x0016, 0x0089)); // GPS Dest Distance Ref +removals_.insert(DicomTag(0x0016, 0x008a)); // GPS Dest Distance +removals_.insert(DicomTag(0x0016, 0x008b)); // GPS Processing Method +removals_.insert(DicomTag(0x0016, 0x008c)); // GPS Area Information +removals_.insert(DicomTag(0x0016, 0x008d)); // GPS Date Stamp +removals_.insert(DicomTag(0x0016, 0x008e)); // GPS Differential +removals_.insert(DicomTag(0x0018, 0x0027)); // Intervention Drug Stop Time +removals_.insert(DicomTag(0x0018, 0x0035)); // Intervention Drug Start Time +removals_.insert(DicomTag(0x0018, 0x1000)); /* X/Z/D */ // Device Serial Number +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, 0x1009)); // Unique Device Identifier +removals_.insert(DicomTag(0x0018, 0x100a)); // UDI Sequence +removals_.insert(DicomTag(0x0018, 0x1012)); // Date of Secondary Capture +removals_.insert(DicomTag(0x0018, 0x1014)); // Time of Secondary Capture +removals_.insert(DicomTag(0x0018, 0x1030)); /* X/D */ // Protocol Name +removals_.insert(DicomTag(0x0018, 0x1042)); // Contrast/Bolus Start Time +removals_.insert(DicomTag(0x0018, 0x1043)); // Contrast/Bolus Stop Time +removals_.insert(DicomTag(0x0018, 0x1072)); // Radiopharmaceutical Start Time +removals_.insert(DicomTag(0x0018, 0x1073)); // Radiopharmaceutical Stop Time +removals_.insert(DicomTag(0x0018, 0x1078)); // Radiopharmaceutical Start DateTime +removals_.insert(DicomTag(0x0018, 0x1079)); // Radiopharmaceutical Stop DateTime +removals_.insert(DicomTag(0x0018, 0x1200)); // Date of Last Calibration +removals_.insert(DicomTag(0x0018, 0x1201)); // Time of Last Calibration +removals_.insert(DicomTag(0x0018, 0x1202)); // DateTime of Last Calibration +removals_.insert(DicomTag(0x0018, 0x1400)); /* X/D */ // Acquisition Device Processing Description +removals_.insert(DicomTag(0x0018, 0x4000)); // Acquisition Comments +removals_.insert(DicomTag(0x0018, 0x5011)); // Transducer Identification Sequence +removals_.insert(DicomTag(0x0018, 0x700a)); /* X/D */ // Detector ID +removals_.insert(DicomTag(0x0018, 0x700c)); /* X/D */ // Date of Last Detector Calibration +removals_.insert(DicomTag(0x0018, 0x700e)); /* X/D */ // Time of Last Detector Calibration +removals_.insert(DicomTag(0x0018, 0x9185)); // Respiratory Motion Compensation Technique Description +removals_.insert(DicomTag(0x0018, 0x9373)); // X-Ray Detector Label +removals_.insert(DicomTag(0x0018, 0x937b)); // Multi-energy Acquisition Description +removals_.insert(DicomTag(0x0018, 0x937f)); // Decomposition Description +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, 0x9937)); // Requested Series Description +removals_.insert(DicomTag(0x0018, 0xa002)); // Contribution DateTime +removals_.insert(DicomTag(0x0018, 0xa003)); // Contribution Description +removals_.insert(DicomTag(0x0020, 0x0027)); // Pyramid Label +removals_.insert(DicomTag(0x0020, 0x3401)); // Modifying Device ID +removals_.insert(DicomTag(0x0020, 0x3403)); // Modified Image Date +removals_.insert(DicomTag(0x0020, 0x3405)); // Modified Image Time +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(0x0028, 0x4000)); // Image Presentation Comments +removals_.insert(DicomTag(0x0032, 0x0012)); // Study ID Issuer +removals_.insert(DicomTag(0x0032, 0x0032)); // Study Verified Date +removals_.insert(DicomTag(0x0032, 0x0033)); // Study Verified Time +removals_.insert(DicomTag(0x0032, 0x0034)); // Study Read Date +removals_.insert(DicomTag(0x0032, 0x0035)); // Study Read Time +removals_.insert(DicomTag(0x0032, 0x1000)); // Scheduled Study Start Date +removals_.insert(DicomTag(0x0032, 0x1001)); // Scheduled Study Start Time +removals_.insert(DicomTag(0x0032, 0x1010)); // Scheduled Study Stop Date +removals_.insert(DicomTag(0x0032, 0x1011)); // Scheduled Study Stop Time +removals_.insert(DicomTag(0x0032, 0x1020)); // Scheduled Study Location +removals_.insert(DicomTag(0x0032, 0x1021)); // Scheduled Study Location AE Title +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, 0x1040)); // Study Arrival Date +removals_.insert(DicomTag(0x0032, 0x1041)); // Study Arrival Time +removals_.insert(DicomTag(0x0032, 0x1050)); // Study Completion Date +removals_.insert(DicomTag(0x0032, 0x1051)); // Study Completion Time +removals_.insert(DicomTag(0x0032, 0x1060)); /* X/Z */ // Requested Procedure Description +removals_.insert(DicomTag(0x0032, 0x1066)); // Reason for Visit +removals_.insert(DicomTag(0x0032, 0x1067)); // Reason for Visit Code Sequence +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, 0x0014)); // Issuer of Admission ID Sequence +removals_.insert(DicomTag(0x0038, 0x001a)); // Scheduled Admission Date +removals_.insert(DicomTag(0x0038, 0x001b)); // Scheduled Admission Time +removals_.insert(DicomTag(0x0038, 0x001c)); // Scheduled Discharge Date +removals_.insert(DicomTag(0x0038, 0x001d)); // Scheduled Discharge Time +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, 0x0030)); // Discharge Date +removals_.insert(DicomTag(0x0038, 0x0032)); // Discharge 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, 0x0064)); // Issuer of Service Episode ID Sequence +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(0x003a, 0x0329)); // Waveform Filter Description +removals_.insert(DicomTag(0x003a, 0x032b)); // Filter Lookup Table Description +removals_.insert(DicomTag(0x0040, 0x0001)); // Scheduled Station AE Title +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's Name +removals_.insert(DicomTag(0x0040, 0x0007)); // Scheduled Procedure Step Description +removals_.insert(DicomTag(0x0040, 0x0009)); // Scheduled Procedure Step ID +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, 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, 0x0310)); // Comments on Radiation Dose +removals_.insert(DicomTag(0x0040, 0x050a)); // Specimen Accession Number +removals_.insert(DicomTag(0x0040, 0x051a)); // Container Description +removals_.insert(DicomTag(0x0040, 0x0555)); /* X/Z */ // Acquisition Context Sequence +removals_.insert(DicomTag(0x0040, 0x0600)); // Specimen Short Description +removals_.insert(DicomTag(0x0040, 0x0602)); // Specimen Detailed Description +removals_.insert(DicomTag(0x0040, 0x06fa)); // Slide Identifier +removals_.insert(DicomTag(0x0040, 0x1001)); // Requested Procedure ID +removals_.insert(DicomTag(0x0040, 0x1002)); // Reason for the Requested Procedure +removals_.insert(DicomTag(0x0040, 0x1004)); // Patient Transport Arrangements +removals_.insert(DicomTag(0x0040, 0x1005)); // Requested Procedure Location +removals_.insert(DicomTag(0x0040, 0x100a)); // Reason for Requested Procedure Code Sequence +removals_.insert(DicomTag(0x0040, 0x1010)); // Names of Intended Recipients of Results +removals_.insert(DicomTag(0x0040, 0x1011)); // Intended Recipients of Results Identification Sequence +removals_.insert(DicomTag(0x0040, 0x1102)); // Person's 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, 0x2004)); // Issue Date of Imaging Service Request +removals_.insert(DicomTag(0x0040, 0x2005)); // Issue Time of Imaging Service Request +removals_.insert(DicomTag(0x0040, 0x2008)); // Order Entered By +removals_.insert(DicomTag(0x0040, 0x2009)); // Order Enterer's 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, 0x4008)); // Scheduled Procedure Step Expiration DateTime +removals_.insert(DicomTag(0x0040, 0x4010)); // Scheduled Procedure Step Modification DateTime +removals_.insert(DicomTag(0x0040, 0x4011)); // Expected Completion DateTime +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 Performer's Organization +removals_.insert(DicomTag(0x0040, 0x4037)); // Human Performer's 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, 0xa023)); // Findings Group Recording Date (Trial) +removals_.insert(DicomTag(0x0040, 0xa024)); // Findings Group Recording Time (Trial) +removals_.insert(DicomTag(0x0040, 0xa032)); /* X/D */ // Observation DateTime +removals_.insert(DicomTag(0x0040, 0xa033)); // Observation Start DateTime +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, 0xa110)); // Date of Document or Verbal Transaction (Trial) +removals_.insert(DicomTag(0x0040, 0xa112)); // Time of Document or Verbal Transaction (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, 0xdb06)); // Template Version +removals_.insert(DicomTag(0x0040, 0xdb07)); // Template Local Version +removals_.insert(DicomTag(0x0040, 0xe004)); // HL7 Document Effective Time +removals_.insert(DicomTag(0x0044, 0x0004)); // Approval Status DateTime +removals_.insert(DicomTag(0x0044, 0x000b)); // Product Expiration DateTime +removals_.insert(DicomTag(0x0044, 0x0010)); // Substance Administration DateTime +removals_.insert(DicomTag(0x0044, 0x0105)); // Assertion Expiration DateTime +removals_.insert(DicomTag(0x0050, 0x001b)); // Container Component ID +removals_.insert(DicomTag(0x0050, 0x0020)); // Device Description +removals_.insert(DicomTag(0x0050, 0x0021)); // Long Device Description +removals_.insert(DicomTag(0x006a, 0x0006)); // Annotation Group Description +removals_.insert(DicomTag(0x0070, 0x0082)); // Presentation Creation Date +removals_.insert(DicomTag(0x0070, 0x0083)); // Presentation Creation Time +removals_.insert(DicomTag(0x0070, 0x0086)); // Content Creator's Identification Code Sequence +removals_.insert(DicomTag(0x0074, 0x1234)); // Receiving AE +removals_.insert(DicomTag(0x0074, 0x1236)); // Requesting AE +removals_.insert(DicomTag(0x0088, 0x0200)); // Icon Image Sequence +removals_.insert(DicomTag(0x0088, 0x0904)); // Topic Title +removals_.insert(DicomTag(0x0088, 0x0906)); // Topic Subject +removals_.insert(DicomTag(0x0088, 0x0910)); // Topic Author +removals_.insert(DicomTag(0x0088, 0x0912)); // Topic Keywords +removals_.insert(DicomTag(0x0100, 0x0420)); // SOP Authorization DateTime +removals_.insert(DicomTag(0x0400, 0x0310)); // Certified Timestamp +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, 0x0551)); // Nonconforming Modified Attributes Sequence +removals_.insert(DicomTag(0x0400, 0x0552)); // Nonconforming Data Element Value +removals_.insert(DicomTag(0x0400, 0x0561)); // Original Attributes Sequence +removals_.insert(DicomTag(0x0400, 0x0600)); // Instance Origin Status +removals_.insert(DicomTag(0x2030, 0x0020)); // Text String +removals_.insert(DicomTag(0x2100, 0x0040)); // Creation Date +removals_.insert(DicomTag(0x2100, 0x0050)); // Creation Time +removals_.insert(DicomTag(0x2100, 0x0070)); // Originator +removals_.insert(DicomTag(0x2200, 0x0002)); /* X/Z */ // Label Text +removals_.insert(DicomTag(0x2200, 0x0005)); /* X/Z */ // Barcode Value +removals_.insert(DicomTag(0x3002, 0x0121)); // Position Acquisition Template Name +removals_.insert(DicomTag(0x3002, 0x0123)); // Position Acquisition Template Description +removals_.insert(DicomTag(0x3006, 0x0004)); // Structure Set Name +removals_.insert(DicomTag(0x3006, 0x0006)); // Structure Set Description +removals_.insert(DicomTag(0x3006, 0x0028)); // ROI Description +removals_.insert(DicomTag(0x3006, 0x0038)); // ROI Generation Description +removals_.insert(DicomTag(0x3006, 0x0085)); // ROI Observation Label +removals_.insert(DicomTag(0x3006, 0x0088)); // ROI Observation Description +removals_.insert(DicomTag(0x3008, 0x0054)); /* X/D */ // First Treatment Date +removals_.insert(DicomTag(0x3008, 0x0056)); /* X/D */ // Most Recent Treatment Date +removals_.insert(DicomTag(0x3008, 0x0105)); /* X/Z */ // Source Serial Number +removals_.insert(DicomTag(0x3008, 0x0250)); /* X/D */ // Treatment Date +removals_.insert(DicomTag(0x3008, 0x0251)); /* X/D */ // Treatment Time +removals_.insert(DicomTag(0x300a, 0x0003)); // RT Plan Name +removals_.insert(DicomTag(0x300a, 0x0004)); // RT Plan Description +removals_.insert(DicomTag(0x300a, 0x0006)); /* X/D */ // RT Plan Date +removals_.insert(DicomTag(0x300a, 0x0007)); /* X/D */ // RT Plan Time +removals_.insert(DicomTag(0x300a, 0x000b)); // Treatment Sites +removals_.insert(DicomTag(0x300a, 0x000e)); // Prescription Description +removals_.insert(DicomTag(0x300a, 0x0016)); // Dose Reference Description +removals_.insert(DicomTag(0x300a, 0x0072)); // Fraction Group Description +removals_.insert(DicomTag(0x300a, 0x00b2)); /* X/Z */ // Treatment Machine Name +removals_.insert(DicomTag(0x300a, 0x00c3)); // Beam Description +removals_.insert(DicomTag(0x300a, 0x00dd)); // Bolus Description +removals_.insert(DicomTag(0x300a, 0x0196)); // Fixation Device Description +removals_.insert(DicomTag(0x300a, 0x01a6)); // Shielding Device Description +removals_.insert(DicomTag(0x300a, 0x01b2)); // Setup Technique Description +removals_.insert(DicomTag(0x300a, 0x0216)); // Source Manufacturer +removals_.insert(DicomTag(0x300a, 0x02eb)); // Compensator Description +removals_.insert(DicomTag(0x300a, 0x0676)); // Equipment Frame of Reference Description +removals_.insert(DicomTag(0x300a, 0x078e)); // Patient Treatment Preparation Procedure Parameter Description +removals_.insert(DicomTag(0x300a, 0x0792)); // Patient Treatment Preparation Method Description +removals_.insert(DicomTag(0x300a, 0x0794)); // Patient Setup Photo Description +removals_.insert(DicomTag(0x300a, 0x079a)); // Displacement Reference Label +removals_.insert(DicomTag(0x300c, 0x0113)); // Reason for Omission Description +removals_.insert(DicomTag(0x300e, 0x0008)); /* X/Z */ // Reviewer Name +removals_.insert(DicomTag(0x3010, 0x0036)); // Entity Name +removals_.insert(DicomTag(0x3010, 0x0037)); // Entity Description +removals_.insert(DicomTag(0x3010, 0x004c)); /* X/D */ // Intended Phase Start Date +removals_.insert(DicomTag(0x3010, 0x004d)); /* X/D */ // Intended Phase End Date +removals_.insert(DicomTag(0x3010, 0x0056)); /* X/D */ // RT Treatment Approach Label +removals_.insert(DicomTag(0x3010, 0x0061)); // Prior Treatment Dose Description +removals_.insert(DicomTag(0x3010, 0x0077)); /* X/D */ // Treatment Site +removals_.insert(DicomTag(0x3010, 0x0085)); // Intended Fraction Start Time +removals_.insert(DicomTag(0x4000, 0x0010)); // Arbitrary +removals_.insert(DicomTag(0x4000, 0x4000)); // Text Comments +removals_.insert(DicomTag(0x4008, 0x0040)); // Results ID +removals_.insert(DicomTag(0x4008, 0x0042)); // Results ID Issuer +removals_.insert(DicomTag(0x4008, 0x0100)); // Interpretation Recorded Date +removals_.insert(DicomTag(0x4008, 0x0101)); // Interpretation Recorded Time +removals_.insert(DicomTag(0x4008, 0x0102)); // Interpretation Recorder +removals_.insert(DicomTag(0x4008, 0x0108)); // Interpretation Transcription Date +removals_.insert(DicomTag(0x4008, 0x0109)); // Interpretation Transcription Time +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, 0x0112)); // Interpretation Approval Date +removals_.insert(DicomTag(0x4008, 0x0113)); // Interpretation Approval Time +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, 0x0200)); // Interpretation ID +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 +removedRanges_.push_back(DicomTagRange(0x5000, 0x50ff, 0x0000, 0xffff)); // Curve Data +removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x3000, 0x3000)); // Overlay Data +removedRanges_.push_back(DicomTagRange(0x6000, 0x60ff, 0x4000, 0x4000)); // Overlay Comments +uids_.insert(DicomTag(0x0000, 0x1001)); // Requested SOP Instance UID +uids_.insert(DicomTag(0x0002, 0x0003)); // Media Storage SOP Instance UID +uids_.insert(DicomTag(0x0004, 0x1511)); // Referenced SOP Instance UID in File +uids_.insert(DicomTag(0x0008, 0x0014)); // Instance Creator UID +uids_.insert(DicomTag(0x0008, 0x0017)); // Acquisition UID +uids_.insert(DicomTag(0x0008, 0x0019)); // Pyramid UID +uids_.insert(DicomTag(0x0008, 0x0058)); // Failed SOP Instance UID List +uids_.insert(DicomTag(0x0008, 0x1155)); // Referenced SOP Instance UID +uids_.insert(DicomTag(0x0008, 0x1195)); // Transaction UID +uids_.insert(DicomTag(0x0008, 0x3010)); // Irradiation Event UID +uids_.insert(DicomTag(0x0018, 0x1002)); // Device UID +uids_.insert(DicomTag(0x0018, 0x100b)); // Manufacturer's Device Class UID +uids_.insert(DicomTag(0x0018, 0x2042)); // Target UID +uids_.insert(DicomTag(0x0020, 0x0052)); // Frame of Reference UID +uids_.insert(DicomTag(0x0020, 0x0200)); // Synchronization Frame of Reference UID +uids_.insert(DicomTag(0x0020, 0x9161)); // Concatenation UID +uids_.insert(DicomTag(0x0020, 0x9164)); // Dimension Organization UID +uids_.insert(DicomTag(0x0028, 0x1199)); // Palette Color Lookup Table UID +uids_.insert(DicomTag(0x0028, 0x1214)); // Large Palette Color Lookup Table UID +uids_.insert(DicomTag(0x003a, 0x0310)); // Multiplex Group UID +uids_.insert(DicomTag(0x0040, 0x0554)); // Specimen UID +uids_.insert(DicomTag(0x0040, 0x4023)); // Referenced General Purpose Scheduled Procedure Step Transaction UID +uids_.insert(DicomTag(0x0040, 0xa124)); // UID +uids_.insert(DicomTag(0x0040, 0xa171)); // Observation UID +uids_.insert(DicomTag(0x0040, 0xa172)); // Referenced Observation UID (Trial) +uids_.insert(DicomTag(0x0040, 0xa402)); // Observation Subject UID (Trial) +uids_.insert(DicomTag(0x0040, 0xdb0c)); // Template Extension Organization UID +uids_.insert(DicomTag(0x0040, 0xdb0d)); // Template Extension Creator UID +uids_.insert(DicomTag(0x0062, 0x0021)); // Tracking UID +uids_.insert(DicomTag(0x0064, 0x0003)); // Source Frame of Reference UID +uids_.insert(DicomTag(0x0070, 0x031a)); // Fiducial UID +uids_.insert(DicomTag(0x0070, 0x1101)); // Presentation Display Collection UID +uids_.insert(DicomTag(0x0070, 0x1102)); // Presentation Sequence Collection UID +uids_.insert(DicomTag(0x0088, 0x0140)); // Storage Media File-set UID +uids_.insert(DicomTag(0x0400, 0x0100)); // Digital Signature UID +uids_.insert(DicomTag(0x3006, 0x0024)); // Referenced Frame of Reference UID +uids_.insert(DicomTag(0x3006, 0x00c2)); // Related Frame of Reference UID +uids_.insert(DicomTag(0x300a, 0x0013)); // Dose Reference UID +uids_.insert(DicomTag(0x300a, 0x0083)); // Referenced Dose Reference UID +uids_.insert(DicomTag(0x300a, 0x0609)); // Treatment Position Group UID +uids_.insert(DicomTag(0x300a, 0x0650)); // Patient Setup UID +uids_.insert(DicomTag(0x300a, 0x0700)); // Treatment Session UID +uids_.insert(DicomTag(0x300a, 0x0785)); // Referenced Treatment Position Group UID +uids_.insert(DicomTag(0x3010, 0x0006)); // Conceptual Volume UID +uids_.insert(DicomTag(0x3010, 0x000b)); // Referenced Conceptual Volume UID +uids_.insert(DicomTag(0x3010, 0x0013)); // Constituent Conceptual Volume UID +uids_.insert(DicomTag(0x3010, 0x0015)); // Source Conceptual Volume UID +uids_.insert(DicomTag(0x3010, 0x0031)); // Referenced Fiducials UID +uids_.insert(DicomTag(0x3010, 0x003b)); // RT Treatment Phase UID +uids_.insert(DicomTag(0x3010, 0x006e)); // Dosimetric Objective UID +uids_.insert(DicomTag(0x3010, 0x006f)); // Referenced Dosimetric Objective UID
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -582,8 +582,8 @@ ignoreTagLength, 1); } - target.SetValue(DicomTag(element->getTag().getGTag(), element->getTag().getETag()), - jsonSequence); + target.SetSequenceValue(DicomTag(element->getTag().getGTag(), element->getTag().getETag()), + jsonSequence); } } } @@ -1981,7 +1981,14 @@ case EVR_OL: // other long (requires byte-swapping) #endif { - ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good(); + if (decoded->find('\\') != std::string::npos) + { + ok = element.putString(decoded->c_str()).good(); + } + else + { + ok = element.putUint32(boost::lexical_cast<Uint32>(*decoded)).good(); + } break; } @@ -2460,7 +2467,7 @@ #if ORTHANC_ENABLE_DCMTK_JPEG == 1 CLOG(INFO, DICOM) << "Registering JPEG codecs in DCMTK"; - DJDecoderRegistration::registerCodecs(); + DJDecoderRegistration::registerCodecs(); # if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 DJEncoderRegistration::registerCodecs(); # endif
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -745,11 +745,20 @@ "Cannot decode a non-palette image"); } + std::string colorModel = Orthanc::Toolbox::StripSpaces(decompressedColorModel.c_str()); + if (target->GetFormat() == PixelFormat_RGB24 && - Orthanc::Toolbox::StripSpaces(decompressedColorModel.c_str()) == "RGB" && + (colorModel == "RGB" || colorModel == "YBR_FULL") && info.IsPlanar()) { - return DecodePlanarConfiguration(*target); + std::unique_ptr<ImageAccessor> output(DecodePlanarConfiguration(*target)); + + if (colorModel == "YBR_FULL") + { + ImageProcessing::ConvertJpegYCbCrToRgb(*output); + } + + return output.release(); } else {
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -76,6 +76,7 @@ #include "Internals/DicomImageDecoder.h" #include "ToDcmtkBridge.h" +#include "../DicomFormat/DicomImageInformation.h" #include "../Images/Image.h" #include "../Images/ImageProcessing.h" #include "../Images/PamReader.h" @@ -1202,7 +1203,42 @@ break; case MimeType_Pdf: - EmbedPdf(content); + { + if (content.size() < 5 || // (*) + strncmp("%PDF-", content.c_str(), 5) != 0) + { + throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file"); + } + + EncapsulateDocument(MimeType_Pdf, content); + + // In Orthanc <= 1.9.7, the "Modality" would have always be overwritten as "OT" + // https://groups.google.com/g/orthanc-users/c/eNSddNrQDtM/m/wc1HahimAAAJ + + SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage); + SetIfAbsent(DICOM_TAG_MODALITY, "OT"); + SetIfAbsent(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD"); + //SetIfAbsent(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1"); + + break; + } + + case MimeType_Mtl: + EncapsulateDocument(mime, content); + SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.5"); + SetIfAbsent(DICOM_TAG_MODALITY, "M3D"); + break; + + case MimeType_Obj: + EncapsulateDocument(mime, content); + SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.4"); + SetIfAbsent(DICOM_TAG_MODALITY, "M3D"); + break; + + case MimeType_Stl: + EncapsulateDocument(mime, content); + SetIfAbsent(DICOM_TAG_SOP_CLASS_UID, "1.2.840.10008.5.1.4.1.1.104.3"); + SetIfAbsent(DICOM_TAG_MODALITY, "M3D"); break; default: @@ -1525,28 +1561,16 @@ } - void ParsedDicomFile::EmbedPdf(const std::string& pdf) + void ParsedDicomFile::EncapsulateDocument(MimeType mime, + const std::string& document) { - if (pdf.size() < 5 || // (*) - strncmp("%PDF-", pdf.c_str(), 5) != 0) - { - throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file"); - } - InvalidateCache(); - // In Orthanc <= 1.9.7, the "Modality" would have always be overwritten as "OT" - // https://groups.google.com/g/orthanc-users/c/eNSddNrQDtM/m/wc1HahimAAAJ - - ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage); - SetIfAbsent(FromDcmtkBridge::Convert(DCM_Modality), "OT"); - SetIfAbsent(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD"); - SetIfAbsent(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), MIME_PDF); - //SetIfAbsent(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1"); + ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), EnumerationToString(mime)); std::unique_ptr<DcmPolymorphOBOW> element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument)); - size_t s = pdf.size(); + size_t s = document.size(); if (s & 1) { // The size of the buffer must be even @@ -1560,10 +1584,12 @@ throw OrthancException(ErrorCode_NotEnoughMemory); } - // Blank pad byte (no access violation, as "pdf.size() >= 5" because of (*) ) - bytes[s - 1] = 0; + if (s > 0) + { + bytes[s - 1] = 0; + } - memcpy(bytes, pdf.c_str(), pdf.size()); + memcpy(bytes, document.c_str(), document.size()); DcmPolymorphOBOW* obj = element.release(); result = GetDcmtkObject().getDataset()->insert(obj); @@ -2121,6 +2147,84 @@ } } + + void ParsedDicomFile::InjectEmptyPixelData(ValueRepresentation vr) + { + DcmItem& dataset = *GetDcmtkObject().getDataset(); + + DcmElement *element = NULL; + if (!dataset.findAndGetElement(DCM_PixelData, element).good() || + element == NULL) + { + // The pixel data is indeed nonexistent, insert it now + switch (vr) + { + case ValueRepresentation_OtherByte: + if (!dataset.putAndInsertUint8Array(DCM_PixelData, NULL, 0).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + break; + + case ValueRepresentation_OtherWord: + if (!dataset.putAndInsertUint16Array(DCM_PixelData, NULL, 0).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } + + + void ParsedDicomFile::RemoveFromPixelData() + { + DcmItem& dataset = *GetDcmtkObject().getDataset(); + + // We need to go backward, otherwise "dataset.card()" is invalidated + for (unsigned long i = dataset.card(); i > 0; i--) + { + DcmElement* element = dataset.getElement(i - 1); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (element->getTag().getGroup() > DCM_PixelData.getGroup() || + (element->getTag().getGroup() == DCM_PixelData.getGroup() && + element->getTag().getElement() >= DCM_PixelData.getElement())) + { + std::unique_ptr<DcmElement> removal(dataset.remove(i - 1)); + } + } + } + + + ValueRepresentation ParsedDicomFile::GuessPixelDataValueRepresentation() const + { + DicomTransferSyntax ts; + if (LookupTransferSyntax(ts)) + { + DcmItem& dataset = *GetDcmtkObjectConst().getDataset(); + + uint16_t bitsAllocated; + if (!dataset.findAndGetUint16(DCM_BitsAllocated, bitsAllocated).good()) + { + bitsAllocated = 8; + } + + return DicomImageInformation::GuessPixelDataValueRepresentation(ts, bitsAllocated); + } + else + { + // Assume "OB" if the transfer syntax is unknown + return ValueRepresentation_OtherByte; + } + } + #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Tue Jul 04 12:20:41 2023 +0200 @@ -97,6 +97,9 @@ bool EmbedContentInternal(const std::string& dataUriScheme); + void EncapsulateDocument(MimeType mime, + const std::string& document); + // For internal use only, in order to provide const-correctness on // the top of DCMTK API DcmFileFormat& GetDcmtkObjectConst() const; @@ -205,6 +208,7 @@ void SaveToFile(const std::string& path); #endif + // This method must only be used on the PixelData and EncapsulatedDocument tags void EmbedContent(const std::string& dataUriScheme); void EmbedImage(const ImageAccessor& accessor); @@ -236,8 +240,6 @@ bool HasTag(const DicomTag& tag) const; - void EmbedPdf(const std::string& pdf); - bool ExtractPdf(std::string& pdf) const; void GetRawFrame(std::string& target, // OUT @@ -309,5 +311,12 @@ ImageAccessor* DecodeAllOverlays(int& originX, int& originY) const; + + void InjectEmptyPixelData(ValueRepresentation vr); + + // Remove all the tags after pixel data + void RemoveFromPixelData(); + + ValueRepresentation GuessPixelDataValueRepresentation() const; }; }
--- a/OrthancFramework/Sources/Enumerations.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/Enumerations.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -31,6 +31,7 @@ #include <boost/thread/mutex.hpp> #include <string.h> #include <cassert> +#include <boost/algorithm/string/replace.hpp> namespace Orthanc { @@ -877,15 +878,15 @@ { case DicomVersion_2008: return "2008"; - break; case DicomVersion_2017c: return "2017c"; - break; case DicomVersion_2021b: return "2021b"; - break; + + case DicomVersion_2023b: + return "2023b"; default: throw OrthancException(ErrorCode_ParameterOutOfRange); @@ -1110,6 +1111,15 @@ case MimeType_Ico: return MIME_ICO; + case MimeType_Obj: + return MIME_OBJ; + + case MimeType_Mtl: + return MIME_MTL; + + case MimeType_Stl: + return MIME_STL; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -1641,6 +1651,10 @@ { return DicomVersion_2021b; } + else if (version == "2023b") + { + return DicomVersion_2023b; + } else { throw OrthancException(ErrorCode_ParameterOutOfRange, @@ -1848,6 +1862,21 @@ target = MimeType_Ico; return true; } + else if (source == MIME_OBJ) + { + target = MimeType_Obj; + return true; + } + else if (source == MIME_MTL) + { + target = MimeType_Mtl; + return true; + } + else if (source == MIME_STL) + { + target = MimeType_Stl; + return true; + } else { return false; @@ -1933,6 +1962,11 @@ std::string s = Toolbox::StripSpaces(specificCharacterSet); Toolbox::ToUpperCase(s); + // handle common spelling mistakes + boost::replace_all(s, "ISO_IR_", "ISO_IR "); + boost::replace_all(s, "ISO_2022_IR_", "ISO 2022 IR "); + + // 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" ||
--- a/OrthancFramework/Sources/Enumerations.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/Enumerations.h Tue Jul 04 12:20:41 2023 +0200 @@ -42,6 +42,11 @@ static const char* const MIME_XML = "application/xml"; static const char* const MIME_XML_UTF8 = "application/xml; charset=utf-8"; + // Added in Orthanc 1.12.1 + static const char* const MIME_OBJ = "model/obj"; + static const char* const MIME_MTL = "model/mtl"; + static const char* const MIME_STL = "model/stl"; + /** * "No Internet Media Type (aka MIME type, content type) for PBM has * been registered with IANA, but the unofficial value @@ -79,7 +84,10 @@ MimeType_PrometheusText, // Prometheus text-based exposition format (for metrics) MimeType_DicomWebJson, MimeType_DicomWebXml, - MimeType_Ico + MimeType_Ico, + MimeType_Mtl, // MTL - New in Orthanc 1.12.1 + MimeType_Obj, // OBJ - New in Orthanc 1.12.1 + MimeType_Stl // STL - New in Orthanc 1.12.1 }; @@ -620,7 +628,8 @@ { DicomVersion_2008, DicomVersion_2017c, - DicomVersion_2021b + DicomVersion_2021b, + DicomVersion_2023b }; enum ModalityManufacturer
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -38,9 +38,11 @@ #endif -static const std::string METRICS_CREATE = "orthanc_storage_create_duration_ms"; -static const std::string METRICS_READ = "orthanc_storage_read_duration_ms"; -static const std::string METRICS_REMOVE = "orthanc_storage_remove_duration_ms"; +static const std::string METRICS_CREATE_DURATION = "orthanc_storage_create_duration_ms"; +static const std::string METRICS_READ_DURATION = "orthanc_storage_read_duration_ms"; +static const std::string METRICS_REMOVE_DURATION = "orthanc_storage_remove_duration_ms"; +static const std::string METRICS_READ_BYTES = "orthanc_storage_read_bytes"; +static const std::string METRICS_WRITTEN_BYTES = "orthanc_storage_written_bytes"; namespace Orthanc @@ -116,9 +118,15 @@ { case CompressionType_None: { - MetricsTimer timer(*this, METRICS_CREATE); + { + MetricsTimer timer(*this, METRICS_CREATE_DURATION); + area_.Create(uuid, data, size, type); + } - area_.Create(uuid, data, size, type); + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, size); + } if (cache_ != NULL) { @@ -143,7 +151,7 @@ } { - MetricsTimer timer(*this, METRICS_CREATE); + MetricsTimer timer(*this, METRICS_CREATE_DURATION); if (compressed.size() > 0) { @@ -155,6 +163,11 @@ } } + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_WRITTEN_BYTES, compressed.size()); + } + if (cache_ != NULL) { cache_->Add(uuid, type, data, size); // always add uncompressed data to cache @@ -189,8 +202,18 @@ { case CompressionType_None: { - MetricsTimer timer(*this, METRICS_READ); - std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType())); + std::unique_ptr<IMemoryBuffer> buffer; + + { + MetricsTimer timer(*this, METRICS_READ_DURATION); + buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + } + + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize()); + } + buffer->MoveToString(content); break; @@ -203,10 +226,15 @@ std::unique_ptr<IMemoryBuffer> compressed; { - MetricsTimer timer(*this, METRICS_READ); + MetricsTimer timer(*this, METRICS_READ_DURATION); compressed.reset(area_.Read(info.GetUuid(), info.GetContentType())); } + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_READ_BYTES, compressed->GetSize()); + } + zlib.Uncompress(content, compressed->GetData(), compressed->GetSize()); break; @@ -234,8 +262,18 @@ { if (cache_ == NULL || !cache_->Fetch(content, info.GetUuid(), info.GetContentType())) { - MetricsTimer timer(*this, METRICS_READ); - std::unique_ptr<IMemoryBuffer> buffer(area_.Read(info.GetUuid(), info.GetContentType())); + std::unique_ptr<IMemoryBuffer> buffer; + + { + MetricsTimer timer(*this, METRICS_READ_DURATION); + buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + } + + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize()); + } + buffer->MoveToString(content); } } @@ -250,7 +288,7 @@ } { - MetricsTimer timer(*this, METRICS_REMOVE); + MetricsTimer timer(*this, METRICS_REMOVE_DURATION); area_.Remove(fileUuid, type); } } @@ -269,9 +307,19 @@ { if (cache_ == NULL || !cache_->FetchStartRange(target, fileUuid, contentType, end)) { - MetricsTimer timer(*this, METRICS_READ); - std::unique_ptr<IMemoryBuffer> buffer(area_.ReadRange(fileUuid, contentType, 0, end)); - assert(buffer->GetSize() == end); + std::unique_ptr<IMemoryBuffer> buffer; + + { + MetricsTimer timer(*this, METRICS_READ_DURATION); + buffer.reset(area_.ReadRange(fileUuid, contentType, 0, end)); + assert(buffer->GetSize() == end); + } + + if (metrics_ != NULL) + { + metrics_->IncrementIntegerValue(METRICS_READ_BYTES, buffer->GetSize()); + } + buffer->MoveToString(target); if (cache_ != NULL)
--- a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -49,28 +49,68 @@ { return true; } - - if (subtype == "*" && type == type_) + else if (subtype == "*" && type == type_) { return true; } - - return type == type_ && subtype == subtype_; + else + { + return type == type_ && subtype == subtype_; + } } - struct HttpContentNegociation::Reference : public boost::noncopyable + class HttpContentNegociation::Reference : public boost::noncopyable { + private: const Handler& handler_; uint8_t level_; float quality_; + Dictionary parameters_; + static float GetQuality(const Dictionary& parameters) + { + Dictionary::const_iterator found = parameters.find("q"); + + if (found != parameters.end()) + { + float quality; + bool ok = false; + + try + { + quality = boost::lexical_cast<float>(found->second); + ok = (quality >= 0.0f && quality <= 1.0f); + } + catch (boost::bad_lexical_cast&) + { + } + + if (ok) + { + return quality; + } + else + { + throw OrthancException( + ErrorCode_BadRequest, + "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + found->second); + } + } + else + { + return 1.0f; // Default quality + } + } + + public: Reference(const Handler& handler, const std::string& type, const std::string& subtype, - float quality) : + const Dictionary& parameters) : handler_(handler), - quality_(quality) + quality_(GetQuality(parameters)), + parameters_(parameters) { if (type == "*" && subtype == "*") { @@ -85,6 +125,11 @@ level_ = 2; } } + + void Call() const + { + handler_.Call(parameters_); + } bool operator< (const Reference& other) const { @@ -92,13 +137,14 @@ { return true; } - - if (level_ > other.level_) + else if (level_ > other.level_) { return false; } - - return quality_ < other.quality_; + else + { + return quality_ < other.quality_; + } } }; @@ -123,58 +169,21 @@ } - float HttpContentNegociation::GetQuality(const Tokens& parameters) - { - for (size_t i = 1; i < parameters.size(); i++) - { - std::string key, value; - if (SplitPair(key, value, parameters[i], '=') && - key == "q") - { - float quality; - bool ok = false; - - try - { - quality = boost::lexical_cast<float>(value); - ok = (quality >= 0.0f && quality <= 1.0f); - } - catch (boost::bad_lexical_cast&) - { - } - - if (ok) - { - return quality; - } - else - { - throw OrthancException( - ErrorCode_BadRequest, - "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + value); - } - } - } - - return 1.0f; // Default quality - } - - - void HttpContentNegociation::SelectBestMatch(std::unique_ptr<Reference>& best, + void HttpContentNegociation::SelectBestMatch(std::unique_ptr<Reference>& target, const Handler& handler, const std::string& type, const std::string& subtype, - float quality) + const Dictionary& parameters) { - std::unique_ptr<Reference> match(new Reference(handler, type, subtype, quality)); + std::unique_ptr<Reference> match(new Reference(handler, type, subtype, parameters)); - if (best.get() == NULL || - *best < *match) + if (target.get() == NULL || + *target < *match) { #if __cplusplus < 201103L - best.reset(match.release()); + target.reset(match.release()); #else - best = std::move(match); + target = std::move(match); #endif } } @@ -198,9 +207,9 @@ } - bool HttpContentNegociation::Apply(const HttpHeaders& headers) + bool HttpContentNegociation::Apply(const Dictionary& headers) { - HttpHeaders::const_iterator accept = headers.find("accept"); + Dictionary::const_iterator accept = headers.find("accept"); if (accept != headers.end()) { return Apply(accept->second); @@ -226,22 +235,44 @@ for (Tokens::const_iterator it = mediaRanges.begin(); it != mediaRanges.end(); ++it) { - Tokens parameters; - Toolbox::TokenizeString(parameters, *it, ';'); + Tokens tokens; + Toolbox::TokenizeString(tokens, *it, ';'); - if (parameters.size() > 0) + if (tokens.size() > 0) { - float quality = GetQuality(parameters); + Dictionary parameters; + for (size_t i = 1; i < tokens.size(); i++) + { + std::string key, value; + + if (SplitPair(key, value, tokens[i], '=')) + { + // Remove the enclosing quotes, if present + if (!value.empty() && + value[0] == '"' && + value[value.size() - 1] == '"') + { + value = value.substr(1, value.size() - 2); + } + } + else + { + key = Toolbox::StripSpaces(tokens[i]); + value = ""; + } + parameters[key] = value; + } + std::string type, subtype; - if (SplitPair(type, subtype, parameters[0], '/')) + if (SplitPair(type, subtype, tokens[0], '/')) { for (Handlers::const_iterator it2 = handlers_.begin(); it2 != handlers_.end(); ++it2) { if (it2->IsMatch(type, subtype)) { - SelectBestMatch(bestMatch, *it2, type, subtype, quality); + SelectBestMatch(bestMatch, *it2, type, subtype, parameters); } } } @@ -254,7 +285,7 @@ } else { - bestMatch->handler_.Call(); + bestMatch->Call(); return true; } }
--- a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h Tue Jul 04 12:20:41 2023 +0200 @@ -39,7 +39,7 @@ class ORTHANC_PUBLIC HttpContentNegociation : public boost::noncopyable { public: - typedef std::map<std::string, std::string> HttpHeaders; + typedef std::map<std::string, std::string> Dictionary; class IHandler : public boost::noncopyable { @@ -49,7 +49,8 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) = 0; + const std::string& subtype, + const Dictionary& parameters) = 0; }; private: @@ -66,9 +67,9 @@ bool IsMatch(const std::string& type, const std::string& subtype) const; - void Call() const + void Call(const Dictionary& parameters) const { - handler_.Handle(type_, subtype_); + handler_.Handle(type_, subtype_, parameters); } }; @@ -86,19 +87,17 @@ const std::string& source, char separator); - static float GetQuality(const Tokens& parameters); - - static void SelectBestMatch(std::unique_ptr<Reference>& best, + static void SelectBestMatch(std::unique_ptr<Reference>& target, const Handler& handler, const std::string& type, const std::string& subtype, - float quality); + const Dictionary& parameters); public: void Register(const std::string& mime, IHandler& handler); - bool Apply(const HttpHeaders& headers); + bool Apply(const Dictionary& headers); bool Apply(const std::string& accept); };
--- a/OrthancFramework/Sources/Images/NumpyWriter.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/Images/NumpyWriter.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -154,7 +154,7 @@ { if (compress) { -#if ORTHANC_ENABLE_ZLIB == 1 +#if (ORTHANC_ENABLE_ZLIB == 1) && (ORTHANC_SANDBOXED == 0) // This is the default name of the first array if arrays are // specified as positional arguments in "numpy.savez()" // https://numpy.org/doc/stable/reference/generated/numpy.savez.html @@ -172,7 +172,7 @@ writer.Write(uncompressed); writer.Close(); #else - throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for zlib"); + throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for ZIP"); #endif } else
--- a/OrthancFramework/Sources/JobsEngine/IJob.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/JobsEngine/IJob.h Tue Jul 04 12:20:41 2023 +0200 @@ -62,5 +62,9 @@ MimeType& mime, std::string& filename, const std::string& key) = 0; + + // This function can only be called if the job has reached its + // "success" state + virtual bool DeleteOutput(const std::string& key) = 0; }; }
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -679,6 +679,33 @@ } } + bool JobsRegistry::DeleteJobOutput(const std::string& job, + const std::string& key) + { + boost::mutex::scoped_lock lock(mutex_); + CheckInvariants(); + + JobsIndex::const_iterator found = jobsIndex_.find(job); + + if (found == jobsIndex_.end()) + { + return false; + } + else + { + const JobHandler& handler = *found->second; + + if (handler.GetState() == JobState_Success) + { + return handler.GetJob().DeleteOutput(key); + } + else + { + return false; + } + } + } + void JobsRegistry::SubmitInternal(std::string& id, JobHandler* handler)
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h Tue Jul 04 12:20:41 2023 +0200 @@ -153,6 +153,9 @@ const std::string& job, const std::string& key); + bool DeleteJobOutput(const std::string& job, + const std::string& key); + void Serialize(Json::Value& target); void Submit(std::string& id,
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -447,13 +447,6 @@ return true; } - bool SequenceOfOperationsJob::GetOutput(std::string& output, - MimeType& mime, - std::string& filename, - const std::string& key) - { - return false; - } void SequenceOfOperationsJob::AwakeTrailingSleep() {
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Tue Jul 04 12:20:41 2023 +0200 @@ -127,7 +127,15 @@ virtual bool GetOutput(std::string& output, MimeType& mime, std::string& filename, - const std::string& key) ORTHANC_OVERRIDE; + const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } void AwakeTrailingSleep(); };
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -269,14 +269,6 @@ return true; } - bool SetOfCommandsJob::GetOutput(std::string &output, - MimeType &mime, - std::string& filename, - const std::string &key) - { - return false; - } - SetOfCommandsJob::SetOfCommandsJob(ICommandUnserializer* unserializer, const Json::Value& source) :
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Tue Jul 04 12:20:41 2023 +0200 @@ -106,6 +106,14 @@ virtual bool GetOutput(std::string& output, MimeType& mime, std::string& filename, - const std::string& key) ORTHANC_OVERRIDE; + const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } }; }
--- a/OrthancFramework/Sources/Lua/LuaContext.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/Lua/LuaContext.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -396,10 +396,6 @@ const std::string s = value.asString(); lua_pushlstring(lua_, s.c_str(), s.size()); } - else if (value.isDouble()) - { - lua_pushnumber(lua_, value.asDouble()); - } else if (value.isInt()) { lua_pushinteger(lua_, value.asInt()); @@ -408,6 +404,10 @@ { lua_pushinteger(lua_, value.asUInt()); } + else if (value.isDouble()) + { + lua_pushnumber(lua_, value.asDouble()); + } else if (value.isBool()) { lua_pushboolean(lua_, value.asBool());
--- a/OrthancFramework/Sources/MetricsRegistry.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/MetricsRegistry.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -28,6 +28,8 @@ #include "Compatibility.h" #include "OrthancException.h" +#include <boost/math/special_functions/round.hpp> + namespace Orthanc { static const boost::posix_time::ptime GetNow() @@ -35,134 +37,277 @@ return boost::posix_time::microsec_clock::universal_time(); } - class MetricsRegistry::Item + namespace { - private: - MetricsType type_; - boost::posix_time::ptime time_; - bool hasValue_; - float value_; - - void Touch(float value, - const boost::posix_time::ptime& now) + template <typename T> + class TimestampedValue : public boost::noncopyable { - hasValue_ = true; - value_ = value; - time_ = now; - } + private: + boost::posix_time::ptime time_; + bool hasValue_; + T value_; + + void SetValue(const T& value, + const boost::posix_time::ptime& now) + { + hasValue_ = true; + value_ = value; + time_ = now; + } - void Touch(float value) - { - Touch(value, GetNow()); - } + bool IsLargerOverPeriod(const T& value, + int duration, + const boost::posix_time::ptime& now) const + { + if (hasValue_) + { + return (value > value_ || + (now - time_).total_seconds() > duration /* old value has expired */); + } + else + { + return true; // No value yet + } + } - void UpdateMax(float value, - int duration) - { - if (hasValue_) + bool IsSmallerOverPeriod(const T& value, + int duration, + const boost::posix_time::ptime& now) const + { + if (hasValue_) + { + return (value < value_ || + (now - time_).total_seconds() > duration /* old value has expired */); + } + else + { + return true; // No value yet + } + } + + public: + explicit TimestampedValue() : + hasValue_(false), + value_(0) + { + } + + void Update(const T& value, + const MetricsUpdatePolicy& policy) { const boost::posix_time::ptime now = GetNow(); - if (value > value_ || - (now - time_).total_seconds() > duration) + switch (policy) { - Touch(value, now); + case MetricsUpdatePolicy_Directly: + SetValue(value, now); + break; + + case MetricsUpdatePolicy_MaxOver10Seconds: + if (IsLargerOverPeriod(value, 10, now)) + { + SetValue(value, now); + } + break; + + case MetricsUpdatePolicy_MaxOver1Minute: + if (IsLargerOverPeriod(value, 60, now)) + { + SetValue(value, now); + } + break; + + case MetricsUpdatePolicy_MinOver10Seconds: + if (IsSmallerOverPeriod(value, 10, now)) + { + SetValue(value, now); + } + break; + + case MetricsUpdatePolicy_MinOver1Minute: + if (IsSmallerOverPeriod(value, 60, now)) + { + SetValue(value, now); + } + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); } } - else + + void Increment(const T& delta) + { + if (hasValue_) + { + value_ += delta; + } + else + { + value_ = delta; + } + } + + bool HasValue() const + { + return hasValue_; + } + + const boost::posix_time::ptime& GetTime() const { - Touch(value); + if (hasValue_) + { + return time_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } } + + const T& GetValue() const + { + if (hasValue_) + { + return value_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + }; + } + + + class MetricsRegistry::Item : public boost::noncopyable + { + private: + MetricsUpdatePolicy policy_; + + public: + explicit Item(MetricsUpdatePolicy policy) : + policy_(policy) + { } - void UpdateMin(float value, - int duration) - { - if (hasValue_) - { - const boost::posix_time::ptime now = GetNow(); - - if (value < value_ || - (now - time_).total_seconds() > duration) - { - Touch(value, now); - } - } - else - { - Touch(value); - } - } - - public: - explicit Item(MetricsType type) : - type_(type), - hasValue_(false), - value_(0) + virtual ~Item() { } - MetricsType GetType() const + MetricsUpdatePolicy GetPolicy() const { - return type_; + return policy_; + } + + virtual void UpdateFloat(float value) = 0; + + virtual void UpdateInteger(int64_t value) = 0; + + virtual void IncrementInteger(int64_t delta) = 0; + + virtual MetricsDataType GetDataType() const = 0; + + virtual bool HasValue() const = 0; + + virtual const boost::posix_time::ptime& GetTime() const = 0; + + virtual std::string FormatValue() const = 0; + }; + + + class MetricsRegistry::FloatItem : public Item + { + private: + TimestampedValue<float> value_; + + public: + explicit FloatItem(MetricsUpdatePolicy policy) : + Item(policy) + { + } + + virtual void UpdateFloat(float value) ORTHANC_OVERRIDE + { + value_.Update(value, GetPolicy()); + } + + virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE + { + value_.Update(static_cast<float>(value), GetPolicy()); + } + + virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE + { + value_.Increment(static_cast<float>(delta)); + } + + virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE + { + return MetricsDataType_Float; + } + + virtual bool HasValue() const ORTHANC_OVERRIDE + { + return value_.HasValue(); } - void Update(float value) + virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE { - switch (type_) - { - case MetricsType_Default: - Touch(value); - break; - - case MetricsType_MaxOver10Seconds: - UpdateMax(value, 10); - break; + return value_.GetTime(); + } + + virtual std::string FormatValue() const ORTHANC_OVERRIDE + { + return boost::lexical_cast<std::string>(value_.GetValue()); + } + }; - case MetricsType_MaxOver1Minute: - UpdateMax(value, 60); - break; - - case MetricsType_MinOver10Seconds: - UpdateMin(value, 10); - break; + + class MetricsRegistry::IntegerItem : public Item + { + private: + TimestampedValue<int64_t> value_; - case MetricsType_MinOver1Minute: - UpdateMin(value, 60); - break; - - default: - throw OrthancException(ErrorCode_NotImplemented); - } + public: + explicit IntegerItem(MetricsUpdatePolicy policy) : + Item(policy) + { + } + + virtual void UpdateFloat(float value) ORTHANC_OVERRIDE + { + value_.Update(boost::math::llround(value), GetPolicy()); } - bool HasValue() const + virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE { - return hasValue_; + value_.Update(value, GetPolicy()); + } + + virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE + { + value_.Increment(delta); } - const boost::posix_time::ptime& GetTime() const + virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE { - if (hasValue_) - { - return time_; - } - else - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } + return MetricsDataType_Integer; } - float GetValue() const + virtual bool HasValue() const ORTHANC_OVERRIDE + { + return value_.HasValue(); + } + + virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE { - if (hasValue_) - { - return value_; - } - else - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } + return value_.GetTime(); + } + + virtual std::string FormatValue() const ORTHANC_OVERRIDE + { + return boost::lexical_cast<std::string>(value_.GetValue()); } }; @@ -190,48 +335,53 @@ void MetricsRegistry::Register(const std::string& name, - MetricsType type) + MetricsUpdatePolicy policy, + MetricsDataType type) { boost::mutex::scoped_lock lock(mutex_); + if (content_.find(name) != content_.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same metrics: " + name); + } + else + { + GetItemInternal(name, policy, type); + } + } + + + MetricsRegistry::Item& MetricsRegistry::GetItemInternal(const std::string& name, + MetricsUpdatePolicy policy, + MetricsDataType type) + { Content::iterator found = content_.find(name); if (found == content_.end()) { - content_[name] = new Item(type); + Item* item = NULL; + + switch (type) + { + case MetricsDataType_Float: + item = new FloatItem(policy); + break; + + case MetricsDataType_Integer: + item = new IntegerItem(policy); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + content_[name] = item; + return *item; } else { assert(found->second != NULL); - - // This metrics already exists: Only recreate it if there is a - // mismatch in the type of metrics - if (found->second->GetType() != type) - { - delete found->second; - found->second = new Item(type); - } - } - } - - void MetricsRegistry::SetValueInternal(const std::string& name, - float value, - MetricsType type) - { - boost::mutex::scoped_lock lock(mutex_); - - Content::iterator found = content_.find(name); - - if (found == content_.end()) - { - std::unique_ptr<Item> item(new Item(type)); - item->Update(value); - content_[name] = item.release(); - } - else - { - assert(found->second != NULL); - found->second->Update(value); + return *found->second; } } @@ -241,29 +391,49 @@ } - void MetricsRegistry::SetValue(const std::string &name, - float value, - MetricsType type) + void MetricsRegistry::SetFloatValue(const std::string& name, + float value, + MetricsUpdatePolicy policy) { // Inlining to avoid loosing time if metrics are disabled if (enabled_) { - SetValueInternal(name, value, type); + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, policy, MetricsDataType_Float).UpdateFloat(value); + } + } + + + void MetricsRegistry::SetIntegerValue(const std::string &name, + int64_t value, + MetricsUpdatePolicy policy) + { + // Inlining to avoid loosing time if metrics are disabled + if (enabled_) + { + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, policy, MetricsDataType_Integer).UpdateInteger(value); } } - void MetricsRegistry::SetValue(const std::string &name, float value) + void MetricsRegistry::IncrementIntegerValue(const std::string &name, + int64_t delta) { - SetValue(name, value, MetricsType_Default); + // Inlining to avoid loosing time if metrics are disabled + if (enabled_) + { + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, MetricsUpdatePolicy_Directly, MetricsDataType_Integer).IncrementInteger(delta); + } } - MetricsType MetricsRegistry::GetMetricsType(const std::string& name) + MetricsUpdatePolicy MetricsRegistry::GetUpdatePolicy(const std::string& metrics) { boost::mutex::scoped_lock lock(mutex_); - Content::const_iterator found = content_.find(name); + Content::const_iterator found = content_.find(metrics); if (found == content_.end()) { @@ -272,7 +442,25 @@ else { assert(found->second != NULL); - return found->second->GetType(); + return found->second->GetPolicy(); + } + } + + + MetricsDataType MetricsRegistry::GetDataType(const std::string& metrics) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::const_iterator found = content_.find(metrics); + + if (found == content_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return found->second->GetDataType(); } } @@ -303,7 +491,7 @@ boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH; std::string line = (it->first + " " + - boost::lexical_cast<std::string>(it->second->GetValue()) + " " + + it->second->FormatValue() + " " + boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n"); buffer.AddChunk(line); @@ -316,18 +504,18 @@ MetricsRegistry::SharedMetrics::SharedMetrics(MetricsRegistry ®istry, const std::string &name, - MetricsType type) : + MetricsUpdatePolicy policy) : registry_(registry), name_(name), value_(0) { } - void MetricsRegistry::SharedMetrics::Add(float delta) + void MetricsRegistry::SharedMetrics::Add(int64_t delta) { boost::mutex::scoped_lock lock(mutex_); value_ += delta; - registry_.SetValue(name_, value_); + registry_.SetIntegerValue(name_, value_); } @@ -361,7 +549,7 @@ const std::string &name) : registry_(registry), name_(name), - type_(MetricsType_MaxOver10Seconds) + policy_(MetricsUpdatePolicy_MaxOver10Seconds) { Start(); } @@ -369,10 +557,10 @@ MetricsRegistry::Timer::Timer(MetricsRegistry ®istry, const std::string &name, - MetricsType type) : + MetricsUpdatePolicy policy) : registry_(registry), name_(name), - type_(type) + policy_(policy) { Start(); } @@ -383,8 +571,7 @@ if (active_) { boost::posix_time::time_duration diff = GetNow() - start_; - registry_.SetValue( - name_, static_cast<float>(diff.total_milliseconds()), type_); + registry_.SetIntegerValue(name_, static_cast<int64_t>(diff.total_milliseconds()), policy_); } } }
--- a/OrthancFramework/Sources/MetricsRegistry.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/MetricsRegistry.h Tue Jul 04 12:20:41 2023 +0200 @@ -35,22 +35,31 @@ #include <boost/thread/mutex.hpp> #include <boost/date_time/posix_time/posix_time.hpp> +#include <stdint.h> namespace Orthanc { - enum MetricsType + enum MetricsUpdatePolicy { - MetricsType_Default, - MetricsType_MaxOver10Seconds, - MetricsType_MaxOver1Minute, - MetricsType_MinOver10Seconds, - MetricsType_MinOver1Minute + MetricsUpdatePolicy_Directly, + MetricsUpdatePolicy_MaxOver10Seconds, + MetricsUpdatePolicy_MaxOver1Minute, + MetricsUpdatePolicy_MinOver10Seconds, + MetricsUpdatePolicy_MinOver1Minute + }; + + enum MetricsDataType + { + MetricsDataType_Float, + MetricsDataType_Integer }; class ORTHANC_PUBLIC MetricsRegistry : public boost::noncopyable { private: class Item; + class FloatItem; + class IntegerItem; typedef std::map<std::string, Item*> Content; @@ -58,9 +67,10 @@ boost::mutex mutex_; Content content_; - void SetValueInternal(const std::string& name, - float value, - MetricsType type); + // The mutex must be locked + Item& GetItemInternal(const std::string& name, + MetricsUpdatePolicy policy, + MetricsDataType type); public: MetricsRegistry(); @@ -72,16 +82,35 @@ void SetEnabled(bool enabled); void Register(const std::string& name, - MetricsType type); + MetricsUpdatePolicy policy, + MetricsDataType type); - void SetValue(const std::string& name, - float value, - MetricsType type); + void SetFloatValue(const std::string& name, + float value, + MetricsUpdatePolicy policy /* only used if this is a new metrics */); + + void SetFloatValue(const std::string& name, + float value) + { + SetFloatValue(name, value, MetricsUpdatePolicy_Directly); + } - void SetValue(const std::string& name, - float value); + void SetIntegerValue(const std::string& name, + int64_t value, + MetricsUpdatePolicy policy /* only used if this is a new metrics */); + + void SetIntegerValue(const std::string& name, + int64_t value) + { + SetIntegerValue(name, value, MetricsUpdatePolicy_Directly); + } + + void IncrementIntegerValue(const std::string& name, + int64_t delta); - MetricsType GetMetricsType(const std::string& name); + MetricsUpdatePolicy GetUpdatePolicy(const std::string& metrics); + + MetricsDataType GetDataType(const std::string& metrics); // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format void ExportPrometheusText(std::string& s); @@ -93,14 +122,14 @@ boost::mutex mutex_; MetricsRegistry& registry_; std::string name_; - float value_; + int64_t value_; public: SharedMetrics(MetricsRegistry& registry, const std::string& name, - MetricsType type); + MetricsUpdatePolicy policy); - void Add(float delta); + void Add(int64_t delta); }; @@ -121,7 +150,7 @@ private: MetricsRegistry& registry_; std::string name_; - MetricsType type_; + MetricsUpdatePolicy policy_; bool active_; boost::posix_time::ptime start_; @@ -133,7 +162,7 @@ Timer(MetricsRegistry& registry, const std::string& name, - MetricsType type); + MetricsUpdatePolicy policy); ~Timer(); };
--- a/OrthancFramework/Sources/SystemToolbox.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -825,6 +825,18 @@ { return MimeType_Zip; } + else if (extension == ".mtl") + { + return MimeType_Mtl; + } + else if (extension == ".obj") + { + return MimeType_Obj; + } + else if (extension == ".stl") + { + return MimeType_Stl; + } // Default type else
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -42,6 +42,7 @@ #include "../Sources/DicomParsing/DicomWebJsonVisitor.h" #include <boost/lexical_cast.hpp> +#include <boost/tuple/tuple.hpp> using namespace Orthanc; @@ -1288,50 +1289,61 @@ { static const std::string PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/"; - typedef std::list< std::pair<std::string, uint64_t> > Sources; + typedef boost::tuple<std::string, uint64_t, ValueRepresentation> Source; + typedef std::list<Source> Sources; + + // $ ~/Subversion/orthanc-tests/Tests/GetPixelDataVR.py ~/Subversion/orthanc-tests/Database/ColorTestMalaterre.dcm ~/Subversion/orthanc-tests/Database/ColorTestImageJ.dcm ~/Subversion/orthanc-tests/Database/Knee/T1/IM-0001-0001.dcm ~/Subversion/orthanc-tests/Database/TransferSyntaxes/*.dcm Sources sources; - sources.push_back(std::make_pair(PATH + "../ColorTestMalaterre.dcm", 0x03a0u)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.1.dcm", 0x037cu)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.2.dcm", 0x03e8u)); // Big Endian - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.50.dcm", 0x04acu)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.51.dcm", 0x072cu)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.57.dcm", 0x0620u)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.70.dcm", 0x065au)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.80.dcm", 0x0b46u)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.81.dcm", 0x073eu)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.90.dcm", 0x0b66u)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.4.91.dcm", 0x19b8u)); - sources.push_back(std::make_pair(PATH + "1.2.840.10008.1.2.5.dcm", 0x0b0au)); + sources.push_back(Source(PATH + "../ColorTestMalaterre.dcm", 0x03a0u, ValueRepresentation_Unknown)); // This file has strange VR + sources.push_back(Source(PATH + "../ColorTestImageJ.dcm", 0x00924, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "../Knee/T1/IM-0001-0001.dcm", 0x00c78, ValueRepresentation_OtherWord)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.1.dcm", 0x037cu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.2.dcm", 0x03e8u, ValueRepresentation_OtherByte)); // Big Endian + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.50.dcm", 0x04acu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.51.dcm", 0x072cu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.57.dcm", 0x0620u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.70.dcm", 0x065au, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.80.dcm", 0x0b46u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.81.dcm", 0x073eu, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.90.dcm", 0x0b66u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.4.91.dcm", 0x19b8u, ValueRepresentation_OtherByte)); + sources.push_back(Source(PATH + "1.2.840.10008.1.2.5.dcm", 0x0b0au, ValueRepresentation_OtherByte)); { std::string dicom; uint64_t offset; + ValueRepresentation vr; + // Not a DICOM image SystemToolbox::ReadFile(dicom, PATH + "1.2.840.10008.1.2.4.50.png", false); - ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, dicom)); + ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom)); // Image without valid DICOM preamble SystemToolbox::ReadFile(dicom, PATH + "1.2.840.10008.1.2.dcm", false); - ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, dicom)); + ASSERT_FALSE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom)); } for (Sources::const_iterator it = sources.begin(); it != sources.end(); ++it) { std::string dicom; - SystemToolbox::ReadFile(dicom, it->first, false); + SystemToolbox::ReadFile(dicom, it->get<0>(), false); { uint64_t offset; - ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, dicom)); - ASSERT_EQ(it->second, offset); + ValueRepresentation vr; + ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom)); + ASSERT_EQ(it->get<1>(), offset); + ASSERT_EQ(it->get<2>(), vr); } { uint64_t offset; - ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, dicom.c_str(), dicom.size())); - ASSERT_EQ(it->second, offset); + ValueRepresentation vr; + ASSERT_TRUE(DicomStreamReader::LookupPixelDataOffset(offset, vr, dicom.c_str(), dicom.size())); + ASSERT_EQ(it->get<1>(), offset); + ASSERT_EQ(it->get<2>(), vr); } ParsedDicomFile a(dicom); @@ -1355,7 +1367,7 @@ r.Consume(visitor); - ASSERT_EQ(it->second, visitor.GetPixelDataOffset()); + ASSERT_EQ(it->get<1>(), visitor.GetPixelDataOffset()); // Truncate the original DICOM up to pixel data dicom.resize(visitor.GetPixelDataOffset());
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -363,6 +363,10 @@ ASSERT_STREQ("image/svg+xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".svg"))); ASSERT_STREQ("text/css", EnumerationToString(SystemToolbox::AutodetectMimeType(".css"))); ASSERT_STREQ("text/html", EnumerationToString(SystemToolbox::AutodetectMimeType(".html"))); + + ASSERT_STREQ("model/obj", EnumerationToString(SystemToolbox::AutodetectMimeType(".obj"))); + ASSERT_STREQ("model/mtl", EnumerationToString(SystemToolbox::AutodetectMimeType(".mtl"))); + ASSERT_STREQ("model/stl", EnumerationToString(SystemToolbox::AutodetectMimeType(".stl"))); } #endif @@ -749,6 +753,7 @@ ASSERT_EQ(DicomVersion_2008, StringToDicomVersion(EnumerationToString(DicomVersion_2008))); ASSERT_EQ(DicomVersion_2017c, StringToDicomVersion(EnumerationToString(DicomVersion_2017c))); ASSERT_EQ(DicomVersion_2021b, StringToDicomVersion(EnumerationToString(DicomVersion_2021b))); + ASSERT_EQ(DicomVersion_2023b, StringToDicomVersion(EnumerationToString(DicomVersion_2023b))); for (int i = static_cast<int>(ValueRepresentation_ApplicationEntity); i < static_cast<int>(ValueRepresentation_NotSupported); i += 1) @@ -790,6 +795,9 @@ ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml))); ASSERT_EQ(MimeType_DicomWebJson, StringToMimeType(EnumerationToString(MimeType_DicomWebJson))); ASSERT_EQ(MimeType_DicomWebXml, StringToMimeType(EnumerationToString(MimeType_DicomWebXml))); + ASSERT_EQ(MimeType_Mtl, StringToMimeType(EnumerationToString(MimeType_Mtl))); + ASSERT_EQ(MimeType_Obj, StringToMimeType(EnumerationToString(MimeType_Obj))); + ASSERT_EQ(MimeType_Stl, StringToMimeType(EnumerationToString(MimeType_Stl))); ASSERT_THROW(StringToMimeType("nope"), OrthancException); ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient)); @@ -1311,7 +1319,7 @@ { MetricsRegistry m; m.SetEnabled(false); - m.SetValue("hello.world", 42.5f); + m.SetIntegerValue("hello.world", 42); std::string s; m.ExportPrometheusText(s); @@ -1320,7 +1328,7 @@ { MetricsRegistry m; - m.Register("hello.world", MetricsType_Default); + m.Register("hello.world", MetricsUpdatePolicy_Directly, MetricsDataType_Integer); std::string s; m.ExportPrometheusText(s); @@ -1329,9 +1337,9 @@ { MetricsRegistry m; - m.SetValue("hello.world", 42.5f); - ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.world")); - ASSERT_THROW(m.GetMetricsType("nope"), OrthancException); + m.SetIntegerValue("hello.world", -42); + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("hello.world")); + ASSERT_THROW(m.GetUpdatePolicy("nope"), OrthancException); std::string s; m.ExportPrometheusText(s); @@ -1339,33 +1347,33 @@ std::vector<std::string> t; Toolbox::TokenizeString(t, s, '\n'); ASSERT_EQ(2u, t.size()); - ASSERT_EQ("hello.world 42.5 ", t[0].substr(0, 17)); + ASSERT_EQ("hello.world -42 ", t[0].substr(0, 16)); ASSERT_TRUE(t[1].empty()); } { MetricsRegistry m; - m.Register("hello.max", MetricsType_MaxOver10Seconds); - m.SetValue("hello.max", 10); - m.SetValue("hello.max", 20); - m.SetValue("hello.max", -10); - m.SetValue("hello.max", 5); + m.Register("hello.max", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + m.SetIntegerValue("hello.max", 10); + m.SetIntegerValue("hello.max", 20); + m.SetIntegerValue("hello.max", -10); + m.SetIntegerValue("hello.max", 5); - m.Register("hello.min", MetricsType_MinOver10Seconds); - m.SetValue("hello.min", 10); - m.SetValue("hello.min", 20); - m.SetValue("hello.min", -10); - m.SetValue("hello.min", 5); + m.Register("hello.min", MetricsUpdatePolicy_MinOver10Seconds, MetricsDataType_Integer); + m.SetIntegerValue("hello.min", 10); + m.SetIntegerValue("hello.min", 20); + m.SetIntegerValue("hello.min", -10); + m.SetIntegerValue("hello.min", 5); - m.Register("hello.default", MetricsType_Default); - m.SetValue("hello.default", 10); - m.SetValue("hello.default", 20); - m.SetValue("hello.default", -10); - m.SetValue("hello.default", 5); + m.Register("hello.directly", MetricsUpdatePolicy_Directly, MetricsDataType_Integer); + m.SetIntegerValue("hello.directly", 10); + m.SetIntegerValue("hello.directly", 20); + m.SetIntegerValue("hello.directly", -10); + m.SetIntegerValue("hello.directly", 5); - ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("hello.max")); - ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("hello.min")); - ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.default")); + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("hello.max")); + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("hello.min")); + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("hello.directly")); std::string s; m.ExportPrometheusText(s); @@ -1385,25 +1393,25 @@ ASSERT_EQ("20", u["hello.max"]); ASSERT_EQ("-10", u["hello.min"]); - ASSERT_EQ("5", u["hello.default"]); + ASSERT_EQ("5", u["hello.directly"]); } { MetricsRegistry m; - m.SetValue("a", 10); - m.SetValue("b", 10, MetricsType_MinOver10Seconds); + m.SetIntegerValue("a", 10); + m.SetIntegerValue("b", 10, MetricsUpdatePolicy_MinOver10Seconds); - m.Register("c", MetricsType_MaxOver10Seconds); - m.SetValue("c", 10, MetricsType_MinOver10Seconds); + m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + m.SetIntegerValue("c", 10, MetricsUpdatePolicy_MinOver10Seconds); - m.Register("d", MetricsType_MaxOver10Seconds); - m.Register("d", MetricsType_Default); + m.Register("d", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + ASSERT_THROW(m.Register("d", MetricsUpdatePolicy_Directly, MetricsDataType_Integer), OrthancException); - ASSERT_EQ(MetricsType_Default, m.GetMetricsType("a")); - ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b")); - ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("c")); - ASSERT_EQ(MetricsType_Default, m.GetMetricsType("d")); + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("a")); + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("b")); + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("d")); } { @@ -1411,11 +1419,47 @@ { MetricsRegistry::Timer t1(m, "a"); - MetricsRegistry::Timer t2(m, "b", MetricsType_MinOver10Seconds); + MetricsRegistry::Timer t2(m, "b", MetricsUpdatePolicy_MinOver10Seconds); } - ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("a")); - ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b")); + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("a")); + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("b")); + } + + { + MetricsRegistry m; + m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Integer); + m.SetFloatValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); + } + + { + MetricsRegistry m; + m.Register("c", MetricsUpdatePolicy_MaxOver10Seconds, MetricsDataType_Float); + m.SetIntegerValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_MaxOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Float, m.GetDataType("c")); + } + + { + MetricsRegistry m; + m.SetIntegerValue("c", 100, MetricsUpdatePolicy_MinOver10Seconds); + m.SetFloatValue("c", 101, MetricsUpdatePolicy_MaxOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_MinOver10Seconds, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); + } + + { + MetricsRegistry m; + m.SetIntegerValue("c", 100); + m.SetFloatValue("c", 101, MetricsUpdatePolicy_MaxOver10Seconds); + + ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("c")); + ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); } } #endif
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -37,6 +37,7 @@ #include <gtest/gtest.h> #include "../Sources/Compatibility.h" +#include "../Sources/DicomFormat/DicomImageInformation.h" #include "../Sources/DicomFormat/DicomPath.h" #include "../Sources/DicomNetworking/DicomFindAnswers.h" #include "../Sources/DicomParsing/DicomModification.h" @@ -314,6 +315,10 @@ ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 192")); ASSERT_EQ(Encoding_Utf8, e); ASSERT_TRUE(GetDicomEncoding(e, "GB18030")); ASSERT_EQ(Encoding_Chinese, e); ASSERT_TRUE(GetDicomEncoding(e, "GBK")); ASSERT_EQ(Encoding_Chinese, e); + + // common spelling mistakes + ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR_100")); ASSERT_EQ(Encoding_Latin1, e); + ASSERT_TRUE(GetDicomEncoding(e, "ISO_2022_IR_6")); ASSERT_EQ(Encoding_Ascii, e); } @@ -2767,7 +2772,7 @@ { DicomModification modif; - modif.SetupAnonymization(DicomVersion_2021b); + modif.SetupAnonymization(DicomVersion_2023b); modif.Apply(*dicom1); modif.Apply(*dicom2); } @@ -2794,7 +2799,7 @@ { DicomModification modif; - modif.SetupAnonymization(DicomVersion_2021b); + modif.SetupAnonymization(DicomVersion_2023b); modif.Keep(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPInstanceUID")); modif.Keep(DicomPath::Parse("RelatedSeriesSequence")); modif.Apply(*dicom); @@ -3217,6 +3222,312 @@ } +TEST(ParsedDicomFile, InjectEmptyPixelData) +{ + static const char* PIXEL_DATA = "7FE00010"; + + { + ParsedDicomFile dicom(true); + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + ASSERT_FALSE(visitor.GetResult().isMember(PIXEL_DATA)); + } + + { + ParsedDicomFile dicom(true); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord); // Must be ignored + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ(2u, visitor.GetResult() [PIXEL_DATA].size()); + ASSERT_EQ("", visitor.GetResult() [PIXEL_DATA]["InlineBinary"].asString()); + ASSERT_EQ("OB", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } + + { + ParsedDicomFile dicom(true); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord); + dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte); // Must be ignored + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ(2u, visitor.GetResult() [PIXEL_DATA].size()); + ASSERT_EQ("", visitor.GetResult() [PIXEL_DATA]["InlineBinary"].asString()); + ASSERT_EQ("OW", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } +} + + +TEST(ParsedDicomFile, RemoveFromPixelData) +{ + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0000), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0009), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint8Array(DcmTag(0x7fe0, 0x0010), NULL, 0).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0011), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe1, 0x0000), "").good()); + + { + DicomMap m; + dicom.ExtractDicomSummary(m, 0); + + ASSERT_EQ(10u, m.GetSize()); + ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PIXEL_DATA)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0011)); + ASSERT_TRUE(m.HasTag(0x7fe1, 0x0000)); + } + + dicom.RemoveFromPixelData(); + + { + DicomMap m; + dicom.ExtractDicomSummary(m, 0); + + ASSERT_EQ(7u, m.GetSize()); + ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009)); + ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA)); + ASSERT_FALSE(m.HasTag(0x7fe0, 0x0011)); + ASSERT_FALSE(m.HasTag(0x7fe1, 0x0000)); + } +} + + +TEST(ParsedDicomFile, GuessPixelDataValueRepresentation) +{ + typedef std::list< std::pair<E_TransferSyntax, DicomTransferSyntax> > Syntaxes; + + // Create a list of the main non-retired transfer syntaxes, from: + // https://www.dicomlibrary.com/dicom/transfer-syntax/ + Syntaxes compressedSyntaxes; + compressedSyntaxes.push_back(std::make_pair(EXS_DeflatedLittleEndianExplicit, DicomTransferSyntax_DeflatedLittleEndianExplicit)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess1, DicomTransferSyntax_JPEGProcess1)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess2_4, DicomTransferSyntax_JPEGProcess2_4)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess14, DicomTransferSyntax_JPEGProcess14)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGProcess14SV1, DicomTransferSyntax_JPEGProcess14SV1)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGLSLossless, DicomTransferSyntax_JPEGLSLossless)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEGLSLossy, DicomTransferSyntax_JPEGLSLossy)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000LosslessOnly, DicomTransferSyntax_JPEG2000LosslessOnly)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000, DicomTransferSyntax_JPEG2000)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000MulticomponentLosslessOnly, DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPEG2000Multicomponent, DicomTransferSyntax_JPEG2000Multicomponent)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPIPReferenced, DicomTransferSyntax_JPIPReferenced)); + compressedSyntaxes.push_back(std::make_pair(EXS_JPIPReferencedDeflate, DicomTransferSyntax_JPIPReferencedDeflate)); + compressedSyntaxes.push_back(std::make_pair(EXS_RLELossless, DicomTransferSyntax_RLELossless)); + compressedSyntaxes.push_back(std::make_pair(EXS_MPEG2MainProfileAtMainLevel, DicomTransferSyntax_MPEG2MainProfileAtMainLevel)); + compressedSyntaxes.push_back(std::make_pair(EXS_MPEG4HighProfileLevel4_1, DicomTransferSyntax_MPEG4HighProfileLevel4_1)); + compressedSyntaxes.push_back(std::make_pair(EXS_MPEG4BDcompatibleHighProfileLevel4_1, DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1)); + + for (unsigned int i = 0; i < 3; i++) + { + unsigned int bitsAllocated; + switch (i) + { + case 0: bitsAllocated = 1; break; + case 1: bitsAllocated = 8; break; + case 2: bitsAllocated = 16; break; + default: + throw OrthancException(ErrorCode_InternalError); + } + + for (Syntaxes::const_iterator it = compressedSyntaxes.begin(); it != compressedSyntaxes.end(); ++it) + { + // All the compressed transfer syntaxes must have "OB" pixel data + ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(it->second, bitsAllocated)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast<std::string>(bitsAllocated), false); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(it->second)); + } + + { + DicomMap dicom; + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(it->second)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(it->first, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + DicomTransferSyntax ts; + ASSERT_TRUE(dicom.LookupTransferSyntax(ts)); + ASSERT_EQ(ts, it->second); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation()); + } + } + + { + // Little endian implicit is always OW + ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit, bitsAllocated)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast<std::string>(bitsAllocated), false); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit)); + } + + { + DicomMap dicom; + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianImplicit)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianImplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation()); + } + } + + } + + // Explicit little and big endian with <= 8 bpp is OB + + for (unsigned int i = 0; i < 2; i++) + { + unsigned int bitsAllocated; + switch (i) + { + case 0: bitsAllocated = 1; break; + case 1: bitsAllocated = 8; break; + default: + throw OrthancException(ErrorCode_InternalError); + } + + ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit, bitsAllocated)); + ASSERT_EQ(ValueRepresentation_OtherByte, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit, bitsAllocated)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, boost::lexical_cast<std::string>(bitsAllocated), false); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit)); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit)); + } + + { + DicomMap dicom; + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit)); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation()); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, bitsAllocated).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_BigEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherByte, dicom.GuessPixelDataValueRepresentation()); + } + } + + // Explicit little and big endian with > 8 bpp is OW + + ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit, 16)); + ASSERT_EQ(ValueRepresentation_OtherWord, DicomImageInformation::GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit, 16)); + + { + DicomMap dicom; + dicom.SetValue(DICOM_TAG_BITS_ALLOCATED, "16", false); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_LittleEndianExplicit)); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation(DicomTransferSyntax_BigEndianExplicit)); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, 16).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_LittleEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation()); + } + + { + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint16(DCM_BitsAllocated, 16).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().chooseRepresentation(EXS_BigEndianExplicit, NULL).good()); + dicom.GetDcmtkObject().removeAllButCurrentRepresentations(); + ASSERT_EQ(ValueRepresentation_OtherWord, dicom.GuessPixelDataValueRepresentation()); + } +} + + +TEST(ParsedDicomFile, DISABLED_InjectEmptyPixelData2) +{ + static const char* PIXEL_DATA = "7FE00010"; + + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::string path = (std::string(getenv("HOME")) + + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string(GetTransferSyntaxUid(a)) + ".dcm"); + if (Orthanc::SystemToolbox::IsRegularFile(path)) + { + printf("\n======= %s\n", GetTransferSyntaxUid(a)); + + std::string source; + Orthanc::SystemToolbox::ReadFile(source, path); + + ParsedDicomFile dicom(source); + std::unique_ptr<DcmElement> removal(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData)); + + { + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); + ASSERT_FALSE(visitor.GetResult().isMember(PIXEL_DATA)); + } + + { + DicomWebJsonVisitor visitor; + dicom.InjectEmptyPixelData(ValueRepresentation_OtherByte); + dicom.Apply(visitor); + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ("OB", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } + + removal.reset(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData)); + + { + DicomWebJsonVisitor visitor; + dicom.InjectEmptyPixelData(ValueRepresentation_OtherWord); + dicom.Apply(visitor); + ASSERT_TRUE(visitor.GetResult().isMember(PIXEL_DATA)); + ASSERT_EQ("OW", visitor.GetResult() [PIXEL_DATA]["vr"].asString()); + } + } + } +} + + #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 @@ -3236,11 +3547,13 @@ DcmtkTranscoder transcoder; for (int j = 0; j < 2; j++) + { for (int i = 0; i <= DicomTransferSyntax_XML; i++) { DicomTransferSyntax a = (DicomTransferSyntax) i; - std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string path = (std::string(getenv("HOME")) + + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + std::string(GetTransferSyntaxUid(a)) + ".dcm"); if (Orthanc::SystemToolbox::IsRegularFile(path)) { @@ -3268,6 +3581,7 @@ } } } + } } @@ -3277,7 +3591,8 @@ { std::string source; - Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm"); + Orthanc::SystemToolbox::ReadFile(source, std::string(getenv("HOME")) + + "/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm"); toto.reset(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size())); }
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -131,6 +131,11 @@ { return false; } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } };
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -384,6 +384,7 @@ private: std::string type_; std::string subtype_; + HttpContentNegociation::Dictionary parameters_; public: AcceptHandler() @@ -393,7 +394,8 @@ void Reset() { - Handle("nope", "nope"); + HttpContentNegociation::Dictionary parameters; + Handle("nope", "nope", parameters); } const std::string& GetType() const @@ -406,11 +408,18 @@ return subtype_; } + HttpContentNegociation::Dictionary& GetParameters() + { + return parameters_; + } + virtual void Handle(const std::string& type, - const std::string& subtype) ORTHANC_OVERRIDE + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE { type_ = type; subtype_ = subtype; + parameters_ = parameters; } }; } @@ -430,22 +439,29 @@ ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic")); ASSERT_EQ("audio", h.GetType()); ASSERT_EQ("basic", h.GetSubType()); + ASSERT_EQ(0u, h.GetParameters().size()); - ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/nope")); + ASSERT_TRUE(d.Apply("audio/*; q=0.2 ; type = test ; hello , audio/nope")); ASSERT_EQ("audio", h.GetType()); ASSERT_EQ("mp3", h.GetSubType()); + ASSERT_EQ(3u, h.GetParameters().size()); + ASSERT_EQ("0.2", h.GetParameters() ["q"]); + ASSERT_EQ("test", h.GetParameters() ["type"]); + ASSERT_EQ("", h.GetParameters() ["hello"]); ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf")); - ASSERT_TRUE(d.Apply("*/*; application/*; q=0.2, application/pdf")); + ASSERT_TRUE(d.Apply("*/*; hello=world, application/*; q=0.2, application/pdf")); ASSERT_EQ("audio", h.GetType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("world", h.GetParameters() ["hello"]); } // "This would be interpreted as "text/html and text/x-c are the // preferred media types, but if they do not exist, then send the // text/x-dvi entity, and if that does not exist, send the // text/plain entity."" - const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"; + const std::string T1 = "text/plain; q=0.5, text/html ; hello = \"world\" , text/x-dvi; q=0.8, text/x-c"; { HttpContentNegociation d; @@ -455,6 +471,8 @@ ASSERT_TRUE(d.Apply(T1)); ASSERT_EQ("text", h.GetType()); ASSERT_EQ("html", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("world", h.GetParameters() ["hello"]); } { @@ -465,6 +483,7 @@ ASSERT_TRUE(d.Apply(T1)); ASSERT_EQ("text", h.GetType()); ASSERT_EQ("x-c", h.GetSubType()); + ASSERT_EQ(0u, h.GetParameters().size()); } { @@ -476,6 +495,15 @@ ASSERT_TRUE(d.Apply(T1)); ASSERT_EQ("text", h.GetType()); ASSERT_TRUE(h.GetSubType() == "x-c" || h.GetSubType() == "html"); + if (h.GetSubType() == "x-c") + { + ASSERT_EQ(0u, h.GetParameters().size()); + } + else + { + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("world", h.GetParameters() ["hello"]); + } } { @@ -485,6 +513,8 @@ ASSERT_TRUE(d.Apply(T1)); ASSERT_EQ("text", h.GetType()); ASSERT_EQ("x-dvi", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); } { @@ -493,6 +523,51 @@ ASSERT_TRUE(d.Apply(T1)); ASSERT_EQ("text", h.GetType()); ASSERT_EQ("plain", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.5", h.GetParameters() ["q"]); + } + + // Below are the tests from issue 216: + // https://bugs.orthanc-server.com/show_bug.cgi?id=216 + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("image/webp, */*;q=0.8, text/html, application/xhtml+xml, application/xml;q=0.9")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); + } + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("image/webp, */*; q = \"0.8\" , text/html, application/xhtml+xml, application/xml;q=0.9")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); + } + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("text/html, application/xhtml+xml, application/xml, image/webp, */*;q=0.8")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ("0.8", h.GetParameters() ["q"]); + } + + { + HttpContentNegociation d; + d.Register("application/dicom+json", h); + ASSERT_TRUE(d.Apply("text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2")); + ASSERT_EQ("application", h.GetType()); + ASSERT_EQ("dicom+json", h.GetSubType()); + ASSERT_EQ(1u, h.GetParameters().size()); + ASSERT_EQ(".2", h.GetParameters() ["q"]); } }
--- a/OrthancServer/OrthancExplorer/explorer.html Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/OrthancExplorer/explorer.html Tue Jul 04 12:20:41 2023 +0200 @@ -536,6 +536,7 @@ <input type="checkbox" name="DR" id="qr-dr" class="custom" /> <label for="qr-dr">DR</label> <input type="checkbox" name="DX" id="qr-dx" class="custom" /> <label for="qr-dx">DX</label> <input type="checkbox" name="MG" id="qr-mg" class="custom" /> <label for="qr-mg">MG</label> + <input type="checkbox" name="XC" id="qr-xc" class="custom" /> <label for="qr-xc">XC</label> </fieldset> </div> </div>
--- a/OrthancServer/OrthancExplorer/explorer.js Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/OrthancExplorer/explorer.js Tue Jul 04 12:20:41 2023 +0200 @@ -1363,7 +1363,7 @@ href: '#', rel: 'close', text: name - }) + }); liElement.append(aElement); items.append(liElement);
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -67,6 +67,7 @@ #include "PluginsEnumerations.h" #include "PluginsJob.h" +#include <boost/math/special_functions/round.hpp> #include <boost/regex.hpp> #include <dcmtk/dcmdata/dcdict.h> #include <dcmtk/dcmdata/dcdicent.h> @@ -2060,6 +2061,7 @@ sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction) || + sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) || @@ -2502,9 +2504,8 @@ std::string buffer_; std::unique_ptr<DicomInstanceToStore> instance_; - public: - DicomInstanceFromBuffer(const void* buffer, - size_t size) + void Setup(const void* buffer, + size_t size) { buffer_.assign(reinterpret_cast<const char*>(buffer), size); @@ -2512,6 +2513,18 @@ instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); } + public: + DicomInstanceFromBuffer(const void* buffer, + size_t size) + { + Setup(buffer, size); + } + + explicit DicomInstanceFromBuffer(const std::string& buffer) + { + Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size()); + } + virtual bool CanBeFreed() const ORTHANC_OVERRIDE { return true; @@ -2524,23 +2537,36 @@ }; - class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance + class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance { private: std::unique_ptr<ParsedDicomFile> parsed_; std::unique_ptr<DicomInstanceToStore> instance_; - public: - explicit DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) : - parsed_(transcoded.ReleaseAsParsedDicomFile()) - { + void Setup(ParsedDicomFile* parsed) + { + parsed_.reset(parsed); + if (parsed_.get() == NULL) { - throw OrthancException(ErrorCode_InternalError); - } - - instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); - instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + throw OrthancException(ErrorCode_NullPointer); + } + else + { + instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + } + + public: + explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded) + { + Setup(transcoded.ReleaseAsParsedDicomFile()); + } + + explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */) + { + Setup(parsed); } virtual bool CanBeFreed() const ORTHANC_OVERRIDE @@ -4386,7 +4412,115 @@ reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers); } - + + + void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params) + { + std::unique_ptr<IDicomInstance> target; + + switch (params.mode) + { + case OrthancPluginLoadDicomInstanceMode_WholeDicom: + { + std::string buffer; + + { + PImpl::ServerContextLock lock(*pimpl_); + lock.GetContext().ReadDicom(buffer, params.instanceId); + } + + target.reset(new DicomInstanceFromBuffer(buffer)); + break; + } + + case OrthancPluginLoadDicomInstanceMode_UntilPixelData: + case OrthancPluginLoadDicomInstanceMode_EmptyPixelData: + { + std::unique_ptr<ParsedDicomFile> parsed; + + { + std::string buffer; + + { + PImpl::ServerContextLock lock(*pimpl_); + if (!lock.GetContext().ReadDicomUntilPixelData(buffer, params.instanceId)) + { + lock.GetContext().ReadDicom(buffer, params.instanceId); + } + } + + parsed.reset(new ParsedDicomFile(buffer)); + } + + parsed->RemoveFromPixelData(); + + if (params.mode == OrthancPluginLoadDicomInstanceMode_EmptyPixelData) + { + bool hasPixelData = false; + ValueRepresentation pixelDataVR = parsed->GuessPixelDataValueRepresentation(); + + { + PImpl::ServerContextLock lock(*pimpl_); + + std::string s; + int64_t revision; // unused + if (lock.GetContext().GetIndex().LookupMetadata( + s, revision, params.instanceId, + ResourceType_Instance, MetadataType_Instance_PixelDataVR)) + { + hasPixelData = true; + if (s == "OB") + { + pixelDataVR = ValueRepresentation_OtherByte; + } + else if (s == "OW") + { + pixelDataVR = ValueRepresentation_OtherWord; + } + else + { + LOG(WARNING) << "Corrupted PixelDataVR metadata associated with instance " + << params.instanceId << ": " << s; + } + } + else if (lock.GetContext().GetIndex().LookupMetadata( + s, revision, params.instanceId, + ResourceType_Instance, MetadataType_Instance_PixelDataOffset)) + { + // This file was stored by an older version of Orthanc, + // "PixelDataVR" is not available, so use the guess + hasPixelData = true; + } + else + { + hasPixelData = false; + } + } + + if (hasPixelData) + { + parsed->InjectEmptyPixelData(pixelDataVR); + } + } + + target.reset(new DicomInstanceFromParsed(parsed.release())); + break; + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (target.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + *params.target = reinterpret_cast<OrthancPluginDicomInstance*>(target.release()); + } + } + void OrthancPlugins::DatabaseAnswer(const void* parameters) { @@ -5163,24 +5297,22 @@ const _OrthancPluginSetMetricsValue& p = *reinterpret_cast<const _OrthancPluginSetMetricsValue*>(parameters); - MetricsType type; - switch (p.type) - { - case OrthancPluginMetricsType_Default: - type = MetricsType_Default; - break; - - case OrthancPluginMetricsType_Timer: - type = MetricsType_MaxOver10Seconds; - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - { PImpl::ServerContextLock lock(*pimpl_); - lock.GetContext().GetMetricsRegistry().SetValue(p.name, p.value, type); + lock.GetContext().GetMetricsRegistry().SetFloatValue(p.name, p.value, Plugins::Convert(p.type)); + } + + return true; + } + + case _OrthancPluginService_SetMetricsIntegerValue: + { + const _OrthancPluginSetMetricsIntegerValue& p = + *reinterpret_cast<const _OrthancPluginSetMetricsIntegerValue*>(parameters); + + { + PImpl::ServerContextLock lock(*pimpl_); + lock.GetContext().GetMetricsRegistry().SetIntegerValue(p.name, p.value, Plugins::Convert(p.type)); } return true; @@ -5279,7 +5411,7 @@ if (success) { *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>( - new DicomInstanceFromTranscoded(transcoded)); + new DicomInstanceFromParsed(transcoded)); return true; } else @@ -5341,6 +5473,14 @@ RegisterIncomingHttpRequestFilter2(parameters); return true; + case _OrthancPluginService_LoadDicomInstance: + { + const _OrthancPluginLoadDicomInstance& p = + *reinterpret_cast<const _OrthancPluginLoadDicomInstance*>(parameters); + ApplyLoadDicomInstance(p); + return true; + } + default: return false; }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Jul 04 12:20:41 2023 +0200 @@ -89,7 +89,7 @@ class IDicomInstance; class DicomInstanceFromCallback; class DicomInstanceFromBuffer; - class DicomInstanceFromTranscoded; + class DicomInstanceFromParsed; class WebDavCollection; void RegisterRestCallback(const void* parameters, @@ -217,6 +217,8 @@ void ApplySendMultipartItem2(const void* parameters); + void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters);
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -578,5 +578,21 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + MetricsUpdatePolicy Convert(OrthancPluginMetricsType type) + { + switch (type) + { + case OrthancPluginMetricsType_Default: + return MetricsUpdatePolicy_Directly; + + case OrthancPluginMetricsType_Timer: + return MetricsUpdatePolicy_MaxOver10Seconds; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } } }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h Tue Jul 04 12:20:41 2023 +0200 @@ -31,6 +31,7 @@ * "orthanc-databases" project. **/ +#include "../../../OrthancFramework/Sources/MetricsRegistry.h" #include "../../Sources/Search/DatabaseConstraint.h" #include "../../Sources/ServerEnumerations.h" #include "../Include/orthanc/OrthancCPlugin.h" @@ -71,6 +72,8 @@ JobStepCode Convert(OrthancPluginJobStepStatus step); StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason); + + MetricsUpdatePolicy Convert(OrthancPluginMetricsType type); } }
--- a/OrthancServer/Plugins/Engine/PluginsJob.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsJob.h Tue Jul 04 12:20:41 2023 +0200 @@ -76,6 +76,12 @@ // TODO return false; } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + // TODO + return false; + } }; }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Jul 04 12:20:41 2023 +0200 @@ -120,7 +120,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 1 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -447,6 +447,7 @@ _OrthancPluginService_CreateMemoryBuffer64 = 40, /* New in Orthanc 1.9.0 */ _OrthancPluginService_CreateDicom2 = 41, /* New in Orthanc 1.9.0 */ _OrthancPluginService_GetDatabaseServerIdentifier = 42, /* New in Orthanc 1.11.1 */ + _OrthancPluginService_SetMetricsIntegerValue = 43, /* New in Orthanc 1.12.1 */ /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -526,6 +527,7 @@ _OrthancPluginService_GetInstanceAdvancedJson = 4017, /* New in Orthanc 1.7.0 */ _OrthancPluginService_GetInstanceDicomWebJson = 4018, /* New in Orthanc 1.7.0 */ _OrthancPluginService_GetInstanceDicomWebXml = 4019, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_LoadDicomInstance = 4020, /* New in Orthanc 1.12.1 */ /* Services for plugins implementing a database back-end */ _OrthancPluginService_RegisterDatabaseBackend = 5000, /* New in Orthanc 0.8.6 */ @@ -1024,6 +1026,28 @@ /** + * Mode specifying how to load a DICOM instance. + * @see OrthancPluginLoadDicomInstance() + **/ + typedef enum + { + OrthancPluginLoadDicomInstanceMode_WholeDicom = 1, + /*!< Load the whole DICOM file, including pixel data */ + + OrthancPluginLoadDicomInstanceMode_UntilPixelData = 2, + /*!< Load the whole DICOM file until pixel data, which will speed + up the loading */ + + OrthancPluginLoadDicomInstanceMode_EmptyPixelData = 3, + /*!< Load the whole DICOM file until pixel data, and replace pixel + data by an empty tag whose VR (value representation) is the same + as those of the original DICOM file */ + + _OrthancPluginLoadDicomInstanceMode_INTERNAL = 0x7fffffff + } OrthancPluginLoadDicomInstanceMode; + + + /** * @brief A 32-bit memory buffer allocated by the core system of Orthanc. * * A memory buffer allocated by the core system of Orthanc. When the @@ -1723,7 +1747,8 @@ * Signature of a callback function that is called by Orthanc * whenever a monitoring tool (such as Prometheus) asks the current * values of the metrics. This callback gives the plugin a chance to - * update its metrics, by calling OrthancPluginSetMetricsValue(). + * update its metrics, by calling OrthancPluginSetMetricsValue() or + * OrthancPluginSetMetricsIntegerValue(). * This is typically useful for metrics that are expensive to * acquire. * @@ -1874,7 +1899,7 @@ * @param expectedRevision Expected revision. * @return 1 if and only if the versions are compatible. If the * result is 0, the initialization of the plugin should fail. - * @see OrthancPluginCheckVersion + * @see OrthancPluginCheckVersion() * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE int OrthancPluginCheckVersionAdvanced( @@ -1906,7 +1931,7 @@ sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || - sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction)) + sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode)) { /* Mismatch in the size of the enumerations */ return 0; @@ -1980,7 +2005,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @return 1 if and only if the versions are compatible. If the * result is 0, the initialization of the plugin should fail. - * @see OrthancPluginCheckVersionAdvanced + * @see OrthancPluginCheckVersionAdvanced() * @ingroup Callbacks **/ ORTHANC_PLUGIN_INLINE int OrthancPluginCheckVersion( @@ -2340,7 +2365,7 @@ * @param uri The URI in the built-in Orthanc API. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiGetAfterPlugins + * @see OrthancPluginRestApiGetAfterPlugins() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGet( @@ -2370,7 +2395,7 @@ * @param uri The URI in the built-in Orthanc API. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiGet + * @see OrthancPluginRestApiGet() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGetAfterPlugins( @@ -2407,7 +2432,7 @@ * @param bodySize The size of the body. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiPostAfterPlugins + * @see OrthancPluginRestApiPostAfterPlugins() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPost( @@ -2442,7 +2467,7 @@ * @param bodySize The size of the body. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiPost + * @see OrthancPluginRestApiPost() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPostAfterPlugins( @@ -2471,7 +2496,7 @@ * @param uri The URI to delete in the built-in Orthanc API. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiDeleteAfterPlugins + * @see OrthancPluginRestApiDeleteAfterPlugins() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiDelete( @@ -2494,7 +2519,7 @@ * @param uri The URI to delete in the built-in Orthanc API. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiDelete + * @see OrthancPluginRestApiDelete() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiDeleteAfterPlugins( @@ -2519,7 +2544,7 @@ * @param bodySize The size of the body. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiPutAfterPlugins + * @see OrthancPluginRestApiPutAfterPlugins() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPut( @@ -2555,7 +2580,7 @@ * @param bodySize The size of the body. * @return 0 if success, or the error code if failure. * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. - * @see OrthancPluginRestApiPut + * @see OrthancPluginRestApiPut() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPutAfterPlugins( @@ -5033,7 +5058,7 @@ * @return The NULL value if the case of an error, or the JSON * string. This string must be freed by OrthancPluginFreeString(). * @ingroup Toolbox - * @see OrthancPluginDicomInstanceToJson + * @see OrthancPluginDicomInstanceToJson() **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson( OrthancPluginContext* context, @@ -5082,7 +5107,7 @@ * @return The NULL value if the case of an error, or the JSON * string. This string must be freed by OrthancPluginFreeString(). * @ingroup Toolbox - * @see OrthancPluginDicomInstanceToJson + * @see OrthancPluginDicomInstanceToJson() **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomInstanceToJson( OrthancPluginContext* context, @@ -5139,7 +5164,7 @@ * @param afterPlugins If 0, the built-in API of Orthanc is used. * If 1, the API is tainted by the plugins. * @return 0 if success, or the error code if failure. - * @see OrthancPluginRestApiGet, OrthancPluginRestApiGetAfterPlugins + * @see OrthancPluginRestApiGet(), OrthancPluginRestApiGetAfterPlugins() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGet2( @@ -5402,8 +5427,8 @@ * @param flags Flags governing the output. * @return 0 if success, other value if error. * @ingroup Toolbox - * @see OrthancPluginCreateDicom2 - * @see OrthancPluginDicomBufferToJson + * @see OrthancPluginCreateDicom2() + * @see OrthancPluginDicomBufferToJson() **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom( OrthancPluginContext* context, @@ -7055,11 +7080,12 @@ } _OrthancPluginSetMetricsValue; /** - * @brief Set the value of a metrics. - * - * This function sets the value of a metrics to monitor the behavior - * of the plugin through tools such as Prometheus. The values of all - * the metrics are stored within the Orthanc context. + * @brief Set the value of a floating-point metrics. + * + * This function sets the value of a floating-point metrics to + * monitor the behavior of the plugin through tools such as + * Prometheus. The values of all the metrics are stored within the + * Orthanc context. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param name The name of the metrics to be set. @@ -7067,6 +7093,7 @@ * @param type The type of the metrics. This parameter is only taken into consideration * the first time this metrics is set. * @ingroup Toolbox + * @see OrthancPluginSetMetricsIntegerValue() **/ ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue( OrthancPluginContext* context, @@ -7092,7 +7119,8 @@ * @brief Register a callback to refresh the metrics. * * This function registers a callback to refresh the metrics. The - * callback must make calls to OrthancPluginSetMetricsValue(). + * callback must make calls to OrthancPluginSetMetricsValue() or + * OrthancPluginSetMetricsIntegerValue(). * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param callback The callback function to handle the refresh. @@ -8402,7 +8430,7 @@ * @return The NULL value if the case of an error, or the JSON * string. This string must be freed by OrthancPluginFreeString(). * @ingroup DicomInstance - * @see OrthancPluginDicomBufferToJson + * @see OrthancPluginDicomBufferToJson() **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson( OrthancPluginContext* context, @@ -8759,8 +8787,8 @@ * Check out the global configuration option "Dictionary" of Orthanc. * @return 0 if success, other value if error. * @ingroup Toolbox - * @see OrthancPluginCreateDicom - * @see OrthancPluginDicomBufferToJson + * @see OrthancPluginCreateDicom() + * @see OrthancPluginDicomBufferToJson() **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom2( OrthancPluginContext* context, @@ -8827,7 +8855,7 @@ * @param afterPlugins If 0, the built-in API of Orthanc is used. * If 1, the API is tainted by the plugins. * @return 0 if success, or the error code if failure. - * @see OrthancPluginRestApiGet2, OrthancPluginRestApiPost, OrthancPluginRestApiPut, OrthancPluginRestApiDelete + * @see OrthancPluginRestApiGet2(), OrthancPluginRestApiPost(), OrthancPluginRestApiPut(), OrthancPluginRestApiDelete() * @ingroup Orthanc **/ ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCallRestApi( @@ -9225,6 +9253,87 @@ return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, ¶ms); } + + typedef struct + { + OrthancPluginDicomInstance** target; + const char* instanceId; + OrthancPluginLoadDicomInstanceMode mode; + } _OrthancPluginLoadDicomInstance; + + /** + * @brief Load a DICOM instance from the Orthanc server. + * + * This function loads a DICOM instance from the content of the + * Orthanc database. The function returns a new pointer to a data + * structure that is managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The Orthanc identifier of the DICOM instance of interest. + * @param mode Flag specifying how to deal with pixel data. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginLoadDicomInstance( + OrthancPluginContext* context, + const char* instanceId, + OrthancPluginLoadDicomInstanceMode mode) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginLoadDicomInstance params; + params.target = ⌖ + params.instanceId = instanceId; + params.mode = mode; + + if (context->InvokeService(context, _OrthancPluginService_LoadDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + const char* name; + int64_t value; + OrthancPluginMetricsType type; + } _OrthancPluginSetMetricsIntegerValue; + + /** + * @brief Set the value of an integer metrics. + * + * This function sets the value of an integer metrics to monitor the + * behavior of the plugin through tools such as Prometheus. The + * values of all the metrics are stored within the Orthanc context. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param name The name of the metrics to be set. + * @param value The value of the metrics. + * @param type The type of the metrics. This parameter is only taken into consideration + * the first time this metrics is set. + * @ingroup Toolbox + * @see OrthancPluginSetMetricsValue() + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsIntegerValue( + OrthancPluginContext* context, + const char* name, + int64_t value, + OrthancPluginMetricsType type) + { + _OrthancPluginSetMetricsIntegerValue params; + params.name = name; + params.value = value; + params.type = type; + context->InvokeService(context, _OrthancPluginService_SetMetricsIntegerValue, ¶ms); + } + + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c Tue Jul 04 12:20:41 2023 +0200 @@ -282,6 +282,63 @@ } +OrthancPluginErrorCode CallbackDicomWeb(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + OrthancPluginLoadDicomInstanceMode mode = OrthancPluginLoadDicomInstanceMode_WholeDicom; + OrthancPluginDicomInstance* instance; + char* json; + + if (request->getCount == 1) + { + if (strcmp(request->getKeys[0], "until-pixel-data") == 0) + { + mode = OrthancPluginLoadDicomInstanceMode_UntilPixelData; + } + else if (strcmp(request->getKeys[0], "empty-pixel-data") == 0) + { + mode = OrthancPluginLoadDicomInstanceMode_EmptyPixelData; + } + else + { + return OrthancPluginErrorCode_ParameterOutOfRange; + } + } + + instance = OrthancPluginLoadDicomInstance(context, request->groups[0], mode); + if (instance == NULL) + { + return OrthancPluginErrorCode_UnknownResource; + } + + json = OrthancPluginEncodeDicomWebXml(context, + OrthancPluginGetInstanceData(context, instance), + OrthancPluginGetInstanceSize(context, instance), + DicomWebBinaryCallback); + OrthancPluginFreeDicomInstance(context, instance); + + if (json != NULL) + { + OrthancPluginAnswerBuffer(context, output, json, strlen(json), "application/json"); + OrthancPluginFreeString(context, json); + } + else + { + return OrthancPluginErrorCode_InternalError; + } + } + + return OrthancPluginErrorCode_Success; +} + + OrthancPluginErrorCode OnStoredCallback(const OrthancPluginDicomInstance* instance, const char* instanceId) { @@ -511,6 +568,7 @@ OrthancPluginRegisterRestCallback(context, "/forward/(built-in)(/.+)", Callback5); OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5); OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom); + OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/dicom-web", CallbackDicomWeb); OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback); OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -253,14 +253,15 @@ // helper class to convert std::map of headers to the plugin SDK C structure class PluginHttpHeaders { + private: std::vector<const char*> headersKeys_; std::vector<const char*> headersValues_; + public: - - PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders) + explicit PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders) { for (std::map<std::string, std::string>::const_iterator - it = httpHeaders.begin(); it != httpHeaders.end(); it++) + it = httpHeaders.begin(); it != httpHeaders.end(); ++it) { headersKeys_.push_back(it->first.c_str()); headersValues_.push_back(it->second.c_str()); @@ -3751,6 +3752,27 @@ #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1) + DicomInstance* DicomInstance::Load(const std::string& instanceId, + OrthancPluginLoadDicomInstanceMode mode) + { + OrthancPluginDicomInstance* instance = OrthancPluginLoadDicomInstance( + GetGlobalContext(), instanceId.c_str(), mode); + + if (instance == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + boost::movelib::unique_ptr<DicomInstance> result(new DicomInstance(instance)); + result->toFree_ = true; + return result.release(); + } + } +#endif + + #if HAS_ORTHANC_PLUGIN_WEBDAV == 1 static std::vector<std::string> WebDavConvertPath(uint32_t pathSize, const char* const* pathItems)
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Tue Jul 04 12:20:41 2023 +0200 @@ -1264,6 +1264,11 @@ ~DicomInstance(); + const OrthancPluginDicomInstance* GetObject() const + { + return instance_; + } + std::string GetRemoteAet() const; const void* GetBuffer() const @@ -1318,6 +1323,11 @@ size_t size, const std::string& transferSyntax); #endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1) + static DicomInstance* Load(const std::string& instanceId, + OrthancPluginLoadDicomInstanceMode mode); +#endif }; // helper method to convert Http headers from the plugin SDK to a std::map
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/LargeDeleteJob.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -168,6 +168,7 @@ levels_(levels), posResources_(0), posInstances_(0), + posSeries_(0), posDelete_(0) { if (resources.size() != levels.size())
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -127,7 +127,7 @@ Json::Value::Members names = scheduleConfiguration.getMemberNames(); for (Json::Value::Members::const_iterator it = names.begin(); - it != names.end(); it++) + it != names.end(); ++it) { for (Json::Value::ArrayIndex i = 0; i < scheduleConfiguration[*it].size(); i++) { @@ -144,7 +144,7 @@ } for (std::list<RunningPeriod>::const_iterator it = runningPeriods_.begin(); - it != runningPeriods_.end(); it++) + it != runningPeriods_.end(); ++it) { if (it->isInPeriod()) {
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -151,11 +151,12 @@ fs::path source(folder_); fs::directory_iterator end; - unsigned int parsedFilesCount = 0; - unsigned int matchedWorklistCount = 0; try { + unsigned int parsedFilesCount = 0; + unsigned int matchedWorklistCount = 0; + for (fs::directory_iterator it(source); it != end; ++it) { fs::file_type type(it->status().type());
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MoveRequestHandler.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -47,12 +47,12 @@ { } - unsigned int GetSubOperationCount() const + virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE { return 1; } - Status DoNext() + virtual Status DoNext() ORTHANC_OVERRIDE { Json::Value answer; @@ -95,15 +95,15 @@ if (OrthancPlugins::RestApiPost(response, "/tools/find", request, false) && response.type() == Json::arrayValue) { - for (Json::Value::ArrayIndex i = 0; i < response.size(); i++) + for (Json::Value::ArrayIndex j = 0; j < response.size(); j++) { - if (response[i].type() != Json::stringValue) + if (response[j].type() != Json::stringValue) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } else { - publicIds.insert(response[i].asString()); + publicIds.insert(response[j].asString()); } } }
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h Tue Jul 04 12:20:41 2023 +0200 @@ -61,7 +61,7 @@ std::unique_ptr<Orthanc::DicomServer> server_; public: - MultitenantDicomServer(const Json::Value& serverConfig); + explicit MultitenantDicomServer(const Json::Value& serverConfig); void Start();
--- a/OrthancServer/Resources/Configuration.json Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Resources/Configuration.json Tue Jul 04 12:20:41 2023 +0200 @@ -852,9 +852,9 @@ // If "DeidentifyLogs" is true, this sets the DICOM standard to // follow for the deidentification/anonymization of the query - // contents. Possible values are "2008", "2017c" and "2021b" (new - // in Orthanc 1.8.2) - "DeidentifyLogsDicomVersion" : "2021b", + // contents. Possible values are "2008", "2017c", "2021b" (new + // in Orthanc 1.8.2), and "2023b" (new in Orthanc 1.12.1) + "DeidentifyLogsDicomVersion" : "2023b", // Maximum length of the PDU (Protocol Data Unit) in the DICOM // network protocol, expressed in bytes. This value affects both
--- a/OrthancServer/Resources/GenerateAnonymizationProfile.py Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Resources/GenerateAnonymizationProfile.py Tue Jul 04 12:20:41 2023 +0200 @@ -26,7 +26,7 @@ import xml.etree.ElementTree as ET # Usage: -# ./GenerateAnonymizationProfile.py https://raw.githubusercontent.com/jodogne/dicom-specification/master/2021b/part15.xml +# ./GenerateAnonymizationProfile.py https://raw.githubusercontent.com/jodogne/dicom-specification/master/2023b/part15.xml > ../../OrthancFramework/Sources/DicomParsing/DicomModification_Anonymization2023b.impl.h if len(sys.argv) != 2: raise Exception('Please provide the path or the URL to the part15.xml file from the DICOM standard')
--- a/OrthancServer/Resources/ImplementationNotes/memory_consumption.txt Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Resources/ImplementationNotes/memory_consumption.txt Tue Jul 04 12:20:41 2023 +0200 @@ -1,6 +1,7 @@ In Orthanc 1.11.3, we have introduced a Housekeeper thread that tries to give back unused memory back to the system. This is implemented -by calling malloc_trim every 100ms. +by calling malloc_trim every 30s (note: on 1.11.3 and 1.12.0, the interval +was 100ms which caused high idle CPU load). Here is how we validated the effect of this new feature: @@ -133,3 +134,16 @@ specifies the minimum size that is released. So, even without malloc_trim, Orthanc is able to give back memory to the system. - free() never gives back block allocated by mmap() to the system, only malloc_trim() does ! + +UPDATE on June 2023: +------------------- + +Given this discussion: https://discourse.orthanc-server.org/t/onchange-callbacks-and-cpu-loads/3534, +changed the interval from 100ms to 30s. +We also added a metrics to monitor the duration: orthanc_memory_trimming_duration_ms + +Good reference article: +https://www.algolia.com/blog/engineering/when-allocators-are-hoarding-your-precious-memory/ + + +
--- a/OrthancServer/Resources/RunCppCheck.sh Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Tue Jul 04 12:20:41 2023 +0200 @@ -12,8 +12,8 @@ constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp -nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322 -stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1475 +nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:315 +stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1476 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:165 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:73 stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:373 @@ -22,7 +22,7 @@ stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:190 stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:334 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:52 -syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:72 +syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:73 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:132 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:310 uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:416 @@ -36,8 +36,9 @@ assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1025 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:289 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:388 -assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3526 +assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3551 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:272 +assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:453 EOF ${CPPCHECK} --enable=all --quiet --std=c++11 \ @@ -88,6 +89,7 @@ -D__cplusplus=201103 \ -D__linux__ \ -UNDEBUG \ + -DHAS_ORTHANC_EXCEPTION=1 \ \ ../../OrthancFramework/Sources \ ../../OrthancFramework/UnitTestsSources \ @@ -95,5 +97,11 @@ ../../OrthancServer/Plugins/Include \ ../../OrthancServer/Sources \ ../../OrthancServer/UnitTestsSources \ + ../../OrthancServer/Plugins/Samples/Common \ + ../../OrthancServer/Plugins/Samples/ConnectivityChecks \ + ../../OrthancServer/Plugins/Samples/DelayedDeletion \ + ../../OrthancServer/Plugins/Samples/Housekeeper \ + ../../OrthancServer/Plugins/Samples/ModalityWorklists \ + ../../OrthancServer/Plugins/Samples/MultitenantDicom \ \ 2>&1
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -2036,6 +2036,24 @@ remainingAncestor_["RemainingAncestor"]["Path"] = GetBasePath(remainingLevel, remainingPublicId); remainingAncestor_["RemainingAncestor"]["Type"] = EnumerationToString(remainingLevel); remainingAncestor_["RemainingAncestor"]["ID"] = remainingPublicId; + + { // update the LastUpdate metadata of all parents + std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); + ResourcesContent content(true); + + int64_t parentId = 0; + if (transaction.LookupResource(parentId, remainingLevel, remainingPublicId)) + { + + do + { + content.AddMetadata(parentId, MetadataType_LastUpdate, now); + } + while (transaction.LookupParent(parentId, parentId)); + + transaction.SetResourcesContent(content); + } + } } else { @@ -2938,6 +2956,7 @@ DicomTransferSyntax transferSyntax, bool hasPixelDataOffset, uint64_t pixelDataOffset, + ValueRepresentation pixelDataVR, MaxStorageMode maximumStorageMode, uint64_t maximumStorageSize, unsigned int maximumPatients, @@ -2957,6 +2976,7 @@ DicomTransferSyntax transferSyntax_; bool hasPixelDataOffset_; uint64_t pixelDataOffset_; + ValueRepresentation pixelDataVR_; MaxStorageMode maximumStorageMode_; uint64_t maximumStorageSize_; unsigned int maximumPatientCount_; @@ -3060,6 +3080,7 @@ DicomTransferSyntax transferSyntax, bool hasPixelDataOffset, uint64_t pixelDataOffset, + ValueRepresentation pixelDataVR, MaxStorageMode maximumStorageMode, uint64_t maximumStorageSize, unsigned int maximumPatientCount, @@ -3075,6 +3096,7 @@ transferSyntax_(transferSyntax), hasPixelDataOffset_(hasPixelDataOffset), pixelDataOffset_(pixelDataOffset), + pixelDataVR_(pixelDataVR), maximumStorageMode_(maximumStorageMode), maximumStorageSize_(maximumStorageSize), maximumPatientCount_(maximumPatientCount), @@ -3098,326 +3120,329 @@ virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE { - try + IDatabaseWrapper::CreateInstanceResult status; + int64_t instanceId; + + bool isNewInstance = transaction.CreateInstance(status, instanceId, hashPatient_, + hashStudy_, hashSeries_, hashInstance_); + + if (isReconstruct_ && isNewInstance) { - IDatabaseWrapper::CreateInstanceResult status; - int64_t instanceId; - - bool isNewInstance = transaction.CreateInstance(status, instanceId, hashPatient_, - hashStudy_, hashSeries_, hashInstance_); - - if (isReconstruct_ && isNewInstance) - { - // In case of reconstruct, we just want to modify the attachments and some metadata like the TransferSyntex - // The DicomTags and many metadata have already been updated before we get here in ReconstructInstance - throw OrthancException(ErrorCode_InternalError, "New instance while reconstructing; this should not happen."); - } - - // Check whether this instance is already stored - if (!isNewInstance && !isReconstruct_) + // In case of reconstruct, we just want to modify the attachments and some metadata like the TransferSyntex + // The DicomTags and many metadata have already been updated before we get here in ReconstructInstance + throw OrthancException(ErrorCode_InternalError, "New instance while reconstructing; this should not happen."); + } + + // Check whether this instance is already stored + if (!isNewInstance && !isReconstruct_) + { + // The instance already exists + if (overwrite_) { - // The instance already exists - if (overwrite_) + // Overwrite the old instance + LOG(INFO) << "Overwriting instance: " << hashInstance_; + transaction.DeleteResource(instanceId); + + // Re-create the instance, now that the old one is removed + if (!transaction.CreateInstance(status, instanceId, hashPatient_, + hashStudy_, hashSeries_, hashInstance_)) { - // Overwrite the old instance - LOG(INFO) << "Overwriting instance: " << hashInstance_; - transaction.DeleteResource(instanceId); - - // Re-create the instance, now that the old one is removed - if (!transaction.CreateInstance(status, instanceId, hashPatient_, - hashStudy_, hashSeries_, hashInstance_)) - { - throw OrthancException(ErrorCode_InternalError, "No new instance while overwriting; this should not happen."); - } - } - else - { - // Do nothing if the instance already exists and overwriting is disabled - transaction.GetAllMetadata(instanceMetadata_, instanceId); - storeStatus_ = StoreStatus_AlreadyStored; - return; + throw OrthancException(ErrorCode_InternalError, "No new instance while overwriting; this should not happen."); } } - - - if (!isReconstruct_) // don't signal new resources if this is a reconstruction + else + { + // Do nothing if the instance already exists and overwriting is disabled + transaction.GetAllMetadata(instanceMetadata_, instanceId); + storeStatus_ = StoreStatus_AlreadyStored; + return; + } + } + + + if (!isReconstruct_) // don't signal new resources if this is a reconstruction + { + // Warn about the creation of new resources. The order must be + // from instance to patient. + + // NB: In theory, could be sped up by grouping the underlying + // calls to "transaction.LogChange()". However, this would only have an + // impact when new patient/study/series get created, which + // occurs far less often that creating new instances. The + // positive impact looks marginal in practice. + transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_); + + if (status.isNewSeries_) { - // Warn about the creation of new resources. The order must be - // from instance to patient. - - // NB: In theory, could be sped up by grouping the underlying - // calls to "transaction.LogChange()". However, this would only have an - // impact when new patient/study/series get created, which - // occurs far less often that creating new instances. The - // positive impact looks marginal in practice. - transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_); - - if (status.isNewSeries_) - { - transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_); - } - - if (status.isNewStudy_) - { - transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_); - } - - if (status.isNewPatient_) - { - transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_); - } - } + transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_); + } + + if (status.isNewStudy_) + { + transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_); + } - // Ensure there is enough room in the storage for the new instance - uint64_t instanceSize = 0; - for (Attachments::const_iterator it = attachments_.begin(); - it != attachments_.end(); ++it) + if (status.isNewPatient_) + { + transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_); + } + } + + // Ensure there is enough room in the storage for the new instance + uint64_t instanceSize = 0; + for (Attachments::const_iterator it = attachments_.begin(); + it != attachments_.end(); ++it) + { + instanceSize += it->GetCompressedSize(); + } + + if (!isReconstruct_) // reconstruction should not affect recycling + { + if (maximumStorageMode_ == MaxStorageMode_Reject) { - instanceSize += it->GetCompressedSize(); - } - - if (!isReconstruct_) // reconstruction should not affect recycling - { - if (maximumStorageMode_ == MaxStorageMode_Reject) + if (transaction.HasReachedMaxStorageSize(maximumStorageSize_, instanceSize)) { - if (transaction.HasReachedMaxStorageSize(maximumStorageSize_, instanceSize)) - { - storeStatus_ = StoreStatus_StorageFull; - throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum storage size reached"); // throw to cancel the transaction - } - if (transaction.HasReachedMaxPatientCount(maximumPatientCount_, hashPatient_)) - { - storeStatus_ = StoreStatus_StorageFull; - throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum patient count reached"); // throw to cancel the transaction - } + storeStatus_ = StoreStatus_StorageFull; + throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum storage size reached"); // throw to cancel the transaction + } + if (transaction.HasReachedMaxPatientCount(maximumPatientCount_, hashPatient_)) + { + storeStatus_ = StoreStatus_StorageFull; + throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum patient count reached"); // throw to cancel the transaction } - else + } + else + { + transaction.Recycle(maximumStorageSize_, maximumPatientCount_, + instanceSize, hashPatient_ /* don't consider the current patient for recycling */); + } + } + + // Attach the files to the newly created instance + for (Attachments::const_iterator it = attachments_.begin(); + it != attachments_.end(); ++it) + { + if (isReconstruct_) + { + // we are replacing attachments during a reconstruction + transaction.DeleteAttachment(instanceId, it->GetContentType()); + } + + transaction.AddAttachment(instanceId, *it, 0 /* this is the first revision */); + } + + if (!isReconstruct_) + { + ResourcesContent content(true /* new resource, metadata can be set */); + + // Attach the user-specified metadata (in case of reconstruction, metadata_ contains all past metadata, including the system ones we want to keep) + for (MetadataMap::const_iterator + it = metadata_.begin(); it != metadata_.end(); ++it) + { + switch (it->first.first) { - transaction.Recycle(maximumStorageSize_, maximumPatientCount_, - instanceSize, hashPatient_ /* don't consider the current patient for recycling */); + case ResourceType_Patient: + content.AddMetadata(status.patientId_, it->first.second, it->second); + break; + + case ResourceType_Study: + content.AddMetadata(status.studyId_, it->first.second, it->second); + break; + + case ResourceType_Series: + content.AddMetadata(status.seriesId_, it->first.second, it->second); + break; + + case ResourceType_Instance: + SetInstanceMetadata(content, instanceMetadata_, instanceId, + it->first.second, it->second); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); } - } - - // Attach the files to the newly created instance - for (Attachments::const_iterator it = attachments_.begin(); - it != attachments_.end(); ++it) - { - if (isReconstruct_) - { - // we are replacing attachments during a reconstruction - transaction.DeleteAttachment(instanceId, it->GetContentType()); - } - - transaction.AddAttachment(instanceId, *it, 0 /* this is the first revision */); } if (!isReconstruct_) { - ResourcesContent content(true /* new resource, metadata can be set */); - - // Attach the user-specified metadata (in case of reconstruction, metadata_ contains all past metadata, including the system ones we want to keep) - for (MetadataMap::const_iterator - it = metadata_.begin(); it != metadata_.end(); ++it) + // Populate the tags of the newly-created resources + content.AddResource(instanceId, ResourceType_Instance, dicomSummary_); + SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance)); // New in Orthanc 1.11.0 + SetMainDicomSequenceMetadata(content, instanceId, dicomSummary_, ResourceType_Instance); // new in Orthanc 1.11.1 + + if (status.isNewSeries_) { - switch (it->first.first) - { - case ResourceType_Patient: - content.AddMetadata(status.patientId_, it->first.second, it->second); - break; - - case ResourceType_Study: - content.AddMetadata(status.studyId_, it->first.second, it->second); - break; - - case ResourceType_Series: - content.AddMetadata(status.seriesId_, it->first.second, it->second); - break; - - case ResourceType_Instance: - SetInstanceMetadata(content, instanceMetadata_, instanceId, - it->first.second, it->second); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_); + content.AddMetadata(status.seriesId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series)); // New in Orthanc 1.11.0 + SetMainDicomSequenceMetadata(content, status.seriesId_, dicomSummary_, ResourceType_Series); // new in Orthanc 1.11.1 } - if (!isReconstruct_) + if (status.isNewStudy_) { - // Populate the tags of the newly-created resources - content.AddResource(instanceId, ResourceType_Instance, dicomSummary_); - SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, instanceId, dicomSummary_, ResourceType_Instance); // new in Orthanc 1.11.1 - - if (status.isNewSeries_) - { - content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_); - content.AddMetadata(status.seriesId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, status.seriesId_, dicomSummary_, ResourceType_Series); // new in Orthanc 1.11.1 - } - - if (status.isNewStudy_) - { - content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_); - content.AddMetadata(status.studyId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, status.studyId_, dicomSummary_, ResourceType_Study); // new in Orthanc 1.11.1 - } - - if (status.isNewPatient_) - { - content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_); - content.AddMetadata(status.patientId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient)); // New in Orthanc 1.11.0 - SetMainDicomSequenceMetadata(content, status.patientId_, dicomSummary_, ResourceType_Patient); // new in Orthanc 1.11.1 - } - - // Attach the auto-computed metadata for the patient/study/series levels - std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); - content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now); - content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now); - content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now); - - if (status.isNewSeries_) + content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_); + content.AddMetadata(status.studyId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study)); // New in Orthanc 1.11.0 + SetMainDicomSequenceMetadata(content, status.studyId_, dicomSummary_, ResourceType_Study); // new in Orthanc 1.11.1 + } + + if (status.isNewPatient_) + { + content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_); + content.AddMetadata(status.patientId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient)); // New in Orthanc 1.11.0 + SetMainDicomSequenceMetadata(content, status.patientId_, dicomSummary_, ResourceType_Patient); // new in Orthanc 1.11.1 + } + + // Attach the auto-computed metadata for the patient/study/series levels + std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); + content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now); + content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now); + content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now); + + if (status.isNewSeries_) + { + if (hasExpectedInstances_) { - if (hasExpectedInstances_) - { - content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances, - boost::lexical_cast<std::string>(expectedInstances_)); - } - - // New in Orthanc 1.9.0 - content.AddMetadata(status.seriesId_, MetadataType_RemoteAet, - origin_.GetRemoteAetC()); - } - // Attach the auto-computed metadata for the instance level, - // reflecting these additions into the input metadata map - SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_ReceptionDate, now); - SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_RemoteAet, - origin_.GetRemoteAetC()); - SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_Instance_Origin, - EnumerationToString(origin_.GetRequestOrigin())); - - std::string s; - - if (origin_.LookupRemoteIp(s)) - { - // New in Orthanc 1.4.0 - SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_RemoteIp, s); + content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances, + boost::lexical_cast<std::string>(expectedInstances_)); } - if (origin_.LookupCalledAet(s)) - { - // New in Orthanc 1.4.0 - SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_CalledAet, s); - } - - if (origin_.LookupHttpUsername(s)) - { - // New in Orthanc 1.4.0 - SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_HttpUsername, s); - } + // New in Orthanc 1.9.0 + content.AddMetadata(status.seriesId_, MetadataType_RemoteAet, + origin_.GetRemoteAetC()); + } + // Attach the auto-computed metadata for the instance level, + // reflecting these additions into the input metadata map + SetInstanceMetadata(content, instanceMetadata_, instanceId, + MetadataType_Instance_ReceptionDate, now); + SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_RemoteAet, + origin_.GetRemoteAetC()); + SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_Instance_Origin, + EnumerationToString(origin_.GetRequestOrigin())); + + std::string s; + + if (origin_.LookupRemoteIp(s)) + { + // New in Orthanc 1.4.0 + SetInstanceMetadata(content, instanceMetadata_, instanceId, + MetadataType_Instance_RemoteIp, s); + } + + if (origin_.LookupCalledAet(s)) + { + // New in Orthanc 1.4.0 + SetInstanceMetadata(content, instanceMetadata_, instanceId, + MetadataType_Instance_CalledAet, s); + } + + if (origin_.LookupHttpUsername(s)) + { + // New in Orthanc 1.4.0 + SetInstanceMetadata(content, instanceMetadata_, instanceId, + MetadataType_Instance_HttpUsername, s); } - - // Following metadatas are also updated if reconstructing the instance. - // They might be missing since they have been introduced along Orthanc versions. - - if (hasTransferSyntax_) + } + + // Following metadatas are also updated if reconstructing the instance. + // They might be missing since they have been introduced along Orthanc versions. + + if (hasTransferSyntax_) + { + // New in Orthanc 1.2.0 + SetInstanceMetadata(content, instanceMetadata_, instanceId, + MetadataType_Instance_TransferSyntax, + GetTransferSyntaxUid(transferSyntax_)); + } + + if (hasPixelDataOffset_) + { + // New in Orthanc 1.9.1 + SetInstanceMetadata(content, instanceMetadata_, instanceId, + MetadataType_Instance_PixelDataOffset, + boost::lexical_cast<std::string>(pixelDataOffset_)); + + // New in Orthanc 1.12.1 + if (dicomSummary_.GuessPixelDataValueRepresentation(transferSyntax_) != pixelDataVR_) { - // New in Orthanc 1.2.0 + // Store the VR of pixel data if it doesn't comply with the standard SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_TransferSyntax, - GetTransferSyntaxUid(transferSyntax_)); + MetadataType_Instance_PixelDataVR, + EnumerationToString(pixelDataVR_)); } - - if (hasPixelDataOffset_) - { - // New in Orthanc 1.9.1 - SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_PixelDataOffset, - boost::lexical_cast<std::string>(pixelDataOffset_)); - } - - const DicomValue* value; - if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && - !value->IsNull() && + } + + const DicomValue* value; + if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && + !value->IsNull() && + !value->IsBinary()) + { + SetInstanceMetadata(content, instanceMetadata_, instanceId, + MetadataType_Instance_SopClassUid, value->GetContent()); + } + + + if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || + (value = dicomSummary_.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) + { + if (!value->IsNull() && !value->IsBinary()) { SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_SopClassUid, value->GetContent()); - } - - - if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || - (value = dicomSummary_.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) - { - if (!value->IsNull() && - !value->IsBinary()) - { - SetInstanceMetadata(content, instanceMetadata_, instanceId, - MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent())); - } - } - - - transaction.SetResourcesContent(content); - } - - - // Check whether the series of this new instance is now completed - int64_t expectedNumberOfInstances; - if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary_)) - { - SeriesStatus seriesStatus = transaction.GetSeriesStatus(status.seriesId_, expectedNumberOfInstances); - if (seriesStatus == SeriesStatus_Complete) - { - transaction.LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries_); + MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent())); } } - - transaction.LogChange(status.seriesId_, ChangeType_NewChildInstance, ResourceType_Series, hashSeries_); - transaction.LogChange(status.studyId_, ChangeType_NewChildInstance, ResourceType_Study, hashStudy_); - transaction.LogChange(status.patientId_, ChangeType_NewChildInstance, ResourceType_Patient, hashPatient_); - - // Mark the parent resources of this instance as unstable - transaction.GetTransactionContext().MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries_); - transaction.GetTransactionContext().MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy_); - transaction.GetTransactionContext().MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient_); - transaction.GetTransactionContext().SignalAttachmentsAdded(instanceSize); - - storeStatus_ = StoreStatus_Success; + + + transaction.SetResourcesContent(content); } - catch (OrthancException& e) + + + // Check whether the series of this new instance is now completed + int64_t expectedNumberOfInstances; + if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary_)) { - if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize) - { - throw; // the transaction has failed -> do not commit the current transaction (and retry) - } - else + SeriesStatus seriesStatus = transaction.GetSeriesStatus(status.seriesId_, expectedNumberOfInstances); + if (seriesStatus == SeriesStatus_Complete) { - LOG(ERROR) << "EXCEPTION [" << e.What() << " - " << e.GetDetails() << "]"; - - if (e.GetErrorCode() == ErrorCode_FullStorage) - { - throw; // do not commit the current transaction - } - - // this is an expected failure, exit normaly and commit the current transaction - storeStatus_ = StoreStatus_Failure; + transaction.LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries_); } } + + transaction.LogChange(status.seriesId_, ChangeType_NewChildInstance, ResourceType_Series, hashSeries_); + transaction.LogChange(status.studyId_, ChangeType_NewChildInstance, ResourceType_Study, hashStudy_); + transaction.LogChange(status.patientId_, ChangeType_NewChildInstance, ResourceType_Patient, hashPatient_); + + // Mark the parent resources of this instance as unstable + transaction.GetTransactionContext().MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries_); + transaction.GetTransactionContext().MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy_); + transaction.GetTransactionContext().MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient_); + transaction.GetTransactionContext().SignalAttachmentsAdded(instanceSize); + + storeStatus_ = StoreStatus_Success; } }; - Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin, - overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset, - pixelDataOffset, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct); - Apply(operations); - return operations.GetStoreStatus(); + Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, + hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, + pixelDataVR, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct); + + try + { + Apply(operations); + return operations.GetStoreStatus(); + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_FullStorage) + { + return StoreStatus_StorageFull; + } + else + { + // the transaction has failed -> do not commit the current transaction (and retry) + throw; + } + } } @@ -3475,7 +3500,7 @@ int64_t resourceId; if (!transaction.LookupResource(resourceId, resourceType, publicId_)) { - status_ = StoreStatus_Failure; // Inexistent resource + throw OrthancException(ErrorCode_InexistentItem, HttpStatus_404_NotFound); } else {
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Jul 04 12:20:41 2023 +0200 @@ -759,6 +759,7 @@ DicomTransferSyntax transferSyntax, bool hasPixelDataOffset, uint64_t pixelDataOffset, + ValueRepresentation pixelDataVR, MaxStorageMode maximumStorageMode, uint64_t maximumStorageSize, unsigned int maximumPatients,
--- a/OrthancServer/Sources/OrthancInitialization.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -241,7 +241,7 @@ if (DicomMap::IsComputedTag(tag)) { - LOG(WARNING) << " - " << tagName << " can not be added in the Extra Main Dicom Tags since the value of this tag is computed when requested"; + LOG(WARNING) << " - " << tagName << " cannot be added in the Extra Main Dicom Tags since the value of this tag is computed when requested"; } else {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -959,7 +959,7 @@ InjectTags(dicom, request[TAGS], decodeBinaryTags, privateCreator, force); - // Inject the content (either an image, or a PDF file) + // Inject the content (either an image, a PDF file, or a STL/OBJ/MTL file) if (request.isMember(CONTENT)) { const Json::Value& content = request[CONTENT]; @@ -1000,8 +1000,9 @@ .SetRequestField(TAGS, RestApiCallDocumentation::Type_JsonObject, "Associative array containing the tags of the new instance to be created", true) .SetRequestField(CONTENT, RestApiCallDocumentation::Type_String, - "This field can be used to embed an image (pixel data) or a PDF inside the created DICOM instance. " - "The PNG image, the JPEG image or the PDF file must be provided using their " + "This field can be used to embed an image (pixel data encoded as PNG or JPEG), a PDF, or a " + "3D manufactoring model (MTL/OBJ/STL) inside the created DICOM instance. " + "The file to be encapsulated must be provided using its " "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme). " "This field can possibly contain a JSON array, in which case a DICOM series is created " "containing one DICOM instance for each item in the `Content` field.", false)
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -259,7 +259,7 @@ resetRequestReceived_(false), activeRequests_(context.GetMetricsRegistry(), "orthanc_rest_api_active_requests", - MetricsType_MaxOver10Seconds) + MetricsUpdatePolicy_MaxOver10Seconds) { RegisterSystem(orthancExplorerEnabled);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -639,7 +639,8 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) ORTHANC_OVERRIDE + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE { assert(type == "image"); assert(subtype == "png"); @@ -658,7 +659,8 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) ORTHANC_OVERRIDE + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE { assert(type == "image"); assert(subtype == "x-portable-arbitrarymap"); @@ -698,7 +700,8 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) ORTHANC_OVERRIDE + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE { assert(type == "image"); assert(subtype == "jpeg");
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -770,6 +770,36 @@ } + static void DeleteJobOutput(RestApiDeleteCall& call) + { + if (call.IsDocumentation()) + { + call.GetDocumentation() + .SetTag("Jobs") + .SetSummary("Delete a job output") + .SetDescription("Delete the output produced by a job. As of Orthanc 1.12.1, only the jobs that generate a " + "DICOMDIR media or a ZIP archive provide such an output (with `key` equals to `archive`).") + .SetUriArgument("id", "Identifier of the job of interest") + .SetUriArgument("key", "Name of the output of interest"); + return; + } + + std::string job = call.GetUriComponent("id", ""); + std::string key = call.GetUriComponent("key", ""); + + if (OrthancRestApi::GetContext(call).GetJobsEngine(). + GetRegistry().DeleteJobOutput(job, key)) + { + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + else + { + throw OrthancException(ErrorCode_InexistentItem, + "Job has no such output: " + key); + } + } + + enum JobAction { JobAction_Cancel, @@ -883,19 +913,19 @@ context.GetIndex().GetLastChange(lastChange); MetricsRegistry& registry = context.GetMetricsRegistry(); - registry.SetValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES); - registry.SetValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES); - registry.SetValue("orthanc_count_patients", static_cast<unsigned int>(countPatients)); - registry.SetValue("orthanc_count_studies", static_cast<unsigned int>(countStudies)); - registry.SetValue("orthanc_count_series", static_cast<unsigned int>(countSeries)); - registry.SetValue("orthanc_count_instances", static_cast<unsigned int>(countInstances)); - registry.SetValue("orthanc_jobs_pending", jobsPending); - registry.SetValue("orthanc_jobs_running", jobsRunning); - registry.SetValue("orthanc_jobs_completed", jobsSuccess + jobsFailed); - registry.SetValue("orthanc_jobs_success", jobsSuccess); - registry.SetValue("orthanc_jobs_failed", jobsFailed); - registry.SetValue("orthanc_up_time_s", serverUpTime); - registry.SetValue("orthanc_last_change", lastChange["Last"].asInt64()); + registry.SetFloatValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES); + registry.SetFloatValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES); + registry.SetIntegerValue("orthanc_count_patients", static_cast<int64_t>(countPatients)); + registry.SetIntegerValue("orthanc_count_studies", static_cast<int64_t>(countStudies)); + registry.SetIntegerValue("orthanc_count_series", static_cast<int64_t>(countSeries)); + registry.SetIntegerValue("orthanc_count_instances", static_cast<int64_t>(countInstances)); + registry.SetIntegerValue("orthanc_jobs_pending", jobsPending); + registry.SetIntegerValue("orthanc_jobs_running", jobsRunning); + registry.SetIntegerValue("orthanc_jobs_completed", jobsSuccess + jobsFailed); + registry.SetIntegerValue("orthanc_jobs_success", jobsSuccess); + registry.SetIntegerValue("orthanc_jobs_failed", jobsFailed); + registry.SetIntegerValue("orthanc_up_time_s", serverUpTime); + registry.SetIntegerValue("orthanc_last_change", lastChange["Last"].asInt64()); std::string s; registry.ExportPrometheusText(s); @@ -1113,6 +1143,7 @@ Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>); Register("/jobs/{id}/resume", ApplyJobAction<JobAction_Resume>); Register("/jobs/{id}/{key}", GetJobOutput); + Register("/jobs/{id}/{key}", DeleteJobOutput); // New in Orthanc 1.9.0 Register("/tools/accepted-transfer-syntaxes", GetAcceptedTransferSyntaxes);
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -248,6 +248,8 @@ { std::unique_ptr<DcmDataset> target(new DcmDataset); + std::string currentPrivateCreator = ""; + for (std::set<DicomTag>::const_iterator it = flatTags_.begin(); it != flatTags_.end(); ++it) { @@ -257,13 +259,19 @@ if (source.findAndGetElement(tag, element).good() && element != NULL) { - if (it->IsPrivate()) + if (tag.isPrivateReservation()) { - throw OrthancException(ErrorCode_NotImplemented, - "Not applicable to private tags: " + it->Format()); + OFString privateCreator; + element->getOFString(privateCreator, 0, false); + currentPrivateCreator = privateCreator.c_str(); } - - std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, "" /* no private creator */)); + else if (!it->IsPrivate()) + { + // reset the private creator as soon as we reach the end of the current private block + currentPrivateCreator = ""; + } + + std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, currentPrivateCreator.c_str())); cloned->copyFrom(*element); target->insert(cloned.release()); }
--- a/OrthancServer/Sources/ServerContext.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -110,16 +110,28 @@ #if HAVE_MALLOC_TRIM == 1 void ServerContext::MemoryTrimmingThread(ServerContext* that, - unsigned int sleepDelay) + unsigned int intervalInSeconds) { + boost::posix_time::ptime lastExecution = boost::posix_time::second_clock::universal_time(); + // This thread is started only if malloc_trim is defined while (!that->done_) { - boost::this_thread::sleep(boost::posix_time::milliseconds(sleepDelay)); - - // If possible, gives memory back to the system - // (see OrthancServer/Resources/ImplementationNotes/memory_consumption.txt) - malloc_trim(128*1024); + boost::posix_time::ptime now = boost::posix_time::second_clock::universal_time(); + boost::posix_time::time_duration elapsed = now - lastExecution; + + if (elapsed.total_seconds() > intervalInSeconds) + { + // If possible, gives memory back to the system + // (see OrthancServer/Resources/ImplementationNotes/memory_consumption.txt) + { + MetricsRegistry::Timer timer(that->GetMetricsRegistry(), "orthanc_memory_trimming_duration_ms"); + malloc_trim(128*1024); + } + lastExecution = boost::posix_time::second_clock::universal_time(); + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); } } #endif @@ -297,10 +309,9 @@ void ServerContext::PublishDicomCacheMetrics() { - metricsRegistry_->SetValue("orthanc_dicom_cache_size", - static_cast<float>(dicomCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024)); - metricsRegistry_->SetValue("orthanc_dicom_cache_count", - static_cast<float>(dicomCache_.GetNumberOfItems())); + metricsRegistry_->SetFloatValue("orthanc_dicom_cache_size", + static_cast<float>(dicomCache_.GetCurrentSize()) / static_cast<float>(1024 * 1024)); + metricsRegistry_->SetIntegerValue("orthanc_dicom_cache_count", dicomCache_.GetNumberOfItems()); } @@ -406,7 +417,7 @@ CLOG(INFO, DICOM) << "Deidentification of log contents (notably for DIMSE queries) is enabled"; DicomVersion version = StringToDicomVersion( - lock.GetConfiguration().GetStringParameter("DeidentifyLogsDicomVersion", "2021b")); + lock.GetConfiguration().GetStringParameter("DeidentifyLogsDicomVersion", "2023b")); CLOG(INFO, DICOM) << "Version of DICOM standard used for deidentification is " << EnumerationToString(version); @@ -440,7 +451,8 @@ changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); #if HAVE_MALLOC_TRIM == 1 - memoryTrimmingThread_ = boost::thread(MemoryTrimmingThread, this, 100); + LOG(INFO) << "Starting memory trimming thread at 30 seconds interval"; + memoryTrimmingThread_ = boost::thread(MemoryTrimmingThread, this, 30); #else LOG(INFO) << "Your platform does not support malloc_trim(), not starting the memory trimming thread"; #endif @@ -552,8 +564,9 @@ bool hasPixelDataOffset; uint64_t pixelDataOffset; + ValueRepresentation pixelDataVR; hasPixelDataOffset = DicomStreamReader::LookupPixelDataOffset( - pixelDataOffset, dicom.GetBufferData(), dicom.GetBufferSize()); + pixelDataOffset, pixelDataVR, dicom.GetBufferData(), dicom.GetBufferSize()); DicomTransferSyntax transferSyntax; bool hasTransferSyntax = dicom.LookupTransferSyntax(transferSyntax); @@ -653,7 +666,7 @@ InstanceMetadata instanceMetadata; result.SetStatus(index_.Store( instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite, - hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, isReconstruct)); + hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, pixelDataVR, isReconstruct)); // Only keep the metadata for the "instance" level dicom.ClearMetadata(); @@ -688,8 +701,12 @@ break; case StoreStatus_Failure: - LOG(ERROR) << "Store failure"; - break; + LOG(ERROR) << "Unknown store failure"; + throw OrthancException(ErrorCode_InternalError, HttpStatus_500_InternalServerError); + + case StoreStatus_StorageFull: + LOG(ERROR) << "Storage full"; + throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage); default: // This should never happen @@ -1090,7 +1107,8 @@ * Orthanc have failed. Try again this precomputation now * for future calls. **/ - if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, dicom) && + ValueRepresentation pixelDataVR; + if (DicomStreamReader::LookupPixelDataOffset(pixelDataOffset, pixelDataVR, dicom) && pixelDataOffset < dicom.size()) { index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, @@ -1138,13 +1156,23 @@ bool ServerContext::ReadDicomUntilPixelData(std::string& dicom, const std::string& instancePublicId) { + FileInfo attachment; + int64_t revision; // Ignored + if (index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_DicomUntilPixelData)) + { + StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); + + accessor.Read(dicom, attachment); + assert(dicom.size() == attachment.GetUncompressedSize()); + + return true; + } + if (!area_.HasReadRange()) { return false; } - FileInfo attachment; - int64_t revision; // Ignored if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom)) { throw OrthancException(ErrorCode_InternalError,
--- a/OrthancServer/Sources/ServerContext.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerContext.h Tue Jul 04 12:20:41 2023 +0200 @@ -189,7 +189,7 @@ #if HAVE_MALLOC_TRIM == 1 static void MemoryTrimmingThread(ServerContext* that, - unsigned int sleepDelay); + unsigned int intervalInSeconds); #endif void SaveJobsEngine();
--- a/OrthancServer/Sources/ServerEnumerations.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -62,6 +62,7 @@ dictMetadataType_.Add(MetadataType_Instance_PixelDataOffset, "PixelDataOffset"); dictMetadataType_.Add(MetadataType_MainDicomTagsSignature, "MainDicomTagsSignature"); dictMetadataType_.Add(MetadataType_MainDicomSequences, "MainDicomSequences"); + dictMetadataType_.Add(MetadataType_Instance_PixelDataVR, "PixelDataVR"); dictContentType_.Add(FileContentType_Dicom, "dicom"); dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json");
--- a/OrthancServer/Sources/ServerEnumerations.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Tue Jul 04 12:20:41 2023 +0200 @@ -160,6 +160,7 @@ MetadataType_Instance_PixelDataOffset = 14, // New in Orthanc 1.9.0 MetadataType_MainDicomTagsSignature = 15, // New in Orthanc 1.11.0 MetadataType_MainDicomSequences = 16, // New in Orthanc 1.11.1 + MetadataType_Instance_PixelDataVR = 17, // New in Orthanc 1.12.1 // Make sure that the value "65535" can be stored into this enumeration MetadataType_StartUser = 1024,
--- a/OrthancServer/Sources/ServerIndex.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -538,6 +538,7 @@ DicomTransferSyntax transferSyntax, bool hasPixelDataOffset, uint64_t pixelDataOffset, + ValueRepresentation pixelDataVR, bool isReconstruct) { uint64_t maximumStorageSize; @@ -553,7 +554,8 @@ return StatelessDatabaseOperations::Store( instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax, - transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct); + transferSyntax, hasPixelDataOffset, pixelDataOffset, pixelDataVR, maximumStorageMode, + maximumStorageSize, maximumPatients, isReconstruct); }
--- a/OrthancServer/Sources/ServerIndex.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Tue Jul 04 12:20:41 2023 +0200 @@ -88,7 +88,8 @@ DicomTransferSyntax transferSyntax, bool hasPixelDataOffset, uint64_t pixelDataOffset, - bool isResonstruct); + ValueRepresentation pixelDataVR, + bool isReconstruct); StoreStatus AddAttachment(int64_t& newRevision /*out*/, const FileInfo& attachment,
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -176,6 +176,7 @@ class ArchiveJob::ThreadedInstanceLoader : public ArchiveJob::InstanceLoader { Semaphore availableInstancesSemaphore_; + Semaphore bufferedInstancesSemaphore_; std::map<std::string, boost::shared_ptr<std::string> > availableInstances_; boost::mutex availableInstancesMutex_; SharedMessageQueue instancesToPreload_; @@ -185,7 +186,8 @@ public: ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax) : InstanceLoader(context, transcode, transferSyntax), - availableInstancesSemaphore_(0) + availableInstancesSemaphore_(0), + bufferedInstancesSemaphore_(3*threadCount) { for (size_t i = 0; i < threadCount; i++) { @@ -227,6 +229,9 @@ { return; } + + // wait for the consumers (zip writer), no need to accumulate instances in memory if loaders are faster than writers + that->bufferedInstancesSemaphore_.Acquire(); try { @@ -270,6 +275,7 @@ { // wait for an instance to be available but this might not be the one we are waiting for ! availableInstancesSemaphore_.Acquire(); + bufferedInstancesSemaphore_.Release(); // unlock the "flow" of loaders boost::shared_ptr<std::string> dicomContent; { @@ -1453,4 +1459,27 @@ return false; } } + + bool ArchiveJob::DeleteOutput(const std::string& key) + { + if (key == "archive" && + !mediaArchiveId_.empty()) + { + SharedArchive::Accessor accessor(context_.GetMediaArchive(), mediaArchiveId_); + + if (accessor.IsValid()) + { + context_.GetMediaArchive().Remove(mediaArchiveId_); + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Tue Jul 04 12:20:41 2023 +0200 @@ -120,5 +120,7 @@ MimeType& mime, std::string& filename, const std::string& key) ORTHANC_OVERRIDE; + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -222,7 +222,7 @@ catch (OrthancException& e) { LOG(WARNING) << "An error occurred while executing a Modification job on instance " << instance << ": " << e.GetDetails(); - return false; + throw; } @@ -762,11 +762,15 @@ (!modification_->IsReplaced(*mainPatientTag) || modification_->GetReplacementAsString(*mainPatientTag) != targetPatientTags.GetStringValue(*mainPatientTag, "", false))) { - throw OrthancException(ErrorCode_BadRequest, std::string("Trying to change patient tags in a study. The Patient already exists and has other studies. All the 'Replace' tags should match the existing patient main dicom tags. Try using /patients/../modify instead to modify the patient. Failing tag: ") + mainPatientTag->Format()); + throw OrthancException(ErrorCode_BadRequest, std::string("Trying to change patient tags in a study. " + "The Patient already exists and has other studies. All the 'Replace' tags should match the existing patient main dicom tags " + "and you should specify all Patient MainDicomTags in your query. Try using /patients/../modify instead to modify the patient. Failing tag: ") + mainPatientTag->Format()); } else if (!targetPatientTags.HasTag(*mainPatientTag) && modification_->IsReplaced(*mainPatientTag) ) { - throw OrthancException(ErrorCode_BadRequest, std::string("Trying to change patient tags in a study. The Patient already exists and has other studies. You are trying to replace a tag that is not defined yet in this patient. Try using /patients/../modify instead to modify the patient. Failing tag: ") + mainPatientTag->Format()); + throw OrthancException(ErrorCode_BadRequest, std::string("Trying to change patient tags in a study. " + "The Patient already exists and has other studies. You are trying to replace a tag that is not defined yet in this patient. " + "Try using /patients/../modify instead to modify the patient. Failing tag: ") + mainPatientTag->Format()); } } }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -295,15 +295,6 @@ } - bool ThreadedSetOfInstancesJob::GetOutput(std::string &output, - MimeType &mime, - std::string& filename, - const std::string &key) - { - return false; - } - - size_t ThreadedSetOfInstancesJob::GetInstancesCount() const { boost::recursive_mutex::scoped_lock lock(mutex_); @@ -351,7 +342,7 @@ if (started_) { - // We actually can not clean the instances that would have been generated during a previous run + // We actually cannot clean the instances that would have been generated during a previous run // because the generated instances may or may not have the same orthanc ids as the source // it is too dangerous to guess if they should be deleted or not currentStep_ = ThreadedJobStep_NotStarted;
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Tue Jul 04 12:20:41 2023 +0200 @@ -152,7 +152,15 @@ virtual bool GetOutput(std::string& output, MimeType& mime, std::string& filename, - const std::string& key) ORTHANC_OVERRIDE; + const std::string& key) ORTHANC_OVERRIDE + { + return false; + } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } bool IsFailedInstance(const std::string& instance) const;
--- a/OrthancServer/Sources/ServerToolbox.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -211,6 +211,31 @@ std::string t; t.reserve(value.size()); +#if 0 + // This version solves some indexing issue (https://discourse.orthanc-server.org/t/postgress-index-effectively-disabled-when-searching-for-greek-names/3371) + // and seems functional: I could run the integration tests with both SQLite and PG + the DicomWeb tests with PG. + // However, it can not go into production because NormalizeIdentifier is used both at ingest time and at search time; + // therefore, if we change it while we have an already populated DB, the searches won't work anymore and, on very large + // systems, running the Housekeeper to rebuild the indexes might take months ... + // We keep it here because it might be handy once we refactor the DicomIdentifier searches in the future. + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == '%' || + value[i] == '_') + { + t.push_back(' '); // These characters might break wildcard queries in SQL + } + else if (//isascii(value[i]) && + !iscntrl(value[i]) && + (!isspace(value[i]) || value[i] == ' ')) + { + t.push_back(value[i]); + } + } + + //Toolbox::ToUpperCase(t); + t = Toolbox::ToUpperCaseWithAccents(t); +#else for (size_t i = 0; i < value.size(); i++) { if (value[i] == '%' || @@ -227,6 +252,7 @@ } Toolbox::ToUpperCase(t); +#endif return Toolbox::StripSpaces(t); }
--- a/OrthancServer/Sources/main.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/Sources/main.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -1181,7 +1181,7 @@ else { context.SetRestApiWriteToFileSystemEnabled(false); - LOG(WARNING) << "REST API can not write to the file system."; + LOG(WARNING) << "REST API cannot write to the file system."; } if (lock.GetConfiguration().GetBooleanParameter("WebDavEnabled", true))
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -742,18 +742,21 @@ ASSERT_EQ(StoreStatus_Success, index.Store( instanceMetadata, summary, attachments, toStore->GetMetadata(), toStore->GetOrigin(), false /* don't overwrite */, - hasTransferSyntax, transferSyntax, true /* pixel data offset */, 42, false)); + hasTransferSyntax, transferSyntax, true /* has pixel data */, 42 /* pixel data offset */, + ValueRepresentation_PersonName /* pixel data VR */, false)); } - ASSERT_EQ(7u, instanceMetadata.size()); + ASSERT_EQ(8u, instanceMetadata.size()); ASSERT_TRUE(instanceMetadata.find(MetadataType_RemoteAet) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_TransferSyntax) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_SopClassUid) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_PixelDataOffset) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_MainDicomTagsSignature) != instanceMetadata.end()); + ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_PixelDataVR) != instanceMetadata.end()); ASSERT_EQ("42", instanceMetadata[MetadataType_Instance_PixelDataOffset]); + ASSERT_EQ("PN", instanceMetadata[MetadataType_Instance_PixelDataVR]); // The default transfer syntax depends on the OS endianness std::string s = instanceMetadata[MetadataType_Instance_TransferSyntax];
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -134,6 +134,11 @@ { return false; } + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE + { + return false; + } }; @@ -720,7 +725,7 @@ { std::unique_ptr<DicomModification> modification(new DicomModification); - modification->SetupAnonymization(DicomVersion_2021b); + modification->SetupAnonymization(DicomVersion_2023b); ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release());
--- a/OrthancServer/UnitTestsSources/VersionsTests.cpp Tue May 16 07:09:06 2023 +0200 +++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp Tue Jul 04 12:20:41 2023 +0200 @@ -112,7 +112,7 @@ TEST(Versions, BoostStatic) { - ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_80" || + ASSERT_TRUE(std::string(BOOST_LIB_VERSION) == "1_82" || std::string(BOOST_LIB_VERSION) == "1_69" /* if USE_LEGACY_BOOST */); }
--- a/TODO Tue May 16 07:09:06 2023 +0200 +++ b/TODO Tue Jul 04 12:20:41 2023 +0200 @@ -99,6 +99,8 @@ https://groups.google.com/g/orthanc-users/c/y3-xa_GcdLM/m/m0Kr5G5UPAAJ * (1) Specify the transfer syntax in /tools/create-dicom https://groups.google.com/g/orthanc-users/c/o15Dekecgds/m/xmPE2y3bAwAJ +* Support Palette PNG in /tools/create-dicom: + https://discourse.orthanc-server.org/t/404-on-tools-create-dicom-endpoint-with-specific-png/3562 * (1) In the /studies/{id}/anonymize route, add an option to remove secondary captures. They usually contains Patient info in the image. The SOPClassUID might be used to identify such secondary @@ -118,6 +120,9 @@ * Also implement a GET variant of /tools/create-archive + sibling routes in which resources & transcode options are provided as get arguments. https://groups.google.com/g/orthanc-users/c/PmaRZ609ztA/m/JdwXvIBKAQAJ +* Allow skiping automatic conversion of color-space in transcoding/decoding. + The patch that was initialy provided was breaking the IngestTranscoding. + https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533/9 --------- @@ -186,6 +191,8 @@ - ModalitiesInStudy - all computed counters at series/study/patient level - RequestAttributesSequence (sequence that must be included in all DicomWeb QIDO-RS for series) +* Investigate increase of CPU consumption while idle after anonymize/delete + (https://discourse.orthanc-server.org/t/onchange-callbacks-and-cpu-loads/3534) * Long-shot & not sure it is even feasible at all: try to reduce memory usage by implementing streaming when receiving DICOM instances from the Rest API or from DICOM and store files directly to disk as they @@ -322,6 +329,12 @@ * Add more complex testing scenarios like data-migration, change of configuration files, multiple orthanc interacting togethers with various config. This should probably look like the python toolbox tests ... + - add a test to validate Modalities and Peers stored in DB are not lost + while upgrading from one version to the other (Sylvain) +* On Ubuntu 20.04, accesses to unitialized memory are sometimes + reported in libgjpeg by valgrind, if running the following command + (this is probably unrelated to Orthanc): + $ ./Start.sh --force Orthanc.test_bitbucket_issue_141 Orthanc.test_create_pdf Orthanc.test_decode_brainix_as_jpeg ---------------------