Mercurial > hg > orthanc
changeset 6364:b4b7b6d39ee3 pixel-anon
merged default -> pixel-anon
| author | Alain Mazy <am@orthanc.team> |
|---|---|
| date | Tue, 04 Nov 2025 15:58:06 +0100 |
| parents | 980ab5ed1f59 (current diff) 43320ad053aa (diff) |
| children | ded8a2be0d46 |
| files | .hgignore OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancFramework/Sources/DicomFormat/DicomTag.h OrthancFramework/Sources/DicomParsing/DicomModification.cpp OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp OrthancFramework/Sources/Enumerations.h OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h TODO |
| diffstat | 239 files changed, 18342 insertions(+), 2799 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Mon Apr 14 15:17:11 2025 +0200 +++ b/.hgignore Tue Nov 04 15:58:06 2025 +0100 @@ -3,6 +3,7 @@ CMakeLists.txt.user *.cpp.orig *.h.orig +*.patch.orig .vs/ .vscode/ *~ @@ -16,3 +17,8 @@ .project Resources/Testing/Issue32/Java/bin Resources/Testing/Issue32/Java/target +build/ + +# when opening Orthanc in Visual Studio, by default, it creates these files/folders: +OrthancServer/CMakeSettings.json +OrthancServer/out/
--- a/CITATION.cff Mon Apr 14 15:17:11 2025 +0200 +++ b/CITATION.cff Tue Nov 04 15:58:06 2025 +0100 @@ -10,5 +10,5 @@ doi: "10.1007/s10278-018-0082-y" license: "GPL-3.0-or-later" repository-code: "https://orthanc.uclouvain.be/hg/orthanc/" -version: 1.12.7 -date-released: 2025-04-07 +version: 1.12.9 +date-released: 2025-08-11
--- a/NEWS Mon Apr 14 15:17:11 2025 +0200 +++ b/NEWS Tue Nov 04 15:58:06 2025 +0100 @@ -1,6 +1,193 @@ Pending changes in the mainline =============================== +General +------- + +* New configuration "HttpBindAddresses" to list the IP addresses on which the + HTTP server listens. By default, this list is empty and the HTTP server listens + on all network interfaces. + +REST API +-------- + +* Fix: C-Get SCU jobs or HTTP responses were always successful even if the C-Get operation + actually failed. +* C-Store, C-Move and C-Get jobs and HTTP responses now include a new "DimseErrorStatus" + field (ONLY if the operation fails). + + +Maintenance +----------- + +* Under Windows, the console output is now configured to interpret strings as UTF-8. + This notably allow cyrillic strings to display correctly in the console. +* Improved streaming of HTTP responses under OS pressure. In some conditions, when the OS + was not able to send a full buffer over the network, Orthanc was not retrying + to send the remaining part. This is now fixed. + (https://discourse.orthanc-server.org/t/incomplete-zip-downloads-from-get-studies-id-media/6046) +* Reworked all the paths handling, improving general support of non ASCII-only paths on Windows, + specifically for the Storage directories and for the configuration files. + However, on Windows, some features might still not support non ASCII-only paths: + - Reading a DCMTK dictionary ("ExternalDictionaries" configuration) + - Using a "TemporaryDirectory" to save zip file or to export DICOMDIR + - The "SslCertificate" and other related configurations +* Fix: DicomGetScu jobs are now saved in DB. +* Fix: When the configuration option "MaximumStorageCacheSize" was set to 0, the default value (128) + was actually applied. From now on, a value of 0 really means that the storage cache is disabled. +* Fix: Orthanc was unable to convert the tags into dicom+json format if the instance contained an + empty element in a sequence. This was preventing access to /dicom-web/../metadata routes and prevented + visualization in e.g. the Stone Web viewer and OHIF. +* Fix issue 252: Disallow colons in HTTP basic usernames +* Fix: Decoding of LUT with less than 256 entries: + https://discourse.orthanc-server.org/t/cannot-preview-dicom-file/6275 +* Fix: Possible deadlocks when using ZipLoaderThreads > 1 when the HttpClient disconnects + while downloading the response. +* Upgraded dependencies for static builds: + - civetweb 1.16, including patch for CVE-2025-55763 + + +Plugins +------- + +* Worklists plugin: + - The Worklists plugin now provides a Rest API to: + - create worklists through POST at /worklists/create + - list the worklists throuhg GET at /worklists + - view a single worklist through GET at /worklists/{uuid} + - modify a worklist through PUT at /worklists/{uuid} + - delete a worklist through DELETE at /worklists/{uuid} + All details are available in the Orthanc book: https://orthanc.uclouvain.be/book/plugins/worklists-plugin.html + - New configurations: + - "SaveInOrthancDatabase" to store the worklists in the Orthanc DB (provided that you are using SQLite or PostgreSQL) + - "DeleteWorklistsOnStableStudy" to delete the worklist once its related study has been received and is stable + - "SetStudyInstanceUidIfMissing" to add a StudyInstanceUID if the Rest API request does not include one. + - "DeleteWorklistsDelay" to delete a worklist N hours after it has been created (only available if using the "SaveInOrthancDatabase" mode). + - "HousekeepingInterval" to define the delay between 2 executions of the Worklist Housekeeping thread that deletes + the worklists when required. + - Note: the previous "Database" configuration has now been renamed in "Directory" to better differentiate the "File" or "DB modes. + + +Version 1.12.9 (2025-08-11) +=========================== + +REST API +-------- + +* When creating a job, a "UserData" field can be added to the payload. + This data will travel along with the job and will be available in the + "/jobs/{jobId}" route. +* Added new metrics in "/tools/metrics-prometheus": + - "orthanc_available_dicom_threads" displays the minimum + number of DICOM threads that were available during the last 10 seconds. + - "orthanc_available_http_threads_count" displays the minimum + number of HTTP threads that were available during the last 10 seconds. + It is basically the opposite of "orthanc_rest_api_active_requests" but + it is more convenient to configure alerts on. "orthanc_rest_api_active_requests" + also monitors the internal requests coming from plugins while "orthanc_available_http_threads" + monitors the requests received by the external HTTP server. + - Fixed the "orthanc_rest_api_active_requests" metrix that was not + reset after 10 seconds. + +Plugin SDK +---------- + +* Added new primitives: + - "OrthancPluginSetStableStatus()" to force the stabilization of a + DICOM resource from a plugin. + - "OrthancPluginRegisterHttpAuthentication()" to install a custom + callback to authenticate HTTP requests. + - "OrthancPluginEmitAuditLog()" to generate an audit log. + - "OrthancPluginRegisterAuditLogHandler()" to handle audit logs. +* The OrthancPluginHttpRequest structure provides the payload of + the possible HTTP authentication callback. +* OrthancPluginCallRestApi() now also returns the body of DELETE requests: + https://discourse.orthanc-server.org/t/response-to-plugin-from-orthanc-api-delete-endpoint/6022 +* Added macro ORTHANC_PLUGIN_SINCE_SDK to track in which version of + Orthanc the SDK primitives were introduced. + +Plugins +------- + +* Housekeeper plugin: + - New "ForceReconstructFiles" option: If set to "true", forces the + "ReconstructFiles" option when reconstructing resources, even if + the plugin did not detect any changes in the configuration that + should trigger a reconstruct. + +Lua +--- + +* Added new "SetStableStatus()" function. + +Maintenance +----------- + +* For security, if the "RegisteredUsers" configuration option is present + but empty, Orthanc does not create the default user "orthanc" anymore. +* Added new CMake option "-DBUILD_UNIT_TESTS=ON" to disable the building of unit tests. +* Fix handling of backslashes in DICOM elements if encoding is ISO_IR 13. +* Fix initialization of ICU. + + +Version 1.12.8 (2025-06-13) +=========================== + +General +------- + +* The default SQLite database engine now supports metadata and attachment revisions. + +REST API +-------- + +* API version upgraded to 29 +* If the database backend provides the "HasExtendedFind" primitive, the + value "IsProtected" can be included in the "ResponseContent" field of + "/tools/find" to request the "IsProtected" status of patient resources. + +Plugin SDK +---------- + +* Added new functions (available to all plugins) to access key-value + stores and queues stored as a part of the Orthanc database. +* New SDK to create storage area plugins (V3) that associate custom data with + attachments. The built-in SQLite database engine supports such custom data. +* New SDK to handle custom data for attachments, key-value stores, and queues + by custom database backends (cf. "OrthancDatabasePlugin.proto"). +* Added OrthancPluginAdoptDicomInstance() to adopt DICOM instances stored elsewhere + than in the storage area (to be used by "orthanc-advanced-storage" plugin). + +Plugins +------- + +* New sample plugins: "CppSkeleton" and "AdoptDicomInstance" +* Housekeeper plugin: + - If "LimitMainDicomTagsReconstructLevel" was set, files were not transcoded + if they had to. The "LimitMainDicomTagsReconstructLevel" configuration is now + ignored when a full processing is required. +* Delayed Deletion plugin: + - Added an index in the delayed-deletion SQLite external DB to speed up delayed + deletions. This new index will only apply to new databases. If you want to speed + up an existing installation, run "CREATE INDEX PendingIndex ON Pending(uuid)" + manually in the plugin SQLite DB. With this patch, we observed a 100 fold + performance improvement when the "Pending" table contains 1-2 millions files. + Contribution by Yurii (George) from ivtech.dev. + +Maintenance +----------- + +* In verbose logs, the elapsed time spent in each HTTP call is now reported. +* Fix computation of MD5 hashes for memory buffers whose size is larger than 2^31 bytes. +* Configuration options "RejectSopClasses" and "RejectedSopClasses" are taken as synonyms. + In Orthanc 1.12.6 and 1.12.7, "RejectSopClasses" was used instead of the expected + "RejectedSopClasses" spelling. +* Fix the re-encoding of DICOM files larger than 4GB. +* Improved translations of HTTP error codes when a plugin calls the core REST API. + In particular, a plugin could receive an error OrthancPluginErrorCode_UnknownResource (code 17) + when the underlying REST handler was actually returning an HTTP error 415. The plugin will + now receive an error OrthancPluginErrorCode_UnsupportedMediaType (code 3000). + Version 1.12.7 (2025-04-07) =========================== @@ -1477,8 +1664,8 @@ ------- * New functions in the SDK: - - OrthancPluginHttpClientChunkedBody(): HTTP client for POST/PUT with a chunked body - - OrthancPluginRegisterMultipartRestCallback(): HTTP server for POST/PUT with multipart body + - OrthancPluginChunkedHttpClient(): HTTP client for POST/PUT with a chunked body + - OrthancPluginRegisterChunkedRestCallback(): HTTP server for POST/PUT with multipart body - OrthancPluginGetTagName(): Retrieve the name of a DICOM tag from its group and element Maintenance
--- a/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake Tue Nov 04 15:58:06 2025 +0100 @@ -20,7 +20,7 @@ # <http://www.gnu.org/licenses/>. -set(EMBED_RESOURCES_PYTHON "${CMAKE_CURRENT_LIST_DIR}/../EmbedResources.py" +set(EMBED_RESOURCES_PYTHON "${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py" CACHE INTERNAL "Path to the EmbedResources.py script from Orthanc") set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED") set(AUTOGENERATED_SOURCES)
--- a/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake Tue Nov 04 15:58:06 2025 +0100 @@ -22,21 +22,21 @@ if (STATIC_BUILD OR NOT USE_SYSTEM_CIVETWEB) - ## WARNING: "civetweb-1.14.tar.gz" comes with a subfolder - ## "civetweb-1.14/test/nonlatin" that cannot be removed by "hg purge + ## WARNING: "civetweb-1.16.tar.gz" comes with a subfolder + ## "civetweb-1.16/test/nonlatin" that cannot be removed by "hg purge ## --all" on Windows hosts. We thus created a custom - ## "civetweb-1.14-fixed.tar.gz" as follows: + ## "civetweb-1.16-fixed.tar.gz" as follows: ## ## $ cd /tmp - ## $ wget https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.14.tar.gz - ## $ tar xvf civetweb-1.14.tar.gz - ## $ rm -rf civetweb-1.14/src/third_party/ civetweb-1.14/test/ - ## $ tar cvfz civetweb-1.14-fixed.tar.gz civetweb-1.14 + ## $ wget https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.16.tar.gz + ## $ tar xvf civetweb-1.16.tar.gz + ## $ rm -rf civetweb-1.16/src/third_party/ civetweb-1.16/test/ + ## $ tar cvfz civetweb-1.16-fixed.tar.gz civetweb-1.16 ## - set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.14) - set(CIVETWEB_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.14-fixed.tar.gz") - set(CIVETWEB_MD5 "1f25d516b7a4e65d8b270d1cc399e0a9") + set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.16) + set(CIVETWEB_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/civetweb-1.16-fixed.tar.gz") + set(CIVETWEB_MD5 "038edf77ea37e47def9458e8903cd53d") if (IS_DIRECTORY "${CIVETWEB_SOURCES_DIR}") set(FirstRun OFF) @@ -48,7 +48,7 @@ execute_process( COMMAND ${PATCH_EXECUTABLE} -p0 -N -i - ${CMAKE_CURRENT_LIST_DIR}/../Patches/civetweb-1.14.patch + ${CMAKE_CURRENT_LIST_DIR}/../Patches/civetweb-1.16.patch WORKING_DIRECTORY ${CMAKE_BINARY_DIR} RESULT_VARIABLE Failure )
--- a/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake Tue Nov 04 15:58:06 2025 +0100 @@ -171,6 +171,10 @@ set(ORTHANC_FRAMEWORK_MD5 "0e971f32f4f3e4951e0f3b5de49a3da6") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.7") set(ORTHANC_FRAMEWORK_MD5 "f27c27d7a7a694dab1fd7f0a99d9715a") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.8") + set(ORTHANC_FRAMEWORK_MD5 "eb1c719234338e8277b80d3453563e9f") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.9") + set(ORTHANC_FRAMEWORK_MD5 "66b5a2ee60706c4a502896083b9e1a01") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc @@ -203,6 +207,14 @@ # DICOMweb 1.15 (framework pre-1.12.2) set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) set(ORTHANC_FRAMEWORK_MD5 "ebe8bdf388319f1c9536b2b680451848") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "36cd91a53403") + # Advanced storage 0.2.0 (framework pre-1.12.10) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "911105f18a154b5e106985d8fcfcb620") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "9eb77f159b9d") + # Advanced storage 0.2.2 (framework pre-1.12.10) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "bd5ba2cec329010b912209345acbdeaf") endif() endif() endif() @@ -515,7 +527,6 @@ include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake) include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake) include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake) - set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py) if (ORTHANC_FRAMEWORK_USE_SHARED) list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Resources/CMake/EmbedResources.py Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,446 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see +# <http://www.gnu.org/licenses/>. + + +import sys +import os +import os.path +import pprint +import re + +UPCASE_CHECK = True +USE_SYSTEM_EXCEPTION = False +EXCEPTION_CLASS = 'OrthancException' +OUT_OF_RANGE_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_ParameterOutOfRange)' +INEXISTENT_PATH_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_InexistentItem)' +NAMESPACE = 'Orthanc.EmbeddedResources' +FRAMEWORK_PATH = None + +ARGS = [] +for i in range(len(sys.argv)): + if not sys.argv[i].startswith('--'): + ARGS.append(sys.argv[i]) + elif sys.argv[i].lower() == '--no-upcase-check': + UPCASE_CHECK = False + elif sys.argv[i].lower() == '--system-exception': + USE_SYSTEM_EXCEPTION = True + EXCEPTION_CLASS = '::std::runtime_error' + OUT_OF_RANGE_EXCEPTION = '%s("Parameter out of range")' % EXCEPTION_CLASS + INEXISTENT_PATH_EXCEPTION = '%s("Unknown path in a directory resource")' % EXCEPTION_CLASS + elif sys.argv[i].startswith('--namespace='): + NAMESPACE = sys.argv[i][sys.argv[i].find('=') + 1 : ] + elif sys.argv[i].startswith('--framework-path='): + FRAMEWORK_PATH = sys.argv[i][sys.argv[i].find('=') + 1 : ] + +if len(ARGS) < 2 or len(ARGS) % 2 != 0: + print ('Usage:') + print ('python %s [--no-upcase-check] [--system-exception] [--namespace=<Namespace>] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0]) + exit(-1) + +TARGET_BASE_FILENAME = ARGS[1] +SOURCES = ARGS[2:] + +try: + # Make sure the destination directory exists + os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..'))) +except: + pass + + +##################################################################### +## Read each resource file +##################################################################### + +def CheckNoUpcase(s): + global UPCASE_CHECK + if (UPCASE_CHECK and + re.search('[A-Z]', s) != None): + raise Exception("Path in a directory with an upcase letter: %s" % s) + +resources = {} + +counter = 0 +i = 0 +while i < len(SOURCES): + resourceName = SOURCES[i].upper() + pathName = SOURCES[i + 1] + + if not os.path.exists(pathName): + raise Exception("Non existing path: %s" % pathName) + + if resourceName in resources: + raise Exception("Twice the same resource: " + resourceName) + + if os.path.isdir(pathName): + # The resource is a directory: Recursively explore its files + content = {} + for root, dirs, files in os.walk(pathName): + dirs.sort() + files.sort() + base = os.path.relpath(root, pathName) + + # Fix issue #24 (Build fails on OSX when directory has .DS_Store files): + # Ignore folders whose name starts with a dot (".") + if base.find('/.') != -1: + print('Ignoring folder: %s' % root) + continue + + for f in files: + if f.find('~') == -1: # Ignore Emacs backup files + if base == '.': + r = f + else: + r = os.path.join(base, f) + + CheckNoUpcase(r) + r = '/' + r.replace('\\', '/') + if r in content: + raise Exception("Twice the same filename (check case): " + r) + + content[r] = { + 'Filename' : os.path.join(root, f), + 'Index' : counter + } + counter += 1 + + resources[resourceName] = { + 'Type' : 'Directory', + 'Files' : content + } + + elif os.path.isfile(pathName): + resources[resourceName] = { + 'Type' : 'File', + 'Index' : counter, + 'Filename' : pathName + } + counter += 1 + + else: + raise Exception("Not a regular file, nor a directory: " + pathName) + + i += 2 + +#pprint.pprint(resources) + + +##################################################################### +## Write .h header +##################################################################### + +header = open(TARGET_BASE_FILENAME + '.h', 'w') + +header.write(""" +#pragma once + +#include <string> +#include <list> + +#if defined(_MSC_VER) +# pragma warning(disable: 4065) // "Switch statement contains 'default' but no 'case' labels" +#endif + +""") + + +for ns in NAMESPACE.split('.'): + header.write('namespace %s {\n' % ns) + + +header.write(""" + enum FileResourceId + { +""") + +isFirst = True +for name in resources: + if resources[name]['Type'] == 'File': + if isFirst: + isFirst = False + else: + header.write(',\n') + header.write(' %s' % name) + +header.write(""" + }; + + enum DirectoryResourceId + { +""") + +isFirst = True +for name in resources: + if resources[name]['Type'] == 'Directory': + if isFirst: + isFirst = False + else: + header.write(',\n') + header.write(' %s' % name) + +header.write(""" + }; + + const void* GetFileResourceBuffer(FileResourceId id); + size_t GetFileResourceSize(FileResourceId id); + void GetFileResource(std::string& result, FileResourceId id); + + const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path); + size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path); + void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path); + + void ListResources(std::list<std::string>& result, DirectoryResourceId id); + +""") + + +for ns in NAMESPACE.split('.'): + header.write('}\n') + +header.close() + + + +##################################################################### +## Write the resource content in the .cpp source +##################################################################### + +PYTHON_MAJOR_VERSION = sys.version_info[0] + +def WriteResource(cpp, item): + cpp.write(' static const uint8_t resource%dBuffer[] = {' % item['Index']) + + f = open(item['Filename'], "rb") + content = f.read() + f.close() + + # http://stackoverflow.com/a/1035360 + pos = 0 + buffer = [] # instead of appending a few bytes at a time to the cpp file, + # we first append each chunk to a list, join it and write it + # to the file. We've measured that it was 2-3 times faster in python3. + # Note that speed is important since if generation is too slow, + # cmake might try to compile the EmbeddedResources.cpp file while it is + # still being generated ! + for b in content: + if PYTHON_MAJOR_VERSION == 2: + c = ord(b[0]) + else: + c = b + + if pos > 0: + buffer.append(",") + + if (pos % 16) == 0: + buffer.append("\n") + + if c < 0: + raise Exception("Internal error") + + buffer.append("0x%02x" % c) + pos += 1 + + cpp.write("".join(buffer)) + # Zero-size array are disallowed, so we put one single void character in it. + if pos == 0: + cpp.write(' 0') + + cpp.write(' };\n') + cpp.write(' static const size_t resource%dSize = %d;\n' % (item['Index'], pos)) + + +cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w') + +cpp.write('#include "%s.h"\n' % os.path.basename(TARGET_BASE_FILENAME)) + +if USE_SYSTEM_EXCEPTION: + cpp.write('#include <stdexcept>') +elif FRAMEWORK_PATH != None: + cpp.write('#include "%s/OrthancException.h"' % FRAMEWORK_PATH) +else: + cpp.write('#include <OrthancException.h>') + +cpp.write(""" +#include <stdint.h> +#include <string.h> + +""") + +for ns in NAMESPACE.split('.'): + cpp.write('namespace %s {\n' % ns) + + +for name in resources: + if resources[name]['Type'] == 'File': + WriteResource(cpp, resources[name]) + else: + for f in resources[name]['Files']: + WriteResource(cpp, resources[name]['Files'][f]) + + + +##################################################################### +## Write the accessors to the file resources in .cpp +##################################################################### + +cpp.write(""" + const void* GetFileResourceBuffer(FileResourceId id) + { + switch (id) + { +""") +for name in resources: + if resources[name]['Type'] == 'File': + cpp.write(' case %s:\n' % name) + cpp.write(' return resource%dBuffer;\n' % resources[name]['Index']) + +cpp.write(""" + default: + throw %s; + } + } + + size_t GetFileResourceSize(FileResourceId id) + { + switch (id) + { +""" % OUT_OF_RANGE_EXCEPTION) + +for name in resources: + if resources[name]['Type'] == 'File': + cpp.write(' case %s:\n' % name) + cpp.write(' return resource%dSize;\n' % resources[name]['Index']) + +cpp.write(""" + default: + throw %s; + } + } +""" % OUT_OF_RANGE_EXCEPTION) + + + +##################################################################### +## Write the accessors to the directory resources in .cpp +##################################################################### + +cpp.write(""" + const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path) + { + switch (id) + { +""") + +for name in resources: + if resources[name]['Type'] == 'Directory': + cpp.write(' case %s:\n' % name) + isFirst = True + for path in resources[name]['Files']: + cpp.write(' if (!strcmp(path, "%s"))\n' % path) + cpp.write(' return resource%dBuffer;\n' % resources[name]['Files'][path]['Index']) + cpp.write(' throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION) + +cpp.write(""" default: + throw %s; + } + } + + size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path) + { + switch (id) + { +""" % OUT_OF_RANGE_EXCEPTION) + +for name in resources: + if resources[name]['Type'] == 'Directory': + cpp.write(' case %s:\n' % name) + isFirst = True + for path in resources[name]['Files']: + cpp.write(' if (!strcmp(path, "%s"))\n' % path) + cpp.write(' return resource%dSize;\n' % resources[name]['Files'][path]['Index']) + cpp.write(' throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION) + +cpp.write(""" default: + throw %s; + } + } +""" % OUT_OF_RANGE_EXCEPTION) + + + + +##################################################################### +## List the resources in a directory +##################################################################### + +cpp.write(""" + void ListResources(std::list<std::string>& result, DirectoryResourceId id) + { + result.clear(); + + switch (id) + { +""") + +for name in resources: + if resources[name]['Type'] == 'Directory': + cpp.write(' case %s:\n' % name) + for path in sorted(resources[name]['Files']): + cpp.write(' result.push_back("%s");\n' % path) + cpp.write(' break;\n\n') + +cpp.write(""" default: + throw %s; + } + } +""" % OUT_OF_RANGE_EXCEPTION) + + + + +##################################################################### +## Write the convenience wrappers in .cpp +##################################################################### + +cpp.write(""" + void GetFileResource(std::string& result, FileResourceId id) + { + size_t size = GetFileResourceSize(id); + result.resize(size); + if (size > 0) + memcpy(&result[0], GetFileResourceBuffer(id), size); + } + + void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path) + { + size_t size = GetDirectoryResourceSize(id, path); + result.resize(size); + if (size > 0) + memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size); + } +""") + + +for ns in NAMESPACE.split('.'): + cpp.write('}\n') + +cpp.close()
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue Nov 04 15:58:06 2025 +0100 @@ -170,6 +170,7 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Enumerations.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/FileInfo.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/MemoryStorageArea.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/FileStorage/PluginStorageAreaAdapter.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/CStringMatcher.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpContentNegociation.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/HttpToolbox.cpp
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Tue Nov 04 15:58:06 2025 +0100 @@ -39,7 +39,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 "28") +set(ORTHANC_API_VERSION "29") #####################################################################
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Tue Nov 04 15:58:06 2025 +0100 @@ -393,7 +393,7 @@ { "Code": 2003, "Name": "HttpPortInUse", - "Description": "The TCP port of the HTTP server is privileged or already in use" + "Description": "The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist" }, { "Code": 2004,
--- a/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py Tue Nov 04 15:58:06 2025 +0100 @@ -37,7 +37,7 @@ ## with open(os.path.join(BASE, 'OrthancFramework', 'Resources', 'CodeGeneration', 'ErrorCodes.json'), 'r') as f: - ERRORS = json.loads(re.sub('/\*.*?\*/', '', f.read())) + ERRORS = json.loads(re.sub(r'/\*.*?\*/', '', f.read())) for error in ERRORS: if error['Code'] >= START_PLUGINS: @@ -48,7 +48,7 @@ a = f.read() HTTP = {} -for i in re.findall('(HttpStatus_([0-9]+)_\w+)', a): +for i in re.findall(r'(HttpStatus_([0-9]+)_\w+)', a): HTTP[int(i[1])] = i[0] @@ -64,7 +64,7 @@ s = ',\n'.join(map(lambda x: ' ErrorCode_%s = %d /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS)) s += ',\n ErrorCode_START_PLUGINS = %d' % START_PLUGINS -a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL) +a = re.sub(r'(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL) with open(path, 'w') as f: f.write(a) @@ -81,7 +81,7 @@ s = ',\n'.join(map(lambda x: ' OrthancPluginErrorCode_%s = %d /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS)) s += ',\n\n _OrthancPluginErrorCode_INTERNAL = 0x7fffffff\n ' -a = re.sub('(typedef enum\s*{)[^}]*?(} OrthancPluginErrorCode;)', r'\1\n%s\2' % s, a, re.DOTALL) +a = re.sub(r'(typedef enum\s*{)[^}]*?(} OrthancPluginErrorCode;)', r'\1\n%s\2' % s, a, re.DOTALL) with open(path, 'w') as f: f.write(a) @@ -99,7 +99,7 @@ a = f.read() s = '\n\n'.join(map(lambda x: ' case ErrorCode_%s:\n return "%s";' % (x['Name'], x['Description']), ERRORS)) -a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)', +a = re.sub(r'(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)', r'\1\n%s\2' % s, a, re.DOTALL) def GetHttpStatus(x): @@ -107,7 +107,7 @@ return ' case ErrorCode_%s:\n return %s;' % (x['Name'], s) s = '\n\n'.join(map(GetHttpStatus, filter(lambda x: 'HttpStatus' in x, ERRORS))) -a = re.sub('(ConvertErrorCodeToHttpStatus\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)', +a = re.sub(r'(ConvertErrorCodeToHttpStatus\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)', r'\1\n%s\2' % s, a, re.DOTALL) with open(path, 'w') as f: @@ -125,10 +125,10 @@ e = list(filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS)) s = ',\n'.join(map(lambda x: ' ErrorCode_%s' % x['Name'], e)) -a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL) +a = re.sub(r'(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL) s = '\n\n'.join(map(lambda x: ' case ErrorCode_%s:\n return "%s";' % (x['Name'], x['Description']), e)) -a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)', +a = re.sub(r'(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)', r'\1\n%s\2' % s, a, re.DOTALL) with open(path, 'w') as f: @@ -145,7 +145,7 @@ a = f.read() s = '\n'.join(map(lambda x: ' PrintErrorCode(ErrorCode_%s, "%s");' % (x['Name'], x['Description']), ERRORS)) -a = re.sub('(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n }' % s, a, re.DOTALL) +a = re.sub(r'(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n }' % s, a, re.DOTALL) with open(path, 'w') as f: f.write(a)
--- a/OrthancFramework/Resources/EmbedResources.py Mon Apr 14 15:17:11 2025 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,446 +0,0 @@ -#!/usr/bin/python - -# Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics -# Department, University Hospital of Liege, Belgium -# Copyright (C) 2017-2023 Osimis S.A., Belgium -# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium -# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium -# -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public License -# as published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program. If not, see -# <http://www.gnu.org/licenses/>. - - -import sys -import os -import os.path -import pprint -import re - -UPCASE_CHECK = True -USE_SYSTEM_EXCEPTION = False -EXCEPTION_CLASS = 'OrthancException' -OUT_OF_RANGE_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_ParameterOutOfRange)' -INEXISTENT_PATH_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_InexistentItem)' -NAMESPACE = 'Orthanc.EmbeddedResources' -FRAMEWORK_PATH = None - -ARGS = [] -for i in range(len(sys.argv)): - if not sys.argv[i].startswith('--'): - ARGS.append(sys.argv[i]) - elif sys.argv[i].lower() == '--no-upcase-check': - UPCASE_CHECK = False - elif sys.argv[i].lower() == '--system-exception': - USE_SYSTEM_EXCEPTION = True - EXCEPTION_CLASS = '::std::runtime_error' - OUT_OF_RANGE_EXCEPTION = '%s("Parameter out of range")' % EXCEPTION_CLASS - INEXISTENT_PATH_EXCEPTION = '%s("Unknown path in a directory resource")' % EXCEPTION_CLASS - elif sys.argv[i].startswith('--namespace='): - NAMESPACE = sys.argv[i][sys.argv[i].find('=') + 1 : ] - elif sys.argv[i].startswith('--framework-path='): - FRAMEWORK_PATH = sys.argv[i][sys.argv[i].find('=') + 1 : ] - -if len(ARGS) < 2 or len(ARGS) % 2 != 0: - print ('Usage:') - print ('python %s [--no-upcase-check] [--system-exception] [--namespace=<Namespace>] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0]) - exit(-1) - -TARGET_BASE_FILENAME = ARGS[1] -SOURCES = ARGS[2:] - -try: - # Make sure the destination directory exists - os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..'))) -except: - pass - - -##################################################################### -## Read each resource file -##################################################################### - -def CheckNoUpcase(s): - global UPCASE_CHECK - if (UPCASE_CHECK and - re.search('[A-Z]', s) != None): - raise Exception("Path in a directory with an upcase letter: %s" % s) - -resources = {} - -counter = 0 -i = 0 -while i < len(SOURCES): - resourceName = SOURCES[i].upper() - pathName = SOURCES[i + 1] - - if not os.path.exists(pathName): - raise Exception("Non existing path: %s" % pathName) - - if resourceName in resources: - raise Exception("Twice the same resource: " + resourceName) - - if os.path.isdir(pathName): - # The resource is a directory: Recursively explore its files - content = {} - for root, dirs, files in os.walk(pathName): - dirs.sort() - files.sort() - base = os.path.relpath(root, pathName) - - # Fix issue #24 (Build fails on OSX when directory has .DS_Store files): - # Ignore folders whose name starts with a dot (".") - if base.find('/.') != -1: - print('Ignoring folder: %s' % root) - continue - - for f in files: - if f.find('~') == -1: # Ignore Emacs backup files - if base == '.': - r = f - else: - r = os.path.join(base, f) - - CheckNoUpcase(r) - r = '/' + r.replace('\\', '/') - if r in content: - raise Exception("Twice the same filename (check case): " + r) - - content[r] = { - 'Filename' : os.path.join(root, f), - 'Index' : counter - } - counter += 1 - - resources[resourceName] = { - 'Type' : 'Directory', - 'Files' : content - } - - elif os.path.isfile(pathName): - resources[resourceName] = { - 'Type' : 'File', - 'Index' : counter, - 'Filename' : pathName - } - counter += 1 - - else: - raise Exception("Not a regular file, nor a directory: " + pathName) - - i += 2 - -#pprint.pprint(resources) - - -##################################################################### -## Write .h header -##################################################################### - -header = open(TARGET_BASE_FILENAME + '.h', 'w') - -header.write(""" -#pragma once - -#include <string> -#include <list> - -#if defined(_MSC_VER) -# pragma warning(disable: 4065) // "Switch statement contains 'default' but no 'case' labels" -#endif - -""") - - -for ns in NAMESPACE.split('.'): - header.write('namespace %s {\n' % ns) - - -header.write(""" - enum FileResourceId - { -""") - -isFirst = True -for name in resources: - if resources[name]['Type'] == 'File': - if isFirst: - isFirst = False - else: - header.write(',\n') - header.write(' %s' % name) - -header.write(""" - }; - - enum DirectoryResourceId - { -""") - -isFirst = True -for name in resources: - if resources[name]['Type'] == 'Directory': - if isFirst: - isFirst = False - else: - header.write(',\n') - header.write(' %s' % name) - -header.write(""" - }; - - const void* GetFileResourceBuffer(FileResourceId id); - size_t GetFileResourceSize(FileResourceId id); - void GetFileResource(std::string& result, FileResourceId id); - - const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path); - size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path); - void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path); - - void ListResources(std::list<std::string>& result, DirectoryResourceId id); - -""") - - -for ns in NAMESPACE.split('.'): - header.write('}\n') - -header.close() - - - -##################################################################### -## Write the resource content in the .cpp source -##################################################################### - -PYTHON_MAJOR_VERSION = sys.version_info[0] - -def WriteResource(cpp, item): - cpp.write(' static const uint8_t resource%dBuffer[] = {' % item['Index']) - - f = open(item['Filename'], "rb") - content = f.read() - f.close() - - # http://stackoverflow.com/a/1035360 - pos = 0 - buffer = [] # instead of appending a few bytes at a time to the cpp file, - # we first append each chunk to a list, join it and write it - # to the file. We've measured that it was 2-3 times faster in python3. - # Note that speed is important since if generation is too slow, - # cmake might try to compile the EmbeddedResources.cpp file while it is - # still being generated ! - for b in content: - if PYTHON_MAJOR_VERSION == 2: - c = ord(b[0]) - else: - c = b - - if pos > 0: - buffer.append(",") - - if (pos % 16) == 0: - buffer.append("\n") - - if c < 0: - raise Exception("Internal error") - - buffer.append("0x%02x" % c) - pos += 1 - - cpp.write("".join(buffer)) - # Zero-size array are disallowed, so we put one single void character in it. - if pos == 0: - cpp.write(' 0') - - cpp.write(' };\n') - cpp.write(' static const size_t resource%dSize = %d;\n' % (item['Index'], pos)) - - -cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w') - -cpp.write('#include "%s.h"\n' % os.path.basename(TARGET_BASE_FILENAME)) - -if USE_SYSTEM_EXCEPTION: - cpp.write('#include <stdexcept>') -elif FRAMEWORK_PATH != None: - cpp.write('#include "%s/OrthancException.h"' % FRAMEWORK_PATH) -else: - cpp.write('#include <OrthancException.h>') - -cpp.write(""" -#include <stdint.h> -#include <string.h> - -""") - -for ns in NAMESPACE.split('.'): - cpp.write('namespace %s {\n' % ns) - - -for name in resources: - if resources[name]['Type'] == 'File': - WriteResource(cpp, resources[name]) - else: - for f in resources[name]['Files']: - WriteResource(cpp, resources[name]['Files'][f]) - - - -##################################################################### -## Write the accessors to the file resources in .cpp -##################################################################### - -cpp.write(""" - const void* GetFileResourceBuffer(FileResourceId id) - { - switch (id) - { -""") -for name in resources: - if resources[name]['Type'] == 'File': - cpp.write(' case %s:\n' % name) - cpp.write(' return resource%dBuffer;\n' % resources[name]['Index']) - -cpp.write(""" - default: - throw %s; - } - } - - size_t GetFileResourceSize(FileResourceId id) - { - switch (id) - { -""" % OUT_OF_RANGE_EXCEPTION) - -for name in resources: - if resources[name]['Type'] == 'File': - cpp.write(' case %s:\n' % name) - cpp.write(' return resource%dSize;\n' % resources[name]['Index']) - -cpp.write(""" - default: - throw %s; - } - } -""" % OUT_OF_RANGE_EXCEPTION) - - - -##################################################################### -## Write the accessors to the directory resources in .cpp -##################################################################### - -cpp.write(""" - const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path) - { - switch (id) - { -""") - -for name in resources: - if resources[name]['Type'] == 'Directory': - cpp.write(' case %s:\n' % name) - isFirst = True - for path in resources[name]['Files']: - cpp.write(' if (!strcmp(path, "%s"))\n' % path) - cpp.write(' return resource%dBuffer;\n' % resources[name]['Files'][path]['Index']) - cpp.write(' throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION) - -cpp.write(""" default: - throw %s; - } - } - - size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path) - { - switch (id) - { -""" % OUT_OF_RANGE_EXCEPTION) - -for name in resources: - if resources[name]['Type'] == 'Directory': - cpp.write(' case %s:\n' % name) - isFirst = True - for path in resources[name]['Files']: - cpp.write(' if (!strcmp(path, "%s"))\n' % path) - cpp.write(' return resource%dSize;\n' % resources[name]['Files'][path]['Index']) - cpp.write(' throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION) - -cpp.write(""" default: - throw %s; - } - } -""" % OUT_OF_RANGE_EXCEPTION) - - - - -##################################################################### -## List the resources in a directory -##################################################################### - -cpp.write(""" - void ListResources(std::list<std::string>& result, DirectoryResourceId id) - { - result.clear(); - - switch (id) - { -""") - -for name in resources: - if resources[name]['Type'] == 'Directory': - cpp.write(' case %s:\n' % name) - for path in sorted(resources[name]['Files']): - cpp.write(' result.push_back("%s");\n' % path) - cpp.write(' break;\n\n') - -cpp.write(""" default: - throw %s; - } - } -""" % OUT_OF_RANGE_EXCEPTION) - - - - -##################################################################### -## Write the convenience wrappers in .cpp -##################################################################### - -cpp.write(""" - void GetFileResource(std::string& result, FileResourceId id) - { - size_t size = GetFileResourceSize(id); - result.resize(size); - if (size > 0) - memcpy(&result[0], GetFileResourceBuffer(id), size); - } - - void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path) - { - size_t size = GetDirectoryResourceSize(id, path); - result.resize(size); - if (size > 0) - memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size); - } -""") - - -for ns in NAMESPACE.split('.'): - cpp.write('}\n') - -cpp.close()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Resources/Patches/civetweb-1.16.patch Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,70 @@ +diff -urEb civetweb-1.16.orig/src/civetweb.c civetweb-1.16/src/civetweb.c +--- civetweb-1.16.orig/src/civetweb.c 2025-09-23 15:37:44.946300709 +0200 ++++ civetweb-1.16/src/civetweb.c 2025-09-23 22:13:44.809084369 +0200 +@@ -573,7 +573,7 @@ + #if (_MSC_VER < 1300) + #define STRX(x) #x + #define STR(x) STRX(x) +-#define __func__ __FILE__ ":" STR(__LINE__) ++#define __func__ __ORTHANC_FILE__ ":" STR(__LINE__) + #define strtoull(x, y, z) ((unsigned __int64)_atoi64(x)) + #define strtoll(x, y, z) (_atoi64(x)) + #else +@@ -1457,14 +1457,14 @@ + } + + +-#define mg_malloc(a) mg_malloc_ex(a, NULL, __FILE__, __LINE__) +-#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __FILE__, __LINE__) +-#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __FILE__, __LINE__) +-#define mg_free(a) mg_free_ex(a, __FILE__, __LINE__) +- +-#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __FILE__, __LINE__) +-#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __FILE__, __LINE__) +-#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __FILE__, __LINE__) ++#define mg_malloc(a) mg_malloc_ex(a, NULL, __ORTHANC_FILE__, __LINE__) ++#define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__) ++#define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __ORTHANC_FILE__, __LINE__) ++#define mg_free(a) mg_free_ex(a, __ORTHANC_FILE__, __LINE__) ++ ++#define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __ORTHANC_FILE__, __LINE__) ++#define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__) ++#define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __ORTHANC_FILE__, __LINE__) + + + #else /* USE_SERVER_STATS */ +@@ -15254,13 +15254,30 @@ + if (!new_path) { + mg_send_http_error(conn, 500, "out or memory"); + } else { ++ /* Start of patch for CVE-2025-55763: https://github.com/civetweb/civetweb/pull/1347 */ ++ size_t len, max_append; + mg_get_request_link(conn, new_path, buflen - 1); +- strcat(new_path, "/"); ++ ++ len = strlen(new_path); ++ if (len + 1 < buflen) { ++ new_path[len] = '/'; ++ new_path[len + 1] = '\0'; ++ len++; ++ } ++ + if (ri->query_string) { +- /* Append ? and query string */ +- strcat(new_path, "?"); +- strcat(new_path, ri->query_string); ++ if (len + 1 < buflen) { ++ new_path[len] = '?'; ++ new_path[len + 1] = '\0'; ++ len++; + } ++ ++ /* Append with size of space left for query string + null terminator */ ++ max_append = buflen - len - 1; ++ strncat(new_path, ri->query_string, max_append); ++ } ++ /* End of patch */ ++ + mg_send_http_redirect(conn, new_path, 301); + mg_free(new_path); + }
--- a/OrthancFramework/SharedLibrary/CMakeLists.txt Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/SharedLibrary/CMakeLists.txt Tue Nov 04 15:58:06 2025 +0100 @@ -45,7 +45,8 @@ # adds CMAKE_INSTALL_PREFIX to the include_directories(), which causes # issues if re-building the shared library after install! set(ORTHANC_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" CACHE PATH "") -SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") +set(BUILD_UNIT_TESTS ON CACHE BOOL "Whether to build the unit tests (new in Orthanc 1.12.9)") +set(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") set(BUILD_SHARED_LIBRARY ON CACHE BOOL "Whether to build a shared library instead of a static library") set(ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES "" CACHE STRING "Additional libraries to link against, separated by whitespaces, typically needed if building the static library (a common minimal value is \"boost_filesystem boost_iostreams boost_locale boost_regex boost_thread jsoncpp pugixml uuid\")") @@ -75,7 +76,6 @@ set(ENABLE_DCMTK ON) set(ENABLE_DCMTK_TRANSCODING ON) -set(ENABLE_GOOGLE_TEST ON) set(ENABLE_JPEG ON) set(ENABLE_LOCALE ON) set(ENABLE_LUA ON) @@ -83,6 +83,13 @@ set(ENABLE_PUGIXML ON) set(ENABLE_ZLIB ON) +if (BUILD_UNIT_TESTS) + set(ENABLE_GOOGLE_TEST ON) +else() + set(ENABLE_GOOGLE_TEST OFF) +endif() + + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # WebAssembly or asm.js set(BOOST_LOCALE_BACKEND "libiconv") @@ -504,7 +511,8 @@ ## Compile the unit tests ##################################################################### -if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") +if (BUILD_UNIT_TESTS AND + NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") include(ExternalProject) if (CMAKE_TOOLCHAIN_FILE) @@ -544,6 +552,7 @@ -DUNIT_TESTS_WITH_HTTP_CONNEXIONS:BOOL=${UNIT_TESTS_WITH_HTTP_CONNEXIONS} -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE:BOOL=${USE_GOOGLE_TEST_DEBIAN_PACKAGE} -DUSE_SYSTEM_GOOGLE_TEST:BOOL=${USE_SYSTEM_GOOGLE_TEST} + -DBUILD_UNIT_TESTS:BOOL=${BUILD_UNIT_TESTS} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
--- a/OrthancFramework/Sources/Cache/MemoryStringCache.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -131,11 +131,6 @@ void MemoryStringCache::SetMaximumSize(size_t size) { - if (size == 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - // // Make sure no accessor is currently open (as its data may be // // removed if recycling is needed) // WriterLock contentLock(contentMutex_);
--- a/OrthancFramework/Sources/ChunkedBuffer.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/ChunkedBuffer.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -25,6 +25,8 @@ #include "PrecompiledHeaders.h" #include "ChunkedBuffer.h" +#include "OrthancException.h" + #include <cassert> #include <string.h> @@ -54,7 +56,16 @@ else { assert(chunkData != NULL); - chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize)); + + try + { + chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize)); + } + catch (...) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + numBytes_ += chunkSize; } } @@ -172,24 +183,59 @@ void ChunkedBuffer::Flatten(std::string& result) { FlushPendingBuffer(); - result.resize(numBytes_); - size_t pos = 0; - for (Chunks::iterator it = chunks_.begin(); - it != chunks_.end(); ++it) + if (chunks_.empty()) { - assert(*it != NULL); - - size_t s = (*it)->size(); - if (s != 0) + if (numBytes_ != 0) { - memcpy(&result[pos], (*it)->c_str(), s); - pos += s; + throw OrthancException(ErrorCode_InternalError); } - delete *it; + result.clear(); + } + else if (chunks_.size() == 1) + { + // Avoid reallocating a buffer if there is a single chunk + assert(chunks_.front() != NULL); + if (chunks_.front()->size() != numBytes_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + chunks_.front()->swap(result); + delete chunks_.front(); + } + } + else + { + try + { + result.resize(numBytes_); + } + catch (...) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + size_t pos = 0; + for (Chunks::iterator it = chunks_.begin(); + it != chunks_.end(); ++it) + { + assert(*it != NULL); + + size_t s = (*it)->size(); + if (s != 0) + { + memcpy(&result[pos], (*it)->c_str(), s); + pos += s; + } + + delete *it; + } } + // Reset the data structure chunks_.clear(); numBytes_ = 0; }
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -149,7 +149,7 @@ } - HierarchicalZipWriter::HierarchicalZipWriter(const char* path) + HierarchicalZipWriter::HierarchicalZipWriter(const boost::filesystem::path& path) { writer_.SetOutputPath(path); writer_.Open();
--- a/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h Tue Nov 04 15:58:06 2025 +0100 @@ -29,6 +29,7 @@ #include <map> #include <list> #include <boost/lexical_cast.hpp> +#include <boost/filesystem.hpp> #if ORTHANC_BUILD_UNIT_TESTS == 1 # include <gtest/gtest_prod.h> @@ -83,7 +84,7 @@ ZipWriter writer_; public: - explicit HierarchicalZipWriter(const char* path); + explicit HierarchicalZipWriter(const boost::filesystem::path& path); HierarchicalZipWriter(ZipWriter::IOutputStream* stream, // transfers ownership bool isZip64);
--- a/OrthancFramework/Sources/Compression/ZipReader.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Compression/ZipReader.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -248,7 +248,7 @@ #if ORTHANC_SANDBOXED != 1 - bool ZipReader::IsZipFile(const std::string& path) + bool ZipReader::IsZipFile(const boost::filesystem::path& path) { std::string content; SystemToolbox::ReadFileRange(content, path, 0, 4, @@ -409,20 +409,20 @@ #if ORTHANC_SANDBOXED != 1 - ZipReader* ZipReader::CreateFromFile(const std::string& path) + ZipReader* ZipReader::CreateFromFile(const boost::filesystem::path& path) { if (!IsZipFile(path)) { - throw OrthancException(ErrorCode_BadFileFormat, "The file doesn't contain a ZIP archive: " + path); + throw OrthancException(ErrorCode_BadFileFormat, "The file doesn't contain a ZIP archive: " + SystemToolbox::PathToUtf8(path)); } else { std::unique_ptr<ZipReader> reader(new ZipReader); - reader->pimpl_->unzip_ = unzOpen64(path.c_str()); + reader->pimpl_->unzip_ = unzOpen64(SystemToolbox::PathToUtf8(path).c_str()); if (reader->pimpl_->unzip_ == NULL) { - throw OrthancException(ErrorCode_BadFileFormat, "Cannot open ZIP archive from file: " + path); + throw OrthancException(ErrorCode_BadFileFormat, "Cannot open ZIP archive from file: " + SystemToolbox::PathToUtf8(path)); } else {
--- a/OrthancFramework/Sources/Compression/ZipReader.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Compression/ZipReader.h Tue Nov 04 15:58:06 2025 +0100 @@ -44,6 +44,9 @@ #include <boost/noncopyable.hpp> #include <boost/shared_ptr.hpp> +#if ORTHANC_SANDBOXED != 1 +# include <boost/filesystem.hpp> +#endif namespace Orthanc { @@ -73,7 +76,7 @@ static ZipReader* CreateFromMemory(const std::string& buffer); #if ORTHANC_SANDBOXED != 1 - static ZipReader* CreateFromFile(const std::string& path); + static ZipReader *CreateFromFile(const boost::filesystem::path& path); #endif static bool IsZipMemoryBuffer(const void* buffer, @@ -82,7 +85,7 @@ static bool IsZipMemoryBuffer(const std::string& content); #if ORTHANC_SANDBOXED != 1 - static bool IsZipFile(const std::string& path); + static bool IsZipFile(const boost::filesystem::path& path); #endif }; }
--- a/OrthancFramework/Sources/Compression/ZipWriter.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Compression/ZipWriter.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -544,11 +544,11 @@ if (isZip64_) { - pimpl_->file_ = zipOpen64(path_.c_str(), mode); + pimpl_->file_ = zipOpen64(SystemToolbox::PathToUtf8(path_).c_str(), mode); } else { - pimpl_->file_ = zipOpen(path_.c_str(), mode); + pimpl_->file_ = zipOpen(SystemToolbox::PathToUtf8(path_).c_str(), mode); } if (!pimpl_->file_) @@ -559,13 +559,13 @@ } } - void ZipWriter::SetOutputPath(const char* path) + void ZipWriter::SetOutputPath(const boost::filesystem::path& path) { Close(); path_ = path; } - const std::string &ZipWriter::GetOutputPath() const + const boost::filesystem::path& ZipWriter::GetOutputPath() const { return path_; } @@ -603,7 +603,7 @@ return compressionLevel_; } - void ZipWriter::OpenFile(const char* path) + void ZipWriter::OpenFile(const char* filename) { Open(); @@ -614,7 +614,7 @@ if (isZip64_) { - result = zipOpenNewFileInZip64(pimpl_->file_, path, + result = zipOpenNewFileInZip64(pimpl_->file_, filename, &zfi, NULL, 0, NULL, 0, @@ -624,7 +624,7 @@ } else { - result = zipOpenNewFileInZip(pimpl_->file_, path, + result = zipOpenNewFileInZip(pimpl_->file_, filename, &zfi, NULL, 0, NULL, 0,
--- a/OrthancFramework/Sources/Compression/ZipWriter.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Compression/ZipWriter.h Tue Nov 04 15:58:06 2025 +0100 @@ -46,6 +46,7 @@ #include <string> #include <boost/noncopyable.hpp> #include <boost/shared_ptr.hpp> +#include <boost/filesystem.hpp> namespace Orthanc { @@ -132,7 +133,7 @@ bool hasFileInZip_; bool append_; uint8_t compressionLevel_; - std::string path_; + boost::filesystem::path path_; std::unique_ptr<IOutputStream> outputStream_; @@ -159,11 +160,11 @@ bool IsOpen() const; - void SetOutputPath(const char* path); + void SetOutputPath(const boost::filesystem::path& path); - const std::string& GetOutputPath() const; + const boost::filesystem::path& GetOutputPath() const; - void OpenFile(const char* path); + void OpenFile(const char* filename); void Write(const void* data, size_t length);
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -566,7 +566,7 @@ { // Check out "../../../OrthancServer/Resources/ImplementationNotes/windowing.py" - float windowWidth = std::abs(window.GetWidth()); + double windowWidth = std::abs(window.GetWidth()); // Avoid divisions by zero static const double MIN = 0.0001;
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -31,7 +31,6 @@ #include "../Compatibility.h" #include "../Endianness.h" -#include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" #include "DicomArray.h" @@ -44,6 +43,7 @@ #if !defined(__EMSCRIPTEN__) // Multithreading is not supported in WebAssembly # include <boost/thread/shared_mutex.hpp> +# include <boost/thread/lock_types.hpp> // For boost::unique_lock<> and boost::shared_lock<> #endif namespace Orthanc @@ -258,6 +258,16 @@ throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined", false); } + if (level == ResourceType_Study) // all patients main dicom tags are also copied at study level + { + std::set<DicomTag>& patientLevelTags = GetMainDicomTagsByLevelInternal(ResourceType_Patient); + + if (patientLevelTags.find(tag) != patientLevelTags.end()) + { + throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined", false); + } + } + existingLevelTags.insert(tag); allMainDicomTags_.insert(tag); @@ -1220,7 +1230,7 @@ } - void DicomMap::LogMissingTagsForStore() const + std::string DicomMap::FormatMissingTagsForStore() const { std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; @@ -1244,14 +1254,14 @@ sopInstanceUid = ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID); } - LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + return FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); } - void DicomMap::LogMissingTagsForStore(const std::string& patientId, - const std::string& studyInstanceUid, - const std::string& seriesInstanceUid, - const std::string& sopInstanceUid) + std::string DicomMap::FormatMissingTagsForStore(const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid) { std::string s, t; @@ -1309,11 +1319,11 @@ if (t.size() == 0) { - LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)"; + return "Store has failed because all the required tags (" + s + ") are missing (is it a DICOMDIR file?)"; } else { - LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t; + return "Store has failed because required tags (" + s + ") are missing for the following instance: " + t; } }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Tue Nov 04 15:58:06 2025 +0100 @@ -171,12 +171,12 @@ const void* dicom, size_t size); - void LogMissingTagsForStore() const; + std::string FormatMissingTagsForStore() const; - static void LogMissingTagsForStore(const std::string& patientId, - const std::string& studyInstanceUid, - const std::string& seriesInstanceUid, - const std::string& sopInstanceUid); + static std::string FormatMissingTagsForStore(const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid); bool LookupStringValue(std::string& result, const DicomTag& tag,
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -672,7 +672,7 @@ reader.Consume(visitor); isLittleEndian = reader.IsLittleEndian(); } - catch (OrthancException& e) + catch (OrthancException&) { // Invalid DICOM file return false;
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h Tue Nov 04 15:58:06 2025 +0100 @@ -194,6 +194,7 @@ static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009); static const DicomTag DICOM_TAG_GRID_FRAME_OFFSET_VECTOR(0x3004, 0x000c); static const DicomTag DICOM_TAG_PIXEL_SPACING(0x0028, 0x0030); + static const DicomTag DICOM_TAG_IMAGER_PIXEL_SPACING(0x0018, 0x1164); static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052); static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053); static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050); @@ -241,6 +242,10 @@ static const DicomTag DICOM_TAG_REFERENCED_FILE_ID(0x0004, 0x1500); // Tags for DicomWeb - static const Orthanc::DicomTag DICOM_TAG_RETRIEVE_URL(0x0008, 0x1190); + static const DicomTag DICOM_TAG_RETRIEVE_URL(0x0008, 0x1190); + // Tags for Worklists + static const DicomTag DICOM_TAG_SCHEDULED_PROCEDURE_STEP_SEQUENCE(0x0040, 0x0100); + static const DicomTag DICOM_TAG_SCHEDULED_STATION_AETITLE(0x0040, 0x0001); + static const DicomTag DICOM_TAG_SCHEDULED_PROCEDURE_STEP_START_DATE(0x0040, 0x0002); }
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -35,6 +35,14 @@ #include <dcmtk/dcmdata/dcdeftag.h> #include <dcmtk/dcmnet/diutil.h> +#if DCMTK_VERSION_NUMBER < 366 +#define STATUS_GET_Pending_SubOperationsAreContinuing 0xff00 +#define STATUS_GET_Success 0x0000 +#define STATUS_MOVE_Pending_SubOperationsAreContinuing 0xff00 +#define STATUS_MOVE_Success_SubOperationsCompleteNoFailures 0x0000 +#endif + + namespace Orthanc { static void TestAndCopyTag(DicomMap& result, @@ -51,6 +59,12 @@ } } + static std::string DimseToHexString(uint16_t dimseStatus) + { + char buf[16]; + sprintf(buf, "0x%04X", dimseStatus); + return buf; + } namespace { @@ -372,23 +386,20 @@ response.DimseStatus != 0xFF00 && // Pending - Matches are continuing response.DimseStatus != 0xFF01) // Pending - Matches are continuing { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess) { throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-FIND SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + - "\" has failed with DIMSE status 0x" + buf + + "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus) + " (unable to process - invalid query ?)"); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + - "\" has failed with DIMSE status 0x" + buf); + "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus)); } } } @@ -401,6 +412,10 @@ DicomControlUserConnection::IProgressListener* listener = reinterpret_cast<DicomControlUserConnection::IProgressListener*>(callbackData); if (listener) { + OFString str; + CLOG(TRACE, DICOM) << "Received Move Progress:" << std::endl + << DIMSE_dumpMessage(str, *response, DIMSE_INCOMING); + listener->OnProgressUpdated(response->NumberOfRemainingSubOperations, response->NumberOfCompletedSubOperations, response->NumberOfFailedSubOperations, @@ -487,32 +502,31 @@ * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2 **/ - if (response.DimseStatus != 0x0000 && // Success - response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing + if (response.DimseStatus != STATUS_MOVE_Success_SubOperationsCompleteNoFailures && + response.DimseStatus != STATUS_MOVE_Pending_SubOperationsAreContinuing) { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess) { throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-MOVE SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + - "\" has failed with DIMSE status 0x" + buf + - " (unable to process - resource not found ?)"); + "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus) + + " (unable to process - resource not found ?)", + response.DimseStatus); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + - "\" has failed with DIMSE status 0x" + buf); + "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus), + response.DimseStatus); } } } - void DicomControlUserConnection::Get(const DicomMap& findResult, + void DicomControlUserConnection::Get(const DicomMap& getQuery, CGetInstanceReceivedCallback instanceReceivedCallback, void* callbackContext) { @@ -520,7 +534,7 @@ association_->Open(parameters_); std::unique_ptr<ParsedDicomFile> query( - ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer())); + ConvertQueryFields(getQuery, parameters_.GetRemoteModality().GetManufacturer())); DcmDataset* queryDataset = query->GetDcmtkObject().getDataset(); std::string remoteAet; @@ -530,7 +544,7 @@ association_->GetAssociationParameters(remoteAet, remoteIp, calledAet); const char* sopClass = NULL; - const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); + const std::string tmp = getQuery.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); ResourceType level = StringToResourceType(tmp.c_str()); switch (level) { @@ -578,7 +592,11 @@ { OFString tempStr; CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond); - // return cond; + + throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed to send the C-GET request"); + } // equivalent to handleCGETSession in DCMTK @@ -607,8 +625,9 @@ { OFString tempStr; CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result); - // delete statusDetail; - break; // TODO: return value + throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed to receive DIMSE command"); } // Handle C-GET Response if (rsp.CommandField == DIMSE_C_GET_RSP) @@ -627,10 +646,18 @@ rsp.msg.CGetRSP.NumberOfWarningSubOperations); } - if (rsp.msg.CGetRSP.DimseStatus == 0x0000) // final success message + if (rsp.msg.CGetRSP.DimseStatus == STATUS_GET_Success) // final success message { continueSession = false; } + else if (rsp.msg.CGetRSP.DimseStatus != STATUS_GET_Pending_SubOperationsAreContinuing) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "C-GET SCU to AET \"" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed with DIMSE status " + DimseToHexString(rsp.msg.CGetRSP.DimseStatus), + rsp.msg.CGetRSP.DimseStatus); + } } // Handle C-STORE Request else if (rsp.CommandField == DIMSE_C_STORE_RQ) @@ -695,7 +722,9 @@ NULL, NULL, NULL /* commandSet */); if (result.bad()) { - continueSession = false; + throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + + "\" has failed to send response message"); } else { @@ -712,16 +741,13 @@ << std::hex << std::setfill('0') << std::setw(4) << static_cast<unsigned int>(rsp.CommandField); - result = DIMSE_BADCOMMANDTYPE; - continueSession = false; + throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + + "\": Expected C-GET response or C-STORE request but received DIMSE command " + DimseToHexString(rsp.CommandField)); } - // delete statusDetail; // should be NULL if not existing or added to response list - // statusDetail = NULL; } /* All responses received or break signal occurred */ - - // return result; } @@ -881,28 +907,28 @@ void DicomControlUserConnection::Move(const std::string& targetAet, ResourceType level, - const DicomMap& findResult) + const DicomMap& moveQuery) { DicomMap move; switch (level) { case ResourceType_Patient: - TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); + TestAndCopyTag(move, moveQuery, DICOM_TAG_PATIENT_ID); break; case ResourceType_Study: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, moveQuery, DICOM_TAG_STUDY_INSTANCE_UID); break; case ResourceType_Series: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); + TestAndCopyTag(move, moveQuery, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, moveQuery, DICOM_TAG_SERIES_INSTANCE_UID); break; case ResourceType_Instance: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); + TestAndCopyTag(move, moveQuery, DICOM_TAG_STUDY_INSTANCE_UID); + TestAndCopyTag(move, moveQuery, DICOM_TAG_SERIES_INSTANCE_UID); + TestAndCopyTag(move, moveQuery, DICOM_TAG_SOP_INSTANCE_UID); break; default: @@ -914,17 +940,17 @@ void DicomControlUserConnection::Move(const std::string& targetAet, - const DicomMap& findResult) + const DicomMap& moveQuery) { - if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + if (!moveQuery.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) { throw OrthancException(ErrorCode_InternalError); } - const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); + const std::string tmp = moveQuery.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); ResourceType level = StringToResourceType(tmp.c_str()); - Move(targetAet, level, findResult); + Move(targetAet, level, moveQuery); }
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Tue Nov 04 15:58:06 2025 +0100 @@ -126,10 +126,10 @@ void Move(const std::string& targetAet, ResourceType level, - const DicomMap& findResult); + const DicomMap& moveQuery); void Move(const std::string& targetAet, - const DicomMap& findResult); + const DicomMap& moveQuery); void MovePatient(const std::string& targetAet, const std::string& patientId);
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -89,7 +89,7 @@ } - DicomServer::DicomServer() : + DicomServer::DicomServer() : pimpl_(new PImpl), checkCalledAet_(true), aet_("ANY-SCP"), @@ -108,7 +108,8 @@ useDicomTls_(false), maximumPduLength_(ASC_DEFAULTMAXPDU), remoteCertificateRequired_(true), - minimumTlsVersion_(0) + minimumTlsVersion_(0), + metricsRegistry_(NULL) { } @@ -432,7 +433,15 @@ CLOG(INFO, DICOM) << "The embedded DICOM server will use " << threadsCount_ << " threads"; - pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-")); + if (metricsRegistry_ == NULL) + { + pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-")); + } + else + { + pimpl_->workers_.reset(new RunnableWorkersPool(threadsCount_, "DICOM-", *metricsRegistry_, "orthanc_available_dicom_threads")); + } + pimpl_->thread_ = boost::thread(ServerThread, this, maximumPduLength_, useDicomTls_); } @@ -529,12 +538,12 @@ "No path to the private key for the default DICOM TLS certificate was provided"); } - if (!SystemToolbox::IsRegularFile(privateKeyPath)) + if (!SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(privateKeyPath))) { throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + privateKeyPath); } - if (!SystemToolbox::IsRegularFile(certificatePath)) + if (!SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(certificatePath))) { throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + certificatePath); } @@ -567,7 +576,7 @@ { CLOG(INFO, DICOM) << "Setting the trusted certificates for DICOM SCP connections: " << path; - if (!SystemToolbox::IsRegularFile(path)) + if (!SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(path))) { throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + path); } @@ -620,4 +629,9 @@ threadsCount_ = threads; } + void DicomServer::SetMetricsRegistry(MetricsRegistry& registry) + { + Stop(); + metricsRegistry_ = ®istry; + } }
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.h Tue Nov 04 15:58:06 2025 +0100 @@ -47,6 +47,8 @@ namespace Orthanc { + class MetricsRegistry; + class DicomServer : public boost::noncopyable { public: @@ -93,7 +95,7 @@ bool remoteCertificateRequired_; // New in 1.9.3 unsigned int minimumTlsVersion_; // New in 1.12.4 std::set<std::string> acceptedCiphers_; // New in 1.12.4 - + MetricsRegistry* metricsRegistry_; // New in 1.12.9 static void ServerThread(DicomServer* server, unsigned int maximumPduLength, @@ -175,5 +177,6 @@ void SetThreadsCount(unsigned int threadsCount); + void SetMetricsRegistry(MetricsRegistry& registry); }; }
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -38,6 +38,13 @@ namespace Orthanc { + static std::string DimseToHexString(uint16_t dimseStatus) + { + char buf[16]; + sprintf(buf, "0x%04X", dimseStatus); + return buf; + } + static void ProgressCallback(void * /*callbackData*/, T_DIMSE_StoreProgress *progress, T_DIMSE_C_StoreRQ * req) @@ -453,12 +460,11 @@ response.DimseStatus != 0xB006 && // Warning - Elements Discarded response.DimseStatus != 0x0111) // Warning - Duplicate SOPInstanceUID (https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3) { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); throw OrthancException(ErrorCode_NetworkProtocol, "C-STORE SCU to AET \"" + GetParameters().GetRemoteModality().GetApplicationEntityTitle() + - "\" has failed with DIMSE status 0x" + buf); + "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus), + response.DimseStatus); } }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -192,7 +192,7 @@ if (e.GetErrorCode() == ErrorCode_InexistentTag) { - FromDcmtkBridge::LogMissingTagsForStore(**imageDataSet); + LOG(ERROR) << FromDcmtkBridge::FormatMissingTagsForStore(**imageDataSet); } else {
--- a/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -135,7 +135,7 @@ { if (dir_.get() == NULL) { - dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), + dir_.reset(new DcmDicomDir(SystemToolbox::PathToUtf8(file_.GetPath()).c_str(), fileSetId_.c_str())); //SetTagValue(dir_->getRootRecord(), DCM_SpecificCharacterSet, GetDicomSpecificCharacterSet(Encoding_Utf8)); } @@ -167,7 +167,8 @@ { if (s != NULL) { - result = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + const bool skipBacklashes = true; // cf. "ISO_IR 13": In this method, the VR will never be UT, ST, or LT + result = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions, skipBacklashes); } return true;
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -179,6 +179,12 @@ return GetDefaultAction(parentTags, parentIndexes, tag); } + virtual Action VisitEmptyElement(const std::vector<DicomTag>& parentTags, + const std::vector<size_t>& parentIndexes) ORTHANC_OVERRIDE + { + return Action_None; + } + virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag,
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -241,6 +241,25 @@ const std::vector<size_t>& parentIndexes, const DicomTag& tag) { + Json::Value& node = CreateEmptyNode(parentTags, parentIndexes); + assert(node.type() == Json::objectValue); + + std::string t = FormatTag(tag); + if (node.isMember(t)) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + node[t] = Json::objectValue; + return node[t]; + } + } + + + Json::Value& DicomWebJsonVisitor::CreateEmptyNode(const std::vector<DicomTag>& parentTags, + const std::vector<size_t>& parentIndexes) + { assert(parentTags.size() == parentIndexes.size()); Json::Value* node = &result_; @@ -291,16 +310,7 @@ assert(node->type() == Json::objectValue); - std::string t = FormatTag(tag); - if (node->isMember(t)) - { - throw OrthancException(ErrorCode_InternalError); - } - else - { - (*node) [t] = Json::objectValue; - return (*node) [t]; - } + return *node; } @@ -425,6 +435,14 @@ return Action_None; } + ITagVisitor::Action + DicomWebJsonVisitor::VisitEmptyElement(const std::vector<DicomTag>& parentTags, + const std::vector<size_t>& parentIndexes) + { + CreateEmptyNode(parentTags, parentIndexes); + return Action_None; + } + ITagVisitor::Action DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
--- a/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h Tue Nov 04 15:58:06 2025 +0100 @@ -70,6 +70,9 @@ const std::vector<size_t>& parentIndexes, const DicomTag& tag); + Json::Value& CreateEmptyNode(const std::vector<DicomTag>& parentTags, + const std::vector<size_t>& parentIndexes); + static Json::Value FormatInteger(int64_t value); static Json::Value FormatDouble(double value); @@ -109,6 +112,10 @@ size_t size) ORTHANC_OVERRIDE; + virtual Action VisitEmptyElement(const std::vector<DicomTag>& parentTags, + const std::vector<size_t>& parentIndexes) + ORTHANC_OVERRIDE; + virtual Action VisitIntegers(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag,
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -38,6 +38,7 @@ #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" +#include "../ChunkedBuffer.h" #include "../Compatibility.h" #include "../Logging.h" #include "../Toolbox.h" @@ -45,6 +46,7 @@ #if ORTHANC_SANDBOXED == 0 # include "../TemporaryFile.h" +# include "../SystemToolbox.h" #endif #include <list> @@ -57,7 +59,6 @@ #include <dcmtk/dcmdata/dcdeftag.h> #include <dcmtk/dcmdata/dcdicent.h> -#include <dcmtk/dcmdata/dcdict.h> #include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmdata/dcistrmb.h> #include <dcmtk/dcmdata/dcostrmb.h> @@ -126,6 +127,38 @@ namespace Orthanc { + FromDcmtkBridge::DictionaryWriterLock::DictionaryWriterLock() : + dictionary_(dcmDataDict.wrlock()) + { + } + + + FromDcmtkBridge::DictionaryWriterLock::~DictionaryWriterLock() + { +#if DCMTK_VERSION_NUMBER >= 364 + dcmDataDict.wrunlock(); +#else + dcmDataDict.unlock(); +#endif + } + + + FromDcmtkBridge::DictionaryReaderLock::DictionaryReaderLock() : + dictionary_(dcmDataDict.rdlock()) + { + } + + + FromDcmtkBridge::DictionaryReaderLock::~DictionaryReaderLock() + { +#if DCMTK_VERSION_NUMBER >= 364 + dcmDataDict.rdunlock(); +#else + dcmDataDict.unlock(); +#endif + } + + static bool IsBinaryTag(const DcmTag& key) { return (key.isUnknownVR() || @@ -147,7 +180,7 @@ TemporaryFile tmp; tmp.Write(content); - if (!dictionary.loadDictionary(tmp.GetPath().c_str())) + if (!dictionary.loadDictionary(SystemToolbox::PathToUtf8(tmp.GetPath()).c_str())) { throw OrthancException(ErrorCode_InternalError, "Cannot read embedded dictionary. Under Windows, make sure that " @@ -167,37 +200,73 @@ namespace { - class DictionaryLocker : public boost::noncopyable + class ChunkedBufferStream : public DcmOutputStream { private: - DcmDataDictionary& dictionary_; + class Consumer : public DcmConsumer + { + private: + ChunkedBuffer buffer_; + + public: + void Flatten(std::string& buffer) + { + buffer_.Flatten(buffer); + } + + OFBool good() const ORTHANC_OVERRIDE + { + return true; + } + + OFCondition status() const ORTHANC_OVERRIDE + { + return EC_Normal; + } + + OFBool isFlushed() const ORTHANC_OVERRIDE + { + return true; + } + + offile_off_t avail() const ORTHANC_OVERRIDE + { + // since we cannot report "unlimited", let's claim that we can still write 10MB. + // Note that offile_off_t is a signed type. + return 10 * 1024 * 1024; + } + + offile_off_t write(const void *buf, + offile_off_t buflen) ORTHANC_OVERRIDE + { + buffer_.AddChunk(buf, buflen); + return buflen; + } + + void flush() ORTHANC_OVERRIDE + { + // Nothing to flush + } + }; + + Consumer consumer_; public: - DictionaryLocker() : dictionary_(dcmDataDict.wrlock()) + ChunkedBufferStream() : + DcmOutputStream(&consumer_) { } - ~DictionaryLocker() + void Flatten(std::string& buffer) { -#if DCMTK_VERSION_NUMBER >= 364 - dcmDataDict.wrunlock(); -#else - dcmDataDict.unlock(); -#endif - } - - DcmDataDictionary& operator*() - { - return dictionary_; - } - - DcmDataDictionary* operator->() - { - return &dictionary_; + consumer_.Flatten(buffer); } }; - - + } + + + namespace + { ORTHANC_FORCE_INLINE static std::string FloatToString(float v) { @@ -296,9 +365,9 @@ #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1 { - DictionaryLocker locker; - - locker->clear(); + DictionaryWriterLock lock; + + lock.GetDictionary().clear(); CLOG(INFO, DICOM) << "Loading the embedded dictionaries"; /** @@ -306,14 +375,14 @@ * command "strace storescu 2>&1 |grep dic" shows that DICONDE * dictionary is not loaded by storescu. **/ - //LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICONDE); - - LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICOM); + //LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICONDE); + + LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_DICOM); if (loadPrivateDictionary) { CLOG(INFO, DICOM) << "Loading the embedded dictionary of private tags"; - LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_PRIVATE); + LoadEmbeddedDictionary(lock.GetDictionary(), FrameworkResources::DICTIONARY_PRIVATE); } else { @@ -373,16 +442,16 @@ void FromDcmtkBridge::LoadExternalDictionaries(const std::vector<std::string>& dictionaries) { - DictionaryLocker locker; + DictionaryWriterLock lock; CLOG(INFO, DICOM) << "Clearing the DICOM dictionary"; - locker->clear(); + lock.GetDictionary().clear(); for (size_t i = 0; i < dictionaries.size(); i++) { LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\""; - if (!locker->loadDictionary(dictionaries[i].c_str())) + if (!lock.GetDictionary().loadDictionary(dictionaries[i].c_str())) { throw OrthancException(ErrorCode_InexistentFile); } @@ -475,10 +544,10 @@ entry->setElementRangeRestriction(DcmDictRange_Unspecified); { - DictionaryLocker locker; - - if (locker->findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()), - privateCreator.empty() ? NULL : privateCreator.c_str())) + DictionaryWriterLock lock; + + if (lock.GetDictionary().findEntry(DcmTagKey(tag.GetGroup(), tag.GetElement()), + privateCreator.empty() ? NULL : privateCreator.c_str())) { throw OrthancException(ErrorCode_AlreadyExistingTag, "Cannot register twice the tag (" + tag.Format() + @@ -486,7 +555,7 @@ } else { - locker->addEntry(entry.release()); + lock.GetDictionary().addEntry(entry.release()); } } } @@ -564,8 +633,8 @@ { target.SetValueInternal(element->getTag().getGTag(), element->getTag().getETag(), - ConvertLeafElement(*element, DicomToJsonFlags_Default, - maxStringLength, encoding, hasCodeExtensions, ignoreTagLength)); + ConvertLeafElement(*element, DicomToJsonFlags_Default, maxStringLength, encoding, + hasCodeExtensions, ignoreTagLength, Convert(element->getVR()))); } else { @@ -626,7 +695,8 @@ unsigned int maxStringLength, Encoding encoding, bool hasCodeExtensions, - const std::set<DicomTag>& ignoreTagLength) + const std::set<DicomTag>& ignoreTagLength, + ValueRepresentation vr) { if (!element.isLeaf()) { @@ -646,7 +716,7 @@ else { const std::string s(c); - const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + const std::string utf8 = Toolbox::ConvertDicomStringToUtf8(s, encoding, hasCodeExtensions, Convert(element.getVR())); return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength); } } @@ -663,10 +733,11 @@ * syntax (cf. DICOM CP 246). * ftp://medical.nema.org/medical/dicom/final/cp246_ft.pdf **/ - DictionaryLocker locker; - - const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), - element.getTag().getPrivateCreator()); + DictionaryReaderLock lock; + + // The "entry" value is only valid while "lock" is active + const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(), + element.getTag().getPrivateCreator()); if (entry != NULL && entry->getVR().isaString()) { @@ -713,7 +784,7 @@ // "SpecificCharacterSet" tag, if present. This branch is // new in Orthanc 1.9.1 (cf. DICOM CP 246). const std::string s(reinterpret_cast<const char*>(data), length); - const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + const std::string utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions, Convert(element.getVR())); return CreateValueFromUtf8String(GetTag(element), utf8, maxStringLength, ignoreTagLength); } } @@ -1033,7 +1104,7 @@ { // The "0" below lets "LeafValueToJson()" take care of "TooLong" values std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement - (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength)); + (element, flags, 0, encoding, hasCodeExtensions, ignoreTagLength, Convert(element.getVR()))); if (ignoreTagLength.find(GetTag(element)) == ignoreTagLength.end()) { @@ -1111,8 +1182,8 @@ if (!(flags & DicomToJsonFlags_IncludeUnknownTags)) { - DictionaryLocker locker; - if (locker->findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL) + DictionaryReaderLock lock; + if (lock.GetDictionary().findEntry(element->getTag(), element->getTag().getPrivateCreator()) == NULL) { continue; } @@ -1572,7 +1643,12 @@ } - +#if 0 + /** + * This was the implementation in Orthanc <= 1.12.7. This version + * uses "DcmFileFormat::calcElementLength()", which cannot handle + * DICOM files whose size cannot be represented on 32 bits. + **/ static bool SaveToMemoryBufferInternal(std::string& buffer, DcmFileFormat& dicom, E_TransferSyntax xfer, @@ -1619,6 +1695,46 @@ return false; } } +#endif + + +#if 1 + /** + * This is the cleaner implementation used in Orthanc >= 1.12.8, + * which allows to write DICOM files larger than 4GB. + **/ + static bool SaveToMemoryBufferInternal(std::string& buffer, + DcmFileFormat& dicom, + E_TransferSyntax xfer, + std::string& errorMessage) + { + ChunkedBufferStream ob; + + // Fill the (chunked) memory buffer with the meta-header and the dataset + dicom.transferInit(); + OFCondition c = dicom.write(ob, xfer, /*opt_sequenceType*/ EET_ExplicitLength, NULL, + /*opt_groupLength*/ EGL_recalcGL, + /*opt_paddingType*/ EPD_noChange, + /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0, + EWM_updateMeta /* creates new SOP instance UID on lossy */); + dicom.transferEnd(); + + if (c.good()) + { + ob.flush(); + ob.Flatten(buffer); + return true; + } + else + { + // Error + buffer.clear(); + errorMessage = std::string(c.text()); + return false; + } + } +#endif + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, DcmDataset& dataSet) @@ -2480,7 +2596,7 @@ element->getString(c).good() && c != NULL) { - std::string a = Toolbox::ConvertToUtf8(c, source, hasSourceCodeExtensions); + std::string a = Toolbox::ConvertToUtf8(c, source, hasSourceCodeExtensions, Convert(element->getVR())); std::string b = Toolbox::ConvertFromUtf8(a, target); element->putString(b.c_str()); } @@ -2573,22 +2689,28 @@ std::set<DcmTagKey> toRemove; - for (unsigned long i = 0; i < dataset.card(); i++) + if (dataset.card() > 0) { - DcmElement* element = dataset.getElement(i); - if (element == NULL) + for (unsigned long i = 0; i < dataset.card(); i++) { - throw OrthancException(ErrorCode_InternalError); + DcmElement* element = dataset.getElement(i); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + if (!ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions)) + { + toRemove.insert(element->getTag()); + } + } } - else - { - if (!ApplyVisitorToElement(*element, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions)) - { - toRemove.insert(element->getTag()); - } - } } - + else + { + visitor.VisitEmptyElement(parentTags, parentIndexes); + } // Remove all the tags that were planned for removal (cf. ITagVisitor::Action_Remove) for (std::set<DcmTagKey>::const_iterator it = toRemove.begin(); it != toRemove.end(); ++it) @@ -2646,10 +2768,11 @@ if (evr == EVR_UN) { // New in Orthanc 1.9.5 - DictionaryLocker locker; - - const DcmDictEntry* entry = locker->findEntry(element.getTag().getXTag(), - element.getTag().getPrivateCreator()); + FromDcmtkBridge::DictionaryReaderLock lock; + + // The "entry" value is only valid while "lock" is active + const DcmDictEntry* entry = lock.GetDictionary().findEntry(element.getTag().getXTag(), + element.getTag().getPrivateCreator()); if (entry != NULL) { @@ -2733,7 +2856,7 @@ else { std::string s(c); - utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions); + utf8 = Toolbox::ConvertDicomStringToUtf8(s, encoding, hasCodeExtensions, FromDcmtkBridge::Convert(element.getVR())); } } @@ -2809,8 +2932,8 @@ std::string ignored; std::string s(reinterpret_cast<const char*>(data), l); - action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr, - Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions)); + std::string utf8 = Toolbox::ConvertDicomStringToUtf8(s, encoding, hasCodeExtensions, FromDcmtkBridge::Convert(element.getVR())); + action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr, utf8); } else { @@ -3162,7 +3285,7 @@ } - void FromDcmtkBridge::LogMissingTagsForStore(DcmDataset& dicom) + std::string FromDcmtkBridge::FormatMissingTagsForStore(DcmDataset& dicom) { std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; @@ -3194,7 +3317,7 @@ sopInstanceUid.assign(c); } - DicomMap::LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + return DicomMap::FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); }
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Tue Nov 04 15:58:06 2025 +0100 @@ -30,9 +30,10 @@ #include "../DicomFormat/DicomPath.h" #include <dcmtk/dcmdata/dcdatset.h> +#include <dcmtk/dcmdata/dcdict.h> +#include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmdata/dcmetinf.h> #include <dcmtk/dcmdata/dcpixseq.h> -#include <dcmtk/dcmdata/dcfilefo.h> #include <json/value.h> #if ORTHANC_ENABLE_DCMTK != 1 @@ -86,6 +87,40 @@ }; + class ORTHANC_PUBLIC DictionaryWriterLock : public boost::noncopyable + { + private: + DcmDataDictionary& dictionary_; + + public: + DictionaryWriterLock(); + + ~DictionaryWriterLock(); + + DcmDataDictionary& GetDictionary() + { + return dictionary_; + } + }; + + + class ORTHANC_PUBLIC DictionaryReaderLock : public boost::noncopyable + { + private: + const DcmDataDictionary& dictionary_; + + public: + DictionaryReaderLock(); + + ~DictionaryReaderLock(); + + const DcmDataDictionary& GetDictionary() const + { + return dictionary_; + } + }; + + private: FromDcmtkBridge(); // Pure static class @@ -157,7 +192,8 @@ unsigned int maxStringLength, Encoding encoding, bool hasCodeExtensions, - const std::set<DicomTag>& ignoreTagLength); + const std::set<DicomTag>& ignoreTagLength, + ValueRepresentation vr); static void ExtractHeaderAsJson(Json::Value& target, DcmMetaInfo& header, @@ -280,7 +316,7 @@ static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, DcmDataset& dicom); - static void LogMissingTagsForStore(DcmDataset& dicom); + static std::string FormatMissingTagsForStore(DcmDataset& dicom); static void RemovePath(DcmDataset& dataset, const DicomPath& path);
--- a/OrthancFramework/Sources/DicomParsing/ITagVisitor.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h Tue Nov 04 15:58:06 2025 +0100 @@ -93,5 +93,9 @@ const DicomTag& tag, ValueRepresentation vr, const std::string& value) = 0; - }; + + // empty sequence element - can return "Remove" or "None" + virtual Action VisitEmptyElement(const std::vector<DicomTag>& parentTags, + const std::vector<size_t>& parentIndexes) = 0; + }; }
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -428,19 +428,36 @@ lutBlue == NULL || pixelData == NULL) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, "Inconsistent Palette Color Lookup Table"); } switch (format) { case PixelFormat_RGB24: { - if (r != "256\\0\\16" || - rc != 256 || - gc != 256 || - bc != 256) + std::vector<std::string> splitR; + Toolbox::SplitString(splitR, r.c_str(), '\\'); + if (splitR.size() != 3) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, std::string("Palette Color Lookup Table Descriptor invalid length: '") + r.c_str() + "'"); + } + + const unsigned int paletteSize = boost::lexical_cast<unsigned int>(splitR[0]); + const unsigned int firstInputValueMapped = boost::lexical_cast<unsigned int>(splitR[1]); + const unsigned int nbBitsUsedInLut = boost::lexical_cast<unsigned int>(splitR[2]); // The LUT is always 16bits but only part of it might be used + if (firstInputValueMapped != 0 || + nbBitsUsedInLut < 8) + { + throw OrthancException(ErrorCode_NotImplemented, "Inconsistent Palette Color Lookup Table"); + } + + const unsigned int offsetBits = nbBitsUsedInLut - 8; // Since the LUT is 16bits and the target color value is 8bpp + + if (rc != paletteSize || + gc != paletteSize || + bc != paletteSize) + { + throw OrthancException(ErrorCode_NotImplemented, std::string("Palette Color Lookup Table Descriptor invalid palette size: '") + r.c_str() + "'"); } if (pixelLength != target->GetWidth() * target->GetHeight()) @@ -477,9 +494,9 @@ for (unsigned int x = 0; x < width; x++) { - p[0] = lutRed[*source] >> 8; - p[1] = lutGreen[*source] >> 8; - p[2] = lutBlue[*source] >> 8; + p[0] = lutRed[*source] >> offsetBits; + p[1] = lutGreen[*source] >> offsetBits; + p[2] = lutBlue[*source] >> offsetBits; source++; p += 3; } @@ -496,7 +513,7 @@ bc != 65536 || pixelLength != 2 * target->GetWidth() * target->GetHeight()) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, std::string("Palette Color Lookup Table Descriptor not supported: '") + r.c_str() + "'"); } const uint16_t* source = reinterpret_cast<const uint16_t*>(pixelData);
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -893,7 +893,7 @@ std::set<DicomTag> tmp; std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement (*element, DicomToJsonFlags_Default, - 0, encoding, hasCodeExtensions, tmp)); + 0, encoding, hasCodeExtensions, tmp, FromDcmtkBridge::Convert(element->getVR()))); if (v.get() == NULL || v->IsNull()) @@ -1694,16 +1694,23 @@ MimeType& mime, unsigned int frameId) const { + DcmDataset* dcmDataset = GetDcmtkObjectConst().getDataset(); + + if (!this->HasTag(DICOM_TAG_PIXEL_DATA) && !DicomImageDecoder::IsPsmctRle1(*dcmDataset)) + { + throw OrthancException(ErrorCode_BadRequest, "Cannot extract a frame from a DIOCM file that does not have pixel data."); + } + if (pimpl_->frameIndex_.get() == NULL) { - assert(pimpl_->file_ != NULL && - GetDcmtkObjectConst().getDataset() != NULL); - pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObjectConst().getDataset())); + assert(pimpl_->file_ != NULL && dcmDataset != NULL); + + pimpl_->frameIndex_.reset(new DicomFrameIndex(*dcmDataset)); } pimpl_->frameIndex_->GetRawFrame(target, frameId); - E_TransferSyntax transferSyntax = GetDcmtkObjectConst().getDataset()->getCurrentXfer(); + E_TransferSyntax transferSyntax = dcmDataset->getCurrentXfer(); switch (transferSyntax) { case EXS_JPEGProcess1:
--- a/OrthancFramework/Sources/Enumerations.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Enumerations.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -247,7 +247,7 @@ return "The specified path does not point to a directory"; case ErrorCode_HttpPortInUse: - return "The TCP port of the HTTP server is privileged or already in use"; + return "The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist"; case ErrorCode_DicomPortInUse: return "The TCP port of the DICOM server is privileged or already in use";
--- a/OrthancFramework/Sources/Enumerations.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Enumerations.h Tue Nov 04 15:58:06 2025 +0100 @@ -192,7 +192,7 @@ ErrorCode_DirectoryOverFile = 2000 /*!< The directory to be created is already occupied by a regular file */, ErrorCode_FileStorageCannotWrite = 2001 /*!< Unable to create a subdirectory or a file in the file storage */, ErrorCode_DirectoryExpected = 2002 /*!< The specified path does not point to a directory */, - ErrorCode_HttpPortInUse = 2003 /*!< The TCP port of the HTTP server is privileged or already in use */, + ErrorCode_HttpPortInUse = 2003 /*!< The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist */, ErrorCode_DicomPortInUse = 2004 /*!< The TCP port of the DICOM server is privileged or already in use */, ErrorCode_BadHttpStatusInRest = 2005 /*!< This HTTP status is not allowed in a REST API */, ErrorCode_RegularFileExpected = 2006 /*!< The specified path does not point to a regular file */,
--- a/OrthancFramework/Sources/FileStorage/FileInfo.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -169,4 +169,56 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + void FileInfo::SetCustomData(const void* data, + size_t size) + { + if (valid_) + { + customData_.assign(reinterpret_cast<const char*>(data), size); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FileInfo::SetCustomData(const std::string& data) + { + if (valid_) + { + customData_ = data; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FileInfo::SwapCustomData(std::string& data) + { + if (valid_) + { + customData_.swap(data); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + const std::string& FileInfo::GetCustomData() const + { + if (valid_) + { + return customData_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancFramework/Sources/FileStorage/FileInfo.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FileInfo.h Tue Nov 04 15:58:06 2025 +0100 @@ -42,6 +42,7 @@ CompressionType compressionType_; uint64_t compressedSize_; std::string compressedMD5_; + std::string customData_; public: FileInfo(); @@ -80,5 +81,14 @@ const std::string& GetCompressedMD5() const; const std::string& GetUncompressedMD5() const; + + void SetCustomData(const void* data, + size_t size); + + void SetCustomData(const std::string& data); + + void SwapCustomData(std::string& data); + + const std::string& GetCustomData() const; }; }
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -72,7 +72,7 @@ return path; } - void FilesystemStorage::Setup(const std::string& root) + void FilesystemStorage::Setup(const boost::filesystem::path& root) { //root_ = boost::filesystem::absolute(root).string(); root_ = root; @@ -80,13 +80,13 @@ SystemToolbox::MakeDirectory(root); } - FilesystemStorage::FilesystemStorage(const std::string &root) : + FilesystemStorage::FilesystemStorage(const boost::filesystem::path &root) : fsyncOnWrite_(false) { Setup(root); } - FilesystemStorage::FilesystemStorage(const std::string &root, + FilesystemStorage::FilesystemStorage(const boost::filesystem::path &root, bool fsyncOnWrite) : fsyncOnWrite_(fsyncOnWrite) { @@ -171,12 +171,12 @@ try { - SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_); + SystemToolbox::WriteFile(content, size, path, fsyncOnWrite_); LOG(INFO) << "Created attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, size) << ")"; return; } - catch (OrthancException& e) + catch (OrthancException&) { if (retryCount >= maxRetryCount) { @@ -187,15 +187,15 @@ } - IMemoryBuffer* FilesystemStorage::Read(const std::string& uuid, - FileContentType type) + IMemoryBuffer* FilesystemStorage::ReadWhole(const std::string& uuid, + FileContentType type) { Toolbox::ElapsedTimer timer; LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) << "\" content type"; std::string content; - SystemToolbox::ReadFile(content, GetPath(uuid).string()); + SystemToolbox::ReadFile(content, GetPath(uuid)); LOG(INFO) << "Read attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, content.size()) << ")"; @@ -214,19 +214,13 @@ std::string content; SystemToolbox::ReadFileRange( - content, GetPath(uuid).string(), start, end, true /* throw if overflow */); + content, GetPath(uuid), start, end, true /* throw if overflow */); LOG(INFO) << "Read range of attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, content.size()) << ")"; return StringMemoryBuffer::CreateFromSwap(content); } - bool FilesystemStorage::HasReadRange() const - { - return true; - } - - uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const { boost::filesystem::path path = GetPath(uuid); @@ -245,7 +239,7 @@ { for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current) { - if (SystemToolbox::IsRegularFile(current->path().string())) + if (SystemToolbox::IsRegularFile(current->path())) { try { @@ -354,7 +348,7 @@ const std::string& uuid, FileContentType type) { - std::unique_ptr<IMemoryBuffer> buffer(Read(uuid, type)); + std::unique_ptr<IMemoryBuffer> buffer(ReadWhole(uuid, type)); buffer->MoveToString(content); } #endif
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h Tue Nov 04 15:58:06 2025 +0100 @@ -55,7 +55,7 @@ boost::filesystem::path GetPath(const std::string& uuid) const; - void Setup(const std::string& root); + void Setup(const boost::filesystem::path& root); #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore @@ -70,9 +70,9 @@ #endif public: - explicit FilesystemStorage(const std::string& root); + explicit FilesystemStorage(const boost::filesystem::path& root); - FilesystemStorage(const std::string& root, + FilesystemStorage(const boost::filesystem::path& root, bool fsyncOnWrite); virtual void Create(const std::string& uuid, @@ -80,15 +80,19 @@ size_t size, FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE; + // This flavor is only used in the "DelayedDeletion" and "orthanc-webviewer" plugins + IMemoryBuffer* ReadWhole(const std::string& uuid, + FileContentType type); virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) ORTHANC_OVERRIDE; - virtual bool HasReadRange() const ORTHANC_OVERRIDE; + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } virtual void Remove(const std::string& uuid, FileContentType type) ORTHANC_OVERRIDE;
--- a/OrthancFramework/Sources/FileStorage/IStorageArea.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h Tue Nov 04 15:58:06 2025 +0100 @@ -24,8 +24,9 @@ #pragma once +#include "../Compatibility.h" +#include "../Enumerations.h" #include "../IMemoryBuffer.h" -#include "../Enumerations.h" #include <stdint.h> #include <string> @@ -33,6 +34,8 @@ namespace Orthanc { + class DicomInstanceToStore; + class IStorageArea : public boost::noncopyable { public: @@ -45,17 +48,44 @@ size_t size, FileContentType type) = 0; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) = 0; - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) = 0; - virtual bool HasReadRange() const = 0; + virtual bool HasEfficientReadRange() const = 0; virtual void Remove(const std::string& uuid, FileContentType type) = 0; }; + + + // storage area with customData (customData are used only in plugins) + class IPluginStorageArea : public boost::noncopyable + { + public: + virtual ~IPluginStorageArea() + { + } + + virtual void Create(std::string& customData /* out */, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) = 0; + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) = 0; + + virtual bool HasEfficientReadRange() const = 0; + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) = 0; + }; }
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -69,31 +69,6 @@ } - IMemoryBuffer* MemoryStorageArea::Read(const std::string& uuid, - FileContentType type) - { - LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" - << static_cast<int>(type) << "\" content type"; - - Mutex::ScopedLock lock(mutex_); - - Content::const_iterator found = content_.find(uuid); - - if (found == content_.end()) - { - throw OrthancException(ErrorCode_InexistentFile); - } - else if (found->second == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - else - { - return StringMemoryBuffer::CreateFromCopy(*found->second); - } - } - - IMemoryBuffer* MemoryStorageArea::ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -113,6 +88,12 @@ } else { + const uint64_t size = end - start; + if (static_cast<uint64_t>(static_cast<size_t>(size)) != size) + { + throw OrthancException(ErrorCode_InternalError, "Buffer larger than 4GB, which is too large for Orthanc running in 32bits"); + } + Mutex::ScopedLock lock(mutex_); Content::const_iterator found = content_.find(uuid); @@ -132,7 +113,7 @@ else { std::string range; - range.resize(end - start); + range.resize(static_cast<size_t>(size)); assert(!range.empty()); memcpy(&range[0], &found->second[start], range.size()); @@ -143,12 +124,6 @@ } - bool MemoryStorageArea::HasReadRange() const - { - return true; - } - - void MemoryStorageArea::Remove(const std::string& uuid, FileContentType type) {
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Tue Nov 04 15:58:06 2025 +0100 @@ -49,15 +49,15 @@ size_t size, FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE; - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) ORTHANC_OVERRIDE; - virtual bool HasReadRange() const ORTHANC_OVERRIDE; + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } virtual void Remove(const std::string& uuid, FileContentType type) ORTHANC_OVERRIDE;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "PluginStorageAreaAdapter.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + PluginStorageAreaAdapter::PluginStorageAreaAdapter(IStorageArea* storage) : + storage_(storage) + { + if (storage == NULL) + { + throw OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + + void PluginStorageAreaAdapter::Create(std::string& customData, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance) + { + customData.clear(); + storage_->Create(uuid, content, size, type); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,69 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "IStorageArea.h" + + +namespace Orthanc +{ + class ORTHANC_PUBLIC PluginStorageAreaAdapter : public IPluginStorageArea + { + private: + std::unique_ptr<IStorageArea> storage_; + + public: + explicit PluginStorageAreaAdapter(IStorageArea* storage /* takes ownership */); + + virtual void Create(std::string& customData, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance) ORTHANC_OVERRIDE; + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + return storage_->ReadRange(uuid, type, start, end); + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + storage_->Remove(uuid, type); + } + + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return storage_->HasEfficientReadRange(); + } + }; +}
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -275,7 +275,7 @@ }; - StorageAccessor::StorageAccessor(IStorageArea& area) : + StorageAccessor::StorageAccessor(IPluginStorageArea& area) : area_(area), cache_(NULL), metrics_(NULL) @@ -283,7 +283,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache) : area_(area), cache_(&cache), @@ -292,7 +292,7 @@ } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics) : area_(area), cache_(NULL), @@ -300,7 +300,7 @@ { } - StorageAccessor::StorageAccessor(IStorageArea& area, + StorageAccessor::StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics) : area_(area), @@ -310,13 +310,15 @@ } - FileInfo StorageAccessor::Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5) + void StorageAccessor::Write(FileInfo& info, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const DicomInstanceToStore* instance) { - std::string uuid = Toolbox::GenerateUuid(); + const std::string uuid = Toolbox::GenerateUuid(); std::string md5; @@ -325,13 +327,15 @@ Toolbox::ComputeMD5(md5, data, size); } + std::string customData; + switch (compression) { case CompressionType_None: { { MetricsTimer timer(*this, METRICS_CREATE_DURATION); - area_.Create(uuid, data, size, type); + area_.Create(customData, uuid, data, size, type, compression, instance); } if (metrics_ != NULL) @@ -345,7 +349,9 @@ cacheAccessor.Add(uuid, type, data, size); } - return FileInfo(uuid, type, size, md5); + info = FileInfo(uuid, type, size, md5); + info.SetCustomData(customData); + return; } case CompressionType_ZlibWithSize: @@ -367,11 +373,11 @@ if (compressed.size() > 0) { - area_.Create(uuid, &compressed[0], compressed.size(), type); + area_.Create(customData, uuid, &compressed[0], compressed.size(), type, compression, instance); } else { - area_.Create(uuid, NULL, 0, type); + area_.Create(customData, uuid, NULL, 0, type, compression, instance); } } @@ -386,8 +392,10 @@ cacheAccessor.Add(uuid, type, data, size); // always add uncompressed data to cache } - return FileInfo(uuid, type, size, md5, + info = FileInfo(uuid, type, size, md5, CompressionType_ZlibWithSize, compressed.size(), compressedMD5); + info.SetCustomData(customData); + return; } default: @@ -395,16 +403,6 @@ } } - FileInfo StorageAccessor::Write(const std::string &data, - FileContentType type, - CompressionType compression, - bool storeMd5) - { - return Write((data.size() == 0 ? NULL : data.c_str()), - data.size(), type, compression, storeMd5); - } - - void StorageAccessor::Read(std::string& content, const FileInfo& info) { @@ -446,7 +444,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -467,7 +465,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - compressed.reset(area_.Read(info.GetUuid(), info.GetContentType())); + compressed.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -526,7 +524,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } if (metrics_ != NULL) @@ -539,7 +537,8 @@ void StorageAccessor::Remove(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { if (cache_ != NULL) { @@ -548,14 +547,14 @@ { MetricsTimer timer(*this, METRICS_REMOVE_DURATION); - area_.Remove(fileUuid, type); + area_.Remove(fileUuid, type, customData); } } void StorageAccessor::Remove(const FileInfo &info) { - Remove(info.GetUuid(), info.GetContentType()); + Remove(info.GetUuid(), info.GetContentType(), info.GetCustomData()); } @@ -616,7 +615,7 @@ { MetricsTimer timer(*this, METRICS_READ_DURATION); - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, end, info.GetCustomData())); assert(buffer->GetSize() == end); } @@ -682,19 +681,19 @@ if (range.HasStart() && range.HasEnd()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1, info.GetCustomData())); } else if (range.HasStart()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize(), info.GetCustomData())); } else if (range.HasEnd()) { - buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1)); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1, info.GetCustomData())); } else { - buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, info.GetCompressedSize(), info.GetCustomData())); } buffer->MoveToString(target); @@ -785,4 +784,5 @@ output.AnswerStream(transcoder); } #endif + }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Tue Nov 04 15:58:06 2025 +0100 @@ -110,7 +110,7 @@ private: class MetricsTimer; - IStorageArea& area_; + IPluginStorageArea& area_; StorageCache* cache_; MetricsRegistry* metrics_; @@ -121,28 +121,25 @@ #endif public: - explicit StorageAccessor(IStorageArea& area); + explicit StorageAccessor(IPluginStorageArea& area); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, MetricsRegistry& metrics); - StorageAccessor(IStorageArea& area, + StorageAccessor(IPluginStorageArea& area, StorageCache& cache, MetricsRegistry& metrics); - FileInfo Write(const void* data, - size_t size, - FileContentType type, - CompressionType compression, - bool storeMd5); - - FileInfo Write(const std::string& data, - FileContentType type, - CompressionType compression, - bool storeMd5); + void Write(FileInfo& info /* out */, + const void* data, + size_t size, + FileContentType type, + CompressionType compression, + bool storeMd5, + const DicomInstanceToStore* instance); void Read(std::string& content, const FileInfo& info); @@ -155,7 +152,8 @@ uint64_t end /* exclusive */); void Remove(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); void Remove(const FileInfo& info); @@ -185,6 +183,7 @@ const std::string& mime, const std::string& contentFilename); #endif + private: void ReadStartRangeInternal(std::string& target, const FileInfo& info,
--- a/OrthancFramework/Sources/HttpClient.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpClient.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -459,7 +459,7 @@ private: boost::mutex mutex_; bool httpsVerifyPeers_; - std::string httpsCACertificates_; + boost::filesystem::path httpsCACertificates_; std::string proxy_; long timeout_; bool verbose_; @@ -480,7 +480,7 @@ } void ConfigureSsl(bool httpsVerifyPeers, - const std::string& httpsCACertificates) + const boost::filesystem::path& httpsCACertificates) { boost::mutex::scoped_lock lock(mutex_); httpsVerifyPeers_ = httpsVerifyPeers; @@ -488,7 +488,7 @@ } void GetSslConfiguration(bool& httpsVerifyPeers, - std::string& httpsCACertificates) + boost::filesystem::path& httpsCACertificates) { boost::mutex::scoped_lock lock(mutex_); httpsVerifyPeers = httpsVerifyPeers_; @@ -1179,19 +1179,19 @@ return verifyPeers_; } - void HttpClient::SetHttpsCACertificates(const std::string &certificates) + void HttpClient::SetHttpsCACertificates(const boost::filesystem::path& certificates) { caCertificates_ = certificates; } - const std::string &HttpClient::GetHttpsCACertificates() const + const boost::filesystem::path& HttpClient::GetHttpsCACertificates() const { return caCertificates_; } void HttpClient::ConfigureSsl(bool httpsVerifyPeers, - const std::string& httpsVerifyCertificates) + const boost::filesystem::path& httpsVerifyCertificates) { #if ORTHANC_ENABLE_SSL == 1 if (httpsVerifyPeers) @@ -1337,8 +1337,8 @@ } - void HttpClient::SetClientCertificate(const std::string& certificateFile, - const std::string& certificateKeyFile, + void HttpClient::SetClientCertificate(const boost::filesystem::path& certificateFile, + const boost::filesystem::path &certificateKeyFile, const std::string& certificateKeyPassword) { if (certificateFile.empty()) @@ -1349,14 +1349,14 @@ if (!SystemToolbox::IsRegularFile(certificateFile)) { throw OrthancException(ErrorCode_InexistentFile, - "Cannot open certificate file: " + certificateFile); + "Cannot open certificate file: " + SystemToolbox::PathToUtf8(certificateFile)); } if (!certificateKeyFile.empty() && !SystemToolbox::IsRegularFile(certificateKeyFile)) { throw OrthancException(ErrorCode_InexistentFile, - "Cannot open key file: " + certificateKeyFile); + "Cannot open key file: " + SystemToolbox::PathToUtf8(certificateKeyFile)); } clientCertificateFile_ = certificateFile; @@ -1374,17 +1374,17 @@ return pkcs11Enabled_; } - const std::string &HttpClient::GetClientCertificateFile() const + const boost::filesystem::path& HttpClient::GetClientCertificateFile() const { return clientCertificateFile_; } - const std::string &HttpClient::GetClientCertificateKeyFile() const + const boost::filesystem::path& HttpClient::GetClientCertificateKeyFile() const { return clientCertificateKeyFile_; } - const std::string &HttpClient::GetClientCertificateKeyPassword() const + const std::string& HttpClient::GetClientCertificateKeyPassword() const { return clientCertificateKeyPassword_; }
--- a/OrthancFramework/Sources/HttpClient.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpClient.h Tue Nov 04 15:58:06 2025 +0100 @@ -31,6 +31,7 @@ #include <string> #include <boost/noncopyable.hpp> #include <boost/shared_ptr.hpp> +#include <boost/filesystem.hpp> #include <json/value.h> #if !defined(ORTHANC_ENABLE_CURL) @@ -100,9 +101,9 @@ long timeout_; std::string proxy_; bool verifyPeers_; - std::string caCertificates_; - std::string clientCertificateFile_; - std::string clientCertificateKeyFile_; + boost::filesystem::path caCertificates_; + boost::filesystem::path clientCertificateFile_; + boost::filesystem::path clientCertificateKeyFile_; std::string clientCertificateKeyPassword_; bool pkcs11Enabled_; bool headersToLowerCase_; @@ -196,21 +197,21 @@ bool IsHttpsVerifyPeers() const; - void SetHttpsCACertificates(const std::string& certificates); + void SetHttpsCACertificates(const boost::filesystem::path& certificates); - const std::string& GetHttpsCACertificates() const; + const boost::filesystem::path& GetHttpsCACertificates() const; - void SetClientCertificate(const std::string& certificateFile, - const std::string& certificateKeyFile, + void SetClientCertificate(const boost::filesystem::path& certificateFile, + const boost::filesystem::path& certificateKeyFile, const std::string& certificateKeyPassword); void SetPkcs11Enabled(bool enabled); bool IsPkcs11Enabled() const; - const std::string& GetClientCertificateFile() const; + const boost::filesystem::path& GetClientCertificateFile() const; - const std::string& GetClientCertificateKeyFile() const; + const boost::filesystem::path& GetClientCertificateKeyFile() const; const std::string& GetClientCertificateKeyPassword() const; @@ -231,7 +232,7 @@ bool verbose); static void ConfigureSsl(bool httpsVerifyPeers, - const std::string& httpsCACertificates); + const boost::filesystem::path& httpsCACertificates); static void SetDefaultVerbose(bool verbose);
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -128,7 +128,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) + size_t /*bodySize*/, + const std::string& authenticationPayload /* ignored */) { if (!Toolbox::IsChildUri(pimpl_->baseUri_, uri)) {
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h Tue Nov 04 15:58:06 2025 +0100 @@ -50,7 +50,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload /* ignored */) ORTHANC_OVERRIDE { return false; } @@ -64,7 +65,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) ORTHANC_OVERRIDE; + size_t /*bodySize*/, + const std::string& authenticationPayload /* ignored */) ORTHANC_OVERRIDE; bool IsListDirectoryContent() const {
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -25,6 +25,7 @@ #include "FilesystemHttpSender.h" #include "../OrthancException.h" +#include "../SystemToolbox.h" static const size_t CHUNK_SIZE = 64 * 1024; // Use 64KB chunks @@ -33,7 +34,7 @@ void FilesystemHttpSender::Initialize(const boost::filesystem::path& path) { SetContentFilename(path.filename().string()); - file_.open(path.string().c_str(), std::ifstream::binary); + file_.open(SystemToolbox::PathToUtf8(path).c_str(), std::ifstream::binary); if (!file_.is_open()) { @@ -47,7 +48,7 @@ FilesystemHttpSender::FilesystemHttpSender(const std::string& path) { - Initialize(path); + Initialize(SystemToolbox::PathFromUtf8(path)); } FilesystemHttpSender::FilesystemHttpSender(const boost::filesystem::path& path) @@ -62,6 +63,13 @@ Initialize(path); } + FilesystemHttpSender::FilesystemHttpSender(const boost::filesystem::path& path, + MimeType contentType) + { + SetContentType(contentType); + Initialize(path); + } + FilesystemHttpSender::FilesystemHttpSender(const FilesystemStorage& storage, const std::string& uuid) {
--- a/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h Tue Nov 04 15:58:06 2025 +0100 @@ -49,6 +49,9 @@ FilesystemHttpSender(const std::string& path, MimeType contentType); + FilesystemHttpSender(const boost::filesystem::path& path, + MimeType contentType); + FilesystemHttpSender(const FilesystemStorage& storage, const std::string& uuid);
--- a/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h Tue Nov 04 15:58:06 2025 +0100 @@ -75,7 +75,7 @@ }; - struct Reference; + class Reference; typedef std::vector<std::string> Tokens; typedef std::list<Handler> Handlers;
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -805,6 +805,8 @@ void HttpOutput::StateMachine::SendStreamItem(const void* data, size_t size) { + LOG(TRACE) << "SendStreamItem " << size << " bytes"; + if (state_ != State_WritingStream) { throw OrthancException(ErrorCode_BadSequenceOfCalls); @@ -821,6 +823,8 @@ void HttpOutput::StateMachine::CloseStream() { + LOG(TRACE) << "CloseStream"; + if (state_ != State_WritingStream) { throw OrthancException(ErrorCode_BadSequenceOfCalls);
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -32,6 +32,7 @@ #include "../Logging.h" #include "../OrthancException.h" #include "../TemporaryFile.h" +#include "../SystemToolbox.h" #include "HttpToolbox.h" #include "IHttpHandler.h" #include "MultipartStreamReader.h" @@ -119,12 +120,19 @@ { size_t packetSize = std::min(remainingSize, static_cast<size_t>(INT_MAX)); - int status = mg_write(connection_, &(reinterpret_cast<const char*>(buffer)[offset]), packetSize); + // note: mg_write may sometimes only send a part of the buffer e.g. when the OS is not able to send the full buffer -> we might need to iterate a few times + size_t totalSent = 0; + while (totalSent < packetSize) + { + int status = mg_write(connection_, &(reinterpret_cast<const char*>(buffer)[offset + totalSent]), packetSize - totalSent); - if (status != static_cast<int>(packetSize)) - { - // status == 0 when the connection has been closed, -1 on error - throw OrthancException(ErrorCode_NetworkProtocol); + if (status <= 0) + { + // status == 0 when the connection has been closed, -1 on error + throw OrthancException(ErrorCode_NetworkProtocol); + } + + totalSent += status; } offset += packetSize; @@ -348,6 +356,7 @@ bool isJQueryUploadChunk_; std::string jqueryUploadFileName_; size_t jqueryUploadFileSize_; + const std::string& authenticationPayload_; void HandleInternal(const MultipartStreamReader::HttpHeaders& headers, const void* part, @@ -358,7 +367,7 @@ HttpToolbox::GetArguments getArguments; if (!handler_.Handle(fakeOutput, RequestOrigin_RestApi, remoteIp_.c_str(), username_.c_str(), - HttpMethod_Post, uri_, headers, getArguments, part, size)) + HttpMethod_Post, uri_, headers, getArguments, part, size, authenticationPayload_)) { throw OrthancException(ErrorCode_UnknownResource); } @@ -370,14 +379,16 @@ const std::string& remoteIp, const std::string& username, const UriComponents& uri, - const MultipartStreamReader::HttpHeaders& headers) : + const MultipartStreamReader::HttpHeaders& headers, + const std::string& authenticationPayload) : handler_(handler), chunkStore_(chunkStore), remoteIp_(remoteIp), username_(username), uri_(uri), isJQueryUploadChunk_(false), - jqueryUploadFileSize_(0) // Dummy initialization + jqueryUploadFileSize_(0), // Dummy initialization + authenticationPayload_(authenticationPayload) { typedef HttpToolbox::Arguments::const_iterator Iterator; @@ -470,9 +481,10 @@ const UriComponents& uri, const std::map<std::string, std::string>& headers, const std::string& body, - const std::string& boundary) + const std::string& boundary, + const std::string& authenticationPayload) { - MultipartFormDataHandler handler(GetHandler(), pimpl_->chunkStore_, remoteIp, username, uri, headers); + MultipartFormDataHandler handler(GetHandler(), pimpl_->chunkStore_, remoteIp, username, uri, headers, authenticationPayload); MultipartStreamReader reader(boundary); reader.SetHandler(handler); @@ -621,7 +633,7 @@ enum AccessMode { - AccessMode_Forbidden, + AccessMode_Unauthorized, AccessMode_AuthorizationToken, AccessMode_RegisteredUser }; @@ -657,7 +669,7 @@ } } - return AccessMode_Forbidden; + return AccessMode_Unauthorized; } @@ -1146,13 +1158,52 @@ } #endif /* ORTHANC_ENABLE_PUGIXML == 1 */ - + + std::string HttpServer::GetRelativePathToRoot(const std::string& uri) + { + if (uri.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (uri == "/") + { + return "./"; + } + + std::string path; + + if (uri[uri.size() - 1] == '/') + { + path = "../"; + } + else + { + path = "./"; + } + + UriComponents components; + Toolbox::SplitUriComponents(components, uri); + + for (size_t i = 1; i < components.size(); i++) + { + path += "../"; + } + + return path; + } + + static void InternalCallback(HttpOutput& output /* out */, HttpMethod& method /* out */, HttpServer& server, struct mg_connection *connection, const struct mg_request_info *request) { + server.UpdateCurrentThreadName(); + + std::unique_ptr<MetricsRegistry::AvailableResourcesDecounter> counter(server.CreateAvailableHttpThreadsDecounter()); + bool localhost; #if ORTHANC_ENABLE_MONGOOSE == 1 @@ -1200,18 +1251,10 @@ HttpToolbox::ParseGetArguments(argumentsGET, request->query_string); } - + AccessMode accessMode = IsAccessGranted(server, headers); - // Authenticate this connection - if (server.IsAuthenticationEnabled() && - accessMode == AccessMode_Forbidden) - { - output.SendUnauthorized(server.GetRealm()); // 401 error - return; - } - #if ORTHANC_ENABLE_MONGOOSE == 1 // Apply the filter, if it is installed char remoteIp[24]; @@ -1235,6 +1278,55 @@ requestUri = ""; } + + const IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); + + // Authenticate this connection + std::string authenticationPayload; + std::string redirection; + IIncomingHttpRequestFilter::AuthenticationStatus status; + + if (filter == NULL) + { + status = IIncomingHttpRequestFilter::AuthenticationStatus_BuiltIn; + } + else + { + status = filter->CheckAuthentication(authenticationPayload, redirection, requestUri, remoteIp, headers, argumentsGET); + } + + switch (status) + { + case IIncomingHttpRequestFilter::AuthenticationStatus_BuiltIn: + // This was the only behavior available in Orthanc <= 1.12.8 + if (server.IsAuthenticationEnabled() && + accessMode == AccessMode_Unauthorized) + { + output.SendUnauthorized(server.GetRealm()); // 401 error + return; + } + break; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Granted: + break; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Redirect: + output.Redirect(Toolbox::JoinUri(HttpServer::GetRelativePathToRoot(requestUri), redirection)); + return; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Unauthorized: + output.SendStatus(HttpStatus_401_Unauthorized); + return; + + case IIncomingHttpRequestFilter::AuthenticationStatus_Forbidden: + output.SendStatus(HttpStatus_403_Forbidden); + return; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + // Decompose the URI into its components UriComponents uri; try @@ -1257,10 +1349,12 @@ HttpMethod filterMethod; - + std::unique_ptr<Toolbox::ApiElapsedTimeLogger> apiLogTimer; // to log the time spent in the API call + if (ExtractMethod(method, request, headers, argumentsGET)) { - CLOG(INFO, HTTP) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); + apiLogTimer.reset(new Toolbox::ApiElapsedTimeLogger(std::string(EnumerationToString(method)) + " " + Toolbox::FlattenUri(uri))); + filterMethod = method; } #if ORTHANC_ENABLE_PUGIXML == 1 @@ -1268,8 +1362,7 @@ !strcmp(request->request_method, "PROPFIND") || !strcmp(request->request_method, "HEAD")) { - CLOG(INFO, HTTP) << "Incoming read-only WebDAV request: " - << request->request_method << " " << requestUri; + apiLogTimer.reset(new Toolbox::ApiElapsedTimeLogger(std::string("Incoming read-only WebDAV request: ") + request->request_method + " " + requestUri)); filterMethod = HttpMethod_Get; isWebDav = true; } @@ -1278,8 +1371,7 @@ !strcmp(request->request_method, "UNLOCK") || !strcmp(request->request_method, "MKCOL")) { - CLOG(INFO, HTTP) << "Incoming read-write WebDAV request: " - << request->request_method << " " << requestUri; + apiLogTimer.reset(new Toolbox::ApiElapsedTimeLogger(std::string("Incoming read-write WebDAV request: ") + request->request_method + " " + requestUri)); filterMethod = HttpMethod_Put; isWebDav = true; } @@ -1300,10 +1392,9 @@ // filter. In the case of an authorization bearer token, grant // full access to the API. - assert(accessMode == AccessMode_Forbidden || // Could be the case if "!server.IsAuthenticationEnabled()" + assert(accessMode == AccessMode_Unauthorized || // Could be the case if "!server.IsAuthenticationEnabled()" accessMode == AccessMode_RegisteredUser); - IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); if (filter != NULL && !filter->IsAllowed(filterMethod, requestUri, remoteIp, username.c_str(), headers, argumentsGET)) @@ -1339,7 +1430,7 @@ if (method == HttpMethod_Post || method == HttpMethod_Put) { - PostDataStatus status; + PostDataStatus postStatus; bool isMultipartForm = false; @@ -1356,10 +1447,10 @@ **/ isMultipartForm = true; - status = ReadBodyToString(body, connection, headers); - if (status == PostDataStatus_Success) + postStatus = ReadBodyToString(body, connection, headers); + if (postStatus == PostDataStatus_Success) { - server.ProcessMultipartFormData(remoteIp, username, uri, headers, body, boundary); + server.ProcessMultipartFormData(remoteIp, username, uri, headers, body, boundary, authenticationPayload); output.SendStatus(HttpStatus_200_Ok); return; } @@ -1372,7 +1463,7 @@ if (server.HasHandler()) { found = server.GetHandler().CreateChunkedRequestReader - (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers); + (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers, authenticationPayload); } if (found) @@ -1382,20 +1473,20 @@ throw OrthancException(ErrorCode_InternalError); } - status = ReadBodyToStream(*stream, connection, headers); + postStatus = ReadBodyToStream(*stream, connection, headers); - if (status == PostDataStatus_Success) + if (postStatus == PostDataStatus_Success) { stream->Execute(output); } } else { - status = ReadBodyToString(body, connection, headers); + postStatus = ReadBodyToString(body, connection, headers); } } - switch (status) + switch (postStatus) { case PostDataStatus_NoLength: output.SendStatus(HttpStatus_411_LengthRequired); @@ -1421,7 +1512,7 @@ server.HasHandler()) { found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), - method, uri, headers, argumentsGET, body.c_str(), body.size()); + method, uri, headers, argumentsGET, body.c_str(), body.size(), authenticationPayload); } if (!found) @@ -1485,7 +1576,7 @@ catch (boost::filesystem::filesystem_error& e) { throw OrthancException(ErrorCode_InternalError, - "Error while accessing the filesystem: " + e.path1().string()); + "Error while accessing the filesystem: " + SystemToolbox::PathToUtf8(e.path1())); } catch (std::runtime_error&) { @@ -1534,8 +1625,6 @@ } } - static uint16_t threadCounter = 0; - #if MONGOOSE_USE_CALLBACKS == 0 static void* Callback(enum mg_event event, struct mg_connection *connection, @@ -1558,12 +1647,6 @@ static int Callback(struct mg_connection *connection) { const struct mg_request_info *request = mg_get_request_info(connection); - - if (!Logging::HasCurrentThreadName()) - { - Logging::SetCurrentThreadName(std::string("HTTP-") + boost::lexical_cast<std::string>(threadCounter++)); - } - ProtectedCallback(connection, request); return 1; // Do not let Mongoose handle the request by itself @@ -1601,7 +1684,8 @@ realm_(ORTHANC_REALM), threadsCount_(50), // Default value in mongoose/civetweb tcpNoDelay_(true), - requestTimeout_(30) // Default value in mongoose/civetweb (30 seconds) + requestTimeout_(30), // Default value in mongoose/civetweb (30 seconds) + threadCounter_(0) { #if ORTHANC_ENABLE_MONGOOSE == 1 CLOG(INFO, HTTP) << "This Orthanc server uses Mongoose as its embedded HTTP server"; @@ -1648,10 +1732,15 @@ return port_; } + void HttpServer::SetBindAddresses(const std::set<std::string>& bindAddresses) + { + bindAddresses_ = bindAddresses; + } + void HttpServer::Start() { // reset thread counter used to generate HTTP thread names. - threadCounter = 0; + threadCounter_ = 0; #if ORTHANC_ENABLE_MONGOOSE == 1 CLOG(INFO, HTTP) << "Starting embedded Web server using Mongoose"; @@ -1674,11 +1763,28 @@ port += "s"; } + std::string listeningPorts; + if (bindAddresses_.size() == 0) // default behaviour till 1.12.9 and when no "HttpBindAddresses" configurations are provided + { + listeningPorts = port; + } + else + { + std::set<std::string> addresses; + for (std::set<std::string>::const_iterator it = bindAddresses_.begin(); it != bindAddresses_.end(); ++it) + { + addresses.insert(*it + ":" + port); + } + + Toolbox::JoinStrings(listeningPorts, addresses, ","); + } + + std::list<std::string> dynamicStrings; std::vector<const char*> options; // Set the TCP port for the HTTP server options.push_back("listening_ports"); - options.push_back(port.c_str()); + options.push_back(listeningPorts.c_str()); // Optimization reported by Chris Hafey // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ @@ -1722,8 +1828,9 @@ if (sslVerifyPeers_) { // Set the trusted client certificates (for X509 mutual authentication) + dynamicStrings.push_back(SystemToolbox::PathToUtf8(trustedClientCertificates_)); options.push_back("ssl_ca_file"); - options.push_back(trustedClientCertificates_.c_str()); + options.push_back(dynamicStrings.back().c_str()); } if (ssl_) @@ -1740,13 +1847,23 @@ } // Set the SSL certificate, if any + dynamicStrings.push_back(SystemToolbox::PathToUtf8(certificate_)); options.push_back("ssl_certificate"); - options.push_back(certificate_.c_str()); + options.push_back(dynamicStrings.back().c_str()); }; + if (CIVETWEB_VERSION_MAJOR > 1 || + (CIVETWEB_VERSION_MAJOR == 1 && + CIVETWEB_VERSION_MINOR >= 15)) + { + // URI-decoding of GET arguments was the default in civetweb <= 1.14 + options.push_back("decode_query_string"); + options.push_back("yes"); + } + assert(options.size() % 2 == 0); options.push_back(NULL); - + #if MONGOOSE_USE_CALLBACKS == 0 pimpl_->context_ = mg_start(&Callback, this, &options[0]); @@ -1788,8 +1905,23 @@ } else { + std::string errorMsgDetails; + if (bindAddresses_.empty()) + { + errorMsgDetails = "port " + port; + } + else + { + errorMsgDetails = listeningPorts; + } + + if (errno != 0) // there might be additionnal details about the error in errno + { + errorMsgDetails += ", errno = " + boost::lexical_cast<std::string>(errno) + ", " + strerror(errno); + } + throw OrthancException(ErrorCode_HttpPortInUse, - " (port = " + boost::lexical_cast<std::string>(port_) + ")"); + " (" + errorMsgDetails + ")"); } } @@ -1801,7 +1933,7 @@ } #endif - CLOG(WARNING, HTTP) << "HTTP server listening on port: " << GetPortNumber() + CLOG(WARNING, HTTP) << "HTTP server listening on" << (bindAddresses_.empty() ? " port: " + port : ": " + listeningPorts) << " (HTTPS encryption is " << (IsSslEnabled() ? "enabled" : "disabled") << ", remote access is " @@ -1839,9 +1971,22 @@ void HttpServer::RegisterUser(const char* username, const char* password) { + const std::string s(username); + if (s.find(':') != std::string::npos) + { + /** + * "A user-id containing a colon character is invalid, as the + * first colon in a user-pass string separates user-id and + * password from one another" (cf. issue 252) + * https://datatracker.ietf.org/doc/html/rfc7617 + **/ + throw OrthancException(ErrorCode_ParameterOutOfRange, "Usernames for HTTP Basic Authentication " + "cannot contain \":\", but found: \"" + s + "\""); + } + Stop(); - std::string tag = std::string(username) + ":" + std::string(password); + std::string tag = s + ":" + std::string(password); std::string encoded; Toolbox::EncodeBase64(encoded, tag); registeredUsers_.insert(encoded); @@ -1984,7 +2129,7 @@ #endif } - const std::string &HttpServer::GetSslCertificate() const + const boost::filesystem::path& HttpServer::GetSslCertificate() const { return certificate_; } @@ -2001,7 +2146,7 @@ return ssl_; } - void HttpServer::SetSslCertificate(const char* path) + void HttpServer::SetSslCertificate(const boost::filesystem::path& path) { Stop(); certificate_ = path; @@ -2012,7 +2157,7 @@ return remoteAllowed_; } - void HttpServer::SetSslTrustedClientCertificates(const char* path) + void HttpServer::SetSslTrustedClientCertificates(const boost::filesystem::path &path) { Stop(); trustedClientCertificates_ = path; @@ -2119,6 +2264,11 @@ Stop(); threadsCount_ = threads; + if (availableHttpThreadsMetrics_.get() != NULL) + { + availableHttpThreadsMetrics_->SetInitialValue(threadsCount_); + } + CLOG(INFO, HTTP) << "The embedded HTTP server will use " << threads << " threads"; } @@ -2204,4 +2354,39 @@ } } #endif + + + void HttpServer::SetMetricsRegistry(MetricsRegistry& metricsRegistry) + { + Stop(); + availableHttpThreadsMetrics_.reset(new MetricsRegistry::SharedMetrics( + metricsRegistry, + "orthanc_available_http_threads_count", + MetricsUpdatePolicy_MinOver10Seconds)); + availableHttpThreadsMetrics_->SetInitialValue(threadsCount_); + } + + + MetricsRegistry::AvailableResourcesDecounter* HttpServer::CreateAvailableHttpThreadsDecounter() + { + // NB: "availableHttpThreadsMetrics_" is protected by the mutex in "mg_stop()" + if (availableHttpThreadsMetrics_.get() != NULL) + { + return new MetricsRegistry::AvailableResourcesDecounter(*availableHttpThreadsMetrics_); + } + else + { + return NULL; + } + } + + + void HttpServer::UpdateCurrentThreadName() + { + if (!Logging::HasCurrentThreadName()) + { + boost::mutex::scoped_lock lock(threadCounterMutex_); + Logging::SetCurrentThreadName(std::string("HTTP-") + boost::lexical_cast<std::string>(threadCounter_++)); + } + } }
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.h Tue Nov 04 15:58:06 2025 +0100 @@ -50,12 +50,15 @@ #include "IIncomingHttpRequestFilter.h" +#include "../MetricsRegistry.h" #include <list> #include <map> #include <set> #include <stdint.h> #include <boost/shared_ptr.hpp> +#include <boost/filesystem.hpp> + namespace Orthanc { @@ -98,13 +101,14 @@ bool remoteAllowed_; bool authentication_; bool sslVerifyPeers_; - std::string trustedClientCertificates_; + boost::filesystem::path trustedClientCertificates_; bool ssl_; - std::string certificate_; + boost::filesystem::path certificate_; unsigned int sslMinimumVersion_; bool sslHasCiphers_; std::string sslCiphers_; uint16_t port_; + std::set<std::string> bindAddresses_; IIncomingHttpRequestFilter* filter_; bool keepAlive_; unsigned int keepAliveTimeout_; @@ -114,6 +118,10 @@ unsigned int threadsCount_; bool tcpNoDelay_; unsigned int requestTimeout_; // In seconds + std::unique_ptr<MetricsRegistry::SharedMetrics> availableHttpThreadsMetrics_; // New in Orthanc 1.12.9 + + boost::mutex threadCounterMutex_; // New in Orthanc 1.12.9 + uint16_t threadCounter_; // Introduced as a global, static variable in Orthanc 1.12.2 #if ORTHANC_ENABLE_PUGIXML == 1 WebDavBuckets webDavBuckets_; @@ -130,6 +138,8 @@ uint16_t GetPortNumber() const; + void SetBindAddresses(const std::set<std::string>& bindAddresses); + void Start(); void Stop(); @@ -155,7 +165,7 @@ void SetSslCiphers(const std::list<std::string>& ciphers); - void SetSslTrustedClientCertificates(const char* path); + void SetSslTrustedClientCertificates(const boost::filesystem::path& path); bool IsKeepAliveEnabled() const; @@ -165,9 +175,9 @@ void SetKeepAliveTimeout(unsigned int timeout); - const std::string& GetSslCertificate() const; + const boost::filesystem::path& GetSslCertificate() const; - void SetSslCertificate(const char* path); + void SetSslCertificate(const boost::filesystem::path& path); bool IsRemoteAccessAllowed() const; @@ -225,6 +235,16 @@ const UriComponents& uri, const std::map<std::string, std::string>& headers, const std::string& body, - const std::string& boundary); + const std::string& boundary, + const std::string& authenticationPayload); + + static std::string GetRelativePathToRoot(const std::string& uri); + + void SetMetricsRegistry(MetricsRegistry& metricsRegistry); + + // Can return NULL if SetMetricsRegistry() was not call beforehand + MetricsRegistry::AvailableResourcesDecounter* CreateAvailableHttpThreadsDecounter(); + + void UpdateCurrentThreadName(); }; }
--- a/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -230,7 +230,8 @@ const std::string& uri, const Arguments& httpHeaders) { - return (IHttpHandler::SimpleDelete(NULL, handler, origin, uri, httpHeaders) == HttpStatus_200_Ok); + std::string ignoredBody; + return (IHttpHandler::SimpleDelete(ignoredBody, NULL, handler, origin, uri, httpHeaders) == HttpStatus_200_Ok); } #endif }
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -49,9 +49,12 @@ HttpOutput http(stream, false /* assume no keep-alive */, 0); if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, - httpHeaders, getArguments, NULL /* no body for GET */, 0)) + httpHeaders, getArguments, NULL /* no body for GET */, 0, "" /* no authentication payload */)) { - stream.GetBody(answerBody); + if (stream.GetStatus() == HttpStatus_200_Ok) + { + stream.GetBody(answerBody); + } if (answerHeaders != NULL) { @@ -86,7 +89,7 @@ HttpOutput http(stream, false /* assume no keep-alive */, 0); if (handler.Handle(http, origin, LOCALHOST, "", method, curi, - httpHeaders, getArguments, bodyData, bodySize)) + httpHeaders, getArguments, bodyData, bodySize, "" /* no authentication payload */)) { stream.GetBody(answerBody); @@ -130,7 +133,8 @@ } - HttpStatus IHttpHandler::SimpleDelete(HttpToolbox::Arguments* answerHeaders, + HttpStatus IHttpHandler::SimpleDelete(std::string& answerBody, + HttpToolbox::Arguments* answerHeaders, IHttpHandler& handler, RequestOrigin origin, const std::string& uri, @@ -145,8 +149,10 @@ HttpOutput http(stream, false /* assume no keep-alive */, 0); if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, - httpHeaders, getArguments, NULL /* no body for DELETE */, 0)) + httpHeaders, getArguments, NULL /* no body for DELETE */, 0, "" /* no authentication payload */)) { + stream.GetBody(answerBody); + if (answerHeaders != NULL) { stream.GetHeaders(*answerHeaders, true /* convert key to lower case */);
--- a/OrthancFramework/Sources/HttpServer/IHttpHandler.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.h Tue Nov 04 15:58:06 2025 +0100 @@ -72,7 +72,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) = 0; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) = 0; virtual bool Handle(HttpOutput& output, RequestOrigin origin, @@ -83,7 +84,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) = 0; + size_t bodySize, + const std::string& authenticationPayload) = 0; /** @@ -116,7 +118,8 @@ size_t bodySize, const HttpToolbox::Arguments& httpHeaders); - static HttpStatus SimpleDelete(HttpToolbox::Arguments* answerHeaders /* out */, + static HttpStatus SimpleDelete(std::string& answerBody /* out */, + HttpToolbox::Arguments* answerHeaders /* out */, IHttpHandler& handler, RequestOrigin origin, const std::string& uri,
--- a/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h Tue Nov 04 15:58:06 2025 +0100 @@ -31,18 +31,36 @@ class IIncomingHttpRequestFilter : public boost::noncopyable { public: + enum AuthenticationStatus + { + AuthenticationStatus_BuiltIn, // Use the default HTTP authentication built in Orthanc + AuthenticationStatus_Granted, // Let the REST callback process the request + AuthenticationStatus_Unauthorized, // 401 HTTP status + AuthenticationStatus_Forbidden, // 403 HTTP status + AuthenticationStatus_Redirect // 307 HTTP status + }; + virtual ~IIncomingHttpRequestFilter() { } // New in Orthanc 1.8.1 - virtual bool IsValidBearerToken(const std::string& token) = 0; + virtual bool IsValidBearerToken(const std::string& token) const = 0; + + // This method corresponds to HTTP authentication + HTTP authorization + virtual AuthenticationStatus CheckAuthentication(std::string& customPayload /* out: payload to provide to "IsAllowed()" */, + std::string& redirection /* out: path relative to the root */, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const = 0; + // This method corresponds to HTTP authorization alone virtual bool IsAllowed(HttpMethod method, const char* uri, const char* ip, const char* username, const HttpToolbox::Arguments& httpHeaders, - const HttpToolbox::GetArguments& getArguments) = 0; + const HttpToolbox::GetArguments& getArguments) const = 0; }; }
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -48,7 +48,20 @@ return s; } } - + +static std::string AddLeadingSlash(const std::string& s) +{ + if (s.empty() || + s[0] != '/') + { + return std::string("/") + s; + } + else + { + return s; + } +} + namespace Orthanc { @@ -163,7 +176,14 @@ const std::string& parentPath) const { std::string href; - Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName()); + std::vector<std::string> pathTokens; + + Toolbox::SplitString(pathTokens, parentPath, '/'); + pathTokens.push_back(GetDisplayName()); + + Toolbox::UriEncode(href, pathTokens); + href = AddLeadingSlash(href); + FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime()); pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop"); @@ -181,7 +201,14 @@ const std::string& parentPath) const { std::string href; - Toolbox::UriEncode(href, AddTrailingSlash(parentPath) + GetDisplayName()); + std::vector<std::string> pathTokens; + + Toolbox::SplitString(pathTokens, parentPath, '/'); + pathTokens.push_back(GetDisplayName()); + + Toolbox::UriEncode(href, pathTokens); + href = AddLeadingSlash(href); + FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime()); pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop"); @@ -245,7 +272,8 @@ } std::string href; - Toolbox::UriEncode(href, Toolbox::FlattenUri(tokens) + "/"); + Toolbox::UriEncode(href, tokens); + href = AddTrailingSlash(AddLeadingSlash(href)); boost::posix_time::ptime now = GetNow(); FormatInternal(self, href, folder, now, now);
--- a/OrthancFramework/Sources/Images/Font.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/Font.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -142,7 +142,7 @@ #if ORTHANC_SANDBOXED == 0 - void Font::LoadFromFile(const std::string& path) + void Font::LoadFromFile(const boost::filesystem::path& path) { std::string font; SystemToolbox::ReadFile(font, path);
--- a/OrthancFramework/Sources/Images/Font.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/Font.h Tue Nov 04 15:58:06 2025 +0100 @@ -33,6 +33,11 @@ #include <map> #include <boost/noncopyable.hpp> +#if ORTHANC_SANDBOXED == 0 +# include <boost/filesystem.hpp> +#endif + + namespace Orthanc { class ORTHANC_PUBLIC Font : public boost::noncopyable @@ -74,7 +79,7 @@ void LoadFromMemory(const std::string& font); #if ORTHANC_SANDBOXED == 0 - void LoadFromFile(const std::string& path); + void LoadFromFile(const boost::filesystem::path& path); #endif const std::string& GetName() const;
--- a/OrthancFramework/Sources/Images/FontRegistry.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/FontRegistry.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -49,7 +49,7 @@ #if ORTHANC_SANDBOXED == 0 - void FontRegistry::AddFromFile(const std::string& path) + void FontRegistry::AddFromFile(const boost::filesystem::path& path) { std::unique_ptr<Font> f(new Font); f->LoadFromFile(path);
--- a/OrthancFramework/Sources/Images/FontRegistry.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/FontRegistry.h Tue Nov 04 15:58:06 2025 +0100 @@ -41,7 +41,7 @@ void AddFromMemory(const std::string& font); #if ORTHANC_SANDBOXED == 0 - void AddFromFile(const std::string& path); + void AddFromFile(const boost::filesystem::path& path); #endif size_t GetSize() const;
--- a/OrthancFramework/Sources/Images/JpegReader.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/JpegReader.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -94,7 +94,7 @@ #if ORTHANC_SANDBOXED == 0 - void JpegReader::ReadFromFile(const std::string& filename) + void JpegReader::ReadFromFile(const boost::filesystem::path& filename) { FILE* fp = SystemToolbox::OpenFile(filename, FileMode_ReadBinary); if (!fp)
--- a/OrthancFramework/Sources/Images/JpegReader.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/JpegReader.h Tue Nov 04 15:58:06 2025 +0100 @@ -40,6 +40,11 @@ #include <string> +#if ORTHANC_SANDBOXED != 1 +#include <boost/filesystem.hpp> +#endif + + namespace Orthanc { class ORTHANC_PUBLIC JpegReader : public ImageAccessor @@ -49,7 +54,7 @@ public: #if ORTHANC_SANDBOXED == 0 - void ReadFromFile(const std::string& filename); + void ReadFromFile(const boost::filesystem::path& filename); #endif void ReadFromMemory(const void* buffer,
--- a/OrthancFramework/Sources/Images/PamReader.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/PamReader.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -272,7 +272,7 @@ #if ORTHANC_SANDBOXED == 0 - void PamReader::ReadFromFile(const std::string& filename) + void PamReader::ReadFromFile(const boost::filesystem::path& filename) { SystemToolbox::ReadFile(content_, filename); ParseContent();
--- a/OrthancFramework/Sources/Images/PamReader.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/PamReader.h Tue Nov 04 15:58:06 2025 +0100 @@ -26,6 +26,10 @@ #include "ImageAccessor.h" +#if ORTHANC_SANDBOXED != 1 +#include <boost/filesystem.hpp> +#endif + #if !defined(ORTHANC_SANDBOXED) # error The macro ORTHANC_SANDBOXED must be defined #endif @@ -72,7 +76,7 @@ virtual ~PamReader(); #if ORTHANC_SANDBOXED == 0 - void ReadFromFile(const std::string& filename); + void ReadFromFile(const boost::filesystem::path& filename); #endif void ReadFromMemory(const std::string& buffer);
--- a/OrthancFramework/Sources/Images/PngReader.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/PngReader.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -44,7 +44,7 @@ { FILE* fp_; - explicit FileRabi(const char* filename) + explicit FileRabi(const boost::filesystem::path& filename) { fp_ = SystemToolbox::OpenFile(filename, FileMode_ReadBinary); if (!fp_) @@ -215,9 +215,9 @@ #if ORTHANC_SANDBOXED == 0 - void PngReader::ReadFromFile(const std::string& filename) + void PngReader::ReadFromFile(const boost::filesystem::path& filename) { - FileRabi f(filename.c_str()); + FileRabi f(filename); char header[8]; if (fread(header, 1, 8, f.fp_) != 8)
--- a/OrthancFramework/Sources/Images/PngReader.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Images/PngReader.h Tue Nov 04 15:58:06 2025 +0100 @@ -44,6 +44,11 @@ # error The macro ORTHANC_SANDBOXED must be defined #endif +#if ORTHANC_SANDBOXED != 1 +#include <boost/filesystem.hpp> +#endif + + namespace Orthanc { class ORTHANC_PUBLIC PngReader : public ImageAccessor @@ -61,7 +66,7 @@ PngReader(); #if ORTHANC_SANDBOXED == 0 - void ReadFromFile(const std::string& filename); + void ReadFromFile(const boost::filesystem::path& filename); #endif void ReadFromMemory(const void* buffer,
--- a/OrthancFramework/Sources/JobsEngine/IJob.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/IJob.h Tue Nov 04 15:58:06 2025 +0100 @@ -76,5 +76,10 @@ // This function can only be called if the job has reached its // "success" state virtual void DeleteAllOutputs() {} + + // UserData are provided by the user when creating the job and they are carried along the job + virtual bool GetUserData(Json::Value& userData) const = 0; + + virtual void SetUserData(const Json::Value& userData) = 0; }; }
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -190,6 +190,16 @@ target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_); target["EffectiveRuntime"] = static_cast<double>(runtime_.total_milliseconds()) / 1000.0; target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f); + + if (status_.HasUserData()) + { + target["UserData"] = status_.GetUserData(); + } + + if (status_.HasDimseErrorStatus()) + { + target["DimseErrorStatus"] = status_.GetDimseErrorStatus(); + } target["Type"] = status_.GetJobType(); target["Content"] = status_.GetPublicContent();
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -34,18 +34,13 @@ progress_(0), jobType_("Invalid"), publicContent_(Json::objectValue), - hasSerialized_(false) + hasSerialized_(false), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) { } - - JobStatus::JobStatus(ErrorCode code, - const std::string& details, - const IJob& job) : - errorCode_(code), - progress_(job.GetProgress()), - publicContent_(Json::objectValue), - details_(details) + void JobStatus::InitInternal(const IJob& job) { if (progress_ < 0) { @@ -59,8 +54,39 @@ job.GetJobType(jobType_); job.GetPublicContent(publicContent_); + job.GetUserData(userData_); + + hasSerialized_ = job.Serialize(serialized_); + } - hasSerialized_ = job.Serialize(serialized_); + JobStatus::JobStatus(ErrorCode code, + const std::string& details, + const IJob& job, + uint16_t dimseErrorStatus) : + errorCode_(code), + progress_(job.GetProgress()), + publicContent_(Json::objectValue), + hasSerialized_(false), + details_(details), + hasDimseErrorStatus_(true), + dimseErrorStatus_(dimseErrorStatus) + { + InitInternal(job); + } + + + JobStatus::JobStatus(ErrorCode code, + const std::string& details, + const IJob& job) : + errorCode_(code), + progress_(job.GetProgress()), + publicContent_(Json::objectValue), + hasSerialized_(false), + details_(details), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) + { + InitInternal(job); }
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h Tue Nov 04 15:58:06 2025 +0100 @@ -25,6 +25,7 @@ #pragma once #include "IJob.h" +#include "../OrthancException.h" namespace Orthanc { @@ -38,12 +39,22 @@ Json::Value serialized_; bool hasSerialized_; std::string details_; + bool hasDimseErrorStatus_; + uint16_t dimseErrorStatus_; + Json::Value userData_; + void InitInternal(const IJob& job); + public: JobStatus(); JobStatus(ErrorCode code, const std::string& details, + const IJob& job, + uint16_t dimseErrorStatus); + + JobStatus(ErrorCode code, + const std::string& details, const IJob& job); ErrorCode GetErrorCode() const @@ -87,5 +98,30 @@ { return details_; } + + bool HasUserData() const + { + return !userData_.isNull(); + } + + const Json::Value& GetUserData() const + { + return userData_; + } + + bool HasDimseErrorStatus() const + { + return hasDimseErrorStatus_; + } + + uint16_t GetDimseErrorStatus() const + { + if (!hasDimseErrorStatus_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return dimseErrorStatus_; + } }; }
--- a/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -32,7 +32,9 @@ JobStepResult::JobStepResult() : code_(JobStepCode_Failure), timeout_(0), - error_(ErrorCode_InternalError) + error_(ErrorCode_InternalError), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) { } @@ -68,11 +70,32 @@ return result; } + JobStepResult JobStepResult::Failure(const ErrorCode& error, + const char* details, + uint16_t dimseErrorStatus) + { + JobStepResult result = Failure(error, details); + + result.hasDimseErrorStatus_ = true; + result.dimseErrorStatus_ = dimseErrorStatus; + + return result; + } + JobStepResult JobStepResult::Failure(const OrthancException& exception) { - return Failure(exception.GetErrorCode(), - exception.HasDetails() ? exception.GetDetails() : NULL); + if (exception.HasDimseErrorStatus()) + { + return Failure(exception.GetErrorCode(), + exception.HasDetails() ? exception.GetDetails() : NULL, + exception.GetDimseErrorStatus()); + } + else + { + return Failure(exception.GetErrorCode(), + exception.HasDetails() ? exception.GetDetails() : NULL); + } } JobStepCode JobStepResult::GetCode() const @@ -118,4 +141,20 @@ throw OrthancException(ErrorCode_BadSequenceOfCalls); } } + + bool JobStepResult::HasDimseErrorStatus() const + { + return hasDimseErrorStatus_; + } + + uint16_t JobStepResult::GetDimseErrorStatus() const + { + if (!hasDimseErrorStatus_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return dimseErrorStatus_; + } + }
--- a/OrthancFramework/Sources/JobsEngine/JobStepResult.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.h Tue Nov 04 15:58:06 2025 +0100 @@ -26,6 +26,9 @@ #include "../Enumerations.h" +#include <json/value.h> +#include <stdint.h> + namespace Orthanc { class OrthancException; @@ -37,11 +40,16 @@ unsigned int timeout_; ErrorCode error_; std::string failureDetails_; + bool hasDimseErrorStatus_; + uint16_t dimseErrorStatus_; + explicit JobStepResult(JobStepCode code) : code_(code), timeout_(0), - error_(ErrorCode_Success) + error_(ErrorCode_Success), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) { } @@ -57,6 +65,10 @@ static JobStepResult Failure(const ErrorCode& error, const char* details); + static JobStepResult Failure(const ErrorCode& error, + const char* details, + uint16_t dimseErrorStatus); + static JobStepResult Failure(const OrthancException& exception); JobStepCode GetCode() const; @@ -66,5 +78,9 @@ ErrorCode GetFailureCode() const; const std::string& GetFailureDetails() const; + + bool HasDimseErrorStatus() const; + + uint16_t GetDimseErrorStatus() const; }; }
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -87,7 +87,14 @@ case JobStepCode_Failure: running.GetJob().Stop(JobStopReason_Failure); - running.UpdateStatus(result.GetFailureCode(), result.GetFailureDetails()); + if (result.HasDimseErrorStatus()) + { + running.UpdateStatus(result.GetFailureCode(), result.GetFailureDetails(), result.GetDimseErrorStatus()); + } + else + { + running.UpdateStatus(result.GetFailureCode(), result.GetFailureDetails()); + } running.MarkFailure(); return false;
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -43,6 +43,8 @@ static const char* RUNTIME = "Runtime"; static const char* ERROR_CODE = "ErrorCode"; static const char* ERROR_DETAILS = "ErrorDetails"; + static const char* USER_DATA = "UserData"; + static const char* DIMSE_ERROR_STATUS = "DimseErrorStatus"; class JobsRegistry::JobHandler : public boost::noncopyable @@ -296,6 +298,18 @@ target[ERROR_CODE] = static_cast<int>(lastStatus_.GetErrorCode()); target[ERROR_DETAILS] = lastStatus_.GetDetails(); + // New in Orthanc 1.12.9 + Json::Value userData; + if (job_->GetUserData(userData)) + { + target[USER_DATA] = userData; + } + + if (lastStatus_.HasDimseErrorStatus()) + { + target[DIMSE_ERROR_STATUS] = lastStatus_.GetDimseErrorStatus(); + } + return true; } else @@ -325,7 +339,6 @@ job_.reset(unserializer.UnserializeJob(serialized[JOB])); job_->GetJobType(jobType_); - job_->Start(); ErrorCode errorCode; if (serialized.isMember(ERROR_CODE)) @@ -343,7 +356,21 @@ details = SerializationToolbox::ReadString(serialized, ERROR_DETAILS); } - lastStatus_ = JobStatus(errorCode, details, *job_); + if (serialized.isMember(USER_DATA)) + { + job_->SetUserData(serialized[USER_DATA]); + } + + if (serialized.isMember(DIMSE_ERROR_STATUS)) + { + lastStatus_ = JobStatus(errorCode, details, *job_, static_cast<uint16_t>(serialized[DIMSE_ERROR_STATUS].asUInt())); + } + else + { + lastStatus_ = JobStatus(errorCode, details, *job_); + } + + job_->Start(); } }; @@ -868,14 +895,18 @@ { ErrorCode code = it->second->GetLastStatus().GetErrorCode(); const std::string& details = it->second->GetLastStatus().GetDetails(); - - if (details.empty()) + + if (it->second->GetLastStatus().HasDimseErrorStatus()) { - throw OrthancException(code); + throw OrthancException(code, details, it->second->GetLastStatus().GetDimseErrorStatus()); + } + else if (!details.empty()) + { + throw OrthancException(code, details); } else { - throw OrthancException(code, details); + throw OrthancException(code); } } else @@ -1438,6 +1469,27 @@ void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code, + const std::string& details, + uint16_t dimseErrorStatus) + { + if (!IsValid()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + JobStatus status(code, details, *job_, dimseErrorStatus); + + boost::mutex::scoped_lock lock(registry_.mutex_); + registry_.CheckInvariants(); + assert(handler_->GetState() == JobState_Running); + + handler_->SetLastStatus(status); + } + } + + + void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code, const std::string& details) { if (!IsValid())
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h Tue Nov 04 15:58:06 2025 +0100 @@ -243,6 +243,10 @@ void UpdateStatus(ErrorCode code, const std::string& details); + + void UpdateStatus(ErrorCode code, + const std::string& details, + uint16_t dimseErrorStatus); }; }; }
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Tue Nov 04 15:58:06 2025 +0100 @@ -28,6 +28,7 @@ #include "IJobOperation.h" #include "../../Compatibility.h" // For ORTHANC_OVERRIDE +#include "../../OrthancException.h" #include <boost/thread/mutex.hpp> #include <boost/thread/condition_variable.hpp> @@ -139,5 +140,15 @@ } void AwakeTrailingSleep(); + + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } }; }
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -235,7 +235,7 @@ static const char* KEY_POSITION = "Position"; static const char* KEY_TYPE = "Type"; static const char* KEY_COMMANDS = "Commands"; - + static const char* KEY_USER_DATA = "UserData"; void SetOfCommandsJob::GetPublicContent(Json::Value& value) const { @@ -254,6 +254,7 @@ target[KEY_PERMISSIVE] = permissive_; target[KEY_POSITION] = static_cast<unsigned int>(position_); target[KEY_DESCRIPTION] = description_; + target[KEY_USER_DATA] = userData_; target[KEY_COMMANDS] = Json::arrayValue; Json::Value& tmp = target[KEY_COMMANDS]; @@ -280,7 +281,13 @@ permissive_ = SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE); position_ = SerializationToolbox::ReadUnsignedInteger(source, KEY_POSITION); description_ = SerializationToolbox::ReadString(source, KEY_DESCRIPTION); - + + // new in 1.12.9 + if (source.isMember(KEY_USER_DATA)) + { + userData_ = source[KEY_USER_DATA]; + } + if (!source.isMember(KEY_COMMANDS) || source[KEY_COMMANDS].type() != Json::arrayValue) {
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Tue Nov 04 15:58:06 2025 +0100 @@ -63,6 +63,7 @@ bool permissive_; size_t position_; std::string description_; + Json::Value userData_; public: SetOfCommandsJob(); @@ -116,5 +117,21 @@ { return false; } + + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE + { + userData_ = userData; + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + if (!userData_.isNull()) + { + userData = userData_; + return true; + } + return false; + } + }; }
--- a/OrthancFramework/Sources/Logging.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Logging.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -30,6 +30,10 @@ #include <cassert> #include <stdint.h> +#if defined(__linux__) && !defined(NDEBUG) +# include <pthread.h> +#endif + /********************************************************* * Common section @@ -596,7 +600,7 @@ **/ boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); - boost::filesystem::path root(directory); + boost::filesystem::path root(SystemToolbox::PathFromUtf8(directory)); boost::filesystem::path exe(SystemToolbox::GetPathToExecutable()); if (!boost::filesystem::exists(root) || @@ -658,6 +662,11 @@ } threadNames_[id] = name; + +#if defined(__linux__) && !defined(NDEBUG) && !defined(__LSB_VERSION__) + // set the thread name at "system" level too -> required to have the thread names visible in GDB ! + pthread_setname_np(pthread_self(), name.substr(0, 15).c_str()); // thread names are limited to 15 in Linux +#endif } void SetCurrentThreadName(const std::string& name)
--- a/OrthancFramework/Sources/MallocMemoryBuffer.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/MallocMemoryBuffer.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -76,7 +76,7 @@ } buffer_ = buffer; - size_ = size; + size_ = static_cast<size_t>(size); free_ = freeFunction; if (size_ != 0 &&
--- a/OrthancFramework/Sources/MetricsRegistry.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/MetricsRegistry.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -38,7 +38,7 @@ return boost::posix_time::microsec_clock::universal_time(); } - namespace + namespace MetricsRegistryInternals { template <typename T> class TimestampedValue : public boost::noncopyable @@ -47,6 +47,8 @@ boost::posix_time::ptime time_; bool hasValue_; T value_; + bool hasNextValue_; // for min and max values over period, we need to store the next value + T nextValue_; void SetValue(const T& value, const boost::posix_time::ptime& now) @@ -89,8 +91,25 @@ public: explicit TimestampedValue() : hasValue_(false), - value_(0) + value_(0), + hasNextValue_(false), + nextValue_(0) + { + } + + int GetPeriodDuration(const MetricsUpdatePolicy& policy) { + switch (policy) + { + case MetricsUpdatePolicy_MaxOver10Seconds: + case MetricsUpdatePolicy_MinOver10Seconds: + return 10; + case MetricsUpdatePolicy_MaxOver1Minute: + case MetricsUpdatePolicy_MinOver1Minute: + return 60; + default: + throw OrthancException(ErrorCode_InternalError); + } } void Update(const T& value, @@ -105,30 +124,54 @@ break; case MetricsUpdatePolicy_MaxOver10Seconds: - if (IsLargerOverPeriod(value, 10, now)) + case MetricsUpdatePolicy_MaxOver1Minute: + if (IsLargerOverPeriod(value, GetPeriodDuration(policy), now)) { SetValue(value, now); } - break; - - case MetricsUpdatePolicy_MaxOver1Minute: - if (IsLargerOverPeriod(value, 60, now)) + else { - SetValue(value, now); + hasNextValue_ = true; + nextValue_ = value; } break; case MetricsUpdatePolicy_MinOver10Seconds: - if (IsSmallerOverPeriod(value, 10, now)) + case MetricsUpdatePolicy_MinOver1Minute: + if (IsSmallerOverPeriod(value, GetPeriodDuration(policy), now)) { SetValue(value, now); } + else + { + hasNextValue_ = true; + nextValue_ = value; + } break; + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + void Refresh(const MetricsUpdatePolicy& policy) + { + const boost::posix_time::ptime now = GetNow(); + + switch (policy) + { + case MetricsUpdatePolicy_Directly: + // nothing to do + break; + + case MetricsUpdatePolicy_MaxOver10Seconds: + case MetricsUpdatePolicy_MinOver10Seconds: + case MetricsUpdatePolicy_MaxOver1Minute: case MetricsUpdatePolicy_MinOver1Minute: - if (IsSmallerOverPeriod(value, 60, now)) + // if the min/max value is older than the period, get the latest value + if ((now - time_).total_seconds() > GetPeriodDuration(policy) /* old value has expired */ && hasNextValue_) { - SetValue(value, now); + SetValue(nextValue_, now); + hasNextValue_ = false; } break; @@ -217,13 +260,17 @@ virtual const boost::posix_time::ptime& GetTime() const = 0; virtual std::string FormatValue() const = 0; + + virtual void Refresh() = 0; + + virtual void SetInitialValue(int64_t value) = 0; }; class MetricsRegistry::FloatItem : public Item { private: - TimestampedValue<float> value_; + MetricsRegistryInternals::TimestampedValue<float> value_; public: explicit FloatItem(MetricsUpdatePolicy policy) : @@ -265,13 +312,23 @@ { return boost::lexical_cast<std::string>(value_.GetValue()); } + + virtual void Refresh() ORTHANC_OVERRIDE + { + value_.Refresh(GetPolicy()); + } + + virtual void SetInitialValue(int64_t value) ORTHANC_OVERRIDE + { + value_.Update(value, MetricsUpdatePolicy_Directly); + } }; class MetricsRegistry::IntegerItem : public Item { private: - TimestampedValue<int64_t> value_; + MetricsRegistryInternals::TimestampedValue<int64_t> value_; public: explicit IntegerItem(MetricsUpdatePolicy policy) : @@ -313,6 +370,16 @@ { return boost::lexical_cast<std::string>(value_.GetValue()); } + + virtual void Refresh() ORTHANC_OVERRIDE + { + value_.Refresh(GetPolicy()); + } + + virtual void SetInitialValue(int64_t value) ORTHANC_OVERRIDE + { + value_.Update(value, MetricsUpdatePolicy_Directly); + } }; @@ -432,6 +499,16 @@ } } + void MetricsRegistry::SetInitialValue(const std::string& name, + int64_t value) + { + if (enabled_) + { + boost::mutex::scoped_lock lock(mutex_); + GetItemInternal(name, MetricsUpdatePolicy_Directly, MetricsDataType_Integer).SetInitialValue(value); + } + } + MetricsUpdatePolicy MetricsRegistry::GetUpdatePolicy(const std::string& metrics) { @@ -494,6 +571,8 @@ { boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH; + it->second->Refresh(); + std::string line = (it->first + " " + it->second->FormatValue() + " " + boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n"); @@ -513,6 +592,7 @@ name_(name), value_(0) { + registry_.Register(name, policy, MetricsDataType_Integer); } void MetricsRegistry::SharedMetrics::Add(int64_t delta) @@ -522,6 +602,12 @@ registry_.SetIntegerValue(name_, value_); } + void MetricsRegistry::SharedMetrics::SetInitialValue(int64_t value) + { + boost::mutex::scoped_lock lock(mutex_); + value_ = value; + registry_.SetInitialValue(name_, value_); + } MetricsRegistry::ActiveCounter::ActiveCounter(MetricsRegistry::SharedMetrics &metrics) : metrics_(metrics) @@ -535,6 +621,18 @@ } + MetricsRegistry::AvailableResourcesDecounter::AvailableResourcesDecounter(MetricsRegistry::SharedMetrics &metrics) : + metrics_(metrics) + { + metrics_.Add(-1); + } + + MetricsRegistry::AvailableResourcesDecounter::~AvailableResourcesDecounter() + { + metrics_.Add(1); + } + + void MetricsRegistry::Timer::Start() { if (registry_.IsEnabled())
--- a/OrthancFramework/Sources/MetricsRegistry.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/MetricsRegistry.h Tue Nov 04 15:58:06 2025 +0100 @@ -106,6 +106,9 @@ SetIntegerValue(name, value, MetricsUpdatePolicy_Directly); } + void SetInitialValue(const std::string& name, + int64_t value); + void IncrementIntegerValue(const std::string& name, int64_t delta); @@ -131,6 +134,8 @@ MetricsUpdatePolicy policy); void Add(int64_t delta); + + void SetInitialValue(int64_t value); }; @@ -146,6 +151,17 @@ }; + class ORTHANC_PUBLIC AvailableResourcesDecounter : public boost::noncopyable + { + private: + SharedMetrics& metrics_; + + public: + explicit AvailableResourcesDecounter(SharedMetrics& metrics); + + ~AvailableResourcesDecounter(); + }; + class ORTHANC_PUBLIC Timer : public boost::noncopyable { private:
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -30,29 +30,38 @@ #include "../OrthancException.h" #include "../Logging.h" + namespace Orthanc { struct RunnableWorkersPool::PImpl { - class Worker + class Worker : public boost::noncopyable { private: const bool& continue_; SharedMessageQueue& queue_; boost::thread thread_; - std::string name_; + std::string threadName_; + MetricsRegistry::SharedMetrics* availableWorkers_; static void WorkerThread(Worker* that) { - Logging::SetCurrentThreadName(that->name_); + Logging::SetCurrentThreadName(that->threadName_); while (that->continue_) { try { std::unique_ptr<IDynamicObject> obj(that->queue_.Dequeue(100)); + if (obj.get() != NULL) { + std::unique_ptr<MetricsRegistry::AvailableResourcesDecounter> counter; + if (that->availableWorkers_ != NULL) + { + counter.reset(new MetricsRegistry::AvailableResourcesDecounter(*that->availableWorkers_)); + } + IRunnableBySteps& runnable = *dynamic_cast<IRunnableBySteps*>(obj.get()); bool wishToContinue = runnable.Step(); @@ -86,10 +95,12 @@ public: Worker(const bool& globalContinue, SharedMessageQueue& queue, - const std::string& name) : + const std::string& threadName, + MetricsRegistry::SharedMetrics* availableWorkers) : continue_(globalContinue), queue_(queue), - name_(name) + threadName_(threadName), + availableWorkers_(availableWorkers) { thread_ = boost::thread(WorkerThread, this); } @@ -103,16 +114,32 @@ } }; - bool continue_; std::vector<Worker*> workers_; SharedMessageQueue queue_; + std::unique_ptr<MetricsRegistry::SharedMetrics> availableWorkers_; + + public: + explicit PImpl(MetricsRegistry::SharedMetrics* availableWorkers /* takes ownership */) : + continue_(false), + availableWorkers_(availableWorkers) + { + } }; + void RunnableWorkersPool::Start(size_t countWorkers, + const std::string& baseThreadName, + MetricsRegistry::SharedMetrics* availableWorkers) + { + std::unique_ptr<MetricsRegistry::SharedMetrics> protection(availableWorkers); - RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers, const std::string& name) : pimpl_(new PImpl) - { + if (pimpl_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + pimpl_.reset(new PImpl(protection.release())); pimpl_->continue_ = true; if (countWorkers == 0) @@ -124,13 +151,34 @@ for (size_t i = 0; i < countWorkers; i++) { - std::string workerName = name + boost::lexical_cast<std::string>(i); - pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_, workerName); + std::string workerName = baseThreadName + boost::lexical_cast<std::string>(i); + pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_, workerName, pimpl_->availableWorkers_.get()); } } - void RunnableWorkersPool::Stop() + RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers, + const std::string& baseThreadName) + { + Start(countWorkers, baseThreadName, NULL); + } + + + RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers, + const std::string& baseThreadName, + MetricsRegistry& registry, + const char* availableWorkersMetricsName) + { + std::unique_ptr<MetricsRegistry::SharedMetrics> availableWorkers( + new MetricsRegistry::SharedMetrics(registry, availableWorkersMetricsName, MetricsUpdatePolicy_MinOver10Seconds)); + + availableWorkers->Add(countWorkers); // mark all workers as available + + Start(countWorkers, baseThreadName, availableWorkers.release()); + } + + + RunnableWorkersPool::~RunnableWorkersPool() { if (pimpl_->continue_) { @@ -150,12 +198,6 @@ } - RunnableWorkersPool::~RunnableWorkersPool() - { - Stop(); - } - - void RunnableWorkersPool::Add(IRunnableBySteps* runnable) { if (!pimpl_->continue_)
--- a/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h Tue Nov 04 15:58:06 2025 +0100 @@ -26,6 +26,8 @@ #include "IRunnableBySteps.h" +#include "../MetricsRegistry.h" + #include <boost/shared_ptr.hpp> namespace Orthanc @@ -36,10 +38,18 @@ struct PImpl; boost::shared_ptr<PImpl> pimpl_; - void Stop(); + void Start(size_t countWorkers, + const std::string& baseThreadName, + MetricsRegistry::SharedMetrics* availableWorkers /* can be NULL */); public: - explicit RunnableWorkersPool(size_t countWorkers, const std::string& name); + RunnableWorkersPool(size_t countWorkers, + const std::string& baseThreadName); + + RunnableWorkersPool(size_t countWorkers, + const std::string& name, + MetricsRegistry& registry, + const char* availableWorkersMetricsName); ~RunnableWorkersPool();
--- a/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h Tue Nov 04 15:58:06 2025 +0100 @@ -52,7 +52,7 @@ // This transfers the ownership of the message void Enqueue(IDynamicObject* message); - // The caller is responsible to delete the dequeud message! + // The caller is responsible to delete the dequeued message! IDynamicObject* Dequeue(int32_t millisecondsTimeout); bool WaitEmpty(int32_t millisecondsTimeout);
--- a/OrthancFramework/Sources/OrthancException.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/OrthancException.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -33,7 +33,9 @@ OrthancException::OrthancException(const OrthancException& other) : errorCode_(other.errorCode_), httpStatus_(other.httpStatus_), - logged_(false) + logged_(false), + hasDimseErrorStatus_(other.hasDimseErrorStatus_), + dimseErrorStatus_(other.dimseErrorStatus_) { if (other.details_.get() != NULL) { @@ -44,7 +46,9 @@ OrthancException::OrthancException(ErrorCode errorCode) : errorCode_(errorCode), httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)), - logged_(false) + logged_(false), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) { } @@ -54,7 +58,28 @@ errorCode_(errorCode), httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)), logged_(log), - details_(new std::string(details)) + details_(new std::string(details)), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) + { +#if ORTHANC_ENABLE_LOGGING == 1 + if (log) + { + LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details; + } +#endif + } + + OrthancException::OrthancException(ErrorCode errorCode, + const std::string& details, + uint16_t dimseErrorStatus, + bool log) : + errorCode_(errorCode), + httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)), + logged_(log), + details_(new std::string(details)), + hasDimseErrorStatus_(true), + dimseErrorStatus_(dimseErrorStatus) { #if ORTHANC_ENABLE_LOGGING == 1 if (log) @@ -68,18 +93,42 @@ HttpStatus httpStatus) : errorCode_(errorCode), httpStatus_(httpStatus), - logged_(false) + logged_(false), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) { } OrthancException::OrthancException(ErrorCode errorCode, HttpStatus httpStatus, const std::string& details, + uint16_t dimseErrorStatus, + bool log) : + errorCode_(errorCode), + httpStatus_(httpStatus), + logged_(log), + details_(new std::string(details)), + hasDimseErrorStatus_(true), + dimseErrorStatus_(dimseErrorStatus) + { +#if ORTHANC_ENABLE_LOGGING == 1 + if (log) + { + LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details; + } +#endif + } + + OrthancException::OrthancException(ErrorCode errorCode, + HttpStatus httpStatus, + const std::string& details, bool log) : errorCode_(errorCode), httpStatus_(httpStatus), logged_(log), - details_(new std::string(details)) + details_(new std::string(details)), + hasDimseErrorStatus_(false), + dimseErrorStatus_(0x0000) { #if ORTHANC_ENABLE_LOGGING == 1 if (log) @@ -121,6 +170,21 @@ } } + bool OrthancException::HasDimseErrorStatus() const + { + return hasDimseErrorStatus_; + } + + uint16_t OrthancException::GetDimseErrorStatus() const + { + if (!hasDimseErrorStatus_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return dimseErrorStatus_; + } + bool OrthancException::HasBeenLogged() const { return logged_;
--- a/OrthancFramework/Sources/OrthancException.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/OrthancException.h Tue Nov 04 15:58:06 2025 +0100 @@ -28,6 +28,8 @@ #include "Enumerations.h" #include "OrthancFramework.h" +#include <stdint.h> // For uint16_t + namespace Orthanc { class ORTHANC_PUBLIC OrthancException @@ -44,6 +46,10 @@ // New in Orthanc 1.5.0 std::unique_ptr<std::string> details_; + // New in Orthanc 1.12.10 + bool hasDimseErrorStatus_; + uint16_t dimseErrorStatus_; + public: OrthancException(const OrthancException& other); @@ -54,6 +60,11 @@ bool log = true); OrthancException(ErrorCode errorCode, + const std::string& details, + uint16_t dimseErrorStatus, + bool log = true); + + OrthancException(ErrorCode errorCode, HttpStatus httpStatus); OrthancException(ErrorCode errorCode, @@ -61,6 +72,12 @@ const std::string& details, bool log = true); + OrthancException(ErrorCode errorCode, + HttpStatus httpStatus, + const std::string& details, + uint16_t dimseErrorStatus, + bool log = true); + ErrorCode GetErrorCode() const; HttpStatus GetHttpStatus() const; @@ -72,5 +89,9 @@ const char* GetDetails() const; bool HasBeenLogged() const; + + bool HasDimseErrorStatus() const; + + uint16_t GetDimseErrorStatus() const; }; }
--- a/OrthancFramework/Sources/RestApi/RestApi.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/RestApi/RestApi.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -775,7 +775,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { return false; } @@ -790,7 +791,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { RestApiOutput wrappedOutput(output, method);
--- a/OrthancFramework/Sources/RestApi/RestApi.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/RestApi/RestApi.h Tue Nov 04 15:58:06 2025 +0100 @@ -46,7 +46,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; virtual bool Handle(HttpOutput& output, RequestOrigin origin, @@ -57,7 +58,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; void Register(const std::string& path, RestApiGetCall::Handler handler);
--- a/OrthancFramework/Sources/SQLite/Statement.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/SQLite/Statement.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -236,10 +236,22 @@ BindString(col, UTF16ToUTF8(value)); }*/ - void Statement::BindBlob(int col, const void* val, int val_len) + void Statement::BindBlob(int col, const void* val, size_t val_len) { - CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, val_len, SQLITE_TRANSIENT), - ErrorCode_BadParameterType); + if (static_cast<size_t>(static_cast<int>(val_len)) != val_len) + { + throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange); + } + else + { + CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, static_cast<int>(val_len), SQLITE_TRANSIENT), + ErrorCode_BadParameterType); + } + } + + void Statement::BindBlob(int col, const std::string& value) + { + BindBlob(col, value.empty() ? NULL : value.c_str(), value.size()); }
--- a/OrthancFramework/Sources/SQLite/Statement.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/SQLite/Statement.h Tue Nov 04 15:58:06 2025 +0100 @@ -130,7 +130,8 @@ void BindCString(int col, const char* val); void BindString(int col, const std::string& val); //void BindString16(int col, const string16& value); - void BindBlob(int col, const void* value, int value_len); + void BindBlob(int col, const void* value, size_t value_len); + void BindBlob(int col, const std::string& value); // Retrieving ----------------------------------------------------------------
--- a/OrthancFramework/Sources/SerializationToolbox.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/SerializationToolbox.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -188,10 +188,11 @@ { ReadArrayOfStrings(target, arr); } - catch (OrthancException& ex) - { // more detailed error + catch (OrthancException&) + { + // more detailed error throw OrthancException(ErrorCode_BadFileFormat, - "List of strings expected in field: " + field); + "List of strings expected in field: " + field); } }
--- a/OrthancFramework/Sources/SharedLibrary.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/SharedLibrary.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -27,6 +27,7 @@ #include "Logging.h" #include "OrthancException.h" +#include "SystemToolbox.h" #include <boost/filesystem.hpp> @@ -40,15 +41,15 @@ namespace Orthanc { - SharedLibrary::SharedLibrary(const std::string& path) : + SharedLibrary::SharedLibrary(const boost::filesystem::path& path) : path_(path), handle_(NULL) { #if defined(_WIN32) - handle_ = ::LoadLibraryA(path_.c_str()); + handle_ = ::LoadLibraryW(path_.wstring().c_str()); if (handle_ == NULL) { - LOG(ERROR) << "LoadLibrary(" << path_ << ") failed: Error " << ::GetLastError(); + LOG(ERROR) << "LoadLibrary(" << SystemToolbox::PathToUtf8(path_) << ") failed: Error " << ::GetLastError(); if (::GetLastError() == ERROR_BAD_EXE_FORMAT && sizeof(void*) == 4) @@ -120,7 +121,7 @@ } - const std::string &SharedLibrary::GetPath() const + const boost::filesystem::path& SharedLibrary::GetPath() const { return path_; }
--- a/OrthancFramework/Sources/SharedLibrary.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/SharedLibrary.h Tue Nov 04 15:58:06 2025 +0100 @@ -40,6 +40,7 @@ #include <string> #include <boost/noncopyable.hpp> +#include <boost/filesystem.hpp> namespace Orthanc { @@ -53,17 +54,17 @@ #endif private: - std::string path_; + boost::filesystem::path path_; void *handle_; FunctionPointer GetFunctionInternal(const std::string& name); public: - explicit SharedLibrary(const std::string& path); + explicit SharedLibrary(const boost::filesystem::path& path); ~SharedLibrary(); - const std::string& GetPath() const; + const boost::filesystem::path& GetPath() const; bool HasFunction(const std::string& name);
--- a/OrthancFramework/Sources/SystemToolbox.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -222,13 +222,13 @@ void SystemToolbox::ReadFile(std::string& content, - const std::string& path, + const boost::filesystem::path& path, bool log) { if (!IsRegularFile(path)) { throw OrthancException(ErrorCode_RegularFileExpected, - "The path does not point to a regular file: " + path, + "The path does not point to a regular file: " + PathToUtf8(path), log); } @@ -238,8 +238,7 @@ f.open(path, std::ifstream::in | std::ifstream::binary); if (!f.good()) { - throw OrthancException(ErrorCode_InexistentFile, - "File not found: " + path, + throw OrthancException(ErrorCode_InexistentFile, "File not found: " + PathToUtf8(path), log); } @@ -270,20 +269,14 @@ } - void SystemToolbox::ReadFile(std::string &content, const std::string &path) - { - ReadFile(content, path, true /* log */); - } - - bool SystemToolbox::ReadHeader(std::string& header, - const std::string& path, + const boost::filesystem::path& path, size_t headerSize) { if (!IsRegularFile(path)) { throw OrthancException(ErrorCode_RegularFileExpected, - "The path does not point to a regular file: " + path); + "The path does not point to a regular file: " + PathToUtf8(path)); } try @@ -331,17 +324,16 @@ } } - - void SystemToolbox::WriteFile(const void* content, - size_t size, - const std::string& path, + + void SystemToolbox::WriteFile(const void *content, + size_t size, + const boost::filesystem::path& path, bool callFsync) { try { - //boost::filesystem::ofstream f; boost::iostreams::stream<boost::iostreams::file_descriptor_sink> f; - + f.open(path, std::ofstream::out | std::ofstream::binary); if (!f.good()) { @@ -372,7 +364,7 @@ * systems: * https://github.com/boostorg/iostreams/blob/develop/include/boost/iostreams/detail/file_handle.hpp **/ - + #if defined(_WIN32) // https://docs.microsoft.com/fr-fr/windows/win32/api/fileapi/nf-fileapi-flushfilebuffers success = (::FlushFileBuffers(f->handle()) != 0); @@ -401,28 +393,15 @@ } - void SystemToolbox::WriteFile(const void *content, size_t size, const std::string &path) - { - WriteFile(content, size, path, false /* don't automatically call fsync */); + void SystemToolbox::WriteFile(const std::string& content, + const boost::filesystem::path& path) + { + WriteFile(content.size() > 0 ? content.c_str() : NULL, + content.size(), path, false /* don't automatically call fsync */); } - void SystemToolbox::WriteFile(const std::string& content, - const std::string& path, - bool callFsync) - { - WriteFile(content.size() > 0 ? content.c_str() : NULL, - content.size(), path, callFsync); - } - - - void SystemToolbox::WriteFile(const std::string &content, const std::string &path) - { - WriteFile(content, path, false /* don't automatically call fsync */); - } - - - void SystemToolbox::RemoveFile(const std::string& path) + void SystemToolbox::RemoveFile(const boost::filesystem::path& path) { if (boost::filesystem::exists(path)) { @@ -438,7 +417,7 @@ } - uint64_t SystemToolbox::GetFileSize(const std::string& path) + uint64_t SystemToolbox::GetFileSize(const boost::filesystem::path& path) { try { @@ -455,7 +434,66 @@ } - void SystemToolbox::MakeDirectory(const std::string& path) +#if ORTHANC_ENABLE_MD5 == 1 + void SystemToolbox::ComputeStreamMD5(std::string& result, + std::istream& inputStream) + { + Toolbox::MD5Context context; + + const size_t bufferSize = 1024; + char buffer[bufferSize]; + + while (inputStream.good()) + { + inputStream.read(buffer, bufferSize); + std::streamsize bytesRead = inputStream.gcount(); + + if (bytesRead > 0) + { + context.Append(buffer, bytesRead); + } + } + + context.Export(result); + } + + + void SystemToolbox::ComputeFileMD5(std::string& result, + const boost::filesystem::path& path) + { + boost::filesystem::ifstream fileStream; + fileStream.open(path, std::ifstream::in | std::ifstream::binary); + + if (!fileStream.good()) + { + throw OrthancException(ErrorCode_InexistentFile, "File not found: " + PathToUtf8(path)); + } + + ComputeStreamMD5(result, fileStream); + } + + + bool SystemToolbox::CompareFilesMD5(const boost::filesystem::path& path1, + const boost::filesystem::path& path2) + { + if (GetFileSize(path1) != GetFileSize(path2)) + { + return false; + } + else + { + std::string path1md5, path2md5; + + ComputeFileMD5(path1md5, path1); + ComputeFileMD5(path2md5, path2); + + return path1md5 == path2md5; + } + } +#endif + + + void SystemToolbox::MakeDirectory(const boost::filesystem::path& path) { if (boost::filesystem::exists(path)) { @@ -474,12 +512,6 @@ } - bool SystemToolbox::IsExistingFile(const std::string& path) - { - return boost::filesystem::exists(path); - } - - #if defined(_WIN32) static std::string GetPathToExecutableInternal() { @@ -552,17 +584,84 @@ #endif - std::string SystemToolbox::GetPathToExecutable() +#ifdef _WIN32 + std::wstring SystemToolbox::Utf8ToWString(const std::string& str) + { + if (str.empty()) + { + return std::wstring(); + } + + int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int) str.size(), NULL, 0); + + std::wstring wstr(sizeNeeded, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int) str.size(), &wstr[0], sizeNeeded); + + return wstr; + } + + std::string SystemToolbox::WStringToUtf8(const std::wstring &wstr) { - boost::filesystem::path p(GetPathToExecutableInternal()); - return boost::filesystem::absolute(p).string(); + if (wstr.empty()) + { + return std::string(); + } + + int sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int) wstr.size(), NULL, 0, NULL, NULL); + + std::string str(sizeNeeded, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int) wstr.size(), &str[0], sizeNeeded, NULL, NULL); + + return str; + } + + std::wstring SystemToolbox::WStringFromCharPtr(const char *str) + { + if (str == NULL) + { + return std::wstring(); + } + + int sizeNeeded = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0); + + std::wstring wstr(sizeNeeded, 0); + MultiByteToWideChar(CP_UTF8, 0, str, -1, &wstr[0], sizeNeeded); + + return wstr; + + } +#endif + + boost::filesystem::path SystemToolbox::PathFromUtf8(const std::string& utf8) + { +#if defined(_WIN32) && !defined(__MINGW32__) // non-ASCII paths are not supported when building with MinGW + return boost::filesystem::path(Utf8ToWString(utf8)); +#else + return boost::filesystem::path(utf8); // POSIX: std::string is UTF-8 +#endif + } + + std::string SystemToolbox::PathToUtf8(const boost::filesystem::path& p) + { +#if defined(_WIN32) && !defined(__MINGW32__) // non-ASCII paths are not supported when building with MinGW + return WStringToUtf8(p.wstring()); +#else + return p.string(); // POSIX: already UTF-8 +#endif } - std::string SystemToolbox::GetDirectoryOfExecutable() + boost::filesystem::path SystemToolbox::GetPathToExecutable() { - boost::filesystem::path p(GetPathToExecutableInternal()); - return boost::filesystem::absolute(p.parent_path()).string(); + boost::filesystem::path p(PathFromUtf8(GetPathToExecutableInternal())); + return boost::filesystem::absolute(p); + } + + + boost::filesystem::path SystemToolbox::GetDirectoryOfExecutable() + { + boost::filesystem::path p(PathFromUtf8(GetPathToExecutableInternal())); + return boost::filesystem::absolute(p.parent_path()); } @@ -629,7 +728,7 @@ } - bool SystemToolbox::IsRegularFile(const std::string& path) + bool SystemToolbox::IsRegularFile(const boost::filesystem::path& path) { try { @@ -648,14 +747,28 @@ } - FILE* SystemToolbox::OpenFile(const std::string& path, + FILE* SystemToolbox::OpenFile(const boost::filesystem::path& path, FileMode mode) { #if defined(_WIN32) - // TODO Deal with special characters by converting to the current locale -#endif + const wchar_t *m; + switch (mode) + { + case FileMode_ReadBinary: + m = L"rb"; + break; - const char* m; + case FileMode_WriteBinary: + m = L"wb"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + return _wfopen(path.wstring().c_str(), m); +#else + const char *m; switch (mode) { case FileMode_ReadBinary: @@ -671,6 +784,7 @@ } return fopen(path.c_str(), m); +#endif } @@ -773,9 +887,10 @@ return false; } - MimeType SystemToolbox::AutodetectMimeType(const std::string& path) + + MimeType SystemToolbox::AutodetectMimeType(const boost::filesystem::path& path) { - std::string extension = boost::filesystem::path(path).extension().string(); + std::string extension = path.extension().string(); Toolbox::ToLowerCase(extension); // http://en.wikipedia.org/wiki/Mime_types @@ -913,16 +1028,15 @@ } - std::string SystemToolbox::InterpretRelativePath(const std::string& baseDirectory, - const std::string& relativePath) + boost::filesystem::path SystemToolbox::InterpretRelativePath(const boost::filesystem::path& baseDirectory, + const std::string& relativePath) { - boost::filesystem::path base(baseDirectory); - boost::filesystem::path relative(relativePath); + boost::filesystem::path relative = SystemToolbox::PathFromUtf8(relativePath); /** The following lines should be equivalent to this one: - return (base / relative).string(); + return (base / relative); However, for some unknown reason, some versions of Boost do not make the proper path resolution when "baseDirectory" is an @@ -931,17 +1045,17 @@ if (relative.is_absolute()) { - return relative.string(); + return relative; } else { - return (base / relative).string(); + return baseDirectory / relative; } } void SystemToolbox::ReadFileRange(std::string& content, - const std::string& path, + const boost::filesystem::path& path, uint64_t start, // Inclusive uint64_t end, // Exclusive bool throwIfOverflow) @@ -954,15 +1068,15 @@ if (!IsRegularFile(path)) { throw OrthancException(ErrorCode_RegularFileExpected, - "The path does not point to a regular file: " + path); + "The path does not point to a regular file: " + SystemToolbox::PathToUtf8(path)); } boost::filesystem::ifstream f; f.open(path, std::ifstream::in | std::ifstream::binary); if (!f.good()) { - throw OrthancException(ErrorCode_InexistentFile, - "File not found: " + path); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + SystemToolbox::PathToUtf8(path)); } uint64_t fileSize = static_cast<uint64_t>(GetStreamSize(f));
--- a/OrthancFramework/Sources/SystemToolbox.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.h Tue Nov 04 15:58:06 2025 +0100 @@ -30,6 +30,10 @@ # error The macro ORTHANC_SANDBOXED must be defined #endif +#if !defined(ORTHANC_ENABLE_MD5) +# error The macro ORTHANC_ENABLE_MD5 must be defined +#endif + #if ORTHANC_SANDBOXED == 1 # error The namespace SystemToolbox cannot be used in sandboxed environments #endif @@ -40,6 +44,10 @@ #include <vector> #include <string> #include <stdint.h> +#include <boost/filesystem.hpp> + +// Note: The use of "boost::filesystem::path" is mandatory to handle +// non ASCII-only path on Windows namespace Orthanc { @@ -53,53 +61,65 @@ static ServerBarrierEvent ServerBarrier(); static void ReadFile(std::string& content, - const std::string& path, + const boost::filesystem::path& path, bool log); - static void ReadFile(std::string& content, - const std::string& path); + static inline void ReadFile(std::string& content, + const boost::filesystem::path& path) + { + ReadFile(content, path, true /* log */); + } static bool ReadHeader(std::string& header, - const std::string& path, + const boost::filesystem::path& path, size_t headerSize); static void WriteFile(const void* content, size_t size, - const std::string& path, - bool callFsync); - - static void WriteFile(const void* content, - size_t size, - const std::string& path); - - static void WriteFile(const std::string& content, - const std::string& path, + const boost::filesystem::path& path, bool callFsync); static void WriteFile(const std::string& content, - const std::string& path); + const boost::filesystem::path& path); + + static void RemoveFile(const boost::filesystem::path& path); - static void RemoveFile(const std::string& path); + static uint64_t GetFileSize(const boost::filesystem::path& path); - static uint64_t GetFileSize(const std::string& path); +#if ORTHANC_ENABLE_MD5 == 1 + static void ComputeStreamMD5(std::string& result, + std::istream& stream); - static void MakeDirectory(const std::string& path); - - static bool IsExistingFile(const std::string& path); + static void ComputeFileMD5(std::string& result, + const boost::filesystem::path& path); - static std::string GetPathToExecutable(); + // Returns true if the two files have the same MD5 + static bool CompareFilesMD5(const boost::filesystem::path& path1, + const boost::filesystem::path& path2); +#endif - static std::string GetDirectoryOfExecutable(); + static void MakeDirectory(const boost::filesystem::path& path); + + static boost::filesystem::path GetPathToExecutable(); + + static boost::filesystem::path GetDirectoryOfExecutable(); static void ExecuteSystemCommand(const std::string& command, const std::vector<std::string>& arguments); static int GetProcessId(); - static bool IsRegularFile(const std::string& path); + static bool IsRegularFile(const boost::filesystem::path& path); + + static FILE* OpenFile(const boost::filesystem::path& path, + FileMode mode); - static FILE* OpenFile(const std::string& path, - FileMode mode); + static MimeType AutodetectMimeType(const boost::filesystem::path& path); + + // This conversion is mandatory to handle non ASCII-only path on Windows + static boost::filesystem::path PathFromUtf8(const std::string& utf8); + + static std::string PathToUtf8(const boost::filesystem::path& p); static std::string GetNowIsoString(bool utc); @@ -113,19 +133,26 @@ static bool IsContentCompressible(const std::string& contentType); - static MimeType AutodetectMimeType(const std::string& path); - static void GetEnvironmentVariables(std::map<std::string, std::string>& env); - static std::string InterpretRelativePath(const std::string& baseDirectory, - const std::string& relativePath); + static boost::filesystem::path InterpretRelativePath(const boost::filesystem::path& baseDirectory, + const std::string& relativePath); - static void ReadFileRange(std::string& content, - const std::string& path, - uint64_t start, // Inclusive - uint64_t end, // Exclusive + static void ReadFileRange(std::string& content, + const boost::filesystem::path& path, + uint64_t start, // Inclusive + uint64_t end, // Exclusive bool throwIfOverflow); static void GetMacAddresses(std::set<std::string>& target); + +#ifdef _WIN32 + static std::wstring Utf8ToWString(const std::string& str); + + static std::string WStringToUtf8(const std::wstring& wstr); + + static std::wstring WStringFromCharPtr(const char *wstr); +#endif + }; }
--- a/OrthancFramework/Sources/TemporaryFile.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/TemporaryFile.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -34,12 +34,12 @@ namespace Orthanc { - static std::string CreateTemporaryPath(const char* temporaryDirectory, - const char* extension) + static boost::filesystem::path CreateTemporaryPath(const boost::filesystem::path& temporaryDirectory, + const char* extension) { boost::filesystem::path dir; - if (temporaryDirectory == NULL) + if (temporaryDirectory.empty()) { #if BOOST_HAS_FILESYSTEM_V3 == 1 dir = boost::filesystem::temp_directory_path(); @@ -69,19 +69,19 @@ } dir /= filename; - return dir.string(); + return dir; } TemporaryFile::TemporaryFile() : - path_(CreateTemporaryPath(NULL, NULL)) + path_(CreateTemporaryPath("", NULL)) { } - TemporaryFile::TemporaryFile(const std::string& temporaryDirectory, + TemporaryFile::TemporaryFile(const boost::filesystem::path& temporaryDirectory, const std::string& extension) : - path_(CreateTemporaryPath(temporaryDirectory.c_str(), extension.c_str())) + path_(CreateTemporaryPath(temporaryDirectory, extension.c_str())) { } @@ -91,7 +91,7 @@ boost::filesystem::remove(path_); } - const std::string &TemporaryFile::GetPath() const + const boost::filesystem::path& TemporaryFile::GetPath() const { return path_; } @@ -106,7 +106,7 @@ catch (OrthancException& e) { throw OrthancException(e.GetErrorCode(), - "Can't create temporary file \"" + path_ + + "Can't create temporary file \"" + SystemToolbox::PathToUtf8(path_) + "\" with " + boost::lexical_cast<std::string>(content.size()) + " bytes: Check you have write access to the " "temporary directory and that it is not full"); @@ -122,8 +122,8 @@ } catch (OrthancException& e) { - throw OrthancException(e.GetErrorCode(), - "Can't read temporary file \"" + path_ + + throw OrthancException(e.GetErrorCode(), + "Can't read temporary file \"" + SystemToolbox::PathToUtf8(path_) + "\": Another process has corrupted the temporary directory"); } }
--- a/OrthancFramework/Sources/TemporaryFile.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/TemporaryFile.h Tue Nov 04 15:58:06 2025 +0100 @@ -35,6 +35,7 @@ #endif #include <boost/noncopyable.hpp> +#include <boost/filesystem.hpp> #include <stdint.h> #include <string> @@ -43,17 +44,17 @@ class ORTHANC_PUBLIC TemporaryFile : public boost::noncopyable { private: - std::string path_; + boost::filesystem::path path_; public: TemporaryFile(); - TemporaryFile(const std::string& temporaryFolder, + TemporaryFile(const boost::filesystem::path& temporaryFolder, const std::string& extension); ~TemporaryFile(); - const std::string& GetPath() const; + const boost::filesystem::path& GetPath() const; void Write(const std::string& content);
--- a/OrthancFramework/Sources/Toolbox.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -37,8 +37,10 @@ # error Cannot access the version of JsonCpp #endif -#if !defined(ORTHANC_ENABLE_ICU) +#if (ORTHANC_ENABLE_LOCALE == 1) && (BOOST_LOCALE_WITH_ICU == 1) # define ORTHANC_ENABLE_ICU 1 +#else +# define ORTHANC_ENABLE_ICU 0 #endif @@ -64,6 +66,7 @@ #include <boost/algorithm/string/join.hpp> #include <boost/lexical_cast.hpp> #include <boost/regex.hpp> +#include <cassert> #if BOOST_VERSION >= 106600 # include <boost/uuid/detail/sha1.hpp> @@ -147,21 +150,22 @@ } -#if defined(ORTHANC_STATIC_ICU) - -# if (ORTHANC_STATIC_ICU == 1) && (ORTHANC_ENABLE_ICU == 1) +#if ORTHANC_ENABLE_ICU == 1 + +# if ORTHANC_STATIC_ICU == 1 # if !defined(ORTHANC_FRAMEWORK_INCLUDE_RESOURCES) || (ORTHANC_FRAMEWORK_INCLUDE_RESOURCES == 1) # include <OrthancFrameworkResources.h> # endif +# include "Compression/GzipCompressor.h" # endif -# if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1) -# include <unicode/udata.h> -# include <unicode/uloc.h> -# include "Compression/GzipCompressor.h" +# include <unicode/udata.h> +# include <unicode/uloc.h> +# include <unicode/uclean.h> static std::string globalIcuData_; +# if ORTHANC_STATIC_ICU == 1 extern "C" { // This is dummy content for the "icudt58_dat" (resp. "icudt63_dat") @@ -169,15 +173,18 @@ // (resp. "icudt63l_dat.c") file that contains a huge C array. In // Orthanc, this array is compressed using gzip and attached as a // resource, then uncompressed during the launch of Orthanc by - // static function "InitializeIcu()". + // static function "InitializeIcu()". WARNING: Do NOT do this if + // dynamically linking against libicu! struct { double bogus; uint8_t *bytes; } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; } - -# if defined(__LSB_VERSION__) +# endif + +# if defined(__LSB_VERSION__) + extern "C" { /** @@ -192,9 +199,9 @@ **/ char *tzname[2] = { (char *) "GMT", (char *) "GMT" }; } -# endif # endif + #endif @@ -207,6 +214,112 @@ namespace Orthanc { +#if ORTHANC_ENABLE_MD5 == 1 + static char GetHexadecimalCharacter(uint8_t value) + { + assert(value < 16); + + if (value < 10) + { + return value + '0'; + } + else + { + return (value - 10) + 'a'; + } + } + + + struct Toolbox::MD5Context::PImpl + { + md5_state_s state_; + bool done_; + + PImpl() : + done_(false) + { + md5_init(&state_); + } + }; + + + Toolbox::MD5Context::MD5Context() : + pimpl_(new PImpl) + { + } + + + void Toolbox::MD5Context::Append(const void* data, + size_t size) + { + static const size_t MAX_SIZE = 128 * 1024 * 1024; + + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const uint8_t *p = reinterpret_cast<const uint8_t*>(data); + + while (size > 0) + { + /** + * The built-in implementation of MD5 requires that "size" can + * be casted to "int", so we feed it by chunks of maximum + * 128MB. This fixes an incorrect behavior in Orthanc <= 1.12.7. + **/ + + int chunkSize; + if (size > MAX_SIZE) + { + chunkSize = static_cast<int>(MAX_SIZE); + } + else + { + chunkSize = static_cast<int>(size); + } + + md5_append(&pimpl_->state_, reinterpret_cast<const md5_byte_t*>(p), chunkSize); + + p += chunkSize; + + assert(static_cast<size_t>(chunkSize) <= size); + size -= chunkSize; + } + } + + + void Toolbox::MD5Context::Append(const std::string& source) + { + if (source.size() > 0) + { + Append(source.c_str(), source.size()); + } + } + + + void Toolbox::MD5Context::Export(std::string& target) + { + if (pimpl_->done_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + pimpl_->done_ = true; + + md5_byte_t actualHash[16]; + md5_finish(&pimpl_->state_, actualHash); + + target.resize(32); + for (unsigned int i = 0; i < 16; i++) + { + target[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16)); + target[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16)); + } + } +#endif /* ORTHANC_ENABLE_MD5 */ + + void Toolbox::LinesIterator::FindEndOfLine() { lineEnd_ = lineStart_; @@ -444,21 +557,6 @@ #if ORTHANC_ENABLE_MD5 == 1 - static char GetHexadecimalCharacter(uint8_t value) - { - assert(value < 16); - - if (value < 10) - { - return value + '0'; - } - else - { - return (value - 10) + 'a'; - } - } - - void Toolbox::ComputeMD5(std::string& result, const std::string& data) { @@ -477,25 +575,9 @@ const void* data, size_t size) { - md5_state_s state; - md5_init(&state); - - if (size > 0) - { - md5_append(&state, - reinterpret_cast<const md5_byte_t*>(data), - static_cast<int>(size)); - } - - md5_byte_t actualHash[16]; - md5_finish(&state, actualHash); - - result.resize(32); - for (unsigned int i = 0; i < 16; i++) - { - result[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16)); - result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16)); - } + MD5Context context; + context.Append(data, size); + context.Export(result); } void Toolbox::ComputeMD5(std::string& result, @@ -622,7 +704,7 @@ return "GB18030"; case Encoding_Thai: -#if BOOST_LOCALE_WITH_ICU == 1 +#if ORTHANC_ENABLE_ICU == 1 return "tis620.2533"; #else return "TIS620.2533-0"; @@ -648,7 +730,8 @@ // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2 std::string Toolbox::ConvertToUtf8(const std::string& source, Encoding sourceEncoding, - bool hasCodeExtensions) + bool hasCodeExtensions, + bool skipBackslashes) { #if ORTHANC_STATIC_ICU == 1 # if ORTHANC_ENABLE_ICU == 0 @@ -684,7 +767,26 @@ else { const char* encoding = GetBoostLocaleEncoding(sourceEncoding); - s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip); + + if (skipBackslashes) + { + /** + * This is to deal with the fact that in Japanese coding + * (ISO_IR 13), backslashes will be converted to the Yen + * character. + **/ + std::vector<std::string> tokens; + TokenizeString(tokens, source, '\\'); + for (size_t i = 0; i < tokens.size(); i++) + { + tokens[i] = boost::locale::conv::to_utf<char>(tokens[i], encoding, boost::locale::conv::skip); + } + JoinStrings(s, tokens, "\\"); + } + else + { + s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip); + } } if (hasCodeExtensions) @@ -754,6 +856,50 @@ #endif +#if ORTHANC_ENABLE_LOCALE == 1 + std::string Toolbox::ConvertDicomStringToUtf8(const std::string& source, + Encoding sourceEncoding, + bool hasCodeExtensions, + ValueRepresentation vr) + { + /** + * This method was added in Orthanc 1.12.9, as a consequence of: + * https://discourse.orthanc-server.org/t/issue-with-special-characters-when-scans-where-uploaded-with-specificcharacterset-dicom-tag-value-as-iso-ir-13/5962 + * + * From the DICOM standard: "Two character codes of the + * single-byte character sets invoked in the GL area of the code + * table, 02/00 and 05/12, have special significance in the DICOM + * Standard. The character SPACE, represented by bit combination + * 02/00, shall be used for the padding of Data Element Values + * that are character strings. The Graphic Character represented + * by the bit combination 05/12, "\" (BACKSLASH) (reverse solidus) + * in the repertoire ISO-IR 6, shall only be used in character + * strings with Value Representations of UT, ST and LT (see + * Section 6.2). Otherwise the character code 05/12 is used as a + * separator for multi-valued Data Elements (see Section + * 6.4). [...] When the Value of Specific Character Set + * (0008,0005) is either "ISO_IR 13" or "ISO 2022 IR 13", the + * graphic character represented by the bit combination 05/12 is a + * "Â¥" (YEN SIGN) in the character set of ISO-IR 14." + * https://www.dicomstandard.org/standards/view/data-structures-and-encoding + * + * This description implies that if "sourceEncoding" (which is + * derived from the value of the DICOM Specific Character Set) + * corresponds "ISO_IR 13" or "ISO 2022 IR 13", AND if the value + * representation is *not* UT, ST, or LT, then backslashes should + * be ignored during the conversion to UTF-8. + **/ + + const bool skipBackslashes = (sourceEncoding == Encoding_Japanese && + vr != ValueRepresentation_UnlimitedText && // UT + vr != ValueRepresentation_ShortText && // ST + vr != ValueRepresentation_LongText); // LT + + return ConvertToUtf8(source, sourceEncoding, hasCodeExtensions, skipBackslashes); + } +#endif + + static bool IsAsciiCharacter(uint8_t c) { return (c != 0 && @@ -1406,10 +1552,25 @@ c == '-' || c == '_' || c == '.' || - c == '~' || - c == '/'); + c == '~'); } + // in this version, each path token is uri encoded separately and then all parts are joined with "/" + void Toolbox::UriEncode(std::string& target, + const std::vector<std::string>& pathTokens) + { + std::vector<std::string> uriEncodedPathTokens; + for (std::vector<std::string>::const_iterator it = pathTokens.begin(); it != pathTokens.end(); ++it) + { + std::string encodedPathToken; + Toolbox::UriEncode(encodedPathToken, *it); + uriEncodedPathTokens.push_back(encodedPathToken); + } + + Toolbox::JoinStrings(target, uriEncodedPathTokens, "/"); + } + + void Toolbox::UriEncode(std::string& target, const std::string& source) { @@ -1696,15 +1857,24 @@ // TODO - The data table must be swapped (uint16_t) throw OrthancException(ErrorCode_NotImplemented); } - - // "First-use of ICU from a single thread before the - // multi-threaded use of ICU begins", to make sure everything is - // properly initialized (should not be mandatory in our - // case). We let boost handle calls to "u_init()" and "u_cleanup()". - // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination - uloc_getDefault(); } #endif + +#if (ORTHANC_ENABLE_ICU == 1) + UErrorCode status = U_ZERO_ERROR; + u_init(&status); + + if (U_FAILURE(status)) + { + throw OrthancException(ErrorCode_InternalError, "Cannot initialize ICU: " + std::string(u_errorName(status))); + } + + // "First-use of ICU from a single thread before the + // multi-threaded use of ICU begins", to make sure everything is + // properly initialized (should not be mandatory in our case). + // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination + uloc_getDefault(); +#endif } void Toolbox::InitializeGlobalLocale(const char* locale) @@ -1714,7 +1884,7 @@ #if defined(__unix__) && ORTHANC_SANDBOXED != 1 static const char* LOCALTIME = "/etc/localtime"; - if (!SystemToolbox::IsExistingFile(LOCALTIME)) + if (!boost::filesystem::exists(LOCALTIME)) { // Check out file // "boost_1_69_0/libs/locale/src/icu/time_zone.cpp": Direct @@ -1774,6 +1944,10 @@ void Toolbox::FinalizeGlobalLocale() { globalLocale_.reset(); + +#if (ORTHANC_ENABLE_ICU == 1) + u_cleanup(); +#endif } @@ -2526,7 +2700,6 @@ #endif } - void Toolbox::RemoveSurroundingQuotes(std::string& value) { if (!value.empty() && @@ -2576,14 +2749,14 @@ return Toolbox::GetHumanTransferSpeed(full, sizeInBytes, GetElapsedNanoseconds()); } - Toolbox::ElapsedTimeLogger::ElapsedTimeLogger(const std::string& message) + Toolbox::DebugElapsedTimeLogger::DebugElapsedTimeLogger(const std::string& message) : message_(message), logged_(false) { Restart(); } - Toolbox::ElapsedTimeLogger::~ElapsedTimeLogger() + Toolbox::DebugElapsedTimeLogger::~DebugElapsedTimeLogger() { if (!logged_) { @@ -2591,17 +2764,29 @@ } } - void Toolbox::ElapsedTimeLogger::Restart() + void Toolbox::DebugElapsedTimeLogger::Restart() { timer_.Restart(); } - void Toolbox::ElapsedTimeLogger::StopAndLog() + void Toolbox::DebugElapsedTimeLogger::StopAndLog() { LOG(WARNING) << "ELAPSED TIMER: " << message_ << " (" << timer_.GetElapsedMicroseconds() << " us)"; logged_ = true; } + Toolbox::ApiElapsedTimeLogger::ApiElapsedTimeLogger(const std::string& message) : + message_(message) + { + timer_.Restart(); + CLOG(INFO, HTTP) << message_; + } + + Toolbox::ApiElapsedTimeLogger::~ApiElapsedTimeLogger() + { + CLOG(INFO, HTTP) << message_ << " (elapsed: " << timer_.GetElapsedMicroseconds() << " us)"; + } + std::string Toolbox::GetHumanFileSize(uint64_t sizeInBytes) { if (sizeInBytes < 1024)
--- a/OrthancFramework/Sources/Toolbox.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Tue Nov 04 15:58:06 2025 +0100 @@ -82,7 +82,26 @@ class ORTHANC_PUBLIC Toolbox { public: - class ORTHANC_PUBLIC LinesIterator +#if ORTHANC_ENABLE_MD5 == 1 + class ORTHANC_PUBLIC MD5Context : public boost::noncopyable + { + private: + class PImpl; + boost::shared_ptr<PImpl> pimpl_; + + public: + MD5Context(); + + void Append(const void* data, + size_t size); + + void Append(const std::string& source); + + void Export(std::string& target); + }; +#endif + + class ORTHANC_PUBLIC LinesIterator : public boost::noncopyable { private: const std::string& content_; @@ -167,7 +186,13 @@ #if ORTHANC_ENABLE_LOCALE == 1 static std::string ConvertToUtf8(const std::string& source, Encoding sourceEncoding, - bool hasCodeExtensions); + bool hasCodeExtensions, + bool skipBackslashes /* was always "false" in Orthanc <= 1.12.8 */); + + static std::string ConvertDicomStringToUtf8(const std::string& source, + Encoding sourceEncoding, + bool hasCodeExtensions, + ValueRepresentation vr); static std::string ConvertFromUtf8(const std::string& source, Encoding targetEncoding); @@ -301,6 +326,9 @@ static void UriEncode(std::string& target, const std::string& source); + static void UriEncode(std::string& target, + const std::vector<std::string>& pathTokens); + static std::string GetJsonStringField(const ::Json::Value& json, const std::string& key, const std::string& defaultValue); @@ -377,11 +405,13 @@ static void RemoveSurroundingQuotes(std::string& value); - class ORTHANC_PUBLIC ElapsedTimer + class ORTHANC_PUBLIC ElapsedTimer : public boost::noncopyable { + private: boost::posix_time::ptime start_; + public: - explicit ElapsedTimer(); + ElapsedTimer(); uint64_t GetElapsedMilliseconds(); uint64_t GetElapsedMicroseconds(); @@ -394,23 +424,38 @@ }; // This is a helper class to measure and log time spend e.g in a method. - // This should be used only during debugging and should likely not ever used in a release. + // This should be used only during debugging and should likely not ever be used in a release. // By default, you should use it as a RAII but you may force Restart/StopAndLog manually if needed. - class ORTHANC_PUBLIC ElapsedTimeLogger + class ORTHANC_PUBLIC DebugElapsedTimeLogger : public boost::noncopyable { private: ElapsedTimer timer_; const std::string message_; - bool logged_; + bool logged_; public: - explicit ElapsedTimeLogger(const std::string& message); - ~ElapsedTimeLogger(); + explicit DebugElapsedTimeLogger(const std::string& message); + + ~DebugElapsedTimeLogger(); void Restart(); void StopAndLog(); }; + // This variant logs the same message when entering the method and when exiting (with the elapsed time). + // Logs goes to verbose-http. + class ORTHANC_PUBLIC ApiElapsedTimeLogger : public boost::noncopyable + { + private: + ElapsedTimer timer_; + const std::string message_; + + public: + explicit ApiElapsedTimeLogger(const std::string& message); + + ~ApiElapsedTimeLogger(); + }; + static std::string GetHumanFileSize(uint64_t sizeInBytes); static std::string GetHumanDuration(uint64_t durationInNanoseconds);
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -1466,7 +1466,7 @@ { std::string dicom; - SystemToolbox::ReadFile(dicom, path); + SystemToolbox::ReadFile(dicom, current->path()); ParsedDicomFile f(dicom); f.ExtractDicomSummary(m2, 256);
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -30,10 +30,9 @@ #include <gtest/gtest.h> #include "../Sources/FileStorage/FilesystemStorage.h" +#include "../Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../Sources/FileStorage/StorageAccessor.h" #include "../Sources/FileStorage/StorageCache.h" -#include "../Sources/HttpServer/BufferHttpSender.h" -#include "../Sources/HttpServer/FilesystemHttpSender.h" #include "../Sources/Logging.h" #include "../Sources/OrthancException.h" #include "../Sources/Toolbox.h" @@ -63,12 +62,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, Basic2) @@ -81,12 +86,18 @@ s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); std::string d; { - std::unique_ptr<IMemoryBuffer> buffer(s.Read(uid, FileContentType_Unknown)); + std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); buffer->MoveToString(d); } ASSERT_EQ(d.size(), data.size()); ASSERT_FALSE(memcmp(&d[0], &data[0], data.size())); ASSERT_EQ(s.GetSize(uid), data.size()); + { + std::unique_ptr<IMemoryBuffer> buffer2(s.ReadRange(uid, FileContentType_Unknown, 0, uid.size())); + std::string d2; + buffer2->MoveToString(d2); + ASSERT_EQ(d, d2); + } } TEST(FilesystemStorage, FileWithSameNameAsTopDirectory) @@ -97,7 +108,7 @@ std::vector<uint8_t> data; StringToVector(data, Toolbox::GenerateUuid()); - SystemToolbox::WriteFile("toto", "UnitTestsStorageTop/12"); + SystemToolbox::WriteFile("toto", SystemToolbox::PathFromUtf8("UnitTestsStorageTop/12")); ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException); s.Clear(); } @@ -110,8 +121,8 @@ std::vector<uint8_t> data; StringToVector(data, Toolbox::GenerateUuid()); - SystemToolbox::MakeDirectory("UnitTestsStorageChild/12"); - SystemToolbox::WriteFile("toto", "UnitTestsStorageChild/12/34"); + SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsStorageChild/12")); + SystemToolbox::WriteFile("toto", SystemToolbox::PathFromUtf8("UnitTestsStorageChild/12/34")); ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException); s.Clear(); } @@ -124,12 +135,27 @@ std::vector<uint8_t> data; StringToVector(data, Toolbox::GenerateUuid()); - SystemToolbox::MakeDirectory("UnitTestsStorageFileAlreadyExists/12/34"); - SystemToolbox::WriteFile("toto", "UnitTestsStorageFileAlreadyExists/12/34/12345678-1234-1234-1234-1234567890ab"); + SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsStorageFileAlreadyExists/12/34")); + SystemToolbox::WriteFile("toto", SystemToolbox::PathFromUtf8("UnitTestsStorageFileAlreadyExists/12/34/12345678-1234-1234-1234-1234567890ab")); ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException); s.Clear(); } +#if !defined(__MINGW32__) // non-ASCII paths are not supported when built with mingw +TEST(FilesystemStorage, FileAlreadyExistsUtf8) +{ + FilesystemStorage s(SystemToolbox::PathFromUtf8("\xd0\x95UnitTestsStorageFileAlreadyExists")); + s.Clear(); + + std::vector<uint8_t> data; + StringToVector(data, Toolbox::GenerateUuid()); + + SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("\xd0\x95UnitTestsStorageFileAlreadyExists/12/34")); + SystemToolbox::WriteFile("toto", SystemToolbox::PathFromUtf8("\xd0\x95UnitTestsStorageFileAlreadyExists/12/34/12345678-1234-1234-1234-1234567890ab")); + ASSERT_THROW(s.Create("12345678-1234-1234-1234-1234567890ab", &data[0], data.size(), FileContentType_Unknown), OrthancException); + s.Clear(); +} +#endif TEST(FilesystemStorage, EndToEnd) { @@ -169,13 +195,14 @@ TEST(StorageAccessor, NoCompression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true); - + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_None, true, NULL); + std::string r; accessor.Read(r, info); @@ -191,13 +218,14 @@ TEST(StorageAccessor, Compression) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string data = "Hello world"; - FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_ZlibWithSize, true); - + const std::string data = "Hello world"; + FileInfo info; + accessor.Write(info, data.c_str(), data.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, true, NULL); + std::string r; accessor.Read(r, info); @@ -212,20 +240,22 @@ TEST(StorageAccessor, Mix) { - FilesystemStorage s("UnitTestsStorage"); + PluginStorageAreaAdapter s(new FilesystemStorage("UnitTestsStorage")); StorageCache cache; StorageAccessor accessor(s, cache); - std::string r; - std::string compressedData = "Hello"; - std::string uncompressedData = "HelloWorld"; + const std::string compressedData = "Hello"; + const std::string uncompressedData = "HelloWorld"; - FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false); - FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false); - + FileInfo compressedInfo; + accessor.Write(compressedInfo, compressedData.c_str(), compressedData.size(), FileContentType_Dicom, CompressionType_ZlibWithSize, false, NULL); + + std::string r; accessor.Read(r, compressedInfo); ASSERT_EQ(compressedData, r); + FileInfo uncompressedInfo; + accessor.Write(uncompressedInfo, uncompressedData.c_str(), uncompressedData.size(), FileContentType_Dicom, CompressionType_None, false, NULL); accessor.Read(r, uncompressedInfo); ASSERT_EQ(uncompressedData, r); ASSERT_NE(compressedData, r); @@ -324,4 +354,46 @@ ASSERT_EQ("o", s); ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=5-").Extract(s, "Hello"), OrthancException); } -} \ No newline at end of file +} + + +TEST(SystemToolbox, ReadRange) +{ + const boost::filesystem::path path(SystemToolbox::PathFromUtf8("UnitTestsResults/hello.txt")); + SystemToolbox::WriteFile("abc", path); + + std::string s; + SystemToolbox::ReadFileRange(s, path, 0, 1, true); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ('a', s[0]); + + SystemToolbox::ReadFileRange(s, path, 1, 2, true); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ('b', s[0]); + + SystemToolbox::ReadFileRange(s, path, 2, 3, true); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ('c', s[0]); + + ASSERT_THROW(SystemToolbox::ReadFileRange(s, path, 3, 4, true), OrthancException); + + SystemToolbox::ReadFileRange(s, path, 0, 2, true); + ASSERT_EQ(2u, s.size()); + ASSERT_EQ('a', s[0]); + ASSERT_EQ('b', s[1]); + + SystemToolbox::ReadFileRange(s, path, 1, 3, true); + ASSERT_EQ(2u, s.size()); + ASSERT_EQ('b', s[0]); + ASSERT_EQ('c', s[1]); + + ASSERT_THROW(SystemToolbox::ReadFileRange(s, path, 2, 4, true), OrthancException); + + SystemToolbox::ReadFileRange(s, path, 0, 3, true); + ASSERT_EQ(3u, s.size()); + ASSERT_EQ('a', s[0]); + ASSERT_EQ('b', s[1]); + ASSERT_EQ('c', s[2]); + + ASSERT_THROW(SystemToolbox::ReadFileRange(s, path, 1, 4, true), OrthancException); +}
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -49,6 +49,7 @@ #endif #include <ctype.h> +#include <boost/thread.hpp> using namespace Orthanc; @@ -368,6 +369,9 @@ ASSERT_STREQ("model/obj", EnumerationToString(SystemToolbox::AutodetectMimeType(".obj"))); ASSERT_STREQ("model/mtl", EnumerationToString(SystemToolbox::AutodetectMimeType(".mtl"))); ASSERT_STREQ("model/stl", EnumerationToString(SystemToolbox::AutodetectMimeType(".stl"))); + + // test with utf8 strings + ASSERT_STREQ("model/stl", EnumerationToString(SystemToolbox::AutodetectMimeType("\xd0\x94.stl"))); } #endif @@ -397,6 +401,25 @@ Toolbox::ComputeMD5(s, set); ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); // set md5 same as string with the values sorted + + { + Toolbox::MD5Context context; + context.Append(""); + context.Append(NULL, 0); + context.Append("Hello"); + context.Export(s); + ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s); + ASSERT_THROW(context.Append("World"), OrthancException); + ASSERT_THROW(context.Export(s), OrthancException); + } + +#if ORTHANC_SANDBOXED != 1 + { + std::istringstream iss(std::string("aaabbbccc")); + SystemToolbox::ComputeStreamMD5(s, iss); + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", s); + } +#endif } TEST(Toolbox, ComputeSHA1) @@ -480,7 +503,7 @@ ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s)); // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C" - std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false); + std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false); ASSERT_EQ(15u, utf8.size()); ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[0])); ASSERT_EQ(0xa0, static_cast<unsigned char>(utf8[1])); @@ -507,8 +530,8 @@ std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char)); - ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false), Encoding_Latin1)); - ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false)); + ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false), Encoding_Latin1)); + ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false, false)); } @@ -619,8 +642,8 @@ #if defined(__linux__) TEST(Toolbox, AbsoluteDirectory) { - ASSERT_EQ("/tmp/hello", SystemToolbox::InterpretRelativePath("/tmp", "hello")); - ASSERT_EQ("/tmp", SystemToolbox::InterpretRelativePath("/tmp", "/tmp")); + ASSERT_EQ("/tmp/hello", SystemToolbox::PathToUtf8(SystemToolbox::InterpretRelativePath("/tmp", "hello"))); + ASSERT_EQ("/tmp", SystemToolbox::PathToUtf8(SystemToolbox::InterpretRelativePath("/tmp", "/tmp"))); } #endif @@ -628,7 +651,7 @@ #if ORTHANC_SANDBOXED != 1 TEST(Toolbox, WriteFile) { - std::string path; + boost::filesystem::path path; { TemporaryFile tmp; @@ -640,33 +663,33 @@ s.append("World"); ASSERT_EQ(11u, s.size()); - SystemToolbox::WriteFile(s, path.c_str()); + SystemToolbox::WriteFile(s, path); std::string t; - SystemToolbox::ReadFile(t, path.c_str()); + SystemToolbox::ReadFile(t, path); ASSERT_EQ(11u, t.size()); ASSERT_EQ(0, t[5]); ASSERT_EQ(0, memcmp(s.c_str(), t.c_str(), s.size())); std::string h; - ASSERT_EQ(true, SystemToolbox::ReadHeader(h, path.c_str(), 1)); + ASSERT_EQ(true, SystemToolbox::ReadHeader(h, path, 1)); ASSERT_EQ(1u, h.size()); ASSERT_EQ('H', h[0]); - ASSERT_TRUE(SystemToolbox::ReadHeader(h, path.c_str(), 0)); + ASSERT_TRUE(SystemToolbox::ReadHeader(h, path, 0)); ASSERT_EQ(0u, h.size()); - ASSERT_FALSE(SystemToolbox::ReadHeader(h, path.c_str(), 32)); + ASSERT_FALSE(SystemToolbox::ReadHeader(h, path, 32)); ASSERT_EQ(11u, h.size()); ASSERT_EQ(0, memcmp(s.c_str(), h.c_str(), s.size())); } std::string u; - ASSERT_THROW(SystemToolbox::ReadFile(u, path.c_str()), OrthancException); + ASSERT_THROW(SystemToolbox::ReadFile(u, path), OrthancException); { TemporaryFile tmp; std::string s = "Hello"; - SystemToolbox::WriteFile(s, tmp.GetPath(), true /* call fsync() */); + SystemToolbox::WriteFile(s.c_str(), s.size(), tmp.GetPath(), true /* call fsync() */); std::string t; SystemToolbox::ReadFile(t, tmp.GetPath()); ASSERT_EQ(s, t); @@ -1259,7 +1282,7 @@ Toolbox::UriEncode(s, t); ASSERT_EQ(t, s); - Toolbox::UriEncode(s, "!#$&'()*+,/:;=?@[]"); ASSERT_EQ("%21%23%24%26%27%28%29%2A%2B%2C/%3A%3B%3D%3F%40%5B%5D", s); + Toolbox::UriEncode(s, "!#$&'()*+,/:;=?@[]"); ASSERT_EQ("%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D", s); Toolbox::UriEncode(s, "%"); ASSERT_EQ("%25", s); // Encode characters from UTF-8. This is the test string from the @@ -1406,6 +1429,25 @@ #if ORTHANC_SANDBOXED != 1 + +void GetValuesDico(std::map<std::string, std::string>& values, MetricsRegistry& m) +{ + values.clear(); + + std::string s; + m.ExportPrometheusText(s); + + std::vector<std::string> t; + Toolbox::TokenizeString(t, s, '\n'); + + for (size_t i = 0; i < t.size() - 1; i++) + { + std::vector<std::string> v; + Toolbox::TokenizeString(v, t[i], ' '); + values[v[0]] = v[1]; + } +} + TEST(MetricsRegistry, Basic) { { @@ -1553,6 +1595,63 @@ ASSERT_EQ(MetricsUpdatePolicy_Directly, m.GetUpdatePolicy("c")); ASSERT_EQ(MetricsDataType_Integer, m.GetDataType("c")); } + + { + std::map<std::string, std::string> values; + + MetricsRegistry mr; + + { + MetricsRegistry::SharedMetrics max10(mr, "shared_max10", MetricsUpdatePolicy_MaxOver10Seconds); + + { + MetricsRegistry::ActiveCounter c1(max10); + MetricsRegistry::ActiveCounter c2(max10); + GetValuesDico(values, mr); + ASSERT_EQ("2", values["shared_max10"]); + } + + GetValuesDico(values, mr); + ASSERT_EQ("2", values["shared_max10"]); + + // { // Uncomment to test max values going back to latest values after expiration of the 10 seconds period + // boost::this_thread::sleep(boost::posix_time::milliseconds(12000)); + + // GetValuesDico(values, mr); + // ASSERT_EQ("0", values["shared_max10"]); + // } + } + + { + MetricsRegistry::SharedMetrics min10(mr, "shared_min10", MetricsUpdatePolicy_MinOver10Seconds); + min10.SetInitialValue(10); + + GetValuesDico(values, mr); + ASSERT_EQ("10", values["shared_min10"]); + + { + MetricsRegistry::AvailableResourcesDecounter c1(min10); + MetricsRegistry::AvailableResourcesDecounter c2(min10); + GetValuesDico(values, mr); + ASSERT_EQ("8", values["shared_min10"]); + } + + GetValuesDico(values, mr); + ASSERT_EQ("8", values["shared_min10"]); + + // { + // // Uncomment to test min values going back to latest values after expiration of the 10 seconds period + // boost::this_thread::sleep(boost::posix_time::milliseconds(12000)); + + // GetValuesDico(values, mr); + // ASSERT_EQ("10", values["shared_min10"]); + // } + + min10.SetInitialValue(5); + GetValuesDico(values, mr); + ASSERT_EQ("5", values["shared_min10"]); + } + } } #endif @@ -1591,6 +1690,47 @@ #endif +#if ORTHANC_SANDBOXED != 1 && ORTHANC_ENABLE_MD5 == 1 +TEST(Toolbox, FileMD5) +{ + { + TemporaryFile tmp1, tmp2; + std::string s = "aaabbbccc"; + + SystemToolbox::WriteFile(s, tmp1.GetPath()); + SystemToolbox::WriteFile(s, tmp2.GetPath()); + + std::string md5; + SystemToolbox::ComputeFileMD5(md5, tmp1.GetPath()); + + ASSERT_EQ("d1aaf4767a3c10a473407a4e47b02da6", md5); + ASSERT_TRUE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // different sizes + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbcccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } + + { // same sizes, different contents + TemporaryFile tmp1, tmp2; + std::string s1 = "aaabbbccc"; + std::string s2 = "aaabbbccd"; + + SystemToolbox::WriteFile(s1, tmp1.GetPath()); + SystemToolbox::WriteFile(s2, tmp2.GetPath()); + + ASSERT_FALSE(SystemToolbox::CompareFilesMD5(tmp1.GetPath(), tmp2.GetPath())); + } +} +#endif + #if ORTHANC_SANDBOXED != 1 TEST(Toolbox, GetMacAddressess) {
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -264,7 +264,7 @@ { std::string source(testEncodingsEncoded[i]); std::string expected(testEncodingsExpected[i]); - std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false); + std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false, false); //std::cout << EnumerationToString(testEncodings[i]) << std::endl; EXPECT_EQ(expected, s); } @@ -334,7 +334,7 @@ ParsedDicomFile f(true); f.SetEncoding(testEncodings[i]); - std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); + std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false, false); f.Insert(DICOM_TAG_PATIENT_NAME, s, false, ""); f.SaveToMemoryBuffer(dicom); } @@ -571,7 +571,7 @@ ASSERT_FALSE(hasCodeExtensions); } - Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false); + Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false, false); f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, ""); Json::Value v; @@ -1172,7 +1172,7 @@ // Sanity check to test the proper behavior of "EncodingTests.py" std::string encoded = Toolbox::ConvertFromUtf8(testEncodingsExpected[i], testEncodings[i]); ASSERT_STREQ(testEncodingsEncoded[i], encoded.c_str()); - std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i], false); + std::string decoded = Toolbox::ConvertToUtf8(encoded, testEncodings[i], false, false); ASSERT_STREQ(testEncodingsExpected[i], decoded.c_str()); if (testEncodings[i] != Encoding_Chinese) @@ -1181,7 +1181,7 @@ // test against Chinese, it is normal that it does not correspond to UTF8 const std::string tmp = Toolbox::ConvertToUtf8( - Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i], false); + Toolbox::ConvertFromUtf8(utf8, testEncodings[i]), testEncodings[i], false, false); ASSERT_STREQ(testEncodingsExpected[i], tmp.c_str()); } } @@ -2260,6 +2260,47 @@ } +TEST(DicomWebJson, SequenceWithEmptyItem) +{ + ParsedDicomFile dicom(false); + + { + std::unique_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_OriginalAttributesSequence)); + + for (unsigned int i = 0; i < 3; i++) + { + std::unique_ptr<DcmItem> item(new DcmItem); + if (i != 1) // the 2nd element is empty + { + std::unique_ptr<DcmSequenceOfItems> subSequence(new DcmSequenceOfItems(DCM_ModifiedAttributesSequence)); + + std::unique_ptr<DcmItem> subItem(new DcmItem); + std::string s = "item" + boost::lexical_cast<std::string>(i); + subItem->putAndInsertString(DCM_AccessionNumber, s.c_str(), OFFalse); + + ASSERT_TRUE(subSequence->append(subItem.release()).good()); + ASSERT_TRUE(item->insert(subSequence.release(), false, false).good()); + } + + ASSERT_TRUE(sequence->append(item.release()).good()); + } + + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->insert(sequence.release(), false, false).good()); + } + + DicomWebJsonVisitor visitor; + dicom.Apply(visitor); // this raised an exception in 1.12.9 + + const Json::Value& output = visitor.GetResult(); + // LOG(INFO) << output.toStyledString(); + + ASSERT_EQ("SQ", output["04000561"]["vr"].asString()); + ASSERT_EQ(3u, output["04000561"]["Value"].size()); + + ASSERT_EQ("item0", output["04000561"]["Value"][0]["04000550"]["Value"][0]["00080050"]["Value"][0].asString()); + ASSERT_EQ("item2", output["04000561"]["Value"][2]["04000550"]["Value"][0]["00080050"]["Value"][0].asString()); +} + TEST(ParsedDicomCache, Basic) { ParsedDicomCache cache(10); @@ -2987,6 +3028,13 @@ } } + virtual Action VisitEmptyElement(const std::vector<DicomTag>& parentTags, + const std::vector<size_t>& parentIndexes) ORTHANC_OVERRIDE + { + return Action_None; + } + + virtual Action VisitBinary(const std::vector<DicomTag>& parentTags, const std::vector<size_t>& parentIndexes, const DicomTag& tag, @@ -3510,12 +3558,12 @@ std::string path = (std::string(getenv("HOME")) + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + std::string(GetTransferSyntaxUid(a)) + ".dcm"); - if (Orthanc::SystemToolbox::IsRegularFile(path)) + if (Orthanc::SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(path))) { printf("\n======= %s\n", GetTransferSyntaxUid(a)); std::string source; - Orthanc::SystemToolbox::ReadFile(source, path); + Orthanc::SystemToolbox::ReadFile(source, Orthanc::SystemToolbox::PathFromUtf8(path)); ParsedDicomFile dicom(source); std::unique_ptr<DcmElement> removal(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData)); @@ -3575,12 +3623,12 @@ std::string path = (std::string(getenv("HOME")) + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + std::string(GetTransferSyntaxUid(a)) + ".dcm"); - if (Orthanc::SystemToolbox::IsRegularFile(path)) + if (Orthanc::SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(path))) { printf("\n======= %s\n", GetTransferSyntaxUid(a)); std::string source; - Orthanc::SystemToolbox::ReadFile(source, path); + Orthanc::SystemToolbox::ReadFile(source, SystemToolbox::PathFromUtf8(path)); std::string c, k; try
--- a/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -94,7 +94,7 @@ ASSERT_DOUBLE_EQ(1.0, info.GetRescaleSlope()); ASSERT_DOUBLE_EQ(0.0, info.GetRescaleIntercept()); ASSERT_EQ(PhotometricInterpretation_Monochrome2, info.GetPhotometricInterpretation()); - ASSERT_EQ(0, info.GetWindowsCount()); + ASSERT_EQ(0u, info.GetWindowsCount()); ASSERT_DOUBLE_EQ(14.0, info.ApplyRescale(14.0)); } @@ -108,7 +108,7 @@ ASSERT_DOUBLE_EQ(-1.75, info.GetRescaleIntercept()); ASSERT_EQ(PhotometricInterpretation_Monochrome1, info.GetPhotometricInterpretation()); ASSERT_FALSE(info.HasWindows()); - ASSERT_EQ(0, info.GetWindowsCount()); + ASSERT_EQ(0u, info.GetWindowsCount()); ASSERT_THROW(info.GetWindow(0), OrthancException); ASSERT_DOUBLE_EQ(141.75, info.ApplyRescale(14.0)); }
--- a/OrthancFramework/UnitTestsSources/ImageTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/ImageTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -77,7 +77,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/ColorPattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/ColorPattern.png")); #endif std::string md5; @@ -137,7 +137,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Color16Pattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Color16Pattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Color16Pattern.png")); #endif std::string md5; @@ -172,7 +172,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray8Pattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray8Pattern.png")); #endif std::string md5; @@ -208,7 +208,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray16Pattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray16Pattern.png")); #endif std::string md5; @@ -310,10 +310,10 @@ #if ORTHANC_SANDBOXED != 1 Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/hello.jpg", img); - Orthanc::SystemToolbox::WriteFile(s, "UnitTestsResults/hello2.jpg"); + Orthanc::SystemToolbox::WriteFile(s, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/hello2.jpg")); std::string t; - Orthanc::SystemToolbox::ReadFile(t, "UnitTestsResults/hello.jpg"); + Orthanc::SystemToolbox::ReadFile(t, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/hello.jpg")); ASSERT_EQ(s.size(), t.size()); ASSERT_EQ(0, memcmp(s.c_str(), t.c_str(), s.size())); #endif @@ -327,7 +327,7 @@ #if ORTHANC_SANDBOXED != 1 Orthanc::JpegReader r2; - r2.ReadFromFile("UnitTestsResults/hello.jpg"); + r2.ReadFromFile(Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/hello.jpg")); ASSERT_EQ(16u, r2.GetWidth()); ASSERT_EQ(16u, r2.GetHeight()); #endif @@ -385,7 +385,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/ColorPattern.pam", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.pam"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/ColorPattern.pam")); #endif std::string md5; @@ -419,7 +419,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray8Pattern.pam", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.pam"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray8Pattern.pam")); #endif std::string md5; @@ -455,7 +455,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray16Pattern.pam", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.pam"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray16Pattern.pam")); #endif std::string md5;
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -137,6 +137,16 @@ { return false; } + + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } };
--- a/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -319,7 +319,7 @@ TEST(MemoryStringCache, Basic) { Orthanc::MemoryStringCache c; - ASSERT_THROW(c.SetMaximumSize(0), Orthanc::OrthancException); + ASSERT_NO_THROW(c.SetMaximumSize(0)); // changed in 1.12.10, setting the MaximumSize to zero is a way to disable a cache c.SetMaximumSize(3);
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -38,6 +38,7 @@ #include "../Sources/OrthancException.h" #include "../Sources/RestApi/RestApiHierarchy.h" #include "../Sources/WebServiceParameters.h" +#include "../Sources/MetricsRegistry.h" #include <ctype.h> #include <boost/lexical_cast.hpp> @@ -116,7 +117,7 @@ TEST(HttpClient, Ssl) { - SystemToolbox::WriteFile(GITHUB_CERTIFICATES, "UnitTestsResults/github.cert"); + SystemToolbox::WriteFile(GITHUB_CERTIFICATES, SystemToolbox::PathFromUtf8("UnitTestsResults/github.cert")); /*{ std::string s; @@ -1289,7 +1290,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE { return false; } @@ -1303,7 +1305,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE { printf("received %d\n", static_cast<int>(bodySize)); @@ -1330,7 +1333,6 @@ TEST(HttpClient, DISABLED_Issue156_Slow) { // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=156 - TotoServer handler; HttpServer server; server.SetPortNumber(5000); @@ -1384,3 +1386,14 @@ server.Stop(); } #endif + + +TEST(HttpServer, GetRelativePathToRoot) +{ + ASSERT_THROW(HttpServer::GetRelativePathToRoot(""), OrthancException); + ASSERT_EQ("./", HttpServer::GetRelativePathToRoot("/")); + ASSERT_EQ("./", HttpServer::GetRelativePathToRoot("/system")); + ASSERT_EQ("../", HttpServer::GetRelativePathToRoot("/system/")); + ASSERT_EQ("./../../", HttpServer::GetRelativePathToRoot("/a/b/system")); + ASSERT_EQ("../../../", HttpServer::GetRelativePathToRoot("/a/b/system/")); +}
--- a/OrthancFramework/UnitTestsSources/SQLiteTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/SQLiteTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -54,7 +54,7 @@ TEST(SQLite, Connection) { - SystemToolbox::RemoveFile("UnitTestsResults/coucou"); + SystemToolbox::RemoveFile(SystemToolbox::PathFromUtf8("UnitTestsResults/coucou")); SQLite::Connection c; c.Open("UnitTestsResults/coucou"); c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)");
--- a/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/SharedLibraryUnitTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -43,7 +43,7 @@ Orthanc::Logging::EnableInfoLevel(true); Orthanc::Toolbox::DetectEndianness(); - Orthanc::SystemToolbox::MakeDirectory("UnitTestsResults"); + Orthanc::SystemToolbox::MakeDirectory(Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults")); ::testing::InitGoogleTest(&argc, argv); int result = RUN_ALL_TESTS();
--- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -407,4 +407,32 @@ ASSERT_EQ("8.59Gbps", Toolbox::GetHumanTransferSpeed(false, 1024*1024*1024, 1000000000)); ASSERT_EQ("1.00GB in 1.00s = 8.59Gbps", Toolbox::GetHumanTransferSpeed(true, 1024*1024*1024, 1000000000)); ASSERT_EQ("976.56KB in 1.00s = 8.00Mbps", Toolbox::GetHumanTransferSpeed(true, 1000*1000, 1000000000)); -} \ No newline at end of file +} + +TEST(Toolbox, DISABLED_JapaneseBackslashes) +{ + std::string s = Orthanc::Toolbox::ConvertToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, false); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); // NB: The Yen symbol is encoded as 0xC2 0xA5 in UTF-8 + + s = Orthanc::Toolbox::ConvertToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, true); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_PersonName); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); + + // Backslashes should only be interpreted as the Yen symbol if VR is ST, LT, or UL + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_ShortText); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_LongText); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Japanese, false, ValueRepresentation_UnlimitedText); + ASSERT_EQ("ORIGINAL\302\245PRIMARY", s); + + s = Orthanc::Toolbox::ConvertToUtf8("ORIGINAL\\PRIMARY", Encoding_Latin1, false, false); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); + + s = Orthanc::Toolbox::ConvertDicomStringToUtf8("ORIGINAL\\PRIMARY", Encoding_Latin1, false, ValueRepresentation_ShortText); + ASSERT_EQ("ORIGINAL\\PRIMARY", s); +}
--- a/OrthancFramework/UnitTestsSources/ZipTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancFramework/UnitTestsSources/ZipTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -194,7 +194,7 @@ Orthanc::ZipWriter w; ASSERT_EQ(0u, w.GetArchiveSize()); - w.SetOutputPath(f.GetPath().c_str()); + w.SetOutputPath(f.GetPath()); w.Open(); w.OpenFile("world/hello"); w.Write("Hello world"); @@ -207,7 +207,7 @@ std::unique_ptr<ZipReader> reader(ZipReader::CreateFromFile(f.GetPath())); ASSERT_EQ(1u, reader->GetFilesCount()); - + std::string filename, content; ASSERT_TRUE(reader->ReadNextFile(filename, content)); ASSERT_EQ("world/hello", filename);
--- a/OrthancServer/CMakeLists.txt Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/CMakeLists.txt Tue Nov 04 15:58:06 2025 +0100 @@ -63,6 +63,7 @@ SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin") SET(BUILD_DELAYED_DELETION ON CACHE BOOL "Whether to build the DelayedDeletion plugin") SET(BUILD_MULTITENANT_DICOM ON CACHE BOOL "Whether to build the MultitenantDicom plugin") +SET(BUILD_UNIT_TESTS ON CACHE BOOL "Whether to build the unit tests (new in Orthanc 1.12.9)") SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") @@ -76,6 +77,10 @@ set(ENABLE_PROTOBUF_COMPILER ON) endif() +if (NOT BUILD_UNIT_TESTS) + set(ENABLE_GOOGLE_TEST OFF) +endif() + include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake) include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) @@ -162,37 +167,39 @@ ) -set(ORTHANC_FRAMEWORK_UNIT_TESTS - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/DicomMapTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FileStorageTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FrameworkTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JobsTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LoggingTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LuaTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/RestApiTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/StreamTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ToolboxTests.cpp - ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ZipTests.cpp - ) +if (BUILD_UNIT_TESTS) + set(ORTHANC_FRAMEWORK_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/DicomMapTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FileStorageTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FrameworkTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ImageTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JobsTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LoggingTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/LuaTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/MemoryCacheTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/RestApiTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/SQLiteTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/StreamTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ToolboxTests.cpp + ${CMAKE_SOURCE_DIR}/../OrthancFramework/UnitTestsSources/ZipTests.cpp + ) -set(ORTHANC_SERVER_UNIT_TESTS - ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/UnitTestsMain.cpp - ${CMAKE_SOURCE_DIR}/UnitTestsSources/VersionsTests.cpp - ) + set(ORTHANC_SERVER_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/UnitTestsMain.cpp + ${CMAKE_SOURCE_DIR}/UnitTestsSources/VersionsTests.cpp + ) +endif() if (ENABLE_PLUGINS) @@ -203,15 +210,19 @@ ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV3.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPluginDatabaseV4.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/OrthancPlugins.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer32.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginMemoryBuffer64.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsEnumerations.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsErrorDictionary.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsJob.cpp ${CMAKE_SOURCE_DIR}/Plugins/Engine/PluginsManager.cpp ) - list(APPEND ORTHANC_SERVER_UNIT_TESTS - ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp - ) + if (BUILD_UNIT_TESTS) + list(APPEND ORTHANC_SERVER_UNIT_TESTS + ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp + ) + endif() endif() @@ -243,15 +254,18 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json - LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua - PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql - INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql - INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_SOURCE_DIR}/Sources/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_SOURCE_DIR}/Sources/Database/Upgrade4To5.sql + INSTALL_TRACK_ATTACHMENTS_SIZE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallTrackAttachmentsSize.sql + INSTALL_LABELS_TABLE ${CMAKE_SOURCE_DIR}/Sources/Database/InstallLabelsTable.sql + INSTALL_REVISION_AND_CUSTOM_DATA ${CMAKE_SOURCE_DIR}/Sources/Database/InstallRevisionAndCustomData.sql + INSTALL_DELETED_FILES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallDeletedFiles.sql + INSTALL_KEY_VALUE_STORES_AND_QUEUES ${CMAKE_SOURCE_DIR}/Sources/Database/InstallKeyValueStoresAndQueues.sql ) if (STANDALONE_BUILD) @@ -329,16 +343,19 @@ endif() -if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) - add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) +if (BUILD_UNIT_TESTS) + add_definitions(-DORTHANC_BUILD_UNIT_TESTS=1) + if (UNIT_TESTS_WITH_HTTP_CONNEXIONS) + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=1) + else() + add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) + endif() else() - add_definitions(-DUNIT_TESTS_WITH_HTTP_CONNEXIONS=0) + add_definitions(-DORTHANC_BUILD_UNIT_TESTS=1) endif() add_definitions( - -DORTHANC_BUILD_UNIT_TESTS=1 - # Macros for the plugins -DHAS_ORTHANC_EXCEPTION=0 ) @@ -366,9 +383,11 @@ "PrecompiledHeadersServer.h" "${CMAKE_SOURCE_DIR}/Sources/PrecompiledHeadersServer.cpp" ORTHANC_SERVER_SOURCES ORTHANC_SERVER_PCH) - ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( - "PrecompiledHeadersUnitTests.h" "${CMAKE_SOURCE_DIR}/UnitTestsSources/PrecompiledHeadersUnitTests.cpp" - ORTHANC_SERVER_UNIT_TESTS ORTHANC_UNIT_TESTS_PCH) + if (BUILD_UNIT_TESTS) + ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS( + "PrecompiledHeadersUnitTests.h" "${CMAKE_SOURCE_DIR}/UnitTestsSources/PrecompiledHeadersUnitTests.cpp" + ORTHANC_SERVER_UNIT_TESTS ORTHANC_UNIT_TESTS_PCH) + endif() endif() @@ -472,22 +491,24 @@ ## Build the unit tests ##################################################################### -add_executable(UnitTests - ${GOOGLE_TEST_SOURCES} - ${ORTHANC_UNIT_TESTS_PCH} - ${ORTHANC_FRAMEWORK_UNIT_TESTS} - ${ORTHANC_SERVER_UNIT_TESTS} - ${BOOST_EXTENDED_SOURCES} - ) +if (BUILD_UNIT_TESTS) + add_executable(UnitTests + ${GOOGLE_TEST_SOURCES} + ${ORTHANC_UNIT_TESTS_PCH} + ${ORTHANC_FRAMEWORK_UNIT_TESTS} + ${ORTHANC_SERVER_UNIT_TESTS} + ${BOOST_EXTENDED_SOURCES} + ) -DefineSourceBasenameForTarget(UnitTests) + DefineSourceBasenameForTarget(UnitTests) -target_link_libraries(UnitTests - ServerLibrary - CoreLibrary - ${DCMTK_LIBRARIES} - ${GOOGLE_TEST_LIBRARIES} - ) + target_link_libraries(UnitTests + ServerLibrary + CoreLibrary + ${DCMTK_LIBRARIES} + ${GOOGLE_TEST_LIBRARIES} + ) +endif() ##################################################################### @@ -571,6 +592,7 @@ add_library(ServeFolders SHARED ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/Plugin.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/OrthancFrameworkDependencies.cpp ${SERVE_FOLDERS_RESOURCES} ) @@ -614,6 +636,14 @@ list(APPEND MODALITY_WORKLISTS_RESOURCES ${AUTOGENERATED_DIR}/ModalityWorklists.rc) endif() + EmbedResources( + --target=ModalityWorklistsResources + --namespace=Orthanc.FrameworkResources + --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources + ${LIBICU_RESOURCES} + ${DCMTK_DICTIONARIES} + ) + set_source_files_properties( ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp PROPERTIES COMPILE_DEFINITIONS "MODALITY_WORKLISTS_VERSION=\"${ORTHANC_VERSION}\"" @@ -621,12 +651,14 @@ add_library(ModalityWorklists SHARED ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/OrthancFrameworkDependencies.cpp + ${AUTOGENERATED_DIR}/ModalityWorklistsResources.cpp ${MODALITY_WORKLISTS_RESOURCES} ) DefineSourceBasenameForTarget(ModalityWorklists) - target_link_libraries(ModalityWorklists PluginsDependencies) + target_link_libraries(ModalityWorklists PluginsDependencies ${DCMTK_LIBRARIES}) set_target_properties( ModalityWorklists PROPERTIES @@ -963,9 +995,10 @@ if (ENABLE_PLUGINS) install( FILES + ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCDatabasePlugin.h ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCPlugin.h - ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancCDatabasePlugin.h ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancDatabasePlugin.proto + ${CMAKE_SOURCE_DIR}/Plugins/Include/orthanc/OrthancPluginCodeModel.json DESTINATION include/orthanc ) endif()
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -1448,6 +1448,69 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1620,7 +1683,7 @@ void OrthancPluginDatabase::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Tue Nov 04 15:58:06 2025 +0100 @@ -103,7 +103,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -677,7 +677,6 @@ } } - virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1061,6 +1060,69 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_InternalError); // Not supported + } + + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } }; @@ -1231,7 +1293,7 @@ void OrthancPluginDatabaseV3::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { VoidDatabaseListener listener;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Tue Nov 04 15:58:06 2025 +0100 @@ -76,7 +76,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -41,7 +41,7 @@ #include "OrthancDatabasePlugin.pb.h" // Auto-generated file #include <cassert> - +#include <limits> namespace Orthanc { @@ -100,15 +100,17 @@ } - static FileInfo Convert(const DatabasePluginMessages::FileInfo& source) + static void Convert(FileInfo& info, + const DatabasePluginMessages::FileInfo& source) { - return FileInfo(source.uuid(), + info = FileInfo(source.uuid(), static_cast<FileContentType>(source.content_type()), source.uncompressed_size(), source.uncompressed_hash(), static_cast<CompressionType>(source.compression_type()), source.compressed_size(), source.compressed_hash()); + info.SetCustomData(source.custom_data()); } @@ -576,6 +578,7 @@ request.mutable_add_attachment()->mutable_attachment()->set_compression_type(attachment.GetCompressionType()); request.mutable_add_attachment()->mutable_attachment()->set_compressed_size(attachment.GetCompressedSize()); request.mutable_add_attachment()->mutable_attachment()->set_compressed_hash(attachment.GetCompressedMD5()); + request.mutable_add_attachment()->mutable_attachment()->set_custom_data(attachment.GetCustomData()); // New in 1.12.8 request.mutable_add_attachment()->set_revision(revision); ExecuteTransaction(DatabasePluginMessages::OPERATION_ADD_ATTACHMENT, request); @@ -604,7 +607,9 @@ DatabasePluginMessages::TransactionResponse response; ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DELETE_ATTACHMENT, request); - listener_.SignalAttachmentDeleted(Convert(response.delete_attachment().deleted_attachment())); + FileInfo info; + Convert(info, response.delete_attachment().deleted_attachment()); + listener_.SignalAttachmentDeleted(info); } @@ -629,7 +634,9 @@ for (int i = 0; i < response.delete_resource().deleted_attachments().size(); i++) { - listener_.SignalAttachmentDeleted(Convert(response.delete_resource().deleted_attachments(i))); + FileInfo info; + Convert(info, response.delete_resource().deleted_attachments(i)); + listener_.SignalAttachmentDeleted(info); } for (int i = 0; i < response.delete_resource().deleted_resources().size(); i++) @@ -1006,7 +1013,7 @@ if (response.lookup_attachment().found()) { - attachment = Convert(response.lookup_attachment().attachment()); + Convert(attachment, response.lookup_attachment().attachment()); revision = response.lookup_attachment().revision(); return true; } @@ -1016,7 +1023,48 @@ } } - + + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_attachment_custom_data()->set_uuid(attachmentUuid); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_ATTACHMENT_CUSTOM_DATA, request); + + customData = response.get_attachment_custom_data().custom_data(); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_set_attachment_custom_data()->set_uuid(attachmentUuid); + request.mutable_set_attachment_custom_data()->set_custom_data(customData, customDataSize); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_SET_ATTACHMENT_CUSTOM_DATA, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) ORTHANC_OVERRIDE @@ -1686,7 +1734,9 @@ for (int j = 0; j < source.attachments().size(); j++) { - target->AddAttachment(Convert(source.attachments(j)), source.attachments_revisions(j)); + FileInfo info; + Convert(info, source.attachments(j)); + target->AddAttachment(info, source.attachments_revisions(j)); } Convert(*target, ResourceType_Patient, source.patient_content()); @@ -1748,7 +1798,8 @@ for (int j = 0; j < source.one_instance_attachments().size(); j++) { - FileInfo info(Convert(source.one_instance_attachments(j))); + FileInfo info; + Convert(info, source.one_instance_attachments(j)); if (attachments.find(info.GetContentType()) == attachments.end()) { attachments[info.GetContentType()] = info; @@ -1805,6 +1856,201 @@ find.ExecuteExpand(response, capabilities, request, identifier); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32" + // https://protobuf.dev/programming-guides/proto3/ + if (valueSize > std::numeric_limits<uint32_t>::max()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_store_key_value()->set_store_id(storeId); + request.mutable_store_key_value()->set_key(key); + request.mutable_store_key_value()->set_value(value, valueSize); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_STORE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_delete_key_value()->set_store_id(storeId); + request.mutable_delete_key_value()->set_key(key); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_DELETE_KEY_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_key_value()->set_store_id(storeId); + request.mutable_get_key_value()->set_key(key); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_KEY_VALUE, request); + + if (response.get_key_value().found()) + { + value = response.get_key_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool fromFirst, + const std::string& fromKey, + uint64_t limit) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasKeyValueStoresSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_list_keys_values()->set_store_id(storeId); + request.mutable_list_keys_values()->set_from_first(fromFirst); + request.mutable_list_keys_values()->set_from_key(fromKey); + request.mutable_list_keys_values()->set_limit(limit); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LIST_KEY_VALUES, request); + + for (int i = 0; i < response.list_keys_values().keys_values_size(); ++i) + { + keys.push_back(response.list_keys_values().keys_values(i).key()); + values.push_back(response.list_keys_values().keys_values(i).value()); + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + // In protobuf, bytes "may contain any arbitrary sequence of bytes no longer than 2^32" + // https://protobuf.dev/programming-guides/proto3/ + if (valueSize > std::numeric_limits<uint32_t>::max()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_enqueue_value()->set_queue_id(queueId); + request.mutable_enqueue_value()->set_value(value, valueSize); + + ExecuteTransaction(DatabasePluginMessages::OPERATION_ENQUEUE_VALUE, request); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_dequeue_value()->set_queue_id(queueId); + + switch (origin) + { + case QueueOrigin_Back: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_BACK); + break; + + case QueueOrigin_Front: + request.mutable_dequeue_value()->set_origin(DatabasePluginMessages::QUEUE_ORIGIN_FRONT); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_DEQUEUE_VALUE, request); + + if (response.dequeue_value().found()) + { + value = response.dequeue_value().value(); + return true; + } + else + { + return false; + } + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } + + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + if (database_.GetDatabaseCapabilities().HasQueuesSupport()) + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_get_queue_size()->set_queue_id(queueId); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_GET_QUEUE_SIZE, request); + + return response.get_queue_size().size(); + } + else + { + // This method shouldn't have been called + throw OrthancException(ErrorCode_InternalError); + } + } }; @@ -1895,6 +2141,9 @@ dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency()); dbCapabilities_.SetHasExtendedChanges(systemInfo.has_extended_changes()); dbCapabilities_.SetHasFindSupport(systemInfo.supports_find()); + dbCapabilities_.SetKeyValueStoresSupport(systemInfo.supports_key_value_stores()); + dbCapabilities_.SetQueuesSupport(systemInfo.supports_queues()); + dbCapabilities_.SetAttachmentCustomDataSupport(systemInfo.has_attachment_custom_data()); } open_ = true; @@ -1961,7 +2210,7 @@ void OrthancPluginDatabaseV4::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { if (!open_) {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Nov 04 15:58:06 2025 +0100 @@ -88,7 +88,7 @@ virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -39,7 +39,7 @@ #include "../../../OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h" #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" -#include "../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h" +#include "../../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../../OrthancFramework/Sources/HttpServer/HttpToolbox.h" #include "../../../OrthancFramework/Sources/Images/Image.h" @@ -54,7 +54,6 @@ #include "../../../OrthancFramework/Sources/MetricsRegistry.h" #include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" -#include "../../../OrthancFramework/Sources/StringMemoryBuffer.h" #include "../../../OrthancFramework/Sources/Toolbox.h" #include "../../Sources/Database/VoidDatabaseListener.h" #include "../../Sources/OrthancConfiguration.h" @@ -65,12 +64,12 @@ #include "OrthancPluginDatabase.h" #include "OrthancPluginDatabaseV3.h" #include "OrthancPluginDatabaseV4.h" +#include "PluginMemoryBuffer32.h" #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> #include <dcmtk/dcmnet/dimse.h> @@ -79,6 +78,125 @@ namespace Orthanc { + class OrthancPlugins::IDicomInstance : public boost::noncopyable + { + public: + virtual ~IDicomInstance() + { + } + + virtual bool CanBeFreed() const = 0; + + virtual const DicomInstanceToStore& GetInstance() const = 0; + }; + + + class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance + { + private: + const DicomInstanceToStore& instance_; + + public: + explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) : + instance_(instance) + { + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return false; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance + { + private: + std::string buffer_; + std::unique_ptr<DicomInstanceToStore> instance_; + + void Setup(const void* buffer, + size_t size) + { + buffer_.assign(reinterpret_cast<const char*>(buffer), size); + + instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_)); + 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; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance + { + private: + std::unique_ptr<ParsedDicomFile> parsed_; + std::unique_ptr<DicomInstanceToStore> instance_; + + void Setup(ParsedDicomFile* parsed) + { + parsed_.reset(parsed); + + if (parsed_.get() == NULL) + { + 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 + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return *instance_; + }; + }; + + class OrthancPlugins::WebDavCollection : public IWebDavBucket { private: @@ -149,7 +267,7 @@ if (mimeType == NULL || std::string(mimeType).empty()) { - f->SetMimeType(SystemToolbox::AutodetectMimeType(displayName)); + f->SetMimeType(SystemToolbox::AutodetectMimeType(std::string(displayName))); } else { @@ -417,78 +535,45 @@ }; - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer* target, const void* data, size_t size) { - if (static_cast<uint32_t>(size) != size) - { - throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT); - } - - target.size = size; - - if (size == 0) - { - target.data = NULL; - } - else - { - target.data = malloc(size); - if (target.data != NULL) - { - memcpy(target.data, data, size); - } - else - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - } + PluginMemoryBuffer32 buffer; + buffer.Assign(data, size); + buffer.Release(target); } - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer* target, const std::string& str) { - if (str.size() == 0) - { - target.size = 0; - target.data = NULL; - } - else - { - CopyToMemoryBuffer(target, str.c_str(), str.size()); - } + PluginMemoryBuffer32 buffer; + buffer.Assign(str); + buffer.Release(target); } static char* CopyString(const std::string& str) { - if (static_cast<uint32_t>(str.size()) != str.size()) - { - throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT); - } - char *result = reinterpret_cast<char*>(malloc(str.size() + 1)); if (result == NULL) { throw OrthancException(ErrorCode_NotEnoughMemory); } - if (str.size() == 0) - { - result[0] = '\0'; - } - else - { - memcpy(result, &str[0], str.size() + 1); - } + if (!str.empty()) + { + memcpy(result, str.c_str(), str.size()); + } + + result[str.size()] = '\0'; // Add the null terminator of the string return result; } - static void CopyDictionary(OrthancPluginMemoryBuffer& target, + static void CopyDictionary(PluginMemoryBuffer32& target, const std::map<std::string, std::string>& dictionary) { Json::Value json = Json::objectValue; @@ -499,59 +584,49 @@ json[it->first] = it->second; } - std::string s = json.toStyledString(); - CopyToMemoryBuffer(target, s); + target.Assign(json.toStyledString()); } namespace { - class MemoryBufferRaii : public boost::noncopyable - { - private: - OrthancPluginMemoryBuffer buffer_; - - public: - MemoryBufferRaii() - { - buffer_.size = 0; - buffer_.data = NULL; - } - - ~MemoryBufferRaii() - { - if (buffer_.size != 0) - { - free(buffer_.data); - } - } - - OrthancPluginMemoryBuffer* GetObject() - { - return &buffer_; - } - - void ToString(std::string& target) const - { - if ((buffer_.data == NULL && buffer_.size != 0) || - (buffer_.data != NULL && buffer_.size == 0)) - { - throw OrthancException(ErrorCode_Plugin); + static IMemoryBuffer* GetRangeFromWhole(std::unique_ptr<IMemoryBuffer>& whole, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */) + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new PluginMemoryBuffer64; // Empty + } + else + { + if (start == 0 && + end == whole->GetSize()) + { + return whole.release(); + } + else if (end > whole->GetSize()) + { + throw OrthancException(ErrorCode_BadRange); } else { - target.resize(buffer_.size); - - if (buffer_.size != 0) - { - memcpy(&target[0], buffer_.data, buffer_.size); - } - } - } - }; - - - class StorageAreaBase : public IStorageArea + std::unique_ptr<PluginMemoryBuffer64> range(new PluginMemoryBuffer64); + range->Assign(reinterpret_cast<const char*>(whole->GetData()) + start, end - start); + assert(range->GetSize() > 0); + + return range.release(); + } + } + } + + + // "legacy" storage plugins don't store customData -> derive from IStorageArea + class StorageAreaWithoutCustomData : public IStorageArea { private: OrthancPluginStorageCreate create_; @@ -564,50 +639,10 @@ return errorDictionary_; } - IMemoryBuffer* RangeFromWhole(const std::string& uuid, - FileContentType type, - uint64_t start /* inclusive */, - uint64_t end /* exclusive */) - { - if (start > end) - { - throw OrthancException(ErrorCode_BadRange); - } - else if (start == end) - { - return new StringMemoryBuffer; // Empty - } - else - { - std::unique_ptr<IMemoryBuffer> whole(Read(uuid, type)); - - if (start == 0 && - end == whole->GetSize()) - { - return whole.release(); - } - else if (end > whole->GetSize()) - { - throw OrthancException(ErrorCode_BadRange); - } - else - { - std::string range; - range.resize(end - start); - assert(!range.empty()); - - memcpy(&range[0], reinterpret_cast<const char*>(whole->GetData()) + start, range.size()); - - whole.reset(NULL); - return StringMemoryBuffer::CreateFromSwap(range); - } - } - } - public: - StorageAreaBase(OrthancPluginStorageCreate create, - OrthancPluginStorageRemove remove, - PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(OrthancPluginStorageCreate create, + OrthancPluginStorageRemove remove, + PluginsErrorDictionary& errorDictionary) : create_(create), remove_(remove), errorDictionary_(errorDictionary) @@ -649,24 +684,16 @@ }; - class PluginStorageArea : public StorageAreaBase + class PluginStorageAreaV1 : public StorageAreaWithoutCustomData { private: OrthancPluginStorageRead read_; OrthancPluginFree free_; - void Free(void* buffer) const - { - if (buffer != NULL) - { - free_(buffer); - } - } - public: - PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks, - PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaV1(const _OrthancPluginRegisterStorageArea& callbacks, + PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary), read_(callbacks.read), free_(callbacks.free) { @@ -676,38 +703,34 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); - - void* buffer = NULL; - int64_t size = 0; - - OrthancPluginErrorCode error = read_ - (&buffer, &size, uuid.c_str(), Plugins::Convert(type)); - - if (error == OrthancPluginErrorCode_Success) - { - result->Assign(buffer, size, free_); - return result.release(); - } - else - { - GetErrorDictionary().LogError(error, true); - throw OrthancException(static_cast<ErrorCode>(error)); - } - } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, uint64_t end /* exclusive */) ORTHANC_OVERRIDE { - return RangeFromWhole(uuid, type, start, end); - } - - virtual bool HasReadRange() const ORTHANC_OVERRIDE + std::unique_ptr<IMemoryBuffer> whole(new MallocMemoryBuffer); + + void* buffer = NULL; + int64_t size = 0; + + OrthancPluginErrorCode error = read_(&buffer, &size, uuid.c_str(), Plugins::Convert(type)); + + if (error == OrthancPluginErrorCode_Success) + { + // Beware that the buffer must be unallocated by the "free_" function provided by the plugin, + // so we cannot use "PluginMemoryBuffer64" + dynamic_cast<MallocMemoryBuffer&>(*whole).Assign(buffer, size, free_); + + return GetRangeFromWhole(whole, start, end); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { return false; } @@ -715,16 +738,16 @@ // New in Orthanc 1.9.0 - class PluginStorageArea2 : public StorageAreaBase + class PluginStorageAreaV2 : public StorageAreaWithoutCustomData { private: OrthancPluginStorageReadWhole readWhole_; OrthancPluginStorageReadRange readRange_; public: - PluginStorageArea2(const _OrthancPluginRegisterStorageArea2& callbacks, - PluginsErrorDictionary& errorDictionary) : - StorageAreaBase(callbacks.create, callbacks.remove, errorDictionary), + PluginStorageAreaV2(const _OrthancPluginRegisterStorageArea2& callbacks, + PluginsErrorDictionary& errorDictionary) : + StorageAreaWithoutCustomData(callbacks.create, callbacks.remove, errorDictionary), readWhole_(callbacks.readWhole), readRange_(callbacks.readRange) { @@ -734,29 +757,6 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - std::unique_ptr<MallocMemoryBuffer> result(new MallocMemoryBuffer); - - OrthancPluginMemoryBuffer64 buffer; - buffer.size = 0; - buffer.data = NULL; - - OrthancPluginErrorCode error = readWhole_(&buffer, uuid.c_str(), Plugins::Convert(type)); - - if (error == OrthancPluginErrorCode_Success) - { - result->Assign(buffer.data, buffer.size, ::free); - return result.release(); - } - else - { - GetErrorDictionary().LogError(error, true); - throw OrthancException(static_cast<ErrorCode>(error)); - } - } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -764,34 +764,44 @@ { if (readRange_ == NULL) { - return RangeFromWhole(uuid, type, start, end); + std::unique_ptr<IMemoryBuffer> whole(new PluginMemoryBuffer64); + + OrthancPluginErrorCode error = readWhole_(dynamic_cast<PluginMemoryBuffer64&>(*whole).GetObject(), + uuid.c_str(), Plugins::Convert(type)); + + if (error == OrthancPluginErrorCode_Success) + { + return GetRangeFromWhole(whole, start, end); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } } else { + std::unique_ptr<PluginMemoryBuffer64> buffer(new PluginMemoryBuffer64); + if (start > end) { throw OrthancException(ErrorCode_BadRange); } else if (start == end) { - return new StringMemoryBuffer; + return buffer.release(); } else { - std::string range; - range.resize(end - start); - assert(!range.empty()); - - OrthancPluginMemoryBuffer64 buffer; - buffer.data = &range[0]; - buffer.size = static_cast<uint64_t>(range.size()); + buffer->Resize(end - start); + assert(buffer->GetSize() > 0); OrthancPluginErrorCode error = - readRange_(&buffer, uuid.c_str(), Plugins::Convert(type), start); + readRange_(buffer->GetObject(), uuid.c_str(), Plugins::Convert(type), start); if (error == OrthancPluginErrorCode_Success) { - return StringMemoryBuffer::CreateFromSwap(range); + return buffer.release(); } else { @@ -802,26 +812,148 @@ } } - virtual bool HasReadRange() const ORTHANC_OVERRIDE + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { return (readRange_ != NULL); } }; + // New in Orthanc 1.12.8 + class PluginStorageAreaV3 : public IPluginStorageArea + { + private: + OrthancPluginStorageCreate2 create_; + OrthancPluginStorageReadRange2 readRange_; + OrthancPluginStorageRemove2 remove_; + PluginsErrorDictionary& errorDictionary_; + + protected: + PluginsErrorDictionary& GetErrorDictionary() const + { + return errorDictionary_; + } + + public: + PluginStorageAreaV3(const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + create_(callbacks.create), + readRange_(callbacks.readRange), + remove_(callbacks.remove), + errorDictionary_(errorDictionary) + { + if (create_ == NULL || + readRange_ == NULL || + remove_ == NULL) + { + throw OrthancException(ErrorCode_Plugin, "Storage area plugin does not implement all the required primitives (create, remove, and readRange)"); + } + } + + virtual void Create(std::string& customData /* out */, + const std::string& uuid, + const void* content, + size_t size, + FileContentType type, + CompressionType compression, + const DicomInstanceToStore* dicomInstance /* can be NULL if not a DICOM instance */) ORTHANC_OVERRIDE + { + PluginMemoryBuffer32 customDataBuffer; + OrthancPluginErrorCode error; + + if (dicomInstance != NULL) + { + Orthanc::OrthancPlugins::DicomInstanceFromCallback wrapped(*dicomInstance); + error = create_(customDataBuffer.GetObject(), uuid.c_str(), content, size, Plugins::Convert(type), Plugins::Convert(compression), + reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped)); + } + else + { + error = create_(customDataBuffer.GetObject(), uuid.c_str(), content, size, Plugins::Convert(type), Plugins::Convert(compression), NULL); + } + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + else + { + customDataBuffer.MoveToString(customData); + } + } + + virtual void Remove(const std::string& uuid, + FileContentType type, + const std::string& customData) ORTHANC_OVERRIDE + { + OrthancPluginErrorCode error = remove_(uuid.c_str(), Plugins::Convert(type), + customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (error != OrthancPluginErrorCode_Success) + { + errorDictionary_.LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + + virtual IMemoryBuffer* ReadRange(const std::string& uuid, + FileContentType type, + uint64_t start /* inclusive */, + uint64_t end /* exclusive */, + const std::string& customData) ORTHANC_OVERRIDE + { + if (start > end) + { + throw OrthancException(ErrorCode_BadRange); + } + else if (start == end) + { + return new PluginMemoryBuffer64; + } + else + { + std::unique_ptr<PluginMemoryBuffer64> buffer(new PluginMemoryBuffer64); + buffer->Resize(end - start); + assert(buffer->GetSize() > 0); + + OrthancPluginErrorCode error = + readRange_(buffer->GetObject(), uuid.c_str(), Plugins::Convert(type), start, customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (error == OrthancPluginErrorCode_Success) + { + return buffer.release(); + } + else + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + } + + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE + { + return true; + } + }; + + class StorageAreaFactory : public boost::noncopyable { private: enum Version { Version1, - Version2 + Version2, + Version3 }; SharedLibrary& sharedLibrary_; Version version_; - _OrthancPluginRegisterStorageArea callbacks_; + _OrthancPluginRegisterStorageArea callbacks1_; _OrthancPluginRegisterStorageArea2 callbacks2_; + _OrthancPluginRegisterStorageArea3 callbacks3_; PluginsErrorDictionary& errorDictionary_; static void WarnNoReadRange() @@ -835,7 +967,7 @@ PluginsErrorDictionary& errorDictionary) : sharedLibrary_(sharedLibrary), version_(Version1), - callbacks_(callbacks), + callbacks1_(callbacks), errorDictionary_(errorDictionary) { WarnNoReadRange(); @@ -855,20 +987,37 @@ } } + StorageAreaFactory(SharedLibrary& sharedLibrary, + const _OrthancPluginRegisterStorageArea3& callbacks, + PluginsErrorDictionary& errorDictionary) : + sharedLibrary_(sharedLibrary), + version_(Version3), + callbacks3_(callbacks), + errorDictionary_(errorDictionary) + { + if (callbacks.readRange == NULL) + { + WarnNoReadRange(); + } + } + SharedLibrary& GetSharedLibrary() { return sharedLibrary_; } - IStorageArea* Create() const + IPluginStorageArea* Create() const { switch (version_) { case Version1: - return new PluginStorageArea(callbacks_, errorDictionary_); + return new PluginStorageAreaAdapter(new PluginStorageAreaV1(callbacks1_, errorDictionary_)); case Version2: - return new PluginStorageArea2(callbacks2_, errorDictionary_); + return new PluginStorageAreaAdapter(new PluginStorageAreaV2(callbacks2_, errorDictionary_)); + + case Version3: + return new PluginStorageAreaV3(callbacks3_, errorDictionary_); default: throw OrthancException(ErrorCode_InternalError); @@ -1567,6 +1716,7 @@ typedef std::list<StorageCommitmentScp*> StorageCommitmentScpCallbacks; typedef std::map<Property, std::string> Properties; typedef std::list<WebDavCollection*> WebDavCollections; + typedef std::list<OrthancPluginAuditLogHandler> AuditLogHandlers; PluginsManager manager_; @@ -1590,6 +1740,8 @@ WebDavCollections webDavCollections_; // New in Orthanc 1.10.1 std::unique_ptr<StorageAreaFactory> storageArea_; std::set<std::string> authorizationTokens_; + OrthancPluginHttpAuthentication httpAuthentication_; // New in Orthanc 1.12.9 + AuditLogHandlers auditLogHandlers_; // New in Orthanc 1.12.9 boost::recursive_mutex restCallbackInvokationMutex_; boost::shared_mutex restCallbackRegistrationMutex_; // New in Orthanc 1.9.0 @@ -1603,16 +1755,17 @@ boost::mutex storageCommitmentScpMutex_; boost::recursive_mutex invokeServiceMutex_; boost::shared_mutex incomingHttpRequestFilterMutex_; // New in Orthanc 1.8.2 + boost::shared_mutex auditLogHandlersMutex_; // New in Orthanc 1.12.9 Properties properties_; - int argc_; - char** argv_; + std::vector<std::string> arguments_; std::unique_ptr<OrthancPluginDatabase> database_; std::unique_ptr<OrthancPluginDatabaseV3> databaseV3_; // New in Orthanc 1.9.2 std::unique_ptr<OrthancPluginDatabaseV4> databaseV4_; // New in Orthanc 1.12.0 PluginsErrorDictionary dictionary_; std::string databaseServerIdentifier_; // New in Orthanc 1.9.2 - unsigned int maxDatabaseRetries_; // New in Orthanc 1.9.2 + unsigned int maxDatabaseRetries_; // New in Orthanc 1.9.2 + bool hasStorageAreaCustomData_; // New in Orthanc 1.12.8 explicit PImpl(const std::string& databaseServerIdentifier) : contextRefCount_(0), @@ -1620,10 +1773,10 @@ findCallback_(NULL), worklistCallback_(NULL), receivedInstanceCallback_(NULL), - argc_(1), - argv_(NULL), + httpAuthentication_(NULL), databaseServerIdentifier_(databaseServerIdentifier), - maxDatabaseRetries_(0) + maxDatabaseRetries_(0), + hasStorageAreaCustomData_(false) { memset(&moveCallbacks_, 0, sizeof(moveCallbacks_)); } @@ -1715,7 +1868,7 @@ } } - void GetDicomQuery(OrthancPluginMemoryBuffer& target) const + void GetDicomQuery(OrthancPluginMemoryBuffer* target) const { if (currentQuery_ == NULL) { @@ -1724,7 +1877,7 @@ std::string dicom; currentQuery_->SaveToMemoryBuffer(dicom); - CopyToMemoryBuffer(target, dicom.c_str(), dicom.size()); + CopyToMemoryBuffer(target, dicom); } bool IsMatch(const void* dicom, @@ -2109,16 +2262,16 @@ sizeof(int32_t) != sizeof(OrthancPluginContentType) || sizeof(int32_t) != sizeof(OrthancPluginResourceType) || sizeof(int32_t) != sizeof(OrthancPluginChangeType) || + sizeof(int32_t) != sizeof(OrthancPluginCompressionType) || sizeof(int32_t) != sizeof(OrthancPluginImageFormat) || - sizeof(int32_t) != sizeof(OrthancPluginCompressionType) || sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) || sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) || - sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) || sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) || + sizeof(int32_t) != sizeof(OrthancPluginJobStopReason) || sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || @@ -2127,6 +2280,16 @@ sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) || sizeof(int32_t) != sizeof(OrthancPluginLogLevel) || sizeof(int32_t) != sizeof(OrthancPluginLogCategory) || + sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) || + sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) || + sizeof(int32_t) != sizeof(OrthancPluginStableStatus) || + sizeof(int32_t) != sizeof(OrthancPluginHttpAuthenticationStatus) || + + // From OrthancCDatabasePlugin.h + sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType) || + 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) || @@ -2192,6 +2355,11 @@ std::vector<const char*>& values, const HttpToolbox::Arguments& arguments) { + if (static_cast<uint32_t>(arguments.size()) != arguments.size()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + keys.resize(arguments.size()); values.resize(arguments.size()); @@ -2203,6 +2371,8 @@ values[pos] = it->second.c_str(); pos++; } + + assert(pos == arguments.size()); } @@ -2210,6 +2380,11 @@ std::vector<const char*>& values, const HttpToolbox::GetArguments& arguments) { + if (static_cast<uint32_t>(arguments.size()) != arguments.size()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + keys.resize(arguments.size()); values.resize(arguments.size()); @@ -2296,7 +2471,8 @@ public: HttpRequestConverter(const RestCallbackMatcher& matcher, HttpMethod method, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { memset(&converted_, 0, sizeof(OrthancPluginHttpRequest)); @@ -2339,6 +2515,14 @@ converted_.headersKeys = &headersKeys_[0]; converted_.headersValues = &headersValues_[0]; } + + converted_.authenticationPayload = authenticationPayload.empty() ? NULL : authenticationPayload.c_str(); + converted_.authenticationPayloadSize = static_cast<uint32_t>(authenticationPayload.size()); + + if (converted_.authenticationPayloadSize != authenticationPayload.size()) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } } void SetGetArguments(const HttpToolbox::GetArguments& getArguments) @@ -2410,7 +2594,8 @@ HttpMethod method, const UriComponents& uri, const HttpToolbox::Arguments& headers, - const HttpToolbox::GetArguments& getArguments) + const HttpToolbox::GetArguments& getArguments, + const std::string& authenticationPayload) { RestCallbackMatcher matcher(uri); @@ -2459,7 +2644,7 @@ } else { - HttpRequestConverter converter(matcher, method, headers); + HttpRequestConverter converter(matcher, method, headers, authenticationPayload); converter.SetGetArguments(getArguments); PImpl::PluginHttpOutput pluginOutput(output); @@ -2485,7 +2670,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { RestCallbackMatcher matcher(uri); @@ -2506,12 +2692,12 @@ if (callback == NULL) { // Callback not found, try to find a chunked callback - return HandleChunkedGetDelete(output, method, uri, headers, getArguments); + return HandleChunkedGetDelete(output, method, uri, headers, getArguments, authenticationPayload); } CLOG(INFO, PLUGINS) << "Delegating HTTP request to plugin for URI: " << matcher.GetFlatUri(); - HttpRequestConverter converter(matcher, method, headers); + HttpRequestConverter converter(matcher, method, headers, authenticationPayload); converter.SetGetArguments(getArguments); converter.GetRequest().body = bodyData; converter.GetRequest().bodySize = bodySize; @@ -2527,125 +2713,6 @@ } - class OrthancPlugins::IDicomInstance : public boost::noncopyable - { - public: - virtual ~IDicomInstance() - { - } - - virtual bool CanBeFreed() const = 0; - - virtual const DicomInstanceToStore& GetInstance() const = 0; - }; - - - class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance - { - private: - const DicomInstanceToStore& instance_; - - public: - explicit DicomInstanceFromCallback(const DicomInstanceToStore& instance) : - instance_(instance) - { - } - - virtual bool CanBeFreed() const ORTHANC_OVERRIDE - { - return false; - } - - virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE - { - return instance_; - }; - }; - - - class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance - { - private: - std::string buffer_; - std::unique_ptr<DicomInstanceToStore> instance_; - - void Setup(const void* buffer, - size_t size) - { - buffer_.assign(reinterpret_cast<const char*>(buffer), size); - - instance_.reset(DicomInstanceToStore::CreateFromBuffer(buffer_)); - 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; - } - - virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE - { - return *instance_; - }; - }; - - - class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance - { - private: - std::unique_ptr<ParsedDicomFile> parsed_; - std::unique_ptr<DicomInstanceToStore> instance_; - - void Setup(ParsedDicomFile* parsed) - { - parsed_.reset(parsed); - - if (parsed_.get() == NULL) - { - 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 - { - return true; - } - - virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE - { - return *instance_; - }; - }; - - void OrthancPlugins::SignalStoredInstance(const std::string& instanceId, const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) @@ -2735,32 +2802,22 @@ } - OrthancPluginReceivedInstanceAction OrthancPlugins::ApplyReceivedInstanceCallbacks( - MallocMemoryBuffer& modified, - const void* receivedDicom, - size_t receivedDicomSize, - RequestOrigin origin) + OrthancPluginReceivedInstanceAction OrthancPlugins::ApplyReceivedInstanceCallbacks(PluginMemoryBuffer64& modified, + const void* receivedDicom, + size_t receivedDicomSize, + RequestOrigin origin) { boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); + modified.Clear(); + if (pimpl_->receivedInstanceCallback_ == NULL) { return OrthancPluginReceivedInstanceAction_KeepAsIs; } else { - OrthancPluginReceivedInstanceAction action; - - { - OrthancPluginMemoryBuffer64 buffer; - buffer.size = 0; - buffer.data = NULL; - - action = (*pimpl_->receivedInstanceCallback_) (&buffer, receivedDicom, receivedDicomSize, Plugins::Convert(origin)); - modified.Assign(buffer.data, buffer.size, ::free); - } - - return action; + return (*pimpl_->receivedInstanceCallback_) (modified.GetObject(), receivedDicom, receivedDicomSize, Plugins::Convert(origin)); } } @@ -3052,6 +3109,38 @@ } + void OrthancPlugins::RegisterHttpAuthentication(const void* parameters) + { + const _OrthancPluginHttpAuthentication& p = + *reinterpret_cast<const _OrthancPluginHttpAuthentication*>(parameters); + + boost::unique_lock<boost::shared_mutex> lock(pimpl_->incomingHttpRequestFilterMutex_); + + if (pimpl_->httpAuthentication_ == NULL) + { + CLOG(INFO, PLUGINS) << "Plugin has registered a callback to authenticate incoming HTTP requests"; + pimpl_->httpAuthentication_ = p.callback; + } + else + { + throw OrthancException(ErrorCode_Plugin, + "Only one plugin can register a callback to authenticate incoming HTTP requests"); + } + } + + + void OrthancPlugins::RegisterAuditLogHandler(const void* parameters) + { + const _OrthancPluginAuditLogHandler& p = + *reinterpret_cast<const _OrthancPluginAuditLogHandler*>(parameters); + + boost::unique_lock<boost::shared_mutex> lock(pimpl_->auditLogHandlersMutex_); + + CLOG(INFO, PLUGINS) << "Plugin has registered an AuditLog handler"; + pimpl_->auditLogHandlers_.push_back(p.handler); + } + + void OrthancPlugins::AnswerBuffer(const void* parameters) { const _OrthancPluginAnswerBuffer& p = @@ -3224,7 +3313,7 @@ lock.GetContext().ReadDicom(dicom, p.instanceId); } - CopyToMemoryBuffer(*p.target, dicom); + CopyToMemoryBuffer(p.target, dicom); } static void ThrowOnHttpError(HttpStatus httpStatus) @@ -3242,6 +3331,10 @@ { throw OrthancException(ErrorCode_UnknownResource); } + else if (intHttpStatus == 415) + { + throw OrthancException(ErrorCode_UnsupportedMediaType); + } else { throw OrthancException(ErrorCode_BadRequest); @@ -3270,7 +3363,7 @@ std::string result; ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, httpHeaders)); - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3301,7 +3394,7 @@ std::string result; ThrowOnHttpError(IHttpHandler::SimpleGet(result, NULL, *handler, RequestOrigin_Plugins, p.uri, headers)); - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3331,7 +3424,7 @@ p.body, p.bodySize, httpHeaders) : IHttpHandler::SimplePut(result, NULL, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders))); - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3351,7 +3444,8 @@ std::map<std::string, std::string> httpHeaders; - ThrowOnHttpError(IHttpHandler::SimpleDelete(NULL, *handler, RequestOrigin_Plugins, uri, httpHeaders)); + std::string bodyIgnored; + ThrowOnHttpError(IHttpHandler::SimpleDelete(bodyIgnored, NULL, *handler, RequestOrigin_Plugins, uri, httpHeaders)); } @@ -3611,6 +3705,12 @@ break; } + case OrthancPluginCompressionType_None: + { + CopyToMemoryBuffer(p.target, p.source, p.size); + return; + } + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -3625,7 +3725,7 @@ } } - CopyToMemoryBuffer(*p.target, result); + CopyToMemoryBuffer(p.target, result); } @@ -3686,7 +3786,7 @@ MimeType mime; std::string frame; instance.GetParsedDicomFile().GetRawFrame(frame, mime, p.frameIndex); - CopyToMemoryBuffer(*p.targetBuffer, frame); + CopyToMemoryBuffer(p.targetBuffer, frame); return; } @@ -3716,7 +3816,7 @@ p.targetBuffer->data = NULL; p.targetBuffer->size = 0; - CopyToMemoryBuffer(*p.targetBuffer, instance.GetBufferData(), instance.GetBufferSize()); + CopyToMemoryBuffer(p.targetBuffer, instance.GetBufferData(), instance.GetBufferSize()); return; } @@ -3825,7 +3925,7 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - CopyToMemoryBuffer(*p.target, compressed.size() > 0 ? compressed.c_str() : NULL, compressed.size()); + CopyToMemoryBuffer(p.target, compressed); } @@ -3851,17 +3951,17 @@ std::string certificate(parameters.certificateFile); std::string key, password; - if (parameters.certificateKeyFile) + if (parameters.certificateKeyFile != NULL) { key.assign(parameters.certificateKeyFile); } - if (parameters.certificateKeyPassword) + if (parameters.certificateKeyPassword != NULL) { password.assign(parameters.certificateKeyPassword); } - client.SetClientCertificate(certificate, key, password); + client.SetClientCertificate(SystemToolbox::PathFromUtf8(certificate), SystemToolbox::PathFromUtf8(key), password); } client.SetPkcs11Enabled(parameters.pkcs11 ? true : false); @@ -3922,29 +4022,29 @@ } // Copy the HTTP headers of the answer, if the plugin requested them + PluginMemoryBuffer32 tmpHeaders; if (answerHeaders != NULL) { - CopyDictionary(*answerHeaders, headers); + CopyDictionary(tmpHeaders, headers); } // Copy the body of the answer if it makes sense - if (client.GetMethod() != HttpMethod_Delete) - { - try - { - if (answerBody != NULL) - { - CopyToMemoryBuffer(*answerBody, body); - } - } - catch (OrthancException&) - { - if (answerHeaders != NULL) - { - free(answerHeaders->data); - } - throw; - } + PluginMemoryBuffer32 tmpBody; + if (client.GetMethod() != HttpMethod_Delete && + answerBody != NULL) + { + tmpBody.Assign(body); + } + + // All the memory has been allocated at this point, so we can safely release the buffers + if (answerHeaders != NULL) + { + tmpHeaders.Release(answerHeaders); + } + + if (answerBody != NULL) + { + tmpBody.Release(answerBody); } } @@ -4134,7 +4234,7 @@ case OrthancPluginHttpMethod_Delete: status = IHttpHandler::SimpleDelete( - &answerHeaders, *handler, RequestOrigin_Plugins, p.uri, headers); + answerBody, &answerHeaders, *handler, RequestOrigin_Plugins, p.uri, headers); break; default: @@ -4143,25 +4243,27 @@ *p.httpStatus = static_cast<uint16_t>(status); + PluginMemoryBuffer32 tmpHeaders; if (p.answerHeaders != NULL) { - CopyDictionary(*p.answerHeaders, answerHeaders); - } - - try - { - if (p.answerBody != NULL) - { - CopyToMemoryBuffer(*p.answerBody, answerBody); - } - } - catch (OrthancException&) - { - if (p.answerHeaders != NULL) - { - free(p.answerHeaders->data); - } - throw; + CopyDictionary(tmpHeaders, answerHeaders); + } + + PluginMemoryBuffer32 tmpBody; + if (p.answerBody != NULL) + { + tmpBody.Assign(answerBody); + } + + // All the memory has been allocated at this point, so we can safely release the buffers + if (p.answerHeaders != NULL) + { + tmpHeaders.Release(p.answerHeaders); + } + + if (p.answerBody != NULL) + { + tmpBody.Release(p.answerBody); } } @@ -4228,29 +4330,29 @@ } // Copy the HTTP headers of the answer, if the plugin requested them + PluginMemoryBuffer32 tmpHeaders; if (p.answerHeaders != NULL) { - CopyDictionary(*p.answerHeaders, headers); + CopyDictionary(tmpHeaders, headers); } // Copy the body of the answer if it makes sense - if (p.method != OrthancPluginHttpMethod_Delete) - { - try - { - if (p.answerBody != NULL) - { - CopyToMemoryBuffer(*p.answerBody, body); - } - } - catch (OrthancException&) - { - if (p.answerHeaders != NULL) - { - free(p.answerHeaders->data); - } - throw; - } + PluginMemoryBuffer32 tmpBody; + if (p.method != OrthancPluginHttpMethod_Delete && + p.answerBody != NULL) + { + tmpBody.Assign(body); + } + + // All the memory has been allocated at this point, so we can safely release the buffers + if (p.answerHeaders != NULL) + { + tmpHeaders.Release(p.answerHeaders); + } + + if (p.answerBody != NULL) + { + tmpBody.Release(p.answerBody); } } @@ -4391,7 +4493,7 @@ file->SaveToMemoryBuffer(dicom); } - CopyToMemoryBuffer(*parameters.target, dicom); + CopyToMemoryBuffer(parameters.target, dicom); } @@ -4499,6 +4601,203 @@ reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers); } + void OrthancPlugins::ApplyAdoptDicomInstance(const _OrthancPluginAdoptDicomInstance& parameters) + { + if (!pimpl_->hasStorageAreaCustomData_) + { + LOG(WARNING) << "The adoption of a DICOM instance should only be used in combination with a custom " + << "storage area registered using OrthancPluginRegisterStorageArea3()"; + } + + std::string md5; + Toolbox::ComputeMD5(md5, parameters.dicom, parameters.dicomSize); + + std::unique_ptr<DicomInstanceToStore> dicom(DicomInstanceToStore::CreateFromBuffer(parameters.dicom, parameters.dicomSize)); + dicom->SetOrigin(DicomInstanceOrigin::FromPlugins()); + + const std::string attachmentUuid = Toolbox::GenerateUuid(); + + FileInfo adoptedFile(attachmentUuid, FileContentType_Dicom, parameters.dicomSize, md5); + adoptedFile.SetCustomData(parameters.customData, parameters.customDataSize); + + std::string instanceId; + ServerContext::StoreResult result; + + { + PImpl::ServerContextReference lock(*pimpl_); + result = lock.GetContext().AdoptDicomInstance(instanceId, *dicom, StoreInstanceMode_Default, adoptedFile); + } + + CopyToMemoryBuffer(parameters.attachmentUuid, attachmentUuid); + CopyToMemoryBuffer(parameters.instanceId, instanceId); + *(parameters.storeStatus) = Plugins::Convert(result.GetStatus()); + } + + static void CheckAttachmentCustomDataSupport(ServerContext& context) + { + if (!context.GetIndex().HasAttachmentCustomDataSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support custom data for attachments"); + } + } + + void OrthancPlugins::ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckAttachmentCustomDataSupport(lock.GetContext()); + + std::string customData; + lock.GetContext().GetIndex().GetAttachmentCustomData(customData, parameters.attachmentUuid); + + CopyToMemoryBuffer(parameters.customData, customData); + } + + void OrthancPlugins::ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckAttachmentCustomDataSupport(lock.GetContext()); + + lock.GetContext().GetIndex().SetAttachmentCustomData(parameters.attachmentUuid, parameters.customData, parameters.customDataSize); + } + + static void CheckKeyValueStoresSupport(ServerContext& context) + { + if (!context.GetIndex().HasKeyValueStoresSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support key-value stores"); + } + } + + void OrthancPlugins::ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + lock.GetContext().GetIndex().StoreKeyValue(parameters.storeId, parameters.key, parameters.value, parameters.valueSize); + } + + void OrthancPlugins::ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + lock.GetContext().GetIndex().DeleteKeyValue(parameters.storeId, parameters.key); + } + + void OrthancPlugins::ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + std::string value; + + if (lock.GetContext().GetIndex().GetKeyValue(value, parameters.storeId, parameters.key)) + { + CopyToMemoryBuffer(parameters.target, value); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + void OrthancPlugins::ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckKeyValueStoresSupport(lock.GetContext()); + + *parameters.target = reinterpret_cast<OrthancPluginKeysValuesIterator*>( + new StatelessDatabaseOperations::KeysValuesIterator(lock.GetContext().GetIndex(), parameters.storeId)); + } + + static void CheckQueuesSupport(ServerContext& context) + { + if (!context.GetIndex().HasQueuesSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database engine does not support queues"); + } + } + + void OrthancPlugins::ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + lock.GetContext().GetIndex().EnqueueValue(parameters.queueId, parameters.value, parameters.valueSize); + } + + void OrthancPlugins::ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + std::string value; + + if (lock.GetContext().GetIndex().DequeueValue(value, parameters.queueId, Plugins::Convert(parameters.origin))) + { + CopyToMemoryBuffer(parameters.target, value); + *parameters.found = true; + } + else + { + *parameters.found = false; + } + } + + void OrthancPlugins::ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + + CheckQueuesSupport(lock.GetContext()); + + *parameters.size = lock.GetContext().GetIndex().GetQueueSize(parameters.queueId); + } + + void OrthancPlugins::ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters) + { + PImpl::ServerContextReference lock(*pimpl_); + bool statusHasChanged = false; + + lock.GetContext().GetIndex().SetStableStatus(statusHasChanged, + parameters.resourceId, + (parameters.stableStatus == OrthancPluginStableStatus_Stable)); + if (statusHasChanged) + { + *(parameters.statusHasChanged) = 1; + } + else + { + *(parameters.statusHasChanged) = 0; + } + } + + void OrthancPlugins::ApplyEmitAuditLog(const _OrthancPluginEmitAuditLog& parameters) + { + boost::unique_lock<boost::shared_mutex> lock(pimpl_->auditLogHandlersMutex_); + + for (PImpl::AuditLogHandlers::const_iterator handler = pimpl_->auditLogHandlers_.begin(); + handler != pimpl_->auditLogHandlers_.end(); ++handler) + { + OrthancPluginErrorCode error = (*handler) ( + parameters.sourcePlugin, parameters.userId, parameters.resourceType, + parameters.resourceId, parameters.action, parameters.logData, parameters.logDataSize); + + if (error != OrthancPluginErrorCode_Success) + { + GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + } void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params) { @@ -4624,35 +4923,6 @@ } - namespace - { - class DictionaryReadLocker - { - private: - const DcmDataDictionary& dictionary_; - - public: - DictionaryReadLocker() : dictionary_(dcmDataDict.rdlock()) - { - } - - ~DictionaryReadLocker() - { -#if DCMTK_VERSION_NUMBER >= 364 - dcmDataDict.rdunlock(); -#else - dcmDataDict.unlock(); -#endif - } - - const DcmDataDictionary* operator->() - { - return &dictionary_; - } - }; - } - - void OrthancPlugins::ApplyLookupDictionary(const void* parameters) { const _OrthancPluginLookupDictionary& p = @@ -4661,38 +4931,41 @@ DicomTag tag(FromDcmtkBridge::ParseTag(p.name)); DcmTagKey tag2(tag.GetGroup(), tag.GetElement()); - DictionaryReadLocker locker; - const DcmDictEntry* entry = NULL; - - if (tag.IsPrivate()) - { - // Fix issue 168 (Plugins can't read private tags from the - // configuration file) - // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168 - std::string privateCreator; - { - OrthancConfiguration::ReaderLock lock; - privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator(); - } - - entry = locker->findEntry(tag2, privateCreator.c_str()); - } - else - { - entry = locker->findEntry(tag2, NULL); - } - - if (entry == NULL) - { - throw OrthancException(ErrorCode_UnknownDicomTag, p.name); - } - else - { - p.target->group = entry->getKey().getGroup(); - p.target->element = entry->getKey().getElement(); - p.target->vr = Plugins::Convert(FromDcmtkBridge::Convert(entry->getEVR())); - p.target->minMultiplicity = static_cast<uint32_t>(entry->getVMMin()); - p.target->maxMultiplicity = (entry->getVMMax() == DcmVariableVM ? 0 : static_cast<uint32_t>(entry->getVMMax())); + { + FromDcmtkBridge::DictionaryReaderLock lock; + + const DcmDictEntry* entry = NULL; // This value is only valid while "lock" is active + + if (tag.IsPrivate()) + { + // Fix issue 168 (Plugins can't read private tags from the + // configuration file) + // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=168 + std::string privateCreator; + { + OrthancConfiguration::ReaderLock configurationLock; + privateCreator = configurationLock.GetConfiguration().GetDefaultPrivateCreator(); + } + + entry = lock.GetDictionary().findEntry(tag2, privateCreator.c_str()); + } + else + { + entry = lock.GetDictionary().findEntry(tag2, NULL); + } + + if (entry == NULL) + { + throw OrthancException(ErrorCode_UnknownDicomTag, p.name); + } + else + { + p.target->group = entry->getKey().getGroup(); + p.target->element = entry->getKey().getElement(); + p.target->vr = Plugins::Convert(FromDcmtkBridge::Convert(entry->getEVR())); + p.target->minMultiplicity = static_cast<uint32_t>(entry->getVMMin()); + p.target->maxMultiplicity = (entry->getVMMax() == DcmVariableVM ? 0 : static_cast<uint32_t>(entry->getVMMax())); + } } } @@ -4707,14 +4980,14 @@ { case _OrthancPluginService_GetOrthancPath: { - std::string s = SystemToolbox::GetPathToExecutable(); + std::string s = SystemToolbox::PathToUtf8(SystemToolbox::GetPathToExecutable()); *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s); return true; } case _OrthancPluginService_GetOrthancDirectory: { - std::string s = SystemToolbox::GetDirectoryOfExecutable(); + std::string s = SystemToolbox::PathToUtf8(SystemToolbox::GetDirectoryOfExecutable()); *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s); return true; } @@ -4947,8 +5220,8 @@ *reinterpret_cast<const _OrthancPluginReadFile*>(parameters); std::string content; - SystemToolbox::ReadFile(content, p.path); - CopyToMemoryBuffer(*p.target, content.size() > 0 ? content.c_str() : NULL, content.size()); + SystemToolbox::ReadFile(content, SystemToolbox::PathFromUtf8(p.path)); + CopyToMemoryBuffer(p.target, content); return true; } @@ -4957,7 +5230,7 @@ { const _OrthancPluginWriteFile& p = *reinterpret_cast<const _OrthancPluginWriteFile*>(parameters); - SystemToolbox::WriteFile(p.data, p.size, p.path, true /* run fsync() */); + SystemToolbox::WriteFile(p.data, p.size, std::string(p.path), true /* run fsync() */); return true; } @@ -5066,32 +5339,13 @@ return true; case _OrthancPluginService_StorageAreaCreate: - { - const _OrthancPluginStorageAreaCreate& p = - *reinterpret_cast<const _OrthancPluginStorageAreaCreate*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Create(p.uuid, p.content, static_cast<size_t>(p.size), Plugins::Convert(p.type)); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaCreate() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_StorageAreaRead: - { - const _OrthancPluginStorageAreaRead& p = - *reinterpret_cast<const _OrthancPluginStorageAreaRead*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - std::unique_ptr<IMemoryBuffer> content(storage.Read(p.uuid, Plugins::Convert(p.type))); - CopyToMemoryBuffer(*p.target, content->GetData(), content->GetSize()); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRead() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_StorageAreaRemove: - { - const _OrthancPluginStorageAreaRemove& p = - *reinterpret_cast<const _OrthancPluginStorageAreaRemove*>(parameters); - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); - storage.Remove(p.uuid, Plugins::Convert(p.type)); - return true; - } + throw OrthancException(ErrorCode_NotImplemented, "The SDK function OrthancPluginStorageAreaRemove() is only available in Orthanc <= 1.12.6"); case _OrthancPluginService_DicomBufferToJson: case _OrthancPluginService_DicomInstanceToJson: @@ -5143,7 +5397,7 @@ { const _OrthancPluginWorklistQueryOperation& p = *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); - reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(*p.target); + reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(p.target); return true; } @@ -5526,20 +5780,10 @@ const _OrthancPluginCreateMemoryBuffer& p = *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer*>(parameters); - p.target->data = NULL; - p.target->size = 0; - - if (p.size != 0) - { - p.target->data = malloc(p.size); - if (p.target->data == NULL) - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - - p.target->size = p.size; - } - + PluginMemoryBuffer32 buffer; + buffer.Resize(p.size); + buffer.Release(p.target); + return true; } @@ -5548,19 +5792,9 @@ const _OrthancPluginCreateMemoryBuffer64& p = *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer64*>(parameters); - p.target->data = NULL; - p.target->size = 0; - - if (p.size != 0) - { - p.target->data = malloc(p.size); - if (p.target->data == NULL) - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - - p.target->size = p.size; - } + PluginMemoryBuffer64 buffer; + buffer.Resize(p.size); + buffer.Release(p.target); return true; } @@ -5587,6 +5821,121 @@ return true; } + case _OrthancPluginService_AdoptDicomInstance: + { + const _OrthancPluginAdoptDicomInstance& p = *reinterpret_cast<const _OrthancPluginAdoptDicomInstance*>(parameters); + ApplyAdoptDicomInstance(p); + return true; + } + + case _OrthancPluginService_GetAttachmentCustomData: + { + const _OrthancPluginGetAttachmentCustomData& p = *reinterpret_cast<const _OrthancPluginGetAttachmentCustomData*>(parameters); + ApplyGetAttachmentCustomData(p); + return true; + } + + case _OrthancPluginService_SetAttachmentCustomData: + { + const _OrthancPluginSetAttachmentCustomData& p = *reinterpret_cast<const _OrthancPluginSetAttachmentCustomData*>(parameters); + ApplySetAttachmentCustomData(p); + return true; + } + + case _OrthancPluginService_StoreKeyValue: + { + const _OrthancPluginStoreKeyValue& p = *reinterpret_cast<const _OrthancPluginStoreKeyValue*>(parameters); + ApplyStoreKeyValue(p); + return true; + } + + case _OrthancPluginService_DeleteKeyValue: + { + const _OrthancPluginDeleteKeyValue& p = *reinterpret_cast<const _OrthancPluginDeleteKeyValue*>(parameters); + ApplyDeleteKeyValue(p); + return true; + } + + case _OrthancPluginService_GetKeyValue: + { + const _OrthancPluginGetKeyValue& p = *reinterpret_cast<const _OrthancPluginGetKeyValue*>(parameters); + ApplyGetKeyValue(p); + return true; + } + + case _OrthancPluginService_CreateKeysValuesIterator: + { + const _OrthancPluginCreateKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginCreateKeysValuesIterator*>(parameters); + ApplyCreateKeysValuesIterator(p); + return true; + } + + case _OrthancPluginService_FreeKeysValuesIterator: + { + const _OrthancPluginFreeKeysValuesIterator& p = *reinterpret_cast<const _OrthancPluginFreeKeysValuesIterator*>(parameters); + delete reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + return true; + } + + case _OrthancPluginService_KeysValuesIteratorNext: + { + const _OrthancPluginKeysValuesIteratorNext& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorNext*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + *p.done = iterator.Next() ? 1 : 0; + return true; + } + + case _OrthancPluginService_KeysValuesIteratorGetKey: + { + const _OrthancPluginKeysValuesIteratorGetKey& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetKey*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + *p.target = iterator.GetKey().c_str(); + return true; + } + + case _OrthancPluginService_KeysValuesIteratorGetValue: + { + const _OrthancPluginKeysValuesIteratorGetValue& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetValue*>(parameters); + StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + CopyToMemoryBuffer(p.target, iterator.GetValue()); + return true; + } + + case _OrthancPluginService_EnqueueValue: + { + const _OrthancPluginEnqueueValue& p = *reinterpret_cast<const _OrthancPluginEnqueueValue*>(parameters); + ApplyEnqueueValue(p); + return true; + } + + case _OrthancPluginService_DequeueValue: + { + const _OrthancPluginDequeueValue& p = *reinterpret_cast<const _OrthancPluginDequeueValue*>(parameters); + ApplyDequeueValue(p); + return true; + } + + case _OrthancPluginService_GetQueueSize: + { + const _OrthancPluginGetQueueSize& p = *reinterpret_cast<const _OrthancPluginGetQueueSize*>(parameters); + ApplyGetQueueSize(p); + return true; + } + + case _OrthancPluginService_SetStableStatus: + { + const _OrthancPluginSetStableStatus& p = *reinterpret_cast<const _OrthancPluginSetStableStatus*>(parameters); + ApplySetStableStatus(p); + return true; + } + + case _OrthancPluginService_EmitAuditLog: + { + const _OrthancPluginEmitAuditLog& p = *reinterpret_cast<const _OrthancPluginEmitAuditLog*>(parameters); + ApplyEmitAuditLog(p); + return true; + } + default: return false; } @@ -5668,25 +6017,45 @@ RegisterStorageCommitmentScpCallback(parameters); return true; + case _OrthancPluginService_RegisterHttpAuthentication: + RegisterHttpAuthentication(parameters); + return true; + + case _OrthancPluginService_RegisterAuditLogHandler: + RegisterAuditLogHandler(parameters); + return true; + case _OrthancPluginService_RegisterStorageArea: case _OrthancPluginService_RegisterStorageArea2: - { - CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area"; - + case _OrthancPluginService_RegisterStorageArea3: + { if (pimpl_->storageArea_.get() == NULL) { if (service == _OrthancPluginService_RegisterStorageArea) { + CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v1)"; + const _OrthancPluginRegisterStorageArea& p = *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters); pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); } else if (service == _OrthancPluginService_RegisterStorageArea2) { + CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v2)"; + const _OrthancPluginRegisterStorageArea2& p = *reinterpret_cast<const _OrthancPluginRegisterStorageArea2*>(parameters); pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); } + else if (service == _OrthancPluginService_RegisterStorageArea3) + { + CLOG(INFO, PLUGINS) << "Plugin has registered a custom storage area (v3)"; + + const _OrthancPluginRegisterStorageArea3& p = + *reinterpret_cast<const _OrthancPluginRegisterStorageArea3*>(parameters); + pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary())); + pimpl_->hasStorageAreaCustomData_ = true; + } else { throw OrthancException(ErrorCode_InternalError); @@ -5712,7 +6081,7 @@ { const _OrthancPluginReturnSingleValue& p = *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters); - *(p.resultUint32) = pimpl_->argc_ - 1; + *(p.resultUint32) = static_cast<uint32_t>(pimpl_->arguments_.size()) - 1; return true; } @@ -5721,14 +6090,13 @@ const _OrthancPluginGlobalProperty& p = *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters); - if (p.property + 1 > pimpl_->argc_) + if (p.property + 1 > static_cast<int32_t>(pimpl_->arguments_.size())) { return false; } else { - std::string arg = std::string(pimpl_->argv_[p.property + 1]); - *(p.result) = CopyString(arg); + *(p.result) = CopyString(pimpl_->arguments_[p.property + 1]); return true; } } @@ -5809,7 +6177,7 @@ case _OrthancPluginService_RegisterDatabaseBackendV4: { - CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end"; + CLOG(INFO, PLUGINS) << "Plugin has registered a custom database back-end (v4)"; const _OrthancPluginRegisterDatabaseBackendV4& p = *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV4*>(parameters); @@ -5874,7 +6242,7 @@ VoidDatabaseListener listener; { - IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea); + IPluginStorageArea& storage = *reinterpret_cast<IPluginStorageArea*>(p.storageArea); std::unique_ptr<IDatabaseWrapper::ITransaction> transaction( pimpl_->database_->StartTransaction(TransactionType_ReadWrite, listener)); @@ -5973,7 +6341,7 @@ } - IStorageArea* OrthancPlugins::CreateStorageArea() + IPluginStorageArea* OrthancPlugins::CreateStorageArea() { if (!HasStorageArea()) { @@ -6058,15 +6426,14 @@ } - void OrthancPlugins::SetCommandLineArguments(int argc, char* argv[]) - { - if (argc < 1 || argv == NULL) + void OrthancPlugins::SetCommandLineArguments(const std::vector<std::string>& arguments) + { + if (arguments.size() == 0) { throw OrthancException(ErrorCode_ParameterOutOfRange); } - pimpl_->argc_ = argc; - pimpl_->argv_ = argv; + pimpl_->arguments_ = arguments; } @@ -6358,7 +6725,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { if (method != HttpMethod_Post && method != HttpMethod_Put) @@ -6414,7 +6782,7 @@ { CLOG(INFO, PLUGINS) << "Delegating chunked HTTP request to plugin for URI: " << matcher.GetFlatUri(); - HttpRequestConverter converter(matcher, method, headers); + HttpRequestConverter converter(matcher, method, headers, authenticationPayload); converter.GetRequest().body = NULL; converter.GetRequest().bodySize = 0; @@ -6495,13 +6863,13 @@ transcoder = pimpl_->transcoderCallbacks_.begin(); transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder) { - MemoryBufferRaii a; + PluginMemoryBuffer32 a; if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0], static_cast<uint32_t>(uids.size()), allowNewSopInstanceUid) == OrthancPluginErrorCode_Success) { - a.ToString(target); + a.MoveToString(target); return true; } } @@ -6540,4 +6908,64 @@ pimpl_->webDavCollections_.pop_front(); } } + + + IIncomingHttpRequestFilter::AuthenticationStatus OrthancPlugins::CheckAuthentication( + std::string& customPayload, + std::string& redirection, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const + { + boost::shared_lock<boost::shared_mutex> lock(pimpl_->incomingHttpRequestFilterMutex_); + + if (pimpl_->httpAuthentication_ == NULL) + { + return IIncomingHttpRequestFilter::AuthenticationStatus_BuiltIn; // Use the default authentication of Orthanc + } + else + { + std::vector<const char*> headersKeys, headersValues; + ArgumentsToPlugin(headersKeys, headersValues, httpHeaders); + + std::vector<const char*> getKeys, getValues; + ArgumentsToPlugin(getKeys, getValues, getArguments); + + OrthancPluginHttpAuthenticationStatus status = OrthancPluginHttpAuthenticationStatus_Unauthorized; + PluginMemoryBuffer32 payloadBuffer; + PluginMemoryBuffer32 redirectionBuffer; + OrthancPluginErrorCode code = pimpl_->httpAuthentication_( + &status, payloadBuffer.GetObject(), redirectionBuffer.GetObject(), uri, ip, + headersKeys.size(), headersKeys.empty() ? NULL : &headersKeys[0], headersValues.empty() ? NULL : &headersValues[0], + getKeys.size(), getKeys.empty() ? NULL : &getKeys[0], getValues.empty() ? NULL : &getValues[0]); + + if (code != OrthancPluginErrorCode_Success) + { + throw OrthancException(static_cast<ErrorCode>(code)); + } + else + { + switch (status) + { + case OrthancPluginHttpAuthenticationStatus_Granted: + payloadBuffer.MoveToString(customPayload); + return IIncomingHttpRequestFilter::AuthenticationStatus_Granted; + + case OrthancPluginHttpAuthenticationStatus_Unauthorized: + return IIncomingHttpRequestFilter::AuthenticationStatus_Unauthorized; + + case OrthancPluginHttpAuthenticationStatus_Forbidden: + return IIncomingHttpRequestFilter::AuthenticationStatus_Forbidden; + + case OrthancPluginHttpAuthenticationStatus_Redirect: + redirectionBuffer.MoveToString(redirection); + return IIncomingHttpRequestFilter::AuthenticationStatus_Redirect; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } + } }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Nov 04 15:58:06 2025 +0100 @@ -56,6 +56,7 @@ #include "../../Sources/IDicomImageDecoder.h" #include "../../Sources/IServerListener.h" #include "../../Sources/ServerJobs/IStorageCommitmentFactory.h" +#include "PluginMemoryBuffer64.h" #include "PluginsManager.h" #include <list> @@ -88,11 +89,14 @@ class HttpClientChunkedAnswer; class HttpServerChunkedReader; class IDicomInstance; - class DicomInstanceFromCallback; class DicomInstanceFromBuffer; class DicomInstanceFromParsed; class WebDavCollection; - + +public: + class DicomInstanceFromCallback; + +private: void RegisterRestCallback(const void* parameters, bool lock); @@ -102,7 +106,8 @@ HttpMethod method, const UriComponents& uri, const HttpToolbox::Arguments& headers, - const HttpToolbox::GetArguments& getArguments); + const HttpToolbox::GetArguments& getArguments, + const std::string& authenticationPayload); void RegisterOnStoredInstanceCallback(const void* parameters); @@ -134,6 +139,10 @@ void RegisterStorageCommitmentScpCallback(const void* parameters); + void RegisterHttpAuthentication(const void* parameters); + + void RegisterAuditLogHandler(const void* parameters); + void AnswerBuffer(const void* parameters); void Redirect(const void* parameters); @@ -220,6 +229,30 @@ void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters); + void ApplyAdoptDicomInstance(const _OrthancPluginAdoptDicomInstance& parameters); + + void ApplyGetAttachmentCustomData(const _OrthancPluginGetAttachmentCustomData& parameters); + + void ApplySetAttachmentCustomData(const _OrthancPluginSetAttachmentCustomData& parameters); + + void ApplyStoreKeyValue(const _OrthancPluginStoreKeyValue& parameters); + + void ApplyDeleteKeyValue(const _OrthancPluginDeleteKeyValue& parameters); + + void ApplyGetKeyValue(const _OrthancPluginGetKeyValue& parameters); + + void ApplyCreateKeysValuesIterator(const _OrthancPluginCreateKeysValuesIterator& parameters); + + void ApplyEnqueueValue(const _OrthancPluginEnqueueValue& parameters); + + void ApplyDequeueValue(const _OrthancPluginDequeueValue& parameters); + + void ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters); + + void ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters); + + void ApplyEmitAuditLog(const _OrthancPluginEmitAuditLog& parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters); @@ -263,7 +296,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; virtual bool InvokeService(SharedLibrary& plugin, _OrthancPluginService service, @@ -284,14 +318,14 @@ const DicomInstanceToStore& instance, const Json::Value& simplified) ORTHANC_OVERRIDE; - OrthancPluginReceivedInstanceAction ApplyReceivedInstanceCallbacks(MallocMemoryBuffer& modified, + OrthancPluginReceivedInstanceAction ApplyReceivedInstanceCallbacks(PluginMemoryBuffer64& modified, const void* receivedDicomBuffer, size_t receivedDicomBufferSize, RequestOrigin origin); bool HasStorageArea() const; - IStorageArea* CreateStorageArea(); // To be freed after use + IPluginStorageArea* CreateStorageArea(); // To be freed after use const SharedLibrary& GetStorageAreaLibrary() const; @@ -304,7 +338,7 @@ const char* GetProperty(const char* plugin, _OrthancPluginProperty property) const; - void SetCommandLineArguments(int argc, char* argv[]); + void SetCommandLineArguments(const std::vector<std::string>& arguments); PluginsManager& GetManager(); @@ -371,7 +405,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; // New in Orthanc 1.6.0 IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment( @@ -388,6 +423,14 @@ unsigned int GetMaxDatabaseRetries() const; void RegisterWebDavCollections(HttpServer& target); + + IIncomingHttpRequestFilter::AuthenticationStatus CheckAuthentication( + std::string& customPayload, + std::string& redirection, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,200 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Sources/PrecompiledHeadersServer.h" +#include "PluginMemoryBuffer32.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/Toolbox.h" + +#define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc SDK is necessary to use buffers > 4GB, but is currently not available" + + +namespace Orthanc +{ + void PluginMemoryBuffer32::Clear() + { + if (buffer_.size != 0) + { + ::free(buffer_.data); + } + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer32::SanityCheck() const + { + if ((buffer_.data == NULL && buffer_.size != 0) || + (buffer_.data != NULL && buffer_.size == 0)) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + PluginMemoryBuffer32::PluginMemoryBuffer32() + { + buffer_.size = 0; + buffer_.data = NULL; + } + + + void PluginMemoryBuffer32::MoveToString(std::string& target) + { + SanityCheck(); + + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + + Clear(); + } + + + const void* PluginMemoryBuffer32::GetData() const + { + SanityCheck(); + + if (buffer_.size == 0) + { + return NULL; + } + else + { + return buffer_.data; + } + } + + + size_t PluginMemoryBuffer32::GetSize() const + { + SanityCheck(); + return buffer_.size; + } + + + void PluginMemoryBuffer32::Release(OrthancPluginMemoryBuffer* target) + { + SanityCheck(); + + if (target == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + target->data = buffer_.data; + target->size = buffer_.size; + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer32::Release(OrthancPluginMemoryBuffer64* target) + { + SanityCheck(); + + if (target == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + target->data = buffer_.data; + target->size = buffer_.size; + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer32::Resize(size_t size) + { + if (static_cast<size_t>(static_cast<uint32_t>(size)) != size) + { + throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT); + } + + if (size != buffer_.size) + { + Clear(); + + if (size == 0) + { + buffer_.data = NULL; + } + else + { + buffer_.data = ::malloc(size); + + if (buffer_.data == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + buffer_.size = size; + } + } + + + void PluginMemoryBuffer32::Assign(const void* data, + size_t size) + { + Resize(size); + + if (size != 0) + { + memcpy(buffer_.data, data, size); + } + } + + + void PluginMemoryBuffer32::Assign(const std::string& data) + { + if (data.empty()) + { + Assign(NULL, 0); + } + else + { + Assign(data.c_str(), data.size()); + } + } + + + void PluginMemoryBuffer32::ToJsonObject(Json::Value& target) const + { + SanityCheck(); + + if (!Toolbox::ReadJson(target, buffer_.data, buffer_.size) || + target.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_Plugin, "The plugin has not provided a valid JSON object"); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#if ORTHANC_ENABLE_PLUGINS != 1 +# error The plugin support is disabled +#endif + +#include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h" +#include "../Include/orthanc/OrthancCPlugin.h" + +#include <json/value.h> + +namespace Orthanc +{ + class PluginMemoryBuffer32 : public IMemoryBuffer + { + private: + OrthancPluginMemoryBuffer buffer_; + + void SanityCheck() const; + + public: + PluginMemoryBuffer32(); + + virtual ~PluginMemoryBuffer32() + { + Clear(); + } + + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; + + virtual const void* GetData() const ORTHANC_OVERRIDE; + + virtual size_t GetSize() const ORTHANC_OVERRIDE; + + OrthancPluginMemoryBuffer* GetObject() + { + return &buffer_; + } + + void Release(OrthancPluginMemoryBuffer* target); + + void Release(OrthancPluginMemoryBuffer64* target); + + void Clear(); + + void Resize(size_t size); + + void Assign(const void* data, + size_t size); + + void Assign(const std::string& data); + + void ToJsonObject(Json::Value& target) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,163 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Sources/PrecompiledHeadersServer.h" +#include "PluginMemoryBuffer64.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + void PluginMemoryBuffer64::Clear() + { + if (buffer_.size != 0) + { + ::free(buffer_.data); + } + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer64::SanityCheck() const + { + if ((buffer_.data == NULL && buffer_.size != 0) || + (buffer_.data != NULL && buffer_.size == 0)) + { + throw OrthancException(ErrorCode_Plugin); + } + } + + + PluginMemoryBuffer64::PluginMemoryBuffer64() + { + buffer_.size = 0; + buffer_.data = NULL; + } + + + void PluginMemoryBuffer64::MoveToString(std::string& target) + { + SanityCheck(); + + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + + Clear(); + } + + + const void* PluginMemoryBuffer64::GetData() const + { + SanityCheck(); + + if (buffer_.size == 0) + { + return NULL; + } + else + { + return buffer_.data; + } + } + + + size_t PluginMemoryBuffer64::GetSize() const + { + SanityCheck(); + return buffer_.size; + } + + + void PluginMemoryBuffer64::Release(OrthancPluginMemoryBuffer64* target) + { + SanityCheck(); + + if (target == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + target->data = buffer_.data; + target->size = buffer_.size; + + buffer_.data = NULL; + buffer_.size = 0; + } + + + void PluginMemoryBuffer64::Resize(size_t size) + { + if (size != buffer_.size) + { + Clear(); + + if (size == 0) + { + buffer_.data = NULL; + } + else + { + buffer_.data = ::malloc(size); + + if (buffer_.data == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + + buffer_.size = size; + } + } + + + void PluginMemoryBuffer64::Assign(const void* data, + size_t size) + { + Resize(size); + + if (size != 0) + { + memcpy(buffer_.data, data, size); + } + } + + + void PluginMemoryBuffer64::Assign(const std::string& data) + { + if (data.empty()) + { + Assign(NULL, 0); + } + else + { + Assign(data.c_str(), data.size()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,74 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#if ORTHANC_ENABLE_PLUGINS != 1 +# error The plugin support is disabled +#endif + +#include "../../../OrthancFramework/Sources/MallocMemoryBuffer.h" +#include "../Include/orthanc/OrthancCPlugin.h" + +#include <json/value.h> + +namespace Orthanc +{ + class PluginMemoryBuffer64 : public IMemoryBuffer + { + private: + OrthancPluginMemoryBuffer64 buffer_; + + void SanityCheck() const; + + public: + PluginMemoryBuffer64(); + + virtual ~PluginMemoryBuffer64() + { + Clear(); + } + + virtual void MoveToString(std::string& target) ORTHANC_OVERRIDE; + + virtual const void* GetData() const ORTHANC_OVERRIDE; + + virtual size_t GetSize() const ORTHANC_OVERRIDE; + + OrthancPluginMemoryBuffer64* GetObject() + { + return &buffer_; + } + + void Release(OrthancPluginMemoryBuffer64* target); + + void Clear(); + + void Resize(size_t size); + + void Assign(const void* data, + size_t size); + + void Assign(const std::string& data); + }; +}
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -664,5 +664,116 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + + OrthancPluginCompressionType Convert(CompressionType type) + { + switch (type) + { + case CompressionType_None: + return OrthancPluginCompressionType_None; + + case CompressionType_ZlibWithSize: + return OrthancPluginCompressionType_ZlibWithSize; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + CompressionType Convert(OrthancPluginCompressionType type) + { + switch (type) + { + case OrthancPluginCompressionType_None: + return CompressionType_None; + + case OrthancPluginCompressionType_ZlibWithSize: + return CompressionType_ZlibWithSize; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + OrthancPluginStoreStatus Convert(StoreStatus status) + { + switch (status) + { + case StoreStatus_Success: + return OrthancPluginStoreStatus_Success; + + case StoreStatus_AlreadyStored: + return OrthancPluginStoreStatus_AlreadyStored; + + case StoreStatus_Failure: + return OrthancPluginStoreStatus_Failure; + + case StoreStatus_FilteredOut: + return OrthancPluginStoreStatus_FilteredOut; + + case StoreStatus_StorageFull: + return OrthancPluginStoreStatus_StorageFull; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + StoreStatus Convert(OrthancPluginStoreStatus status) + { + switch (status) + { + case OrthancPluginStoreStatus_Success: + return StoreStatus_Success; + + case OrthancPluginStoreStatus_AlreadyStored: + return StoreStatus_AlreadyStored; + + case OrthancPluginStoreStatus_Failure: + return StoreStatus_Failure; + + case OrthancPluginStoreStatus_FilteredOut: + return StoreStatus_FilteredOut; + + case OrthancPluginStoreStatus_StorageFull: + return StoreStatus_StorageFull; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + OrthancPluginQueueOrigin Convert(QueueOrigin origin) + { + switch (origin) + { + case QueueOrigin_Front: + return OrthancPluginQueueOrigin_Front; + + case QueueOrigin_Back: + return OrthancPluginQueueOrigin_Back; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + QueueOrigin Convert(OrthancPluginQueueOrigin origin) + { + switch (origin) + { + case OrthancPluginQueueOrigin_Front: + return QueueOrigin_Front; + + case OrthancPluginQueueOrigin_Back: + return QueueOrigin_Back; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } }
--- a/OrthancServer/Plugins/Engine/PluginsEnumerations.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h Tue Nov 04 15:58:06 2025 +0100 @@ -73,6 +73,18 @@ ResourceType Convert(OrthancPluginResourceType type); OrthancPluginConstraintType Convert(ConstraintType constraint); + + OrthancPluginCompressionType Convert(CompressionType type); + + CompressionType Convert(OrthancPluginCompressionType type); + + OrthancPluginStoreStatus Convert(StoreStatus type); + + StoreStatus Convert(OrthancPluginStoreStatus type); + + OrthancPluginQueueOrigin Convert(QueueOrigin type); + + QueueOrigin Convert(OrthancPluginQueueOrigin type); } }
--- a/OrthancServer/Plugins/Engine/PluginsJob.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -32,6 +32,7 @@ #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../../OrthancFramework/Sources/Toolbox.h" +#include "PluginMemoryBuffer32.h" #include <cassert> @@ -152,53 +153,11 @@ return parameters_.getProgress(parameters_.job); } - - namespace - { - class MemoryBufferRaii : public boost::noncopyable - { - private: - OrthancPluginMemoryBuffer buffer_; - - public: - MemoryBufferRaii() - { - buffer_.size = 0; - buffer_.data = NULL; - } - - ~MemoryBufferRaii() - { - if (buffer_.size != 0) - { - free(buffer_.data); - } - } - - OrthancPluginMemoryBuffer* GetObject() - { - return &buffer_; - } - - void ToJsonObject(Json::Value& target) const - { - if ((buffer_.data == NULL && buffer_.size != 0) || - (buffer_.data != NULL && buffer_.size == 0) || - !Toolbox::ReadJson(target, buffer_.data, buffer_.size) || - target.type() != Json::objectValue) - { - throw OrthancException(ErrorCode_Plugin, - "A job plugin must provide a JSON object as its public content and as its serialization"); - } - } - }; - } - void PluginsJob::GetPublicContent(Json::Value& value) const { if (parameters_.getContent != NULL) { - MemoryBufferRaii target; + PluginMemoryBuffer32 target; OrthancPluginErrorCode code = parameters_.getContent(target.GetObject(), parameters_.job); @@ -236,7 +195,7 @@ { if (parameters_.getSerialized != NULL) { - MemoryBufferRaii target; + PluginMemoryBuffer32 target; int32_t code = parameters_.getContent(target.GetObject(), parameters_.job);
--- a/OrthancServer/Plugins/Engine/PluginsJob.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsJob.h Tue Nov 04 15:58:06 2025 +0100 @@ -26,6 +26,7 @@ #if ORTHANC_ENABLE_PLUGINS == 1 #include "../../../OrthancFramework/Sources/Compatibility.h" // For ORTHANC_OVERRIDE +#include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../../OrthancFramework/Sources/JobsEngine/IJob.h" #include "../Include/orthanc/OrthancCPlugin.h" @@ -83,6 +84,17 @@ // TODO return false; } + + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } + }; }
--- a/OrthancServer/Plugins/Engine/PluginsManager.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsManager.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -32,6 +32,7 @@ #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" #include "../../../OrthancFramework/Sources/Toolbox.h" +#include "../../../OrthancFramework/Sources/SystemToolbox.h" #include <cassert> #include <memory> @@ -51,7 +52,7 @@ namespace Orthanc { PluginsManager::Plugin::Plugin(PluginsManager& pluginManager, - const std::string& path) : + const boost::filesystem::path& path) : library_(path), pluginManager_(pluginManager) { @@ -240,23 +241,22 @@ } - void PluginsManager::RegisterPlugin(const std::string& path) + void PluginsManager::RegisterPlugin(const boost::filesystem::path& path) { if (!boost::filesystem::exists(path)) { - boost::filesystem::path p(path); - std::string extension = p.extension().string(); + std::string extension = path.extension().string(); Toolbox::ToLowerCase(extension); if (extension == PLUGIN_EXTENSION) { // if this is a plugin path, fail to start - throw OrthancException(ErrorCode_SharedLibrary, "Inexistent path to plugin: " + path); + throw OrthancException(ErrorCode_SharedLibrary, "Inexistent path to plugin: " + SystemToolbox::PathToUtf8(path)); } else { // it might be a directory -> just log a warning - LOG(WARNING) << "Inexistent path to plugins: " << path; + LOG(WARNING) << "Inexistent path to plugins: " << SystemToolbox::PathToUtf8(path); return; } } @@ -293,7 +293,7 @@ } - void PluginsManager::ScanFolderForPlugins(const std::string& folder, + void PluginsManager::ScanFolderForPlugins(const boost::filesystem::path& folder, bool isRecursive) { using namespace boost::filesystem; @@ -303,14 +303,14 @@ return; } - CLOG(INFO, PLUGINS) << "Scanning folder " << folder << " for plugins"; + CLOG(INFO, PLUGINS) << "Scanning folder " << SystemToolbox::PathToUtf8(folder) << " for plugins"; directory_iterator end_it; // default construction yields past-the-end for (directory_iterator it(folder); it != end_it; ++it) { - std::string path = it->path().string(); + boost::filesystem::path path = it->path(); if (is_directory(it->status())) {
--- a/OrthancServer/Plugins/Engine/PluginsManager.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Engine/PluginsManager.h Tue Nov 04 15:58:06 2025 +0100 @@ -29,6 +29,7 @@ #include <map> #include <list> +#include <boost/filesystem.hpp> namespace Orthanc { @@ -45,7 +46,7 @@ public: Plugin(PluginsManager& pluginManager, - const std::string& path); + const boost::filesystem::path& path); SharedLibrary& GetSharedLibrary() { @@ -87,9 +88,9 @@ ~PluginsManager(); - void RegisterPlugin(const std::string& path); + void RegisterPlugin(const boost::filesystem::path& path); - void ScanFolderForPlugins(const std::string& path, + void ScanFolderForPlugins(const boost::filesystem::path& path, bool isRecursive); void RegisterServiceProvider(IPluginServiceProvider& provider)
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Tue Nov 04 15:58:06 2025 +0100 @@ -80,6 +80,23 @@ } _OrthancPluginDatabaseAnswerType; + typedef enum + { + OrthancPluginDatabaseTransactionType_ReadOnly = 1, + OrthancPluginDatabaseTransactionType_ReadWrite = 2, + OrthancPluginDatabaseTransactionType_INTERNAL = 0x7fffffff + } OrthancPluginDatabaseTransactionType; + + + typedef enum + { + OrthancPluginDatabaseEventType_DeletedAttachment = 1, + OrthancPluginDatabaseEventType_DeletedResource = 2, + OrthancPluginDatabaseEventType_RemainingAncestor = 3, + OrthancPluginDatabaseEventType_INTERNAL = 0x7fffffff + } OrthancPluginDatabaseEventType; + + typedef struct { const char* uuid; @@ -899,7 +916,9 @@ OrthancPluginDatabaseContext* result = NULL; _OrthancPluginRegisterDatabaseBackend params; - if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType)) + if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType)) { return NULL; } @@ -951,7 +970,9 @@ OrthancPluginDatabaseContext* result = NULL; _OrthancPluginRegisterDatabaseBackendV2 params; - if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType)) + if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType)) { return NULL; } @@ -982,23 +1003,6 @@ **/ /*<! @cond Doxygen_Suppress */ - typedef enum - { - OrthancPluginDatabaseTransactionType_ReadOnly = 1, - OrthancPluginDatabaseTransactionType_ReadWrite = 2, - OrthancPluginDatabaseTransactionType_INTERNAL = 0x7fffffff - } OrthancPluginDatabaseTransactionType; - - - typedef enum - { - OrthancPluginDatabaseEventType_DeletedAttachment = 1, - OrthancPluginDatabaseEventType_DeletedResource = 2, - OrthancPluginDatabaseEventType_RemainingAncestor = 3, - OrthancPluginDatabaseEventType_INTERNAL = 0x7fffffff - } OrthancPluginDatabaseEventType; - - typedef struct { OrthancPluginDatabaseEventType type; @@ -1327,8 +1331,6 @@ } OrthancPluginDatabaseBackendV3; -/*<! @endcond */ - typedef struct { @@ -1348,7 +1350,9 @@ { _OrthancPluginRegisterDatabaseBackendV3 params; - if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType)) + if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseTransactionType) || + sizeof(int32_t) != sizeof(OrthancPluginDatabaseEventType)) { return OrthancPluginErrorCode_Plugin; } @@ -1361,7 +1365,10 @@ return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV3, ¶ms); } - + +/*<! @endcond */ + + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Tue Nov 04 15:58:06 2025 +0100 @@ -16,7 +16,7 @@ * - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback(). * - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback(). * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). - * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2(). + * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea3(). * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4(). * - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback(). * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). @@ -31,6 +31,8 @@ * - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback(). * - Possibly register a callback to discard instances received through DICOM C-STORE using OrthancPluginRegisterIncomingCStoreInstanceFilter(). * - Possibly register a callback to branch a WebDAV virtual filesystem using OrthancPluginRegisterWebDavCollection(). + * - Possibly register a callback to authenticate HTTP requests using OrthancPluginRegisterHttpAuthentication(). + * - Possibly register a callback to store audit logs using OrthancPluginRegisterAuditLogHandler(). * -# <tt>void OrthancPluginFinalize()</tt>: * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -121,7 +123,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 6 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 9 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -182,6 +184,21 @@ #endif +#ifndef ORTHANC_PLUGIN_SINCE_SDK +/** + * This macro is used by the code model generator that produces the + * "OrthancPluginCodeModel.json" file. The code model is notably used + * to generate the Python and Java wrappers. Primitives that are not + * tagged with this macro were introduced before Orthanc 1.0.0. + **/ +# if defined(__clang__) +# define ORTHANC_PLUGIN_SINCE_SDK(version) __attribute__ ((annotate("ORTHANC_PLUGIN_SINCE_SDK " version))) +# else +# define ORTHANC_PLUGIN_SINCE_SDK(version) +# endif +#endif + + /******************************************************************** ** Inclusion of standard libraries. @@ -282,7 +299,7 @@ OrthancPluginErrorCode_DirectoryOverFile = 2000 /*!< The directory to be created is already occupied by a regular file */, OrthancPluginErrorCode_FileStorageCannotWrite = 2001 /*!< Unable to create a subdirectory or a file in the file storage */, OrthancPluginErrorCode_DirectoryExpected = 2002 /*!< The specified path does not point to a directory */, - OrthancPluginErrorCode_HttpPortInUse = 2003 /*!< The TCP port of the HTTP server is privileged or already in use */, + OrthancPluginErrorCode_HttpPortInUse = 2003 /*!< The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist */, OrthancPluginErrorCode_DicomPortInUse = 2004 /*!< The TCP port of the DICOM server is privileged or already in use */, OrthancPluginErrorCode_BadHttpStatusInRest = 2005 /*!< This HTTP status is not allowed in a REST API */, OrthancPluginErrorCode_RegularFileExpected = 2006 /*!< The specified path does not point to a regular file */, @@ -418,10 +435,27 @@ **/ const char* const* headersValues; + + /* -------------------------------------------------- + New in version 1.12.9 + -------------------------------------------------- */ + + /** + * @brief If a HTTP authentication callback is registered, the + * content of the custom payload generated by the callback. + **/ + const void* authenticationPayload; + + /** + * @brief The size of the custom authentication payload (0 if no + * authentication callback is registered). + **/ + uint32_t authenticationPayloadSize; + } OrthancPluginHttpRequest; - typedef enum + typedef enum { /* Generic services */ _OrthancPluginService_LogInfo = 1, @@ -469,7 +503,22 @@ _OrthancPluginService_SetMetricsIntegerValue = 43, /* New in Orthanc 1.12.1 */ _OrthancPluginService_SetCurrentThreadName = 44, /* New in Orthanc 1.12.2 */ _OrthancPluginService_LogMessage = 45, /* New in Orthanc 1.12.4 */ - + _OrthancPluginService_AdoptDicomInstance = 46, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetAttachmentCustomData = 47, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_SetAttachmentCustomData = 48, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_StoreKeyValue = 49, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DeleteKeyValue = 50, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetKeyValue = 51, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_CreateKeysValuesIterator = 52, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_FreeKeysValuesIterator = 53, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorNext = 54, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetKey = 55, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetValue = 56, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_EnqueueValue = 57, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DequeueValue = 58, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetQueueSize = 59, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_SetStableStatus = 60, /* New in Orthanc 1.12.9 */ + _OrthancPluginService_EmitAuditLog = 61, /* New in Orthanc 1.12.9 */ /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -485,13 +534,16 @@ _OrthancPluginService_RegisterIncomingHttpRequestFilter2 = 1010, _OrthancPluginService_RegisterRefreshMetricsCallback = 1011, _OrthancPluginService_RegisterChunkedRestCallback = 1012, /* New in Orthanc 1.5.7 */ - _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, - _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014, + _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, /* New in Orthanc 1.6.0 */ + _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014, /* New in Orthanc 1.6.1 */ _OrthancPluginService_RegisterTranscoderCallback = 1015, /* New in Orthanc 1.7.0 */ _OrthancPluginService_RegisterStorageArea2 = 1016, /* New in Orthanc 1.9.0 */ _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.10.0 */ _OrthancPluginService_RegisterWebDavCollection = 1019, /* New in Orthanc 1.10.1 */ + _OrthancPluginService_RegisterStorageArea3 = 1020, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_RegisterHttpAuthentication = 1021, /* New in Orthanc 1.12.9 */ + _OrthancPluginService_RegisterAuditLogHandler = 1022, /* New in Orthanc 1.12.9 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -562,7 +614,7 @@ _OrthancPluginService_StorageAreaRemove = 5005, _OrthancPluginService_RegisterDatabaseBackendV3 = 5006, /* New in Orthanc 1.9.2 */ _OrthancPluginService_RegisterDatabaseBackendV4 = 5007, /* New in Orthanc 1.12.0 */ - + /* Primitives for handling images */ _OrthancPluginService_GetImagePixelFormat = 6000, _OrthancPluginService_GetImageWidth = 6001, @@ -680,7 +732,7 @@ * This format describes a color image. The pixels are stored in 6 * consecutive bytes. The memory layout is RRGGBB. **/ - OrthancPluginPixelFormat_RGB48 = 7, + OrthancPluginPixelFormat_RGB48 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 7, /** * @brief Graylevel, unsigned 32bpp image. @@ -688,7 +740,7 @@ * The image is graylevel. Each pixel is unsigned and stored in * four bytes. **/ - OrthancPluginPixelFormat_Grayscale32 = 8, + OrthancPluginPixelFormat_Grayscale32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 8, /** * @brief Graylevel, floating-point 32bpp image. @@ -696,7 +748,7 @@ * The image is graylevel. Each pixel is floating-point and stored * in four bytes. **/ - OrthancPluginPixelFormat_Float32 = 9, + OrthancPluginPixelFormat_Float32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 9, /** * @brief Color image in BGRA32 format. @@ -704,7 +756,7 @@ * This format describes a color image. The pixels are stored in 4 * consecutive bytes. The memory layout is BGRA. **/ - OrthancPluginPixelFormat_BGRA32 = 10, + OrthancPluginPixelFormat_BGRA32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 10, /** * @brief Graylevel, unsigned 64bpp image. @@ -712,7 +764,7 @@ * The image is graylevel. Each pixel is unsigned and stored in * eight bytes. **/ - OrthancPluginPixelFormat_Grayscale64 = 11, + OrthancPluginPixelFormat_Grayscale64 ORTHANC_PLUGIN_SINCE_SDK("1.4.0") = 11, _OrthancPluginPixelFormat_INTERNAL = 0x7fffffff } OrthancPluginPixelFormat; @@ -727,7 +779,7 @@ OrthancPluginContentType_Unknown = 0, /*!< Unknown content type */ OrthancPluginContentType_Dicom = 1, /*!< DICOM */ OrthancPluginContentType_DicomAsJson = 2, /*!< JSON summary of a DICOM file */ - OrthancPluginContentType_DicomUntilPixelData = 3, /*!< DICOM Header till pixel data */ + OrthancPluginContentType_DicomUntilPixelData ORTHANC_PLUGIN_SINCE_SDK("1.9.2") = 3, /*!< DICOM Header till pixel data */ _OrthancPluginContentType_INTERNAL = 0x7fffffff } OrthancPluginContentType; @@ -752,7 +804,7 @@ /** * The supported types of changes that can be signaled to the change callback. - * Note: this enum is not used to store changes in the DB ! + * Note: This enumeration is not used to store changes in the database! * @ingroup Callbacks **/ typedef enum @@ -771,11 +823,11 @@ OrthancPluginChangeType_OrthancStopped = 11, /*!< Orthanc is stopping */ OrthancPluginChangeType_UpdatedAttachment = 12, /*!< Some user-defined attachment has changed for this resource */ OrthancPluginChangeType_UpdatedMetadata = 13, /*!< Some user-defined metadata has changed for this resource */ - OrthancPluginChangeType_UpdatedPeers = 14, /*!< The list of Orthanc peers has changed */ - OrthancPluginChangeType_UpdatedModalities = 15, /*!< The list of DICOM modalities has changed */ - OrthancPluginChangeType_JobSubmitted = 16, /*!< New Job submitted */ - OrthancPluginChangeType_JobSuccess = 17, /*!< A Job has completed successfully */ - OrthancPluginChangeType_JobFailure = 18, /*!< A Job has failed */ + OrthancPluginChangeType_UpdatedPeers ORTHANC_PLUGIN_SINCE_SDK("1.4.2") = 14, /*!< The list of Orthanc peers has changed */ + OrthancPluginChangeType_UpdatedModalities ORTHANC_PLUGIN_SINCE_SDK("1.4.2") = 15, /*!< The list of DICOM modalities has changed */ + OrthancPluginChangeType_JobSubmitted ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 16, /*!< New Job submitted */ + OrthancPluginChangeType_JobSuccess ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 17, /*!< A Job has completed successfully */ + OrthancPluginChangeType_JobFailure ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 18, /*!< A Job has failed */ _OrthancPluginChangeType_INTERNAL = 0x7fffffff } OrthancPluginChangeType; @@ -791,6 +843,7 @@ OrthancPluginCompressionType_ZlibWithSize = 1, /*!< zlib, prefixed with uncompressed size (uint64_t) */ OrthancPluginCompressionType_Gzip = 2, /*!< Standard gzip compression */ OrthancPluginCompressionType_GzipWithSize = 3, /*!< gzip, prefixed with uncompressed size (uint64_t) */ + OrthancPluginCompressionType_None ORTHANC_PLUGIN_SINCE_SDK("1.12.8") = 4, /*!< No compression (new in Orthanc 1.12.8) */ _OrthancPluginCompressionType_INTERNAL = 0x7fffffff } OrthancPluginCompressionType; @@ -877,8 +930,8 @@ OrthancPluginDicomToJsonFlags_IncludePixelData = (1 << 3), /*!< Include the pixel data */ OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii = (1 << 4), /*!< Output binary tags as-is, dropping non-ASCII */ OrthancPluginDicomToJsonFlags_ConvertBinaryToNull = (1 << 5), /*!< Signal binary tags as null values */ - OrthancPluginDicomToJsonFlags_StopAfterPixelData = (1 << 6), /*!< Stop processing after pixel data (new in 1.9.1) */ - OrthancPluginDicomToJsonFlags_SkipGroupLengths = (1 << 7), /*!< Skip tags whose element is zero (new in 1.9.1) */ + OrthancPluginDicomToJsonFlags_StopAfterPixelData ORTHANC_PLUGIN_SINCE_SDK("1.9.1") = (1 << 6), /*!< Stop processing after pixel data (new in 1.9.1) */ + OrthancPluginDicomToJsonFlags_SkipGroupLengths ORTHANC_PLUGIN_SINCE_SDK("1.9.1") = (1 << 7), /*!< Skip tags whose element is zero (new in 1.9.1) */ _OrthancPluginDicomToJsonFlags_INTERNAL = 0x7fffffff } OrthancPluginDicomToJsonFlags; @@ -891,7 +944,7 @@ **/ typedef enum { - OrthancPluginCreateDicomFlags_None = 0, /*!< Default mode */ + OrthancPluginCreateDicomFlags_None ORTHANC_PLUGIN_SINCE_SDK("1.2.0") = 0, /*!< Default mode */ OrthancPluginCreateDicomFlags_DecodeDataUriScheme = (1 << 0), /*!< Decode fields encoded using data URI scheme */ OrthancPluginCreateDicomFlags_GenerateIdentifiers = (1 << 1), /*!< Automatically generate DICOM identifiers */ @@ -941,7 +994,7 @@ OrthancPluginInstanceOrigin_RestApi = 3, /*!< Instance received through REST API of Orthanc */ OrthancPluginInstanceOrigin_Plugin = 4, /*!< Instance added to Orthanc by a plugin */ OrthancPluginInstanceOrigin_Lua = 5, /*!< Instance added to Orthanc by a Lua script */ - OrthancPluginInstanceOrigin_WebDav = 6, /*!< Instance received through WebDAV (new in 1.8.0) */ + OrthancPluginInstanceOrigin_WebDav ORTHANC_PLUGIN_SINCE_SDK("1.8.0") = 6, /*!< Instance received through WebDAV (new in 1.8.0) */ _OrthancPluginInstanceOrigin_INTERNAL = 0x7fffffff } OrthancPluginInstanceOrigin; @@ -950,7 +1003,7 @@ /** * The possible status for one single step of a job. **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.4.2") { OrthancPluginJobStepStatus_Success = 1, /*!< The job has successfully executed all its steps */ OrthancPluginJobStepStatus_Failure = 2, /*!< The job has failed while executing this step */ @@ -964,7 +1017,7 @@ * the "paused" condition and the "final" conditions (success, * failure, or canceled). **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.4.2") { OrthancPluginJobStopReason_Success = 1, /*!< The job has succeeded */ OrthancPluginJobStopReason_Paused = 2, /*!< The job was paused, and will be resumed later */ @@ -976,7 +1029,7 @@ /** * The available types of metrics. **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.5.4") { OrthancPluginMetricsType_Default = 0, /*!< Default metrics */ @@ -993,7 +1046,7 @@ * The available modes to export a binary DICOM tag into a DICOMweb * JSON or XML document. **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.5.4") { OrthancPluginDicomWebBinaryMode_Ignore = 0, /*!< Don't include binary tags */ OrthancPluginDicomWebBinaryMode_InlineBinary = 1, /*!< Inline encoding using Base64 */ @@ -1006,7 +1059,7 @@ * storage commitment. * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.6.0") { /** * Success: The DICOM instance is properly stored in the SCP @@ -1054,7 +1107,7 @@ /** * The action to be taken after ReceivedInstanceCallback is triggered **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.10.0") { OrthancPluginReceivedInstanceAction_KeepAsIs = 1, /*!< Keep the instance as is */ OrthancPluginReceivedInstanceAction_Modify = 2, /*!< Modify the instance */ @@ -1068,7 +1121,7 @@ * Mode specifying how to load a DICOM instance. * @see OrthancPluginLoadDicomInstance() **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.1") { /** * Load the whole DICOM file, including pixel data @@ -1098,7 +1151,7 @@ * These values must match those of enumeration "LogLevel" in the * Orthanc Core. **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.4") { OrthancPluginLogLevel_Error = 0, /*!< Error log level */ OrthancPluginLogLevel_Warning = 1, /*!< Warning log level */ @@ -1115,7 +1168,7 @@ * These values must match those of enumeration "LogCategory" in the * Orthanc Core. **/ - typedef enum + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.4") { OrthancPluginLogCategory_Generic = (1 << 0), /*!< Generic (default) category */ OrthancPluginLogCategory_Plugins = (1 << 1), /*!< Plugin engine related logs (shall not be used by plugins) */ @@ -1130,6 +1183,59 @@ /** + * The store status related to the adoption of a DICOM instance. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + { + OrthancPluginStoreStatus_Success = 0, /*!< The file has been stored/adopted */ + OrthancPluginStoreStatus_AlreadyStored = 1, /*!< The file has already been stored/adopted (only if OverwriteInstances is set to false)*/ + OrthancPluginStoreStatus_Failure = 2, /*!< The file could not be stored/adopted */ + OrthancPluginStoreStatus_FilteredOut = 3, /*!< The file has been filtered out by a Lua script or a plugin */ + OrthancPluginStoreStatus_StorageFull = 4, /*!< The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)*/ + + _OrthancPluginStoreStatus_INTERNAL = 0x7fffffff + } OrthancPluginStoreStatus; + + + /** + * The supported modes to remove an element from a queue. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + { + OrthancPluginQueueOrigin_Front = 0, /*!< Dequeue from the front of the queue */ + OrthancPluginQueueOrigin_Back = 1, /*!< Dequeue from the back of the queue */ + + _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff + } OrthancPluginQueueOrigin; + + + /** + * The "Stable" status of a resource. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + { + OrthancPluginStableStatus_Stable = 0, /*!< The resource is stable */ + OrthancPluginStableStatus_Unstable = 1, /*!< The resource is unstable */ + + _OrthancPluginStableStatus_INTERNAL = 0x7fffffff + } OrthancPluginStableStatus; + + + /** + * Status associated with the authentication of a HTTP request. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + { + OrthancPluginHttpAuthenticationStatus_Granted = 0, /*!< The authentication has been granted */ + OrthancPluginHttpAuthenticationStatus_Unauthorized = 1, /*!< The authentication has failed (401 HTTP status) */ + OrthancPluginHttpAuthenticationStatus_Forbidden = 2, /*!< The authorization has failed (403 HTTP status) */ + OrthancPluginHttpAuthenticationStatus_Redirect = 3, /*!< Redirect to another path (307 HTTP status, e.g., for login) */ + + _OrthancPluginHttpAuthenticationStatus_INTERNAL = 0x7fffffff + } OrthancPluginHttpAuthenticationStatus; + + + /** * @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 @@ -1158,7 +1264,7 @@ * content of the buffer is not useful anymore, it must be free by a * call to ::OrthancPluginFreeMemoryBuffer64(). **/ - typedef struct + ORTHANC_PLUGIN_SINCE_SDK("1.9.0") typedef struct { /** * @brief The content of the buffer. @@ -1226,7 +1332,8 @@ * @brief Opaque structure to an object that represents a C-Find query. * @ingroup DicomCallbacks **/ - typedef struct _OrthancPluginFindQuery_t OrthancPluginFindQuery; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.1.0") + _OrthancPluginFindQuery_t OrthancPluginFindQuery; @@ -1234,7 +1341,8 @@ * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists. * @ingroup DicomCallbacks **/ - typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindAnswers; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.1.0") + _OrthancPluginFindAnswers_t OrthancPluginFindAnswers; @@ -1242,7 +1350,8 @@ * @brief Opaque structure to an object that can be used to check whether a DICOM instance matches a C-Find query. * @ingroup Toolbox **/ - typedef struct _OrthancPluginFindMatcher_t OrthancPluginFindMatcher; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.2.0") + _OrthancPluginFindMatcher_t OrthancPluginFindMatcher; @@ -1250,7 +1359,8 @@ * @brief Opaque structure to the set of remote Orthanc Peers that are known to the local Orthanc server. * @ingroup Toolbox **/ - typedef struct _OrthancPluginPeers_t OrthancPluginPeers; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + _OrthancPluginPeers_t OrthancPluginPeers; @@ -1258,7 +1368,8 @@ * @brief Opaque structure to a job to be executed by Orthanc. * @ingroup Toolbox **/ - typedef struct _OrthancPluginJob_t OrthancPluginJob; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + _OrthancPluginJob_t OrthancPluginJob; @@ -1267,7 +1378,8 @@ * document used in DICOMweb. * @ingroup Toolbox **/ - typedef struct _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.5.4") + _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode; @@ -1277,7 +1389,7 @@ **/ typedef OrthancPluginErrorCode (*OrthancPluginRestCallback) ( OrthancPluginRestOutput* output, - const char* url, + const char* uri, const OrthancPluginHttpRequest* request); @@ -1367,8 +1479,7 @@ * @param type The content type corresponding to this file. * @return 0 if success, other value if error. * @ingroup Callbacks - * @deprecated New plugins should use OrthancPluginStorageRead2 - * + * * @warning The "content" buffer *must* have been allocated using * the "malloc()" function of your C standard library (i.e. nor * "new[]", neither a pointer to a buffer). The "free()" function of @@ -1443,6 +1554,83 @@ /** + * @brief Callback for writing to the storage area. + * + * Signature of a callback function that is triggered when Orthanc writes a file to the storage area. + * + * @param customData Custom, plugin-specific data associated with the attachment (out). + * It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer(). The core of Orthanc will free it. + * If the plugin does not generate custom data, leave `customData` unchanged; it will default to an empty value. + * @param uuid The UUID of the file. + * @param content The content of the file (might be compressed data). + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @param compressionType The compression algorithm that was used to encode `content` + * (the absence of compression is indicated using `OrthancPluginCompressionType_None`). + * @param dicomInstance The DICOM instance being stored. Equals `NULL` if not storing a DICOM instance. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate2) ( + OrthancPluginMemoryBuffer* customData, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type, + OrthancPluginCompressionType compressionType, + const OrthancPluginDicomInstance* dicomInstance); + + + + /** + * @brief Callback for reading a range of a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a portion of a file from the storage area. Orthanc + * indicates the start position and the length of the range. + * + * @param target Memory buffer where to store the content of the range. + * The memory buffer is allocated and freed by Orthanc. The length of the range + * of interest corresponds to the size of this buffer. + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @param rangeStart Start position of the requested range in the file. + * @param customData The custom data of the file of interest. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + uint32_t customDataSize); + + + + /** + * @brief Callback for removing a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * deletes a file from the storage area. + * + * @param uuid The UUID of the file to be removed. + * @param type The content type corresponding to this file. + * @param customData The custom data of the file to be removed. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) ( + const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint32_t customDataSize); + + + /** * @brief Callback to handle the C-Find SCP requests for worklists. * * Signature of a callback function that is triggered when Orthanc @@ -1509,6 +1697,10 @@ * concurrently by different threads of the Web server of * Orthanc. You must implement proper locking if applicable. * + * Note that if you are using HTTP basic authentication, you can + * extract the username from the "Authorization" HTTP header. The + * value of that header contains username:pwd encoded in base64. + * * @param method The HTTP method used by the request. * @param uri The URI of interest. * @param ip The IP address of the HTTP client. @@ -1984,6 +2176,7 @@ * @see OrthancPluginCheckVersion() * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.0") ORTHANC_PLUGIN_INLINE int32_t OrthancPluginCheckVersionAdvanced( OrthancPluginContext* context, int32_t expectedMajor, @@ -2010,13 +2203,19 @@ sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) || sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) || + sizeof(int32_t) != sizeof(OrthancPluginJobStopReason) || sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || 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) || sizeof(int32_t) != sizeof(OrthancPluginLogLevel) || - sizeof(int32_t) != sizeof(OrthancPluginLogCategory)) + sizeof(int32_t) != sizeof(OrthancPluginLogCategory) || + sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) || + sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) || + sizeof(int32_t) != sizeof(OrthancPluginStableStatus) || + sizeof(int32_t) != sizeof(OrthancPluginHttpAuthenticationStatus)) { /* Mismatch in the size of the enumerations */ return 0; @@ -2128,6 +2327,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param buffer The memory buffer to release. **/ + ORTHANC_PLUGIN_SINCE_SDK("1.9.0") ORTHANC_PLUGIN_INLINE void OrthancPluginFreeMemoryBuffer64( OrthancPluginContext* context, OrthancPluginMemoryBuffer64* buffer) @@ -3323,7 +3523,7 @@ * @param read The callback function to read a file from the custom storage area. * @param remove The callback function to remove a file from the custom storage area. * @ingroup Callbacks - * @deprecated Please use OrthancPluginRegisterStorageArea2() + * @deprecated New plugins should use OrthancPluginRegisterStorageArea3() **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea( OrthancPluginContext* context, @@ -3528,6 +3728,7 @@ * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()"). * @param uri The root URI for this plugin. **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.4") ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri2( OrthancPluginContext* context, const char* plugin, @@ -3578,6 +3779,7 @@ * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()"). * @param description The description. **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.4") ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription2( OrthancPluginContext* context, const char* plugin, @@ -3628,6 +3830,7 @@ * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()"). * @param javascript The custom JavaScript code. **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.4") ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer2( OrthancPluginContext* context, const char* plugin, @@ -4911,6 +5114,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiPut()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaCreate( OrthancPluginContext* context, @@ -4955,6 +5160,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiGet()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRead( OrthancPluginContext* context, @@ -4994,6 +5201,8 @@ * @ingroup Callbacks * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiDelete()" on * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. **/ ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRemove( OrthancPluginContext* context, @@ -5143,6 +5352,7 @@ * @see OrthancPluginRegisterDictionaryTag() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.2.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterPrivateDictionaryTag( OrthancPluginContext* context, uint16_t group, @@ -5935,6 +6145,7 @@ * @see OrthancPluginSendMultipartItem() * @ingroup REST **/ + ORTHANC_PLUGIN_SINCE_SDK("1.0.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem2( OrthancPluginContext* context, OrthancPluginRestOutput* output, @@ -5973,6 +6184,7 @@ * @ingroup Callbacks * @deprecated Please instead use OrthancPluginRegisterIncomingHttpRequestFilter2() **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter( OrthancPluginContext* context, OrthancPluginIncomingHttpRequestFilter callback) @@ -6048,6 +6260,7 @@ * @see OrthancPluginCallPeerApi() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpClient( OrthancPluginContext* context, OrthancPluginMemoryBuffer* answerBody, @@ -6103,6 +6316,7 @@ * containing the UUID. This string must be freed by OrthancPluginFreeString(). * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateUuid( OrthancPluginContext* context) { @@ -6142,6 +6356,7 @@ * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterFindCallback( OrthancPluginContext* context, OrthancPluginFindCallback callback) @@ -6181,6 +6396,7 @@ * @ingroup DicomCallbacks * @see OrthancPluginCreateDicom() **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginFindAddAnswer( OrthancPluginContext* context, OrthancPluginFindAnswers* answers, @@ -6210,6 +6426,7 @@ * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginFindMarkIncomplete( OrthancPluginContext* context, OrthancPluginFindAnswers* answers) @@ -6234,6 +6451,7 @@ * @return The number of tags. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFindQuerySize( OrthancPluginContext* context, const OrthancPluginFindQuery* query) @@ -6271,6 +6489,7 @@ * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetFindQueryTag( OrthancPluginContext* context, uint16_t* group, @@ -6302,6 +6521,7 @@ * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginGetFindQueryTagName( OrthancPluginContext* context, const OrthancPluginFindQuery* query, @@ -6340,6 +6560,7 @@ * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginGetFindQueryValue( OrthancPluginContext* context, const OrthancPluginFindQuery* query, @@ -6388,6 +6609,7 @@ * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterMoveCallback( OrthancPluginContext* context, OrthancPluginMoveCallback callback, @@ -6427,6 +6649,7 @@ * @return The newly allocated matcher. It must be freed with OrthancPluginFreeFindMatcher(). * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.2.0") ORTHANC_PLUGIN_INLINE OrthancPluginFindMatcher* OrthancPluginCreateFindMatcher( OrthancPluginContext* context, const void* query, @@ -6465,6 +6688,7 @@ * @param matcher The matcher of interest. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.2.0") ORTHANC_PLUGIN_INLINE void OrthancPluginFreeFindMatcher( OrthancPluginContext* context, OrthancPluginFindMatcher* matcher) @@ -6498,6 +6722,7 @@ * @return 1 if the DICOM instance matches the query, 0 otherwise. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.2.0") ORTHANC_PLUGIN_INLINE int32_t OrthancPluginFindMatcherIsMatch( OrthancPluginContext* context, const OrthancPluginFindMatcher* matcher, @@ -6540,6 +6765,7 @@ * @return 0 if success, other value if error. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.3.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter2( OrthancPluginContext* context, OrthancPluginIncomingHttpRequestFilter2 callback) @@ -6568,6 +6794,7 @@ * This structure must be freed with OrthancPluginFreePeers(). * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE OrthancPluginPeers* OrthancPluginGetPeers( OrthancPluginContext* context) { @@ -6602,6 +6829,7 @@ * @param peers The data structure describing the Orthanc peers. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE void OrthancPluginFreePeers( OrthancPluginContext* context, OrthancPluginPeers* peers) @@ -6632,6 +6860,7 @@ * @result The number of peers. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetPeersCount( OrthancPluginContext* context, const OrthancPluginPeers* peers) @@ -6680,6 +6909,7 @@ * @result The symbolic name, or NULL in the case of an error. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerName( OrthancPluginContext* context, const OrthancPluginPeers* peers, @@ -6721,6 +6951,7 @@ * @result The URL, or NULL in the case of an error. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUrl( OrthancPluginContext* context, const OrthancPluginPeers* peers, @@ -6767,6 +6998,7 @@ * @result The value of the user property, or NULL if it is not defined. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUserProperty( OrthancPluginContext* context, const OrthancPluginPeers* peers, @@ -6848,6 +7080,7 @@ * @see OrthancPluginHttpClient() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCallPeerApi( OrthancPluginContext* context, OrthancPluginMemoryBuffer* answerBody, @@ -6934,6 +7167,7 @@ * @ingroup Toolbox * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3. **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob( OrthancPluginContext *context, void *job, @@ -7020,6 +7254,7 @@ * as long as it is not submitted with OrthancPluginSubmitJob(). * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.11.3") ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob2( OrthancPluginContext *context, void *job, @@ -7075,6 +7310,7 @@ * @param job The job. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE void OrthancPluginFreeJob( OrthancPluginContext* context, OrthancPluginJob* job) @@ -7107,6 +7343,7 @@ * @return ID of the newly-submitted job. This string must be freed by OrthancPluginFreeString(). * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE char *OrthancPluginSubmitJob( OrthancPluginContext *context, OrthancPluginJob *job, @@ -7152,6 +7389,7 @@ * @param unserializer The job unserializer. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterJobsUnserializer( OrthancPluginContext* context, OrthancPluginJobsUnserializer unserializer) @@ -7188,6 +7426,7 @@ * @param log Whether to also write the detailed error to the Orthanc logs. * @ingroup REST **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.0") ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpErrorDetails( OrthancPluginContext* context, OrthancPluginRestOutput* output, @@ -7220,6 +7459,7 @@ * string, do not free it. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.0") ORTHANC_PLUGIN_INLINE const char* OrthancPluginAutodetectMimeType( OrthancPluginContext* context, const char* path) @@ -7266,6 +7506,7 @@ * @ingroup Toolbox * @see OrthancPluginSetMetricsIntegerValue() **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.4") ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue( OrthancPluginContext* context, const char* name, @@ -7297,6 +7538,7 @@ * @param callback The callback function to handle the refresh. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.4") ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRefreshMetricsCallback( OrthancPluginContext* context, OrthancPluginRefreshMetricsCallback callback) @@ -7333,6 +7575,7 @@ * @deprecated OrthancPluginEncodeDicomWebJson2() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.4") ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson( OrthancPluginContext* context, const void* dicom, @@ -7375,6 +7618,7 @@ * @deprecated OrthancPluginEncodeDicomWebXml2() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.4") ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml( OrthancPluginContext* context, const void* dicom, @@ -7427,6 +7671,7 @@ * @see OrthancPluginCreateDicom() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson2( OrthancPluginContext* context, const void* dicom, @@ -7471,6 +7716,7 @@ * @see OrthancPluginCreateDicom() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2( OrthancPluginContext* context, const void* dicom, @@ -7684,6 +7930,7 @@ * @see OrthancPluginHttpClient() * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.7") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginChunkedHttpClient( OrthancPluginContext* context, void* answer, @@ -7745,7 +7992,8 @@ * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer. * @ingroup Callbacks **/ - typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.5.7") + _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader; @@ -7760,13 +8008,13 @@ * * @see OrthancPluginRegisterChunkedRestCallback() * @param reader Memory location that must be filled with the newly-created reader. - * @param url The URI that is accessed. + * @param uri The URI that is accessed. * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used. * @return 0 if success, or the error code if failure. **/ typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) ( OrthancPluginServerChunkedRequestReader** reader, - const char* url, + const char* uri, const OrthancPluginHttpRequest* request); @@ -7866,6 +8114,7 @@ * * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.7") ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterChunkedRestCallback( OrthancPluginContext* context, const char* pathRegularExpression, @@ -7917,6 +8166,7 @@ * OrthancPluginFreeString(). * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.7") ORTHANC_PLUGIN_INLINE char* OrthancPluginGetTagName( OrthancPluginContext* context, uint16_t group, @@ -8042,6 +8292,7 @@ * @return 0 if success, other value if error. * @ingroup DicomCallbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.6.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterStorageCommitmentScpCallback( OrthancPluginContext* context, OrthancPluginStorageCommitmentFactory factory, @@ -8103,6 +8354,7 @@ * @return 0 if success, other value if error. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.6.1") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingDicomInstanceFilter( OrthancPluginContext* context, OrthancPluginIncomingDicomInstanceFilter callback) @@ -8166,6 +8418,7 @@ * @return 0 if success, other value if error. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.10.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingCStoreInstanceFilter( OrthancPluginContext* context, OrthancPluginIncomingCStoreInstanceFilter callback) @@ -8243,6 +8496,7 @@ * @return 0 if success, other value if error. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.10.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterReceivedInstanceCallback( OrthancPluginContext* context, OrthancPluginReceivedInstanceCallback callback) @@ -8266,6 +8520,7 @@ * transfer syntax UID. This string must be freed by OrthancPluginFreeString(). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.6.1") ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance) @@ -8301,6 +8556,7 @@ * the tag is missing, or "-1" in the case of an error. * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.6.1") ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance) @@ -8351,6 +8607,7 @@ * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance( OrthancPluginContext* context, const void* buffer, @@ -8389,6 +8646,7 @@ * @param dicom The DICOM instance. * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE void OrthancPluginFreeDicomInstance( OrthancPluginContext* context, OrthancPluginDicomInstance* dicom) @@ -8426,6 +8684,7 @@ * @return The number of frames (will be zero in the case of an error). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance) @@ -8466,6 +8725,7 @@ * @return 0 if success, or the error code if failure. * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame( OrthancPluginContext* context, OrthancPluginMemoryBuffer* target, @@ -8494,6 +8754,7 @@ * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance, @@ -8533,6 +8794,7 @@ * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance( OrthancPluginContext* context, const void* buffer, @@ -8571,6 +8833,7 @@ * @return 0 if success, or the error code if failure. * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance( OrthancPluginContext* context, OrthancPluginMemoryBuffer* target, @@ -8603,6 +8866,7 @@ * @ingroup DicomInstance * @see OrthancPluginDicomBufferToJson() **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance, @@ -8646,6 +8910,7 @@ * be freed by OrthancPluginFreeString(). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance, @@ -8686,6 +8951,7 @@ * be freed by OrthancPluginFreeString(). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml( OrthancPluginContext* context, const OrthancPluginDicomInstance* instance, @@ -8758,6 +9024,7 @@ * @return 0 if success, other value if error. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterTranscoderCallback( OrthancPluginContext* context, OrthancPluginTranscoderCallback callback) @@ -8793,6 +9060,7 @@ * @return 0 if success, or the error code if failure. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.7.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer( OrthancPluginContext* context, OrthancPluginMemoryBuffer* target, @@ -8831,6 +9099,7 @@ * This string must be freed by OrthancPluginFreeString(). * @ingroup Orthanc **/ + ORTHANC_PLUGIN_SINCE_SDK("1.8.1") ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateRestApiAuthorizationToken( OrthancPluginContext* context) { @@ -8877,6 +9146,7 @@ * @return 0 if success, or the error code if failure. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.9.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer64( OrthancPluginContext* context, OrthancPluginMemoryBuffer64* target, @@ -8913,7 +9183,9 @@ * If this feature is not supported by the plugin, this value can be set to NULL. * @param remove The callback function to remove a file from the custom storage area. * @ingroup Callbacks - **/ + * @deprecated New plugins should use OrthancPluginRegisterStorageArea3() + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.9.0") ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea2( OrthancPluginContext* context, OrthancPluginStorageCreate create, @@ -8961,6 +9233,7 @@ * @see OrthancPluginCreateDicom() * @see OrthancPluginDicomBufferToJson() **/ + ORTHANC_PLUGIN_SINCE_SDK("1.9.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom2( OrthancPluginContext* context, OrthancPluginMemoryBuffer* target, @@ -9029,6 +9302,7 @@ * @see OrthancPluginRestApiGet2(), OrthancPluginRestApiPost(), OrthancPluginRestApiPut(), OrthancPluginRestApiDelete() * @ingroup Orthanc **/ + ORTHANC_PLUGIN_SINCE_SDK("1.9.2") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCallRestApi( OrthancPluginContext* context, OrthancPluginMemoryBuffer* answerBody, @@ -9067,7 +9341,8 @@ * @brief Opaque structure that represents a WebDAV collection. * @ingroup Callbacks **/ - typedef struct _OrthancPluginWebDavCollection_t OrthancPluginWebDavCollection; + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.10.1") + _OrthancPluginWebDavCollection_t OrthancPluginWebDavCollection; /** @@ -9310,6 +9585,7 @@ * @return 0 if success, other value if error. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.10.1") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWebDavCollection( OrthancPluginContext* context, const char* uri, @@ -9343,6 +9619,7 @@ * string, do not free it. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.11.1") ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetDatabaseServerIdentifier( OrthancPluginContext* context) { @@ -9364,6 +9641,41 @@ } + typedef struct + { + OrthancPluginStorageCreate2 create; + OrthancPluginStorageReadRange2 readRange; + OrthancPluginStorageRemove2 remove; + } _OrthancPluginRegisterStorageArea3; + + /** + * @brief Register a custom storage area, with support for custom data. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * @param remove The callback function to remove a file from the custom storage area. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3( + OrthancPluginContext* context, + OrthancPluginStorageCreate2 create, + OrthancPluginStorageReadRange2 readRange, + OrthancPluginStorageRemove2 remove) + { + _OrthancPluginRegisterStorageArea3 params; + params.create = create; + params.readRange = readRange; + params.remove = remove; + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, ¶ms); + } + /** * @brief Signature of a callback function that is triggered when * the Orthanc core requests an operation from the database plugin. @@ -9408,6 +9720,7 @@ * @return 0 if success, other value if error. * @ingroup Callbacks **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.0") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV4( OrthancPluginContext* context, void* backend, @@ -9445,6 +9758,7 @@ * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). * @ingroup DicomInstance **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.1") ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginLoadDicomInstance( OrthancPluginContext* context, const char* instanceId, @@ -9491,6 +9805,7 @@ * @ingroup Toolbox * @see OrthancPluginSetMetricsValue() **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.1") ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsIntegerValue( OrthancPluginContext* context, const char* name, @@ -9518,6 +9833,7 @@ * @return 0 if success, other value if error. * @ingroup Toolbox **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.2") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetCurrentThreadName( OrthancPluginContext* context, const char* threadName) @@ -9551,6 +9867,7 @@ * @param category The category. * @param level The level of the message. **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.4") ORTHANC_PLUGIN_INLINE void OrthancPluginLogMessage( OrthancPluginContext* context, const char* message, @@ -9589,6 +9906,7 @@ * @see OrthancPluginSendStreamChunk() * @ingroup REST **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.6") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartStreamAnswer( OrthancPluginContext* context, OrthancPluginRestOutput* output, @@ -9616,6 +9934,7 @@ * @see OrthancPluginStartStreamAnswer() * @ingroup REST **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.6") ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendStreamChunk( OrthancPluginContext* context, OrthancPluginRestOutput* output, @@ -9631,10 +9950,765 @@ } + typedef struct + { + OrthancPluginMemoryBuffer* instanceId; + OrthancPluginMemoryBuffer* attachmentUuid; + OrthancPluginStoreStatus* storeStatus; + const void* dicom; + uint64_t dicomSize; + const void* customData; + uint32_t customDataSize; + } _OrthancPluginAdoptDicomInstance; + + /** + * @brief Adopt a DICOM instance read from the filesystem. + * + * This function requests Orthanc to create a DICOM resource at the + * "Instance" level in its database, using the content of a DICOM + * instance read from the filesystem. The newly created DICOM + * resource is associated with an attachment whose content type is + * "OrthancPluginContentType_Dicom". The attachment is associated + * with the provided custom data. + * + * This function should only be used in combination with a custom + * storage area featuring support for custom data (i.e., installed + * using OrthancPluginRegisterStorageArea3()). The custom storage + * area is responsible for *not* duplicating the DICOM file into the + * storage area of Orthanc, hence the name "Adopt". The support for + * custom data is necessary for the custom storage area to + * distinguish between adopted and non-adopted DICOM instances. + * + * Check out the "AdoptDicomInstance" plugin in the source + * distribution of Orthanc for a working sample: + * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Plugins/Samples/AdoptDicomInstance/ + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The target memory buffer that will be filled by + * the Orthanc core with the public identifier of the newly created + * instance. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param attachmentUuid The target memory buffer that will be + * filled by the Orthanc core with the UUID of the newly created + * attachment corresponding to the adopted DICOM instance. It must + * be freed with OrthancPluginFreeMemoryBuffer(). + * @param storeStatus Variable that will be filled by the Orthanc core + * with the status of store operation. + * @param dicom Pointer to the DICOM instance read from the filesystem. + * @param dicomSize Size of the DICOM instance. + * @param customData The custom data to associated with the attachment. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginAdoptDicomInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* instanceId, /* out */ + OrthancPluginMemoryBuffer* attachmentUuid, /* out */ + OrthancPluginStoreStatus* storeStatus, /* out */ + const void* dicom, + uint64_t dicomSize, + const void* customData, + uint32_t customDataSize) + { + _OrthancPluginAdoptDicomInstance params; + params.instanceId = instanceId; + params.attachmentUuid = attachmentUuid; + params.storeStatus = storeStatus; + params.dicom = dicom; + params.dicomSize = dicomSize; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_AdoptDicomInstance, ¶ms); + } + + + typedef struct + { + OrthancPluginMemoryBuffer* customData; + const char* attachmentUuid; + } _OrthancPluginGetAttachmentCustomData; + + /** + * @brief Retrieve the custom data associated with an attachment in the Orthanc database. + * + * If no custom data is associated with the attachment of interest, + * the target memory buffer is filled with the NULL value and a zero size. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param customData Memory buffer where to store the retrieved value. It must be freed + * by the plugin by calling OrthancPluginFreeMemoryBuffer(). + * @param attachmentUuid The UUID of the attachment of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetAttachmentCustomData( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* customData, /* out */ + const char* attachmentUuid /* in */) + { + _OrthancPluginGetAttachmentCustomData params; + params.customData = customData; + params.attachmentUuid = attachmentUuid; + + return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* attachmentUuid; + const void* customData; + uint32_t customDataSize; + } _OrthancPluginSetAttachmentCustomData; + + /** + * @brief Update the custom data associated with an attachment in the Orthanc database. + * + * This function is notably used in the "orthanc-advanced-storage" + * when the plugin moves an attachment. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param attachmentUuid The UUID of the attachment of interest. + * @param customData The value to store. + * @param customDataSize The size of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetAttachmentCustomData( + OrthancPluginContext* context, + const char* attachmentUuid, /* in */ + const void* customData, /* in */ + uint32_t customDataSize /* in */) + { + _OrthancPluginSetAttachmentCustomData params; + params.attachmentUuid = attachmentUuid; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_SetAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + const void* value; + uint32_t valueSize; + } _OrthancPluginStoreKeyValue; + + /** + * @brief Store a key-value pair in the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @param value The value to store. + * @param valueSize The length of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginStoreKeyValue params; + params.storeId = storeId; + params.key = key; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + } _OrthancPluginDeleteKeyValue; + + /** + * @brief Delete a key-value pair from the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginDeleteKeyValue params; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, ¶ms); + } + + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* storeId; + const char* key; + } _OrthancPluginGetKeyValue; + + /** + * @brief Get the value associated with a key in the Orthanc key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. the key exists in the key-value store. + * @param target Memory buffer where to store the retrieved value. It must be freed + * by the plugin by calling OrthancPluginFreeMemoryBuffer(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to retrieve from the store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginGetKeyValue params; + params.found = found; + params.target = target; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_GetKeyValue, ¶ms); + } + + + /** + * @brief Opaque structure that represents an iterator over the keys and values of + * a key-value store. + * @ingroup Callbacks + **/ + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator; + + + typedef struct + { + OrthancPluginKeysValuesIterator** target; + const char* storeId; + } _OrthancPluginCreateKeysValuesIterator; + + + /** + * @brief Create an iterator over the key-value pairs of a key-value store in the Orthanc database. + * + * The iterator loops over the keys according to the lexicographical order. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @return The newly allocated iterator, or NULL in the case of an error. + * The iterator must be freed by calling OrthancPluginFreeKeysValuesIterator(). + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginKeysValuesIterator* OrthancPluginCreateKeysValuesIterator( + OrthancPluginContext* context, + const char* storeId) + { + OrthancPluginKeysValuesIterator* target = NULL; + + _OrthancPluginCreateKeysValuesIterator params; + params.target = ⌖ + params.storeId = storeId; + + if (context->InvokeService(context, _OrthancPluginService_CreateKeysValuesIterator, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginFreeKeysValuesIterator; + + /** + * @brief Free an iterator over a key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeKeysValuesIterator( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + _OrthancPluginFreeKeysValuesIterator params; + params.iterator = iterator; + + context->InvokeService(context, _OrthancPluginService_FreeKeysValuesIterator, ¶ms); + } + + + typedef struct + { + uint8_t* done; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorNext; + + /** + * @brief Read the next element of an iterator over a key-value store. + * + * The iterator loops over the keys according to the lexicographical order. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param done Pointer to a Boolean that is set to "true" iff. the iterator has reached the end of the store. + * @param iterator The iterator of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorNext( + OrthancPluginContext* context, + uint8_t* done, /* out */ + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorNext params; + params.done = done; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorNext, ¶ms); + } + + + typedef struct + { + const char** target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetKey; + + /** + * @brief Get the current key of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + * @return The current key, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetKey( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + const char* target = NULL; + + _OrthancPluginKeysValuesIteratorGetKey params; + params.target = ⌖ + params.iterator = iterator; + + if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetKey, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + return NULL; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetValue; + + /** + * @brief Get the current value of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Memory buffer where to store the value that has been retrieved from the key-value store. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param iterator The iterator of interest. + * @return The current value, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorGetValue( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target /* out */, + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorGetValue params; + params.target = target; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetValue, ¶ms); + } + + + typedef struct + { + const char* queueId; + const void* value; + uint32_t valueSize; + } _OrthancPluginEnqueueValue; + + /** + * @brief Append a value to the back of a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param value The value to store. + * @param valueSize The size of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue( + OrthancPluginContext* context, + const char* queueId, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginEnqueueValue params; + params.queueId = queueId; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_EnqueueValue, ¶ms); + } + + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* queueId; + OrthancPluginQueueOrigin origin; + } _OrthancPluginDequeueValue; + + /** + * @brief Dequeue a value from a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. a value has been dequeued. + * @param target Memory buffer where to store the value that has been retrieved from the queue. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param origin The position from where the value is dequeued (back for LIFO, front for FIFO). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* queueId, /* in */ + OrthancPluginQueueOrigin origin /* in */) + { + _OrthancPluginDequeueValue params; + params.found = found; + params.target = target; + params.queueId = queueId; + params.origin = origin; + + return context->InvokeService(context, _OrthancPluginService_DequeueValue, ¶ms); + } + + + typedef struct + { + const char* queueId; + uint64_t* size; + } _OrthancPluginGetQueueSize; + + /** + * @brief Get the number of elements in a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param size The number of elements in the queue. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetQueueSize( + OrthancPluginContext* context, + const char* queueId, /* in */ + uint64_t* size /* out */) + { + _OrthancPluginGetQueueSize params; + params.queueId = queueId; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_GetQueueSize, ¶ms); + } + + + typedef struct + { + const char* resourceId; + OrthancPluginStableStatus stableStatus; + uint8_t* statusHasChanged; + } _OrthancPluginSetStableStatus; + + /** + * @brief Change the "Stable" status of a resource. + * + * Forcing a resource to "Stable" if it is currently "Unstable" will + * change its "Stable" status AND trigger a new "Stable" change, + * which will also trigger listener callbacks. + * + * Forcing a resource to "Stable" if it is already "Stable" has no + * effect (no-op). + * + * Forcing a resource to "Unstable" will change its "Stable" status + * to "Unstable" AND reset its stabilization period, no matter its + * initial state. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param statusHasChanged Whether the status has changed (1) or not (0) during the execution of this command. + * @param resourceId The Orthanc identifier of the DICOM resource of interest. + * @param stableStatus The new stable status of the resource of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetStableStatus( + OrthancPluginContext* context, + uint8_t* statusHasChanged, /* out */ + const char* resourceId, /* in */ + OrthancPluginStableStatus stableStatus /* in */) + { + _OrthancPluginSetStableStatus params; + params.resourceId = resourceId; + params.stableStatus= stableStatus; + params.statusHasChanged = statusHasChanged; + + return context->InvokeService(context, _OrthancPluginService_SetStableStatus, ¶ms); + } + + + /** + * @brief Callback to authenticate a HTTP request. + * + * Signature of a callback function that authenticates every incoming HTTP request. + * + * @param status The output status of the authentication. + * @param customPayload If status is `OrthancPluginHttpAuthenticationStatus_Granted`, + * a custom payload that will be provided to the HTTP handler callback. + * @param redirection If status is `OrthancPluginHttpAuthenticationStatus_Redirect`, + * a buffer filled with the path where to redirect the user (typically, a login page). + * The path is relative to the root of the Web server of Orthanc. + * @param uri The URI of interest (without the possible GET arguments). + * @param ip The IP address of the HTTP client. + * @param headersCount The number of HTTP headers. + * @param headersKeys The keys of the HTTP headers (always converted to low-case). + * @param headersValues The values of the HTTP headers. + * @param getCount For a GET request, the number of GET parameters. + * @param getKeys For a GET request, the keys of the GET parameters. + * @param getValues For a GET request, the values of the GET parameters. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginHttpAuthentication) ( + OrthancPluginHttpAuthenticationStatus* status, /* out */ + OrthancPluginMemoryBuffer* customPayload, /* out */ + OrthancPluginMemoryBuffer* redirection, /* out */ + const char* uri, + const char* ip, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + uint32_t getCount, + const char* const* getKeys, + const char* const* getValues); + + + typedef struct + { + OrthancPluginHttpAuthentication callback; + } _OrthancPluginHttpAuthentication; + + /** + * @brief Register a callback to handle HTTP authentication (and + * possibly HTTP authorization). + * + * This function installs a callback that is executed for each + * incoming HTTP request to handle HTTP authentication. At most one + * plugin can register such a callback. This gives the opportunity + * to the plugin to validate access tokens (such as a JWT), possibly + * redirecting the user to a login page. The authentication callback + * can generate a custom payload that will be provided to the + * subsequent REST handling callback (cf. `authenticationPayload` in + * `OrthancPluginHttpRequest`). + * + * If one plugin installs a HTTP authentication callback, the + * built-in HTTP authentication of Orthanc is disabled. This means + * that the "RegisteredUsers" and "AuthenticationEnabled" + * configuration options of Orthanc are totally ignored. In + * addition, tokens generated by + * OrthancPluginGenerateRestApiAuthorizationToken() become + * ineffective. + * + * This HTTP authentication callback can notably be used if some + * resource in the REST API must be available for public access, if + * the "RemoteAccessAllowed" configuration option is set to "true" + * (which necessitates bypassing the built-in HTTP authentication of + * Orthanc). + * + * In addition, the callback can handle HTTP authorization + * simultaneously with HTTP authentication, by reporting the + * "OrthancPluginHttpAuthenticationStatus_Forbidden" status. This + * corresponds to the behavior of callbacks installed using + * OrthancPluginRegisterIncomingHttpRequestFilter2(), but the latter + * callbacks do not provide access to the authentication payload. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The HTTP authentication callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterHttpAuthentication( + OrthancPluginContext* context, + OrthancPluginHttpAuthentication callback) + { + _OrthancPluginHttpAuthentication params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterHttpAuthentication, ¶ms); + } + + + typedef struct + { + const char* sourcePlugin; + const char* userId; + OrthancPluginResourceType resourceType; + const char* resourceId; + const char* action; + const void* logData; + uint32_t logDataSize; + } _OrthancPluginEmitAuditLog; + + + /** + * @brief Generate an audit log to signal security-related events. + * + * Generate an audit log that will be broadcasted to all the plugins + * that have registered a callback handler using + * OrthancPluginRegisterAuditLogHandler(). If no plugin has + * registered such a callback, the audit log is ignored. + * + * A typical handler would record the audit log in a database and/or + * relay the audit log to a message broker. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param sourcePlugin The name of the source plugin, to properly interpret the + * content of "action" and "logData". + * @param userId A string that uniquely identifies the user or + * entity that is executing the action on the resource. + * @param resourceType The type of the resource this audit log relates to. + * @param resourceId The resource this audit log relates to. + * @param action The action that was performed on the resource. + * @param logData A pointer to custom log data. + * @param logDataSize The size of the custom log data. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE void OrthancPluginEmitAuditLog( + OrthancPluginContext* context, + const char* sourcePlugin, + const char* userId, + OrthancPluginResourceType resourceType, + const char* resourceId, + const char* action, + const void* logData, + uint32_t logDataSize) + { + _OrthancPluginEmitAuditLog m; + m.sourcePlugin = sourcePlugin; + m.userId = userId; + m.resourceType = resourceType; + m.resourceId = resourceId; + m.action = action; + m.logData = logData; + m.logDataSize = logDataSize; + context->InvokeService(context, _OrthancPluginService_EmitAuditLog, &m); + } + + + /** + * @brief Callback to handle an audit log. + * + * Signature of a callback function that handles an audit log + * emitted by a source plugin. + * + * @param sourcePlugin The name of the source plugin. This information can + * be used to properly interpret the content of the "action" and + * "logData" arguments. + * @param userId A string uniquely identifying the user or entity that is executing the action on the resource. + * @param resourceType The type of the resource this log relates to. + * @param resourceId The resource this log relates to. + * @param action The action that is performed on the resource. + * @param logData A pointer to custom log data. + * @param logDataSize The size of the custom log data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginAuditLogHandler) ( + const char* sourcePlugin, + const char* userId, + OrthancPluginResourceType resourceType, + const char* resourceId, + const char* action, + const void* logData, + uint32_t logDataSize); + + typedef struct + { + OrthancPluginAuditLogHandler handler; + } _OrthancPluginAuditLogHandler; + + /** + * @brief Register a callback to handle audit logs. + * + * This function installs a callback to listen to each audit log + * that is generated by some other plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param handler The audit log handler. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterAuditLogHandler( + OrthancPluginContext* context, + OrthancPluginAuditLogHandler handler) + { + _OrthancPluginAuditLogHandler params; + params.handler = handler; + + return context->InvokeService(context, _OrthancPluginService_RegisterAuditLogHandler, ¶ms); + } + + #ifdef __cplusplus } #endif /** @} */ -
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Tue Nov 04 15:58:06 2025 +0100 @@ -55,6 +55,7 @@ int32 compression_type = 5; // opaque "CompressionType" in Orthanc uint64 compressed_size = 6; string compressed_hash = 7; + bytes custom_data = 8; // New in 1.12.8 } enum ResourceType { @@ -94,6 +95,11 @@ ORDERING_CAST_FLOAT = 2; } +enum QueueOrigin { + QUEUE_ORIGIN_FRONT = 0; + QUEUE_ORIGIN_BACK = 1; +} + message ServerIndexChange { int64 seq = 1; int32 change_type = 2; // opaque "ChangeType" in Orthanc @@ -166,6 +172,9 @@ bool has_measure_latency = 7; bool supports_find = 8; // New in Orthanc 1.12.5 bool has_extended_changes = 9; // New in Orthanc 1.12.5 + bool supports_key_value_stores = 10; // New in Orthanc 1.12.8 + bool supports_queues = 11; // New in Orthanc 1.12.8 + bool has_attachment_custom_data = 12; // New in Orthanc 1.12.8 } } @@ -321,6 +330,16 @@ OPERATION_FIND = 50; // New in Orthanc 1.12.5 OPERATION_GET_CHANGES_EXTENDED = 51; // New in Orthanc 1.12.5 OPERATION_COUNT_RESOURCES = 52; // New in Orthanc 1.12.5 + OPERATION_STORE_KEY_VALUE = 53; // New in Orthanc 1.12.8 + OPERATION_DELETE_KEY_VALUE = 54; // New in Orthanc 1.12.8 + OPERATION_GET_KEY_VALUE = 55; // New in Orthanc 1.12.8 + OPERATION_LIST_KEY_VALUES = 56; // New in Orthanc 1.12.8 + OPERATION_ENQUEUE_VALUE = 57; // New in Orthanc 1.12.8 + OPERATION_DEQUEUE_VALUE = 58; // New in Orthanc 1.12.8 + OPERATION_GET_QUEUE_SIZE = 59; // New in Orthanc 1.12.8 + OPERATION_GET_ATTACHMENT_CUSTOM_DATA = 60; // New in Orthanc 1.12.8 + OPERATION_SET_ATTACHMENT_CUSTOM_DATA = 61; // New in Orthanc 1.12.8 + } message Rollback { @@ -974,6 +993,109 @@ } } +message StoreKeyValue { + message Request { + string store_id = 1; + string key = 2; + bytes value = 3; + } + + message Response { + } +} + +message DeleteKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + } +} + +message GetKeyValue { + message Request { + string store_id = 1; + string key = 2; + } + + message Response { + bool found = 1; + bytes value = 2; + } +} + +message ListKeysValues { + message Request { + string store_id = 1; + bool from_first = 2; + string from_key = 3; // Only meaningful if "from_first == false" + uint64 limit = 4; + } + + message Response { + message KeyValue { + string key = 1; + bytes value = 2; + } + repeated KeyValue keys_values = 1; + } +} + +message EnqueueValue { + message Request { + string queue_id = 1; + bytes value = 2; + } + + message Response { + } +} + +message DequeueValue { + message Request { + string queue_id = 1; + QueueOrigin origin = 2; + } + + message Response { + bool found = 1; + bytes value = 2; + } +} + +message GetQueueSize { + message Request { + string queue_id = 1; + } + + message Response { + uint64 size = 1; + } +} + +message GetAttachmentCustomData { + message Request { + string uuid = 1; + } + + message Response { + bytes custom_data = 1; + } +} + +message SetAttachmentCustomData { + message Request { + string uuid = 1; + bytes custom_data = 2; + } + + message Response { + } +} + + message TransactionRequest { sfixed64 transaction = 1; TransactionOperation operation = 2; @@ -1031,6 +1153,15 @@ Find.Request find = 150; GetChangesExtended.Request get_changes_extended = 151; Find.Request count_resources = 152; + StoreKeyValue.Request store_key_value = 153; + DeleteKeyValue.Request delete_key_value = 154; + GetKeyValue.Request get_key_value = 155; + ListKeysValues.Request list_keys_values = 156; + EnqueueValue.Request enqueue_value = 157; + DequeueValue.Request dequeue_value = 158; + GetQueueSize.Request get_queue_size = 159; + GetAttachmentCustomData.Request get_attachment_custom_data = 160; + SetAttachmentCustomData.Request set_attachment_custom_data = 161; } message TransactionResponse { @@ -1087,6 +1218,15 @@ repeated Find.Response find = 150; // One message per found resource GetChangesExtended.Response get_changes_extended = 151; CountResources.Response count_resources = 152; + StoreKeyValue.Response store_key_value = 153; + DeleteKeyValue.Response delete_key_value = 154; + GetKeyValue.Response get_key_value = 155; + ListKeysValues.Response list_keys_values = 156; + EnqueueValue.Response enqueue_value = 157; + DequeueValue.Response dequeue_value = 158; + GetQueueSize.Response get_queue_size = 159; + GetAttachmentCustomData.Response get_attachment_custom_data = 160; + SetAttachmentCustomData.Response set_attachment_custom_data = 161; } enum RequestType {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancPluginCodeModel.json Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,5543 @@ +{ + "classes": [ + { + "destructor": "OrthancPluginFreeDicomInstance", + "methods": [ + { + "args": [], + "c_function": "OrthancPluginGetInstanceRemoteAet", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the Application Entity Title (AET) of the DICOM modality from which a DICOM instance originates." + ], + "return": "The AET if success, NULL if error.", + "summary": "Get the AET of a DICOM instance." + }, + "return_sdk_type": "const char *" + }, + { + "args": [], + "c_function": "OrthancPluginGetInstanceSize", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the number of bytes of the given DICOM instance." + ], + "return": "The size of the file, -1 in case of error.", + "summary": "Get the size of a DICOM file." + }, + "return_sdk_type": "int64_t" + }, + { + "args": [], + "c_function": "OrthancPluginGetInstanceJson", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns a pointer to a newly created string containing a JSON file. This JSON file encodes the tag hierarchy of the given DICOM instance." + ], + "return": "The NULL value in case of error, or a string containing the JSON file. This string must be freed by OrthancPluginFreeString().", + "summary": "Get the DICOM tag hierarchy as a JSON file." + }, + "return_sdk_type": "char *" + }, + { + "args": [], + "c_function": "OrthancPluginGetInstanceSimplifiedJson", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns a pointer to a newly created string containing a JSON file. This JSON file encodes the tag hierarchy of the given DICOM instance. In contrast with ::OrthancPluginGetInstanceJson(), the returned JSON file is in its simplified version." + ], + "return": "The NULL value in case of error, or a string containing the JSON file. This string must be freed by OrthancPluginFreeString().", + "summary": "Get the DICOM tag hierarchy as a JSON file (with simplification)." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "metadata", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginHasInstanceMetadata", + "const": true, + "documentation": { + "args": { + "metadata": "The metadata of interest." + }, + "description": [ + "This function checks whether the DICOM instance of interest is associated with some metadata. As of Orthanc 0.8.1, in the callbacks registered by ::OrthancPluginRegisterOnStoredInstanceCallback(), the only possibly available metadata are \"ReceptionDate\", \"RemoteAET\" and \"IndexInSeries\"." + ], + "return": "1 if the metadata is present, 0 if it is absent, -1 in case of error.", + "summary": "Check whether a DICOM instance is associated with some metadata." + }, + "return_sdk_type": "int32_t" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "metadata", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginGetInstanceMetadata", + "const": true, + "documentation": { + "args": { + "metadata": "The metadata of interest." + }, + "description": [ + "This functions returns the value of some metadata that is associated with the DICOM instance of interest. Before calling this function, the existence of the metadata must have been checked with ::OrthancPluginHasInstanceMetadata()." + ], + "return": "The metadata value if success, NULL if error. Please note that the returned string belongs to the instance object and must NOT be deallocated. Please make a copy of the string if you wish to access it later.", + "summary": "Get the value of some metadata associated with a given DICOM instance." + }, + "return_sdk_type": "const char *" + }, + { + "args": [], + "c_function": "OrthancPluginGetInstanceOrigin", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the origin of a DICOM instance that has been received by Orthanc." + ], + "return": "The origin of the instance.", + "summary": "Get the origin of a DICOM file." + }, + "return_sdk_enumeration": "OrthancPluginInstanceOrigin", + "return_sdk_type": "enumeration" + }, + { + "args": [], + "c_function": "OrthancPluginGetInstanceTransferSyntaxUid", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns a pointer to a newly created string that contains the transfer syntax UID of the DICOM instance. The empty string might be returned if this information is unknown." + ], + "return": "The NULL value in case of error, or a string containing the transfer syntax UID. This string must be freed by OrthancPluginFreeString().", + "summary": "Get the transfer syntax of a DICOM file." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 6, + 1 + ] + }, + { + "args": [], + "c_function": "OrthancPluginHasInstancePixelData", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns a Boolean value indicating whether the DICOM instance contains the pixel data (7FE0,0010) tag." + ], + "return": "\"1\" if the DICOM instance contains pixel data, or \"0\" if the tag is missing, or \"-1\" in the case of an error.", + "summary": "Check whether the DICOM file has pixel data." + }, + "return_sdk_type": "int32_t", + "since_sdk": [ + 1, + 6, + 1 + ] + }, + { + "args": [], + "c_function": "OrthancPluginGetInstanceFramesCount", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the number of frames that are part of a DICOM image managed by the Orthanc core." + ], + "return": "The number of frames (will be zero in the case of an error).", + "summary": "Get the number of frames in a DICOM instance." + }, + "return_sdk_type": "uint32_t", + "since_sdk": [ + 1, + 7, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "frameIndex", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetInstanceRawFrame", + "const": true, + "documentation": { + "args": { + "frameIndex": "The index of the frame of interest.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "This function returns a memory buffer containing the raw content of a frame in a DICOM instance that is managed by the Orthanc core. This is notably useful for compressed transfer syntaxes, as it gives access to the embedded files (such as JPEG, JPEG-LS or JPEG2k). The Orthanc core transparently reassembles the fragments to extract the raw frame." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Get the raw content of a frame in a DICOM instance." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *", + "since_sdk": [ + 1, + 7, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "frameIndex", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetInstanceDecodedFrame", + "const": true, + "documentation": { + "args": { + "frameIndex": "The index of the frame of interest." + }, + "description": [ + "This function decodes one frame of a DICOM image that is managed by the Orthanc core." + ], + "return": "The uncompressed image. It must be freed with OrthancPluginFreeImage().", + "summary": "Decode one frame from a DICOM instance." + }, + "return_sdk_class": "OrthancPluginImage", + "return_sdk_type": "object", + "since_sdk": [ + 1, + 7, + 0 + ] + }, + { + "args": [], + "c_function": "OrthancPluginSerializeDicomInstance", + "const": true, + "documentation": { + "args": { + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "This function returns a memory buffer containing the serialization of a DICOM instance that is managed by the Orthanc core." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Writes a DICOM instance to a memory buffer." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *", + "since_sdk": [ + 1, + 7, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginDicomToJsonFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg1", + "sdk_enumeration": "OrthancPluginDicomToJsonFlags", + "sdk_name": "flags", + "sdk_type": "enumeration" + }, + { + "name": "arg2", + "sdk_name": "maxStringLength", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetInstanceAdvancedJson", + "const": true, + "documentation": { + "args": { + "flags": "Flags governing the output.", + "format": "The output format.", + "maxStringLength": "The maximum length of a field. Too long fields will be output as \"null\". The 0 value means no maximum length." + }, + "description": [ + "This function takes as DICOM instance managed by the Orthanc core, and outputs a JSON string representing the tags of this DICOM file." + ], + "return": "The NULL value if the case of an error, or the JSON string. This string must be freed by OrthancPluginFreeString().", + "summary": "Format a DICOM memory buffer as a JSON string." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 7, + 0 + ] + } + ], + "name": "OrthancPluginDicomInstance" + }, + { + "methods": [], + "name": "OrthancPluginDicomWebNode", + "since_sdk": [ + 1, + 5, + 4 + ] + }, + { + "methods": [ + { + "args": [ + { + "name": "arg0", + "sdk_name": "dicom", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginFindAddAnswer", + "const": false, + "documentation": { + "args": { + "dicom": "The answer to be added, encoded as a DICOM file.", + "size": "The size of the DICOM file." + }, + "description": [ + "This function adds one answer (encoded as a DICOM file) to the set of answers corresponding to some C-Find SCP request that is not related to modality worklists." + ], + "return": "0 if success, other value if error.", + "summary": "Add one answer to some C-Find request." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 1, + 0 + ] + }, + { + "args": [], + "c_function": "OrthancPluginFindMarkIncomplete", + "const": false, + "documentation": { + "args": {}, + "description": [ + "This function marks as incomplete the set of answers corresponding to some C-Find SCP request that is not related to modality worklists. This must be used if canceling the handling of a request when too many answers are to be returned." + ], + "return": "0 if success, other value if error.", + "summary": "Mark the set of C-Find answers as incomplete." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 1, + 0 + ] + } + ], + "name": "OrthancPluginFindAnswers", + "since_sdk": [ + 1, + 1, + 0 + ] + }, + { + "destructor": "OrthancPluginFreeFindMatcher", + "methods": [ + { + "args": [ + { + "name": "arg0", + "sdk_name": "dicom", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginFindMatcherIsMatch", + "const": true, + "documentation": { + "args": { + "dicom": "The DICOM instance to be matched.", + "size": "The size of the DICOM instance." + }, + "description": [ + "This function checks whether one DICOM instance matches C-Find matcher that was previously allocated using OrthancPluginCreateFindMatcher()." + ], + "return": "1 if the DICOM instance matches the query, 0 otherwise.", + "summary": "Test whether a DICOM instance matches a C-Find query." + }, + "return_sdk_type": "int32_t", + "since_sdk": [ + 1, + 2, + 0 + ] + } + ], + "name": "OrthancPluginFindMatcher", + "since_sdk": [ + 1, + 2, + 0 + ] + }, + { + "methods": [ + { + "args": [], + "c_function": "OrthancPluginGetFindQuerySize", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the number of tags that are contained in the given C-Find query." + ], + "return": "The number of tags.", + "summary": "Get the number of tags in a C-Find query." + }, + "return_sdk_type": "uint32_t", + "since_sdk": [ + 1, + 1, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "index", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetFindQueryTagName", + "const": true, + "documentation": { + "args": { + "index": "The index of the tag of interest." + }, + "description": [ + "This function returns the symbolic name of one DICOM tag in the given C-Find query." + ], + "return": "0 if success, other value if error.", + "summary": "Get the symbolic name of one tag in a C-Find query." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 1, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "index", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetFindQueryValue", + "const": true, + "documentation": { + "args": { + "index": "The index of the tag of interest." + }, + "description": [ + "This function returns the value associated with one tag in the given C-Find query." + ], + "return": "0 if success, other value if error.", + "summary": "Get the value associated with one tag in a C-Find query." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 1, + 0 + ] + } + ], + "name": "OrthancPluginFindQuery", + "since_sdk": [ + 1, + 1, + 0 + ] + }, + { + "destructor": "OrthancPluginFreeImage", + "methods": [ + { + "args": [], + "c_function": "OrthancPluginGetImagePixelFormat", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the type of memory layout for the pixels of the given image." + ], + "return": "The pixel format.", + "summary": "Return the pixel format of an image." + }, + "return_sdk_enumeration": "OrthancPluginPixelFormat", + "return_sdk_type": "enumeration" + }, + { + "args": [], + "c_function": "OrthancPluginGetImageWidth", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the width of the given image." + ], + "return": "The width.", + "summary": "Return the width of an image." + }, + "return_sdk_type": "uint32_t" + }, + { + "args": [], + "c_function": "OrthancPluginGetImageHeight", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the height of the given image." + ], + "return": "The height.", + "summary": "Return the height of an image." + }, + "return_sdk_type": "uint32_t" + }, + { + "args": [], + "c_function": "OrthancPluginGetImagePitch", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the pitch of the given image. The pitch is defined as the number of bytes between 2 successive lines of the image in the memory buffer." + ], + "return": "The pitch.", + "summary": "Return the pitch of an image." + }, + "return_sdk_type": "uint32_t" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginPixelFormat", + "sdk_name": "targetFormat", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginConvertPixelFormat", + "const": true, + "documentation": { + "args": { + "targetFormat": "The target pixel format." + }, + "description": [ + "This function creates a new image, changing the memory layout of the pixels." + ], + "return": "The resulting image. It must be freed with OrthancPluginFreeImage().", + "summary": "Change the pixel format of an image." + }, + "return_sdk_class": "OrthancPluginImage", + "return_sdk_type": "object" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "fontIndex", + "sdk_type": "uint32_t" + }, + { + "name": "arg1", + "sdk_name": "utf8Text", + "sdk_type": "const char *" + }, + { + "name": "arg2", + "sdk_name": "x", + "sdk_type": "int32_t" + }, + { + "name": "arg3", + "sdk_name": "y", + "sdk_type": "int32_t" + }, + { + "name": "arg4", + "sdk_name": "r", + "sdk_type": "uint8_t" + }, + { + "name": "arg5", + "sdk_name": "g", + "sdk_type": "uint8_t" + }, + { + "name": "arg6", + "sdk_name": "b", + "sdk_type": "uint8_t" + } + ], + "c_function": "OrthancPluginDrawText", + "const": false, + "documentation": { + "args": { + "b": "The value of the blue color channel of the text.", + "fontIndex": "The index of the font. This value must be less than OrthancPluginGetFontsCount().", + "g": "The value of the green color channel of the text.", + "r": "The value of the red color channel of the text.", + "utf8Text": "The text to be drawn, encoded as an UTF-8 zero-terminated string.", + "x": "The X position of the text over the image.", + "y": "The Y position of the text over the image." + }, + "description": [ + "This function draws some text on some image." + ], + "return": "0 if success, other value if error.", + "summary": "Draw text on an image." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + } + ], + "name": "OrthancPluginImage" + }, + { + "destructor": "OrthancPluginFreeJob", + "methods": [ + { + "args": [ + { + "name": "arg0", + "sdk_name": "priority", + "sdk_type": "int32_t" + } + ], + "c_function": "OrthancPluginSubmitJob", + "const": false, + "documentation": { + "args": { + "priority": "The priority of the job." + }, + "description": [ + "This function adds the given job to the pending jobs of Orthanc. Orthanc will take take of freeing it by invoking the finalization callback provided to OrthancPluginCreateJob()." + ], + "return": "ID of the newly-submitted job. This string must be freed by OrthancPluginFreeString().", + "summary": "Submit a new job to the jobs engine of Orthanc." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 4, + 2 + ] + } + ], + "name": "OrthancPluginJob", + "since_sdk": [ + 1, + 4, + 2 + ] + }, + { + "destructor": "OrthancPluginFreeKeysValuesIterator", + "methods": [ + { + "args": [], + "c_function": "OrthancPluginKeysValuesIteratorGetKey", + "const": false, + "documentation": { + "args": {}, + "description": [ + "Before using this function, the function OrthancPluginKeysValuesIteratorNext() must have been called at least once." + ], + "return": "The current key, or NULL in the case of an error.", + "summary": "Get the current key of an iterator over a key-value store." + }, + "return_sdk_type": "const char *", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "args": [], + "c_function": "OrthancPluginKeysValuesIteratorGetValue", + "const": false, + "documentation": { + "args": { + "target": "Memory buffer where to store the value that has been retrieved from the key-value store. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "Before using this function, the function OrthancPluginKeysValuesIteratorNext() must have been called at least once." + ], + "return": "The current value, or NULL in the case of an error.", + "summary": "Get the current value of an iterator over a key-value store." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *", + "since_sdk": [ + 1, + 12, + 8 + ] + } + ], + "name": "OrthancPluginKeysValuesIterator", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "destructor": "OrthancPluginFreePeers", + "methods": [ + { + "args": [], + "c_function": "OrthancPluginGetPeersCount", + "const": true, + "documentation": { + "args": {}, + "description": [ + "This function returns the number of Orthanc peers.", + "This function is thread-safe: Several threads sharing the same OrthancPluginPeers object can simultaneously call this function." + ], + "return": "The number of peers.", + "summary": "Get the number of Orthanc peers." + }, + "return_sdk_type": "uint32_t", + "since_sdk": [ + 1, + 4, + 2 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "peerIndex", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetPeerName", + "const": true, + "documentation": { + "args": { + "peerIndex": "The index of the peer of interest. This value must be lower than OrthancPluginGetPeersCount()." + }, + "description": [ + "This function returns the symbolic name of the Orthanc peer, which corresponds to the key of the \"OrthancPeers\" configuration option of Orthanc.", + "This function is thread-safe: Several threads sharing the same OrthancPluginPeers object can simultaneously call this function." + ], + "return": "The symbolic name, or NULL in the case of an error.", + "summary": "Get the symbolic name of an Orthanc peer." + }, + "return_sdk_type": "const char *", + "since_sdk": [ + 1, + 4, + 2 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "peerIndex", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetPeerUrl", + "const": true, + "documentation": { + "args": { + "peerIndex": "The index of the peer of interest. This value must be lower than OrthancPluginGetPeersCount()." + }, + "description": [ + "This function returns the base URL to the REST API of some Orthanc peer.", + "This function is thread-safe: Several threads sharing the same OrthancPluginPeers object can simultaneously call this function." + ], + "return": "The URL, or NULL in the case of an error.", + "summary": "Get the base URL of an Orthanc peer." + }, + "return_sdk_type": "const char *", + "since_sdk": [ + 1, + 4, + 2 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "peerIndex", + "sdk_type": "uint32_t" + }, + { + "name": "arg1", + "sdk_name": "userProperty", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginGetPeerUserProperty", + "const": true, + "documentation": { + "args": { + "peerIndex": "The index of the peer of interest. This value must be lower than OrthancPluginGetPeersCount().", + "userProperty": "The user property of interest." + }, + "description": [ + "This function returns some user-defined property of some Orthanc peer. An user-defined property is a property that is associated with the peer in the Orthanc configuration file, but that is not recognized by the Orthanc core.", + "This function is thread-safe: Several threads sharing the same OrthancPluginPeers object can simultaneously call this function." + ], + "return": "The value of the user property, or NULL if it is not defined.", + "summary": "Get some user-defined property of an Orthanc peer." + }, + "return_sdk_type": "const char *", + "since_sdk": [ + 1, + 4, + 2 + ] + } + ], + "name": "OrthancPluginPeers", + "since_sdk": [ + 1, + 4, + 2 + ] + }, + { + "methods": [ + { + "args": [ + { + "name": "arg0", + "sdk_name": "answer", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg2", + "sdk_name": "mimeType", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginAnswerBuffer", + "const": false, + "documentation": { + "args": { + "answer": "Pointer to the memory buffer containing the answer.", + "answerSize": "Number of bytes of the answer.", + "mimeType": "The MIME type of the answer." + }, + "description": [ + "This function answers to a REST request with the content of a memory buffer." + ], + "summary": "Answer to a REST request." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginPixelFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg1", + "sdk_name": "width", + "sdk_type": "uint32_t" + }, + { + "name": "arg2", + "sdk_name": "height", + "sdk_type": "uint32_t" + }, + { + "name": "arg3", + "sdk_name": "pitch", + "sdk_type": "uint32_t" + }, + { + "name": "arg4", + "sdk_name": "buffer", + "sdk_type": "const void *" + } + ], + "c_function": "OrthancPluginCompressAndAnswerPngImage", + "const": false, + "documentation": { + "args": { + "buffer": "The memory buffer containing the uncompressed image.", + "format": "The memory layout of the uncompressed image.", + "height": "The height of the image.", + "pitch": "The pitch of the image (i.e. the number of bytes between 2 successive lines of the image in the memory buffer).", + "width": "The width of the image." + }, + "description": [ + "This function answers to a REST request with a PNG image. The parameters of this function describe a memory buffer that contains an uncompressed image. The image will be automatically compressed as a PNG image by the core system of Orthanc." + ], + "summary": "Answer to a REST request with a PNG image." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "redirection", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginRedirect", + "const": false, + "documentation": { + "args": { + "redirection": "Where to redirect." + }, + "description": [ + "This function answers to a REST request by redirecting the user to another URI using HTTP status 301." + ], + "summary": "Redirect a REST request." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "status", + "sdk_type": "uint16_t" + } + ], + "c_function": "OrthancPluginSendHttpStatusCode", + "const": false, + "documentation": { + "args": { + "status": "The HTTP status code to be sent." + }, + "description": [ + "This function answers to a REST request by sending a HTTP status code (such as \"400 - Bad Request\"). Note that: - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer(). - Redirections (status 301) must use ::OrthancPluginRedirect(). - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized(). - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed()." + ], + "summary": "Send a HTTP status code." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "realm", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSendUnauthorized", + "const": false, + "documentation": { + "args": { + "realm": "The realm for the authorization process." + }, + "description": [ + "This function answers to a REST request by signaling that it is not authorized." + ], + "summary": "Signal that a REST request is not authorized." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "allowedMethods", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSendMethodNotAllowed", + "const": false, + "documentation": { + "args": { + "allowedMethods": "The allowed methods for this URI (e.g. \"GET,POST\" after a PUT or a POST request)." + }, + "description": [ + "This function answers to a REST request by signaling that the queried URI does not support this method." + ], + "summary": "Signal that this URI does not support this HTTP method." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "cookie", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "value", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetCookie", + "const": false, + "documentation": { + "args": { + "cookie": "The cookie to be set.", + "value": "The value of the cookie." + }, + "description": [ + "This function sets a cookie in the HTTP client." + ], + "summary": "Set a cookie." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "key", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "value", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetHttpHeader", + "const": false, + "documentation": { + "args": { + "key": "The HTTP header to be set.", + "value": "The value of the HTTP header." + }, + "description": [ + "This function sets a HTTP header in the HTTP answer." + ], + "summary": "Set some HTTP header." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "subType", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "contentType", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginStartMultipartAnswer", + "const": false, + "documentation": { + "args": { + "contentType": "The MIME type of the items in the multipart answer.", + "subType": "The sub-type of the multipart answer (\"mixed\" or \"related\")." + }, + "description": [ + "Initiates a HTTP multipart answer, as the result of a REST request." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Start an HTTP multipart answer." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "answer", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginSendMultipartItem", + "const": false, + "documentation": { + "args": { + "answer": "Pointer to the memory buffer containing the item.", + "answerSize": "Number of bytes of the item." + }, + "description": [ + "This function sends an item as a part of some HTTP multipart answer that was initiated by OrthancPluginStartMultipartAnswer()." + ], + "return": "0 if success, or the error code if failure (this notably happens if the connection is closed by the client).", + "summary": "Send an item as a part of some HTTP multipart answer." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "status", + "sdk_type": "uint16_t" + }, + { + "name": "arg1", + "sdk_name": "body", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginSendHttpStatus", + "const": false, + "documentation": { + "args": { + "body": "The body of the answer.", + "bodySize": "The size of the body.", + "status": "The HTTP status code to be sent." + }, + "description": [ + "This function answers to a HTTP request by sending a HTTP status code (such as \"400 - Bad Request\"), together with a body describing the error. The body will only be returned if the configuration option \"HttpDescribeErrors\" of Orthanc is set to \"true\".", + "Note that: - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer(). - Redirections (status 301) must use ::OrthancPluginRedirect(). - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized(). - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed()." + ], + "summary": "Send a HTTP status, with a custom body." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginPixelFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg1", + "sdk_name": "width", + "sdk_type": "uint32_t" + }, + { + "name": "arg2", + "sdk_name": "height", + "sdk_type": "uint32_t" + }, + { + "name": "arg3", + "sdk_name": "pitch", + "sdk_type": "uint32_t" + }, + { + "name": "arg4", + "sdk_name": "buffer", + "sdk_type": "const void *" + }, + { + "name": "arg5", + "sdk_name": "quality", + "sdk_type": "uint8_t" + } + ], + "c_function": "OrthancPluginCompressAndAnswerJpegImage", + "const": false, + "documentation": { + "args": { + "buffer": "The memory buffer containing the uncompressed image.", + "format": "The memory layout of the uncompressed image.", + "height": "The height of the image.", + "pitch": "The pitch of the image (i.e. the number of bytes between 2 successive lines of the image in the memory buffer).", + "quality": "The quality of the JPEG encoding, between 1 (worst quality, best compression) and 100 (best quality, worst compression).", + "width": "The width of the image." + }, + "description": [ + "This function answers to a REST request with a JPEG image. The parameters of this function describe a memory buffer that contains an uncompressed image. The image will be automatically compressed as a JPEG image by the core system of Orthanc." + ], + "summary": "Answer to a REST request with a JPEG image." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "details", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "log", + "sdk_type": "uint8_t" + } + ], + "c_function": "OrthancPluginSetHttpErrorDetails", + "const": false, + "documentation": { + "args": { + "details": "The details of the error message.", + "log": "Whether to also write the detailed error to the Orthanc logs." + }, + "description": [ + "This function sets the detailed description associated with an HTTP error. This description will be displayed in the \"Details\" field of the JSON body of the HTTP answer. It is only taken into consideration if the REST callback returns an error code that is different from \"OrthancPluginErrorCode_Success\", and if the \"HttpDescribeErrors\" configuration option of Orthanc is set to \"true\"." + ], + "summary": "Provide a detailed description for an HTTP error." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 5, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "contentType", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginStartStreamAnswer", + "const": false, + "documentation": { + "args": { + "contentType": "The MIME type of the items in the stream answer." + }, + "description": [ + "Initiates an HTTP stream answer, as the result of a REST request." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Start an HTTP stream answer." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 12, + 6 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "answer", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginSendStreamChunk", + "const": false, + "documentation": { + "args": { + "answer": "Pointer to the memory buffer containing the item.", + "answerSize": "Number of bytes of the item." + }, + "description": [ + "This function sends a chunk as part of an HTTP stream answer that was initiated by OrthancPluginStartStreamAnswer()." + ], + "return": "0 if success, or the error code if failure (this notably happens if the connection is closed by the client).", + "summary": "Send a chunk as a part of an HTTP stream answer." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 12, + 6 + ] + } + ], + "name": "OrthancPluginRestOutput" + }, + { + "methods": [], + "name": "OrthancPluginServerChunkedRequestReader", + "since_sdk": [ + 1, + 5, + 7 + ] + }, + { + "methods": [ + { + "args": [ + { + "name": "arg0", + "sdk_name": "uuid", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "content", + "sdk_type": "const void *" + }, + { + "name": "arg2", + "sdk_name": "size", + "sdk_type": "uint64_t" + }, + { + "name": "arg3", + "sdk_enumeration": "OrthancPluginContentType", + "sdk_name": "type", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginStorageAreaCreate", + "const": false, + "documentation": { + "args": { + "content": "The content to store in the newly created file.", + "size": "The size of the content.", + "type": "The type of the file content.", + "uuid": "The identifier of the file to be created." + }, + "description": [ + "This function creates a new file inside the storage area that is currently used by Orthanc.", + "Warning: This function will result in a \"not implemented\" error on versions of the Orthanc core above 1.12.6." + ], + "return": "0 if success, other value if error.", + "summary": "Create a file inside the storage area." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uuid", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_enumeration": "OrthancPluginContentType", + "sdk_name": "type", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginStorageAreaRead", + "const": false, + "documentation": { + "args": { + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "type": "The type of the file content.", + "uuid": "The identifier of the file to be read." + }, + "description": [ + "This function reads the content of a given file from the storage area that is currently used by Orthanc.", + "Warning: This function will result in a \"not implemented\" error on versions of the Orthanc core above 1.12.6." + ], + "return": "0 if success, other value if error.", + "summary": "Read a file from the storage area." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uuid", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_enumeration": "OrthancPluginContentType", + "sdk_name": "type", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginStorageAreaRemove", + "const": false, + "documentation": { + "args": { + "type": "The type of the file content.", + "uuid": "The identifier of the file to be removed." + }, + "description": [ + "This function removes a given file from the storage area that is currently used by Orthanc.", + "Warning: This function will result in a \"not implemented\" error on versions of the Orthanc core above 1.12.6." + ], + "return": "0 if success, other value if error.", + "summary": "Remove a file from the storage area." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginResourceType", + "sdk_name": "level", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginReconstructMainDicomTags", + "const": false, + "documentation": { + "args": { + "level": "The type of the resources of interest." + }, + "description": [ + "This function requests the Orthanc core to reconstruct the main DICOM tags of all the resources of the given type. This function can only be used as a part of the upgrade of a custom database back-end. A database transaction will be automatically setup." + ], + "return": "0 if success, other value if error.", + "summary": "Reconstruct the main DICOM tags." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + } + ], + "name": "OrthancPluginStorageArea" + }, + { + "methods": [], + "name": "OrthancPluginWebDavCollection", + "since_sdk": [ + 1, + 10, + 1 + ] + }, + { + "methods": [ + { + "args": [ + { + "name": "arg0", + "sdk_class": "OrthancPluginWorklistQuery", + "sdk_name": "query", + "sdk_type": "const_object" + }, + { + "name": "arg1", + "sdk_name": "dicom", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginWorklistAddAnswer", + "const": false, + "documentation": { + "args": { + "dicom": "The worklist to answer, encoded as a DICOM file.", + "query": "The worklist query, as received by the callback.", + "size": "The size of the DICOM file." + }, + "description": [ + "This function adds one worklist (encoded as a DICOM file) to the set of answers corresponding to some C-Find SCP request against modality worklists." + ], + "return": "0 if success, other value if error.", + "summary": "Add one answer to some modality worklist request." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [], + "c_function": "OrthancPluginWorklistMarkIncomplete", + "const": false, + "documentation": { + "args": {}, + "description": [ + "This function marks as incomplete the set of answers corresponding to some C-Find SCP request against modality worklists. This must be used if canceling the handling of a request when too many answers are to be returned." + ], + "return": "0 if success, other value if error.", + "summary": "Mark the set of worklist answers as incomplete." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + } + ], + "name": "OrthancPluginWorklistAnswers" + }, + { + "methods": [ + { + "args": [ + { + "name": "arg0", + "sdk_name": "dicom", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginWorklistIsMatch", + "const": true, + "documentation": { + "args": { + "dicom": "The worklist to answer, encoded as a DICOM file.", + "size": "The size of the DICOM file." + }, + "description": [ + "This function checks whether one worklist (encoded as a DICOM file) matches the C-Find SCP query against modality worklists. This function must be called before adding the worklist as an answer through OrthancPluginWorklistAddAnswer()." + ], + "return": "1 if the worklist matches the query, 0 otherwise.", + "summary": "Test whether a worklist matches the query." + }, + "return_sdk_type": "int32_t" + }, + { + "args": [], + "c_function": "OrthancPluginWorklistGetDicomQuery", + "const": true, + "documentation": { + "args": { + "target": "Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "This function retrieves the DICOM file that underlies a C-Find SCP query against modality worklists." + ], + "return": "0 if success, other value if error.", + "summary": "Retrieve the worklist query as a DICOM file." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + } + ], + "name": "OrthancPluginWorklistQuery" + } + ], + "enumerations": [ + { + "documentation": "The supported types of changes that can be signaled to the change callback. Note: This enumeration is not used to store changes in the database!", + "name": "OrthancPluginChangeType", + "values": [ + { + "documentation": "Series is now complete", + "key": "CompletedSeries", + "value": 0 + }, + { + "documentation": "Deleted resource", + "key": "Deleted", + "value": 1 + }, + { + "documentation": "A new instance was added to this resource", + "key": "NewChildInstance", + "value": 2 + }, + { + "documentation": "New instance received", + "key": "NewInstance", + "value": 3 + }, + { + "documentation": "New patient created", + "key": "NewPatient", + "value": 4 + }, + { + "documentation": "New series created", + "key": "NewSeries", + "value": 5 + }, + { + "documentation": "New study created", + "key": "NewStudy", + "value": 6 + }, + { + "documentation": "Timeout: No new instance in this patient", + "key": "StablePatient", + "value": 7 + }, + { + "documentation": "Timeout: No new instance in this series", + "key": "StableSeries", + "value": 8 + }, + { + "documentation": "Timeout: No new instance in this study", + "key": "StableStudy", + "value": 9 + }, + { + "documentation": "Orthanc has started", + "key": "OrthancStarted", + "value": 10 + }, + { + "documentation": "Orthanc is stopping", + "key": "OrthancStopped", + "value": 11 + }, + { + "documentation": "Some user-defined attachment has changed for this resource", + "key": "UpdatedAttachment", + "value": 12 + }, + { + "documentation": "Some user-defined metadata has changed for this resource", + "key": "UpdatedMetadata", + "value": 13 + }, + { + "documentation": "The list of Orthanc peers has changed", + "key": "UpdatedPeers", + "since_sdk": [ + 1, + 4, + 2 + ], + "value": 14 + }, + { + "documentation": "The list of DICOM modalities has changed", + "key": "UpdatedModalities", + "since_sdk": [ + 1, + 4, + 2 + ], + "value": 15 + }, + { + "documentation": "New Job submitted", + "key": "JobSubmitted", + "since_sdk": [ + 1, + 7, + 2 + ], + "value": 16 + }, + { + "documentation": "A Job has completed successfully", + "key": "JobSuccess", + "since_sdk": [ + 1, + 7, + 2 + ], + "value": 17 + }, + { + "documentation": "A Job has failed", + "key": "JobFailure", + "since_sdk": [ + 1, + 7, + 2 + ], + "value": 18 + } + ] + }, + { + "documentation": "The compression algorithms that are supported by the Orthanc core.", + "name": "OrthancPluginCompressionType", + "values": [ + { + "documentation": "Standard zlib compression", + "key": "Zlib", + "value": 0 + }, + { + "documentation": "zlib, prefixed with uncompressed size (uint64_t)", + "key": "ZlibWithSize", + "value": 1 + }, + { + "documentation": "Standard gzip compression", + "key": "Gzip", + "value": 2 + }, + { + "documentation": "gzip, prefixed with uncompressed size (uint64_t)", + "key": "GzipWithSize", + "value": 3 + }, + { + "documentation": "No compression (new in Orthanc 1.12.8)", + "key": "None", + "since_sdk": [ + 1, + 12, + 8 + ], + "value": 4 + } + ] + }, + { + "documentation": "The constraints on the tags (main DICOM tags and identifier tags) that must be supported by the database plugins.", + "name": "OrthancPluginConstraintType", + "values": [ + { + "documentation": "Equal", + "key": "Equal", + "value": 1 + }, + { + "documentation": "Less or equal", + "key": "SmallerOrEqual", + "value": 2 + }, + { + "documentation": "More or equal", + "key": "GreaterOrEqual", + "value": 3 + }, + { + "documentation": "Wildcard matching", + "key": "Wildcard", + "value": 4 + }, + { + "documentation": "List of values", + "key": "List", + "value": 5 + } + ] + }, + { + "documentation": "The content types that are supported by Orthanc plugins.", + "name": "OrthancPluginContentType", + "values": [ + { + "documentation": "Unknown content type", + "key": "Unknown", + "value": 0 + }, + { + "documentation": "DICOM", + "key": "Dicom", + "value": 1 + }, + { + "documentation": "JSON summary of a DICOM file", + "key": "DicomAsJson", + "value": 2 + }, + { + "documentation": "DICOM Header till pixel data", + "key": "DicomUntilPixelData", + "since_sdk": [ + 1, + 9, + 2 + ], + "value": 3 + } + ] + }, + { + "documentation": "Flags for the creation of a DICOM file.", + "name": "OrthancPluginCreateDicomFlags", + "values": [ + { + "documentation": "Default mode", + "key": "None", + "since_sdk": [ + 1, + 2, + 0 + ], + "value": 0 + }, + { + "documentation": "Decode fields encoded using data URI scheme", + "key": "DecodeDataUriScheme", + "value": 1 + }, + { + "documentation": "Automatically generate DICOM identifiers", + "key": "GenerateIdentifiers", + "value": 2 + } + ] + }, + { + "documentation": "Flags to customize a DICOM-to-JSON conversion. By default, binary tags are formatted using Data URI scheme.", + "name": "OrthancPluginDicomToJsonFlags", + "values": [ + { + "documentation": "Default formatting", + "key": "None", + "value": 0 + }, + { + "documentation": "Include the binary tags", + "key": "IncludeBinary", + "value": 1 + }, + { + "documentation": "Include the private tags", + "key": "IncludePrivateTags", + "value": 2 + }, + { + "documentation": "Include the tags unknown by the dictionary", + "key": "IncludeUnknownTags", + "value": 4 + }, + { + "documentation": "Include the pixel data", + "key": "IncludePixelData", + "value": 8 + }, + { + "documentation": "Output binary tags as-is, dropping non-ASCII", + "key": "ConvertBinaryToAscii", + "value": 16 + }, + { + "documentation": "Signal binary tags as null values", + "key": "ConvertBinaryToNull", + "value": 32 + }, + { + "documentation": "Stop processing after pixel data (new in 1.9.1)", + "key": "StopAfterPixelData", + "since_sdk": [ + 1, + 9, + 1 + ], + "value": 64 + }, + { + "documentation": "Skip tags whose element is zero (new in 1.9.1)", + "key": "SkipGroupLengths", + "since_sdk": [ + 1, + 9, + 1 + ], + "value": 128 + } + ] + }, + { + "documentation": "The possible output formats for a DICOM-to-JSON conversion.", + "name": "OrthancPluginDicomToJsonFormat", + "values": [ + { + "documentation": "Full output, with most details", + "key": "Full", + "value": 1 + }, + { + "documentation": "Tags output as hexadecimal numbers", + "key": "Short", + "value": 2 + }, + { + "documentation": "Human-readable JSON", + "key": "Human", + "value": 3 + } + ] + }, + { + "documentation": "The available modes to export a binary DICOM tag into a DICOMweb JSON or XML document.", + "name": "OrthancPluginDicomWebBinaryMode", + "since_sdk": [ + 1, + 5, + 4 + ], + "values": [ + { + "documentation": "Don't include binary tags", + "key": "Ignore", + "value": 0 + }, + { + "documentation": "Inline encoding using Base64", + "key": "InlineBinary", + "value": 1 + }, + { + "documentation": "Use a bulk data URI field", + "key": "BulkDataUri", + "value": 2 + } + ] + }, + { + "documentation": "The various error codes that can be returned by the Orthanc core.", + "name": "OrthancPluginErrorCode", + "values": [ + { + "documentation": "Internal error", + "key": "InternalError", + "value": -1 + }, + { + "documentation": "Success", + "key": "Success", + "value": 0 + }, + { + "documentation": "Error encountered within the plugin engine", + "key": "Plugin", + "value": 1 + }, + { + "documentation": "Not implemented yet", + "key": "NotImplemented", + "value": 2 + }, + { + "documentation": "Parameter out of range", + "key": "ParameterOutOfRange", + "value": 3 + }, + { + "documentation": "The server hosting Orthanc is running out of memory", + "key": "NotEnoughMemory", + "value": 4 + }, + { + "documentation": "Bad type for a parameter", + "key": "BadParameterType", + "value": 5 + }, + { + "documentation": "Bad sequence of calls", + "key": "BadSequenceOfCalls", + "value": 6 + }, + { + "documentation": "Accessing an inexistent item", + "key": "InexistentItem", + "value": 7 + }, + { + "documentation": "Bad request", + "key": "BadRequest", + "value": 8 + }, + { + "documentation": "Error in the network protocol", + "key": "NetworkProtocol", + "value": 9 + }, + { + "documentation": "Error while calling a system command", + "key": "SystemCommand", + "value": 10 + }, + { + "documentation": "Error with the database engine", + "key": "Database", + "value": 11 + }, + { + "documentation": "Badly formatted URI", + "key": "UriSyntax", + "value": 12 + }, + { + "documentation": "Inexistent file", + "key": "InexistentFile", + "value": 13 + }, + { + "documentation": "Cannot write to file", + "key": "CannotWriteFile", + "value": 14 + }, + { + "documentation": "Bad file format", + "key": "BadFileFormat", + "value": 15 + }, + { + "documentation": "Timeout", + "key": "Timeout", + "value": 16 + }, + { + "documentation": "Unknown resource", + "key": "UnknownResource", + "value": 17 + }, + { + "documentation": "Incompatible version of the database", + "key": "IncompatibleDatabaseVersion", + "value": 18 + }, + { + "documentation": "The file storage is full", + "key": "FullStorage", + "value": 19 + }, + { + "documentation": "Corrupted file (e.g. inconsistent MD5 hash)", + "key": "CorruptedFile", + "value": 20 + }, + { + "documentation": "Inexistent tag", + "key": "InexistentTag", + "value": 21 + }, + { + "documentation": "Cannot modify a read-only data structure", + "key": "ReadOnly", + "value": 22 + }, + { + "documentation": "Incompatible format of the images", + "key": "IncompatibleImageFormat", + "value": 23 + }, + { + "documentation": "Incompatible size of the images", + "key": "IncompatibleImageSize", + "value": 24 + }, + { + "documentation": "Error while using a shared library (plugin)", + "key": "SharedLibrary", + "value": 25 + }, + { + "documentation": "Plugin invoking an unknown service", + "key": "UnknownPluginService", + "value": 26 + }, + { + "documentation": "Unknown DICOM tag", + "key": "UnknownDicomTag", + "value": 27 + }, + { + "documentation": "Cannot parse a JSON document", + "key": "BadJson", + "value": 28 + }, + { + "documentation": "Bad credentials were provided to an HTTP request", + "key": "Unauthorized", + "value": 29 + }, + { + "documentation": "Badly formatted font file", + "key": "BadFont", + "value": 30 + }, + { + "documentation": "The plugin implementing a custom database back-end does not fulfill the proper interface", + "key": "DatabasePlugin", + "value": 31 + }, + { + "documentation": "Error in the plugin implementing a custom storage area", + "key": "StorageAreaPlugin", + "value": 32 + }, + { + "documentation": "The request is empty", + "key": "EmptyRequest", + "value": 33 + }, + { + "documentation": "Cannot send a response which is acceptable according to the Accept HTTP header", + "key": "NotAcceptable", + "value": 34 + }, + { + "documentation": "Cannot handle a NULL pointer", + "key": "NullPointer", + "value": 35 + }, + { + "documentation": "The database is currently not available (probably a transient situation)", + "key": "DatabaseUnavailable", + "value": 36 + }, + { + "documentation": "This job was canceled", + "key": "CanceledJob", + "value": 37 + }, + { + "documentation": "Geometry error encountered in Stone", + "key": "BadGeometry", + "value": 38 + }, + { + "documentation": "Cannot initialize SSL encryption, check out your certificates", + "key": "SslInitialization", + "value": 39 + }, + { + "documentation": "Calling a function that has been removed from the Orthanc Framework", + "key": "DiscontinuedAbi", + "value": 40 + }, + { + "documentation": "Incorrect range request", + "key": "BadRange", + "value": 41 + }, + { + "documentation": "Database could not serialize access due to concurrent update, the transaction should be retried", + "key": "DatabaseCannotSerialize", + "value": 42 + }, + { + "documentation": "A bad revision number was provided, which might indicate conflict between multiple writers", + "key": "Revision", + "value": 43 + }, + { + "documentation": "A main DICOM Tag has been defined multiple times for the same resource level", + "key": "MainDicomTagsMultiplyDefined", + "value": 44 + }, + { + "documentation": "Access to a resource is forbidden", + "key": "ForbiddenAccess", + "value": 45 + }, + { + "documentation": "Duplicate resource", + "key": "DuplicateResource", + "value": 46 + }, + { + "documentation": "Your configuration file contains configuration that are mutually incompatible", + "key": "IncompatibleConfigurations", + "value": 47 + }, + { + "documentation": "SQLite: The database is not opened", + "key": "SQLiteNotOpened", + "value": 1000 + }, + { + "documentation": "SQLite: Connection is already open", + "key": "SQLiteAlreadyOpened", + "value": 1001 + }, + { + "documentation": "SQLite: Unable to open the database", + "key": "SQLiteCannotOpen", + "value": 1002 + }, + { + "documentation": "SQLite: This cached statement is already being referred to", + "key": "SQLiteStatementAlreadyUsed", + "value": 1003 + }, + { + "documentation": "SQLite: Cannot execute a command", + "key": "SQLiteExecute", + "value": 1004 + }, + { + "documentation": "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)", + "key": "SQLiteRollbackWithoutTransaction", + "value": 1005 + }, + { + "documentation": "SQLite: Committing a nonexistent transaction", + "key": "SQLiteCommitWithoutTransaction", + "value": 1006 + }, + { + "documentation": "SQLite: Unable to register a function", + "key": "SQLiteRegisterFunction", + "value": 1007 + }, + { + "documentation": "SQLite: Unable to flush the database", + "key": "SQLiteFlush", + "value": 1008 + }, + { + "documentation": "SQLite: Cannot run a cached statement", + "key": "SQLiteCannotRun", + "value": 1009 + }, + { + "documentation": "SQLite: Cannot step over a cached statement", + "key": "SQLiteCannotStep", + "value": 1010 + }, + { + "documentation": "SQLite: Bind a value while out of range (serious error)", + "key": "SQLiteBindOutOfRange", + "value": 1011 + }, + { + "documentation": "SQLite: Cannot prepare a cached statement", + "key": "SQLitePrepareStatement", + "value": 1012 + }, + { + "documentation": "SQLite: Beginning the same transaction twice", + "key": "SQLiteTransactionAlreadyStarted", + "value": 1013 + }, + { + "documentation": "SQLite: Failure when committing the transaction", + "key": "SQLiteTransactionCommit", + "value": 1014 + }, + { + "documentation": "SQLite: Cannot start a transaction", + "key": "SQLiteTransactionBegin", + "value": 1015 + }, + { + "documentation": "The directory to be created is already occupied by a regular file", + "key": "DirectoryOverFile", + "value": 2000 + }, + { + "documentation": "Unable to create a subdirectory or a file in the file storage", + "key": "FileStorageCannotWrite", + "value": 2001 + }, + { + "documentation": "The specified path does not point to a directory", + "key": "DirectoryExpected", + "value": 2002 + }, + { + "documentation": "The TCP port of the HTTP server is privileged or already in use", + "key": "HttpPortInUse", + "value": 2003 + }, + { + "documentation": "The TCP port of the DICOM server is privileged or already in use", + "key": "DicomPortInUse", + "value": 2004 + }, + { + "documentation": "This HTTP status is not allowed in a REST API", + "key": "BadHttpStatusInRest", + "value": 2005 + }, + { + "documentation": "The specified path does not point to a regular file", + "key": "RegularFileExpected", + "value": 2006 + }, + { + "documentation": "Unable to get the path to the executable", + "key": "PathToExecutable", + "value": 2007 + }, + { + "documentation": "Cannot create a directory", + "key": "MakeDirectory", + "value": 2008 + }, + { + "documentation": "An application entity title (AET) cannot be empty or be longer than 16 characters", + "key": "BadApplicationEntityTitle", + "value": 2009 + }, + { + "documentation": "No request handler factory for DICOM C-FIND SCP", + "key": "NoCFindHandler", + "value": 2010 + }, + { + "documentation": "No request handler factory for DICOM C-MOVE SCP", + "key": "NoCMoveHandler", + "value": 2011 + }, + { + "documentation": "No request handler factory for DICOM C-STORE SCP", + "key": "NoCStoreHandler", + "value": 2012 + }, + { + "documentation": "No application entity filter", + "key": "NoApplicationEntityFilter", + "value": 2013 + }, + { + "documentation": "DicomUserConnection: Unable to find the SOP class and instance", + "key": "NoSopClassOrInstance", + "value": 2014 + }, + { + "documentation": "DicomUserConnection: No acceptable presentation context for modality", + "key": "NoPresentationContext", + "value": 2015 + }, + { + "documentation": "DicomUserConnection: The C-FIND command is not supported by the remote SCP", + "key": "DicomFindUnavailable", + "value": 2016 + }, + { + "documentation": "DicomUserConnection: The C-MOVE command is not supported by the remote SCP", + "key": "DicomMoveUnavailable", + "value": 2017 + }, + { + "documentation": "Cannot store an instance", + "key": "CannotStoreInstance", + "value": 2018 + }, + { + "documentation": "Only string values are supported when creating DICOM instances", + "key": "CreateDicomNotString", + "value": 2019 + }, + { + "documentation": "Trying to override a value inherited from a parent module", + "key": "CreateDicomOverrideTag", + "value": 2020 + }, + { + "documentation": "Use \\\"Content\\\" to inject an image into a new DICOM instance", + "key": "CreateDicomUseContent", + "value": 2021 + }, + { + "documentation": "No payload is present for one instance in the series", + "key": "CreateDicomNoPayload", + "value": 2022 + }, + { + "documentation": "The payload of the DICOM instance must be specified according to Data URI scheme", + "key": "CreateDicomUseDataUriScheme", + "value": 2023 + }, + { + "documentation": "Trying to attach a new DICOM instance to an inexistent resource", + "key": "CreateDicomBadParent", + "value": 2024 + }, + { + "documentation": "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)", + "key": "CreateDicomParentIsInstance", + "value": 2025 + }, + { + "documentation": "Unable to get the encoding of the parent resource", + "key": "CreateDicomParentEncoding", + "value": 2026 + }, + { + "documentation": "Unknown modality", + "key": "UnknownModality", + "value": 2027 + }, + { + "documentation": "Bad ordering of filters in a job", + "key": "BadJobOrdering", + "value": 2028 + }, + { + "documentation": "Cannot convert the given JSON object to a Lua table", + "key": "JsonToLuaTable", + "value": 2029 + }, + { + "documentation": "Cannot create the Lua context", + "key": "CannotCreateLua", + "value": 2030 + }, + { + "documentation": "Cannot execute a Lua command", + "key": "CannotExecuteLua", + "value": 2031 + }, + { + "documentation": "Arguments cannot be pushed after the Lua function is executed", + "key": "LuaAlreadyExecuted", + "value": 2032 + }, + { + "documentation": "The Lua function does not give the expected number of outputs", + "key": "LuaBadOutput", + "value": 2033 + }, + { + "documentation": "The Lua function is not a predicate (only true/false outputs allowed)", + "key": "NotLuaPredicate", + "value": 2034 + }, + { + "documentation": "The Lua function does not return a string", + "key": "LuaReturnsNoString", + "value": 2035 + }, + { + "documentation": "Another plugin has already registered a custom storage area", + "key": "StorageAreaAlreadyRegistered", + "value": 2036 + }, + { + "documentation": "Another plugin has already registered a custom database back-end", + "key": "DatabaseBackendAlreadyRegistered", + "value": 2037 + }, + { + "documentation": "Plugin trying to call the database during its initialization", + "key": "DatabaseNotInitialized", + "value": 2038 + }, + { + "documentation": "Orthanc has been built without SSL support", + "key": "SslDisabled", + "value": 2039 + }, + { + "documentation": "Unable to order the slices of the series", + "key": "CannotOrderSlices", + "value": 2040 + }, + { + "documentation": "No request handler factory for DICOM C-Find Modality SCP", + "key": "NoWorklistHandler", + "value": 2041 + }, + { + "documentation": "Cannot override the value of a tag that already exists", + "key": "AlreadyExistingTag", + "value": 2042 + }, + { + "documentation": "No request handler factory for DICOM N-ACTION SCP (storage commitment)", + "key": "NoStorageCommitmentHandler", + "value": 2043 + }, + { + "documentation": "No request handler factory for DICOM C-GET SCP", + "key": "NoCGetHandler", + "value": 2044 + }, + { + "documentation": "DicomUserConnection: The C-GET command is not supported by the remote SCP", + "key": "DicomGetUnavailable", + "value": 2045 + }, + { + "documentation": "Unsupported media type", + "key": "UnsupportedMediaType", + "value": 3000 + } + ] + }, + { + "documentation": "Status associated with the authentication of a HTTP request.", + "name": "OrthancPluginHttpAuthenticationStatus", + "since_sdk": [ + 1, + 12, + 9 + ], + "values": [ + { + "documentation": "The authentication has been granted", + "key": "Granted", + "value": 0 + }, + { + "documentation": "The authentication has failed (401 HTTP status)", + "key": "Unauthorized", + "value": 1 + }, + { + "documentation": "The authorization has failed (403 HTTP status)", + "key": "Forbidden", + "value": 2 + }, + { + "documentation": "Redirect to another path (307 HTTP status, e.g., for login)", + "key": "Redirect", + "value": 3 + } + ] + }, + { + "documentation": "The various HTTP methods for a REST call.", + "name": "OrthancPluginHttpMethod", + "values": [ + { + "documentation": "GET request", + "key": "Get", + "value": 1 + }, + { + "documentation": "POST request", + "key": "Post", + "value": 2 + }, + { + "documentation": "PUT request", + "key": "Put", + "value": 3 + }, + { + "documentation": "DELETE request", + "key": "Delete", + "value": 4 + } + ] + }, + { + "documentation": "The constraints on the DICOM identifiers that must be supported by the database plugins.", + "name": "OrthancPluginIdentifierConstraint", + "values": [ + { + "documentation": "Equal", + "key": "Equal", + "value": 1 + }, + { + "documentation": "Less or equal", + "key": "SmallerOrEqual", + "value": 2 + }, + { + "documentation": "More or equal", + "key": "GreaterOrEqual", + "value": 3 + }, + { + "documentation": "Case-sensitive wildcard matching (with * and ?)", + "key": "Wildcard", + "value": 4 + } + ] + }, + { + "documentation": "The image formats that are supported by the Orthanc core.", + "name": "OrthancPluginImageFormat", + "values": [ + { + "documentation": "Image compressed using PNG", + "key": "Png", + "value": 0 + }, + { + "documentation": "Image compressed using JPEG", + "key": "Jpeg", + "value": 1 + }, + { + "documentation": "Image compressed using DICOM", + "key": "Dicom", + "value": 2 + } + ] + }, + { + "documentation": "The origin of a DICOM instance that has been received by Orthanc.", + "name": "OrthancPluginInstanceOrigin", + "values": [ + { + "documentation": "Unknown origin", + "key": "Unknown", + "value": 1 + }, + { + "documentation": "Instance received through DICOM protocol", + "key": "DicomProtocol", + "value": 2 + }, + { + "documentation": "Instance received through REST API of Orthanc", + "key": "RestApi", + "value": 3 + }, + { + "documentation": "Instance added to Orthanc by a plugin", + "key": "Plugin", + "value": 4 + }, + { + "documentation": "Instance added to Orthanc by a Lua script", + "key": "Lua", + "value": 5 + }, + { + "documentation": "Instance received through WebDAV (new in 1.8.0)", + "key": "WebDav", + "since_sdk": [ + 1, + 8, + 0 + ], + "value": 6 + } + ] + }, + { + "documentation": "The possible status for one single step of a job.", + "name": "OrthancPluginJobStepStatus", + "since_sdk": [ + 1, + 4, + 2 + ], + "values": [ + { + "documentation": "The job has successfully executed all its steps", + "key": "Success", + "value": 1 + }, + { + "documentation": "The job has failed while executing this step", + "key": "Failure", + "value": 2 + }, + { + "documentation": "The job has still data to process after this step", + "key": "Continue", + "value": 3 + } + ] + }, + { + "documentation": "Explains why the job should stop and release the resources it has allocated. This is especially important to disambiguate between the \"paused\" condition and the \"final\" conditions (success, failure, or canceled).", + "name": "OrthancPluginJobStopReason", + "since_sdk": [ + 1, + 4, + 2 + ], + "values": [ + { + "documentation": "The job has succeeded", + "key": "Success", + "value": 1 + }, + { + "documentation": "The job was paused, and will be resumed later", + "key": "Paused", + "value": 2 + }, + { + "documentation": "The job has failed, and might be resubmitted later", + "key": "Failure", + "value": 3 + }, + { + "documentation": "The job was canceled, and might be resubmitted later", + "key": "Canceled", + "value": 4 + } + ] + }, + { + "documentation": "Mode specifying how to load a DICOM instance.", + "name": "OrthancPluginLoadDicomInstanceMode", + "since_sdk": [ + 1, + 12, + 1 + ], + "values": [ + { + "documentation": "Load the whole DICOM file, including pixel data", + "key": "WholeDicom", + "value": 1 + }, + { + "documentation": "Load the whole DICOM file until pixel data, which speeds up the loading", + "key": "UntilPixelData", + "value": 2 + }, + { + "documentation": "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", + "key": "EmptyPixelData", + "value": 3 + } + ] + }, + { + "documentation": "The log categories supported by Orthanc. These values must match those of enumeration \"LogCategory\" in the Orthanc Core.", + "name": "OrthancPluginLogCategory", + "since_sdk": [ + 1, + 12, + 4 + ], + "values": [ + { + "documentation": "Generic (default) category", + "key": "Generic", + "value": 1 + }, + { + "documentation": "Plugin engine related logs (shall not be used by plugins)", + "key": "Plugins", + "value": 2 + }, + { + "documentation": "HTTP related logs", + "key": "Http", + "value": 4 + }, + { + "documentation": "SQLite related logs (shall not be used by plugins)", + "key": "Sqlite", + "value": 8 + }, + { + "documentation": "DICOM related logs", + "key": "Dicom", + "value": 16 + }, + { + "documentation": "jobs related logs", + "key": "Jobs", + "value": 32 + }, + { + "documentation": "Lua related logs (shall not be used by plugins)", + "key": "Lua", + "value": 64 + } + ] + }, + { + "documentation": "The log levels supported by Orthanc. These values must match those of enumeration \"LogLevel\" in the Orthanc Core.", + "name": "OrthancPluginLogLevel", + "since_sdk": [ + 1, + 12, + 4 + ], + "values": [ + { + "documentation": "Error log level", + "key": "Error", + "value": 0 + }, + { + "documentation": "Warning log level", + "key": "Warning", + "value": 1 + }, + { + "documentation": "Info log level", + "key": "Info", + "value": 2 + }, + { + "documentation": "Trace log level", + "key": "Trace", + "value": 3 + } + ] + }, + { + "documentation": "The available types of metrics.", + "name": "OrthancPluginMetricsType", + "since_sdk": [ + 1, + 5, + 4 + ], + "values": [ + { + "documentation": "Default metrics", + "key": "Default", + "value": 0 + }, + { + "documentation": "This metrics represents a time duration. Orthanc will keep the maximum value of the metrics over a sliding window of ten seconds, which is useful if the metrics is sampled frequently.", + "key": "Timer", + "value": 1 + } + ] + }, + { + "documentation": "The memory layout of the pixels of an image.", + "name": "OrthancPluginPixelFormat", + "values": [ + { + "documentation": "Graylevel 8bpp image. The image is graylevel. Each pixel is unsigned and stored in one byte.", + "key": "Grayscale8", + "value": 1 + }, + { + "documentation": "Graylevel, unsigned 16bpp image. The image is graylevel. Each pixel is unsigned and stored in two bytes.", + "key": "Grayscale16", + "value": 2 + }, + { + "documentation": "Graylevel, signed 16bpp image. The image is graylevel. Each pixel is signed and stored in two bytes.", + "key": "SignedGrayscale16", + "value": 3 + }, + { + "documentation": "Color image in RGB24 format. This format describes a color image. The pixels are stored in 3 consecutive bytes. The memory layout is RGB.", + "key": "RGB24", + "value": 4 + }, + { + "documentation": "Color image in RGBA32 format. This format describes a color image. The pixels are stored in 4 consecutive bytes. The memory layout is RGBA.", + "key": "RGBA32", + "value": 5 + }, + { + "documentation": "Unknown pixel format", + "key": "Unknown", + "value": 6 + }, + { + "documentation": "Color image in RGB48 format. This format describes a color image. The pixels are stored in 6 consecutive bytes. The memory layout is RRGGBB.", + "key": "RGB48", + "since_sdk": [ + 1, + 3, + 1 + ], + "value": 7 + }, + { + "documentation": "Graylevel, unsigned 32bpp image. The image is graylevel. Each pixel is unsigned and stored in four bytes.", + "key": "Grayscale32", + "since_sdk": [ + 1, + 3, + 1 + ], + "value": 8 + }, + { + "documentation": "Graylevel, floating-point 32bpp image. The image is graylevel. Each pixel is floating-point and stored in four bytes.", + "key": "Float32", + "since_sdk": [ + 1, + 3, + 1 + ], + "value": 9 + }, + { + "documentation": "Color image in BGRA32 format. This format describes a color image. The pixels are stored in 4 consecutive bytes. The memory layout is BGRA.", + "key": "BGRA32", + "since_sdk": [ + 1, + 3, + 1 + ], + "value": 10 + }, + { + "documentation": "Graylevel, unsigned 64bpp image. The image is graylevel. Each pixel is unsigned and stored in eight bytes.", + "key": "Grayscale64", + "since_sdk": [ + 1, + 4, + 0 + ], + "value": 11 + } + ] + }, + { + "documentation": "The supported modes to remove an element from a queue.", + "name": "OrthancPluginQueueOrigin", + "since_sdk": [ + 1, + 12, + 8 + ], + "values": [ + { + "documentation": "Dequeue from the front of the queue", + "key": "Front", + "value": 0 + }, + { + "documentation": "Dequeue from the back of the queue", + "key": "Back", + "value": 1 + } + ] + }, + { + "documentation": "The action to be taken after ReceivedInstanceCallback is triggered", + "name": "OrthancPluginReceivedInstanceAction", + "since_sdk": [ + 1, + 10, + 0 + ], + "values": [ + { + "documentation": "Keep the instance as is", + "key": "KeepAsIs", + "value": 1 + }, + { + "documentation": "Modify the instance", + "key": "Modify", + "value": 2 + }, + { + "documentation": "Discard the instance", + "key": "Discard", + "value": 3 + } + ] + }, + { + "documentation": "The supported types of DICOM resources.", + "name": "OrthancPluginResourceType", + "values": [ + { + "documentation": "Patient", + "key": "Patient", + "value": 0 + }, + { + "documentation": "Study", + "key": "Study", + "value": 1 + }, + { + "documentation": "Series", + "key": "Series", + "value": 2 + }, + { + "documentation": "Instance", + "key": "Instance", + "value": 3 + }, + { + "documentation": "Unavailable resource type", + "key": "None", + "value": 4 + } + ] + }, + { + "documentation": "The \"Stable\" status of a resource.", + "name": "OrthancPluginStableStatus", + "since_sdk": [ + 1, + 12, + 9 + ], + "values": [ + { + "documentation": "The resource is stable", + "key": "Stable", + "value": 0 + }, + { + "documentation": "The resource is unstable", + "key": "Unstable", + "value": 1 + } + ] + }, + { + "documentation": "The available values for the Failure Reason (0008,1197) during storage commitment. http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1", + "name": "OrthancPluginStorageCommitmentFailureReason", + "since_sdk": [ + 1, + 6, + 0 + ], + "values": [ + { + "documentation": "Success: The DICOM instance is properly stored in the SCP", + "key": "Success", + "value": 0 + }, + { + "documentation": "0110H: A general failure in processing the operation was encountered", + "key": "ProcessingFailure", + "value": 1 + }, + { + "documentation": "0112H: One or more of the elements in the Referenced SOP Instance Sequence was not available", + "key": "NoSuchObjectInstance", + "value": 2 + }, + { + "documentation": "0213H: The SCP does not currently have enough resources to store the requested SOP Instance(s)", + "key": "ResourceLimitation", + "value": 3 + }, + { + "documentation": "0122H: Storage Commitment has been requested for a SOP Instance with a SOP Class that is not supported by the SCP", + "key": "ReferencedSOPClassNotSupported", + "value": 4 + }, + { + "documentation": "0119H: The SOP Class of an element in the Referenced SOP Instance Sequence did not correspond to the SOP class registered for this SOP Instance at the SCP", + "key": "ClassInstanceConflict", + "value": 5 + }, + { + "documentation": "0131H: The Transaction UID of the Storage Commitment Request is already in use", + "key": "DuplicateTransactionUID", + "value": 6 + } + ] + }, + { + "documentation": "The store status related to the adoption of a DICOM instance.", + "name": "OrthancPluginStoreStatus", + "since_sdk": [ + 1, + 12, + 8 + ], + "values": [ + { + "documentation": "The file has been stored/adopted", + "key": "Success", + "value": 0 + }, + { + "documentation": "The file has already been stored/adopted (only if OverwriteInstances is set to false)", + "key": "AlreadyStored", + "value": 1 + }, + { + "documentation": "The file could not be stored/adopted", + "key": "Failure", + "value": 2 + }, + { + "documentation": "The file has been filtered out by a Lua script or a plugin", + "key": "FilteredOut", + "value": 3 + }, + { + "documentation": "The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)", + "key": "StorageFull", + "value": 4 + } + ] + }, + { + "documentation": "The value representations present in the DICOM standard (version 2013).", + "name": "OrthancPluginValueRepresentation", + "values": [ + { + "documentation": "Application Entity", + "key": "AE", + "value": 1 + }, + { + "documentation": "Age String", + "key": "AS", + "value": 2 + }, + { + "documentation": "Attribute Tag", + "key": "AT", + "value": 3 + }, + { + "documentation": "Code String", + "key": "CS", + "value": 4 + }, + { + "documentation": "Date", + "key": "DA", + "value": 5 + }, + { + "documentation": "Decimal String", + "key": "DS", + "value": 6 + }, + { + "documentation": "Date Time", + "key": "DT", + "value": 7 + }, + { + "documentation": "Floating Point Double", + "key": "FD", + "value": 8 + }, + { + "documentation": "Floating Point Single", + "key": "FL", + "value": 9 + }, + { + "documentation": "Integer String", + "key": "IS", + "value": 10 + }, + { + "documentation": "Long String", + "key": "LO", + "value": 11 + }, + { + "documentation": "Long Text", + "key": "LT", + "value": 12 + }, + { + "documentation": "Other Byte String", + "key": "OB", + "value": 13 + }, + { + "documentation": "Other Float String", + "key": "OF", + "value": 14 + }, + { + "documentation": "Other Word String", + "key": "OW", + "value": 15 + }, + { + "documentation": "Person Name", + "key": "PN", + "value": 16 + }, + { + "documentation": "Short String", + "key": "SH", + "value": 17 + }, + { + "documentation": "Signed Long", + "key": "SL", + "value": 18 + }, + { + "documentation": "Sequence of Items", + "key": "SQ", + "value": 19 + }, + { + "documentation": "Signed Short", + "key": "SS", + "value": 20 + }, + { + "documentation": "Short Text", + "key": "ST", + "value": 21 + }, + { + "documentation": "Time", + "key": "TM", + "value": 22 + }, + { + "documentation": "Unique Identifier (UID)", + "key": "UI", + "value": 23 + }, + { + "documentation": "Unsigned Long", + "key": "UL", + "value": 24 + }, + { + "documentation": "Unknown", + "key": "UN", + "value": 25 + }, + { + "documentation": "Unsigned Short", + "key": "US", + "value": 26 + }, + { + "documentation": "Unlimited Text", + "key": "UT", + "value": 27 + } + ] + } + ], + "global_functions": [ + { + "args": [ + { + "name": "arg0", + "sdk_name": "expectedMajor", + "sdk_type": "int32_t" + }, + { + "name": "arg1", + "sdk_name": "expectedMinor", + "sdk_type": "int32_t" + }, + { + "name": "arg2", + "sdk_name": "expectedRevision", + "sdk_type": "int32_t" + } + ], + "c_function": "OrthancPluginCheckVersionAdvanced", + "documentation": { + "args": { + "expectedMajor": "Expected major version.", + "expectedMinor": "Expected minor version.", + "expectedRevision": "Expected revision." + }, + "description": [ + "This function checks whether the version of the Orthanc server running this plugin, is above the given version. Contrarily to OrthancPluginCheckVersion(), it is up to the developer of the plugin to make sure that all the Orthanc SDK services called by the plugin are actually implemented in the given version of Orthanc." + ], + "return": "1 if and only if the versions are compatible. If the result is 0, the initialization of the plugin should fail.", + "summary": "Check that the version of the hosting Orthanc is above a given version." + }, + "return_sdk_type": "int32_t", + "since_sdk": [ + 1, + 4, + 0 + ] + }, + { + "args": [], + "c_function": "OrthancPluginCheckVersion", + "documentation": { + "args": {}, + "description": [ + "This function checks whether the version of the Orthanc server running this plugin, is above the version of the current Orthanc SDK header. This guarantees that the plugin is compatible with the hosting Orthanc (i.e. it will not call unavailable services). The result of this function should always be checked in the OrthancPluginInitialize() entry point of the plugin." + ], + "return": "1 if and only if the versions are compatible. If the result is 0, the initialization of the plugin should fail.", + "summary": "Check the compatibility of the plugin wrt. the version of its hosting Orthanc." + }, + "return_sdk_type": "int32_t" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "message", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLogError", + "documentation": { + "args": { + "message": "The message to be logged." + }, + "description": [ + "Log an error message using the Orthanc logging system." + ], + "summary": "Log an error." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "message", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLogWarning", + "documentation": { + "args": { + "message": "The message to be logged." + }, + "description": [ + "Log a warning message using the Orthanc logging system." + ], + "summary": "Log a warning." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "message", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLogInfo", + "documentation": { + "args": { + "message": "The message to be logged." + }, + "description": [ + "Log an information message using the Orthanc logging system." + ], + "summary": "Log an information." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "instanceId", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginGetDicomForInstance", + "documentation": { + "args": { + "instanceId": "The Orthanc identifier of the DICOM instance of interest.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "Retrieve a DICOM instance using its Orthanc identifier. The DICOM file is stored into a newly allocated memory buffer." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Retrieve a DICOM instance using its Orthanc identifier." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginRestApiGet", + "documentation": { + "args": { + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "uri": "The URI in the built-in Orthanc API." + }, + "description": [ + "Make a GET call to the built-in Orthanc REST API. The result to the query is stored into a newly allocated memory buffer.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a GET call to the built-in Orthanc REST API." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginRestApiGetAfterPlugins", + "documentation": { + "args": { + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "uri": "The URI in the built-in Orthanc API." + }, + "description": [ + "Make a GET call to the Orthanc REST API, after all the plugins are applied. In other words, if some plugin overrides or adds the called URI to the built-in Orthanc REST API, this call will return the result provided by this plugin. The result to the query is stored into a newly allocated memory buffer.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a GET call to the REST API, as tainted by the plugins." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "body", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginRestApiPost", + "documentation": { + "args": { + "body": "The body of the POST request.", + "bodySize": "The size of the body.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "uri": "The URI in the built-in Orthanc API." + }, + "description": [ + "Make a POST call to the built-in Orthanc REST API. The result to the query is stored into a newly allocated memory buffer.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a POST call to the built-in Orthanc REST API." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "body", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginRestApiPostAfterPlugins", + "documentation": { + "args": { + "body": "The body of the POST request.", + "bodySize": "The size of the body.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "uri": "The URI in the built-in Orthanc API." + }, + "description": [ + "Make a POST call to the Orthanc REST API, after all the plugins are applied. In other words, if some plugin overrides or adds the called URI to the built-in Orthanc REST API, this call will return the result provided by this plugin. The result to the query is stored into a newly allocated memory buffer.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a POST call to the REST API, as tainted by the plugins." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginRestApiDelete", + "documentation": { + "args": { + "uri": "The URI to delete in the built-in Orthanc API." + }, + "description": [ + "Make a DELETE call to the built-in Orthanc REST API.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a DELETE call to the built-in Orthanc REST API." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginRestApiDeleteAfterPlugins", + "documentation": { + "args": { + "uri": "The URI to delete in the built-in Orthanc API." + }, + "description": [ + "Make a DELETE call to the Orthanc REST API, after all the plugins are applied. In other words, if some plugin overrides or adds the called URI to the built-in Orthanc REST API, this call will return the result provided by this plugin.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a DELETE call to the REST API, as tainted by the plugins." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "body", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginRestApiPut", + "documentation": { + "args": { + "body": "The body of the PUT request.", + "bodySize": "The size of the body.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "uri": "The URI in the built-in Orthanc API." + }, + "description": [ + "Make a PUT call to the built-in Orthanc REST API. The result to the query is stored into a newly allocated memory buffer.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a PUT call to the built-in Orthanc REST API." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "body", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginRestApiPutAfterPlugins", + "documentation": { + "args": { + "body": "The body of the PUT request.", + "bodySize": "The size of the body.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "uri": "The URI in the built-in Orthanc API." + }, + "description": [ + "Make a PUT call to the Orthanc REST API, after all the plugins are applied. In other words, if some plugin overrides or adds the called URI to the built-in Orthanc REST API, this call will return the result provided by this plugin. The result to the query is stored into a newly allocated memory buffer.", + "Remark: If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Make a PUT call to the REST API, as tainted by the plugins." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "patientID", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLookupPatient", + "documentation": { + "args": { + "patientID": "The Patient ID of interest." + }, + "description": [ + "Look for a patient stored in Orthanc, using its Patient ID tag (0x0010, 0x0020). This function uses the database index to run as fast as possible (it does not loop over all the stored patients)." + ], + "return": "The NULL value if the patient is non-existent, or a string containing the Orthanc ID of the patient. This string must be freed by OrthancPluginFreeString().", + "summary": "Look for a patient." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "studyUID", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLookupStudy", + "documentation": { + "args": { + "studyUID": "The Study Instance UID of interest." + }, + "description": [ + "Look for a study stored in Orthanc, using its Study Instance UID tag (0x0020, 0x000d). This function uses the database index to run as fast as possible (it does not loop over all the stored studies)." + ], + "return": "The NULL value if the study is non-existent, or a string containing the Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().", + "summary": "Look for a study." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "accessionNumber", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLookupStudyWithAccessionNumber", + "documentation": { + "args": { + "accessionNumber": "The Accession Number of interest." + }, + "description": [ + "Look for a study stored in Orthanc, using its Accession Number tag (0x0008, 0x0050). This function uses the database index to run as fast as possible (it does not loop over all the stored studies)." + ], + "return": "The NULL value if the study is non-existent, or a string containing the Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().", + "summary": "Look for a study, using the accession number." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "seriesUID", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLookupSeries", + "documentation": { + "args": { + "seriesUID": "The Series Instance UID of interest." + }, + "description": [ + "Look for a series stored in Orthanc, using its Series Instance UID tag (0x0020, 0x000e). This function uses the database index to run as fast as possible (it does not loop over all the stored series)." + ], + "return": "The NULL value if the series is non-existent, or a string containing the Orthanc ID of the series. This string must be freed by OrthancPluginFreeString().", + "summary": "Look for a series." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "sopInstanceUID", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginLookupInstance", + "documentation": { + "args": { + "sopInstanceUID": "The SOP Instance UID of interest." + }, + "description": [ + "Look for an instance stored in Orthanc, using its SOP Instance UID tag (0x0008, 0x0018). This function uses the database index to run as fast as possible (it does not loop over all the stored instances)." + ], + "return": "The NULL value if the instance is non-existent, or a string containing the Orthanc ID of the instance. This string must be freed by OrthancPluginFreeString().", + "summary": "Look for an instance." + }, + "return_sdk_type": "char *" + }, + { + "args": [], + "c_function": "OrthancPluginGetOrthancPath", + "documentation": { + "args": {}, + "description": [ + "This function returns the path to the Orthanc executable." + ], + "return": "NULL in the case of an error, or a newly allocated string containing the path. This string must be freed by OrthancPluginFreeString().", + "summary": "Return the path to the Orthanc executable." + }, + "return_sdk_type": "char *" + }, + { + "args": [], + "c_function": "OrthancPluginGetOrthancDirectory", + "documentation": { + "args": {}, + "description": [ + "This function returns the path to the directory containing the Orthanc executable." + ], + "return": "NULL in the case of an error, or a newly allocated string containing the path. This string must be freed by OrthancPluginFreeString().", + "summary": "Return the directory containing the Orthanc." + }, + "return_sdk_type": "char *" + }, + { + "args": [], + "c_function": "OrthancPluginGetConfigurationPath", + "documentation": { + "args": {}, + "description": [ + "This function returns the path to the configuration file(s) that was specified when starting Orthanc. Since version 0.9.1, this path can refer to a folder that stores a set of configuration files. This function is deprecated in favor of OrthancPluginGetConfiguration()." + ], + "return": "NULL in the case of an error, or a newly allocated string containing the path. This string must be freed by OrthancPluginFreeString().", + "summary": "Return the path to the configuration file(s)." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "uri", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetRootUri", + "documentation": { + "args": { + "uri": "The root URI for this plugin." + }, + "description": [ + "For plugins that come with a Web interface, this function declares the entry path where to find this interface. This information is notably used in the \"Plugins\" page of Orthanc Explorer." + ], + "summary": "Set the URI where the plugin provides its Web interface." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "plugin", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "uri", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetRootUri2", + "documentation": { + "args": { + "plugin": "Identifier of your plugin (it must match \"OrthancPluginGetName()\").", + "uri": "The root URI for this plugin." + }, + "description": [ + "For plugins that come with a Web interface, this function declares the entry path where to find this interface. This information is notably used in the \"Plugins\" page of Orthanc Explorer." + ], + "summary": "Set the URI where the plugin provides its Web interface." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 12, + 4 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "description", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetDescription", + "documentation": { + "args": { + "description": "The description." + }, + "description": [ + "Set a description for this plugin. It is displayed in the \"Plugins\" page of Orthanc Explorer." + ], + "summary": "Set a description for this plugin." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "plugin", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "description", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetDescription2", + "documentation": { + "args": { + "description": "The description.", + "plugin": "Identifier of your plugin (it must match \"OrthancPluginGetName()\")." + }, + "description": [ + "Set a description for this plugin. It is displayed in the \"Plugins\" page of Orthanc Explorer." + ], + "summary": "Set a description for this plugin." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 12, + 4 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "javascript", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginExtendOrthancExplorer", + "documentation": { + "args": { + "javascript": "The custom JavaScript code." + }, + "description": [ + "Add JavaScript code to customize the default behavior of Orthanc Explorer. This can for instance be used to add new buttons." + ], + "summary": "Extend the JavaScript code of Orthanc Explorer." + }, + "return_sdk_type": "void" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "plugin", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "javascript", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginExtendOrthancExplorer2", + "documentation": { + "args": { + "javascript": "The custom JavaScript code.", + "plugin": "Identifier of your plugin (it must match \"OrthancPluginGetName()\")." + }, + "description": [ + "Add JavaScript code to customize the default behavior of Orthanc Explorer. This can for instance be used to add new buttons." + ], + "summary": "Extend the JavaScript code of Orthanc Explorer." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 12, + 4 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "property", + "sdk_type": "int32_t" + }, + { + "name": "arg1", + "sdk_name": "defaultValue", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginGetGlobalProperty", + "documentation": { + "args": { + "defaultValue": "The value to return, if the global property is unset.", + "property": "The global property of interest." + }, + "description": [ + "Get the value of a global property that is stored in the Orthanc database. Global properties whose index is below 1024 are reserved by Orthanc." + ], + "return": "The value of the global property, or NULL in the case of an error. This string must be freed by OrthancPluginFreeString().", + "summary": "Get the value of a global property." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "property", + "sdk_type": "int32_t" + }, + { + "name": "arg1", + "sdk_name": "value", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetGlobalProperty", + "documentation": { + "args": { + "property": "The global property of interest.", + "value": "The value to be set in the global property." + }, + "description": [ + "Set the value of a global property into the Orthanc database. Setting a global property can be used by plugins to save their internal parameters. Plugins are only allowed to set properties whose index are above or equal to 1024 (properties below 1024 are read-only and reserved by Orthanc)." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Set the value of a global property." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [], + "c_function": "OrthancPluginGetCommandLineArgumentsCount", + "documentation": { + "args": {}, + "description": [ + "Retrieve the number of command-line arguments that were used to launch Orthanc." + ], + "return": "The number of arguments.", + "summary": "Get the number of command-line arguments." + }, + "return_sdk_type": "uint32_t" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "argument", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetCommandLineArgument", + "documentation": { + "args": { + "argument": "The index of the argument." + }, + "description": [ + "Get the value of one of the command-line arguments that were used to launch Orthanc. The number of available arguments can be retrieved by OrthancPluginGetCommandLineArgumentsCount()." + ], + "return": "The value of the argument, or NULL in the case of an error. This string must be freed by OrthancPluginFreeString().", + "summary": "Get the value of a command-line argument." + }, + "return_sdk_type": "char *" + }, + { + "args": [], + "c_function": "OrthancPluginGetExpectedDatabaseVersion", + "documentation": { + "args": {}, + "description": [ + "Retrieve the expected version of the database schema." + ], + "return": "The version.", + "summary": "Get the expected version of the database schema." + }, + "return_sdk_type": "uint32_t" + }, + { + "args": [], + "c_function": "OrthancPluginGetConfiguration", + "documentation": { + "args": {}, + "description": [ + "This function returns the content of the configuration that is used by Orthanc, formatted as a JSON string." + ], + "return": "NULL in the case of an error, or a newly allocated string containing the configuration. This string must be freed by OrthancPluginFreeString().", + "summary": "Return the content of the configuration file(s)." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "source", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginCompressionType", + "sdk_name": "compression", + "sdk_type": "enumeration" + }, + { + "name": "arg3", + "sdk_name": "uncompress", + "sdk_type": "uint8_t" + } + ], + "c_function": "OrthancPluginBufferCompression", + "documentation": { + "args": { + "compression": "The compression algorithm.", + "size": "The size in bytes of the source buffer.", + "source": "The source buffer.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "uncompress": "If set to \"0\", the buffer must be compressed. If set to \"1\", the buffer must be uncompressed." + }, + "description": [ + "This function compresses or decompresses a buffer, using the version of the zlib library that is used by the Orthanc core." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Compress or decompress a buffer." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "path", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginReadFile", + "documentation": { + "args": { + "path": "The path of the file to be read.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "Read the content of a file on the filesystem, and returns it into a newly allocated memory buffer." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Read a file." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "path", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "data", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginWriteFile", + "documentation": { + "args": { + "data": "The content of the memory buffer.", + "path": "The path of the file to be written.", + "size": "The size of the memory buffer." + }, + "description": [ + "Write the content of a memory buffer to the filesystem." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Write a file." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginErrorCode", + "sdk_name": "error", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginGetErrorDescription", + "documentation": { + "args": { + "error": "The error code of interest." + }, + "description": [ + "This function returns the description of a given error code." + ], + "return": "The error description. This is a statically-allocated string, do not free it.", + "summary": "Get the description of a given error code." + }, + "return_sdk_type": "const char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "data", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginImageFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginUncompressImage", + "documentation": { + "args": { + "data": "Pointer to a memory buffer containing the compressed image.", + "format": "The file format of the compressed image.", + "size": "Size of the memory buffer containing the compressed image." + }, + "description": [ + "This function decodes a compressed image from a memory buffer." + ], + "return": "The uncompressed image. It must be freed with OrthancPluginFreeImage().", + "summary": "Decode a compressed image." + }, + "return_sdk_class": "OrthancPluginImage", + "return_sdk_type": "object" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginPixelFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg1", + "sdk_name": "width", + "sdk_type": "uint32_t" + }, + { + "name": "arg2", + "sdk_name": "height", + "sdk_type": "uint32_t" + }, + { + "name": "arg3", + "sdk_name": "pitch", + "sdk_type": "uint32_t" + }, + { + "name": "arg4", + "sdk_name": "buffer", + "sdk_type": "const void *" + } + ], + "c_function": "OrthancPluginCompressPngImage", + "documentation": { + "args": { + "buffer": "The memory buffer containing the uncompressed image.", + "format": "The memory layout of the uncompressed image.", + "height": "The height of the image.", + "pitch": "The pitch of the image (i.e. the number of bytes between 2 successive lines of the image in the memory buffer).", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "width": "The width of the image." + }, + "description": [ + "This function compresses the given memory buffer containing an image using the PNG specification, and stores the result of the compression into a newly allocated memory buffer." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Encode a PNG image." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginPixelFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg1", + "sdk_name": "width", + "sdk_type": "uint32_t" + }, + { + "name": "arg2", + "sdk_name": "height", + "sdk_type": "uint32_t" + }, + { + "name": "arg3", + "sdk_name": "pitch", + "sdk_type": "uint32_t" + }, + { + "name": "arg4", + "sdk_name": "buffer", + "sdk_type": "const void *" + }, + { + "name": "arg5", + "sdk_name": "quality", + "sdk_type": "uint8_t" + } + ], + "c_function": "OrthancPluginCompressJpegImage", + "documentation": { + "args": { + "buffer": "The memory buffer containing the uncompressed image.", + "format": "The memory layout of the uncompressed image.", + "height": "The height of the image.", + "pitch": "The pitch of the image (i.e. the number of bytes between 2 successive lines of the image in the memory buffer).", + "quality": "The quality of the JPEG encoding, between 1 (worst quality, best compression) and 100 (best quality, worst compression).", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "width": "The width of the image." + }, + "description": [ + "This function compresses the given memory buffer containing an image using the JPEG specification, and stores the result of the compression into a newly allocated memory buffer." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Encode a JPEG image." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "url", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "username", + "sdk_type": "const char *" + }, + { + "name": "arg2", + "sdk_name": "password", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginHttpGet", + "documentation": { + "args": { + "password": "The password (can be \"NULL\" if no password protection).", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "url": "The URL of interest.", + "username": "The username (can be \"NULL\" if no password protection)." + }, + "description": [ + "Make a HTTP GET call to the given URL. The result to the query is stored into a newly allocated memory buffer. Favor OrthancPluginRestApiGet() if calling the built-in REST API of the Orthanc instance that hosts this plugin." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Issue a HTTP GET call." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "url", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "body", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg3", + "sdk_name": "username", + "sdk_type": "const char *" + }, + { + "name": "arg4", + "sdk_name": "password", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginHttpPost", + "documentation": { + "args": { + "body": "The content of the body of the request.", + "bodySize": "The size of the body of the request.", + "password": "The password (can be \"NULL\" if no password protection).", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "url": "The URL of interest.", + "username": "The username (can be \"NULL\" if no password protection)." + }, + "description": [ + "Make a HTTP POST call to the given URL. The result to the query is stored into a newly allocated memory buffer. Favor OrthancPluginRestApiPost() if calling the built-in REST API of the Orthanc instance that hosts this plugin." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Issue a HTTP POST call." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "url", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "body", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg3", + "sdk_name": "username", + "sdk_type": "const char *" + }, + { + "name": "arg4", + "sdk_name": "password", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginHttpPut", + "documentation": { + "args": { + "body": "The content of the body of the request.", + "bodySize": "The size of the body of the request.", + "password": "The password (can be \"NULL\" if no password protection).", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().", + "url": "The URL of interest.", + "username": "The username (can be \"NULL\" if no password protection)." + }, + "description": [ + "Make a HTTP PUT call to the given URL. The result to the query is stored into a newly allocated memory buffer. Favor OrthancPluginRestApiPut() if calling the built-in REST API of the Orthanc instance that hosts this plugin." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Issue a HTTP PUT call." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "url", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "username", + "sdk_type": "const char *" + }, + { + "name": "arg2", + "sdk_name": "password", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginHttpDelete", + "documentation": { + "args": { + "password": "The password (can be \"NULL\" if no password protection).", + "url": "The URL of interest.", + "username": "The username (can be \"NULL\" if no password protection)." + }, + "description": [ + "Make a HTTP DELETE call to the given URL. Favor OrthancPluginRestApiDelete() if calling the built-in REST API of the Orthanc instance that hosts this plugin." + ], + "return": "0 if success, or the error code if failure.", + "summary": "Issue a HTTP DELETE call." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [], + "c_function": "OrthancPluginGetFontsCount", + "documentation": { + "args": {}, + "description": [ + "This function returns the number of fonts that are built in the Orthanc core. These fonts can be used to draw texts on images through OrthancPluginDrawText()." + ], + "return": "The number of fonts.", + "summary": "Return the number of available fonts." + }, + "return_sdk_type": "uint32_t" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "fontIndex", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetFontName", + "documentation": { + "args": { + "fontIndex": "The index of the font. This value must be less than OrthancPluginGetFontsCount()." + }, + "description": [ + "This function returns the name of a font that is built in the Orthanc core." + ], + "return": "The font name. This is a statically-allocated string, do not free it.", + "summary": "Return the name of a font." + }, + "return_sdk_type": "const char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "fontIndex", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginGetFontSize", + "documentation": { + "args": { + "fontIndex": "The index of the font. This value must be less than OrthancPluginGetFontsCount()." + }, + "description": [ + "This function returns the size of a font that is built in the Orthanc core." + ], + "return": "The font size.", + "summary": "Return the size of a font." + }, + "return_sdk_type": "uint32_t" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "code", + "sdk_type": "int32_t" + }, + { + "name": "arg1", + "sdk_name": "httpStatus", + "sdk_type": "uint16_t" + }, + { + "name": "arg2", + "sdk_name": "message", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginRegisterErrorCode", + "documentation": { + "args": { + "code": "The error code that is internal to this plugin.", + "httpStatus": "The HTTP status corresponding to this error.", + "message": "The description of the error." + }, + "description": [ + "This function declares a custom error code that can be generated by this plugin. This declaration is used to enrich the body of the HTTP answer in the case of an error, and to set the proper HTTP status code." + ], + "return": "The error code that has been assigned inside the Orthanc core.", + "summary": "Declare a custom error code for this plugin." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "group", + "sdk_type": "uint16_t" + }, + { + "name": "arg1", + "sdk_name": "element", + "sdk_type": "uint16_t" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginValueRepresentation", + "sdk_name": "vr", + "sdk_type": "enumeration" + }, + { + "name": "arg3", + "sdk_name": "name", + "sdk_type": "const char *" + }, + { + "name": "arg4", + "sdk_name": "minMultiplicity", + "sdk_type": "uint32_t" + }, + { + "name": "arg5", + "sdk_name": "maxMultiplicity", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginRegisterDictionaryTag", + "documentation": { + "args": { + "element": "The element of the tag.", + "group": "The group of the tag.", + "maxMultiplicity": "The maximum multiplicity of the tag. A value of 0 means an arbitrary multiplicity (\"\"n\"\").", + "minMultiplicity": "The minimum multiplicity of the tag (must be above 0).", + "name": "The nickname of the tag.", + "vr": "The value representation of the tag." + }, + "description": [ + "This function declares a new public tag in the dictionary of DICOM tags that are known to Orthanc. This function should be used in the OrthancPluginInitialize() callback." + ], + "return": "0 if success, other value if error.", + "summary": "Register a new tag into the DICOM dictionary." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "group", + "sdk_type": "uint16_t" + }, + { + "name": "arg1", + "sdk_name": "element", + "sdk_type": "uint16_t" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginValueRepresentation", + "sdk_name": "vr", + "sdk_type": "enumeration" + }, + { + "name": "arg3", + "sdk_name": "name", + "sdk_type": "const char *" + }, + { + "name": "arg4", + "sdk_name": "minMultiplicity", + "sdk_type": "uint32_t" + }, + { + "name": "arg5", + "sdk_name": "maxMultiplicity", + "sdk_type": "uint32_t" + }, + { + "name": "arg6", + "sdk_name": "privateCreator", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginRegisterPrivateDictionaryTag", + "documentation": { + "args": { + "element": "The element of the tag.", + "group": "The group of the tag.", + "maxMultiplicity": "The maximum multiplicity of the tag. A value of 0 means an arbitrary multiplicity (\"\"n\"\").", + "minMultiplicity": "The minimum multiplicity of the tag (must be above 0).", + "name": "The nickname of the tag.", + "privateCreator": "The private creator of this private tag.", + "vr": "The value representation of the tag." + }, + "description": [ + "This function declares a new private tag in the dictionary of DICOM tags that are known to Orthanc. This function should be used in the OrthancPluginInitialize() callback." + ], + "return": "0 if success, other value if error.", + "summary": "Register a new private tag into the DICOM dictionary." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 2, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "buffer", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginDicomToJsonFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg3", + "sdk_enumeration": "OrthancPluginDicomToJsonFlags", + "sdk_name": "flags", + "sdk_type": "enumeration" + }, + { + "name": "arg4", + "sdk_name": "maxStringLength", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginDicomBufferToJson", + "documentation": { + "args": { + "buffer": "The memory buffer containing the DICOM file.", + "flags": "Flags governing the output.", + "format": "The output format.", + "maxStringLength": "The maximum length of a field. Too long fields will be output as \"null\". The 0 value means no maximum length.", + "size": "The size of the memory buffer." + }, + "description": [ + "This function takes as input a memory buffer containing a DICOM file, and outputs a JSON string representing the tags of this DICOM file." + ], + "return": "The NULL value if the case of an error, or the JSON string. This string must be freed by OrthancPluginFreeString().", + "summary": "Format a DICOM memory buffer as a JSON string." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "instanceId", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_enumeration": "OrthancPluginDicomToJsonFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginDicomToJsonFlags", + "sdk_name": "flags", + "sdk_type": "enumeration" + }, + { + "name": "arg3", + "sdk_name": "maxStringLength", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginDicomInstanceToJson", + "documentation": { + "args": { + "flags": "Flags governing the output.", + "format": "The output format.", + "instanceId": "The Orthanc identifier of the instance.", + "maxStringLength": "The maximum length of a field. Too long fields will be output as \"null\". The 0 value means no maximum length." + }, + "description": [ + "This function formats a DICOM instance that is stored in Orthanc, and outputs a JSON string representing the tags of this DICOM instance." + ], + "return": "The NULL value if the case of an error, or the JSON string. This string must be freed by OrthancPluginFreeString().", + "summary": "Format a DICOM instance as a JSON string." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "json", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_class": "OrthancPluginImage", + "sdk_name": "pixelData", + "sdk_type": "const_object" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginCreateDicomFlags", + "sdk_name": "flags", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginCreateDicom", + "documentation": { + "args": { + "flags": "Flags governing the output.", + "json": "The input JSON file.", + "pixelData": "The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "This function takes as input a string containing a JSON file describing the content of a DICOM instance. As an output, it writes the corresponding DICOM instance to a newly allocated memory buffer. Additionally, an image to be encoded within the DICOM instance can also be provided.", + "Private tags will be associated with the private creator whose value is specified in the \"DefaultPrivateCreator\" configuration option of Orthanc. The function OrthancPluginCreateDicom2() can be used if another private creator must be used to create this instance." + ], + "return": "0 if success, other value if error.", + "summary": "Create a DICOM instance from a JSON string and an image." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_enumeration": "OrthancPluginPixelFormat", + "sdk_name": "format", + "sdk_type": "enumeration" + }, + { + "name": "arg1", + "sdk_name": "width", + "sdk_type": "uint32_t" + }, + { + "name": "arg2", + "sdk_name": "height", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginCreateImage", + "documentation": { + "args": { + "format": "The format of the pixels.", + "height": "The height of the image.", + "width": "The width of the image." + }, + "description": [ + "This function creates an image of given size and format." + ], + "return": "The newly allocated image. It must be freed with OrthancPluginFreeImage().", + "summary": "Create an image." + }, + "return_sdk_class": "OrthancPluginImage", + "return_sdk_type": "object" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "buffer", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg2", + "sdk_name": "frameIndex", + "sdk_type": "uint32_t" + } + ], + "c_function": "OrthancPluginDecodeDicomImage", + "documentation": { + "args": { + "buffer": "Pointer to a memory buffer containing the DICOM image.", + "bufferSize": "Size of the memory buffer containing the DICOM image.", + "frameIndex": "The index of the frame of interest in a multi-frame image." + }, + "description": [ + "This function decodes one frame of a DICOM image that is stored in a memory buffer. This function will give the same result as OrthancPluginUncompressImage() for single-frame DICOM images." + ], + "return": "The uncompressed image. It must be freed with OrthancPluginFreeImage().", + "summary": "Decode one frame from a DICOM instance." + }, + "return_sdk_class": "OrthancPluginImage", + "return_sdk_type": "object" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "buffer", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginComputeMd5", + "documentation": { + "args": { + "buffer": "The source memory buffer.", + "size": "The size in bytes of the source buffer." + }, + "description": [ + "This functions computes the MD5 cryptographic hash of the given memory buffer." + ], + "return": "The NULL value in case of error, or a string containing the cryptographic hash. This string must be freed by OrthancPluginFreeString().", + "summary": "Compute an MD5 hash." + }, + "return_sdk_type": "char *" + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "buffer", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginComputeSha1", + "documentation": { + "args": { + "buffer": "The source memory buffer.", + "size": "The size in bytes of the source buffer." + }, + "description": [ + "This functions computes the SHA-1 cryptographic hash of the given memory buffer." + ], + "return": "The NULL value in case of error, or a string containing the cryptographic hash. This string must be freed by OrthancPluginFreeString().", + "summary": "Compute a SHA-1 hash." + }, + "return_sdk_type": "char *" + }, + { + "args": [], + "c_function": "OrthancPluginGenerateUuid", + "documentation": { + "args": {}, + "description": [ + "Generate a random GUID/UUID (globally unique identifier)." + ], + "return": "NULL in the case of an error, or a newly allocated string containing the UUID. This string must be freed by OrthancPluginFreeString().", + "summary": "Generate an UUID." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 1, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "query", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginCreateFindMatcher", + "documentation": { + "args": { + "query": "The C-Find DICOM query.", + "size": "The size of the DICOM query." + }, + "description": [ + "This function creates a \"matcher\" object that can be used to check whether a DICOM instance matches a C-Find query. The C-Find query must be expressed as a DICOM buffer." + ], + "return": "The newly allocated matcher. It must be freed with OrthancPluginFreeFindMatcher().", + "summary": "Create a C-Find matcher." + }, + "return_sdk_class": "OrthancPluginFindMatcher", + "return_sdk_type": "object", + "since_sdk": [ + 1, + 2, + 0 + ] + }, + { + "args": [], + "c_function": "OrthancPluginGetPeers", + "documentation": { + "args": {}, + "description": [ + "This function returns the parameters of the Orthanc peers that are known to the Orthanc server hosting the plugin." + ], + "return": "NULL if error, or a newly allocated opaque data structure containing the peers. This structure must be freed with OrthancPluginFreePeers().", + "summary": "Return the list of available Orthanc peers." + }, + "return_sdk_class": "OrthancPluginPeers", + "return_sdk_type": "object", + "since_sdk": [ + 1, + 4, + 2 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "path", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginAutodetectMimeType", + "documentation": { + "args": { + "path": "Path to the file." + }, + "description": [ + "This function returns the MIME type of a file by inspecting its extension." + ], + "return": "The MIME type. This is a statically-allocated string, do not free it.", + "summary": "Detect the MIME type of a file." + }, + "return_sdk_type": "const char *", + "since_sdk": [ + 1, + 5, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "name", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "value", + "sdk_type": "float" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginMetricsType", + "sdk_name": "type", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginSetMetricsValue", + "documentation": { + "args": { + "name": "The name of the metrics to be set.", + "type": "The type of the metrics. This parameter is only taken into consideration the first time this metrics is set.", + "value": "The value of the metrics." + }, + "description": [ + "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." + ], + "summary": "Set the value of a floating-point metrics." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 5, + 4 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "group", + "sdk_type": "uint16_t" + }, + { + "name": "arg1", + "sdk_name": "element", + "sdk_type": "uint16_t" + }, + { + "name": "arg2", + "sdk_name": "privateCreator", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginGetTagName", + "documentation": { + "args": { + "element": "The element of the tag.", + "group": "The group of the tag.", + "privateCreator": "For private tags, the name of the private creator (can be NULL)." + }, + "description": [ + "This function makes a lookup to the dictionary of DICOM tags that are known to Orthanc, and returns the symbolic name of a DICOM tag." + ], + "return": "NULL in the case of an error, or a newly allocated string containing the path. This string must be freed by OrthancPluginFreeString().", + "summary": "Returns the symbolic name of a DICOM tag." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 5, + 7 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "buffer", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginCreateDicomInstance", + "documentation": { + "args": { + "buffer": "The memory buffer containing the DICOM instance.", + "size": "The size of the memory buffer." + }, + "description": [ + "This function parses a memory buffer that contains a DICOM file. The function returns a new pointer to a data structure that is managed by the Orthanc core." + ], + "return": "The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().", + "summary": "Parse a DICOM instance." + }, + "return_sdk_class": "OrthancPluginDicomInstance", + "return_sdk_type": "object", + "since_sdk": [ + 1, + 7, + 0 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "buffer", + "sdk_type": "const_void_pointer_with_size" + }, + { + "name": "arg2", + "sdk_name": "transferSyntax", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginTranscodeDicomInstance", + "documentation": { + "args": { + "buffer": "The memory buffer containing the DICOM instance.", + "size": "The size of the memory buffer.", + "transferSyntax": "The transfer syntax UID for the transcoding." + }, + "description": [ + "This function parses a memory buffer that contains a DICOM file, then transcodes it to the given transfer syntax. The function returns a new pointer to a data structure that is managed by the Orthanc core." + ], + "return": "The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().", + "summary": "Parse and transcode a DICOM instance." + }, + "return_sdk_class": "OrthancPluginDicomInstance", + "return_sdk_type": "object", + "since_sdk": [ + 1, + 7, + 0 + ] + }, + { + "args": [], + "c_function": "OrthancPluginGenerateRestApiAuthorizationToken", + "documentation": { + "args": {}, + "description": [ + "This function generates a token that can be set in the HTTP header \"Authorization\" so as to grant full access to the REST API of Orthanc using an external HTTP client. Using this function avoids the need of adding a separate user in the \"RegisteredUsers\" configuration of Orthanc, which eases deployments.", + "This feature is notably useful in multiprocess scenarios, where a subprocess created by a plugin has no access to the \"OrthancPluginContext\", and thus cannot call \"OrthancPluginRestApi[Get|Post|Put|Delete]()\".", + "This situation is frequently encountered in Python plugins, where the \"multiprocessing\" package can be used to bypass the Global Interpreter Lock (GIL) and thus to improve performance and concurrency." + ], + "return": "The authorization token, or NULL value in the case of an error. This string must be freed by OrthancPluginFreeString().", + "summary": "Generate a token to grant full access to the REST API of Orthanc." + }, + "return_sdk_type": "char *", + "since_sdk": [ + 1, + 8, + 1 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "json", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_class": "OrthancPluginImage", + "sdk_name": "pixelData", + "sdk_type": "const_object" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginCreateDicomFlags", + "sdk_name": "flags", + "sdk_type": "enumeration" + }, + { + "name": "arg3", + "sdk_name": "privateCreator", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginCreateDicom2", + "documentation": { + "args": { + "flags": "Flags governing the output.", + "json": "The input JSON file.", + "pixelData": "The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme.", + "privateCreator": "The private creator to be used for the private DICOM tags. Check out the global configuration option \"Dictionary\" of Orthanc.", + "target": "The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "This function takes as input a string containing a JSON file describing the content of a DICOM instance. As an output, it writes the corresponding DICOM instance to a newly allocated memory buffer. Additionally, an image to be encoded within the DICOM instance can also be provided.", + "Contrarily to the function OrthancPluginCreateDicom(), this function can be explicitly provided with a private creator." + ], + "return": "0 if success, other value if error.", + "summary": "Create a DICOM instance from a JSON string and an image, with a private creator." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *", + "since_sdk": [ + 1, + 9, + 0 + ] + }, + { + "args": [], + "c_function": "OrthancPluginGetDatabaseServerIdentifier", + "documentation": { + "args": {}, + "description": [], + "return": "the database server identifier. This is a statically-allocated string, do not free it.", + "summary": "Gets the DatabaseServerIdentifier." + }, + "return_sdk_type": "const char *", + "since_sdk": [ + 1, + 11, + 1 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "instanceId", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_enumeration": "OrthancPluginLoadDicomInstanceMode", + "sdk_name": "mode", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginLoadDicomInstance", + "documentation": { + "args": { + "instanceId": "The Orthanc identifier of the DICOM instance of interest.", + "mode": "Flag specifying how to deal with pixel data." + }, + "description": [ + "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." + ], + "return": "The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().", + "summary": "Load a DICOM instance from the Orthanc server." + }, + "return_sdk_class": "OrthancPluginDicomInstance", + "return_sdk_type": "object", + "since_sdk": [ + 1, + 12, + 1 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "name", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "value", + "sdk_type": "int64_t" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginMetricsType", + "sdk_name": "type", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginSetMetricsIntegerValue", + "documentation": { + "args": { + "name": "The name of the metrics to be set.", + "type": "The type of the metrics. This parameter is only taken into consideration the first time this metrics is set.", + "value": "The value of the metrics." + }, + "description": [ + "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." + ], + "summary": "Set the value of an integer metrics." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 12, + 1 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "threadName", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginSetCurrentThreadName", + "documentation": { + "args": { + "threadName": "The name of the current thread. A thread name cannot be longer than 16 characters." + }, + "description": [ + "This function gives a name to the thread that is calling this function. This name is used in the Orthanc logs. This function must only be called from threads that the plugin has created itself." + ], + "return": "0 if success, other value if error.", + "summary": "Set the name of the current thread." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 12, + 2 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "message", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "plugin", + "sdk_type": "const char *" + }, + { + "name": "arg2", + "sdk_name": "file", + "sdk_type": "const char *" + }, + { + "name": "arg3", + "sdk_name": "line", + "sdk_type": "uint32_t" + }, + { + "name": "arg4", + "sdk_enumeration": "OrthancPluginLogCategory", + "sdk_name": "category", + "sdk_type": "enumeration" + }, + { + "name": "arg5", + "sdk_enumeration": "OrthancPluginLogLevel", + "sdk_name": "level", + "sdk_type": "enumeration" + } + ], + "c_function": "OrthancPluginLogMessage", + "documentation": { + "args": { + "category": "The category.", + "file": "The filename in the plugin code.", + "level": "The level of the message.", + "line": "The file line in the plugin code.", + "message": "The message to be logged.", + "plugin": "The plugin name." + }, + "description": [ + "Log a message using the Orthanc logging system." + ], + "summary": "Log a message." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 12, + 4 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "attachmentUuid", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginGetAttachmentCustomData", + "documentation": { + "args": { + "attachmentUuid": "The UUID of the attachment of interest.", + "customData": "Memory buffer where to store the retrieved value. It must be freed by the plugin by calling OrthancPluginFreeMemoryBuffer()." + }, + "description": [ + "If no custom data is associated with the attachment of interest, the target memory buffer is filled with the NULL value and a zero size." + ], + "return": "0 if success, other value if error.", + "summary": "Retrieve the custom data associated with an attachment in the Orthanc database." + }, + "return_sdk_type": "OrthancPluginMemoryBuffer *", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "attachmentUuid", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "customData", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginSetAttachmentCustomData", + "documentation": { + "args": { + "attachmentUuid": "The UUID of the attachment of interest.", + "customData": "The value to store.", + "customDataSize": "The size of the value to store." + }, + "description": [ + "This function is notably used in the \"orthanc-advanced-storage\" when the plugin moves an attachment." + ], + "return": "0 if success, other value if error.", + "summary": "Update the custom data associated with an attachment in the Orthanc database." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "storeId", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "key", + "sdk_type": "const char *" + }, + { + "name": "arg2", + "sdk_name": "value", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginStoreKeyValue", + "documentation": { + "args": { + "key": "The key of the value to store (note: storeId + key must be unique).", + "storeId": "A unique identifier identifying both the plugin and the key-value store.", + "value": "The value to store.", + "valueSize": "The length of the value to store." + }, + "description": [], + "return": "0 if success, other value if error.", + "summary": "Store a key-value pair in the Orthanc database." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "storeId", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "key", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginDeleteKeyValue", + "documentation": { + "args": { + "key": "The key of the value to store (note: storeId + key must be unique).", + "storeId": "A unique identifier identifying both the plugin and the key-value store." + }, + "description": [], + "return": "0 if success, other value if error.", + "summary": "Delete a key-value pair from the Orthanc database." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "storeId", + "sdk_type": "const char *" + } + ], + "c_function": "OrthancPluginCreateKeysValuesIterator", + "documentation": { + "args": { + "storeId": "A unique identifier identifying both the plugin and the key-value store." + }, + "description": [ + "The iterator loops over the keys according to the lexicographical order." + ], + "return": "The newly allocated iterator, or NULL in the case of an error. The iterator must be freed by calling OrthancPluginFreeKeysValuesIterator().", + "summary": "Create an iterator over the key-value pairs of a key-value store in the Orthanc database." + }, + "return_sdk_class": "OrthancPluginKeysValuesIterator", + "return_sdk_type": "object", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "queueId", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "value", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginEnqueueValue", + "documentation": { + "args": { + "queueId": "A unique identifier identifying both the plugin and the queue.", + "value": "The value to store.", + "valueSize": "The size of the value to store." + }, + "description": [], + "return": "0 if success, other value if error.", + "summary": "Append a value to the back of a queue." + }, + "return_sdk_enumeration": "OrthancPluginErrorCode", + "return_sdk_type": "enumeration", + "since_sdk": [ + 1, + 12, + 8 + ] + }, + { + "args": [ + { + "name": "arg0", + "sdk_name": "sourcePlugin", + "sdk_type": "const char *" + }, + { + "name": "arg1", + "sdk_name": "userId", + "sdk_type": "const char *" + }, + { + "name": "arg2", + "sdk_enumeration": "OrthancPluginResourceType", + "sdk_name": "resourceType", + "sdk_type": "enumeration" + }, + { + "name": "arg3", + "sdk_name": "resourceId", + "sdk_type": "const char *" + }, + { + "name": "arg4", + "sdk_name": "action", + "sdk_type": "const char *" + }, + { + "name": "arg5", + "sdk_name": "logData", + "sdk_type": "const_void_pointer_with_size" + } + ], + "c_function": "OrthancPluginEmitAuditLog", + "documentation": { + "args": { + "action": "The action that was performed on the resource.", + "logData": "A pointer to custom log data.", + "logDataSize": "The size of the custom log data.", + "resourceId": "The resource this audit log relates to.", + "resourceType": "The type of the resource this audit log relates to.", + "sourcePlugin": "The name of the source plugin, to properly interpret the content of \"action\" and \"logData\".", + "userId": "A string that uniquely identifies the user or entity that is executing the action on the resource." + }, + "description": [ + "Generate an audit log that will be broadcasted to all the plugins that have registered a callback handler using OrthancPluginRegisterAuditLogHandler(). If no plugin has registered such a callback, the audit log is ignored.", + "A typical handler would record the audit log in a database and/or relay the audit log to a message broker." + ], + "summary": "Generate an audit log to signal security-related events." + }, + "return_sdk_type": "void", + "since_sdk": [ + 1, + 12, + 9 + ] + } + ], + "unwrapped_functions": [ + "OrthancPluginRegisterRestCallback", + "OrthancPluginRegisterRestCallbackNoLock", + "OrthancPluginRegisterOnStoredInstanceCallback", + "OrthancPluginGetInstanceData", + "OrthancPluginRegisterStorageArea", + "OrthancPluginRegisterOnChangeCallback", + "OrthancPluginGetImageBuffer", + "OrthancPluginRestApiGet2", + "OrthancPluginRegisterWorklistCallback", + "OrthancPluginRegisterDecodeImageCallback", + "OrthancPluginCreateImageAccessor", + "OrthancPluginLookupDictionary", + "OrthancPluginSendMultipartItem2", + "OrthancPluginRegisterIncomingHttpRequestFilter", + "OrthancPluginHttpClient", + "OrthancPluginRegisterFindCallback", + "OrthancPluginGetFindQueryTag", + "OrthancPluginRegisterMoveCallback", + "OrthancPluginRegisterIncomingHttpRequestFilter2", + "OrthancPluginCallPeerApi", + "OrthancPluginCreateJob", + "OrthancPluginCreateJob2", + "OrthancPluginRegisterJobsUnserializer", + "OrthancPluginRegisterRefreshMetricsCallback", + "OrthancPluginEncodeDicomWebJson", + "OrthancPluginEncodeDicomWebXml", + "OrthancPluginEncodeDicomWebJson2", + "OrthancPluginEncodeDicomWebXml2", + "OrthancPluginChunkedHttpClient", + "OrthancPluginRegisterChunkedRestCallback", + "OrthancPluginRegisterStorageCommitmentScpCallback", + "OrthancPluginRegisterIncomingDicomInstanceFilter", + "OrthancPluginRegisterIncomingCStoreInstanceFilter", + "OrthancPluginRegisterReceivedInstanceCallback", + "OrthancPluginGetInstanceDicomWebJson", + "OrthancPluginGetInstanceDicomWebXml", + "OrthancPluginRegisterTranscoderCallback", + "OrthancPluginRegisterStorageArea2", + "OrthancPluginCallRestApi", + "OrthancPluginRegisterWebDavCollection", + "OrthancPluginRegisterStorageArea3", + "OrthancPluginRegisterDatabaseBackendV4", + "OrthancPluginAdoptDicomInstance", + "OrthancPluginGetKeyValue", + "OrthancPluginKeysValuesIteratorNext", + "OrthancPluginDequeueValue", + "OrthancPluginGetQueueSize", + "OrthancPluginSetStableStatus", + "OrthancPluginRegisterHttpAuthentication", + "OrthancPluginRegisterAuditLogHandler" + ] +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/CMakeLists.txt Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,88 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +cmake_minimum_required(VERSION 2.8...4.0) +cmake_policy(SET CMP0058 NEW) + +project(AdoptDicomInstance) + +SET(PLUGIN_NAME "sample-adopt" CACHE STRING "Name of the plugin") +SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin") + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) + +set(ENABLE_SQLITE OFF) +set(ENABLE_MODULE_IMAGES OFF) +set(ENABLE_MODULE_JOBS OFF) +set(ENABLE_MODULE_DICOM OFF) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) + +include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/WindowsResources.py + ${PLUGIN_VERSION} AdoptDicomInstance AdoptDicomInstance.dll "Sample Orthanc plugin illustrating how to adopt DICOM instances" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/AdoptDicomInstance.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/AdoptDicomInstance.rc) +endif() + +add_definitions( + -DHAS_ORTHANC_EXCEPTION=1 + -DPLUGIN_NAME="${PLUGIN_NAME}" + -DPLUGIN_VERSION="${PLUGIN_VERSION}" + -DORTHANC_ENABLE_LOGGING=1 + -DORTHANC_ENABLE_PLUGINS=1 + ) + +include_directories( + ${CMAKE_SOURCE_DIR}/../../Include/ + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/ + ) + +add_library(AdoptDicomInstance SHARED + ${ADDITIONAL_RESOURCES} + ${ORTHANC_CORE_SOURCES} + ${CMAKE_SOURCE_DIR}/Plugin.cpp + ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp + ) + +DefineSourceBasenameForTarget(AdoptDicomInstance) + +set_target_properties( + AdoptDicomInstance PROPERTIES + VERSION ${PLUGIN_VERSION} + SOVERSION ${PLUGIN_VERSION} + ) + +install( + TARGETS AdoptDicomInstance + DESTINATION . + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/Plugin.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,266 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../../../../OrthancFramework/Sources/Logging.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" +#include "../../../../OrthancFramework/Sources/Toolbox.h" +#include "../Common/OrthancPluginCppWrapper.h" + +#include <boost/filesystem.hpp> + + +static boost::filesystem::path storageDirectory_; + + +static boost::filesystem::path GetStorageDirectoryPath(const char* uuid) +{ + if (uuid == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + return (storageDirectory_ / std::string(uuid)); + } +} + + +#define CATCH_EXCEPTIONS(errorValue) \ + catch (Orthanc::OrthancException& e) \ + { \ + LOG(ERROR) << "Orthanc exception: " << e.What(); \ + return errorValue; \ + } \ + catch (std::runtime_error& e) \ + { \ + LOG(ERROR) << "Native exception: " << e.what(); \ + return errorValue; \ + } \ + catch (...) \ + { \ + return errorValue; \ + } + + +OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type, + OrthancPluginCompressionType compressionType, + const OrthancPluginDicomInstance* dicomInstance) +{ + try + { + Json::Value info; + info["IsAdopted"] = false; + + OrthancPlugins::MemoryBuffer buffer; + buffer.Assign(info.toStyledString()); + *customData = buffer.Release(); + + const boost::filesystem::path path = GetStorageDirectoryPath(uuid); + LOG(WARNING) << "Creating non-adopted file: " << path; + Orthanc::SystemToolbox::WriteFile(content, size, path, false /* no fsync */); + + return OrthancPluginErrorCode_Success; + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +OrthancPluginErrorCode StorageReadRange(OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + uint32_t customDataSize) +{ + try + { + Json::Value info; + if (!Orthanc::Toolbox::ReadJson(info, customData, customDataSize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + boost::filesystem::path path; + + if (info["IsAdopted"].asBool()) + { + path = info["AdoptedPath"].asString(); + LOG(WARNING) << "Reading adopted file from: " << path; + } + else + { + path = GetStorageDirectoryPath(uuid); + LOG(WARNING) << "Reading non-adopted file from: " << path; + } + + std::string range; + Orthanc::SystemToolbox::ReadFileRange(range, path, rangeStart, rangeStart + target->size, true); + + assert(range.size() == target->size); + + if (target->size != 0) + { + memcpy(target->data, range.c_str(), target->size); + } + + return OrthancPluginErrorCode_Success; + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +OrthancPluginErrorCode StorageRemove(const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint32_t customDataSize) +{ + try + { + Json::Value info; + if (!Orthanc::Toolbox::ReadJson(info, customData, customDataSize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + // Only remove non-adopted files (i.e., whose for which Orthanc has the ownership) + if (info["IsAdopted"].asBool()) + { + LOG(WARNING) << "Don't removing adopted file: " << info["AdoptedPath"].asString(); + } + else + { + const boost::filesystem::path path = GetStorageDirectoryPath(uuid); + LOG(WARNING) << "Removing non-adopted file from: " << path; + Orthanc::SystemToolbox::RemoveFile(path); + } + + return OrthancPluginErrorCode_Success; + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +OrthancPluginErrorCode Adopt(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + try + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + return OrthancPluginErrorCode_Success; + } + else + { + const std::string path(reinterpret_cast<const char*>(request->body), request->bodySize); + LOG(WARNING) << "Adopting DICOM instance from path: " << path; + + std::string dicom; + Orthanc::SystemToolbox::ReadFile(dicom, path); + + Json::Value info; + info["IsAdopted"] = true; + info["AdoptedPath"] = path; + + const std::string customData = info.toStyledString(); + + OrthancPluginStoreStatus status; + OrthancPlugins::MemoryBuffer instanceId, attachmentUuid; + + OrthancPluginErrorCode code = OrthancPluginAdoptDicomInstance( + OrthancPlugins::GetGlobalContext(), *instanceId, *attachmentUuid, &status, + dicom.empty() ? NULL : dicom.c_str(), dicom.size(), + customData.empty() ? NULL : customData.c_str(), customData.size()); + + if (code == OrthancPluginErrorCode_Success) + { + const std::string answer = "OK\n"; + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, answer.c_str(), answer.size(), "text/plain"); + return OrthancPluginErrorCode_Success; + } + else + { + return code; + } + } + } + CATCH_EXCEPTIONS(OrthancPluginErrorCode_Plugin); +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + OrthancPlugins::SetGlobalContext(context, PLUGIN_NAME); + Orthanc::Logging::InitializePluginContext(context, PLUGIN_NAME); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(context) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + context->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context, info); + return -1; + } + + OrthancPluginSetDescription2(context, PLUGIN_NAME, "Sample plugin illustrating the adoption of DICOM instances."); + OrthancPluginRegisterStorageArea3(context, StorageCreate, StorageReadRange, StorageRemove); + + try + { + OrthancPlugins::OrthancConfiguration config; + storageDirectory_ = config.GetStringValue("StorageDirectory", "OrthancStorage"); + + Orthanc::SystemToolbox::MakeDirectory(storageDirectory_); + + OrthancPluginRegisterRestCallback(context, "/adopt", Adopt); + } + CATCH_EXCEPTIONS(-1) + + return 0; + } + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return PLUGIN_NAME; + } + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return PLUGIN_VERSION; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/README Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,13 @@ +This sample plugin illustrates how to use the +"OrthancPluginAdoptDicomInstance()" primitive in the Orthanc SDK. + +The plugin replaces the built-in storage area of Orthanc, by a flat +directory "./OrthancStorage" containing the attachments. + +DICOM instances can then be adopted by typing: + +$ curl http://localhost:8042/adopt -d /tmp/sample.dcm + +An adopted DICOM instance is not copied inside the "OrthancStorage" +folder, but is read from its original location (in the example above, +from "/tmp/sample.dcm").
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c Tue Nov 04 15:58:06 2025 +0100 @@ -226,6 +226,44 @@ } + +OrthancPluginErrorCode CallbackStabilizeStudy(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(context, output, "POST"); + } + else + { + const char* studyId = request->groups[0]; + int32_t statusHasChanged = 0; + + if (strcmp(request->groups[1], "stabilize") == 0) + { + OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Stable); + } + else + { + OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Unstable); + } + + if (statusHasChanged) + { + OrthancPluginAnswerBuffer(context, output, "CHANGED\n", 8, "text/plain"); + } + else + { + OrthancPluginAnswerBuffer(context, output, "UNCHANGED\n", 10, "text/plain"); + } + } + + return OrthancPluginErrorCode_Success; +} + + OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request) @@ -572,6 +610,7 @@ OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5); OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom); OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/dicom-web", CallbackDicomWeb); + OrthancPluginRegisterRestCallback(context, "/studies/([^/]+)/(stabilize|unstabilize)", CallbackStabilizeStudy); OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback); OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -23,6 +23,7 @@ #include "OrthancPluginCppWrapper.h" +#include <cassert> #include <boost/algorithm/string/predicate.hpp> #include <boost/move/unique_ptr.hpp> #include <boost/thread.hpp> @@ -220,28 +221,6 @@ } -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) - MemoryBuffer::MemoryBuffer(const void* buffer, - size_t size) - { - uint32_t s = static_cast<uint32_t>(size); - if (static_cast<size_t>(s) != size) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); - } - else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != - OrthancPluginErrorCode_Success) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); - } - else - { - memcpy(buffer_.data, buffer, size); - } - } -#endif - - void MemoryBuffer::Clear() { if (buffer_.data != NULL) @@ -253,6 +232,51 @@ } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::Assign(const void* buffer, + size_t size) + { + uint32_t s = static_cast<uint32_t>(size); + if (static_cast<size_t>(s) != size) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + Clear(); + + if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != + OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else + { + if (size > 0) + { + memcpy(buffer_.data, buffer, size); + } + } + } +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::Assign(const std::string& s) + { + Assign(s.empty() ? NULL : s.c_str(), s.size()); + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void MemoryBuffer::AssignJson(const Json::Value& value) + { + std::string s; + WriteFastJson(s, value); + Assign(s); + } +#endif + + void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other) { Clear(); @@ -327,6 +351,34 @@ } } + static void DecodeHttpHeaders(HttpHeaders& target, + const MemoryBuffer& source) + { + Json::Value v; + source.ToJson(v); + + if (v.type() != Json::objectValue) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + Json::Value::Members members = v.getMemberNames(); + target.clear(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& h = v[members[i]]; + if (h.type() != Json::stringValue) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + target[members[i]] = h.asString(); + } + } + } + // helper class to convert std::map of headers to the plugin SDK C structure class PluginHttpHeaders { @@ -673,7 +725,7 @@ { OrthancString str; str.Assign(OrthancPluginDicomBufferToJson - (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength)); + (GetGlobalContext(), reinterpret_cast<const char*>(GetData()), GetSize(), format, flags, maxStringLength)); str.ToJson(target); } @@ -1566,7 +1618,7 @@ { if (!answer.IsEmpty()) { - result.assign(answer.GetData(), answer.GetSize()); + result.assign(reinterpret_cast<const char*>(answer.GetData()), answer.GetSize()); } return true; } @@ -2052,6 +2104,48 @@ DoPost(target, index, uri, body, headers)); } + bool OrthancPeers::DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { + MemoryBuffer buffer; + HttpHeaders answerHeaders; + + if (DoPost(buffer, answerHeaders, index, uri, body, headers, timeout)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + + bool OrthancPeers::DoPost(Json::Value& target, + HttpHeaders& answerHeaders, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { + MemoryBuffer buffer; + + if (DoPost(buffer, answerHeaders, index, uri, body, headers, timeout)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + bool OrthancPeers::DoPost(Json::Value& target, size_t index, @@ -2099,6 +2193,29 @@ const std::string& body, const HttpHeaders& headers) const { + HttpHeaders answerHeaders; + return DoPost(target, answerHeaders, index, uri, body, headers, timeout_); + } + + bool OrthancPeers::DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { + HttpHeaders answerHeaders; + return DoPost(target, answerHeaders, index, uri, body, headers, timeout); + } + + bool OrthancPeers::DoPost(MemoryBuffer& target, + HttpHeaders& answerHeaders, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const + { if (index >= index_.size()) { ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); @@ -2111,17 +2228,20 @@ } OrthancPlugins::MemoryBuffer answer; + OrthancPlugins::MemoryBuffer answerHeadersBuffer; uint16_t status; PluginHttpHeaders pluginHeaders(headers); OrthancPluginErrorCode code = OrthancPluginCallPeerApi - (GetGlobalContext(), *answer, NULL, &status, peers_, + (GetGlobalContext(), *answer, *answerHeadersBuffer, &status, peers_, static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(), - pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_); + pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout); if (code == OrthancPluginErrorCode_Success) { target.Swap(answer); + DecodeHttpHeaders(answerHeaders, answerHeadersBuffer); + return (status == 200); } else @@ -3168,36 +3288,6 @@ } #endif - - static void DecodeHttpHeaders(HttpHeaders& target, - const MemoryBuffer& source) - { - Json::Value v; - source.ToJson(v); - - if (v.type() != Json::objectValue) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); - } - - Json::Value::Members members = v.getMemberNames(); - target.clear(); - - for (size_t i = 0; i < members.size(); i++) - { - const Json::Value& h = v[members[i]]; - if (h.type() != Json::stringValue) - { - ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); - } - else - { - target[members[i]] = h.asString(); - } - } - } - - void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus, HttpHeaders& answerHeaders, std::string& answerBody, @@ -4068,6 +4158,16 @@ } #endif + void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request) + { + result.clear(); + + for (uint32_t i = 0; i < request->getCount; ++i) + { + result[request->getKeys[i]] = request->getValues[i]; + } + } + void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request) { result.clear(); @@ -4168,6 +4268,11 @@ { path_ += "?" + getArguments; } + + if (request->bodySize > 0 && request->body != NULL) + { + requestBody_.assign(reinterpret_cast<const char*>(request->body), request->bodySize); + } } #endif @@ -4189,6 +4294,15 @@ #if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 + void RestApiClient::SetRequestHeader(const std::string& key, + const std::string& value) + { + requestHeaders_[key] = value; + } +#endif + + +#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 bool RestApiClient::Execute() { if (requestBody_.size() > 0xffffffffu) @@ -4235,9 +4349,17 @@ } } - void RestApiClient::Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output) - { - if (Execute() && httpStatus_ == 200) + void RestApiClient::ExecuteAndForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output) + { + if (Execute()) + { + ForwardAnswer(context, output); + } + } + + void RestApiClient::ForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output) + { + if (httpStatus_ == 200) { const char* mimeType = NULL; for (HttpHeaders::const_iterator h = answerHeaders_.begin(); h != answerHeaders_.end(); ++h) @@ -4316,4 +4438,209 @@ } } #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator *iterator) : + iterator_(iterator) + { + if (iterator_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator::~Iterator() + { + OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::Iterator::Next() + { + uint8_t done; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(OrthancPlugins::GetGlobalContext(), &done, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return (done != 0); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + std::string KeyValueStore::Iterator::GetKey() const + { + const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + return s; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Iterator::GetValue(std::string& value) const + { + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorGetValue(OrthancPlugins::GetGlobalContext(), *valueBuffer, iterator_); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + valueBuffer.ToString(value); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::Store(const std::string& key, + const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginStoreKeyValue(OrthancPlugins::GetGlobalContext(), storeId_.c_str(), + key.c_str(), value, static_cast<uint32_t>(valueSize)); + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + bool KeyValueStore::GetValue(std::string& value, + const std::string& key) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + OrthancPluginErrorCode code = OrthancPluginGetKeyValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + void KeyValueStore::DeleteKey(const std::string& key) + { + OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(), + storeId_.c_str(), key.c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + KeyValueStore::Iterator* KeyValueStore::CreateIterator() + { + return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str())); + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + void Queue::Enqueue(const void* value, + size_t valueSize) + { + if (static_cast<size_t>(static_cast<uint32_t>(valueSize)) != valueSize) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + OrthancPluginErrorCode code = OrthancPluginEnqueueValue(OrthancPlugins::GetGlobalContext(), + queueId_.c_str(), value, static_cast<uint32_t>(valueSize)); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + bool Queue::DequeueInternal(std::string& value, + OrthancPluginQueueOrigin origin) + { + uint8_t found = false; + OrthancPlugins::MemoryBuffer valueBuffer; + + OrthancPluginErrorCode code = OrthancPluginDequeueValue(OrthancPlugins::GetGlobalContext(), &found, + *valueBuffer, queueId_.c_str(), origin); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else if (found) + { + valueBuffer.ToString(value); + return true; + } + else + { + return false; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + uint64_t Queue::GetSize() + { + uint64_t size = 0; + OrthancPluginErrorCode code = OrthancPluginGetQueueSize(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), &size); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + else + { + return size; + } + } +#endif }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Tue Nov 04 15:58:06 2025 +0100 @@ -134,6 +134,14 @@ # define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 0 #endif +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 1 +# define HAS_ORTHANC_PLUGIN_QUEUES 1 +#else +# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 0 +# define HAS_ORTHANC_PLUGIN_QUEUES 0 +#endif + // Macro to tag a function as having been deprecated #if (__cplusplus >= 201402L) // C++14 @@ -172,6 +180,8 @@ { typedef std::map<std::string, std::string> HttpHeaders; + typedef std::map<std::string, std::string> GetArguments; + typedef void (*RestCallback) (OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request); @@ -203,13 +213,6 @@ public: MemoryBuffer(); -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) - // This constructor makes a copy of the given buffer in the memory - // handled by the Orthanc core - MemoryBuffer(const void* buffer, - size_t size); -#endif - ~MemoryBuffer() { Clear(); @@ -220,6 +223,20 @@ return &buffer_; } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + // Copy of the given buffer into the memory managed by the Orthanc core + void Assign(const void* buffer, + size_t size); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void Assign(const std::string& s); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void AssignJson(const Json::Value& value); +#endif + // This transfers ownership from "other" to "this" void Assign(OrthancPluginMemoryBuffer& other); @@ -227,11 +244,11 @@ OrthancPluginMemoryBuffer Release(); - const char* GetData() const + const void* GetData() const { if (buffer_.size > 0) { - return reinterpret_cast<const char*>(buffer_.data); + return buffer_.data; } else { @@ -855,6 +872,21 @@ const HttpHeaders& headers) const; bool DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(MemoryBuffer& target, + HttpHeaders& answerHeaders, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(MemoryBuffer& target, const std::string& name, const std::string& uri, const std::string& body, @@ -867,6 +899,21 @@ const HttpHeaders& headers) const; bool DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(Json::Value& target, + HttpHeaders& answerHeaders, + size_t index, + const std::string& uri, + const std::string& body, + const HttpHeaders& headers, + unsigned int timeout) const; + + bool DoPost(Json::Value& target, const std::string& name, const std::string& uri, const std::string& body, @@ -1402,6 +1449,9 @@ // helper method to re-serialize the get arguments from the SDK into a string void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request); +// helper method to convert Get arguments from the plugin SDK to a std::map +void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request); + #if HAS_ORTHANC_PLUGIN_WEBDAV == 1 class IWebDavCollection : public boost::noncopyable { @@ -1559,6 +1609,9 @@ void AddRequestHeader(const std::string& key, const std::string& value); + void SetRequestHeader(const std::string& key, + const std::string& value); + const HttpHeaders& GetRequestHeaders() const { return requestHeaders_; @@ -1589,10 +1642,14 @@ return requestBody_; } + // Execute only bool Execute(); + // Forward response as is + void ForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output); + // Execute and forward the response as is - void Forward(OrthancPluginContext* context, OrthancPluginRestOutput* output); + void ExecuteAndForwardAnswer(OrthancPluginContext* context, OrthancPluginRestOutput* output); uint16_t GetHttpStatus() const; @@ -1604,4 +1661,101 @@ bool GetAnswerJson(Json::Value& output) const; }; #endif + + +#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 + class KeyValueStore : public boost::noncopyable + { + public: + class Iterator : public boost::noncopyable + { + private: + OrthancPluginKeysValuesIterator *iterator_; + + public: + explicit Iterator(OrthancPluginKeysValuesIterator *iterator); + + ~Iterator(); + + bool Next(); + + std::string GetKey() const; + + void GetValue(std::string& target) const; + }; + + private: + std::string storeId_; + + public: + explicit KeyValueStore(const std::string& storeId) : + storeId_(storeId) + { + } + + const std::string& GetStoreId() const + { + return storeId_; + } + + void Store(const std::string& key, + const void* value, + size_t valueSize); + + void Store(const std::string& key, + const std::string& value) + { + Store(key, value.empty() ? NULL : value.c_str(), value.size()); + } + + bool GetValue(std::string& value, + const std::string& key); + + void DeleteKey(const std::string& key); + + Iterator* CreateIterator(); + }; +#endif + + +#if HAS_ORTHANC_PLUGIN_QUEUES == 1 + class Queue : public boost::noncopyable + { + private: + std::string queueId_; + + bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin); + + public: + explicit Queue(const std::string& queueId) : + queueId_(queueId) + { + } + + const std::string& GetQueueId() const + { + return queueId_; + } + + void Enqueue(const void* value, + size_t valueSize); + + void Enqueue(const std::string& value) + { + Enqueue(value.empty() ? NULL : value.c_str(), value.size()); + } + + bool DequeueBack(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Back); + } + + bool DequeueFront(std::string& value) + { + return DequeueInternal(value, OrthancPluginQueueOrigin_Front); + } + + uint64_t GetSize(); + }; +#endif }
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/OrthancFrameworkDependencies.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -27,7 +27,24 @@ * dictionary. **/ -#define ORTHANC_ENABLE_ICU 0 +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + #include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" #include "../../../../OrthancFramework/Sources/Enumerations.cpp"
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -44,8 +44,9 @@ return OrthancPluginErrorCode_Success; } - std::string path = "/" + std::string(request->groups[0]); - std::string mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path)); + const std::string path = "/" + std::string(request->groups[0]); + std::string mime = Orthanc::EnumerationToString( + Orthanc::SystemToolbox::AutodetectMimeType(Orthanc::SystemToolbox::PathFromUtf8(path))); try {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/CppSkeleton/CMakeLists.txt Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,200 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +cmake_minimum_required(VERSION 2.8) + +project(OrthancSkeleton) + +set(ORTHANC_PLUGIN_VERSION "mainline") +set(ORTHANC_PLUGIN_NAME "skeleton") + +if (ORTHANC_PLUGIN_VERSION STREQUAL "mainline") + set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") +else() + set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.7") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") +endif() + + +# Parameters of the build +set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") +set(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") +set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc framework (can be \"system\", \"hg\", \"archive\", \"web\" or \"path\")") +set(ORTHANC_FRAMEWORK_VERSION "${ORTHANC_FRAMEWORK_DEFAULT_VERSION}" CACHE STRING "Version of the Orthanc framework") +set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"") +set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"") + + +# Advanced parameters to fine-tune linking against system libraries +set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK") +set(ORTHANC_FRAMEWORK_STATIC OFF CACHE BOOL "If linking against the Orthanc framework system library, indicates whether this library was statically linked") +mark_as_advanced(ORTHANC_FRAMEWORK_STATIC) + + +# Download and setup the Orthanc framework +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path") + include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/DownloadOrthancFramework.cmake) +else() + include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake) +endif() + + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system") + if (ORTHANC_FRAMEWORK_USE_SHARED) + include(FindBoost) + find_package(Boost COMPONENTS filesystem regex thread) + + if (NOT Boost_FOUND) + message(FATAL_ERROR "Unable to locate Boost on this system") + endif() + + link_libraries(${Boost_LIBRARIES} jsoncpp) + endif() + + link_libraries(${ORTHANC_FRAMEWORK_LIBRARIES}) +else() + include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake) + + set(ENABLE_LOCALE OFF) # Disable support for locales (notably in Boost) + set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "") + set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "") + + include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkConfiguration.cmake) + include_directories(${ORTHANC_FRAMEWORK_ROOT}) +endif() + + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path") + include(${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake) + include_directories(${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Samples/Common/) + + set(ORTHANC_PLUGINS_SOURCES + ${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp + ) +else() + include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake) + include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/) + + set(ORTHANC_PLUGINS_SOURCES + ${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp + ) +endif() + + +# Check that the Orthanc SDK headers are available +if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK) + if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path") + include_directories(${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Include) + else() + include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-1.0.0) + endif() +else () + CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H) + if (NOT HAVE_ORTHANC_H) + message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK") + endif() +endif() + + +if (STANDALONE_BUILD) + add_definitions( + -DORTHANC_STANDALONE=1 + ) + set(EMBEDDED_RESOURCES + PLUGIN_RESOURCES ${CMAKE_SOURCE_DIR}/EmbeddedResources/ + ) +else() + add_definitions( + -DORTHANC_STANDALONE=0 + -DPLUGIN_RESOURCES_PATH="${CMAKE_SOURCE_DIR}/EmbeddedResources/" + ) +endif() + + +message("Setting the version of the library to ${ORTHANC_PLUGIN_VERSION}") + +add_definitions( + -DHAS_ORTHANC_EXCEPTION=1 + -DORTHANC_PLUGIN_NAME="${ORTHANC_PLUGIN_NAME}" + -DORTHANC_PLUGIN_VERSION="${ORTHANC_PLUGIN_VERSION}" + ) + +EmbedResources( + ${EMBEDDED_RESOURCES} + ) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + link_libraries(rt) +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lws2_32") + + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_FRAMEWORK_ROOT}/../Resources/WindowsResources.py + ${ORTHANC_PLUGIN_VERSION} "OrthancSkeleton" OrthancSkeleton.dll "Skeleton plugin for Orthanc" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/Version.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND AUTOGENERATED_SOURCES ${AUTOGENERATED_DIR}/Version.rc) +endif() + + +add_custom_target( + AutogeneratedTarget + DEPENDS + ${AUTOGENERATED_SOURCES} + ) + +add_library(OrthancSkeleton + SHARED + ${AUTOGENERATED_SOURCES} + ${ORTHANC_CORE_SOURCES} + ${ORTHANC_PLUGINS_SOURCES} + + # Your source files + ${CMAKE_SOURCE_DIR}/Plugin.cpp + ) + +add_dependencies(OrthancSkeleton AutogeneratedTarget) + + +set_target_properties(OrthancSkeleton PROPERTIES + VERSION ${ORTHANC_PLUGIN_VERSION} + SOVERSION ${ORTHANC_PLUGIN_VERSION}) + +install( + TARGETS OrthancSkeleton + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) + +if (COMMAND DefineSourceBasenameForTarget) + DefineSourceBasenameForTarget(OrthancSkeleton) +endif()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/CppSkeleton/EmbeddedResources/app.js Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,1 @@ +console.log('Hello, world!');
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/CppSkeleton/NOTES.txt Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,43 @@ + +Content +======= + +This folder contains a sample, minimal C++ plugin for Orthanc that +supports multiple target architectures. This skeleton also illustrates +how to properly include the Orthanc Framework and how to use embedded +resources. + + +Build instructions +================== + +To compile for developers: + +$ mkdir Build +$ cd Build +$ cmake .. \ + -DORTHANC_FRAMEWORK_ROOT=${PWD}/../../../../../OrthancFramework/Sources \ + -DORTHANC_FRAMEWORK_SOURCE=path \ + -DUSE_SYSTEM_ORTHANC_SDK=OFF +$ make -j4 + + +Shipping to users +================= + +If shipping the plugin to users (i.e., non-developers), you would have +to prepare your project by typing: + +$ ./Resources/SyncOrthancFolder.py + +The resulting content of the "./Resources/Orthanc/" must be included +in your project. + +Evidently, do not forget to adapt the values in "CMakeLists.txt" + +The users would then use the following build instructions: + +$ mkdir Build +$ cd Build +$ cmake .. -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_ORTHANC_SDK=OFF +$ make -j4
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/CppSkeleton/Plugin.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,121 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include <Logging.h> +#include <OrthancPluginCppWrapper.h> + +#if ORTHANC_STANDALONE == 1 +# include <EmbeddedResources.h> +#else +# include <SystemToolbox.h> +#endif + + +static void GetEmbeddedResource(std::string& target, + const std::string& path) +{ +#if ORTHANC_STANDALONE == 0 + Orthanc::SystemToolbox::ReadFile(target, Orthanc::SystemToolbox::InterpretRelativePath(PLUGIN_RESOURCES_PATH, path)); +#else + const std::string s = "/" + path; + Orthanc::EmbeddedResources::GetDirectoryResource(target, Orthanc::EmbeddedResources::PLUGIN_RESOURCES, s.c_str()); +#endif +} + + +static bool DisplayPerformanceWarning() +{ + (void) DisplayPerformanceWarning; // Disable warning about unused function + LOG(WARNING) << "Performance warning in plugin: " + << "Non-release build, runtime debug assertions are turned on"; + return true; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + OrthancPlugins::SetGlobalContext(context, ORTHANC_PLUGIN_NAME); + +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4) + Orthanc::Logging::InitializePluginContext(context, ORTHANC_PLUGIN_NAME); +#elif ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2) + Orthanc::Logging::InitializePluginContext(context); +#else + Orthanc::Logging::Initialize(context); +#endif + + assert(DisplayPerformanceWarning()); + + Orthanc::Logging::EnableInfoLevel(true); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(context) == 0) + { + char info[1024]; + sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", + context->orthancVersion, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + OrthancPluginLogError(context, info); + return -1; + } + + OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Skeleton of a C++ plugin Orthanc."); + + try + { + std::string s; + GetEmbeddedResource(s, "app.js"); + LOG(WARNING) << s; + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception while initializing the plugin: " << e.What(); + return -1; + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + LOG(WARNING) << "Finalizing the skeleton plugin"; + Orthanc::Logging::Finalize(); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return ORTHANC_PLUGIN_NAME; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return ORTHANC_PLUGIN_VERSION; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/CppSkeleton/Resources/SyncOrthancFolder.py Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,103 @@ +#!/usr/bin/python3 + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +# +# This maintenance script updates the content of the "Orthanc" folder +# to match the latest version of the Orthanc source code. +# + +import multiprocessing +import os +import stat +import urllib.request + +TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc') +PLUGIN_SDK_VERSION = '1.0.0' +REPOSITORY = 'https://orthanc.uclouvain.be/hg/%s/raw-file' + +FILES = [ + ('orthanc', 'OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake', 'CMake'), + ('orthanc', 'OrthancFramework/Resources/CMake/Compiler.cmake', 'CMake'), + ('orthanc', 'OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake', 'CMake'), + ('orthanc', 'OrthancFramework/Resources/CMake/DownloadPackage.cmake', 'CMake'), + ('orthanc', 'OrthancFramework/Resources/EmbedResources.py', 'CMake'), + + ('orthanc', 'OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake', 'Toolchains'), + ('orthanc', 'OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake', 'Toolchains'), + ('orthanc', 'OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake', 'Toolchains'), + ('orthanc', 'OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake', 'Toolchains'), + + ('orthanc', 'OrthancServer/Plugins/Samples/Common/ExportedSymbolsPlugins.list', 'Plugins'), + ('orthanc', 'OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp', 'Plugins'), + ('orthanc', 'OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h', 'Plugins'), + ('orthanc', 'OrthancServer/Plugins/Samples/Common/OrthancPluginException.h', 'Plugins'), + ('orthanc', 'OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake', 'Plugins'), + ('orthanc', 'OrthancServer/Plugins/Samples/Common/VersionScriptPlugins.map', 'Plugins'), +] + +SDK = [ + 'orthanc/OrthancCPlugin.h', +] + + +def Download(x): + repository = x[0] + branch = x[1] + source = x[2] + target = os.path.join(TARGET, x[3]) + print(target) + + try: + os.makedirs(os.path.dirname(target)) + except: + pass + + url = '%s/%s/%s' % (REPOSITORY % repository, branch, source) + + with open(target, 'wb') as f: + try: + f.write(urllib.request.urlopen(url).read()) + except: + print('ERROR: %s' % url) + raise + + +commands = [] + +for f in FILES: + commands.append([ f[0], + 'default', + f[1], + os.path.join(f[2], os.path.basename(f[1])) ]) + +for f in SDK: + commands.append([ + 'orthanc', + 'Orthanc-%s' % PLUGIN_SDK_VERSION, + 'Plugins/Include/%s' % f, + 'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f) + ]) + + +pool = multiprocessing.Pool(10) # simultaneous downloads +pool.map(Download, commands)
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/OrthancFrameworkDependencies.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -27,7 +27,24 @@ * dictionary. **/ -#define ORTHANC_ENABLE_ICU 0 +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + #include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" #include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp"
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -42,6 +42,12 @@ if (!db_.DoesTableExist("Pending")) { db_.Execute("CREATE TABLE Pending(uuid TEXT, type INTEGER)"); + + // New in v 1.12.7+ + // add an index on uuid to speed up the DELETE FROM Pending WHERE uuid=? + // With this patch, we observed a 100 fold performance + // improvement when the Pending table contains 1-2 millions files. + db_.Execute("CREATE INDEX PendingIndex ON Pending(uuid)"); } t.Commit();
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -26,6 +26,7 @@ #include "../../../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" #include "../../../../OrthancFramework/Sources/Logging.h" #include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" #include "../Common/OrthancPluginCppWrapper.h" #include <boost/thread.hpp> @@ -113,7 +114,7 @@ { try { - std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->Read(uuid, Convert(type))); + std::unique_ptr<Orthanc::IMemoryBuffer> buffer(storage_->ReadWhole(uuid, Convert(type))); // copy from a buffer allocated on plugin's heap into a buffer allocated on core's heap if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, buffer->GetSize()) != OrthancPluginErrorCode_Success) @@ -329,9 +330,9 @@ std::string pathStorage = orthancConfig.GetStringValue("StorageDirectory", "OrthancStorage"); LOG(WARNING) << "DelayedDeletion - Path to the storage area: " << pathStorage; - storage_.reset(new Orthanc::FilesystemStorage(pathStorage)); + storage_.reset(new Orthanc::FilesystemStorage(Orthanc::SystemToolbox::PathFromUtf8(pathStorage))); - boost::filesystem::path defaultDbPath = boost::filesystem::path(pathStorage) / (std::string("pending-deletions.") + databaseServerIdentifier_ + ".db"); + boost::filesystem::path defaultDbPath = Orthanc::SystemToolbox::PathFromUtf8(pathStorage) / (std::string("pending-deletions.") + databaseServerIdentifier_ + ".db"); std::string dbPath = delayedDeletionConfig.GetStringValue("Path", defaultDbPath.string()); LOG(WARNING) << "DelayedDeletion - Path to the SQLite database: " << dbPath;
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -41,6 +41,7 @@ static int globalPropertyId_ = 0; static bool force_ = false; +static bool forceReconstructFiles_ = false; static unsigned int throttleDelay_ = 0; static std::unique_ptr<boost::thread> workerThread_; static bool workerThreadShouldStop_ = false; @@ -568,10 +569,10 @@ { Json::Value result; - if (needsReconstruct || needsReingest ||force_) + if (needsReconstruct || needsReingest || force_) { Json::Value request; - if (needsReingest) + if (needsReingest || forceReconstructFiles_) { request["ReconstructFiles"] = true; } @@ -651,6 +652,13 @@ needsFullProcessing = needsReconstruct || needsReingest || needsDicomWebCaching; needsProcessing = needsFullProcessing; + if (needsFullProcessing && !limitToChange_.empty()) + { + ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: full processing needed -> ignoring the \"LimitMainDicomTagsReconstructLevel\" configuration"); + limitToChange_.clear(); + limitToUrl_.clear(); + } + // if a processing was in progress, check if the config has changed since if (pluginStatus_.currentlyProcessingConfiguration.IsDefined()) { @@ -679,7 +687,7 @@ } else { - ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !"); + ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the Orthanc configuration has changed since last run, will reprocess the whole DB !"); } Json::Value changes; @@ -695,7 +703,7 @@ } else { - ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the DB configuration has not changed since last run, will continue processing changes"); + ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the Orthanc configuration has not changed since last run, will continue processing changes"); } bool completed = false; @@ -849,6 +857,13 @@ // any changes in configuration "Force": false, + // New in 1.12.9 + // If "Force" is set to true, forces the "ReconstructFiles" + // option when reconstructing resources even if the plugin + // did not detect any changes in the configuration that + // should trigger a Reconstruct. + "ForceReconstructFiles": false, + // Delay (in seconds) between reconstruction of 2 studies // This avoids overloading Orthanc with the housekeeping // process and leaves room for other operations. @@ -891,6 +906,7 @@ globalPropertyId_ = housekeeper.GetIntegerValue("GlobalPropertyId", 1025); force_ = housekeeper.GetBooleanValue("Force", false); + forceReconstructFiles_ = housekeeper.GetBooleanValue("ForceReconstructFiles", false); throttleDelay_ = housekeeper.GetUnsignedIntegerValue("ThrottleDelay", 5); if (housekeeper.GetJson().isMember("Triggers"))
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt Tue Nov 04 15:58:06 2025 +0100 @@ -27,22 +27,31 @@ SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") +include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) -include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake) +set(ENABLE_JPEG ON CACHE INTERNAL "") +set(ENABLE_PNG ON CACHE INTERNAL "") +set(ENABLE_ZLIB ON CACHE INTERNAL "") +set(ENABLE_LOCALE ON CACHE INTERNAL "") +set(ENABLE_DCMTK ON CACHE INTERNAL "") +set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "") +set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "") + +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) add_library(ModalityWorklists SHARED Plugin.cpp ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp - ${JSONCPP_SOURCES} - ${BOOST_SOURCES} + ${ORTHANC_CORE_SOURCES} + ${ORTHANC_DICOM_SOURCES} + ${AUTOGENERATED_SOURCES} ) DefineSourceBasenameForTarget(ModalityWorklists) +target_link_libraries(ModalityWorklists ${DCMTK_LIBRARIES}) + message("Setting the version of the plugin to ${MODALITY_WORKLISTS_VERSION}") add_definitions( -DMODALITY_WORKLISTS_VERSION="${MODALITY_WORKLISTS_VERSION}"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/OrthancFrameworkDependencies.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,102 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * Remove the dependency upon ICU in plugins, as this greatly increase + * the size of the resulting binaries, since they must embed the ICU + * dictionary. + **/ + +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + +#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" +#include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp" +#include "../../../../OrthancFramework/Sources/Compression/GzipCompressor.cpp" +#include "../../../../OrthancFramework/Sources/Compression/ZlibCompressor.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomElement.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/Window.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp" +#include "../../../../OrthancFramework/Sources/Enumerations.cpp" +#include "../../../../OrthancFramework/Sources/HttpServer/HttpOutput.cpp" +#include "../../../../OrthancFramework/Sources/Images/IImageWriter.cpp" +#include "../../../../OrthancFramework/Sources/Images/Image.cpp" +#include "../../../../OrthancFramework/Sources/Images/ImageAccessor.cpp" +#include "../../../../OrthancFramework/Sources/Images/ImageBuffer.cpp" +#include "../../../../OrthancFramework/Sources/Images/ImageProcessing.cpp" +#include "../../../../OrthancFramework/Sources/Images/JpegErrorManager.cpp" +#include "../../../../OrthancFramework/Sources/Images/JpegReader.cpp" +#include "../../../../OrthancFramework/Sources/Images/JpegWriter.cpp" +#include "../../../../OrthancFramework/Sources/Images/PamReader.cpp" +#include "../../../../OrthancFramework/Sources/Images/PamWriter.cpp" +#include "../../../../OrthancFramework/Sources/Images/PngReader.cpp" +#include "../../../../OrthancFramework/Sources/Images/PngWriter.cpp" +#include "../../../../OrthancFramework/Sources/Logging.cpp" +#include "../../../../OrthancFramework/Sources/MetricsRegistry.cpp" +#include "../../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp" +#include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp" +#include "../../../../OrthancFramework/Sources/OrthancException.cpp" +#include "../../../../OrthancFramework/Sources/OrthancFramework.cpp" +#include "../../../../OrthancFramework/Sources/RestApi/RestApiOutput.cpp" +#include "../../../../OrthancFramework/Sources/SerializationToolbox.cpp" +#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp" +#include "../../../../OrthancFramework/Sources/TemporaryFile.cpp" +#include "../../../../OrthancFramework/Sources/Toolbox.cpp" + +namespace Orthanc +{ + void HttpClient::GlobalInitialize() + { + } + + void HttpClient::GlobalFinalize() + { + } +}
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -24,17 +24,85 @@ #define MODALITY_WORKLISTS_NAME "worklists" #include "../../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../../OrthancFramework/Sources/Logging.h" +#include "../../../../OrthancFramework/Sources/Toolbox.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" +#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.h" +#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h" #include "../Common/OrthancPluginCppWrapper.h" +#include <boost/thread.hpp> #include <boost/filesystem.hpp> #include <json/value.h> #include <string.h> #include <iostream> #include <algorithm> -static std::string folder_; +static boost::filesystem::path worklistDirectory_; static bool filterIssuerAet_ = false; static unsigned int limitAnswers_ = 0; +static std::unique_ptr<boost::thread> worklistHousekeeperThread_; +static bool worklistHousekeeperThreadShouldStop_ = false; +static bool deleteWorklistsOnStableStudy_ = true; +static unsigned int hkIntervalInSeconds_ = 60; +static unsigned int deleteDelayInHours_ = 24; +static std::unique_ptr<OrthancPlugins::KeyValueStore> worklistsStore_; +static bool setStudyInstanceUidIfMissing_ = true; + +enum WorklistStorageType +{ + WorklistStorageType_Folder = 1, + WorklistStorageType_OrthancDb = 2 +}; + +static WorklistStorageType worklistStorage_ = WorklistStorageType_Folder; + +struct Worklist +{ + std::string id_; + std::string createdAt_; + std::string dicomContent_; + + Worklist(const std::string& id, + const std::string& dicomContent) : + id_(id), + createdAt_(Orthanc::SystemToolbox::GetNowIsoString(true)), + dicomContent_(dicomContent) + { + } + + + Worklist(const std::string& id, const Json::Value& jsonWl) : + id_(id) + { + createdAt_ = jsonWl["CreatedAt"].asString(); + std::string b64DicomContent = jsonWl["Dicom"].asString(); + Orthanc::Toolbox::DecodeBase64(dicomContent_, b64DicomContent); + + } + + void Serialize(std::string& target) const + { + Json::Value t; + t["CreatedAt"] = createdAt_; + std::string b64DicomContent; + Orthanc::Toolbox::EncodeBase64(b64DicomContent, dicomContent_); + t["Dicom"] = b64DicomContent; + + Orthanc::Toolbox::WriteFastJson(target, t); + } + + bool IsOlderThan(unsigned int delayInHours) const + { + boost::posix_time::ptime now(boost::posix_time::from_iso_string(Orthanc::SystemToolbox::GetNowIsoString(true))); + boost::posix_time::ptime creationDate(boost::posix_time::from_iso_string(createdAt_)); + + return (now - creationDate).total_seconds() > (3600 * deleteDelayInHours_); + } +}; /** * This is the main function for matching a DICOM worklist against a query. @@ -42,10 +110,10 @@ static bool MatchWorklist(OrthancPluginWorklistAnswers* answers, const OrthancPluginWorklistQuery* query, const OrthancPlugins::FindMatcher& matcher, - const std::string& path) + const std::string& dicomContent) { OrthancPlugins::MemoryBuffer dicom; - dicom.ReadFile(path); + dicom.Assign(dicomContent); if (matcher.IsMatch(dicom)) { @@ -137,70 +205,116 @@ } } +static void ListWorklistsFromDb(std::vector<Worklist>& target) +{ + assert(worklistStorage_ == WorklistStorageType_OrthancDb); + assert(worklistsStore_); + + std::unique_ptr<OrthancPlugins::KeyValueStore::Iterator> it(worklistsStore_->CreateIterator()); + + while (it->Next()) + { + std::string serialized; + it->GetValue(serialized); + + Json::Value jsonWl; + if (Orthanc::Toolbox::ReadJson(jsonWl, serialized)) + { + Worklist wl(it->GetKey(), jsonWl); + target.push_back(wl); + } + }; + +} -OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers* answers, - const OrthancPluginWorklistQuery* query, - const char* issuerAet, - const char* calledAet) +static void ListWorklistsFromFolder(std::vector<Worklist>& target) +{ + assert(worklistStorage_ == WorklistStorageType_Folder); + + // Loop over the regular files in the database folder + namespace fs = boost::filesystem; + + fs::path source = worklistDirectory_; + fs::directory_iterator end; + + try + { + for (fs::directory_iterator it(source); it != end; ++it) + { + fs::file_type type(it->status().type()); + + if (type == fs::regular_file || + type == fs::reparse_file) // cf. BitBucket issue #11 + { + std::string extension = it->path().extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); // Convert to lowercase + + if (extension == ".wl") + { + std::string worklistId = Orthanc::SystemToolbox::PathToUtf8(it->path().filename().replace_extension("")); + std::string dicomContent; + Orthanc::SystemToolbox::ReadFile(dicomContent, it->path()); + + Worklist wl(worklistId, dicomContent); + target.push_back(wl); + } + } + } + } + catch (fs::filesystem_error&) + { + LOG(ERROR) << "Inexistent folder while scanning for worklists: " + source.string(); + } +} + + +OrthancPluginErrorCode WorklistCallback(OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* issuerAet, + const char* calledAet) { try { + unsigned int parsedFilesCount = 0; + unsigned int matchedWorklistCount = 0; + // Construct an object to match the worklists in the database against the C-Find query std::unique_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet)); - // Loop over the regular files in the database folder - namespace fs = boost::filesystem; + std::vector<Worklist> worklists; - fs::path source(folder_); - fs::directory_iterator end; - - try + if (worklistStorage_ == WorklistStorageType_Folder) { - unsigned int parsedFilesCount = 0; - unsigned int matchedWorklistCount = 0; - - for (fs::directory_iterator it(source); it != end; ++it) - { - fs::file_type type(it->status().type()); - - if (type == fs::regular_file || - type == fs::reparse_file) // cf. BitBucket issue #11 - { - std::string extension = it->path().extension().string(); - std::transform(extension.begin(), extension.end(), extension.begin(), tolower); // Convert to lowercase + ListWorklistsFromFolder(worklists); + } + else if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + ListWorklistsFromDb(worklists); + } - if (extension == ".wl") - { - parsedFilesCount++; - // We found a worklist (i.e. a DICOM find with extension ".wl"), match it against the query - if (MatchWorklist(answers, query, *matcher, it->path().string())) - { - if (limitAnswers_ != 0 && - matchedWorklistCount >= limitAnswers_) - { - // Too many answers are to be returned wrt. the - // "LimitAnswers" configuration parameter. Mark the - // C-FIND result as incomplete. - OrthancPluginWorklistMarkIncomplete(OrthancPlugins::GetGlobalContext(), answers); - return OrthancPluginErrorCode_Success; - } - - ORTHANC_PLUGINS_LOG_INFO("Worklist matched: " + it->path().string()); - matchedWorklistCount++; - } - } + for (std::vector<Worklist>::const_iterator it = worklists.begin(); it != worklists.end(); ++it) + { + if (MatchWorklist(answers, query, *matcher, it->dicomContent_)) + { + if (limitAnswers_ != 0 && + matchedWorklistCount >= limitAnswers_) + { + // Too many answers are to be returned wrt. the + // "LimitAnswers" configuration parameter. Mark the + // C-FIND result as incomplete. + OrthancPluginWorklistMarkIncomplete(OrthancPlugins::GetGlobalContext(), answers); + return OrthancPluginErrorCode_Success; } + + LOG(INFO) << "Worklist matched: " << it->id_; + matchedWorklistCount++; } - - ORTHANC_PLUGINS_LOG_INFO("Worklist C-Find: parsed " + boost::lexical_cast<std::string>(parsedFilesCount) + - " files, found " + boost::lexical_cast<std::string>(matchedWorklistCount) + " match(es)"); } - catch (fs::filesystem_error&) - { - ORTHANC_PLUGINS_LOG_ERROR("Inexistent folder while scanning for worklists: " + source.string()); - return OrthancPluginErrorCode_DirectoryExpected; - } + + LOG(INFO) << "Worklist C-Find: parsed " << boost::lexical_cast<std::string>(parsedFilesCount) << + " worklists, found " << boost::lexical_cast<std::string>(matchedWorklistCount) << " match(es)"; + return OrthancPluginErrorCode_Success; } @@ -210,13 +324,411 @@ } } +static void DeleteWorklist(const std::string& worklistId) +{ + switch (worklistStorage_) + { + case WorklistStorageType_Folder: + { + boost::filesystem::path path = worklistDirectory_ / (worklistId + ".wl"); + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + Orthanc::SystemToolbox::RemoveFile(path); + break; + } + + case WorklistStorageType_OrthancDb: + if (worklistsStore_.get() != NULL) + { + std::string notUsed; + if (!worklistsStore_->GetValue(notUsed, worklistId)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + worklistsStore_->DeleteKey(worklistId); + } + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + +} + +static void WorklistHkWorkerThread() +{ + OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "WL HOUSEKEEPER"); + + OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Worklist Housekeeper worker thread"); + Orthanc::Toolbox::ElapsedTimer timer; + + while (!worklistHousekeeperThreadShouldStop_) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + + if (timer.GetElapsedMilliseconds() > (hkIntervalInSeconds_ * 1000)) + { + timer.Restart(); + LOG(INFO) << "Performing Worklist Housekeeping"; + + std::vector<Worklist> worklists; + + if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + ListWorklistsFromDb(worklists); + } + else if (worklistStorage_ == WorklistStorageType_Folder) + { + ListWorklistsFromFolder(worklists); + } + + for (std::vector<Worklist>::const_iterator it = worklists.begin(); it != worklists.end(); ++it) + { + if (deleteDelayInHours_ > 0 && it->IsOlderThan(deleteDelayInHours_)) + { + LOG(INFO) << "Deleting worklist " << it->id_ << " " << deleteDelayInHours_ << " hours after its creation"; + DeleteWorklist(it->id_); + } + else if (deleteWorklistsOnStableStudy_) + { + std::string studyInstanceUid; + std::string patientId; + + Orthanc::ParsedDicomFile parsed(it->dicomContent_); + + if (parsed.GetTagValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID) && + parsed.GetTagValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID)) + { + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, "fake-id", "fake-id"); + const std::string& studyOrthancId = hasher.HashStudy(); + + Json::Value studyInfo; + if (OrthancPlugins::RestApiGet(studyInfo, "/studies/" + studyOrthancId, false)) + { + if (studyInfo["IsStable"].asBool()) + { + LOG(INFO) << "Deleting worklist " << it->id_ << " because its study is now stable"; + DeleteWorklist(it->id_); + } + } + } + } + } + } + } +} + + +static Orthanc::DicomToJsonFormat GetFormat(const OrthancPluginHttpRequest* request) +{ + std::map<std::string, std::string> getArguments; + OrthancPlugins::GetGetArguments(getArguments, request); + + Orthanc::DicomToJsonFormat format = Orthanc::DicomToJsonFormat_Human; + + if (getArguments.find("format") != getArguments.end()) + { + format = Orthanc::StringToDicomToJsonFormat(getArguments["format"]); + } + + return format; +} + +static Orthanc::ParsedDicomFile* GetWorklist(const std::string& id) +{ + std::string fileContent; + + switch (worklistStorage_) + { + case WorklistStorageType_Folder: + { + boost::filesystem::path path = worklistDirectory_ / Orthanc::SystemToolbox::PathFromUtf8(id + ".wl"); // the id might be a filename from a file that was pushed by an external program (therefore, it can contain fancy characters) + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Worklist not found"); + } + + Orthanc::SystemToolbox::ReadFile(fileContent, path); + break; + } + + case WorklistStorageType_OrthancDb: + { + std::string serializedWl; + if (!worklistsStore_->GetValue(serializedWl, id)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Worklist not found"); + } + + Json::Value jsonWl; + if (Orthanc::Toolbox::ReadJson(jsonWl, serializedWl)) + { + Worklist wl(id, jsonWl); + fileContent = wl.dicomContent_; + } + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + return new Orthanc::ParsedDicomFile(fileContent); +} + +static void SerializeWorklistForApi(Json::Value& target, const std::string& id, const Orthanc::ParsedDicomFile& parsed, Orthanc::DicomToJsonFormat format) +{ + target["ID"] = id; + target["Tags"] = Json::objectValue; + parsed.DatasetToJson(target["Tags"], format, Orthanc::DicomToJsonFlags_None, 0); +} extern "C" { + + OrthancPluginErrorCode ListWorklists(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPlugins::AnswerMethodNotAllowed(output, "GET"); + } + else + { + Json::Value response = Json::arrayValue; + Orthanc::DicomToJsonFormat format = GetFormat(request); + + std::vector<Worklist> worklists; + + if (worklistStorage_ == WorklistStorageType_Folder) + { + ListWorklistsFromFolder(worklists); + } + else if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + ListWorklistsFromDb(worklists); + } + + for (std::vector<Worklist>::const_iterator it = worklists.begin(); it != worklists.end(); ++it) + { + Orthanc::ParsedDicomFile parsed(it->dicomContent_); + Json::Value jsonWl; + SerializeWorklistForApi(jsonWl, it->id_, parsed, format); + + if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + jsonWl["CreationDate"] = it->createdAt_; + } + + response.append(jsonWl); + } + + OrthancPlugins::AnswerJson(response, output); + } + + return OrthancPluginErrorCode_Success; + } + + + void CreateOrUpdateWorklist(std::string& worklistId, + bool defaultForceValue, + OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + Json::Value body; + + if (!OrthancPlugins::ReadJson(body, request->body, request->bodySize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected"); + } + + if (!body.isMember("Tags") || !body["Tags"].isObject()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'Tags' field is missing or not a JSON object"); + } + + bool force = defaultForceValue; + if (body.isMember("Force")) { + force = body["Force"].asBool(); + } + + Json::Value& jsonWorklist = body["Tags"]; + + if (!jsonWorklist.isMember("SpecificCharacterSet")) + { + jsonWorklist["SpecificCharacterSet"] = Orthanc::GetDicomSpecificCharacterSet(Orthanc::Encoding_Utf8); + } + + std::unique_ptr<Orthanc::ParsedDicomFile> dicom(Orthanc::ParsedDicomFile::CreateFromJson(jsonWorklist, Orthanc::DicomFromJsonFlags_None, "")); + + if (!force) + { + if (!dicom->HasTag(Orthanc::DICOM_TAG_SCHEDULED_PROCEDURE_STEP_SEQUENCE)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'Tags' is missing a 'ScheduledProcedureStepSequence'. Use 'Force': true to bypass this check."); + } + Orthanc::DicomMap step; + if (!dicom->LookupSequenceItem(step, Orthanc::DicomPath::Parse("ScheduledProcedureStepSequence"), 0) || !step.HasTag(Orthanc::DICOM_TAG_MODALITY)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'ScheduledProcedureStepSequence' is missing a 'Modality' Use 'Force': true to bypass this check."); + } + if (!dicom->LookupSequenceItem(step, Orthanc::DicomPath::Parse("ScheduledProcedureStepSequence"), 0) || !step.HasTag(Orthanc::DICOM_TAG_SCHEDULED_PROCEDURE_STEP_START_DATE)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'ScheduledProcedureStepSequence' is missing a 'ScheduledProcedureStepStartDate' Use 'Force': true to bypass this check."); + } + } + + dicom->SetIfAbsent(Orthanc::DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, "1.2.276.0.7230010.3.1.0.1"); + + if (setStudyInstanceUidIfMissing_) + { + dicom->SetIfAbsent(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, Orthanc::FromDcmtkBridge::GenerateUniqueIdentifier(Orthanc::ResourceType_Study)); + } + + if (worklistId.empty()) + { + worklistId = Orthanc::Toolbox::GenerateUuid(); + } + + std::string dicomContent; + dicom->SaveToMemoryBuffer(dicomContent); + + switch (worklistStorage_) + { + case WorklistStorageType_Folder: + Orthanc::SystemToolbox::WriteFile(dicomContent.empty() ? NULL : dicomContent.c_str(), dicomContent.size(), + worklistDirectory_ / Orthanc::SystemToolbox::PathFromUtf8(worklistId + ".wl"), true); + break; + + case WorklistStorageType_OrthancDb: + { + Worklist wl(worklistId, dicomContent); + std::string serializedWl; + wl.Serialize(serializedWl); + + worklistsStore_->Store(worklistId, serializedWl); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + Json::Value response; + + response["ID"] = worklistId; + response["Path"] = "/worklists/" + worklistId; + + OrthancPlugins::AnswerJson(response, output); + } + + + OrthancPluginErrorCode GetPutDeleteWorklist(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + std::string worklistId = std::string(request->groups[0]); + + if (request->method == OrthancPluginHttpMethod_Delete) + { + DeleteWorklist(worklistId); + + OrthancPlugins::AnswerString("{}", "application/json", output); + } + else if (request->method == OrthancPluginHttpMethod_Get) + { + Orthanc::DicomToJsonFormat format = GetFormat(request); + std::unique_ptr<Orthanc::ParsedDicomFile> parsed(GetWorklist(worklistId)); + + Json::Value jsonWl; + SerializeWorklistForApi(jsonWl, worklistId, *parsed, format); + + OrthancPlugins::AnswerJson(jsonWl, output); + } + else if (request->method == OrthancPluginHttpMethod_Put) + { + CreateOrUpdateWorklist(worklistId, true, output, url, request); + } + else + { + OrthancPlugins::AnswerMethodNotAllowed(output, "DELETE,GET"); + } + + return OrthancPluginErrorCode_Success; + } + + + OrthancPluginErrorCode PostCreateWorklist(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPlugins::AnswerMethodNotAllowed(output, "POST"); + } + else + { + std::string worklistId; + CreateOrUpdateWorklist(worklistId, false, output, url, request); + } + return OrthancPluginErrorCode_Success; + } + + static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char *resourceId) + { + try + { + if (changeType == OrthancPluginChangeType_OrthancStarted) + { + Json::Value system; + bool hasKeyValueStores = (OrthancPlugins::RestApiGet(system, "/system", false) && system.isMember("Capabilities") && + system["Capabilities"].isMember("HasKeyValueStores") && system["Capabilities"]["HasKeyValueStores"].asBool()); + + if (worklistStorage_ == WorklistStorageType_OrthancDb && !hasKeyValueStores) + { + LOG(ERROR) << "The Orthanc DB plugin does not support Key Value Stores. It is therefore impossible to store the worklists in Orthanc Database"; + return OrthancPluginErrorCode_IncompatibleConfigurations; + } + + if (deleteDelayInHours_ > 0 && !hasKeyValueStores) + { + LOG(ERROR) << "The Orthanc DB plugin does not support Key Value Stores. It is therefore impossible to use the \"DeleteWorklistsDelay\" option"; + return OrthancPluginErrorCode_IncompatibleConfigurations; + } + + if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + worklistsStore_.reset(new OrthancPlugins::KeyValueStore("worklists")); + } + + if (deleteDelayInHours_ > 0 || deleteWorklistsOnStableStudy_) + { + worklistHousekeeperThread_.reset(new boost::thread(WorklistHkWorkerThread)); + } + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception: " << e.What(); + } + catch (...) + { + LOG(ERROR) << "Uncatched native exception"; + } + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) { OrthancPlugins::SetGlobalContext(c, MODALITY_WORKLISTS_NAME); - + Orthanc::Logging::InitializePluginContext(c, MODALITY_WORKLISTS_NAME); + /* Check the version of the Orthanc core */ if (OrthancPluginCheckVersion(c) == 0) { @@ -226,6 +738,29 @@ return -1; } + { // init the OrthancFramework + static const char* const LOCALE = "Locale"; + static const char* const DEFAULT_ENCODING = "DefaultEncoding"; + + /** + * This function is a simplified version of function + * "Orthanc::OrthancInitialize()" that is executed when starting the + * Orthanc server. + **/ + OrthancPlugins::OrthancConfiguration globalConfig; + Orthanc::InitializeFramework(globalConfig.GetStringValue(LOCALE, ""), false /* loadPrivateDictionary */); + + std::string encoding; + if (globalConfig.LookupStringValue(encoding, DEFAULT_ENCODING)) + { + Orthanc::SetDefaultDicomEncoding(Orthanc::StringToEncoding(encoding.c_str())); + } + else + { + Orthanc::SetDefaultDicomEncoding(Orthanc::ORTHANC_DEFAULT_DICOM_ENCODING); + } + } + ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is initializing"); OrthancPluginSetDescription2(c, MODALITY_WORKLISTS_NAME, "Serve DICOM modality worklists from a folder with Orthanc."); @@ -237,19 +772,52 @@ bool enabled = worklists.GetBooleanValue("Enable", false); if (enabled) { - if (worklists.LookupStringValue(folder_, "Database")) + std::string folder; + if (worklists.LookupStringValue(folder, "Database") || worklists.LookupStringValue(folder, "Directory")) { - ORTHANC_PLUGINS_LOG_WARNING("The database of worklists will be read from folder: " + folder_); - OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback); + if (worklists.GetBooleanValue("SaveInOrthancDatabase", false)) + { + LOG(ERROR) << "Worklists plugin: you can not set the \"SaveInOrthancDatabase\" configuration to \"true\" once you have configured the \"Directory\" (or former \"Database\") configuration."; + return -1; + } + + worklistStorage_ = WorklistStorageType_Folder; + worklistDirectory_ = folder; //Orthanc::SystemToolbox::PathFromUtf8(folder); + LOG(WARNING) << "The database of worklists will be read from folder: " << folder; + } + else if (worklists.GetBooleanValue("SaveInOrthancDatabase", false)) + { + + worklistStorage_ = WorklistStorageType_OrthancDb; + ORTHANC_PLUGINS_LOG_WARNING("The database of worklists will be read from Orthanc Database"); } else { - ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"Worklists.Database\" must contain a path"); + LOG(ERROR) << "The configuration option \"Worklists.Directory\" must contain a path"; return -1; } + OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), WorklistCallback); + filterIssuerAet_ = worklists.GetBooleanValue("FilterIssuerAet", false); limitAnswers_ = worklists.GetUnsignedIntegerValue("LimitAnswers", 0); + + deleteWorklistsOnStableStudy_ = worklists.GetBooleanValue("DeleteWorklistsOnStableStudy", true); + hkIntervalInSeconds_ = worklists.GetUnsignedIntegerValue("HousekeepingInterval", 60); + deleteDelayInHours_ = worklists.GetUnsignedIntegerValue("DeleteWorklistsDelay", 0); + setStudyInstanceUidIfMissing_ = worklists.GetBooleanValue("SetStudyInstanceUidIfMissing", true); + + if (deleteDelayInHours_ > 0 && worklistStorage_ == WorklistStorageType_Folder) + { + LOG(ERROR) << "Worklists plugin: you can not set the \"DeleteWorklistsDelay\" configuration once you have configured the \"Directory\" (or former \"Database\") configuration. This feature only works once \"SaveInOrthancDatabase\" is set to true."; + return -1; + } + + OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); + + OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists/create", PostCreateWorklist); + OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists/([^/]+)", GetPutDeleteWorklist); + OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists", ListWorklists); } else { @@ -263,6 +831,13 @@ ORTHANC_PLUGINS_API void OrthancPluginFinalize() { ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is finalizing"); + + worklistHousekeeperThreadShouldStop_ = true; + if (worklistHousekeeperThread_.get() != NULL && worklistHousekeeperThread_->joinable()) + { + worklistHousekeeperThread_->join(); + } + worklistHousekeeperThread_.reset(NULL); }
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/MultitenantDicomServer.h Tue Nov 04 15:58:06 2025 +0100 @@ -27,6 +27,7 @@ #include "PluginEnumerations.h" #include "../../../../OrthancFramework/Sources/DicomNetworking/DicomServer.h" +#include "../../../../OrthancFramework/Sources/MetricsRegistry.h" #include <boost/thread/mutex.hpp> @@ -59,6 +60,7 @@ bool isStrictAet_; DicomFilter filter_; std::unique_ptr<Orthanc::DicomServer> server_; + Orthanc::MetricsRegistry dummyMetricsRegistry_; public: explicit MultitenantDicomServer(const Json::Value& serverConfig);
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/OrthancFrameworkDependencies.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -77,6 +77,7 @@ #include "../../../../OrthancFramework/Sources/Images/PngReader.cpp" #include "../../../../OrthancFramework/Sources/Images/PngWriter.cpp" #include "../../../../OrthancFramework/Sources/Logging.cpp" +#include "../../../../OrthancFramework/Sources/MetricsRegistry.cpp" #include "../../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp" #include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp" #include "../../../../OrthancFramework/Sources/OrthancException.cpp"
--- a/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt Tue Nov 04 15:58:06 2025 +0100 @@ -27,18 +27,24 @@ SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake) + +include_directories( + ${CMAKE_SOURCE_DIR}/../../Include/ + ) add_library(ServeFolders SHARED Plugin.cpp + ${ORTHANC_CORE_SOURCES_DEPENDENCIES} + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Enumerations.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Logging.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/OrthancException.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SystemToolbox.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Toolbox.cpp ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp - ${JSONCPP_SOURCES} - ${BOOST_SOURCES} ) DefineSourceBasenameForTarget(ServeFolders)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/ServeFolders/OrthancFrameworkDependencies.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * Remove the dependency upon ICU in plugins, as this greatly increase + * the size of the resulting binaries, since they must embed the ICU + * dictionary. + **/ + +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + + +#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" +#include "../../../../OrthancFramework/Sources/Enumerations.cpp" +#include "../../../../OrthancFramework/Sources/Logging.cpp" +#include "../../../../OrthancFramework/Sources/OrthancException.cpp" +#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp" +#include "../../../../OrthancFramework/Sources/Toolbox.cpp"
--- a/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -25,6 +25,8 @@ #include "../Common/OrthancPluginCppWrapper.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" + #include <json/value.h> #include <boost/filesystem.hpp> #include <boost/date_time/posix_time/posix_time.hpp> @@ -158,11 +160,11 @@ if (LookupFolder(folder, output, request)) { const fs::path item(request->groups[1]); - const fs::path parent((fs::path(folder) / item).parent_path()); + const fs::path parent((Orthanc::SystemToolbox::PathFromUtf8(folder) / item).parent_path()); if (item.filename().string() == "index.html" && fs::is_directory(parent) && - !fs::is_regular_file(fs::path(folder) / item)) + !fs::is_regular_file(Orthanc::SystemToolbox::PathFromUtf8(folder) / item)) { // On-the-fly generation of an "index.html" std::string s; @@ -216,12 +218,12 @@ } boost::posix_time::ptime lastModification = - boost::posix_time::from_time_t(fs::last_write_time(path)); + boost::posix_time::from_time_t(fs::last_write_time(Orthanc::SystemToolbox::PathFromUtf8(path))); std::string t = boost::posix_time::to_iso_string(lastModification); OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Last-Modified", t.c_str()); - Answer(output, content.GetData(), content.GetSize(), mime); + Answer(output, reinterpret_cast<const char*>(content.GetData()), content.GetSize(), mime); } } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/CodeModel/Dockerfile Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,28 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && \ + apt install -y python3 python3-clang-14 && \ + apt clean
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/CodeModel/GenerateCodeModel.py Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,603 @@ +#!/usr/bin/env python3 + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import argparse +import clang.cindex +import json +import os +import pprint +import re +import sys + + +ROOT = os.path.dirname(os.path.realpath(sys.argv[0])) + +parser = argparse.ArgumentParser(description = 'Generate the code model of the Orthanc SDK.') +parser.add_argument('--libclang', + default = 'libclang-14.so.1', + help = 'manually provides the path to the libclang shared library') +parser.add_argument('--source', + default = os.path.join(ROOT, '../../Plugins/Include/orthanc/OrthancCPlugin.h'), + help = 'input path to the Orthanc SDK header') +parser.add_argument('--target', + default = os.path.join(ROOT, '../../Plugins/Include/orthanc/OrthancPluginCodeModel.json'), + help = 'target path to store the JSON code model') + +args = parser.parse_args() + +if len(args.libclang) != 0: + clang.cindex.Config.set_library_file(args.libclang) + +index = clang.cindex.Index.create() + +tu = index.parse(args.source, [ ]) + +TARGET = os.path.realpath(args.target) + + + +SPECIAL_FUNCTIONS = [ + 'OrthancPluginCreateMemoryBuffer', + 'OrthancPluginCreateMemoryBuffer64', + 'OrthancPluginFreeMemoryBuffer', + 'OrthancPluginFreeMemoryBuffer64', + 'OrthancPluginFreeString', + ] + + + +# First, discover the classes and enumerations +classes = {} +enumerations = {} + +def ParseDocumentationLines(comment): + s = re.sub('^[ ]*/', '', comment) + s = re.sub('/[ ]*$', '', s) + s = re.sub('<tt>', '"', s) + s = re.sub('</tt>', '"', s) + return list(map(lambda x: re.sub('[ ]*\*+', '', x).strip(), s.splitlines())) + +def ParseEnumerationDocumentation(comment): + result = '' + for line in ParseDocumentationLines(comment): + if len(line) > 0 and not line.startswith('@'): + if len(result) == 0: + result = line + else: + result = result + ' ' + line + return result + +def ParseEnumValueDocumentation(comment): + m = re.match(r'/\*!<\s*(.*?)\s*\*/$', comment, re.MULTILINE) + if m != None: + return m.group(1) + else: + result = '' + for line in ParseDocumentationLines(comment): + if len(line) > 0: + if len(result) == 0: + result = line + else: + result = result + ' ' + line + return result.replace('@brief ', '') + +def InjectSinceSdk(target, node): + since_sdk = None + + for child in node.get_children(): + if child.kind == clang.cindex.CursorKind.ANNOTATE_ATTR: + s = child.spelling.split(' ') + if s[0] == 'ORTHANC_PLUGIN_SINCE_SDK': + assert(len(s) == 2) + version = s[1].split('.') + assert(len(version) == 3) + assert(since_sdk == None) # Cannot be defined multiple time + since_sdk = [ int(version[0]), int(version[1]), int(version[2]) ] + + if since_sdk != None: + target['since_sdk'] = since_sdk + + +for node in tu.cursor.get_children(): + # Only consider the Orthanc SDK + path = node.location.file.name + if os.path.split(path) [-1] != 'OrthancCPlugin.h': + continue + + if node.kind == clang.cindex.CursorKind.ENUM_DECL: + if node.type.spelling.startswith('OrthancPlugin'): + name = node.type.spelling + + if name in enumerations: + raise Exception('Enumeration declared twice: %s' % name) + + if node.raw_comment == None: + raise Exception('Enumeration without documentation: %s' % name) + + values = [] + for item in node.get_children(): + if (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and + item.spelling.startswith(name + '_')): + + if item.raw_comment == None: + raise Exception('Enumeration value without documentation: %s' % item.spelling) + + key = item.spelling[len(name + '_'):] + value = { + 'key' : key, + 'value' : item.enum_value, + 'documentation' : ParseEnumValueDocumentation(item.raw_comment), + } + + InjectSinceSdk(value, item) + values.append(value) + + elif (item.kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL and + item.spelling == '_%s_INTERNAL' % name): + pass + + elif (item.kind == clang.cindex.CursorKind.ANNOTATE_ATTR and + item.spelling.startswith('ORTHANC_PLUGIN_SINCE_SDK ')): + pass + + else: + raise Exception('Ignoring unknown enumeration item: %s' % item.spelling) + + value = { + 'values' : values, + 'documentation' : ParseEnumerationDocumentation(node.raw_comment), + } + + InjectSinceSdk(value, node) + enumerations[name] = value + + elif node.spelling == '': # Unnamed enumeration (presumbaly "_OrthancPluginService") + pass + + else: + raise Exception('Ignoring unknown enumeration: %s' % node.spelling) + + elif node.kind == clang.cindex.CursorKind.STRUCT_DECL: + if (node.spelling.startswith('_OrthancPlugin') and + node.spelling.endswith('_t') and + node.spelling != '_OrthancPluginContext_t'): + + name = node.spelling[len('_') : -len('_t')] + value = { + 'name' : name, + 'methods' : [ ], + } + + InjectSinceSdk(value, node) + classes[name] = value + + elif node.spelling in [ '', # This is an internal structure to call Orthanc SDK + '_OrthancPluginContext_t' ]: + pass + + else: + raise Exception('Ignoring unknown structure: %s' % node.spelling) + + +# Secondly, loop over the C functions and categorize them either as +# method, or as global functions + + +def RemovePrefix(prefix, s): + if not s.startswith(prefix): + raise Exception('String "%s" does not start with prefix "%s"' % (s, prefix)) + else: + return s[len(prefix):] + + +def IsClassType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + not t.get_pointee().is_const_qualified() and + t.get_pointee().spelling in classes) + + +def IsConstClassType(t): + return (t.kind == clang.cindex.TypeKind.POINTER and + t.get_pointee().is_const_qualified() and + t.get_pointee().spelling.startswith('const ') and + t.get_pointee().spelling[len('const '):] in classes) + + +def EncodeArguments(target, args): + assert(type(target) is dict) + result = [] + + i = 0 + while i < len(args): + arg = { + 'name' : 'arg%d' % i, + 'sdk_name' : args[i].spelling, + 'sdk_type' : args[i].type.spelling, + } + + if (i + 1 < len(args) and + args[i].type.spelling == 'const void *' and + args[i + 1].type.spelling == 'uint32_t'): + + arg['sdk_type'] = 'const_void_pointer_with_size' + + # Skip the size argument + i += 1 + + elif arg['sdk_type'] in [ 'float', + 'int32_t', + 'int64_t', + 'uint8_t', + 'uint16_t', + 'uint32_t', + 'uint64_t', + 'const char *', + 'const void *' ]: + pass + + elif arg['sdk_type'] in enumerations: + arg['sdk_type'] = 'enumeration' + arg['sdk_enumeration'] = args[i].type.spelling + + elif IsClassType(args[i].type): + arg['sdk_type'] = 'object' + arg['sdk_class'] = args[i].type.get_pointee().spelling + + elif IsConstClassType(args[i].type): + arg['sdk_type'] = 'const_object' + arg['sdk_class'] = RemovePrefix('const ', args[i].type.get_pointee().spelling) + + else: + print('[WARNING] Unsupported argument type in a method (%s), cannot wrap: %s' % ( + args[i].type.spelling, node.spelling)) + return False + + result.append(arg) + i += 1 + + target['args'] = result + return True + + +def EncodeResultType(target, returnBufferType, t): + assert(type(target) is dict) + assert('args' in target) + + target['return_sdk_type'] = t.spelling + + if returnBufferType != None: + target['return_sdk_type'] = returnBufferType + return True + + elif target['return_sdk_type'] in [ 'void', + 'int32_t', + 'uint32_t', + 'int64_t', + 'char *', + 'const char *' ]: + return True + + elif target['return_sdk_type'] in enumerations: + target['return_sdk_type'] = 'enumeration' + target['return_sdk_enumeration'] = t.spelling + return True + + elif IsClassType(t): + target['return_sdk_type'] = 'object' + target['return_sdk_class'] = t.get_pointee().spelling + return True + + else: + return False + + +def ParseFunctionDocumentation(comment): + lines = ParseDocumentationLines(comment) + + sections = [] + currentType = None + currentSection = None + + for i in range(len(lines)): + if lines[i].find('@') > 0: + raise Exception('Character "@" not occurring at the beggining of a documentation paragraph') + + if (len(lines[i]) == 0 and + currentType == None): + continue + + m = re.match(r'^@([a-z]+)\s*', lines[i]) + + if m == None: + if currentType == None: + print(comment) + raise Exception('Documentation does not begin with a "@"') + + assert(currentSection != None) + currentSection.append(lines[i]) + else: + if currentType != None: + sections.append({ + 'type' : currentType, + 'lines' : currentSection, + }) + + currentType = m.group(1) + currentSection = [ lines[i][m.span() [1] : ] ] + + if currentType == None: + raise Exception('Empty documentation') + + sections.append({ + 'type' : currentType, + 'lines' : currentSection, + }) + + for i in range(len(sections)): + paragraphs = [] + lines = sections[i]['lines'] + currentParagraph = '' + for j in range(len(lines)): + if len(lines[j]) == 0: + if currentParagraph != '': + paragraphs.append(currentParagraph) + currentParagraph = '' + else: + if currentParagraph == '': + currentParagraph = lines[j] + else: + currentParagraph = '%s %s' % (currentParagraph, lines[j]) + if currentParagraph != '': + paragraphs.append(currentParagraph) + + sections[i]['paragraphs'] = paragraphs + + documentation = { + 'args' : {} + } + + for i in range(len(sections)): + t = sections[i]['type'] + paragraphs = sections[i]['paragraphs'] + + if t == 'brief': + if len(paragraphs) < 1: + raise Exception('Bad @brief') + + documentation['summary'] = paragraphs[0] + documentation['description'] = paragraphs[1:] + + elif t in [ 'return', 'result' ]: + if len(paragraphs) != 1: + raise Exception('Bad @return') + + documentation['return'] = paragraphs[0] + + elif t == 'param': + if len(paragraphs) != 1: + raise Exception('Bad @param') + + m = re.match(r'^([a-zA-Z0-9]+)\s+(.+)', paragraphs[0]) + if m == None: + raise Exception('Bad @param') + + key = m.group(1) + value = m.group(2) + if (len(key) == 0 or + len(value) == 0): + raise Exception('Bad @param') + + if key in documentation['args']: + raise Exception('Twice the same parameter: %s' % key) + + documentation['args'][key] = value + + elif t == 'warning': + if not 'description' in documentation: + raise Exception('@warning before @summary') + + if len(paragraphs) == 0: + raise Exception('Bad @warning') + + for j in range(len(paragraphs)): + if j == 0: + documentation['description'].append('Warning: %s' % paragraphs[j]) + else: + documentation['description'].append(paragraphs[j]) + + elif t == 'note': + if not 'description' in documentation: + raise Exception('@note before @summary') + + if len(paragraphs) == 0: + raise Exception('Bad @note') + + for j in range(len(paragraphs)): + if j == 0: + documentation['description'].append('Remark: %s' % paragraphs[j]) + else: + documentation['description'].append(paragraphs[j]) + + elif t in [ + 'deprecated', + 'ingroup', + 'see', + ]: + pass + + else: + raise Exception('Unsupported documentation token: @%s' % t) + + return documentation + + +globalFunctions = [] +countWrappedFunctions = 0 +countAllFunctions = 0 +unwrappedFunctions = [] + +for node in tu.cursor.get_children(): + # Only consider the Orthanc SDK + path = node.location.file.name + if os.path.split(path) [-1] != 'OrthancCPlugin.h': + continue + + if (node.kind == clang.cindex.CursorKind.FUNCTION_DECL and + node.spelling.startswith('OrthancPlugin')): + + if node.spelling in SPECIAL_FUNCTIONS: + countAllFunctions += 1 + continue + + args = list(filter(lambda x: x.kind == clang.cindex.CursorKind.PARM_DECL, + node.get_children())) + + # Check that the first argument is the Orthanc context + if (len(args) == 0 or + args[0].type.kind != clang.cindex.TypeKind.POINTER or + args[0].type.get_pointee().spelling != 'OrthancPluginContext'): + print('[INFO] Not in the Orthanc SDK: %s()' % node.spelling) + continue + + countAllFunctions += 1 + + contextName = args[0].spelling + args = args[1:] # Skip the Orthanc context + + if (len(args) >= 1 and + args[0].type.spelling in [ 'OrthancPluginMemoryBuffer *', + 'OrthancPluginMemoryBuffer64 *' ]): + # The method/function returns a byte array + returnBufferType = args[0].type.spelling + args = args[1:] + else: + returnBufferType = None + + if (len(args) >= 1 and + (IsClassType(args[0].type) or + IsConstClassType(args[0].type))): + + # This is a class method + cls = args[0].type.get_pointee().spelling + if IsConstClassType(args[0].type): + cls = RemovePrefix('const ', cls) + + # Special case of destructors + if (len(args) == 1 and + not args[0].type.get_pointee().is_const_qualified() and + node.spelling.startswith('OrthancPluginFree')): + classes[cls]['destructor'] = node.spelling + countWrappedFunctions += 1 + + else: + if node.raw_comment == None: + raise Exception('Method without documentation: %s' % node.spelling) + + doc = ParseFunctionDocumentation(node.raw_comment) + del doc['args'][contextName] # Remove OrthancPluginContext from documentation + del doc['args'][args[0].spelling] # Remove self from documentation + + method = { + 'c_function' : node.spelling, + 'const' : args[0].type.get_pointee().is_const_qualified(), + 'documentation' : doc, + } + + InjectSinceSdk(method, node) + + if not EncodeArguments(method, args[1:]): + unwrappedFunctions.append(node.spelling) + elif EncodeResultType(method, returnBufferType, node.result_type): + classes[cls]['methods'].append(method) + countWrappedFunctions += 1 + else: + unwrappedFunctions.append(node.spelling) + print('[WARNING] Unsupported return type in a method (%s), cannot wrap: %s' % ( + node.result_type.spelling, node.spelling)) + + else: + # This is a global function + if node.raw_comment == None: + raise Exception('Global function without documentation: %s' % node.spelling) + + doc = ParseFunctionDocumentation(node.raw_comment) + del doc['args'][contextName] # Remove OrthancPluginContext from documentation + + f = { + 'c_function' : node.spelling, + 'documentation' : doc, + } + + InjectSinceSdk(f, node) + + if not EncodeArguments(f, args): + unwrappedFunctions.append(node.spelling) + elif EncodeResultType(f, returnBufferType, node.result_type): + globalFunctions.append(f) + countWrappedFunctions += 1 + else: + unwrappedFunctions.append(node.spelling) + print('[WARNING] Unsupported return type in a global function (%s), cannot wrap: %s' % ( + node.result_type.spelling, node.spelling)) + + + +# Thirdly, export the code model + +def FlattenEnumerations(): + result = [] + for (name, content) in enumerations.items(): + item = { + 'name' : name, + 'values' : content['values'], + 'documentation' : content['documentation'], + } + + if 'since_sdk' in content: + item['since_sdk'] = content['since_sdk'] + + result.append(item) + + return result + +def FlattenDictionary(source): + result = [] + for (name, value) in source.items(): + result.append(value) + return result + +codeModel = { + 'classes' : sorted(FlattenDictionary(classes), key = lambda x: x['name']), + 'enumerations' : sorted(FlattenEnumerations(), key = lambda x: x['name']), + 'global_functions' : globalFunctions, # Global functions are ordered in the same order as in the C header + 'unwrapped_functions' : unwrappedFunctions, + } + + +with open(TARGET, 'w') as f: + f.write(json.dumps(codeModel, sort_keys = True, indent = 4)) + +print('\nTotal functions in the SDK: %d' % countAllFunctions) +print('Total wrapped functions (including destructors): %d' % countWrappedFunctions) +print('Coverage: %.0f%%' % (float(countWrappedFunctions) / + float(countAllFunctions) * 100.0))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/CodeModel/README.txt Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,38 @@ + +Introduction +============ + +The "./GenerateCodeModel.py" Python script analyzes the header of the +Orthanc Plugin SDK using clang, and saves this code model as a JSON +file. + +The code model can then be used to generate wrappers. It is notably +used by the "orthanc-java" (starting with its release 1.0) and +"orthanc-python" plugins (starting with its release 4.3). + +NB: The generation of the code model was originally part of the +"orthanc-java" project. It was integrated into the Orthanc core +repository in release 1.12.9. + + +Usage on Ubuntu 22.04 LTS +========================= + +Executing with default parameters: + +$ sudo apt install python3-clang-14 +$ python3 ./GenerateCodeModel.py + + +Or, if you want to have more control: + +$ python3 ./GenerateCodeModel.py \ + --libclang=libclang-14.so.1 \ + --source ../../Plugins/Include/orthanc/OrthancCPlugin.h \ + --target ../../Plugins/Include/orthanc/OrthancPluginCodeModel.json + + +Generation using Docker +======================= + +$ ./docker-run.sh
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/CodeModel/docker-internal.sh Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,29 @@ +#!/bin/bash + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +set -ex + +python3 /source/OrthancServer/Resources/CodeModel/GenerateCodeModel.py \ + --libclang=libclang-14.so.1 \ + --source /source/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h \ + --target /target/OrthancPluginCodeModel.json
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/CodeModel/docker-run.sh Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,43 @@ +#!/bin/bash + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +set -ex + +if [ -t 1 ]; then + # TTY is available => use interactive mode + DOCKER_FLAGS='-i' +fi + +ROOT_DIR=`dirname $(readlink -f $0)`/../../.. + +( cd ${ROOT_DIR}/OrthancServer/Resources/CodeModel/ && \ + docker build -t orthanc-code-model . ) + +docker run -t ${DOCKER_FLAGS} --rm \ + --user $(id -u):$(id -g) \ + -v ${ROOT_DIR}:/source:ro \ + -v ${ROOT_DIR}/OrthancServer/Plugins/Include/orthanc/:/target:rw \ + orthanc-code-model \ + bash /source/OrthancServer/Resources/CodeModel/docker-internal.sh + +ls -lR ${ROOT_DIR}/OrthancServer/Plugins/Include/orthanc/
--- a/OrthancServer/Resources/Configuration.json Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Resources/Configuration.json Tue Nov 04 15:58:06 2025 +0100 @@ -71,7 +71,6 @@ // The period (in seconds) between 2 calls of the "OnHeartBeat" // lua callback. O means the heart beat is disabled. - // TODO: text below for Orthanc book: // Note: that the period is actually not the delay between // the end of an execution and the triggering of the next one. // Since there is only one lua context, if other lua code is being @@ -118,6 +117,19 @@ // HTTP port for the REST services and for the GUI "HttpPort" : 8042, + // IP addresses the HTTP server listens on. + // This is usefull when, e.g. your computer has multiple network + // interfaces and you want Orthanc to be accessible on a single + // sub-network. + // By default, the HTTP server listens on all IP addresses. + // Note that, setting "HttpBindAddresses" to ["127.0.0.1"] is + // almost equivalent to settting "RemoteAccessAllowed". In the + // first case, external HTTP clients won't be able to connect at + // all to Orthanc while, in the second case, external HTTP clients + // will be able to connect but will receive a 401 Unauthorized + // HTTP status code. + //"HttpBindAddresses": ["1.2.3.4", "127.0.0.1"] + // When the following option is "true", if an error is encountered // while calling the REST API, a JSON message describing the error // is put in the HTTP answer. This feature can be disabled if the @@ -307,10 +319,11 @@ // The list of the registered users. Because Orthanc uses HTTP // Basic Authentication, the passwords are stored as plain text. - "RegisteredUsers" : { - // "alice" : "alicePassword" - }, - + /** + "RegisteredUsers" : { + // "alice" : "alicePassword" + }, + **/ /**
--- a/OrthancServer/Resources/OrthancPlugin.doxygen Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Resources/OrthancPlugin.doxygen Tue Nov 04 15:58:06 2025 +0100 @@ -1945,7 +1945,8 @@ # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. PREDEFINED = ORTHANC_PLUGIN_INLINE= \ - ORTHANC_PLUGIN_DEPRECATED= + ORTHANC_PLUGIN_DEPRECATED= \ + ORTHANC_PLUGIN_SINCE_SDK= # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The
--- a/OrthancServer/Resources/RunCppCheck-2.17.0.sh Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck-2.17.0.sh Tue Nov 04 15:58:06 2025 +0100 @@ -9,7 +9,7 @@ fi cat <<EOF > /tmp/cppcheck-suppressions.txt -nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321 +nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322 stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1525 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74 @@ -106,5 +106,6 @@ ../../OrthancServer/Plugins/Samples/Housekeeper \ ../../OrthancServer/Plugins/Samples/ModalityWorklists \ ../../OrthancServer/Plugins/Samples/MultitenantDicom \ + ../../OrthancServer/Plugins/Samples/AdoptDicomInstance \ \ 2>&1
--- a/OrthancServer/Resources/RunCppCheck.sh Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Tue Nov 04 15:58:06 2025 +0100 @@ -12,7 +12,7 @@ constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp -nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321 +nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322 stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1525 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74 @@ -36,7 +36,7 @@ assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292 assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391 -assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3058 +assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3066 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286 assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454 EOF @@ -102,5 +102,6 @@ ../../OrthancServer/Plugins/Samples/Housekeeper \ ../../OrthancServer/Plugins/Samples/ModalityWorklists \ ../../OrthancServer/Plugins/Samples/MultitenantDicom \ + ../../OrthancServer/Plugins/Samples/AdoptDicomInstance \ \ 2>&1
--- a/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -27,12 +27,43 @@ #include <stdio.h> -int main(int argc, const char* argv[]) +#if defined(_WIN32) || defined(__CYGWIN__) +#include <windows.h> +#endif + + +#if defined(_WIN32) && !defined(__MINGW32__) +// arguments are passed as UTF-16 on Windows +int wmain(int argc, wchar_t *argv[]) { - if (argc != 2 && argc != 3) + // Set Windows console output to UTF-8 (otherwise, strings are considered to be in UTF-16. For example, Cyrillic UTF-8 strings appear as garbage without that config) + SetConsoleOutputCP(CP_UTF8); + + // Transform the UTF-16 arguments into UTF-8 arguments + std::vector<std::string> arguments; // UTF-8 arguments + + for (int i = 0; i < argc; i++) + { + std::wstring argument(argv[i]); + arguments.push_back(Orthanc::SystemToolbox::WStringToUtf8(argument)); + } + +#else +int main(int argc, char *argv[]) +{ + std::vector<std::string> arguments; // UTF-8 arguments + + // the arguments are assumed to be directly in UTF-8 + for (int i = 0; i < argc; i++) + { + arguments.push_back(argv[i]); + } +#endif + + if (arguments.size() != 2 && arguments.size() != 3) { fprintf(stderr, "Maintenance tool to recover a DICOM file that was compressed by Orthanc.\n\n"); - fprintf(stderr, "Usage: %s <input> [output]\n", argv[0]); + fprintf(stderr, "Usage: %s <input> [output]\n", arguments[0].c_str()); fprintf(stderr, "If \"output\" is not given, the data will be output to stdout\n"); return -1; } @@ -43,7 +74,7 @@ fflush(stderr); std::string content; - Orthanc::SystemToolbox::ReadFile(content, argv[1]); + Orthanc::SystemToolbox::ReadFile(content, Orthanc::SystemToolbox::PathFromUtf8(arguments[1])); fprintf(stderr, "Decompressing the content of the file...\n"); fflush(stderr); @@ -59,7 +90,7 @@ if (argc == 3) { - Orthanc::SystemToolbox::WriteFile(uncompressed, argv[2]); + Orthanc::SystemToolbox::WriteFile(uncompressed, Orthanc::SystemToolbox::PathFromUtf8(arguments[2])); } else {
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Nov 04 15:58:06 2025 +0100 @@ -56,6 +56,9 @@ bool hasMeasureLatency_; bool hasFindSupport_; bool hasExtendedChanges_; + bool hasAttachmentCustomDataSupport_; + bool hasKeyValueStoresSupport_; + bool hasQueuesSupport_; public: Capabilities() : @@ -66,7 +69,10 @@ hasUpdateAndGetStatistics_(false), hasMeasureLatency_(false), hasFindSupport_(false), - hasExtendedChanges_(false) + hasExtendedChanges_(false), + hasAttachmentCustomDataSupport_(false), + hasKeyValueStoresSupport_(false), + hasQueuesSupport_(false) { } @@ -100,6 +106,16 @@ return hasLabelsSupport_; } + void SetAttachmentCustomDataSupport(bool value) + { + hasAttachmentCustomDataSupport_ = value; + } + + bool HasAttachmentCustomDataSupport() const + { + return hasAttachmentCustomDataSupport_; + } + void SetHasExtendedChanges(bool value) { hasExtendedChanges_ = value; @@ -149,6 +165,27 @@ { return hasFindSupport_; } + + void SetKeyValueStoresSupport(bool value) + { + hasKeyValueStoresSupport_ = value; + } + + bool HasKeyValueStoresSupport() const + { + return hasKeyValueStoresSupport_; + } + + void SetQueuesSupport(bool value) + { + hasQueuesSupport_ = value; + } + + bool HasQueuesSupport() const + { + return hasQueuesSupport_; + } + }; @@ -250,6 +287,13 @@ int64_t id, FileContentType contentType) = 0; + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) = 0; + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) = 0; + /** * If "shared" is "true", the property is shared by all the * Orthanc servers that access the same database. If "shared" is @@ -390,6 +434,43 @@ int64_t to, uint32_t limit, const std::set<ChangeType>& filterType) = 0; + + // New in Orthanc 1.12.8 + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) = 0; + + // New in Orthanc 1.12.8 + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) = 0; + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* exclusive bound, only used if "first == false" */, + uint64_t limit /* maximum number of elements */) = 0; + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) = 0; + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) = 0; + + // New in Orthanc 1.12.8, for statistics only + virtual uint64_t GetQueueSize(const std::string& queueId) = 0; + }; @@ -456,7 +537,7 @@ virtual unsigned int GetDatabaseVersion() = 0; virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) = 0; + IPluginStorageArea& storageArea) = 0; virtual const Capabilities GetDatabaseCapabilities() const = 0;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallDeletedFiles.sql Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,52 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +CREATE TABLE DeletedFiles( + uuid TEXT NOT NULL, -- 0 + customData BLOB -- 1 +); + +-- We need to use another AttachedFileDeleted trigger than the legacy one in "Upgrade4To5.sql". +-- +-- We want to keep backward compatibility and avoid changing the database version number (which would force +-- users to upgrade the DB). By keeping backward compatibility, we mean "allow a user to run a previous Orthanc +-- version after it has run this update script". +-- We must preserve the signature of the initial trigger (it is impossible to have 2 triggers on the same event). +-- We tried adding a trigger on "BEFORE DELETE" but then it is being called when running the previous Orthanc +-- which makes it fail. +-- But, we need the customData in the C++ function that is called when a AttachedFiles is deleted. +-- The trick is then to save the customData in a DeletedFiles table. +-- The SignalFileDeleted C++ function will then get the customData from this table and delete the entry. +-- Drawback: if you downgrade Orthanc, the DeletedFiles table will remain and will be populated by the trigger +-- but not consumed by the C++ function -> we consider this is an acceptable drawback for a few people compared +-- to the burden of upgrading the DB. + +DROP TRIGGER IF EXISTS AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + INSERT INTO DeletedFiles VALUES(old.uuid, old.customData); + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + old.uncompressedMD5, old.compressedMD5 + ); +END;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallKeyValueStoresAndQueues.sql Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,35 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +CREATE TABLE KeyValueStores( + storeId TEXT NOT NULL, + key TEXT NOT NULL, + value BLOB NOT NULL, + PRIMARY KEY(storeId, key) -- Prevents duplicates + ); + +CREATE TABLE Queues ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queueId TEXT NOT NULL, + value BLOB NOT NULL +); + +CREATE INDEX QueuesIndex ON Queues (queueId, id);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/InstallRevisionAndCustomData.sql Tue Nov 04 15:58:06 2025 +0100 @@ -0,0 +1,35 @@ +-- Orthanc - A Lightweight, RESTful DICOM Store +-- Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +-- Department, University Hospital of Liege, Belgium +-- Copyright (C) 2017-2023 Osimis S.A., Belgium +-- Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +-- Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +-- +-- This program is free software: you can redistribute it and/or +-- modify it under the terms of the GNU General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-- General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. + + +-- +-- This SQLite script installs revision and customData without changing the Orthanc database version +-- + +-- Add new columns for revision +ALTER TABLE Metadata ADD COLUMN revision INTEGER; +ALTER TABLE AttachedFiles ADD COLUMN revision INTEGER; + +-- Add new column for customData +ALTER TABLE AttachedFiles ADD COLUMN customData BLOB; + +-- Record that this upgrade has been performed + +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Nov 04 15:58:06 2025 +0100 @@ -55,6 +55,7 @@ id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, type INTEGER, value TEXT, + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) PRIMARY KEY(id, type) ); @@ -67,6 +68,8 @@ compressionType INTEGER, uncompressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) compressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) + revision INTEGER, -- New in Orthanc 1.12.8 (added in InstallRevisionAndCustomData.sql) + customData BLOB, -- New in Orthanc 1.12.8 (added in InstallDeletedFiles.sql) PRIMARY KEY(id, fileType) ); @@ -95,22 +98,12 @@ patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE ); --- New in Orthanc 1.12.0 -CREATE TABLE Labels( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - label TEXT NOT NULL, - PRIMARY KEY(id, label) -- Prevents duplicates - ); - CREATE INDEX ChildrenIndex ON Resources(parentId); CREATE INDEX PublicIndex ON Resources(publicId); CREATE INDEX ResourceTypeIndex ON Resources(resourceType); CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); --- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up --- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); --- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); -- The 3 following indexes were added in Orthanc 0.8.5 (database v5) CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); @@ -119,18 +112,6 @@ CREATE INDEX ChangesIndex ON Changes(internalId); --- New in Orthanc 1.12.0 -CREATE INDEX LabelsIndex1 ON Labels(id); -CREATE INDEX LabelsIndex2 ON Labels(label); -- This index allows efficient lookups - -CREATE TRIGGER AttachedFileDeleted -AFTER DELETE ON AttachedFiles -BEGIN - SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, - old.compressionType, old.compressedSize, - -- These 2 arguments are new in Orthanc 0.7.3 (database v4) - old.uncompressedMD5, old.compressedMD5); -END; CREATE TRIGGER ResourceDeleted AFTER DELETE ON Resources @@ -156,6 +137,27 @@ END; +-- new in Orthanc 1.5.1 -------------------------- equivalent to InstallTrackAttachmentsSize.sql +${INSTALL_TRACK_ATTACHMENTS_SIZE} + + +-- new in Orthanc 1.12.0 ------------------------- equivalent to InstallLabelsTable.sql +${INSTALL_LABELS_TABLE} + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallDeletedFiles.sql +${INSTALL_DELETED_FILES} + + +-- new in Orthanc 1.12.8 ------------------------- equivalent to InstallKeyValueStoresAndQueues.sql +${INSTALL_KEY_VALUE_STORES_AND_QUEUES} + + +-- Track the fact that the "revision" column exists in the "Metadata" and "AttachedFiles" +-- tables, and that the "customData" column exists in the "AttachedFiles" table +INSERT INTO GlobalProperties VALUES (7, 1); -- GlobalProperty_SQLiteHasCustomDataAndRevision + + -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration INSERT INTO GlobalProperties VALUES (1, "6");
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -39,8 +39,10 @@ #include <OrthancServerResources.h> #include <stdio.h> +#include <boost/algorithm/string/replace.hpp> #include <boost/lexical_cast.hpp> + namespace Orthanc { static std::string JoinRequestedMetadata(const FindRequest::ChildrenSpecification& childrenSpec) @@ -384,19 +386,22 @@ } } - boost::mutex::scoped_lock lock_; + boost::recursive_mutex::scoped_lock lock_; IDatabaseListener& listener_; SignalRemainingAncestor& signalRemainingAncestor_; + bool hasFastTotalSize_; public: - TransactionBase(boost::mutex& mutex, + TransactionBase(boost::recursive_mutex& mutex, SQLite::Connection& db, IDatabaseListener& listener, - SignalRemainingAncestor& signalRemainingAncestor) : + SignalRemainingAncestor& signalRemainingAncestor, + bool hasFastTotalSize) : UnitTestsTransaction(db), lock_(mutex), listener_(listener), - signalRemainingAncestor_(signalRemainingAncestor) + signalRemainingAncestor_(signalRemainingAncestor), + hasFastTotalSize_(hasFastTotalSize) { } @@ -410,8 +415,9 @@ const FileInfo& attachment, int64_t revision) ORTHANC_OVERRIDE { - // TODO - REVISIONS - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5) VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO AttachedFiles (id, fileType, uuid, compressedSize, uncompressedSize, compressionType, uncompressedMD5, compressedMD5, revision, customData) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, attachment.GetContentType()); s.BindString(2, attachment.GetUuid()); @@ -420,10 +426,11 @@ s.BindInt(5, attachment.GetCompressionType()); s.BindString(6, attachment.GetUncompressedMD5()); s.BindString(7, attachment.GetCompressedMD5()); + s.BindInt(8, revision); + s.BindBlob(9, attachment.GetCustomData()); s.Run(); } - virtual void ApplyLookupResources(std::list<std::string>& resourcesId, std::list<std::string>* instancesId, const DatabaseDicomTagConstraints& lookup, @@ -473,10 +480,12 @@ #define C3_STRING_1 3 #define C4_STRING_2 4 #define C5_STRING_3 5 -#define C6_INT_1 6 -#define C7_INT_2 7 -#define C8_BIG_INT_1 8 -#define C9_BIG_INT_2 9 +#define C6_STRING_4 6 +#define C7_INT_1 7 +#define C8_INT_2 8 +#define C9_INT_3 9 +#define C10_BIG_INT_1 10 +#define C11_BIG_INT_2 11 #define QUERY_LOOKUP 1 #define QUERY_MAIN_DICOM_TAGS 2 @@ -525,6 +534,19 @@ } + static void ReadCustomData(FileInfo& info, + SQLite::Statement& statement, + int column) + { + std::string customData; + if (!statement.ColumnIsNull(column) && + statement.ColumnBlobAsString(column, &customData)) + { + info.SwapCustomData(customData); + } + } + + virtual void ExecuteFind(FindResponse& response, const FindRequest& request, const Capabilities& capabilities) ORTHANC_OVERRIDE @@ -588,10 +610,12 @@ " Lookup.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM Lookup "; // need one instance info ? (part 2: execute the queries) @@ -605,10 +629,12 @@ " instancePublicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " instanceInternalId AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " instanceInternalId AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM OneInstance "; sql += " UNION SELECT" @@ -618,10 +644,12 @@ " Metadata.value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " Metadata.type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " Metadata.type AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " " FROM OneInstance " " INNER JOIN Metadata ON Metadata.id = OneInstance.instanceInternalId "; @@ -632,10 +660,12 @@ " uuid AS c3_string1, " " uncompressedMD5 AS c4_string2, " " compressedMD5 AS c5_string3, " - " fileType AS c6_int1, " - " compressionType AS c7_int2, " - " compressedSize AS c8_big_int1, " - " uncompressedSize AS c9_big_int2 " + " customData AS c6_string4, " + " fileType AS c7_int1, " + " compressionType AS c8_int2, " + " revision AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " " FROM OneInstance " " INNER JOIN AttachedFiles ON AttachedFiles.id = OneInstance.instanceInternalId "; @@ -651,10 +681,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN MainDicomTags ON MainDicomTags.id = Lookup.internalId "; } @@ -669,10 +701,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Metadata ON Metadata.id = Lookup.internalId "; } @@ -687,10 +721,12 @@ " uuid AS c3_string1, " " uncompressedMD5 AS c4_string2, " " compressedMD5 AS c5_string3, " - " fileType AS c6_int1, " - " compressionType AS c7_int2, " - " compressedSize AS c8_big_int1, " - " uncompressedSize AS c9_big_int2 " + " customData AS c6_string4, " + " fileType AS c7_int1, " + " compressionType AS c8_int2, " + " revision AS c9_int3, " + " compressedSize AS c10_big_int1, " + " uncompressedSize AS c11_big_int2 " "FROM Lookup " "INNER JOIN AttachedFiles ON AttachedFiles.id = Lookup.internalId "; } @@ -706,10 +742,12 @@ " label AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Labels ON Labels.id = Lookup.internalId "; } @@ -726,10 +764,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN MainDicomTags ON MainDicomTags.id = currentLevel.parentId "; @@ -745,10 +785,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Metadata ON Metadata.id = currentLevel.parentId "; @@ -766,10 +808,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -786,10 +830,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources currentLevel ON Lookup.internalId = currentLevel.internalId " "INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId " @@ -808,10 +854,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN MainDicomTags ON MainDicomTags.id = childLevel.internalId AND " + JoinRequestedTags(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))); @@ -827,10 +875,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " tagGroup AS c6_int1, " - " tagElement AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " tagGroup AS c7_int1, " + " tagElement AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -847,10 +897,12 @@ " parentLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources currentLevel ON currentLevel.internalId = Lookup.internalId " " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId "; @@ -866,10 +918,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Metadata ON Metadata.id = childLevel.internalId AND Metadata.type IN (" + JoinRequestedMetadata(request.GetChildrenSpecification(static_cast<ResourceType>(requestLevel + 1))) + ") "; @@ -885,10 +939,12 @@ " value AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " type AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " type AS c7_int1, " + " revision AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON childLevel.parentId = Lookup.internalId " " INNER JOIN Resources grandChildLevel ON grandChildLevel.parentId = childLevel.internalId " @@ -907,10 +963,12 @@ " childLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId "; } @@ -926,10 +984,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " " INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId GROUP BY Lookup.internalId "; } @@ -945,10 +1005,12 @@ " grandChildLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId "; @@ -964,10 +1026,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId GROUP BY Lookup.internalId "; @@ -983,10 +1047,12 @@ " grandGrandChildLevel.publicId AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " NULL AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " NULL AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -1002,10 +1068,12 @@ " NULL AS c3_string1, " " NULL AS c4_string2, " " NULL AS c5_string3, " - " COUNT(*) AS c6_int1, " - " NULL AS c7_int2, " - " NULL AS c8_big_int1, " - " NULL AS c9_big_int2 " + " NULL AS c6_string4, " + " COUNT(*) AS c7_int1, " + " NULL AS c8_int2, " + " NULL AS c9_int3, " + " NULL AS c10_big_int1, " + " NULL AS c11_big_int2 " "FROM Lookup " "INNER JOIN Resources childLevel ON Lookup.internalId = childLevel.parentId " "INNER JOIN Resources grandChildLevel ON childLevel.internalId = grandChildLevel.parentId " @@ -1043,19 +1111,21 @@ case QUERY_ATTACHMENTS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), - s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), - static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), - s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); - res.AddAttachment(file, 0 /* TODO - REVISIONS */); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), + s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C8_INT_2)), + s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3)); + ReadCustomData(file, s, C6_STRING_4); + + res.AddAttachment(file, s.ColumnInt(C9_INT_3)); }; break; case QUERY_MAIN_DICOM_TAGS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(requestLevel, - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1063,8 +1133,8 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 1), - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1072,8 +1142,8 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddStringDicomTag(static_cast<ResourceType>(requestLevel - 2), - static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), - static_cast<uint16_t>(s.ColumnInt(C7_INT_2)), + static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), + static_cast<uint16_t>(s.ColumnInt(C8_INT_2)), s.ColumnString(C3_STRING_1)); }; break; @@ -1081,7 +1151,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 1), - DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))), s.ColumnString(C3_STRING_1)); }; break; @@ -1089,7 +1159,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMainDicomTagValue(static_cast<ResourceType>(requestLevel + 2), - DicomTag(static_cast<uint16_t>(s.ColumnInt(C6_INT_1)), static_cast<uint16_t>(s.ColumnInt(C7_INT_2))), + DicomTag(static_cast<uint16_t>(s.ColumnInt(C7_INT_1)), static_cast<uint16_t>(s.ColumnInt(C8_INT_2))), s.ColumnString(C3_STRING_1)); }; break; @@ -1097,31 +1167,31 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_PARENT_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel - 1), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_GRAND_PARENT_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddMetadata(static_cast<ResourceType>(requestLevel - 2), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), - s.ColumnString(C3_STRING_1), 0 /* no support for revision */); + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), + s.ColumnString(C3_STRING_1), s.ColumnInt(C8_INT_2)); }; break; case QUERY_CHILDREN_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 1), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; @@ -1129,7 +1199,7 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.AddChildrenMetadataValue(static_cast<ResourceType>(requestLevel + 2), - static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), + static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; @@ -1170,21 +1240,21 @@ { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 1), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_GRAND_CHILDREN_COUNT: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 2), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_GRAND_GRAND_CHILDREN_COUNT: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); res.SetChildrenCount(static_cast<ResourceType>(requestLevel + 3), - static_cast<uint64_t>(s.ColumnInt64(C6_INT_1))); + static_cast<uint64_t>(s.ColumnInt64(C7_INT_1))); }; break; case QUERY_ONE_INSTANCE_IDENTIFIER: @@ -1196,16 +1266,18 @@ case QUERY_ONE_INSTANCE_METADATA: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C6_INT_1)), s.ColumnString(C3_STRING_1)); + res.AddOneInstanceMetadata(static_cast<MetadataType>(s.ColumnInt(C7_INT_1)), s.ColumnString(C3_STRING_1)); }; break; case QUERY_ONE_INSTANCE_ATTACHMENTS: { FindResponse::Resource& res = response.GetResourceByInternalId(internalId); - FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C6_INT_1)), - s.ColumnInt64(C9_BIG_INT_2), s.ColumnString(C4_STRING_2), - static_cast<CompressionType>(s.ColumnInt(C7_INT_2)), - s.ColumnInt64(C8_BIG_INT_1), s.ColumnString(C5_STRING_3)); + FileInfo file(s.ColumnString(C3_STRING_1), static_cast<FileContentType>(s.ColumnInt(C7_INT_1)), + s.ColumnInt64(C11_BIG_INT_2), s.ColumnString(C4_STRING_2), + static_cast<CompressionType>(s.ColumnInt(C8_INT_2)), + s.ColumnInt64(C10_BIG_INT_1), s.ColumnString(C5_STRING_3)); + ReadCustomData(file, s, C6_STRING_4); + res.AddOneInstanceAttachment(file); }; break; @@ -1312,6 +1384,32 @@ } } + void DeleteDeletedFile(const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + s.Run(); + } + + void GetDeletedFileCustomData(std::string& customData, const std::string& uuid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT customData FROM DeletedFiles WHERE uuid=?"); + s.BindString(0, uuid); + + if (s.Step()) + { + if (s.ColumnIsNull(0) || + !s.ColumnBlobAsString(0, &customData)) + { + customData.clear(); + } + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } virtual void GetAllMetadata(std::map<MetadataType, std::string>& target, int64_t id) ORTHANC_OVERRIDE @@ -1597,23 +1695,39 @@ virtual uint64_t GetTotalCompressedSize() ORTHANC_OVERRIDE { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); + std::unique_ptr<SQLite::Statement> statement; + + if (hasFastTotalSize_) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0")); + } + else + { + // Old SQL query that was used in Orthanc <= 1.5.0: + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles")); + } + + statement->Run(); + return static_cast<uint64_t>(statement->ColumnInt64(0)); } virtual uint64_t GetTotalUncompressedSize() ORTHANC_OVERRIDE { - // Old SQL query that was used in Orthanc <= 1.5.0: - // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1"); - s.Run(); - return static_cast<uint64_t>(s.ColumnInt64(0)); + std::unique_ptr<SQLite::Statement> statement; + + if (hasFastTotalSize_) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1")); + } + else + { + // Old SQL query that was used in Orthanc <= 1.5.0: + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles")); + } + + statement->Run(); + return static_cast<uint64_t>(statement->ColumnInt64(0)); } @@ -1687,7 +1801,7 @@ { SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT uuid, uncompressedSize, compressionType, compressedSize, " - "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); + "uncompressedMD5, compressedMD5, revision, customData FROM AttachedFiles WHERE id=? AND fileType=?"); s.BindInt64(0, id); s.BindInt(1, contentType); @@ -1704,11 +1818,46 @@ static_cast<CompressionType>(s.ColumnInt(2)), s.ColumnInt64(3), s.ColumnString(5)); - revision = 0; // TODO - REVISIONS + ReadCustomData(attachment, s, 7); + revision = s.ColumnInt(6); return true; } } + virtual void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT customData FROM AttachedFiles WHERE uuid=?"); + s.BindString(0, attachmentUuid); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + if (s.ColumnIsNull(0)) + { + customData.clear(); + } + else if (!s.ColumnBlobAsString(0, &customData)) + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + virtual void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "UPDATE AttachedFiles SET customData=? WHERE uuid=?"); + s.BindBlob(0, customData, customDataSize); + s.BindString(1, attachmentUuid); + s.Run(); + } virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property, @@ -1739,7 +1888,7 @@ MetadataType type) ORTHANC_OVERRIDE { SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM Metadata WHERE id=? AND type=?"); + "SELECT value, revision FROM Metadata WHERE id=? AND type=?"); s.BindInt64(0, id); s.BindInt(1, type); @@ -1750,7 +1899,7 @@ else { target = s.ColumnString(0); - revision = 0; // TODO - REVISIONS + revision = s.ColumnInt(1); return true; } } @@ -1922,11 +2071,11 @@ const std::string& value, int64_t revision) ORTHANC_OVERRIDE { - // TODO - REVISIONS - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value) VALUES(?, ?, ?)"); + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata (id, type, value, revision) VALUES(?, ?, ?, ?)"); s.BindInt64(0, id); s.BindInt(1, type); s.BindString(2, value); + s.BindInt(3, revision); s.Run(); } @@ -2027,6 +2176,170 @@ target.insert(s.ColumnString(0)); } } + + virtual void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO KeyValueStores (storeId, key, value) VALUES(?, ?, ?)"); + s.BindString(0, storeId); + s.BindString(1, key); + s.BindBlob(2, value, valueSize); + s.Run(); + } + + virtual void DeleteKeyValue(const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM KeyValueStores WHERE storeId = ? AND key = ?"); + s.BindString(0, storeId); + s.BindString(1, key); + s.Run(); + } + + virtual bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM KeyValueStores WHERE storeId=? AND key=?"); + s.BindString(0, storeId); + s.BindString(1, key); + + if (!s.Step()) + { + // No value found + return false; + } + else + { + if (!s.ColumnBlobAsString(0, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + else + { + return true; + } + } + } + + // New in Orthanc 1.12.8 + virtual void ListKeysValues(std::list<std::string>& keys /* out */, + std::list<std::string>& values /* out */, + const std::string& storeId, + bool first, + const std::string& from /* only used if "first == false" */, + uint64_t limit) ORTHANC_OVERRIDE + { + int64_t actualLimit = limit; + if (limit == 0) + { + actualLimit = -1; // In SQLite, "if negative, there is no upper bound on the number of rows returned" + } + + std::unique_ptr<SQLite::Statement> statement; + + if (first) + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindInt64(1, actualLimit); + } + else + { + statement.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT key, value FROM KeyValueStores WHERE storeId=? AND key>? ORDER BY key ASC LIMIT ?")); + statement->BindString(0, storeId); + statement->BindString(1, from); + statement->BindInt64(2, actualLimit); + } + + while (statement->Step()) + { + std::string value; + if (!statement->ColumnBlobAsString(1, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + keys.push_back(statement->ColumnString(0)); + values.push_back(value); + } + } + + + // New in Orthanc 1.12.8 + virtual void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) ORTHANC_OVERRIDE + { + if (static_cast<size_t>(static_cast<int>(valueSize)) != valueSize) + { + throw OrthancException(ErrorCode_NotEnoughMemory, "Value is too large for a SQLite database"); + } + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO Queues (queueId, value) VALUES (?, ?)"); + s.BindString(0, queueId); + s.BindBlob(1, value, valueSize); + s.Run(); + } + + // New in Orthanc 1.12.8 + virtual bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) ORTHANC_OVERRIDE + { + int64_t rowId; + std::unique_ptr<SQLite::Statement> s; + + switch (origin) + { + case QueueOrigin_Front: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id ASC LIMIT 1")); + break; + + case QueueOrigin_Back: + s.reset(new SQLite::Statement(db_, SQLITE_FROM_HERE, "SELECT id, value FROM Queues WHERE queueId=? ORDER BY id DESC LIMIT 1")); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s->BindString(0, queueId); + if (!s->Step()) + { + // No value found + return false; + } + else + { + rowId = s->ColumnInt64(0); + + if (!s->ColumnBlobAsString(1, &value)) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + SQLite::Statement s2(db_, SQLITE_FROM_HERE, + "DELETE FROM Queues WHERE id = ?"); + s2.BindInt64(0, rowId); + s2.Run(); + + return true; + } + } + + // New in Orthanc 1.12.8 + virtual uint64_t GetQueueSize(const std::string& queueId) ORTHANC_OVERRIDE + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT COUNT(*) FROM Queues WHERE queueId=?"); + s.BindString(0, queueId); + s.Step(); + return s.ColumnInt64(0); + } }; @@ -2055,6 +2368,11 @@ { if (sqlite_.activeTransaction_ != NULL) { + std::string id = context.GetStringValue(0); + + std::string customData; + sqlite_.activeTransaction_->GetDeletedFileCustomData(customData, id); + std::string uncompressedMD5, compressedMD5; if (!context.IsNullValue(5)) @@ -2074,8 +2392,10 @@ static_cast<CompressionType>(context.GetIntValue(3)), static_cast<uint64_t>(context.GetInt64Value(4)), compressedMD5); + info.SwapCustomData(customData); sqlite_.activeTransaction_->GetListener().SignalAttachmentDeleted(info); + sqlite_.activeTransaction_->DeleteDeletedFile(id); } } }; @@ -2120,20 +2440,40 @@ SQLiteDatabaseWrapper& that_; std::unique_ptr<SQLite::Transaction> transaction_; int64_t initialDiskSize_; + bool isNested_; + + // Rationale for the isNested_ field: + // This was added while implementing the DelayedDeletion part of the advanced-storage plugin. + // When Orthanc deletes an attachment, a SQLite transaction is created to delete the attachment from + // the SQLite DB and, while the transaction is still active, the StorageRemove callback is called. + // The DelayedDeleter does not delete the file directly but, instead, it queues it for deletion. + // Queuing is done through the Orthanc SDK that creates a RW transaction (because it is a generic function). + // Since there is already an active RW transaction, this "nested" transaction does not need to perform anything + // in its Begin/Commit since this will be performed at higher level by the current activeTransaction_. + // However, in case of Rollback, this nested transaction must call the top level transaction Rollback. public: ReadWriteTransaction(SQLiteDatabaseWrapper& that, - IDatabaseListener& listener) : - TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), + IDatabaseListener& listener, + bool hasFastTotalSize) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize), that_(that), - transaction_(new SQLite::Transaction(that_.db_)) + transaction_(new SQLite::Transaction(that_.db_)), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + if (dynamic_cast<SQLiteDatabaseWrapper::ReadWriteTransaction*>(that_.activeTransaction_) == NULL) + { + throw OrthancException(ErrorCode_InternalError, "Unable to create a nested RW transaction, the current transaction is not a RW transaction"); + } + + isNested_ = true; } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } #if defined(NDEBUG) // Release mode @@ -2146,26 +2486,42 @@ virtual ~ReadWriteTransaction() { - assert(that_.activeTransaction_ != NULL); - that_.activeTransaction_ = NULL; + if (!isNested_) + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } } - void Begin() + virtual void Begin() { - transaction_->Begin(); + if (!isNested_) + { + transaction_->Begin(); + } } virtual void Rollback() ORTHANC_OVERRIDE { - transaction_->Rollback(); + if (isNested_) + { + that_.activeTransaction_->Rollback(); + } + else + { + transaction_->Rollback(); + } } virtual void Commit(int64_t fileSizeDelta /* only used in debug */) ORTHANC_OVERRIDE { - transaction_->Commit(); - - assert(initialDiskSize_ + fileSizeDelta >= 0 && - initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize())); + if (!isNested_) + { + transaction_->Commit(); + + assert(initialDiskSize_ + fileSizeDelta >= 0 && + initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(GetTotalCompressedSize())); + } } }; @@ -2174,25 +2530,34 @@ { private: SQLiteDatabaseWrapper& that_; + bool isNested_; // see explanation on the ReadWriteTransaction public: ReadOnlyTransaction(SQLiteDatabaseWrapper& that, - IDatabaseListener& listener) : - TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_), - that_(that) + IDatabaseListener& listener, + bool hasFastTotalSize) : + TransactionBase(that.mutex_, that.db_, listener, *that.signalRemainingAncestor_, hasFastTotalSize), + that_(that), + isNested_(false) { if (that_.activeTransaction_ != NULL) { - throw OrthancException(ErrorCode_InternalError); + isNested_ = true; + // throw OrthancException(ErrorCode_InternalError); } - - that_.activeTransaction_ = this; + else + { + that_.activeTransaction_ = this; + } } virtual ~ReadOnlyTransaction() { - assert(that_.activeTransaction_ != NULL); - that_.activeTransaction_ = NULL; + if (!isNested_) + { + assert(that_.activeTransaction_ != NULL); + that_.activeTransaction_ = NULL; + } } virtual void Rollback() ORTHANC_OVERRIDE @@ -2214,11 +2579,14 @@ signalRemainingAncestor_(NULL), version_(0) { - // TODO: implement revisions in SQLite + dbCapabilities_.SetRevisionsSupport(true); dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); dbCapabilities_.SetHasExtendedChanges(true); dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); + dbCapabilities_.SetKeyValueStoresSupport(true); + dbCapabilities_.SetQueuesSupport(true); + dbCapabilities_.SetAttachmentCustomDataSupport(true); db_.Open(path); } @@ -2228,11 +2596,14 @@ signalRemainingAncestor_(NULL), version_(0) { - // TODO: implement revisions in SQLite + dbCapabilities_.SetRevisionsSupport(true); dbCapabilities_.SetFlushToDisk(true); dbCapabilities_.SetLabelsSupport(true); dbCapabilities_.SetHasExtendedChanges(true); dbCapabilities_.SetHasFindSupport(HasIntegratedFind()); + dbCapabilities_.SetKeyValueStoresSupport(true); + dbCapabilities_.SetQueuesSupport(true); + dbCapabilities_.SetAttachmentCustomDataSupport(true); db_.OpenInMemory(); } @@ -2245,10 +2616,29 @@ } + static void ExecuteEmbeddedScript(SQLite::Connection& db, + ServerResources::FileResourceId resourceId) + { + std::string script; + ServerResources::GetFileResource(script, resourceId); + db.Execute(script); + } + + + static void InjectEmbeddedScript(std::string& sql, + const std::string& name, + ServerResources::FileResourceId resourceId) + { + std::string script; + ServerResources::GetFileResource(script, resourceId); + boost::replace_all(sql, name, script); + } + + void SQLiteDatabaseWrapper::Open() { { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (signalRemainingAncestor_ != NULL) { @@ -2283,6 +2673,12 @@ LOG(INFO) << "Creating the database"; std::string query; ServerResources::GetFileResource(query, ServerResources::PREPARE_DATABASE); + + InjectEmbeddedScript(query, "${INSTALL_TRACK_ATTACHMENTS_SIZE}", ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); + InjectEmbeddedScript(query, "${INSTALL_LABELS_TABLE}", ServerResources::INSTALL_LABELS_TABLE); + InjectEmbeddedScript(query, "${INSTALL_DELETED_FILES}", ServerResources::INSTALL_DELETED_FILES); + InjectEmbeddedScript(query, "${INSTALL_KEY_VALUE_STORES_AND_QUEUES}", ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES); + db_.Execute(query); } @@ -2317,18 +2713,35 @@ tmp != "1") { LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments"; - std::string query; - ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); - db_.Execute(query); + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE); } // New in Orthanc 1.12.0 if (!db_.DoesTableExist("Labels")) { LOG(INFO) << "Installing the \"Labels\" table"; - std::string query; - ServerResources::GetFileResource(query, ServerResources::INSTALL_LABELS_TABLE); - db_.Execute(query); + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_LABELS_TABLE); + } + + // New in Orthanc 1.12.8 + if (!transaction->LookupGlobalProperty(tmp, GlobalProperty_SQLiteHasRevisionAndCustomData, true /* unused in SQLite */) + || tmp != "1") + { + LOG(INFO) << "Upgrading SQLite schema to support revision and customData"; + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + } + + // New in Orthanc 1.12.8 + if (!db_.DoesTableExist("DeletedFiles")) + { + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_DELETED_FILES); + } + + // New in Orthanc 1.12.8 + if (!db_.DoesTableExist("KeyValueStores")) + { + LOG(INFO) << "Installing the \"KeyValueStores\" and \"Queues\" tables"; + ExecuteEmbeddedScript(db_, ServerResources::INSTALL_KEY_VALUE_STORES_AND_QUEUES); } } @@ -2339,7 +2752,7 @@ void SQLiteDatabaseWrapper::Close() { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); // close and delete the WAL when exiting properly -> the DB is stored in a single file (no more -wal and -shm files) db_.Execute("PRAGMA JOURNAL_MODE=DELETE;"); db_.Close(); @@ -2358,9 +2771,9 @@ void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); if (targetVersion != 6) { @@ -2401,33 +2814,59 @@ VoidDatabaseListener listener; { - std::unique_ptr<ITransaction> transaction(StartTransaction(TransactionType_ReadWrite, listener)); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Patient); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Study); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Series); - ServerToolbox::ReconstructMainDicomTags(*transaction, storageArea, ResourceType_Instance); + ReadWriteTransaction transaction(*this, listener, false /* GetTotalSizeIsFast necessitates the table "GlobalIntegers" */); + transaction.Begin(); + + // ReconstructMaindDicomTags uses LookupAttachment that needs revision and customData. Since we don't want to maintain a legacy version + // of LookupAttachment, we modify the table now) + LOG(INFO) << "First upgrading SQLite schema to support revision and customData to be able to reconstruct main DICOM tags"; + std::string query; + ServerResources::GetFileResource(query, ServerResources::INSTALL_REVISION_AND_CUSTOM_DATA); + db_.Execute(query); + + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Patient); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Study); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Series); + ServerToolbox::ReconstructMainDicomTags(transaction, storageArea, ResourceType_Instance); db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + boost::lexical_cast<std::string>(GlobalProperty_DatabaseSchemaVersion) + ";"); - transaction->Commit(0); + transaction.Commit(0); } version_ = 6; } + } + // class RaiiTransactionLogger + // { + // TransactionType type_; + // public: + // RaiiTransactionLogger(TransactionType type) + // : type_(type) + // { + // LOG(INFO) << "IN " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // ~RaiiTransactionLogger() + // { + // LOG(INFO) << "OUT " << (type_ == TransactionType_ReadOnly ? "RO" : "RW"); + // } + // }; IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction(TransactionType type, IDatabaseListener& listener) { + // RaiiTransactionLogger logger(type); + switch (type) { case TransactionType_ReadOnly: - return new ReadOnlyTransaction(*this, listener); // This is a no-op transaction in SQLite (thanks to mutex) + return new ReadOnlyTransaction(*this, listener, true); // This is a no-op transaction in SQLite (thanks to mutex) case TransactionType_ReadWrite: { std::unique_ptr<ReadWriteTransaction> transaction; - transaction.reset(new ReadWriteTransaction(*this, listener)); + transaction.reset(new ReadWriteTransaction(*this, listener, true)); transaction->Begin(); return transaction.release(); } @@ -2440,7 +2879,7 @@ void SQLiteDatabaseWrapper::FlushToDisk() { - boost::mutex::scoped_lock lock(mutex_); + boost::recursive_mutex::scoped_lock lock(mutex_); db_.FlushToDisk(); }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Tue Nov 04 15:58:06 2025 +0100 @@ -27,7 +27,7 @@ #include "../../../OrthancFramework/Sources/SQLite/Connection.h" -#include <boost/thread/mutex.hpp> +#include <boost/thread/recursive_mutex.hpp> namespace Orthanc { @@ -47,7 +47,7 @@ class ReadWriteTransaction; class LookupFormatter; - boost::mutex mutex_; + boost::recursive_mutex mutex_; SQLite::Connection db_; TransactionBase* activeTransaction_; SignalRemainingAncestor* signalRemainingAncestor_; @@ -88,7 +88,7 @@ } virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) ORTHANC_OVERRIDE; + IPluginStorageArea& storageArea) ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE {
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -1394,28 +1394,36 @@ } - bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type, - const std::string& publicId) + bool StatelessDatabaseOperations::LookupResource(int64_t& internalId, + ResourceType& type, + const std::string& publicId) { - class Operations : public ReadOnlyOperationsT3<bool&, ResourceType&, const std::string&> + class Operations : public ReadOnlyOperationsT4<bool&, int64_t&, ResourceType&, const std::string&> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { // TODO - CANDIDATE FOR "TransactionType_Implicit" - int64_t id; - tuple.get<0>() = transaction.LookupResource(id, tuple.get<1>(), tuple.get<2>()); + tuple.get<0>() = transaction.LookupResource(tuple.get<1>(), tuple.get<2>(), tuple.get<3>()); } }; bool found; Operations operations; - operations.Apply(*this, found, type, publicId); + operations.Apply(*this, found, internalId, type, publicId); return found; } + bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type, + const std::string& publicId) + { + int64_t internalId; + return LookupResource(internalId, type, publicId); + } + + bool StatelessDatabaseOperations::LookupParent(std::string& target, const std::string& publicId, ResourceType parentType) @@ -3200,6 +3208,24 @@ return db_.GetDatabaseCapabilities().HasFindSupport(); } + bool StatelessDatabaseOperations::HasAttachmentCustomDataSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasAttachmentCustomDataSupport(); + } + + bool StatelessDatabaseOperations::HasKeyValueStoresSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasKeyValueStoresSupport(); + } + + bool StatelessDatabaseOperations::HasQueuesSupport() + { + boost::shared_lock<boost::shared_mutex> lock(mutex_); + return db_.GetDatabaseCapabilities().HasQueuesSupport(); + } + void StatelessDatabaseOperations::ExecuteCount(uint64_t& count, const FindRequest& request) { @@ -3320,4 +3346,413 @@ } } } + + void StatelessDatabaseOperations::StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value == NULL && + valueSize > 0) + { + throw OrthancException(ErrorCode_NullPointer); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + const void* value_; + size_t valueSize_; + + public: + Operations(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) : + storeId_(storeId), + key_(key), + value_(value), + valueSize_(valueSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.StoreKeyValue(storeId_, key_, value_, valueSize_); + } + }; + + Operations operations(storeId, key, value, valueSize); + Apply(operations); + } + + void StatelessDatabaseOperations::DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& storeId_; + const std::string& key_; + + public: + Operations(const std::string& storeId, + const std::string& key) : + storeId_(storeId), + key_(key) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.DeleteKeyValue(storeId_, key_); + } + }; + + Operations operations(storeId, key); + Apply(operations); + } + + bool StatelessDatabaseOperations::GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public ReadOnlyOperationsT3<std::string&, const std::string&, const std::string& > + { + bool found_; + public: + Operations(): + found_(false) + {} + + bool HasFound() + { + return found_; + } + + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + found_ = transaction.GetKeyValue(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + Operations operations; + operations.Apply(*this, value, storeId, key); + + return operations.HasFound(); + } + + void StatelessDatabaseOperations::EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (value == NULL && + valueSize > 0) + { + throw OrthancException(ErrorCode_NullPointer); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + const void* value_; + size_t valueSize_; + + public: + Operations(const std::string& queueId, + const void* value, + size_t valueSize) : + queueId_(queueId), + value_(value), + valueSize_(valueSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.EnqueueValue(queueId_, value_, valueSize_); + } + }; + + Operations operations(queueId, value, valueSize); + Apply(operations); + } + + bool StatelessDatabaseOperations::DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public IReadWriteOperations + { + private: + const std::string& queueId_; + std::string& value_; + QueueOrigin origin_; + bool found_; + + public: + Operations(std::string& value, + const std::string& queueId, + QueueOrigin origin) : + queueId_(queueId), + value_(value), + origin_(origin), + found_(false) + { + } + + bool HasFound() + { + return found_; + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + found_ = transaction.DequeueValue(value_, queueId_, origin_); + } + }; + + Operations operations(value, queueId, origin); + Apply(operations); + + return operations.HasFound(); + } + + uint64_t StatelessDatabaseOperations::GetQueueSize(const std::string& queueId) + { + if (queueId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + class Operations : public ReadOnlyOperationsT2<uint64_t&, const std::string& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + tuple.get<0>() = transaction.GetQueueSize(tuple.get<1>()); + } + }; + + uint64_t size; + + Operations operations; + operations.Apply(*this, size, queueId); + + return size; + } + + + void StatelessDatabaseOperations::GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) + { + class Operations : public ReadOnlyOperationsT2<std::string&, const std::string& > + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.GetAttachmentCustomData(tuple.get<0>(), tuple.get<1>()); + } + }; + + Operations operations; + operations.Apply(*this, customData, attachmentUuid); + } + + + void StatelessDatabaseOperations::SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) + { + class Operations : public IReadWriteOperations + { + private: + const std::string& attachmentUuid_; + const void* customData_; + size_t customDataSize_; + + public: + Operations(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) : + attachmentUuid_(attachmentUuid), + customData_(customData), + customDataSize_(customDataSize) + { + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.SetAttachmentCustomData(attachmentUuid_, customData_, customDataSize_); + } + }; + + Operations operations(attachmentUuid, customData, customDataSize); + Apply(operations); + } + + + StatelessDatabaseOperations::KeysValuesIterator::KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId) : + db_(db), + state_(State_Waiting), + storeId_(storeId), + limit_(100) + { + if (storeId.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + bool StatelessDatabaseOperations::KeysValuesIterator::Next() + { + if (state_ == State_Done) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (state_ == State_Available) + { + assert(currentKey_ != keys_.end()); + assert(currentValue_ != values_.end()); + ++currentKey_; + ++currentValue_; + + if (currentKey_ != keys_.end() && + currentValue_ != values_.end()) + { + // A value is still available in the last keys-values block fetched from the database + return true; + } + else if (currentKey_ != keys_.end() || + currentValue_ != values_.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + class Operations : public ReadOnlyOperationsT6<std::list<std::string>&, std::list<std::string>&, const std::string&, bool, const std::string&, uint64_t> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ListKeysValues(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>(), tuple.get<4>(), tuple.get<5>()); + } + }; + + if (state_ == State_Waiting) + { + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, true, "", limit_); + } + else + { + assert(state_ == State_Available); + if (keys_.empty()) + { + state_ = State_Done; + return false; + } + else + { + const std::string lastKey = keys_.back(); + keys_.clear(); + values_.clear(); + + Operations operations; + operations.Apply(db_, keys_, values_, storeId_, false, lastKey, limit_); + } + } + + if (keys_.size() != values_.size()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (limit_ != 0 && + keys_.size() > limit_) + { + // The database plugin has returned too many key-value pairs + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (keys_.empty() && + values_.empty()) + { + state_ = State_Done; + return false; + } + else if (!keys_.empty() && + !values_.empty()) + { + state_ = State_Available; + currentKey_ = keys_.begin(); + currentValue_ = values_.begin(); + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); // Should never happen + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetKey() const + { + if (state_ == State_Available) + { + return *currentKey_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + const std::string &StatelessDatabaseOperations::KeysValuesIterator::GetValue() const + { + if (state_ == State_Available) + { + return *currentValue_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Nov 04 15:58:06 2025 +0100 @@ -226,6 +226,12 @@ return transaction_.LookupAttachment(attachment, revision, id, contentType); } + void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid) + { + return transaction_.GetAttachmentCustomData(customData, attachmentUuid); + } + bool LookupGlobalProperty(std::string& target, GlobalProperty property, bool shared) @@ -293,6 +299,28 @@ { transaction_.ExecuteExpand(response, capabilities, request, identifier); } + + bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key) + { + return transaction_.GetKeyValue(value, storeId, key); + } + + uint64_t GetQueueSize(const std::string& queueId) + { + return transaction_.GetQueueSize(queueId); + } + + void ListKeysValues(std::list<std::string>& keys, + std::list<std::string>& values, + const std::string& storeId, + bool first, + const std::string& from, + uint64_t limit) + { + return transaction_.ListKeysValues(keys, values, storeId, first, from, limit); + } }; @@ -428,6 +456,42 @@ { transaction_.RemoveLabel(id, label); } + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize) + { + transaction_.StoreKeyValue(storeId, key, value, valueSize); + } + + void DeleteKeyValue(const std::string& storeId, + const std::string& key) + { + transaction_.DeleteKeyValue(storeId, key); + } + + void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize) + { + transaction_.EnqueueValue(queueId, value, valueSize); + } + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin) + { + return transaction_.DequeueValue(value, queueId, origin); + } + + void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize) + { + return transaction_.SetAttachmentCustomData(attachmentUuid, customData, customDataSize); + } + }; @@ -523,6 +587,13 @@ /* out */ uint64_t& countSeries, /* out */ uint64_t& countInstances); + void GetAttachmentCustomData(std::string& customData, + const std::string& attachmentUuid); + + void SetAttachmentCustomData(const std::string& attachmentUuid, + const void* customData, + size_t customDataSize); + bool LookupAttachment(FileInfo& attachment, int64_t& revision, ResourceType level, @@ -544,7 +615,13 @@ bool HasExtendedChanges(); bool HasFindSupport(); - + + bool HasAttachmentCustomDataSupport(); + + bool HasKeyValueStoresSupport(); + + bool HasQueuesSupport(); + void GetExportedResources(Json::Value& target, int64_t since, uint32_t limit); @@ -615,6 +692,10 @@ bool GetAllMainDicomTags(DicomMap& result, const std::string& instancePublicId); + bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId); + bool LookupResourceType(ResourceType& type, const std::string& publicId); @@ -724,5 +805,81 @@ void ExecuteCount(uint64_t& count, const FindRequest& request); + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const void* value, + size_t valueSize); + + void StoreKeyValue(const std::string& storeId, + const std::string& key, + const std::string& value) + { + StoreKeyValue(storeId, key, value.empty() ? NULL : value.c_str(), value.size()); + } + + void DeleteKeyValue(const std::string& storeId, + const std::string& key); + + bool GetKeyValue(std::string& value, + const std::string& storeId, + const std::string& key); + + void EnqueueValue(const std::string& queueId, + const void* value, + size_t valueSize); + + void EnqueueValue(const std::string& queueId, + const std::string& value) + { + EnqueueValue(queueId, value.empty() ? NULL : value.c_str(), value.size()); + } + + bool DequeueValue(std::string& value, + const std::string& queueId, + QueueOrigin origin); + + uint64_t GetQueueSize(const std::string& queueId); + + class KeysValuesIterator : public boost::noncopyable + { + private: + enum State + { + State_Waiting, + State_Available, + State_Done + }; + + StatelessDatabaseOperations& db_; + State state_; + std::string storeId_; + uint64_t limit_; + std::list<std::string> keys_; + std::list<std::string> values_; + std::list<std::string>::const_iterator currentKey_; + std::list<std::string>::const_iterator currentValue_; + + public: + KeysValuesIterator(StatelessDatabaseOperations& db, + const std::string& storeId); + + void SetLimit(uint64_t limit) + { + limit_ = limit; + } + + uint64_t GetLimit() const + { + return limit_; + } + + bool Next(); + + const std::string& GetKey() const; + + const std::string& GetValue() const; + }; + }; }
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -51,7 +51,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) + size_t /*bodySize*/, + const std::string& /*authenticationPayload*/) { if (!Toolbox::IsChildUri(baseUri_, uri)) { @@ -66,7 +67,7 @@ } std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size()); - MimeType contentType = SystemToolbox::AutodetectMimeType(resourcePath); + MimeType contentType = SystemToolbox::AutodetectMimeType(Orthanc::SystemToolbox::PathFromUtf8(resourcePath)); try {
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.h Tue Nov 04 15:58:06 2025 +0100 @@ -47,7 +47,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE { return false; } @@ -61,6 +62,7 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& arguments, const void* /*bodyData*/, - size_t /*bodySize*/) ORTHANC_OVERRIDE; + size_t /*bodySize*/, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/LuaScripting.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/LuaScripting.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -552,7 +552,8 @@ try { - if (IHttpHandler::SimpleDelete(NULL, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), + std::string bodyIgnored; + if (IHttpHandler::SimpleDelete(bodyIgnored, NULL, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), RequestOrigin_Lua, uri, headers) == HttpStatus_200_Ok) { lua_pushboolean(state, 1); @@ -570,6 +571,60 @@ return 1; } + // Syntax in Lua: SetStableStatus(resourceId, true) + int LuaScripting::SetStableStatus(lua_State* state) + { + ServerContext* serverContext = GetServerContext(state); + if (serverContext == NULL) + { + LOG(ERROR) << "Lua: The Orthanc API is unavailable"; + lua_pushnil(state); + return 1; + } + + // Check the types of the arguments + int nArgs = lua_gettop(state); + if (nArgs < 1 || nArgs > 3 || + !lua_isstring(state, 1) || // Resource + !lua_isboolean(state, 2)) // newStateIsStable + { + LOG(ERROR) << "Lua: Bad parameters to SetStableStatus()"; + lua_pushnil(state); + return 1; + } + + const char* resourceId = lua_tostring(state, 1); + bool newStateIsStable = lua_toboolean(state, 2); + + try + { + bool hasStateChanged = false; + + if (serverContext->GetIndex().SetStableStatus(hasStateChanged, resourceId, newStateIsStable)) + { + if (hasStateChanged) + { + lua_pushboolean(state, 1); + } + else + { + lua_pushboolean(state, 0); + } + + return 1; + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Lua: " << e.What(); + } + + LOG(ERROR) << "Lua: Error in SetStableStatus() for Resource: " << resourceId; + lua_pushnil(state); + + return 1; + } + // Syntax in Lua: GetOrthancConfiguration() int LuaScripting::GetOrthancConfiguration(lua_State *state) @@ -760,6 +815,7 @@ lua_.RegisterFunction("RestApiPut", RestApiPut); lua_.RegisterFunction("RestApiDelete", RestApiDelete); lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration); + lua_.RegisterFunction("SetStableStatus", SetStableStatus); LOG(INFO) << "Initializing Lua for the event handler"; LoadGlobalConfiguration(); @@ -1040,8 +1096,8 @@ for (std::list<std::string>::const_iterator it = luaScripts.begin(); it != luaScripts.end(); ++it) { - std::string path = configLock.GetConfiguration().InterpretStringParameterAsPath(*it); - LOG(INFO) << "Installing the Lua scripts from: " << path; + boost::filesystem::path path = configLock.GetConfiguration().InterpretStringParameterAsPath(*it); + LOG(INFO) << "Installing the Lua scripts from: " << SystemToolbox::PathToUtf8(path); std::string script; SystemToolbox::ReadFile(script, path);
--- a/OrthancServer/Sources/LuaScripting.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/LuaScripting.h Tue Nov 04 15:58:06 2025 +0100 @@ -62,6 +62,7 @@ static int RestApiPut(lua_State *state); static int RestApiDelete(lua_State *state); static int GetOrthancConfiguration(lua_State *state); + static int SetStableStatus(lua_State* state); size_t ParseOperation(LuaJobManager::Lock& lock, const std::string& operation,
--- a/OrthancServer/Sources/OrthancConfiguration.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -56,13 +56,13 @@ std::map<std::string, std::string> env; SystemToolbox::GetEnvironmentVariables(env); - LOG(WARNING) << "Reading the configuration from: " << path; + LOG(WARNING) << "Reading the configuration from: " << SystemToolbox::PathToUtf8(path); Json::Value config; { std::string content; - SystemToolbox::ReadFile(content, path.string()); + SystemToolbox::ReadFile(content, path); content = Toolbox::SubstituteVariables(content, env); @@ -71,7 +71,7 @@ tmp.type() != Json::objectValue) { throw OrthancException(ErrorCode_BadJson, - "The configuration file does not follow the JSON syntax: " + path.string()); + "The configuration file does not follow the JSON syntax: " + SystemToolbox::PathToUtf8(path)); } Toolbox::CopyJsonWithoutComments(config, tmp); @@ -103,11 +103,11 @@ static void ScanFolderForConfiguration(Json::Value& target, - const char* folder) + const boost::filesystem::path& folder) { using namespace boost::filesystem; - LOG(WARNING) << "Scanning folder \"" << folder << "\" for configuration files"; + LOG(WARNING) << "Scanning folder \"" << Orthanc::SystemToolbox::PathToUtf8(folder) << "\" for configuration files"; directory_iterator end_it; // default construction yields past-the-end for (directory_iterator it(folder); @@ -121,25 +121,25 @@ if (extension == ".json") { - AddFileToConfiguration(target, it->path().string()); + AddFileToConfiguration(target, it->path()); } } } } - static void ReadConfiguration(Json::Value& target, - const char* configurationFile) + static void ReadConfiguration(Json::Value& target, + const boost::filesystem::path &configurationFile) { target = Json::objectValue; - if (configurationFile != NULL) + if (!configurationFile.empty()) { if (!boost::filesystem::exists(configurationFile)) { throw OrthancException(ErrorCode_InexistentFile, "Inexistent path to configuration: " + - std::string(configurationFile)); + SystemToolbox::PathToUtf8(configurationFile)); } if (boost::filesystem::is_directory(configurationFile)) @@ -583,7 +583,7 @@ } - void OrthancConfiguration::Read(const char* configurationFile) + void OrthancConfiguration::Read(const boost::filesystem::path& configurationFile) { // Read the content of the configuration configurationFileArg_ = configurationFile; @@ -593,11 +593,11 @@ defaultDirectory_ = boost::filesystem::current_path(); configurationAbsolutePath_ = ""; - if (configurationFile) + if (configurationFile.empty()) { if (boost::filesystem::is_directory(configurationFile)) { - defaultDirectory_ = boost::filesystem::path(configurationFile); + defaultDirectory_ = configurationFile; configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string(); } else @@ -707,39 +707,53 @@ } - bool OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const + OrthancConfiguration::RegisteredUsersStatus OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const { + static const char* const REGISTERED_USERS = "RegisteredUsers"; + httpServer.ClearUsers(); - if (!json_.isMember("RegisteredUsers")) + if (!json_.isMember(REGISTERED_USERS)) { - return false; + return RegisteredUsersStatus_NoConfiguration; } - - const Json::Value& users = json_["RegisteredUsers"]; - if (users.type() != Json::objectValue) + else { - throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); - } + const Json::Value& users = json_[REGISTERED_USERS]; + if (users.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); + } - bool hasUser = false; - Json::Value::Members usernames = users.getMemberNames(); - for (size_t i = 0; i < usernames.size(); i++) - { - const std::string& username = usernames[i]; - std::string password = users[username].asString(); - httpServer.RegisterUser(username.c_str(), password.c_str()); - hasUser = true; + bool hasUser = false; + Json::Value::Members usernames = users.getMemberNames(); + for (size_t i = 0; i < usernames.size(); i++) + { + const std::string& username = usernames[i]; + + if (users[username].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); + } + else + { + std::string password = users[username].asString(); + httpServer.RegisterUser(username.c_str(), password.c_str()); + hasUser = true; + } + } + + return (hasUser ? + RegisteredUsersStatus_HasUser : + RegisteredUsersStatus_NoUser); } - - return hasUser; } - std::string OrthancConfiguration::InterpretStringParameterAsPath( + boost::filesystem::path OrthancConfiguration::InterpretStringParameterAsPath( const std::string& parameter) const { - return SystemToolbox::InterpretRelativePath(defaultDirectory_.string(), parameter); + return SystemToolbox::InterpretRelativePath(defaultDirectory_, parameter); }
--- a/OrthancServer/Sources/OrthancConfiguration.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.h Tue Nov 04 15:58:06 2025 +0100 @@ -49,6 +49,14 @@ class OrthancConfiguration : public boost::noncopyable { + public: + enum RegisteredUsersStatus + { + RegisteredUsersStatus_NoConfiguration, // There is no "RegisteredUsers" section in the configuration file + RegisteredUsersStatus_NoUser, // The "RegisteredUsers" section is present, but declares no user + RegisteredUsersStatus_HasUser // The "RegisteredUsers" section is present and contains at least 1 user + }; + private: typedef std::map<std::string, RemoteModalityParameters> Modalities; typedef std::map<std::string, WebServiceParameters> Peers; @@ -59,7 +67,7 @@ boost::filesystem::path defaultDirectory_; std::string configurationAbsolutePath_; FontRegistry fontRegistry_; - const char* configurationFileArg_; + boost::filesystem::path configurationFileArg_; Modalities modalities_; Peers peers_; JobsEngineThreadsCount jobsEngineThreadsCount_; @@ -67,7 +75,6 @@ std::set<Warnings> disabledWarnings_; OrthancConfiguration() : - configurationFileArg_(NULL), serverIndex_(NULL) { } @@ -156,7 +163,7 @@ return fontRegistry_; } - void Read(const char* configurationFile); + void Read(const boost::filesystem::path &configurationFile); // "SetServerIndex()" must have been called void LoadModalitiesAndPeers(); @@ -198,11 +205,10 @@ void GetListOfOrthancPeers(std::set<std::string>& target) const; unsigned int GetDicomLossyTranscodingQuality() const; - - // Returns "true" iff. at least one user is registered - bool SetupRegisteredUsers(HttpServer& httpServer) const; - std::string InterpretStringParameterAsPath(const std::string& parameter) const; + RegisteredUsersStatus SetupRegisteredUsers(HttpServer& httpServer) const; + + boost::filesystem::path InterpretStringParameterAsPath(const std::string& parameter) const; void GetListOfStringsParameter(std::list<std::string>& target, const std::string& key) const;
--- a/OrthancServer/Sources/OrthancHttpHandler.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancHttpHandler.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -36,7 +36,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) { if (method != HttpMethod_Post && method != HttpMethod_Put) @@ -47,7 +48,7 @@ for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) { if ((*it)->CreateChunkedRequestReader - (target, origin, remoteIp, username, method, uri, headers)) + (target, origin, remoteIp, username, method, uri, headers, authenticationPayload)) { if (target.get() == NULL) { @@ -71,12 +72,13 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) { if ((*it)->Handle(output, origin, remoteIp, username, method, uri, - headers, getArguments, bodyData, bodySize)) + headers, getArguments, bodyData, bodySize, authenticationPayload)) { return true; }
--- a/OrthancServer/Sources/OrthancHttpHandler.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancHttpHandler.h Tue Nov 04 15:58:06 2025 +0100 @@ -46,7 +46,8 @@ const char* username, HttpMethod method, const UriComponents& uri, - const HttpToolbox::Arguments& headers) ORTHANC_OVERRIDE; + const HttpToolbox::Arguments& headers, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; virtual bool Handle(HttpOutput& output, RequestOrigin origin, @@ -57,7 +58,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; void Register(IHttpHandler& handler, bool isOrthancRestApi);
--- a/OrthancServer/Sources/OrthancInitialization.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -56,6 +56,7 @@ #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/HttpClient.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/OrthancException.h" @@ -333,7 +334,7 @@ - void OrthancInitialize(const char* configurationFile) + void OrthancInitialize(const boost::filesystem::path& configurationFile) { static const char* const LOCALE = "Locale"; static const char* const PKCS11 = "Pkcs11"; @@ -441,7 +442,7 @@ boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath( lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr)); - LOG(WARNING) << "SQLite index directory: " << indexDirectory; + LOG(WARNING) << "SQLite index directory: " << SystemToolbox::PathToUtf8(indexDirectory); try { @@ -451,7 +452,7 @@ { } - return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index"); + return new SQLiteDatabaseWrapper(Orthanc::SystemToolbox::PathToUtf8(indexDirectory) + "/index"); } @@ -465,7 +466,7 @@ FilesystemStorage storage_; public: - FilesystemStorageWithoutDicom(const std::string& path, + FilesystemStorageWithoutDicom(const boost::filesystem::path& path, bool fsyncOnWrite) : storage_(path, fsyncOnWrite) { @@ -482,19 +483,6 @@ } } - virtual IMemoryBuffer* Read(const std::string& uuid, - FileContentType type) ORTHANC_OVERRIDE - { - if (type != FileContentType_Dicom) - { - return storage_.Read(uuid, type); - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - virtual IMemoryBuffer* ReadRange(const std::string& uuid, FileContentType type, uint64_t start /* inclusive */, @@ -510,9 +498,9 @@ } } - virtual bool HasReadRange() const ORTHANC_OVERRIDE + virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE { - return storage_.HasReadRange(); + return storage_.HasEfficientReadRange(); } virtual void Remove(const std::string& uuid, @@ -527,7 +515,7 @@ } - static IStorageArea* CreateFilesystemStorage() + static IPluginStorageArea* CreateFilesystemStorage() { static const char* const SYNC_STORAGE_AREA = "SyncStorageArea"; static const char* const STORE_DICOM = "StoreDicom"; @@ -540,19 +528,19 @@ boost::filesystem::path storageDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr); - LOG(WARNING) << "Storage directory: " << storageDirectory; + LOG(WARNING) << "Storage directory: " << SystemToolbox::PathToUtf8(storageDirectory); // New in Orthanc 1.7.4 bool fsyncOnWrite = lock.GetConfiguration().GetBooleanParameter(SYNC_STORAGE_AREA, true); if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true)) { - return new FilesystemStorage(storageDirectory.string(), fsyncOnWrite); + return new PluginStorageAreaAdapter(new FilesystemStorage(storageDirectory, fsyncOnWrite)); } else { LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode"; - return new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite); + return new PluginStorageAreaAdapter(new FilesystemStorageWithoutDicom(storageDirectory, fsyncOnWrite)); } } @@ -563,7 +551,7 @@ } - IStorageArea* CreateStorageArea() + IPluginStorageArea* CreateStorageArea() { return CreateFilesystemStorage(); }
--- a/OrthancServer/Sources/OrthancInitialization.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.h Tue Nov 04 15:58:06 2025 +0100 @@ -29,13 +29,13 @@ namespace Orthanc { - void OrthancInitialize(const char* configurationFile = NULL); + void OrthancInitialize(const boost::filesystem::path& configurationFile); void OrthancFinalize(); IDatabaseWrapper* CreateDatabaseWrapper(); - IStorageArea* CreateStorageArea(); + IPluginStorageArea* CreateStorageArea(); void SetGlobalVerbosity(Verbosity verbosity);
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -109,8 +109,8 @@ } std::string sopClassUid, sopInstanceUid; // Unused - context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom, - true, originatorAet_, originatorId_); + context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom, + true, originatorAet_, originatorId_); return Status_Success; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -297,13 +297,14 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) + size_t bodySize, + const std::string& authenticationPayload) { MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_rest_api_duration_ms"); MetricsRegistry::ActiveCounter counter(activeRequests_); return RestApi::Handle(output, origin, remoteIp, username, method, - uri, headers, getArguments, bodyData, bodySize); + uri, headers, getArguments, bodyData, bodySize, authenticationPayload); } @@ -324,6 +325,7 @@ static const char* KEY_PRIORITY = "Priority"; static const char* KEY_SYNCHRONOUS = "Synchronous"; static const char* KEY_ASYNCHRONOUS = "Asynchronous"; + static const char* KEY_USER_DATA = "UserData"; bool OrthancRestApi::IsSynchronousJobRequest(bool isDefaultSynchronous, @@ -441,6 +443,11 @@ job->SetPermissive(false); } + if (body.isMember(KEY_USER_DATA)) + { + job->SetUserData(body[KEY_USER_DATA]); + } + SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body); } @@ -467,6 +474,11 @@ job->SetPermissive(false); } + if (body.isMember(KEY_USER_DATA)) + { + job->SetUserData(body[KEY_USER_DATA]); + } + SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body); } @@ -481,7 +493,7 @@ "If `true`, run the job in asynchronous mode, which means that the REST API call will immediately " "return, reporting the identifier of a job. Prefer this flavor wherever possible.", false) .SetRequestField(KEY_PRIORITY, RestApiCallDocumentation::Type_Number, - "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false) + "In asynchronous mode, the priority of the job. The higher the value, the higher the priority. Default value is `0`", false) .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "In asynchronous mode, identifier of the job") .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "In asynchronous mode, path to access the job in the REST API"); } @@ -492,7 +504,9 @@ DocumentSubmitGenericJob(call); call.GetDocumentation() .SetRequestField(KEY_PERMISSIVE, RestApiCallDocumentation::Type_Boolean, - "If `true`, ignore errors during the individual steps of the job.", false); + "If `true`, ignore errors during the individual steps of the job. Default value is `false`.", false) + .SetRequestField(KEY_USER_DATA, RestApiCallDocumentation::Type_JsonObject, + "User data that will travel along with the job.", false); } @@ -705,8 +719,8 @@ { call.GetDocumentation().SetHttpGetArgument(GET_RESPONSE_CONTENT, RestApiCallDocumentation::Type_String, "Defines the content of response for each returned resource. Allowed values are `MainDicomTags`, " - "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. If not specified, Orthanc " - "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`." + "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`, `Attachments`. If not specified, Orthanc " + "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`." "e.g: '" + GET_RESPONSE_CONTENT + "=MainDicomTags;Children " "(new in Orthanc 1.12.5 - overrides `expand`)", false); @@ -719,8 +733,8 @@ call.GetDocumentation().SetRequestField(POST_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings, "Defines the content of response for each returned resource. (this field, if present, overrides the \"Expand\" field). " "Allowed values are `MainDicomTags`, " - "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. If not specified, Orthanc " - "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`." + "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`, `Attachments`. If not specified, Orthanc " + "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`." "(new in Orthanc 1.12.5)", false); call.GetDocumentation().SetRequestField(POST_EXPAND, RestApiCallDocumentation::Type_Boolean,
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Tue Nov 04 15:58:06 2025 +0100 @@ -78,7 +78,8 @@ const HttpToolbox::Arguments& headers, const HttpToolbox::GetArguments& getArguments, const void* bodyData, - size_t bodySize) ORTHANC_OVERRIDE; + size_t bodySize, + const std::string& authenticationPayload) ORTHANC_OVERRIDE; const bool& LeaveBarrierFlag() const {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -44,7 +44,8 @@ static const char* const KEY_TRANSCODE = "Transcode"; static const char* const KEY_LOSSY_QUALITY = "LossyQuality"; static const char* const KEY_FILENAME = "Filename"; - + static const char* const KEY_USER_DATA = "UserData"; + static const char* const GET_TRANSCODE = "transcode"; static const char* const GET_LOSSY_QUALITY = "lossy-quality"; static const char* const GET_FILENAME = "filename"; @@ -124,6 +125,7 @@ int& priority, /* out */ unsigned int& loaderThreads, /* out */ std::string& filename, /* out */ + Json::Value& userData, /* out */ const Json::Value& body, /* in */ const bool defaultExtended /* in */, const std::string& defaultFilename /* in */) @@ -174,6 +176,12 @@ filename = defaultFilename; } + if (body.type() == Json::objectValue && + body.isMember(KEY_USER_DATA) && body[KEY_USER_DATA].isString()) + { + userData = body[KEY_USER_DATA].asString(); + } + { OrthancConfiguration::ReaderLock lock; loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0); // New in Orthanc 1.10.0 @@ -548,6 +556,8 @@ "(including file extension)", false) .SetRequestField("Priority", RestApiCallDocumentation::Type_Number, "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false) + .SetRequestField(KEY_USER_DATA, RestApiCallDocumentation::Type_JsonObject, + "In asynchronous mode, user data that will be attached to the job.", false) .AddAnswerType(MimeType_Zip, "In synchronous mode, the ZIP file containing the archive") .AddAnswerType(MimeType_Json, "In asynchronous mode, information about the job that has been submitted to " "generate the archive: https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs") @@ -593,9 +603,10 @@ unsigned int loaderThreads; std::string filename; unsigned int lossyQuality; + Json::Value userData; GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality, - priority, loaderThreads, filename, body, DEFAULT_IS_EXTENDED, "Archive.zip"); + priority, loaderThreads, filename, userData, body, DEFAULT_IS_EXTENDED, "Archive.zip"); std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient)); AddResourcesOfInterest(*job, body); @@ -607,6 +618,7 @@ } job->SetLoaderThreads(loaderThreads); + job->SetUserData(userData); SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename); } @@ -783,8 +795,10 @@ unsigned int loaderThreads; std::string filename; unsigned int lossyQuality; + Json::Value userData; + GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality, - priority, loaderThreads, filename, body, false /* by default, not extented */, id + ".zip"); + priority, loaderThreads, filename, userData, body, false /* by default, not extented */, id + ".zip"); std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL)); job->AddResource(id, true, LEVEL); @@ -796,6 +810,7 @@ } job->SetLoaderThreads(loaderThreads); + job->SetUserData(userData); SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename); }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -685,9 +685,9 @@ "Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false) .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number, "Timeout for the C-FIND command and subsequent C-MOVE retrievals, in seconds (new in Orthanc 1.9.1)", false) - .SetAnswerField("ID", RestApiCallDocumentation::Type_JsonObject, + .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Identifier of the query, to be used with `/queries/{id}`") - .SetAnswerField("Path", RestApiCallDocumentation::Type_JsonObject, + .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Root path to the query in the REST API"); return; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -494,7 +494,7 @@ std::string target; call.BodyToString(target); - SystemToolbox::WriteFile(dicom, target); + SystemToolbox::WriteFile(dicom, SystemToolbox::PathFromUtf8(target)); call.GetOutput().AnswerBuffer("{}", MimeType_Json); } @@ -2666,7 +2666,7 @@ } int64_t newRevision; - context.AddAttachment(newRevision, publicId, StringToContentType(name), call.GetBodyData(), + context.AddAttachment(newRevision, publicId, level, StringToContentType(name), call.GetBodyData(), call.GetBodySize(), hasOldRevision, oldRevision, oldMD5); SetBufferContentETag(call.GetOutput(), newRevision, call.GetBodyData(), call.GetBodySize()); // New in Orthanc 1.9.2
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -95,6 +95,8 @@ static const char* const HAS_LABELS = "HasLabels"; static const char* const CAPABILITIES = "Capabilities"; static const char* const HAS_EXTENDED_CHANGES = "HasExtendedChanges"; + static const char* const HAS_KEY_VALUE_STORES = "HasKeyValueStores"; + static const char* const HAS_QUEUES = "HasQueues"; static const char* const HAS_EXTENDED_FIND = "HasExtendedFind"; static const char* const READ_ONLY = "ReadOnly"; @@ -188,14 +190,12 @@ if (plugins.HasStorageArea()) { - std::string p = plugins.GetStorageAreaLibrary().GetPath(); - result[STORAGE_AREA_PLUGIN] = boost::filesystem::canonical(p).string(); + result[STORAGE_AREA_PLUGIN] = SystemToolbox::PathToUtf8(plugins.GetStorageAreaLibrary().GetPath()); } if (plugins.HasDatabaseBackend()) { - std::string p = plugins.GetDatabaseBackendLibrary().GetPath(); - result[DATABASE_BACKEND_PLUGIN] = boost::filesystem::canonical(p).string(); + result[DATABASE_BACKEND_PLUGIN] = SystemToolbox::PathToUtf8(plugins.GetDatabaseBackendLibrary().GetPath()); } #else result[PLUGINS_ENABLED] = false; @@ -211,6 +211,8 @@ result[CAPABILITIES] = Json::objectValue; result[CAPABILITIES][HAS_EXTENDED_CHANGES] = OrthancRestApi::GetIndex(call).HasExtendedChanges(); result[CAPABILITIES][HAS_EXTENDED_FIND] = OrthancRestApi::GetIndex(call).HasFindSupport(); + result[CAPABILITIES][HAS_KEY_VALUE_STORES] = OrthancRestApi::GetIndex(call).HasKeyValueStoresSupport(); + result[CAPABILITIES][HAS_QUEUES] = OrthancRestApi::GetIndex(call).HasQueuesSupport(); call.GetOutput().AnswerJson(result); }
--- a/OrthancServer/Sources/ResourceFinder.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ResourceFinder.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -394,6 +394,14 @@ } } + if (responseContent_ & ResponseContentFlags_IsProtected) + { + if (resource.GetLevel() == ResourceType_Patient ) + { + target["IsProtected"] = index.IsProtectedPatient(resource.GetIdentifier()); + } + } + if (responseContent_ & ResponseContentFlags_MainDicomTags) { DicomMap allMainDicomTags;
--- a/OrthancServer/Sources/ResourceFinder.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ResourceFinder.h Tue Nov 04 15:58:06 2025 +0100 @@ -178,7 +178,7 @@ request_.SetRetrieveAttachments(retrieve); } - // NB: "index" is only used in this method to fill the "IsStable" information + // NB: "index" is used in this method to fill the "IsStable" and "IsProtected" information void Expand(Json::Value& target, const FindResponse::Resource& resource, ServerIndex& index,
--- a/OrthancServer/Sources/Search/DatabaseLookup.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -111,8 +111,8 @@ std::set<DicomTag> ignoreTagLength; std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement - (*element, DicomToJsonFlags_None, - 0, encoding, hasCodeExtensions, ignoreTagLength)); + (*element, DicomToJsonFlags_None, 0, encoding, hasCodeExtensions, + ignoreTagLength, FromDcmtkBridge::Convert(element->getVR()))); // WARNING: Also modify "HierarchicalMatcher::Setup()" if modifying this code if (value.get() == NULL ||
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -114,8 +114,8 @@ std::set<DicomTag> ignoreTagLength; std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement - (*element, DicomToJsonFlags_None, - 0, encoding, hasCodeExtensions, ignoreTagLength)); + (*element, DicomToJsonFlags_None, 0, encoding, hasCodeExtensions, + ignoreTagLength, FromDcmtkBridge::Convert(element->getVR()))); // WARNING: Also modify "DatabaseLookup::IsMatch()" if modifying this code if (value.get() == NULL ||
--- a/OrthancServer/Sources/ServerContext.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -357,7 +357,7 @@ ServerContext::ServerContext(IDatabaseWrapper& database, - IStorageArea& area, + IPluginStorageArea& area, bool unitTesting, size_t maxCompletedJobs, bool readOnly, @@ -496,7 +496,27 @@ std::list<std::string> acceptedSopClasses; std::set<std::string> rejectedSopClasses; lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses"); - lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses"); + + static const char* const REJECTED_SOP_CLASS = "RejectedSopClasses"; + if (lock.GetJson().isMember(REJECTED_SOP_CLASS)) + { + lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, REJECTED_SOP_CLASS); + } + else + { + static const char* const REJECT_SOP_CLASS = "RejectSopClasses"; + if (lock.GetJson().isMember(REJECT_SOP_CLASS)) + { + /** + * This is for backward compatibility. In Orthanc 1.12.6, + * there was a typo: "RejectedSopClasses" was spelled as + * "RejectSopClasses". + * https://discourse.orthanc-server.org/t/fix-for-config-param-rejectsopclasses-vs-rejectedsopclasses/5900 + **/ + lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, REJECT_SOP_CLASS); + } + } + SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses); defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE")); @@ -594,10 +614,11 @@ void ServerContext::RemoveFile(const std::string& fileUuid, - FileContentType type) + FileContentType type, + const std::string& customData) { StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - accessor.Remove(fileUuid, type); + accessor.Remove(fileUuid, type, customData); } @@ -606,6 +627,37 @@ StoreInstanceMode mode, bool isReconstruct) { + FileInfo adoptedFileNotUsed; + + return StoreAfterTranscoding(resultPublicId, + dicom, + mode, + isReconstruct, + false, + adoptedFileNotUsed); + } + + ServerContext::StoreResult ServerContext::AdoptDicomInstance(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + const FileInfo& adoptedFile) + { + return StoreAfterTranscoding(resultPublicId, + dicom, + mode, + false, + true, + adoptedFile); + } + + + ServerContext::StoreResult ServerContext::StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + bool isReconstruct, + bool isAdoption, + const FileInfo& adoptedFile) + { bool overwrite; switch (mode) { @@ -708,19 +760,25 @@ // TODO Should we use "gzip" instead? CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); - FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), - FileContentType_Dicom, compression, storeMD5_); + StatelessDatabaseOperations::Attachments attachments; + FileInfo dicomInfo; - ServerIndex::Attachments attachments; - attachments.push_back(dicomInfo); + if (!isAdoption) + { + accessor.Write(dicomInfo, dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom, compression, storeMD5_, &dicom); + attachments.push_back(dicomInfo); + } + else + { + attachments.push_back(adoptedFile); + } FileInfo dicomUntilPixelData; if (hasPixelDataOffset && - (!area_.HasReadRange() || + (!area_.HasEfficientReadRange() || compressionEnabled_)) { - dicomUntilPixelData = accessor.Write(dicom.GetBufferData(), pixelDataOffset, - FileContentType_DicomUntilPixelData, compression, storeMD5_); + accessor.Write(dicomUntilPixelData, dicom.GetBufferData(), pixelDataOffset, FileContentType_DicomUntilPixelData, compression, storeMD5_, NULL); attachments.push_back(dicomUntilPixelData); } @@ -765,7 +823,10 @@ if (result.GetStatus() != StoreStatus_Success) { - accessor.Remove(dicomInfo); + if (!isAdoption) + { + accessor.Remove(dicomInfo); + } if (dicomUntilPixelData.IsValid()) { @@ -779,7 +840,14 @@ switch (result.GetStatus()) { case StoreStatus_Success: - LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + if (isAdoption) + { + LOG(INFO) << "New instance adopted (" << resultPublicId << ")"; + } + else + { + LOG(INFO) << "New instance stored (" << resultPublicId << ")"; + } break; case StoreStatus_AlreadyStored: @@ -827,7 +895,7 @@ { if (e.GetErrorCode() == ErrorCode_InexistentTag) { - summary.LogMissingTagsForStore(); + LOG(ERROR) << summary.FormatMissingTagsForStore(); } throw; @@ -841,12 +909,12 @@ { DicomInstanceToStore* dicom = &receivedDicom; +#if ORTHANC_ENABLE_PLUGINS == 1 // WARNING: The scope of "modifiedBuffer" and "modifiedDicom" must // be the same as that of "dicom" - MallocMemoryBuffer modifiedBuffer; + PluginMemoryBuffer64 modifiedBuffer; std::unique_ptr<DicomInstanceToStore> modifiedDicom; -#if ORTHANC_ENABLE_PLUGINS == 1 if (HasPlugins()) { // New in Orthanc 1.10.0 @@ -1019,8 +1087,8 @@ StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); accessor.Read(content, attachment); - FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(), - content.size(), attachmentType, compression, storeMD5_); + FileInfo modified; + accessor.Write(modified, content.empty() ? NULL : content.c_str(), content.size(), attachmentType, compression, storeMD5_, NULL); try { @@ -1202,7 +1270,7 @@ if (hasPixelDataOffset && - area_.HasReadRange() && + area_.HasEfficientReadRange() && LookupAttachment(attachment, FileContentType_Dicom, instanceAttachments) && attachment.GetCompressionType() == CompressionType_None) { @@ -1280,13 +1348,13 @@ index_.OverwriteMetadata(instancePublicId, MetadataType_Instance_PixelDataOffset, boost::lexical_cast<std::string>(pixelDataOffset)); - if (!area_.HasReadRange() || + if (!area_.HasEfficientReadRange() || compressionEnabled_) { int64_t newRevision; - AddAttachment(newRevision, instancePublicId, FileContentType_DicomUntilPixelData, + AddAttachment(newRevision, instancePublicId, ResourceType_Instance, FileContentType_DicomUntilPixelData, dicom.empty() ? NULL: dicom.c_str(), pixelDataOffset, - false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); + false /* no old revision */, -1 /* dummy revision */, "" /* dummy MD5 */); } } } @@ -1355,7 +1423,7 @@ return true; } - if (!area_.HasReadRange()) + if (!area_.HasEfficientReadRange()) { return false; } @@ -1514,6 +1582,7 @@ bool ServerContext::AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -1527,7 +1596,11 @@ CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None); StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_); + + assert(attachmentType != FileContentType_Dicom && attachmentType != FileContentType_DicomUntilPixelData); // this method can not be used to store instances + + FileInfo attachment; + accessor.Write(attachment, data, size, attachmentType, compression, storeMD5_, NULL); try { @@ -1902,13 +1975,13 @@ } - void ServerContext::StoreWithTranscoding(std::string& sopClassUid, - std::string& sopInstanceUid, - DicomStoreUserConnection& connection, - const std::string& dicom, - bool hasMoveOriginator, - const std::string& moveOriginatorAet, - uint16_t moveOriginatorId) + void ServerContext::PerformCStoreWithTranscoding(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomStoreUserConnection& connection, + const std::string& dicom, + bool hasMoveOriginator, + const std::string& moveOriginatorAet, + uint16_t moveOriginatorId) { const void* data = dicom.empty() ? NULL : dicom.c_str(); const RemoteModalityParameters& modality = connection.GetParameters().GetRemoteModality();
--- a/OrthancServer/Sources/ServerContext.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.h Tue Nov 04 15:58:06 2025 +0100 @@ -43,7 +43,7 @@ namespace Orthanc { class DicomInstanceToStore; - class IStorageArea; + class IPluginStorageArea; class JobsEngine; class MetricsRegistry; class OrthancPlugins; @@ -193,7 +193,7 @@ virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE; ServerIndex index_; - IStorageArea& area_; + IPluginStorageArea& area_; StorageCache storageCache_; bool compressionEnabled_; @@ -269,9 +269,17 @@ StoreInstanceMode mode, bool isReconstruct); + StoreResult StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + bool isReconstruct, + bool isAdoption, + const FileInfo& adoptedFile); + // This method must only be called from "ServerIndex"! void RemoveFile(const std::string& fileUuid, - FileContentType type); + FileContentType type, + const std::string& customData); // This DicomModification object is intended to be used as a // "rules engine" when de-identifying logs for C-Find, C-Get, and @@ -305,7 +313,7 @@ }; ServerContext(IDatabaseWrapper& database, - IStorageArea& area, + IPluginStorageArea& area, bool unitTesting, size_t maxCompletedJobs, bool readOnly, @@ -344,6 +352,7 @@ bool AddAttachment(int64_t& newRevision, const std::string& resourceId, + ResourceType resourceType, FileContentType attachmentType, const void* data, size_t size, @@ -355,6 +364,11 @@ DicomInstanceToStore& dicom, StoreInstanceMode mode); + StoreResult AdoptDicomInstance(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode, + const FileInfo& adoptedFile); + StoreResult TranscodeAndStore(std::string& resultPublicId, DicomInstanceToStore* dicom, StoreInstanceMode mode, @@ -563,13 +577,13 @@ size_t size, unsigned int frameIndex); - void StoreWithTranscoding(std::string& sopClassUid, - std::string& sopInstanceUid, - DicomStoreUserConnection& connection, - const std::string& dicom, - bool hasMoveOriginator, - const std::string& moveOriginatorAet, - uint16_t moveOriginatorId); + void PerformCStoreWithTranscoding(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomStoreUserConnection& connection, + const std::string& dicom, + bool hasMoveOriginator, + const std::string& moveOriginatorAet, + uint16_t moveOriginatorId); // This method can be used even if the global option // "TranscodeDicomProtocol" is set to "false"
--- a/OrthancServer/Sources/ServerEnumerations.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -64,6 +64,8 @@ dictMetadataType_.Add(MetadataType_MainDicomTagsSignature, "MainDicomTagsSignature"); dictMetadataType_.Add(MetadataType_MainDicomSequences, "MainDicomSequences"); dictMetadataType_.Add(MetadataType_Instance_PixelDataVR, "PixelDataVR"); + dictMetadataType_.Add(MetadataType_Patient_IsProtected, "IsProtected"); + dictMetadataType_.Add(MetadataType_Patient_PatientRecyclingOrder, "PatientRecyclingOrder"); dictContentType_.Add(FileContentType_Dicom, "dicom"); dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json"); @@ -655,6 +657,10 @@ { return ResponseContentFlags_IsStable; } + else if (value == "IsProtected") + { + return ResponseContentFlags_IsProtected; + } else { throw OrthancException(ErrorCode_ParameterOutOfRange,
--- a/OrthancServer/Sources/ServerEnumerations.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Tue Nov 04 15:58:06 2025 +0100 @@ -136,6 +136,7 @@ ResponseContentFlags_Children = (1 << 10), ResponseContentFlags_Labels = (1 << 11), ResponseContentFlags_IsStable = (1 << 12), + ResponseContentFlags_IsProtected = (1 << 13), ResponseContentFlags_INTERNAL_CountResources = (1 << 30), @@ -150,7 +151,8 @@ ResponseContentFlags_Parent | ResponseContentFlags_Children | ResponseContentFlags_Labels | - ResponseContentFlags_IsStable), // equivalent to "Expand": true + ResponseContentFlags_IsStable | + ResponseContentFlags_IsProtected), // equivalent to "Expand": true ResponseContentFlags_Default = (ResponseContentFlags_ID | ResponseContentFlags_Type | @@ -171,6 +173,7 @@ GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 + GlobalProperty_SQLiteHasRevisionAndCustomData = 7, // New in Orthanc 1.12.8 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0 @@ -207,6 +210,8 @@ 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 + MetadataType_Patient_IsProtected = 18, // New in Orthanc 1.12.9 (used only by DB plugins) + MetadataType_Patient_PatientRecyclingOrder = 19, // New in Orthanc 1.12.9 (used only by DB plugins) // Make sure that the value "65535" can be stored into this enumeration MetadataType_StartUser = 1024, @@ -258,6 +263,11 @@ Warnings_007_MissingRequestedTagsNotReadFromDisk // new in Orthanc 1.12.5 }; + enum QueueOrigin + { + QueueOrigin_Front, + QueueOrigin_Back + }; void InitializeServerEnumerations();
--- a/OrthancServer/Sources/ServerIndex.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerIndex.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -45,12 +45,14 @@ struct FileToRemove { private: - std::string uuid_; - FileContentType type_; + std::string uuid_; + std::string customData_; + FileContentType type_; public: explicit FileToRemove(const FileInfo& info) : - uuid_(info.GetUuid()), + uuid_(info.GetUuid()), + customData_(info.GetCustomData()), type_(info.GetContentType()) { } @@ -60,6 +62,11 @@ return uuid_; } + const std::string& GetCustomData() const + { + return customData_; + } + FileContentType GetContentType() const { return type_; @@ -93,7 +100,7 @@ { try { - context_.RemoveFile(it->GetUuid(), it->GetContentType()); + context_.RemoveFile(it->GetUuid(), it->GetContentType(), it->GetCustomData()); } catch (OrthancException& e) { @@ -305,7 +312,7 @@ bool ServerIndex::IsUnstableResource(ResourceType type, int64_t id) { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); return unstableResources_.Contains(std::make_pair(type, id)); } @@ -387,7 +394,7 @@ void ServerIndex::SetMaximumPatientCount(unsigned int count) { { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumPatients_ = count; if (count == 0) @@ -407,7 +414,7 @@ void ServerIndex::SetMaximumStorageSize(uint64_t size) { { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageSize_ = size; if (size == 0) @@ -426,7 +433,7 @@ void ServerIndex::SetMaximumStorageMode(MaxStorageMode mode) { { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageMode_ = mode; if (mode == MaxStorageMode_Recycle) @@ -479,7 +486,7 @@ int64_t stableId; { - boost::mutex::scoped_lock lock(that->monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(that->monitoringMutex_); if (!that->unstableResources_.IsEmpty() && that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge)) @@ -498,43 +505,52 @@ } } - try - { - /** - * WARNING: Don't protect the calls to "LogChange()" using - * "monitoringMutex_", as this could lead to deadlocks in - * other threads (typically, if "Store()" is being running in - * another thread, which leads to calls to "MarkAsUnstable()", - * which leads to two lockings of "monitoringMutex_"). - **/ - switch (stableLevel) - { - case ResourceType_Patient: - that->LogChange(stableId, ChangeType_StablePatient, stablePayload.GetPublicId(), ResourceType_Patient); - break; - - case ResourceType_Study: - that->LogChange(stableId, ChangeType_StableStudy, stablePayload.GetPublicId(), ResourceType_Study); - break; - - case ResourceType_Series: - that->LogChange(stableId, ChangeType_StableSeries, stablePayload.GetPublicId(), ResourceType_Series); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - catch (OrthancException& e) - { - LOG(ERROR) << "Cannot log a change about a stable resource into the database"; - } + // must not be protected by monitoringMutex_ + that->LogStableChange(stableLevel, stableId, stablePayload.GetPublicId()); } } LOG(INFO) << "Closing the monitor thread for stable resources"; } + void ServerIndex::LogStableChange(ResourceType stableLevel, + int64_t stableId, + const std::string& publicId) + { + try + { + /** + * WARNING: Don't protect the calls to "LogChange()" using + * "monitoringMutex_", as this could lead to deadlocks in + * other threads (typically, if "Store()" is being running in + * another thread, which leads to calls to "MarkAsUnstable()", + * which leads to two lockings of "monitoringMutex_"). + **/ + switch (stableLevel) + { + case ResourceType_Patient: + LogChange(stableId, ChangeType_StablePatient, publicId, ResourceType_Patient); + break; + + case ResourceType_Study: + LogChange(stableId, ChangeType_StableStudy, publicId, ResourceType_Study); + break; + + case ResourceType_Series: + LogChange(stableId, ChangeType_StableSeries, publicId, ResourceType_Series); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Cannot log a change about a stable resource into the database"; + } + } + + void ServerIndex::MarkAsUnstable(ResourceType type, int64_t id, @@ -545,13 +561,59 @@ type == ResourceType_Series); { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); UnstableResourcePayload payload(publicId); unstableResources_.AddOrMakeMostRecent(std::make_pair(type, id), payload); //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; } } + bool ServerIndex::SetStableStatus(bool& statusHasChanged, + const std::string& resourceId, + bool setNewStatusToStable) + { + int64_t id; + ResourceType type; + + if (LookupResource(id, type, resourceId)) + { + if (setNewStatusToStable) + { + { + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); + + if (IsUnstableResource(type, id)) + { + unstableResources_.Invalidate(std::pair<ResourceType, int64_t>(type, id)); + statusHasChanged = true; + } + } + + if (statusHasChanged) + { + // must not be protected by monitoringMutex_ + LogStableChange(type, id, resourceId); + } + } + else + { + { + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); + + statusHasChanged = !IsUnstableResource(type, id); + + // no matter what was the status, we mark it as unstable to reset its stabilization period + MarkAsUnstable(type, id, resourceId); + } + + } + + return true; + } + + return false; + } + StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, const DicomMap& dicomSummary, @@ -571,7 +633,7 @@ MaxStorageMode maximumStorageMode; { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageSize = maximumStorageSize_; maximumPatients = maximumPatients_; maximumStorageMode = maximumStorageMode_; @@ -595,7 +657,7 @@ unsigned int maximumPatients; { - boost::mutex::scoped_lock lock(monitoringMutex_); + boost::recursive_mutex::scoped_lock lock(monitoringMutex_); maximumStorageSize = maximumStorageSize_; maximumPatients = maximumPatients_; }
--- a/OrthancServer/Sources/ServerIndex.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Tue Nov 04 15:58:06 2025 +0100 @@ -40,7 +40,7 @@ class UnstableResourcePayload; bool done_; - boost::mutex monitoringMutex_; + boost::recursive_mutex monitoringMutex_; boost::thread flushThread_; boost::thread unstableResourcesMonitorThread_; @@ -61,6 +61,10 @@ int64_t id, const std::string& publicId); + void LogStableChange(ResourceType type, + int64_t id, + const std::string& publicId); + public: ServerIndex(ServerContext& context, IDatabaseWrapper& database, @@ -101,5 +105,9 @@ bool IsUnstableResource(ResourceType type, int64_t id); + + bool SetStableStatus(bool& statusHasChanged, + const std::string& resourceId, + bool setNewStatusToStable); }; }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -191,13 +191,15 @@ boost::mutex availableInstancesMutex_; SharedMessageQueue instancesToPreload_; std::vector<boost::thread*> threads_; + bool loadersShouldStop_; public: ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality) : InstanceLoader(context, transcode, transferSyntax, lossyQuality), availableInstancesSemaphore_(0), - bufferedInstancesSemaphore_(3*threadCount) + bufferedInstancesSemaphore_(3*threadCount), + loadersShouldStop_(false) { for (size_t i = 0; i < threadCount; i++) { @@ -212,22 +214,38 @@ virtual void Clear() ORTHANC_OVERRIDE { - for (size_t i = 0; i < threads_.size(); i++) - { - instancesToPreload_.Enqueue(NULL); - } - - for (size_t i = 0; i < threads_.size(); i++) + if (threads_.size() > 0) { - if (threads_[i]->joinable()) + LOG(INFO) << "Waiting for loader threads to complete"; + loadersShouldStop_ = true; // not need to protect this by a mutex. This is the only "writer" and all loaders are "readers" + + // unlock the loaders if they are waiting on this message queue (this happens when the job completes sucessfully) + for (size_t i = 0; i < threads_.size(); i++) { - threads_[i]->join(); + instancesToPreload_.Enqueue(NULL); + } + + // If the consumer stops e.g. because the HttpClient disconnected, we must make sure the loader threads are not blocked waiting for room in the bufferedInstances. + // If the loader threads have completed their jobs, this is harmless to release the bufferedInstances since they won't be used anymore. + for (size_t i = 0; i < threads_.size(); i++) + { + bufferedInstancesSemaphore_.Release(); } - delete threads_[i]; - } - threads_.clear(); - availableInstances_.clear(); + for (size_t i = 0; i < threads_.size(); i++) + { + if (threads_[i]->joinable()) + { + threads_[i]->join(); + } + delete threads_[i]; + } + + threads_.clear(); + availableInstances_.clear(); + + LOG(INFO) << "Waiting for loader threads to complete - done"; + } } static void PreloaderWorkerThread(ThreadedInstanceLoader* that) @@ -235,11 +253,14 @@ static uint16_t threadCounter = 0; Logging::SetCurrentThreadName(std::string("ARCH-LOAD-") + boost::lexical_cast<std::string>(threadCounter++)); + LOG(INFO) << "Loader thread has started"; + while (true) { std::unique_ptr<InstanceToPreload> instanceToPreload(dynamic_cast<InstanceToPreload*>(that->instancesToPreload_.Dequeue(0))); - if (instanceToPreload.get() == NULL) // that's the signal to exit the thread + if (instanceToPreload.get() == NULL || that->loadersShouldStop_) // that's the signal to exit the thread { + LOG(INFO) << "Loader thread has completed"; return; } @@ -269,6 +290,15 @@ } catch (OrthancException& e) { + LOG(ERROR) << "Failed to load instance " << instanceToPreload->GetId() << " error: " << e.GetDetails(); + boost::mutex::scoped_lock lock(that->availableInstancesMutex_); + // store a NULL result to notify that we could not read the instance + that->availableInstances_[instanceToPreload->GetId()] = boost::shared_ptr<std::string>(); + that->availableInstancesSemaphore_.Release(); + } + catch (...) + { + LOG(ERROR) << "Failed to load instance " << instanceToPreload->GetId() << " unknown error"; boost::mutex::scoped_lock lock(that->availableInstancesMutex_); // store a NULL result to notify that we could not read the instance that->availableInstances_[instanceToPreload->GetId()] = boost::shared_ptr<std::string>(); @@ -1114,11 +1144,11 @@ } } - void SetOutputFile(const std::string& path) + void SetOutputFile(const boost::filesystem::path& path) { if (zip_.get() == NULL) { - zip_.reset(new HierarchicalZipWriter(path.c_str())); + zip_.reset(new HierarchicalZipWriter(path)); zip_->SetZip64(commands_.IsZip64()); isStream_ = false; } @@ -1542,6 +1572,12 @@ synchronousTarget_.reset(); asynchronousTarget_.reset(); + + // clear the loader threads + if (instanceLoader_.get() != NULL) + { + instanceLoader_->Clear(); + } } }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Tue Nov 04 15:58:06 2025 +0100 @@ -58,6 +58,7 @@ bool enableExtendedSopClass_; std::string description_; std::string filename_; + Json::Value userData_; boost::shared_ptr<ZipWriterIterator> writer_; size_t currentStep_; @@ -137,5 +138,20 @@ virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE; virtual void DeleteAllOutputs() ORTHANC_OVERRIDE; + + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE + { + userData_ = userData; + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + if (!userData_.isNull()) + { + userData = userData_; + return true; + } + return false; + } }; }
--- a/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -117,5 +117,10 @@ } } + DicomGetScuJob::DicomGetScuJob(ServerContext& context, + const Json::Value& serialized) : + DicomRetrieveScuBaseJob(context, serialized) + { + } }
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -64,8 +64,8 @@ } std::string sopClassUid, sopInstanceUid; - context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom, - HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_); + context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom, + HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_); if (storageCommitment_) {
--- a/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -55,8 +55,8 @@ instance.ReadDicom(dicom); std::string sopClassUid, sopInstanceUid; // Unused - context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom, - false /* Not a C-MOVE */, "", 0); + context_.PerformCStoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom, + false /* Not a C-MOVE */, "", 0); } catch (OrthancException& e) {
--- a/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -89,7 +89,7 @@ tmp->Write(dicom); - arguments.push_back(tmp->GetPath()); + arguments.push_back(SystemToolbox::PathToUtf8(tmp->GetPath())); break; }
--- a/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -39,6 +39,7 @@ #include "DicomModalityStoreJob.h" #include "DicomMoveScuJob.h" +#include "DicomGetScuJob.h" #include "MergeStudyJob.h" #include "OrthancPeerStoreJob.h" #include "ResourceModificationJob.h" @@ -87,6 +88,10 @@ { return new DicomMoveScuJob(context_, source); } + else if (type == "DicomGetScu") + { + return new DicomGetScuJob(context_, source); + } else if (type == "StorageCommitmentScp") { return new StorageCommitmentScpJob(context_, source);
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -367,6 +367,7 @@ static const char* KEY_PARENT_RESOURCES = "ParentResources"; static const char* KEY_DESCRIPTION = "Description"; static const char* KEY_PERMISSIVE = "Permissive"; + static const char* KEY_USER_DATA = "UserData"; static const char* KEY_CURRENT_STEP = "CurrentStep"; static const char* KEY_TYPE = "Type"; static const char* KEY_INSTANCES = "Instances"; @@ -402,6 +403,7 @@ target[KEY_TYPE] = type; target[KEY_PERMISSIVE] = permissive_; + target[KEY_USER_DATA] = userData_; target[KEY_CURRENT_STEP] = static_cast<unsigned int>(currentStep_); target[KEY_DESCRIPTION] = description_; target[KEY_KEEP_SOURCE] = keepSource_; @@ -456,6 +458,17 @@ { currentStep_ = static_cast<ThreadedJobStep>(SerializationToolbox::ReadUnsignedInteger(source, KEY_CURRENT_STEP)); } + + if (source.isMember(KEY_PERMISSIVE)) + { + SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE); + } + + // new in 1.12.9 + if (source.isMember(KEY_USER_DATA)) + { + userData_ = source[KEY_USER_DATA]; + } } @@ -538,6 +551,27 @@ return description_; } + + void ThreadedSetOfInstancesJob::SetUserData(const Json::Value& userData) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + userData_ = userData; + } + + bool ThreadedSetOfInstancesJob::GetUserData(Json::Value& userData) const + { + boost::recursive_mutex::scoped_lock lock(mutex_); + + if (!userData_.isNull()) + { + userData = userData_; + return true; + } + return false; + } + + void ThreadedSetOfInstancesJob::SetErrorCode(ErrorCode errorCode) { boost::recursive_mutex::scoped_lock lock(mutex_);
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Tue Nov 04 15:58:06 2025 +0100 @@ -62,6 +62,7 @@ bool permissive_; ThreadedJobStep currentStep_; std::string description_; + Json::Value userData_; size_t workersCount_; ServerContext& context_; @@ -170,5 +171,8 @@ return context_; } + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE; + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Sources/ServerToolbox.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -96,7 +96,7 @@ void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, ResourceType level) { // WARNING: The database should be locked with a transaction!
--- a/OrthancServer/Sources/ServerToolbox.h Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/ServerToolbox.h Tue Nov 04 15:58:06 2025 +0100 @@ -32,7 +32,7 @@ namespace Orthanc { class ServerContext; - class IStorageArea; + class IPluginStorageArea; namespace ServerToolbox { @@ -42,7 +42,7 @@ ResourceType type); void ReconstructMainDicomTags(IDatabaseWrapper::ITransaction& transaction, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, ResourceType level); void LoadIdentifiers(const DicomTag*& tags,
--- a/OrthancServer/Sources/main.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/Sources/main.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -30,6 +30,7 @@ #include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h" #include "../../OrthancFramework/Sources/HttpServer/HttpServer.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -51,6 +52,9 @@ #include <boost/algorithm/string/predicate.hpp> +#if defined(_WIN32) || defined(__CYGWIN__) +#include <windows.h> +#endif using namespace Orthanc; @@ -533,7 +537,7 @@ { } - virtual bool IsValidBearerToken(const std::string& token) ORTHANC_OVERRIDE + virtual bool IsValidBearerToken(const std::string& token) const ORTHANC_OVERRIDE { #if ORTHANC_ENABLE_PLUGINS == 1 return (plugins_ != NULL && @@ -548,7 +552,7 @@ const char* ip, const char* username, const HttpToolbox::Arguments& httpHeaders, - const HttpToolbox::GetArguments& getArguments) ORTHANC_OVERRIDE + const HttpToolbox::GetArguments& getArguments) const ORTHANC_OVERRIDE { #if ORTHANC_ENABLE_PLUGINS == 1 if (plugins_ != NULL && @@ -569,24 +573,24 @@ switch (method) { - case HttpMethod_Get: - call.PushString("GET"); - break; + case HttpMethod_Get: + call.PushString("GET"); + break; - case HttpMethod_Put: - call.PushString("PUT"); - break; + case HttpMethod_Put: + call.PushString("PUT"); + break; - case HttpMethod_Post: - call.PushString("POST"); - break; + case HttpMethod_Post: + call.PushString("POST"); + break; - case HttpMethod_Delete: - call.PushString("DELETE"); - break; + case HttpMethod_Delete: + call.PushString("DELETE"); + break; - default: - return true; + default: + return true; } call.PushString(uri); @@ -603,6 +607,23 @@ return true; } + + virtual AuthenticationStatus CheckAuthentication(std::string& customPayload /* out: payload to provide to "IsAllowed()" */, + std::string& redirection /* out: path relative to the root */, + const char* uri, + const char* ip, + const HttpToolbox::Arguments& httpHeaders, + const HttpToolbox::GetArguments& getArguments) const ORTHANC_OVERRIDE + { +#if ORTHANC_ENABLE_PLUGINS == 1 + if (plugins_ != NULL) + { + return plugins_->CheckAuthentication(customPayload, redirection, uri, ip, httpHeaders, getArguments); + } +#endif + + return AuthenticationStatus_BuiltIn; + } }; @@ -683,6 +704,11 @@ message["Details"] = exception.GetDetails(); } + if (exception.HasDimseErrorStatus()) + { + message["DimseErrorStatus"] = exception.GetDimseErrorStatus(); + } + std::string info = message.toStyledString(); output.SendStatus(httpStatus, info); } @@ -690,10 +716,10 @@ }; -static void PrintHelp(const char* path) +static void PrintHelp(const boost::filesystem::path& path) { std::cout - << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl + << "Usage: " << SystemToolbox::PathToUtf8(path) << " [OPTION]... [CONFIGURATION]" << std::endl << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl << std::endl << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl @@ -755,10 +781,10 @@ } -static void PrintVersion(const char* path) +static void PrintVersion(const boost::filesystem::path &path) { std::cout - << path << " " << ORTHANC_VERSION << std::endl + << SystemToolbox::PathToUtf8(path) << " " << ORTHANC_VERSION << std::endl << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl << "Copyright (C) 2017-2023 Osimis S.A. (Belgium)" << std::endl << "Copyright (C) 2024-2025 Orthanc Team SRL (Belgium)" << std::endl @@ -780,10 +806,10 @@ } -static void PrintErrors(const char* path) +static void PrintErrors(const boost::filesystem::path& path) { std::cout - << path << " " << ORTHANC_VERSION << std::endl + << SystemToolbox::PathToUtf8(path) << " " << ORTHANC_VERSION << std::endl << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl << std::endl << "List of error codes that could be returned by Orthanc:" @@ -860,7 +886,7 @@ PrintErrorCode(ErrorCode_DirectoryOverFile, "The directory to be created is already occupied by a regular file"); PrintErrorCode(ErrorCode_FileStorageCannotWrite, "Unable to create a subdirectory or a file in the file storage"); PrintErrorCode(ErrorCode_DirectoryExpected, "The specified path does not point to a directory"); - PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is privileged or already in use"); + PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is privileged or already in use or one of the HTTP bind addresses does not exist"); PrintErrorCode(ErrorCode_DicomPortInUse, "The TCP port of the DICOM server is privileged or already in use"); PrintErrorCode(ErrorCode_BadHttpStatusInRest, "This HTTP status is not allowed in a REST API"); PrintErrorCode(ErrorCode_RegularFileExpected, "The specified path does not point to a regular file"); @@ -924,7 +950,7 @@ for (std::list<std::string>::const_iterator it = pathList.begin(); it != pathList.end(); ++it) { - std::string path; + boost::filesystem::path path; { OrthancConfiguration::ReaderLock lock; @@ -1042,6 +1068,8 @@ # error "Either Mongoose or Civetweb must be enabled to compile this file" #endif + httpServer.SetMetricsRegistry(context.GetMetricsRegistry()); + { OrthancConfiguration::ReaderLock lock; @@ -1050,6 +1078,9 @@ // HTTP server httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount", 50)); httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042)); + std::set<std::string> httpBindAddresses; + lock.GetConfiguration().GetSetOfStringsParameter(httpBindAddresses, "HttpBindAddresses"); + httpServer.SetBindAddresses(httpBindAddresses); httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false)); httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive)); httpServer.SetKeepAliveTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("KeepAliveTimeout", 1)); @@ -1089,12 +1120,16 @@ httpServer.SetAuthenticationEnabled(false); } - bool hasUsers = lock.GetConfiguration().SetupRegisteredUsers(httpServer); + OrthancConfiguration::RegisteredUsersStatus status = lock.GetConfiguration().SetupRegisteredUsers(httpServer); + assert(status == OrthancConfiguration::RegisteredUsersStatus_NoConfiguration || + status == OrthancConfiguration::RegisteredUsersStatus_NoUser || + status == OrthancConfiguration::RegisteredUsersStatus_HasUser); if (httpServer.IsAuthenticationEnabled() && - !hasUsers) + status != OrthancConfiguration::RegisteredUsersStatus_HasUser) { - if (httpServer.IsRemoteAccessAllowed()) + if (httpServer.IsRemoteAccessAllowed() && + status == OrthancConfiguration::RegisteredUsersStatus_NoConfiguration) { /** * Starting with Orthanc 1.5.8, if no user is explicitly @@ -1116,13 +1151,14 @@ else { LOG(WARNING) << "HTTP authentication is enabled, but no user is declared, " - << "check the value of configuration option \"RegisteredUsers\""; + << "check the value of configuration option \"RegisteredUsers\" " + << "if you cannot access Orthanc as expected"; } } if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false)) { - std::string certificate = lock.GetConfiguration().InterpretStringParameterAsPath( + boost::filesystem::path certificate = lock.GetConfiguration().InterpretStringParameterAsPath( lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem")); httpServer.SetSslEnabled(true); httpServer.SetSslCertificate(certificate.c_str()); @@ -1171,10 +1207,10 @@ if (lock.GetConfiguration().GetBooleanParameter("SslVerifyPeers", false)) { - std::string trustedClientCertificates = lock.GetConfiguration().InterpretStringParameterAsPath( + boost::filesystem::path trustedClientCertificates = lock.GetConfiguration().InterpretStringParameterAsPath( lock.GetConfiguration().GetStringParameter("SslTrustedClientCertificates", "trustedCertificates.pem")); httpServer.SetSslVerifyPeers(true); - httpServer.SetSslTrustedClientCertificates(trustedClientCertificates.c_str()); + httpServer.SetSslTrustedClientCertificates(trustedClientCertificates); } else { @@ -1274,6 +1310,7 @@ // Setup the DICOM server DicomServer dicomServer; + dicomServer.SetMetricsRegistry(context.GetMetricsRegistry()); dicomServer.SetRemoteModalities(modalities); dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); @@ -1426,7 +1463,7 @@ static void UpgradeDatabase(IDatabaseWrapper& database, - IStorageArea& storageArea) + IPluginStorageArea& storageArea) { // Upgrade the schema of the database, if needed unsigned int currentVersion = database.GetDatabaseVersion(); @@ -1529,7 +1566,7 @@ static bool ConfigureServerContext(IDatabaseWrapper& database, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, OrthancPlugins *plugins, bool loadJobsFromDatabase) { @@ -1544,10 +1581,11 @@ // ServerContext, otherwise the possible Lua scripts will not be // able to properly issue HTTP/HTTPS queries - std::string httpsCaCertificates = lock.GetConfiguration().GetStringParameter("HttpsCACertificates", ""); - if (!httpsCaCertificates.empty()) + std::string httpsCaCertificatesStr = lock.GetConfiguration().GetStringParameter("HttpsCACertificates", ""); + boost::filesystem::path httpsCaCertificates; + if (!httpsCaCertificatesStr.empty()) { - httpsCaCertificates = lock.GetConfiguration().InterpretStringParameterAsPath(httpsCaCertificates); + httpsCaCertificates = lock.GetConfiguration().InterpretStringParameterAsPath(httpsCaCertificatesStr); } HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true), @@ -1667,7 +1705,7 @@ static bool ConfigureDatabase(IDatabaseWrapper& database, - IStorageArea& storageArea, + IPluginStorageArea& storageArea, OrthancPlugins *plugins, bool upgradeDatabase, bool loadJobsFromDatabase) @@ -1740,13 +1778,12 @@ } -static bool ConfigurePlugins(int argc, - char* argv[], +static bool ConfigurePlugins(const std::vector<std::string> arguments, bool upgradeDatabase, bool loadJobsFromDatabase) { std::unique_ptr<IDatabaseWrapper> databasePtr; - std::unique_ptr<IStorageArea> storage; + std::unique_ptr<IPluginStorageArea> storage; #if ORTHANC_ENABLE_PLUGINS == 1 std::string databaseServerIdentifier; @@ -1756,7 +1793,7 @@ } OrthancPlugins plugins(databaseServerIdentifier); - plugins.SetCommandLineArguments(argc, argv); + plugins.SetCommandLineArguments(arguments); LoadPlugins(plugins); IDatabaseWrapper* database = NULL; @@ -1805,12 +1842,11 @@ } -static bool StartOrthanc(int argc, - char* argv[], +static bool StartOrthanc(const std::vector<std::string>& arguments, bool upgradeDatabase, bool loadJobsFromDatabase) { - return ConfigurePlugins(argc, argv, upgradeDatabase, loadJobsFromDatabase); + return ConfigurePlugins(arguments, upgradeDatabase, loadJobsFromDatabase); } @@ -1838,24 +1874,49 @@ } -int main(int argc, char* argv[]) +#if defined(_WIN32) && !defined(__MINGW32__) +// arguments are passed as UTF-16 on Windows +int wmain(int argc, wchar_t *argv[]) { + // Set Windows console output to UTF-8 (otherwise, strings are considered to be in UTF-16. For example, Cyrillic UTF-8 strings appear as garbage without that config) + SetConsoleOutputCP(CP_UTF8); + + // Transform the UTF-16 arguments into UTF-8 arguments + std::vector<std::string> arguments; // UTF-8 arguments + + for (int i = 0; i < argc; i++) + { + std::wstring argument(argv[i]); + arguments.push_back(SystemToolbox::WStringToUtf8(argument)); + } + +#else +int main(int argc, char* argv[]) +{ + std::vector<std::string> arguments; // UTF-8 arguments + + // the arguments are assumed to be directly in UTF-8 + for (int i = 0; i < argc; i++) + { + arguments.push_back(argv[i]); + } +#endif + Logging::Initialize(); Logging::SetCurrentThreadName("MAIN"); SetGlobalVerbosity(Verbosity_Default); bool upgradeDatabase = false; bool loadJobsFromDatabase = true; - const char* configurationFile = NULL; - + boost::filesystem::path configurationFile; /** * Parse the command-line options. **/ - for (int i = 1; i < argc; i++) + for (size_t i = 1; i < arguments.size(); i++) { - std::string argument(argv[i]); + const std::string& argument = arguments[i]; if (argument.empty()) { @@ -1863,7 +1924,7 @@ } else if (argument[0] != '-') { - if (configurationFile != NULL) + if (!configurationFile.empty()) { LOG(ERROR) << "More than one configuration path were provided on the command line, aborting"; return -1; @@ -1873,23 +1934,28 @@ // Use the first argument that does not start with a "-" as // the configuration file - // TODO WHAT IS THE ENCODING? - configurationFile = argv[i]; + configurationFile = SystemToolbox::PathFromUtf8(argument); +// // TODO WHAT IS THE ENCODING? +//#if defined(_WIN32) +// //configurationFileUtf8Str = SystemToolbox::WStringToUtf8(SystemToolbox::WStringFromCharPtr(argv[i])); +//#else +// configurationFileUtf8Str = std::string(argv[i]); +//#endif } } else if (argument == "--errors") { - PrintErrors(argv[0]); + PrintErrors(SystemToolbox::PathFromUtf8(arguments[0])); return 0; } else if (argument == "--help") { - PrintHelp(argv[0]); + PrintHelp(SystemToolbox::PathFromUtf8(arguments[0])); return 0; } else if (argument == "--version") { - PrintVersion(argv[0]); + PrintVersion(SystemToolbox::PathFromUtf8(arguments[0])); return 0; } else if (argument == "--verbose") @@ -1976,7 +2042,7 @@ } else { - SystemToolbox::WriteFile(configurationSample, target); + SystemToolbox::WriteFile(configurationSample, SystemToolbox::PathFromUtf8(target)); } return 0; } @@ -1997,7 +2063,7 @@ { SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); - MemoryStorageArea inMemoryStorage; + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateOpenApiDocumentation(openapi); @@ -2027,7 +2093,7 @@ } else { - SystemToolbox::WriteFile(s, target); + SystemToolbox::WriteFile(s, SystemToolbox::PathFromUtf8(target)); } return 0; } @@ -2048,7 +2114,7 @@ { SQLiteDatabaseWrapper inMemoryDatabase; inMemoryDatabase.Open(); - MemoryStorageArea inMemoryStorage; + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */, false /* readonly */, 1 /* DCMTK concurrent transcoders */); OrthancRestApi restApi(context, false /* no Orthanc Explorer */); restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html"); @@ -2061,7 +2127,7 @@ } else { - SystemToolbox::WriteFile(cheatsheet, target); + SystemToolbox::WriteFile(cheatsheet, SystemToolbox::PathFromUtf8(target)); } return 0; } @@ -2141,7 +2207,7 @@ { OrthancInitialize(configurationFile); - bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase); + bool restart = StartOrthanc(arguments, upgradeDatabase, loadJobsFromDatabase); if (restart) { OrthancFinalize();
--- a/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -26,9 +26,8 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" -#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/Logging.h" -#include "../../OrthancFramework/Sources/SerializationToolbox.h" #include "../Sources/Database/SQLiteDatabaseWrapper.h" #include "../Sources/ServerContext.h" @@ -39,7 +38,7 @@ { const std::string path = "UnitTestsStorage"; - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false, 1); @@ -99,7 +98,7 @@ context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); context.GetAcceptedSopClasses(s, 0); - ASSERT_EQ(1, s.size()); + ASSERT_EQ(1u, s.size()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end()); } @@ -112,7 +111,7 @@ context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); context.GetAcceptedSopClasses(s, 0); - ASSERT_LE(10, s.size()); + ASSERT_LE(10u, s.size()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end()); @@ -126,7 +125,7 @@ context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); context.GetAcceptedSopClasses(s, 0); - ASSERT_LE(10, s.size()); + ASSERT_LE(10u, s.size()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end()); ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end()); @@ -139,7 +138,7 @@ context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses); context.GetAcceptedSopClasses(s, 0); - ASSERT_EQ(1, s.size()); + ASSERT_EQ(1u, s.size()); ASSERT_TRUE(s.find("1.2.3.4") != s.end()); }
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -27,6 +27,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/Images/Image.h" #include "../../OrthancFramework/Sources/Logging.h" @@ -196,6 +197,74 @@ transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */); } }; + + class DummyTransactionContextFactory : public StatelessDatabaseOperations::ITransactionContextFactory + { + public: + virtual StatelessDatabaseOperations::ITransactionContext* Create() + { + class DummyTransactionContext : public StatelessDatabaseOperations::ITransactionContext + { + public: + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentDeleted(const FileInfo& info) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalResourceDeleted(ResourceType type, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void Commit() ORTHANC_OVERRIDE + { + } + + virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE + { + return 0; + } + + virtual bool IsUnstableResource(Orthanc::ResourceType type, + int64_t id) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */, + ResourceType& remainingLevel /* out */) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void MarkAsUnstable(Orthanc::ResourceType type, + int64_t id, + const std::string& publicId) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalAttachmentsAdded(uint64_t compressedSize) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + }; + + return new DummyTransactionContext; + } + }; } @@ -295,12 +364,18 @@ transaction_->GetAllMetadata(md, a[4]); ASSERT_EQ(0u, md.size()); - transaction_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", - CompressionType_ZlibWithSize, 21, "compressedMD5"), 42); - transaction_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"), 43); - transaction_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"), 44); + FileInfo attachment1("my json file", FileContentType_DicomAsJson, 42, "md5", + CompressionType_ZlibWithSize, 21, "compressedMD5"); + attachment1.SetCustomData("hello"); + transaction_->AddAttachment(a[4], attachment1, 42); + + FileInfo attachment2("my dicom file", FileContentType_Dicom, 43, "md5_2"); + transaction_->AddAttachment(a[4], attachment2, 43); + + FileInfo attachment3("world", FileContentType_Dicom, 44, "md5_3"); + attachment3.SetCustomData("world"); + transaction_->AddAttachment(a[6], attachment3, 44); - // TODO - REVISIONS - "42" is revision number, that is not currently stored (*) transaction_->SetMetadata(a[4], MetadataType_RemoteAet, "PINNACLE", 42); transaction_->GetAllMetadata(md, a[4]); @@ -326,8 +401,8 @@ ASSERT_EQ("PINNACLE", md2[MetadataType_RemoteAet]); - ASSERT_EQ(21u + 42u + 44u, transaction_->GetTotalCompressedSize()); - ASSERT_EQ(42u + 42u + 44u, transaction_->GetTotalUncompressedSize()); + ASSERT_EQ(21u + 43u + 44u, transaction_->GetTotalCompressedSize()); + ASSERT_EQ(42u + 43u + 44u, transaction_->GetTotalUncompressedSize()); transaction_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName"); @@ -339,17 +414,17 @@ int64_t revision; ASSERT_TRUE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_RemoteAet)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(42, revision); ASSERT_FALSE(transaction_->LookupMetadata(s, revision, a[4], MetadataType_Instance_IndexInSeries)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_EQ("PINNACLE", s); std::string u; ASSERT_TRUE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_RemoteAet)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_EQ("PINNACLE", u); ASSERT_FALSE(transaction_->LookupMetadata(u, revision, a[4], MetadataType_Instance_IndexInSeries)); - ASSERT_EQ(0, revision); + ASSERT_EQ(42, revision); ASSERT_TRUE(transaction_->LookupGlobalProperty(s, GlobalProperty_FlushSleep, true)); ASSERT_FALSE(transaction_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42), true)); @@ -357,22 +432,34 @@ FileInfo att; ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_DicomAsJson)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(42, revision); ASSERT_EQ("my json file", att.GetUuid()); ASSERT_EQ(21u, att.GetCompressedSize()); ASSERT_EQ("md5", att.GetUncompressedMD5()); ASSERT_EQ("compressedMD5", att.GetCompressedMD5()); ASSERT_EQ(42u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType()); + ASSERT_EQ("hello", att.GetCustomData()); + + ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[4], FileContentType_Dicom)); + ASSERT_EQ(43, revision); + ASSERT_EQ("my dicom file", att.GetUuid()); + ASSERT_EQ(43u, att.GetCompressedSize()); + ASSERT_EQ("md5_2", att.GetUncompressedMD5()); + ASSERT_EQ("md5_2", att.GetCompressedMD5()); + ASSERT_EQ(43u, att.GetUncompressedSize()); + ASSERT_EQ(CompressionType_None, att.GetCompressionType()); + ASSERT_TRUE(att.GetCustomData().empty()); ASSERT_TRUE(transaction_->LookupAttachment(att, revision, a[6], FileContentType_Dicom)); - ASSERT_EQ(0, revision); // "0" instead of "42" because of (*) + ASSERT_EQ(44, revision); ASSERT_EQ("world", att.GetUuid()); ASSERT_EQ(44u, att.GetCompressedSize()); - ASSERT_EQ("md5", att.GetUncompressedMD5()); - ASSERT_EQ("md5", att.GetCompressedMD5()); + ASSERT_EQ("md5_3", att.GetUncompressedMD5()); + ASSERT_EQ("md5_3", att.GetCompressedMD5()); ASSERT_EQ(44u, att.GetUncompressedSize()); ASSERT_EQ(CompressionType_None, att.GetCompressionType()); + ASSERT_EQ("world", att.GetCustomData()); ASSERT_EQ(0u, listener_->deletedFiles_.size()); ASSERT_EQ(0u, listener_->deletedResources_.size()); @@ -402,7 +489,7 @@ CheckTableRecordCount(0, "Resources"); CheckTableRecordCount(0, "AttachedFiles"); - CheckTableRecordCount(3, "GlobalProperties"); + CheckTableRecordCount(4, "GlobalProperties"); std::string tmp; ASSERT_TRUE(transaction_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion, true)); @@ -618,7 +705,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -700,7 +787,7 @@ const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); - FilesystemStorage storage(path); + PluginStorageAreaAdapter storage(new FilesystemStorage(path)); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -817,7 +904,7 @@ { bool overwrite = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -982,7 +1069,7 @@ { const bool compression = (i == 0); - MemoryStorageArea storage; + PluginStorageAreaAdapter storage(new MemoryStorageArea); SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */); @@ -1058,3 +1145,215 @@ ASSERT_FALSE(ServerToolbox::IsValidLabel("&")); ASSERT_FALSE(ServerToolbox::IsValidLabel(".")); } + + +TEST(SQLiteDatabaseWrapper, KeyValueStores) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello", "world"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.StoreKeyValue("test", "hello2", "world2"); + op.StoreKeyValue("test", "hello3", "world3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello2", it.GetKey()); + ASSERT_EQ("world2", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + ASSERT_THROW(it.GetValue(), OrthancException); + ASSERT_THROW(it.GetKey(), OrthancException); + } + + op.DeleteKeyValue("test", "hello2"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello", it.GetKey()); + ASSERT_EQ("world", it.GetValue()); + ASSERT_TRUE(it.Next()); + ASSERT_EQ("hello3", it.GetKey()); + ASSERT_EQ("world3", it.GetValue()); + ASSERT_FALSE(it.Next()); + } + + std::string s; + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("world", s); + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello3")); ASSERT_EQ("world3", s); + ASSERT_FALSE(op.GetKeyValue(s, "test", "hello2")); + + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("world", s); + op.StoreKeyValue("test", "hello", "overwritten"); + ASSERT_TRUE(op.GetKeyValue(s, "test", "hello")); ASSERT_EQ("overwritten", s); + + op.DeleteKeyValue("test", "nope"); + + op.DeleteKeyValue("test", "hello"); + op.DeleteKeyValue("test", "hello3"); + + for (unsigned int limit = 0; limit < 5; limit++) + { + StatelessDatabaseOperations::KeysValuesIterator it(op, "test"); + it.SetLimit(limit); + ASSERT_FALSE(it.Next()); + } + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + op.StoreKeyValue("test", "blob", blob); // Storing binary values + } + + ASSERT_TRUE(op.GetKeyValue(s, "test", "blob")); + ASSERT_EQ(4u, s.size()); + ASSERT_EQ(0, static_cast<uint8_t>(s[0])); + ASSERT_EQ(1, static_cast<uint8_t>(s[1])); + ASSERT_EQ(0, static_cast<uint8_t>(s[2])); + ASSERT_EQ(2, static_cast<uint8_t>(s[3])); + op.DeleteKeyValue("test", "blob"); + ASSERT_FALSE(op.GetKeyValue(s, "test", "blob")); + } + + db.Close(); +} + + +TEST(SQLiteDatabaseWrapper, Queues) +{ + SQLiteDatabaseWrapper db; // The SQLite DB is in memory + db.Open(); + + { + StatelessDatabaseOperations op(db, false); + op.SetTransactionContextFactory(new DummyTransactionContextFactory); + + ASSERT_EQ(0u, op.GetQueueSize("test")); + op.EnqueueValue("test", "hello"); + ASSERT_EQ(1u, op.GetQueueSize("test")); + op.EnqueueValue("test", "world"); + ASSERT_EQ(2u, op.GetQueueSize("test")); + + std::string s; + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("world", s); + ASSERT_EQ(1u, op.GetQueueSize("test")); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Back)); ASSERT_EQ("hello", s); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Back)); + + op.EnqueueValue("test", "hello"); + op.EnqueueValue("test", "world"); + ASSERT_EQ(2u, op.GetQueueSize("test")); + + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("hello", s); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); ASSERT_EQ("world", s); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front)); + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + op.EnqueueValue("test", blob); // Storing binary values + } + + ASSERT_EQ(1u, op.GetQueueSize("test")); + ASSERT_TRUE(op.DequeueValue(s, "test", QueueOrigin_Front)); + ASSERT_EQ(0u, op.GetQueueSize("test")); + ASSERT_EQ(4u, s.size()); + ASSERT_EQ(0, static_cast<uint8_t>(s[0])); + ASSERT_EQ(1, static_cast<uint8_t>(s[1])); + ASSERT_EQ(0, static_cast<uint8_t>(s[2])); + ASSERT_EQ(2, static_cast<uint8_t>(s[3])); + ASSERT_FALSE(op.DequeueValue(s, "test", QueueOrigin_Front)); + } + + db.Close(); +} + + +TEST_F(DatabaseWrapperTest, BinaryCustomData) +{ + int64_t patient = transaction_->CreateResource("Patient", ResourceType_Patient); + + { + FileInfo info("hello", FileContentType_Dicom, 10, "md5"); + + { + std::string blob; + blob.push_back(0); + blob.push_back(1); + blob.push_back(0); + blob.push_back(2); + info.SetCustomData(blob); + } + + transaction_->AddAttachment(patient, info, 43); + } + + { + FileInfo info; + int64_t revision; + ASSERT_TRUE(transaction_->LookupAttachment(info, revision, patient, FileContentType_Dicom)); + ASSERT_EQ(43u, revision); + ASSERT_EQ("hello", info.GetUuid()); + ASSERT_EQ(CompressionType_None, info.GetCompressionType()); + ASSERT_EQ(10u, info.GetCompressedSize()); + ASSERT_EQ("md5", info.GetCompressedMD5()); + ASSERT_EQ(4u, info.GetCustomData().size()); + ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[0])); + ASSERT_EQ(1, static_cast<uint8_t>(info.GetCustomData()[1])); + ASSERT_EQ(0, static_cast<uint8_t>(info.GetCustomData()[2])); + ASSERT_EQ(2, static_cast<uint8_t>(info.GetCustomData()[3])); + } + + transaction_->DeleteResource(patient); +} \ No newline at end of file
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -26,6 +26,7 @@ #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h" +#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h" #include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h" #include "../../OrthancFramework/Sources/Logging.h" #include "../../OrthancFramework/Sources/SerializationToolbox.h" @@ -140,6 +141,16 @@ { return false; } + + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } }; @@ -147,7 +158,6 @@ { private: bool trailingStepDone_; - protected: virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE { @@ -206,6 +216,16 @@ { s = "DummyInstancesJob"; } + + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE + { + throw OrthancException(ErrorCode_NotImplemented); + } + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE + { + return false; + } }; @@ -528,12 +548,13 @@ class OrthancJobsSerialization : public testing::Test { private: - MemoryStorageArea storage_; - SQLiteDatabaseWrapper db_; // The SQLite DB is in memory - std::unique_ptr<ServerContext> context_; + PluginStorageAreaAdapter storage_; + SQLiteDatabaseWrapper db_; // The SQLite DB is in memory + std::unique_ptr<ServerContext> context_; public: - OrthancJobsSerialization() + OrthancJobsSerialization() : + storage_(new MemoryStorageArea) { db_.Open(); context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */));
--- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -47,6 +47,9 @@ #include <dcmtk/dcmdata/dcdeftag.h> +#if defined(_WIN32) || defined(__CYGWIN__) +#include <windows.h> +#endif using namespace Orthanc; @@ -194,7 +197,7 @@ const unsigned char raw[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 }; std::string latin1((char*) &raw[0], sizeof(raw) / sizeof(char)); - std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1, false); + std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1, false, false); ParsedDicomFile dicom(false); dicom.SetEncoding(Encoding_Latin1); @@ -515,12 +518,16 @@ int main(int argc, char **argv) { +#if defined(_WIN32) && !defined(__MINGW32__) + // Set Windows console output to UTF-8 (otherwise, strings are considered to be in UTF-16. For example, Cyrillic UTF-8 strings appear as garbage without that config) + SetConsoleOutputCP(CP_UTF8); +#endif + Logging::Initialize(); - Toolbox::InitializeGlobalLocale(NULL); SetGlobalVerbosity(Verbosity_Verbose); Toolbox::DetectEndianness(); - SystemToolbox::MakeDirectory("UnitTestsResults"); - OrthancInitialize(); + SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsResults")); + OrthancInitialize(""); ::testing::InitGoogleTest(&argc, argv); int result = RUN_ALL_TESTS();
--- a/OrthancServer/UnitTestsSources/VersionsTests.cpp Mon Apr 14 15:17:11 2025 +0200 +++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp Tue Nov 04 15:58:06 2025 +0100 @@ -195,7 +195,7 @@ TEST(Versions, Civetweb) { ASSERT_EQ(1, CIVETWEB_VERSION_MAJOR); - ASSERT_EQ(14, CIVETWEB_VERSION_MINOR); + ASSERT_EQ(16, CIVETWEB_VERSION_MINOR); ASSERT_EQ(0, CIVETWEB_VERSION_PATCH); } #endif
--- a/TODO Mon Apr 14 15:17:11 2025 +0200 +++ b/TODO Tue Nov 04 15:58:06 2025 +0100 @@ -1,11 +1,3 @@ -current work on C-Get SCU: -- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ? -- SetupPresentationContexts -- handle progress -- handle cancellation when the job is cancelled ? - - - ======================= === Orthanc Roadmap === ======================= @@ -31,7 +23,6 @@ * Option to enable DNS lookups in DICOM: https://orthanc.uclouvain.be/hg/orthanc/file/Orthanc-1.9.3/OrthancFramework/Sources/OrthancFramework.cpp#l88 * Toolbox::ComputeMD5() fails on files larger than 4GB -* Add an option to run Orthanc in read-only mode both for DICOM and for Rest API. * Logging: add more specific information to contextualize the logs. For a DICOM Transfer, that would be nice to include the modality in the context + a study identifier or a job id. * (1) Accept extra DICOM tags dictionaries in the DCMTK format '.dic' (easier to use than declare @@ -74,6 +65,16 @@ * Allow saving PrivateTags in ExtraMainDicomTags. Note: they can actually be stored but they then appear as "Unknown Tag & Data" in the responses. If we try to add the PrivateCreator in the ExtraMainDicomTags, then, the DICOMWeb plugin fails to initialize because the private tags are not known. +* Support hashed passwords in RegisteredUsers. E.g: + "RegisteredUsers": { + "admin": { + "Password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "Hashing": "sha1"} } +* Reuse the ThreadedInstanceLoader from the ArchiveJob in C-Store, peer/store ... to optimize loading + time when working with an object storage. +* Implement a SharedMessageQueue with a max size -> that would greatly simplify the flow control in the + ThreadedInstanceLoader (also used in DicomWeb). + ============================ Documentation (Orthanc Book) @@ -143,6 +144,9 @@ require ffmpeg or a similar library -> can not be done in the Orthanc core. -> keep it for a python plugin -> or require the payload to include rows/columns/cinerate/... +* When creating e.g a PDF series with /tools/create-dicom with a ParentStudy, + the new series does not have the InstitutionName copied from the parent study. + Can be tested with "Add PDF" in OE2. * (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 @@ -168,8 +172,7 @@ The patch that was initialy provided was breaking the IngestTranscoding. This might require a DCMTK decoding plugin ? https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533/9 -* Implement a 'commit' route to force the Stable status earlier. - https://discourse.orthanc-server.org/t/expediting-stability-of-a-dicom-study-new-api-endpoint/1684 + --------- Long-term @@ -184,6 +187,14 @@ DICOM ===== +---------- +Short-term +---------- + +* Investigate stucked C-MOVE that seems to block the job engine: + https://discourse.orthanc-server.org/t/frozen-jobs-and-rest-api-not-responding-after-stuck-c-move/6280/11 + + -------- Mid-term -------- @@ -202,7 +213,6 @@ DicomImageDecoder * Strict hierarchical C-FIND: https://groups.google.com/d/msg/orthanc-users/VBHpeGVSNKM/tkaVvjWFBwAJ -* report DIMSE error codes in Rest API and job status for /store /query /move /retrieve * Log outgoing C-Find queries * Support other Transfer Syntaxes in the Worklist plugin: https://discourse.orthanc-server.org/t/could-you-please-create-an-option-to-set-the-transfer-syntax-in-the-worklist-plugin-currently-little-endian-explicit-is-fixed/4871 @@ -210,7 +220,6 @@ We should implement something like GetDeidentifiedContent(const DcmDataSet& dataset) that would reproduce DcmDataSet::print but hide individual elements that contain PHI. - --------- Long-term --------- @@ -294,20 +303,49 @@ * Provide a C++ callback similar to "ReceivedInstanceFilter()" in Lua https://orthanc.uclouvain.be/book/users/lua.html#filtering-incoming-dicom-instances https://groups.google.com/d/msg/orthanc-users/BtvLTE5Ni8A/vIMhmMgfBAAJ -* Update the SDK to handle buffer sizes > 4GB (all sizes are currently coded in uint32_t) +* Update the SDK to handle buffer sizes > 4GB (currently, sizes are often coded in uint32_t), + only where it makes sense, by introducing additional primitives that use + "OrthancPluginMemoryBuffer64" instead of "OrthancPluginMemoryBuffer" * Add a C-Get SCP handler: OrthancPluginRegisterGetCallback https://groups.google.com/g/orthanc-users/c/NRhPkYX9IXQ/m/mWS11g0jBwAJ * Add a primitive for user authentication (to generate 401 HTTP status, whereas the "RegisterIncomingHttpRequestFilter()" can only generate 403 HTTP status) https://groups.google.com/g/orthanc-users/c/ymtaAmgSs6Q/m/PqVBactQAQAJ -* Add an index on the UUID column in the DelayedDeletion plugin: - https://discourse.orthanc-server.org/t/delayeddeletion-improvement-unique-index-on-pending-uuid-column/4032 + NB: OrthancPluginRegisterHttpAuthentication() was introduced in Orthanc 1.12.9 * Orthanc shall refuse to start if one registers 2 storage plugins. Right now, this is not possible because OrthancPluginRegisterStorageArea2 does not return any value and it can not throw an Exception because that's a core function called from a plugin -> the Exception can not cross the C/C++ frontier safely -> we need a OrthancPluginRegisterStorageArea3 with a return value. Ex: install DelayedDeletion + S3 storage. Right now, the second plugin to load is just ignored with an error message in the logs. +* Queues: There is currently a risk of loosing messages from the Queues: + message = orthanc.DequeueValue("instances-to-process", orthanc.QueueOrigin.FRONT) + # consider Orthanc is interrupted here (hard shutdown) + proccess(message) + The message will never be processed ... + We should have an acknowledge/commit mechanism e.g: + message, messageId = orthanc.DequeueValue2("instances-to-process", orthanc.QueueOrigin.FRONT, 5) # where 5 is a "timeout" + # At this point, the message is still in the queue but will not be dequeued by other consumers. + # If the message is not acknowledged within 5 seconds, it will get back into the queue. + process(message) + orthanc.AcknowledgeQueue("instances-to-process", messageId) + # This requires adding a new "timeout" column in the DB with the reservation_expiration timestamp. + + Note by SJ: Introducing an acknowledgement would greatly complexify + the SDK and the core of Orthanc. I would favor another approach by + introducing 2 functions in the SDK: + - OrthancPluginQueuePeekFront(context, found, target, queueId): Same behavior as + OrthancPluginDequeueValue(), but limited to the back of the queue, and doesn't modify the queue + - OrthancPluginQueuePopFront(context, target): Remove the front element from the queue + As long as OrthancPluginQueuePopFront() is not called, no message would be lost. + Note that we cannot propose "OrthancPluginQueuePeekBack()" or + "OrthancPluginQueuePeek(..., OrthancPluginQueueOrigin_Back)" (i.e., LIFO stacks), as a + concurrent call to "OrthancPluginEnqueueValue()" would alter the back of the queue. + + Note by AM: With PeekFront: 2 consumers on 2 Orthanc instances might consume the same message no ? + +* Add OrthancPluginSetStableStatus2() which would take an additional OrthancPluginResourceType + argument to avoid possible ambiguity about the resource of interest ----------- @@ -374,9 +412,6 @@ Microsoft Windows ----------------- -* Add compatibility with non-ASCII paths (Orthanc expresses its paths - as UTF-8 strings, but Windows expects them to be translated to the - system locale) * Fix error message when stopping the service: https://groups.google.com/g/orthanc-users/c/NyrwUJ9N6Ec/m/sTZIcWvaAgAJ
