From 3e63ff2a3767fd9ceddc14ef95676506ad02dbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Gere=C4=8Dnik?= Date: Sat, 15 Feb 2025 10:13:20 +0100 Subject: [PATCH] Added mammography plugin button on instance level --- .gitignore | 9 + AUTHORS | 27 + CITATION.cff | 14 + CMakeLists.txt | 237 + LICENSE | 661 ++ Plugin/DefaultConfiguration.json | 297 + Plugin/OrthancExplorer.js | 23 + Plugin/Plugin.cpp | 933 ++ README.md | 148 + Resources/CreateDistPackage.sh | 55 + Resources/CreateDistPackage/Dockerfile | 28 + Resources/CreateDistPackage/build.sh | 30 + Resources/MoveTranslations.py | 38 + .../Orthanc/CMake/AutoGeneratedCode.cmake | 80 + Resources/Orthanc/CMake/Compiler.cmake | 303 + .../CMake/DownloadOrthancFramework.cmake | 578 ++ Resources/Orthanc/CMake/DownloadPackage.cmake | 287 + Resources/Orthanc/CMake/EmbedResources.py | 446 + .../CMake/GoogleTestConfiguration.cmake | 91 + .../Plugins/ExportedSymbolsPlugins.list | 7 + .../Plugins/OrthancPluginCppWrapper.cpp | 4117 ++++++++ .../Orthanc/Plugins/OrthancPluginCppWrapper.h | 1511 +++ .../Orthanc/Plugins/OrthancPluginException.h | 91 + .../Plugins/OrthancPluginsExports.cmake | 33 + .../Orthanc/Plugins/VersionScriptPlugins.map | 12 + Resources/Orthanc/README.txt | 2 + .../Sdk-1.11.2/orthanc/OrthancCPlugin.h | 9043 +++++++++++++++++ .../LinuxStandardBaseToolchain.cmake | 101 + .../Toolchains/MinGW-W64-Toolchain32.cmake | 39 + .../Toolchains/MinGW-W64-Toolchain64.cmake | 39 + .../Orthanc/Toolchains/MinGWToolchain.cmake | 42 + Resources/SyncOrthancFolder.py | 78 + TODO | 98 + WebApplication/index.html | 24 + WebApplication/package-lock.json | 1341 +++ WebApplication/package.json | 33 + WebApplication/public/favicon.ico | Bin 0 -> 4286 bytes WebApplication/retrieve-and-view.html | 13 + WebApplication/src/App.vue | 84 + WebApplication/src/AppLanding.vue | 47 + WebApplication/src/AppRetrieveAndView.vue | 33 + WebApplication/src/assets/css/common.css | 75 + .../src/assets/css/defaults-dark.css | 67 + .../src/assets/css/defaults-light.css | 72 + WebApplication/src/assets/css/layout.css | 3 + WebApplication/src/assets/images/orthanc.png | Bin 0 -> 35026 bytes WebApplication/src/assets/logo.png | Bin 0 -> 6849 bytes .../src/components/AddSeriesModal.vue | 335 + .../src/components/BulkLabelsModal.vue | 225 + .../src/components/CopyToClipboardButton.vue | 75 + .../src/components/InstanceDetails.vue | 127 + .../src/components/InstanceItem.vue | 117 + .../src/components/InstanceList.vue | 109 + .../src/components/InstanceListExtended.vue | 128 + WebApplication/src/components/JobItem.vue | 99 + WebApplication/src/components/JobsList.vue | 46 + .../src/components/LabelsEditor.vue | 116 + .../src/components/LanguagePicker.vue | 125 + WebApplication/src/components/Modal.vue | 45 + WebApplication/src/components/ModifyModal.vue | 998 ++ WebApplication/src/components/NotFound.vue | 22 + .../src/components/ResourceButtonGroup.vue | 1006 ++ .../src/components/ResourceDetailText.vue | 78 + .../src/components/RetrieveAndView.vue | 151 + .../src/components/SeriesDetails.vue | 165 + WebApplication/src/components/SeriesItem.vue | 140 + WebApplication/src/components/SeriesList.vue | 149 + WebApplication/src/components/Settings.vue | 221 + .../src/components/SettingsLabels.vue | 222 + .../src/components/SettingsPermissions.vue | 427 + WebApplication/src/components/ShareModal.vue | 200 + WebApplication/src/components/SideBar.vue | 488 + .../src/components/StudyDetails.vue | 194 + WebApplication/src/components/StudyItem.vue | 328 + WebApplication/src/components/StudyList.vue | 1232 +++ WebApplication/src/components/TagsTree.vue | 84 + .../src/components/TokenLanding.vue | 57 + .../src/components/TokenLinkButton.vue | 142 + .../src/components/UploadHandler.vue | 216 + .../src/components/UploadReport.vue | 109 + WebApplication/src/globalConfigurations.js | 3 + .../src/helpers/clipboard-helpers.js | 39 + WebApplication/src/helpers/date-helpers.js | 91 + WebApplication/src/helpers/label-helpers.js | 22 + .../src/helpers/resource-helpers.js | 89 + WebApplication/src/helpers/source-type.js | 7 + WebApplication/src/locales/de.json | 213 + WebApplication/src/locales/en.json | 270 + WebApplication/src/locales/es.json | 190 + WebApplication/src/locales/fr.json | 270 + WebApplication/src/locales/i18n.js | 48 + WebApplication/src/locales/it.json | 190 + WebApplication/src/locales/ka.json | 190 + WebApplication/src/locales/ru.json | 213 + WebApplication/src/locales/si.json | 217 + WebApplication/src/locales/uk.json | 214 + WebApplication/src/locales/zh.json | 208 + WebApplication/src/main-landing.js | 41 + WebApplication/src/main-retrieve-and-view.js | 41 + WebApplication/src/main.js | 115 + WebApplication/src/orthancApi.js | 682 ++ WebApplication/src/router.js | 71 + WebApplication/src/store/index.js | 18 + .../src/store/modules/configuration.js | 176 + WebApplication/src/store/modules/i18n.js | 21 + WebApplication/src/store/modules/jobs.js | 73 + WebApplication/src/store/modules/labels.js | 39 + WebApplication/src/store/modules/studies.js | 369 + WebApplication/token-landing.html | 13 + WebApplication/vite.config.js | 40 + release-notes.md | 556 + scripts/check-translations.py | 82 + scripts/logs/.gitignore | 1 + scripts/nginx-dev.conf | 80 + scripts/start-nginx.sh | 11 + scripts/stop-nginx.sh | 11 + tests/manual-tests.md | 163 + tests/stimuli/TEST_1/10.dcm | Bin 0 -> 297374 bytes tests/stimuli/TEST_1/12.dcm | Bin 0 -> 295402 bytes 119 files changed, 34543 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CITATION.cff create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 Plugin/DefaultConfiguration.json create mode 100644 Plugin/OrthancExplorer.js create mode 100644 Plugin/Plugin.cpp create mode 100644 README.md create mode 100644 Resources/CreateDistPackage.sh create mode 100644 Resources/CreateDistPackage/Dockerfile create mode 100644 Resources/CreateDistPackage/build.sh create mode 100644 Resources/MoveTranslations.py create mode 100644 Resources/Orthanc/CMake/AutoGeneratedCode.cmake create mode 100644 Resources/Orthanc/CMake/Compiler.cmake create mode 100644 Resources/Orthanc/CMake/DownloadOrthancFramework.cmake create mode 100644 Resources/Orthanc/CMake/DownloadPackage.cmake create mode 100644 Resources/Orthanc/CMake/EmbedResources.py create mode 100644 Resources/Orthanc/CMake/GoogleTestConfiguration.cmake create mode 100644 Resources/Orthanc/Plugins/ExportedSymbolsPlugins.list create mode 100644 Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp create mode 100644 Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h create mode 100644 Resources/Orthanc/Plugins/OrthancPluginException.h create mode 100644 Resources/Orthanc/Plugins/OrthancPluginsExports.cmake create mode 100644 Resources/Orthanc/Plugins/VersionScriptPlugins.map create mode 100644 Resources/Orthanc/README.txt create mode 100644 Resources/Orthanc/Sdk-1.11.2/orthanc/OrthancCPlugin.h create mode 100644 Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake create mode 100644 Resources/Orthanc/Toolchains/MinGW-W64-Toolchain32.cmake create mode 100644 Resources/Orthanc/Toolchains/MinGW-W64-Toolchain64.cmake create mode 100644 Resources/Orthanc/Toolchains/MinGWToolchain.cmake create mode 100644 Resources/SyncOrthancFolder.py create mode 100644 TODO create mode 100644 WebApplication/index.html create mode 100644 WebApplication/package-lock.json create mode 100644 WebApplication/package.json create mode 100644 WebApplication/public/favicon.ico create mode 100644 WebApplication/retrieve-and-view.html create mode 100644 WebApplication/src/App.vue create mode 100644 WebApplication/src/AppLanding.vue create mode 100644 WebApplication/src/AppRetrieveAndView.vue create mode 100644 WebApplication/src/assets/css/common.css create mode 100644 WebApplication/src/assets/css/defaults-dark.css create mode 100644 WebApplication/src/assets/css/defaults-light.css create mode 100644 WebApplication/src/assets/css/layout.css create mode 100644 WebApplication/src/assets/images/orthanc.png create mode 100644 WebApplication/src/assets/logo.png create mode 100644 WebApplication/src/components/AddSeriesModal.vue create mode 100644 WebApplication/src/components/BulkLabelsModal.vue create mode 100644 WebApplication/src/components/CopyToClipboardButton.vue create mode 100644 WebApplication/src/components/InstanceDetails.vue create mode 100644 WebApplication/src/components/InstanceItem.vue create mode 100644 WebApplication/src/components/InstanceList.vue create mode 100644 WebApplication/src/components/InstanceListExtended.vue create mode 100644 WebApplication/src/components/JobItem.vue create mode 100644 WebApplication/src/components/JobsList.vue create mode 100644 WebApplication/src/components/LabelsEditor.vue create mode 100644 WebApplication/src/components/LanguagePicker.vue create mode 100644 WebApplication/src/components/Modal.vue create mode 100644 WebApplication/src/components/ModifyModal.vue create mode 100644 WebApplication/src/components/NotFound.vue create mode 100644 WebApplication/src/components/ResourceButtonGroup.vue create mode 100644 WebApplication/src/components/ResourceDetailText.vue create mode 100644 WebApplication/src/components/RetrieveAndView.vue create mode 100644 WebApplication/src/components/SeriesDetails.vue create mode 100644 WebApplication/src/components/SeriesItem.vue create mode 100644 WebApplication/src/components/SeriesList.vue create mode 100644 WebApplication/src/components/Settings.vue create mode 100644 WebApplication/src/components/SettingsLabels.vue create mode 100644 WebApplication/src/components/SettingsPermissions.vue create mode 100644 WebApplication/src/components/ShareModal.vue create mode 100644 WebApplication/src/components/SideBar.vue create mode 100644 WebApplication/src/components/StudyDetails.vue create mode 100644 WebApplication/src/components/StudyItem.vue create mode 100644 WebApplication/src/components/StudyList.vue create mode 100644 WebApplication/src/components/TagsTree.vue create mode 100644 WebApplication/src/components/TokenLanding.vue create mode 100644 WebApplication/src/components/TokenLinkButton.vue create mode 100644 WebApplication/src/components/UploadHandler.vue create mode 100644 WebApplication/src/components/UploadReport.vue create mode 100644 WebApplication/src/globalConfigurations.js create mode 100644 WebApplication/src/helpers/clipboard-helpers.js create mode 100644 WebApplication/src/helpers/date-helpers.js create mode 100644 WebApplication/src/helpers/label-helpers.js create mode 100644 WebApplication/src/helpers/resource-helpers.js create mode 100644 WebApplication/src/helpers/source-type.js create mode 100644 WebApplication/src/locales/de.json create mode 100644 WebApplication/src/locales/en.json create mode 100644 WebApplication/src/locales/es.json create mode 100644 WebApplication/src/locales/fr.json create mode 100644 WebApplication/src/locales/i18n.js create mode 100644 WebApplication/src/locales/it.json create mode 100644 WebApplication/src/locales/ka.json create mode 100644 WebApplication/src/locales/ru.json create mode 100644 WebApplication/src/locales/si.json create mode 100644 WebApplication/src/locales/uk.json create mode 100644 WebApplication/src/locales/zh.json create mode 100644 WebApplication/src/main-landing.js create mode 100644 WebApplication/src/main-retrieve-and-view.js create mode 100644 WebApplication/src/main.js create mode 100644 WebApplication/src/orthancApi.js create mode 100644 WebApplication/src/router.js create mode 100644 WebApplication/src/store/index.js create mode 100644 WebApplication/src/store/modules/configuration.js create mode 100644 WebApplication/src/store/modules/i18n.js create mode 100644 WebApplication/src/store/modules/jobs.js create mode 100644 WebApplication/src/store/modules/labels.js create mode 100644 WebApplication/src/store/modules/studies.js create mode 100644 WebApplication/token-landing.html create mode 100644 WebApplication/vite.config.js create mode 100644 release-notes.md create mode 100644 scripts/check-translations.py create mode 100644 scripts/logs/.gitignore create mode 100644 scripts/nginx-dev.conf create mode 100644 scripts/start-nginx.sh create mode 100644 scripts/stop-nginx.sh create mode 100644 tests/manual-tests.md create mode 100644 tests/stimuli/TEST_1/10.dcm create mode 100644 tests/stimuli/TEST_1/12.dcm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..310c943 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +WebApplication/node_modules +WebApplication/dist/ +ThirdPartyDownloads/ +.DS_Store +dist-ssr +*.local +.vscode/ +*~ +build \ No newline at end of file diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..7a7a95f --- /dev/null +++ b/AUTHORS @@ -0,0 +1,27 @@ +Orthanc Explorer 2 +================== + + +Authors +------- + +* Orthanc Team SRL + Rue Joseph Marchal 14 + 4910 Theux + Belgium + https://orthanc.team/ + +* Sebastien Jodogne + +* Osimis S.A. + Quai Banning 6 + 4000 Liege + Belgium + +* ICTEAM, UCLouvain + Place de l'Universite 1 + 1348 Ottignies-Louvain-la-Neuve + Belgium + https://uclouvain.be/icteam + +* Luc Billaud diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..f871c69 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,14 @@ +cff-version: "1.1.0" +message: "If you use this software, please cite it using these metadata." +title: Orthanc +abstract: "Orthanc is a lightweight open-source DICOM server for medical imaging supporting representational state transfer (REST)." +authors: + - + affiliation: UCLouvain + family-names: Jodogne + given-names: "Sébastien" +doi: "10.1007/s10278-018-0082-y" +license: "GPL-3.0-or-later" +repository-code: "https://orthanc.uclouvain.be/hg/orthanc/" +version: 1.12.3 +date-released: 2024-01-31 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5a3f5dc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,237 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2024 Osimis S.A., Belgium +# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero 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 +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +cmake_minimum_required(VERSION 3.1) + +if (APPLE) + set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "Build architectures for Mac OS X" FORCE) + set(CMAKE_CXX_STANDARD 11) +endif(APPLE) + +project(OrthancExplorer2) + +set(PLUGIN_VERSION "mainline" CACHE STRING "plugin version 'mainline' or '1.2.3'") + +if (PLUGIN_VERSION MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$") + set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.2") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") + set(ORTHANC_OE2_VERSION ${PLUGIN_VERSION}) +else() + set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline") + set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") + set(ORTHANC_OE2_VERSION "mainline") +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 ON 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\"") +set(WEBAPP_DIST_PATH "${CMAKE_SOURCE_DIR}/WebApplication/dist" CACHE STRING "Path to the WebApplication/dist folder (the output of 'npm run build')") +set(WEBAPP_DIST_SOURCE "LOCAL" CACHE STRING "Source for the WebApplication/dist folder ('LOCAL' = assume it is build localy, 'WEB' = download it from web)") +set(WEBAPP_DIST_VERSION "${PLUGIN_VERSION}" CACHE STRING "Version of WebApplication/dist folder to download") +file(TO_CMAKE_PATH "${WEBAPP_DIST_PATH}" WEBAPP_DIST_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_SDK_VERSION "1.11.2" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"1.11.2\")") +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 +include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake) + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system") + if (ORTHANC_FRAMEWORK_USE_SHARED) + include(FindBoost) + find_package(Boost COMPONENTS 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}) + + set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test") + set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)") + mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE) + include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/CMake/GoogleTestConfiguration.cmake) + +else() + include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake) + + set(ENABLE_LOCALE OFF) # Enable support for locales (notably in Boost) + #set(ENABLE_GOOGLE_TEST ON) + #set(ENABLE_WEB_CLIENT ON) + + # Those modules of the Orthanc framework are not needed + set(ENABLE_MODULE_IMAGES OFF) + set(ENABLE_MODULE_JOBS OFF) + set(ENABLE_MODULE_DICOM OFF) + + include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkConfiguration.cmake) + include_directories(${ORTHANC_FRAMEWORK_ROOT}) +endif() + + +include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake) + + +if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK) + if (ORTHANC_SDK_VERSION STREQUAL "1.11.2") + include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-1.11.2) + else() + message(FATAL_ERROR "Unsupported version of the Orthanc plugin SDK: ${ORTHANC_SDK_VERSION}") + 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 (${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_OE2_VERSION} "Orthanc Explorer 2 plugin" OrthancExplorer2.dll + "Advanced User Interface Plugin for Orthanc" + ERROR_VARIABLE Failure + OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/Version.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + list(APPEND AUTOGENERATED_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/Version.rc) +endif() + + +if (APPLE) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework CoreFoundation") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -framework CoreFoundation") +endif() + +if (${WEBAPP_DIST_SOURCE} STREQUAL "WEB") + SET(WEBAPP_DIST_PATH "${CMAKE_BINARY_DIR}/dist") # downloading the WEB version -> don't modify source folder + if (${WEBAPP_DIST_VERSION} STREQUAL "mainline") + set(WEBAPP_DIST_VERSION "master") # the branch is named master, not mainline + endif() + DownloadPackage(no-check "https://public-files.orthanc.team/oe2-dist-files/${WEBAPP_DIST_VERSION}.zip" ${WEBAPP_DIST_PATH}) +endif() + +if (STANDALONE_BUILD) + add_definitions(-DORTHANC_STANDALONE=1) + set(ADDITIONAL_RESOURCES + DEFAULT_CONFIGURATION ${CMAKE_SOURCE_DIR}/Plugin/DefaultConfiguration.json + WEB_APPLICATION_ASSETS ${WEBAPP_DIST_PATH}/assets + WEB_APPLICATION_FAVICON ${WEBAPP_DIST_PATH}/favicon.ico + WEB_APPLICATION_INDEX ${WEBAPP_DIST_PATH}/index.html + WEB_APPLICATION_INDEX_LANDING ${WEBAPP_DIST_PATH}/token-landing.html + WEB_APPLICATION_INDEX_RETRIEVE_AND_VIEW ${WEBAPP_DIST_PATH}/retrieve-and-view.html + DEFAULT_CSS_LIGHT ${CMAKE_SOURCE_DIR}/WebApplication/src/assets/css/defaults-light.css + DEFAULT_CSS_DARK ${CMAKE_SOURCE_DIR}/WebApplication/src/assets/css/defaults-dark.css + ) +else() + add_definitions(-DORTHANC_STANDALONE=0) +endif() + +EmbedResources( + --no-upcase-check + ${ADDITIONAL_RESOURCES} + ORTHANC_EXPLORER ${CMAKE_SOURCE_DIR}/Plugin/OrthancExplorer.js + ) + + +# As the embedded resources are shared by both the "UnitTests" and the +# "OrthancExplorer2" targets, avoid race conditions in the code +# generation by adding a target between them +add_custom_target( + AutogeneratedTarget + DEPENDS + ${AUTOGENERATED_SOURCES} + ) + + +add_definitions( + -DHAS_ORTHANC_EXCEPTION=1 + -DORTHANC_ENABLE_LOGGING_PLUGIN=1 + ) + +set(CORE_SOURCES +# Plugin/Configuration.cpp + + ${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp + ${ORTHANC_CORE_SOURCES} + ) + +add_library(OrthancExplorer2 SHARED ${CORE_SOURCES} + ${CMAKE_SOURCE_DIR}/Plugin/Plugin.cpp + ${AUTOGENERATED_SOURCES} + ) + +add_dependencies(OrthancExplorer2 AutogeneratedTarget) + +DefineSourceBasenameForTarget(OrthancExplorer2) + +message("Setting the version of the library to ${ORTHANC_OE2_VERSION}") + +add_definitions(-DORTHANC_OE2_VERSION="${ORTHANC_OE2_VERSION}") + +set_target_properties(OrthancExplorer2 PROPERTIES + VERSION ${ORTHANC_OE2_VERSION} + SOVERSION ${ORTHANC_OE2_VERSION} + ) + +install( + TARGETS OrthancExplorer2 + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) + +# add_executable(UnitTests +# ${AUTOGENERATED_SOURCES} +# ${CORE_SOURCES} +# ${GOOGLE_TEST_SOURCES} +# # UnitTestsSources/UnitTestsMain.cpp +# ) + +# add_dependencies(UnitTests AutogeneratedTarget) + +# target_link_libraries(UnitTests +# ${GOOGLE_TEST_LIBRARIES} +# ) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/Plugin/DefaultConfiguration.json b/Plugin/DefaultConfiguration.json new file mode 100644 index 0000000..6139329 --- /dev/null +++ b/Plugin/DefaultConfiguration.json @@ -0,0 +1,297 @@ +{ + "OrthancExplorer2" : { + // enables or disables the plugin completely + "Enable": true, + + // Prefix URL of the OE2 application (and API) + // A value of '/my-ui' means that the app will be available under /my-ui/app/ + // and the api will be available under /my-ui/api/... + // Should start and end with a '/' + "Root" : "/ui/", + + // Whether OE2 shall replace the default Orthanc Explorer interface ('/' URL will redirect to OE2) + "IsDefaultOrthancUI": true, + + // Base theme for the UI (before custom CSS are applied). + // Allowed values: "light", "dark" + "Theme": "light", + + // Path to custom CSS file or logo. + // The custom CSS are applied after these default files and they are loaded after all other CSS. + // You may actually override any CSS value or variable from the application. + // - https://github.com/orthanc-server/orthanc-explorer-2/blob/master/WebApplication/src/assets/css/defaults-dark.css + // - https://github.com/orthanc-server/orthanc-explorer-2/blob/master/WebApplication/src/assets/css/defaults-light.css + // "CustomCssPath": "/home/my/path/to/custom.css", + // "CustomLogoUrl": "https://my.company/logo.png", + // "CustomLogoPath": "/home/my/path/to/logo.png", + // "CustomFavIconPath": "/home/my/path/to/favicon.ico", // note: this can be a .png, .svg or .ico file + + // Custom Window/Tab Title + // "CustomTitle": "Orthanc Explorer 2", + + // This block of configuration is transmitted as is to the frontend application. + // Make sure not to store any secret here + "UiOptions" : { + // note: all the "Enable..." variables can be set to false by the user-profile (if using the authorization plugin) + + "EnableStudyList": true, // Enables the access to the study list (TODO) + "EnableUpload": true, // Enables the upload menu/interface + "EnableDicomModalities": true, // Enables the 'DICOM Modalities' interface in the side menu + "EnableDeleteResources": true, // Enables the delete button for Studies/Series/Instances + "EnableDownloadZip": true, // Enables the download zip button for Studies/Series + "EnableDownloadDicomDir": false, // Enables the download DICOM DIR button for Studies/Series + "EnableDownloadDicomFile": true, // Enables the download DICOM file button for Instances + "EnableAnonymization": true, // Enables the anonymize button + "EnableModification": true, // Enables the modify button + "EnableSendTo": true, // Enables the 'SendTo' button for Studies/Series/Instances + "EnableApiViewMenu": false, // Enables the API button to open API routes for Studies/Series/Instances (developer mode) + "EnableSettings": true, // Enables the settings menu/interface + "EnableLinkToLegacyUi": true, // Enables a link to the legacy Orthanc UI + "EnableChangePassword": true, // Enables the 'change password' button in the side bar. Only applicable if Keycloak is enabled + "EnableViewerQuickButton": true, // Enables a button in the study list to directly open a viewer + "EnableReportQuickButton": false, // Enables a button in the study list to directly open a PDF report if available in the study + + "EnableEditLabels": true, // Enables labels management (create/delete/assign/unassign) + "AvailableLabels": [], // If not empty, this list prevents the creation of new labels and only allows add/remove of the listed labels. + // This configuration may be overriden when you use Keycloak and an auth-service that implements roles/permissions API. + "EnableLabelsCount": true, // Enables display of study count next to the each label (this might slow down the UI) + + "EnableShares": false, // Enables sharing studies. See "Tokens" section below. + "DefaultShareDuration": 0, // [in days]. 0 means no expiration date, + "ShareDurations": [0, 7, 15, 30, 90, 365], // The share durations proposed in the UI + + "EnableAddSeries": true, // Enables the "Add series" button + "AddSeriesDefaultTags": { // Default tag values when adding a new series of each type ("pdf", "image" or "stl") + "pdf" : { + "SeriesDescription": "Report", + "Modality": "DOC", + "SeriesDate": "$TODAY$" // Allowed keywords: $TODAY$, $STUDY_DATE$, $FILE_DATE$ + }, + "image" : { + "SeriesDescription": "Picture", + "Modality": "XC", + "SeriesDate": "$TODAY$" + }, + "stl" : { + "SeriesDescription": "Model", + "Modality": "M3D", + "SeriesDate": "$TODAY$" + } + }, + + "EnabledMammographyPlugin": false, + + // If both OHIF viewer configurations are enabled, only the v3 is taken into account + "EnableOpenInOhifViewer": false, // Enables a "open in OHIF viewer" button. Note: OHIF can not be used together with KeyCloak (https://community.ohif.org/t/ohif-orthanc-token-to-access-a-single-study/727) + "OhifViewerPublicRoot" : "http://to-be-defined/", // must end with a '/'. e.g: "http://ohif.my.site/" means that link to OHIF will look like http://ohif.my.site/Viewer/1.2.3.444.5555.... + "EnableOpenInOhifViewer3": false, // Enables a "open in OHIF viewer" button. If the OHIF plugin is loaded, the default value is 'true' + "OhifViewer3PublicRoot" : "/ohif/", // must end with a '/'. e.g: "http://ohif.my.site/" means that link to OHIF will look like http://ohif.my.site/viewer?StudyInstanceUIDs=1.2.3.444.5555.... + + "EnableOpenInMedDreamViewer": false, // Enables a "open in MedDream viewer" button + "MedDreamViewerPublicRoot" : "http://to-be-defined/", // must end with a '/'. e.g: "http://meddream.my.site/Viewer/" means that link to MedDream will look like http://meddream.my.site/?study=1.2.3.444.5555.... + + // Defines which icons is used by which enabled viewer. + // Allowed keys: "meddream", "osimis-web-viewer", "stone-webviewer", "ohif", "ohif-vr", "ohif-tmtv" + // Allowed values: CSS class that defines the viewer icons (only from bootstrap icons) + "ViewersIcons" : { + "osimis-web-viewer": "bi bi-eye", + "stone-webviewer": "bi bi-eye-fill", + "ohif": "bi bi-grid", + "ohif-vr": "bi bi-grid-1x2", + "ohif-tmtv": "bi bi-grid-3x3-gap", + "ohif-seg": "fa-solid fa-shapes fa-button", + "ohif-micro": "fa-solid fa-microscope fa-button", + "meddream": "bi bi-columns-gap", + "volview": "bi bi-box", + "wsi": "fa-solid fa-microscope fa-button" + }, + // Defines the order in which the viewer icons should appear in the interface + // OHIF viewers modes that are not listed here, won't appear in the interface. + "ViewersOrdering" : [ + // "osimis-web-viewer", // now deprecated + "stone-webviewer", + "ohif", + "ohif-vr", + "ohif-tmtv", + "ohif-seg", + // "ohif-micro", // currently disabled, this is still experimental in OHIF + "meddream", + "volview", + "wsi" + ], + + "MaxStudiesDisplayed": 100, // The maximum number of studies displayed in the study list. + // From v 1.7.0, this option is not used anymore in the local study list when using + // a DB backend that supports ExtendedFind (SQLite and PostgreSQL) + // but is still used in the DicomWeb queries. + "PageLoadSize": 50, // The number of items that are loaded when scrolling the study or instance list. + // Only applicable with a DB backend that supports ExtendedFind. + + "MaxMyJobsHistorySize": 5, // The maximum number of jobs appearing under 'my jobs' in side bar (0 = unlimited) + + "StudyListSearchMode": "search-as-you-type",// mode to trigger a search in the StudyList. Accepted values: 'search-as-you-type' or 'search-button' + "StudyListSearchAsYouTypeMinChars": 3, // minimum number of characters to enter in a text search field before it starts searching the DB + "StudyListSearchAsYouTypeDelay": 400, // Delay [ms] between the last key stroke and the trigger of the search + "StudyListContentIfNoSearch": "most-recents", // Defines what to show if no search criteria has been entered + // Allowed values: "empty", "most-recents" + // From v 1.7.0, this option is always considered as "most-recents" when using + // a DB backend that supports ExtendedFind (SQLite and PostgreSQL) + + // Default settings are ok for "small" Orthanc Databases. For large databases, it is recommended to use these settings: + // "StudyListSearchMode": "search-button" + // "StudyListContentIfNoSearch": "empty" + + "ShowOrthancName": true, // display the Orthanc Name in the side menu + + // The list of tags to be displayed in the upload dialog result list + // (the first N defined tags in the list are displayed on the UI) + // Allowed values are: "StudyDate", "AccessionNumber", "PatientID", + // "PatientName", "PatientBirthDate", "StudyDescription" + "UploadReportTags" : [ + "PatientName", + "StudyDescription", + "PatientID", + "AccessionNumber", + "StudyDate" + ], + "UploadReportMaxTags" : 2, // See above, the max number of tags displayed in the upload report + + // The ordered list of columns to display in the study list. + // Allowed values are: + // - Dicom Tags: "StudyDate", "AccessionNumber", "PatientID" + // "PatientName", "PatientBirthDate", "StudyDescription" + // - special columns: + // - "modalities": the list of modalities in the study + // - "seriesCount": the number of series in the study + // - "instancesCount": the number of instances in the study + // - "seriesAndInstancesCount": a combined value with the number of series/instances in the study + "StudyListColumns" : [ + "PatientBirthDate", + "PatientName", + "PatientID", + "StudyDescription", + "StudyDate", + "modalities", + "AccessionNumber", + "seriesAndInstancesCount" + ], + + // The list of patient level tags that are displayed in the study details and in the modification dialog. + // Note that these tags must be defined in the Orthanc main dicom tags (https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html) + "PatientMainTags" : [ + "PatientID", + "PatientName", + "PatientBirthDate", + "PatientSex", + "OtherPatientIDs" + ], + + // The list of study level tags that are displayed in the study details and in the modification dialog. + // Note that these tags must be defined in the Orthanc main dicom tags (https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html) + "StudyMainTags" : [ + "StudyDate", + "StudyTime", + "StudyDescription", + "AccessionNumber", + "StudyID", + "StudyInstanceUID", + "RequestingPhysician", + "ReferringPhysicianName", + "InstitutionName" + ], + + // The list of series level tags that are displayed in the study details and in the modification dialog. + // Note that these tags must be defined in the Orthanc main dicom tags (https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html) + "SeriesMainTags" : [ + "SeriesDate", + "SeriesTime", + "SeriesDescription", + "SeriesNumber", + "BodyPartExamined", + "ProtocolName", + "SeriesInstanceUID" + ], + + // The modalities to display in the Modalities filter dropdown in the Study List + "ModalitiesFilter": [ + "CR", "CT", "DOC", "DR", "DX", "KO", "MG", "MR", "NM", "OT", "PR", "PT", "PX", "RTDOSE", "RTSTRUCT", "RTPLAN", "SEG", "SR", "US", "XA", "XC" + ], + + // Defines the list of languages available in the language picker + // ex: "AvailableLanguages" : ["en", "fr"], + // ex: "AvailableLanguages" : [] -> this won't show the language picker at all and force usage of the DefaultLanguage + "AvailableLanguages": ["en", "de", "es", "fr", "it", "ka", "ru", "si", "uk", "zh"], + + // Force the default language. If null (default), the language is the language from the visitor's browser. + // ex: "DefaultLanguage" : "en" + "DefaultLanguage" : null, + + // Should DicomTags be translated (true) or shall we use the English symbolic name whatever the selected language (false) + // if true: "PatientID" is displayed as e.g "ID Patient" in french + // if false: "PatientID" is displayed as "PatientID" in all languages + "TranslateDicomTags" : true, + + // Display format for dates in the study list based on these definitions: https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + // example: "dd/MM/yyyy" for European Format, + // "yyyyMMdd" for DICOM format + "DateFormat": "yyyyMMdd", + + // PatientName transformation for display. This requires a Regex for capturing and one expression for formatting. + // If PatientNameFormatting is not defined, no transformation occurs (this is the default behaviour). + // The Regex is a Javascript regex and is applied this way: + // FormattedName = PatientName.replace(new RegExp(PatientNameCapture), PatientNameFormatting); + // in example, to capture carets separated names and re-order them, one would use: + // FormattedName = PatientName.replace(new RegExp("([^\\^]+)\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?"), '$1 $3 $2'); + "PatientNameCapture" : "([^\\^]+)\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?", + // "PatientNameFormatting": "$1 $3 $2" + + // modifications dialog options + "Modifications": { + // Modes define options to the /modify route: + // "modify-keep-uids" is equivalent to KeepSource=True and Keep=["StudyInstanceUID", "SeriesInstanceUID", "SOPInstanceUID"] + // "modify-new-uids" is equivalent to KeepSource=False + // "duplicate" is equivalent to KeepSource=True + // "AllowedModes" and "DefaultMode" apply to studies modification + "AllowedModes": ["modify-new-uids", "modify-keep-uids", "duplicate"], + "DefaultMode": "modify-new-uids", + + // same configurations for Series (introduced in 1.2.0) + "SeriesAllowedModes": ["modify-new-uids", "modify-keep-uids", "duplicate"], + "SeriesDefaultMode": "modify-new-uids" + + }, + + // List of tags that are used to identify studies belonging to the same patient. + // This appears as "This patient has {count} studies in total" in the study details. + // ex: "ShowSamePatientStudiesFilter" : ["PatientID", "PatientBirthDate", "PatientSex"] + "ShowSamePatientStudiesFilter" : [ + "PatientID" + ] + }, + + "Shares" : { + "TokenService" : { + "Url": "http://change-me:8000/shares", + "Username": "change-me", + "Password": "change-me" + }, + "Type": "osimis-viewer-publication" // allowed values: "osimis-viewer-publication", "meddream-viewer-publication", "stone-viewer-publication" + }, + + // When using Keycloak for user management + "Keycloak" : { + "Enable": false, + "Url": "http://change-me:8080/", + "Realm": "change-me", + "ClientId": "change-me" + }, + + + // this section is only relevant if the authorization plugin is enabled and user-profile based permissions are implemented + "Tokens" : { + "InstantLinksValidity": 200, // the duration, in seconds an 'instant' token is valid (e.g. used to download a study, open a study in a viewer) + "ShareType": "stone-viewer-publication" // allowed values: "stone-viewer-publication", "osimis-viewer-publication", "meddream-viewer-publication" + //"RequiredForLinks": false // experimental, set it to false when using basic-auth together with the auth-plugin (https://discourse.orthanc-server.org/t/user-based-access-control-with-label-based-resource-access/5454) + } + } +} \ No newline at end of file diff --git a/Plugin/OrthancExplorer.js b/Plugin/OrthancExplorer.js new file mode 100644 index 0000000..17971a1 --- /dev/null +++ b/Plugin/OrthancExplorer.js @@ -0,0 +1,23 @@ +$('#lookup').live('pagebeforeshow', function() { + $('#open-oe2').remove(); + + var b = $('
') + .attr('id', 'open-oe2') + .addClass('ui-grid-b') + .append($('
') + .addClass('ui-block-a')) + .append($('
') + .addClass('ui-block-b') + .append($('') + .attr('data-role', 'button') + .attr('href', '#') + .attr('data-icon', 'forward') + .attr('data-theme', 'a') + .text('Open Orthanc Explorer 2') + .button() + .click(function(e) { + window.open('../${OE2_BASE_URL}/app/index.html'); + }))); + + b.insertAfter($('#lookup-result')); +}); diff --git a/Plugin/Plugin.cpp b/Plugin/Plugin.cpp new file mode 100644 index 0000000..a631e9a --- /dev/null +++ b/Plugin/Plugin.cpp @@ -0,0 +1,933 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + +#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#include +#include +#include +#include + +#include + +#define ORTHANC_PLUGIN_NAME "orthanc-explorer-2" + +// we are using Orthanc 1.11.0 API (RequestedTags in tools/find) +#define ORTHANC_CORE_MINIMAL_MAJOR 1 +#define ORTHANC_CORE_MINIMAL_MINOR 11 +#define ORTHANC_CORE_MINIMAL_REVISION 0 + + +std::unique_ptr orthancFullConfiguration_; +Json::Value pluginJsonConfiguration_; +std::string oe2BaseUrl_; + +Json::Value pluginsConfiguration_; +bool hasUserProfile_ = false; +bool openInOhifV3IsExplicitelyDisabled = false; +bool enableShares_ = false; +bool isReadOnly_ = false; +std::string customCssPath_; +std::string theme_ = "light"; +std::string customLogoPath_; +std::string customLogoUrl_; +std::string customFavIconPath_; +std::string customTitle_; + +enum CustomFilesPath +{ + CustomFilesPath_Logo, + CustomFilesPath_FavIcon +}; + + +template +void ServeEmbeddedFolder(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + std::string path = "/" + std::string(request->groups[0]); + Orthanc::MimeType mimeType = Orthanc::SystemToolbox::AutodetectMimeType(path); + const char* mime = Orthanc::EnumerationToString(mimeType); + + std::string fileContent; + Orthanc::EmbeddedResources::GetDirectoryResource(fileContent, folder, path.c_str()); + + const char* resource = fileContent.size() ? fileContent.c_str() : NULL; + OrthancPluginAnswerBuffer(context, output, resource, fileContent.size(), mime); + } +} + +template +void ServeEmbeddedFile(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + std::string s; + Orthanc::EmbeddedResources::GetFileResource(s, file); + + if (file == Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX && theme_ != "light") + { + boost::replace_all(s, "data-bs-theme=\"light\"", "data-bs-theme=\"" + theme_ + "\""); + } + + const char* resource = s.size() ? s.c_str() : NULL; + OrthancPluginAnswerBuffer(context, output, resource, s.size(), Orthanc::EnumerationToString(mime)); + } +} + +template +void ServeCustomFile(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + std::string fileContent; + std::string customFileContent; + std::string customFilePath; + if (customFile == CustomFilesPath_FavIcon) + { + customFilePath = customFavIconPath_; + } + else if (customFile == CustomFilesPath_Logo) + { + customFilePath = customLogoPath_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + Orthanc::SystemToolbox::ReadFile(fileContent, customFilePath); + Orthanc::MimeType mimeType = Orthanc::SystemToolbox::AutodetectMimeType(customFilePath); + + // include an ETag for correct cache handling + OrthancPlugins::OrthancString md5; + size_t size = fileContent.size(); + md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), fileContent.c_str(), size)); + + std::string etag = "\"" + std::string(md5.GetContent()) + "\""; + OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str()); + OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Cache-Control", "no-cache"); + + OrthancPluginAnswerBuffer(context, output, fileContent.c_str(), size, Orthanc::EnumerationToString(mimeType)); + } +} + +// serves either the default CSS or a custom file CSS +void ServeCustomCss(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + std::string cssFileContent; + + if (strstr(url, "custom.css") != NULL) + { + if (theme_ == "dark") + { + Orthanc::EmbeddedResources::GetFileResource(cssFileContent, Orthanc::EmbeddedResources::DEFAULT_CSS_DARK); + } + else + { + Orthanc::EmbeddedResources::GetFileResource(cssFileContent, Orthanc::EmbeddedResources::DEFAULT_CSS_LIGHT); + } + + if (!customCssPath_.empty()) + { // append the custom CSS + std::string customCssFileContent; + Orthanc::SystemToolbox::ReadFile(customCssFileContent, customCssPath_); + cssFileContent += "\n/* Appending the custom CSS */\n" + customCssFileContent; + } + } + + const char* resource = cssFileContent.size() ? cssFileContent.c_str() : NULL; + size_t size = cssFileContent.size(); + + // include an ETag for correct cache handling + OrthancPlugins::OrthancString md5; + md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), resource, size)); + + std::string etag = "\"" + std::string(md5.GetContent()) + "\""; + OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str()); + OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Cache-Control", "no-cache"); + + OrthancPluginAnswerBuffer(context, output, resource, size, Orthanc::EnumerationToString(Orthanc::MimeType_Css)); + } +} + + +void RedirectRoot(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + for (uint32_t i = 0; i < request->headersCount; ++i) + { + OrthancPlugins::LogError(std::string(request->headersKeys[i]) + " : " + request->headersValues[i]); + } + + std::string oe2BaseApp = oe2BaseUrl_ + "app/"; + OrthancPluginRedirect(context, output, &(oe2BaseApp.c_str()[1])); // remove the first '/' to make a relative redirect ! + } +} + +void MergeJson(Json::Value &a, const Json::Value &b) { + + if (!a.isObject() || !b.isObject()) + { + return; + } + + Json::Value::Members members = b.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + std::string key = members[i]; + + if (!a[key].isNull() && a[key].type() == Json::objectValue && b[key].type() == Json::objectValue) + { + MergeJson(a[key], b[key]); + } + else + { + // const std::string& val = b[key].asString(); + a[key] = b[key]; + } + } +} + + +void ReadConfiguration() +{ + orthancFullConfiguration_.reset(new OrthancPlugins::OrthancConfiguration); + + // read default configuration + std::string defaultConfigurationFileContent; + Orthanc::EmbeddedResources::GetFileResource(defaultConfigurationFileContent, Orthanc::EmbeddedResources::DEFAULT_CONFIGURATION); + + Json::Value defaultConfiguration; + OrthancPlugins::ReadJsonWithoutComments(defaultConfiguration, defaultConfigurationFileContent); + pluginJsonConfiguration_ = defaultConfiguration["OrthancExplorer2"]; + + if (orthancFullConfiguration_->IsSection("OrthancExplorer2")) + { + OrthancPlugins::OrthancConfiguration pluginConfiguration(false); + orthancFullConfiguration_->GetSection(pluginConfiguration, "OrthancExplorer2"); + + Json::Value jsonConfig = pluginConfiguration.GetJson(); + + // backward compatibility + if (jsonConfig.isMember("UiOptions")) + { + // fix typo from version 0.7.0 + if (jsonConfig["UiOptions"].isMember("EnableAnonimization") && !jsonConfig["UiOptions"].isMember("EnableAnonymization")) + { + LOG(WARNING) << "You are still using the 'UiOptions.EnableAnonimization' configuration that has a typo. You should use 'UiOptions.EnableAnonymization' instead."; + jsonConfig["UiOptions"]["EnableAnonymization"] = jsonConfig["UiOptions"]["EnableAnonimization"]; + } + + if (jsonConfig["UiOptions"].isMember("StudyListEmptyIfNoSearch") && !jsonConfig["UiOptions"].isMember("StudyListContentIfNoSearch")) + { + if (jsonConfig["UiOptions"]["StudyListEmptyIfNoSearch"].asBool() == true) + { + LOG(WARNING) << "You are still using the 'UiOptions.StudyListEmptyIfNoSearch' configuration that is now deprecated a typo. You should use 'UiOptions.StudyListContentIfNoSearch' instead."; + jsonConfig["UiOptions"]["StudyListContentIfNoSearch"] = "empty"; + } + } + + openInOhifV3IsExplicitelyDisabled = jsonConfig["UiOptions"].isMember("EnableOpenInOhifViewer3") && jsonConfig["UiOptions"]["EnableOpenInOhifViewer3"].asBool() == false; + } + + MergeJson(pluginJsonConfiguration_, jsonConfig); + + if (jsonConfig.isMember("CustomCssPath") && jsonConfig["CustomCssPath"].isString()) + { + customCssPath_ = jsonConfig["CustomCssPath"].asString(); + if (!Orthanc::SystemToolbox::IsExistingFile(customCssPath_)) + { + LOG(ERROR) << "Unable to accesss the 'CustomCssPath': " << customCssPath_; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile); + } + } + + if (jsonConfig.isMember("CustomLogoPath") && jsonConfig["CustomLogoPath"].isString()) + { + customLogoPath_ = jsonConfig["CustomLogoPath"].asString(); + if (!Orthanc::SystemToolbox::IsExistingFile(customLogoPath_)) + { + LOG(ERROR) << "Unable to accesss the 'CustomLogoPath': " << customLogoPath_; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile); + } + } + + if (jsonConfig.isMember("CustomLogoUrl") && jsonConfig["CustomLogoUrl"].isString()) + { + customLogoUrl_ = jsonConfig["CustomLogoUrl"].asString(); + } + + if (jsonConfig.isMember("Theme") && jsonConfig["Theme"].isString() && jsonConfig["Theme"].asString() == "dark") + { + theme_ = "dark"; + } + + if (jsonConfig.isMember("CustomFavIconPath") && jsonConfig["CustomFavIconPath"].isString()) + { + customFavIconPath_ = jsonConfig["CustomFavIconPath"].asString(); + if (!Orthanc::SystemToolbox::IsExistingFile(customFavIconPath_)) + { + LOG(ERROR) << "Unable to accesss the 'CustomFavIconPath': " << customFavIconPath_; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile); + } + } + + if (jsonConfig.isMember("CustomTitle") && jsonConfig["CustomTitle"].isString()) + { + customTitle_ = jsonConfig["CustomTitle"].asString(); + } + } + + enableShares_ = pluginJsonConfiguration_["UiOptions"]["EnableShares"].asBool(); // we are sure that the value exists since it is in the default configuration file + + isReadOnly_ = orthancFullConfiguration_->GetBooleanValue("ReadOnly", false); +} + +bool GetPluginConfiguration(Json::Value& jsonPluginConfiguration, const std::string& sectionName) +{ + if (orthancFullConfiguration_->IsSection(sectionName)) + { + OrthancPlugins::OrthancConfiguration pluginConfiguration(false); + orthancFullConfiguration_->GetSection(pluginConfiguration, sectionName); + + jsonPluginConfiguration = pluginConfiguration.GetJson(); + return true; + } + + return false; +} + + +bool IsPluginEnabledInConfiguration(const std::string& sectionName, const std::string& enableValueName, bool defaultValue) +{ + if (orthancFullConfiguration_->IsSection(sectionName)) + { + OrthancPlugins::OrthancConfiguration pluginConfiguration(false); + orthancFullConfiguration_->GetSection(pluginConfiguration, sectionName); + + return pluginConfiguration.GetBooleanValue(enableValueName, defaultValue); + } + + return defaultValue; +} + +Json::Value GetPluginInfo(const std::string& pluginName) +{ + Json::Value pluginInfo; + + OrthancPlugins::RestApiGet(pluginInfo, "/plugins/" + pluginName, false); + + return pluginInfo; +} + +Json::Value GetKeycloakConfiguration() +{ + if (pluginJsonConfiguration_.isMember("Keycloak")) + { + const Json::Value& keyCloakSection = pluginJsonConfiguration_["Keycloak"]; + if (keyCloakSection.isMember("Enable") && keyCloakSection["Enable"].asBool() == true) + { + return pluginJsonConfiguration_["Keycloak"]; + } + } + + return Json::nullValue; +} + +Json::Value GetPluginsConfiguration(bool& hasUserProfile) +{ + Json::Value pluginsConfiguration; + Json::Value pluginList; + + Orthanc::UriComponents components; + Orthanc::Toolbox::SplitUriComponents(components, oe2BaseUrl_); + std::string pluginUriPrefix = ""; // the RootUri is provided relative to Orthanc Explorer /app/explorer.html -> we need to correct this ! + for (size_t i = 0; i < components.size(); i++) + { + pluginUriPrefix += "../"; + } + + OrthancPlugins::RestApiGet(pluginList, "/plugins", false); + + for (Json::Value::ArrayIndex i = 0; i < pluginList.size(); i++) + { + Json::Value pluginConfiguration; + std::string pluginName = pluginList[i].asString(); + + if (pluginName == "explorer.js") + { + continue; + } + + Json::Value pluginInfo = GetPluginInfo(pluginName); + + if (pluginInfo.isMember("RootUri") && pluginInfo["RootUri"].asString().size() > 0) + { + pluginInfo["RootUri"] = pluginUriPrefix + pluginInfo["RootUri"].asString(); + } + + pluginsConfiguration[pluginName] = pluginInfo; + pluginsConfiguration[pluginName]["Enabled"] = true; // we assume that unknown plugins are enabled (if they are loaded by Orthanc) + + if (pluginName == "authorization") + { + pluginConfiguration = Json::nullValue; + pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "Authorization") + && (pluginConfiguration.isMember("WebService") + || pluginConfiguration.isMember("WebServiceRootUrl") + || pluginConfiguration.isMember("WebServiceUserProfileUrl") + || pluginConfiguration.isMember("WebServiceTokenValidationUrl") + || pluginConfiguration.isMember("WebServiceTokenCreationBaseUrl") + || pluginConfiguration.isMember("WebServiceTokenDecoderUrl")); + hasUserProfile = GetPluginConfiguration(pluginConfiguration, "Authorization") && (pluginConfiguration.isMember("WebServiceUserProfileUrl") || pluginConfiguration.isMember("WebServiceRootUrl")); + + if (!pluginConfiguration.isMember("CheckedLevel") || pluginConfiguration["CheckedLevel"].asString() != "studies") + { + LOG(WARNING) << "When using OE2 and the authorization plugin together, you must set 'Authorization.CheckedLevel' to 'studies'. Unless you are using this orthanc only to generate tokens."; + } + } + else if (pluginName == "AWS S3 Storage") + { + pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "AwsS3Storage"); + } + else if (pluginName == "Azure Blob Storage") + { + pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "AzureBlobStorage"); + } + else if (pluginName == "connectivity-checks") + { + pluginsConfiguration[pluginName]["Enabled"] = true; + } + else if (pluginName == "ohif") + { + pluginsConfiguration[pluginName]["Enabled"] = true; + std::string ohifDataSource = "dicom-web"; + if (GetPluginConfiguration(pluginConfiguration, "OHIF")) + { + if (pluginConfiguration.isMember("DataSource") && pluginConfiguration["DataSource"].asString() == "dicom-json") + { + ohifDataSource = "dicom-json"; + } + } + pluginsConfiguration[pluginName]["DataSource"] = ohifDataSource; + } + else if (pluginName == "delayed-deletion") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("DelayedDeletion", "Enable", false); + } + else if (pluginName == "dicom-web") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("DicomWeb", "Enable", false); + } + else if (pluginName == "gdcm") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Gdcm", "Enable", true); + } + else if (pluginName == "Google Cloud Storage") + { + pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "GoogleCloudStorage"); + } + else if (pluginName == "mysql-index") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("MySQL", "EnableIndex", false); + } + else if (pluginName == "mysql-storage") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("MySQL", "EnableStorage", false); + } + else if (pluginName == "odbc-index") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Odbc", "EnableIndex", false); + } + else if (pluginName == "odbc-storage") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Odbc", "EnableStorage", false); + } + else if (pluginName == "postgresql-index") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("PostgreSQL", "EnableIndex", false); + } + else if (pluginName == "postgresql-storage") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("PostgreSQL", "EnableStorage", false); + } + else if (pluginName == "osimis-web-viewer") + { + pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "WebViewer"); + } + else if (pluginName == "python") + { + std::string notUsed; + pluginsConfiguration[pluginName]["Enabled"] = orthancFullConfiguration_->LookupStringValue(notUsed, "PythonScript"); + } + else if (pluginName == "serve-folders") + { + pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "ServeFolders"); + } + else if (pluginName == "stone-webviewer") + { + pluginsConfiguration[pluginName]["Enabled"] = true; + } + else if (pluginName == "volview") + { + pluginsConfiguration[pluginName]["Enabled"] = true; + } + else if (pluginName == "tcia") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Tcia", "Enable", false); + } + else if (pluginName == "transfers") + { + pluginsConfiguration[pluginName]["Enabled"] = true; + } + else if (pluginName == "web-viewer") + { + pluginsConfiguration[pluginName]["Enabled"] = true; + } + else if (pluginName == "worklists") + { + pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Worklists", "Enable", false); + } + else if (pluginName == "wsi") + { + pluginsConfiguration[pluginName]["Enabled"] = true; + } + else if (pluginName == "multitenant-dicom") + { + pluginsConfiguration[pluginName]["Enabled"] = false; + Json::Value pluginConfiguration; + if (GetPluginConfiguration(pluginConfiguration, "MultitenantDicom")) + { + pluginsConfiguration[pluginName]["Enabled"] = pluginConfiguration.isMember("Servers") && pluginConfiguration["Servers"].isArray() && pluginConfiguration["Servers"].size() > 0; + } + } + + } + + return pluginsConfiguration; +} + +void UpdateUiOptions(Json::Value& uiOption, const std::list& permissions, const std::string& anyOfPermissions) +{ + std::vector permissionsVector; + Orthanc::Toolbox::TokenizeString(permissionsVector, anyOfPermissions, '|'); + + bool hasPermission = false; + + for (size_t i = 0; i < permissionsVector.size(); ++i) + { + hasPermission |= std::find(permissions.begin(), permissions.end(), permissionsVector[i]) != permissions.end(); + } + + uiOption = uiOption.asBool() && hasPermission; +} + +void GetOE2Configuration(OrthancPluginRestOutput* output, + const char* /*url*/, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + Json::Value oe2Configuration; + + oe2Configuration["Plugins"] = pluginsConfiguration_; + oe2Configuration["UiOptions"] = pluginJsonConfiguration_["UiOptions"]; + + // if OHIF has not been explicitely disabled in the config and if the plugin is loaded, enable it + if (!openInOhifV3IsExplicitelyDisabled && pluginsConfiguration_.isMember("ohif")) + { + oe2Configuration["UiOptions"]["EnableOpenInOhifViewer3"] = true; + } + + Json::Value tokens = pluginJsonConfiguration_["Tokens"]; + if (!tokens.isMember("RequiredForLinks")) + { + tokens["RequiredForLinks"] = hasUserProfile_; + } + + oe2Configuration["Tokens"] = tokens; + + oe2Configuration["HasCustomLogo"] = !customLogoPath_.empty() || !customLogoUrl_.empty(); + if (!customLogoUrl_.empty()) + { + oe2Configuration["CustomLogoUrl"] = customLogoUrl_; + } + + if (!customTitle_.empty()) + { + oe2Configuration["CustomTitle"] = customTitle_; + } + + Json::Value& uiOptions = oe2Configuration["UiOptions"]; + + if (hasUserProfile_) + { + {// get the available-labels from the auth plugin (and the auth-service) + std::map headers; + OrthancPlugins::GetHttpHeaders(headers, request); + + uiOptions["EnablePermissionsEdition"] = false; + + Json::Value rolesConfig; + if (OrthancPlugins::RestApiGet(rolesConfig, "/auth/settings/roles", headers, true)) + { + if (rolesConfig.isObject() && rolesConfig.isMember("available-labels")) + { + LOG(INFO) << "Overriding \"AvailableLabels\" in UiOptions with the values from the auth-service"; + uiOptions["AvailableLabels"] = rolesConfig["available-labels"]; + } + + LOG(INFO) << rolesConfig.toStyledString(); + + // if the auth-service is not fully configured, disable permissions edition + if (rolesConfig.isObject() && rolesConfig.isMember("roles") && rolesConfig["roles"].isObject() && rolesConfig["roles"].size() > 0) + { + uiOptions["EnablePermissionsEdition"] = true; + } + } + } + + {// get the user profile from the auth plugin (and the auth-service) + std::map headers; + OrthancPlugins::GetHttpHeaders(headers, request); + + Json::Value userProfile; + OrthancPlugins::RestApiGet(userProfile, "/auth/user/profile", headers, true); + + // modify the UiOptions based on the user profile + std::list permissions; + Orthanc::SerializationToolbox::ReadListOfStrings(permissions, userProfile, "permissions"); + + LOG(INFO) << "Overriding \"Enable...\" in UiOptions with the permissions from the auth-service for this user-profile"; + + UpdateUiOptions(uiOptions["EnableStudyList"], permissions, "all|view"); + UpdateUiOptions(uiOptions["EnableViewerQuickButton"], permissions, "all|view"); + UpdateUiOptions(uiOptions["EnableReportQuickButton"], permissions, "all|view"); + UpdateUiOptions(uiOptions["EnableUpload"], permissions, "all|upload"); + UpdateUiOptions(uiOptions["EnableAddSeries"], permissions, "all|upload"); + UpdateUiOptions(uiOptions["EnableDicomModalities"], permissions, "all|q-r-remote-modalities"); + UpdateUiOptions(uiOptions["EnableDeleteResources"], permissions, "all|delete"); + UpdateUiOptions(uiOptions["EnableDownloadZip"], permissions, "all|download"); + UpdateUiOptions(uiOptions["EnableDownloadDicomDir"], permissions, "all|download"); + UpdateUiOptions(uiOptions["EnableDownloadDicomFile"], permissions, "all|download"); + UpdateUiOptions(uiOptions["EnableModification"], permissions, "all|modify"); + UpdateUiOptions(uiOptions["EnableAnonymization"], permissions, "all|anonymize"); + UpdateUiOptions(uiOptions["EnableSendTo"], permissions, "all|send"); + UpdateUiOptions(uiOptions["EnableApiViewMenu"], permissions, "all|api-view"); + UpdateUiOptions(uiOptions["EnableSettings"], permissions, "all|settings"); + UpdateUiOptions(uiOptions["EnableShares"], permissions, "all|share"); + UpdateUiOptions(uiOptions["EnableEditLabels"], permissions, "all|edit-labels"); + UpdateUiOptions(uiOptions["EnablePermissionsEdition"], permissions, "admin-permissions"); + + // the Legacy UI is not available with user profile since it would not refresh the tokens + uiOptions["EnableLinkToLegacyUi"] = false; + + oe2Configuration["Profile"] = userProfile; + } + + } + + // disable operations on read only systems + if (isReadOnly_) + { + uiOptions["EnableUpload"] = false; + uiOptions["EnableAddSeries"] = false; + uiOptions["EnableDeleteResources"] = false; + uiOptions["EnableModification"] = false; + uiOptions["EnableAnonymization"] = false; + uiOptions["EnableEditLabels"] = false; + uiOptions["EnablePermissionsEdition"] = false; + } + + + oe2Configuration["Keycloak"] = GetKeycloakConfiguration(); + std::string answer = oe2Configuration.toStyledString(); + OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json"); + } +} + +void GetOE2PreLoginConfiguration(OrthancPluginRestOutput* output, + const char* /*url*/, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + Json::Value oe2Configuration; + oe2Configuration["Keycloak"] = GetKeycloakConfiguration(); + + std::string answer = oe2Configuration.toStyledString(); + OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json"); + } +} + + +static bool DisplayPerformanceWarning(OrthancPluginContext* context) +{ + (void) DisplayPerformanceWarning; // Disable warning about unused function + OrthancPluginLogWarning(context, "Performance warning in Orthanc Explorer 2: " + "Non-release build, runtime debug assertions are turned on"); + return true; +} + + +static void CheckRootUrlIsValid(const std::string& value, const std::string& name, bool allowEmpty) +{ + if (allowEmpty && value.size() == 0) + { + return; + } + + if (value.size() < 1 || + value[0] != '/' || + value[value.size() - 1] != '/') + { + OrthancPlugins::LogError("Orthanc-Explorer 2: '" + name + "' configuration shall start with a '/' and end with a '/': " + value); + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } +} + +OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) +{ + try + { + if (changeType == OrthancPluginChangeType_OrthancStarted) + { + // this can not be performed during plugin initialization because it is accessing the DB -> must be done when Orthanc has just started + pluginsConfiguration_ = GetPluginsConfiguration(hasUserProfile_); + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception: " << e.What(); + return static_cast(e.GetErrorCode()); + } + + return OrthancPluginErrorCode_Success; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) + { + assert(DisplayPerformanceWarning(context)); + + OrthancPlugins::SetGlobalContext(context); + + Orthanc::Logging::InitializePluginContext(context); + + Orthanc::Logging::EnableInfoLevel(true); + + + /* Check the version of the Orthanc core */ + if (!OrthancPlugins::CheckMinimalOrthancVersion(ORTHANC_CORE_MINIMAL_MAJOR, + ORTHANC_CORE_MINIMAL_MINOR, + ORTHANC_CORE_MINIMAL_REVISION)) + { + OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_CORE_MINIMAL_MAJOR, + ORTHANC_CORE_MINIMAL_MINOR, + ORTHANC_CORE_MINIMAL_REVISION); + return -1; + } + + OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Advanced User Interface for Orthanc"); + + try + { + ReadConfiguration(); + + if (pluginJsonConfiguration_["Enable"].asBool()) + { + oe2BaseUrl_ = pluginJsonConfiguration_["Root"].asString(); + + CheckRootUrlIsValid(oe2BaseUrl_, "Root", false); + + OrthancPlugins::LogWarning("Root URI to the Orthanc-Explorer 2 application: " + oe2BaseUrl_); + + + OrthancPlugins::RegisterRestCallback + + (oe2BaseUrl_ + "app/customizable/custom.css", true); + + if (!customLogoPath_.empty()) + { + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/customizable/custom-logo", true); + } + + // we need to mix the "routing" between the server and the frontend (vue-router) + // first part are the files that are 'static files' that must be served by the backend + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/assets/(.*)", true); + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/index.html", true); + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/token-landing.html", true); + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/retrieve-and-view.html", true); + + if (customFavIconPath_.empty()) + { + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/favicon.ico", true); + } + else + { + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/favicon.ico", true); + } + // second part are all the routes that are actually handled by vue-router and that are actually returning the same file (index.html) + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app/(.*)", true); + OrthancPlugins::RegisterRestCallback + > + (oe2BaseUrl_ + "app", true); + + OrthancPlugins::RegisterRestCallback(oe2BaseUrl_ + "api/configuration", true); + OrthancPlugins::RegisterRestCallback(oe2BaseUrl_ + "api/pre-login-configuration", true); + + std::string pluginRootUri = oe2BaseUrl_ + "app/"; + OrthancPlugins::SetRootUri(ORTHANC_PLUGIN_NAME, pluginRootUri.c_str()); + + if (pluginJsonConfiguration_["IsDefaultOrthancUI"].asBool()) + { + OrthancPlugins::RegisterRestCallback("/", true); + } + + OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback); + + { + std::string explorer; + Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER); + + std::map dictionary; + dictionary["OE2_BASE_URL"] = oe2BaseUrl_.substr(1, oe2BaseUrl_.size() - 2); // Remove heading and trailing slashes + std::string explorerConfigured = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary); + + OrthancPluginExtendOrthancExplorer(OrthancPlugins::GetGlobalContext(), explorerConfigured.c_str()); + } + } + else + { + OrthancPlugins::LogWarning("Orthanc Explorer 2 plugin is disabled"); + } + } + catch (Orthanc::OrthancException& e) + { + OrthancPlugins::LogError("Exception while initializing the Orthanc-Explorer 2 plugin: " + + std::string(e.What())); + return -1; + } + catch (...) + { + OrthancPlugins::LogError("Exception while initializing the Orthanc-Explorer 2 plugin"); + return -1; + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return ORTHANC_PLUGIN_NAME; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return ORTHANC_OE2_VERSION; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..560f782 --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +# Orthanc Explorer 2 + +This [Orthanc](https://www.orthanc-server.com) plugin implements a new +user interface for Orthanc to replace the Orthanc Explorer. + +## Installation + +Binaries are available: +- [here on Github](https://github.com/orthanc-server/orthanc-explorer-2/releases), you'll find the plugin for + - Windows 64bits + - Ubuntu 20.04 + - MacOS (Universal binary) +- in the [Windows Installers](https://orthanc.uclouvain.be/downloads/windows-64/installers/index.html) (64bits version only), + the plugin is enabled but, right now, the legacy Orthanc Explorer + remains the default UI. Note that, in a future release, OE2 will become the default UI for Orthanc in the Windows Installers. +- in the [orthancteam/orthanc](https://hub.docker.com/r/orthancteam/orthanc) Docker images, + the plugin is enabled but, right now, the legacy Orthanc Explorer + remains the default UI. Note that, in a future release, OE2 will become the default UI for Orthanc in the Windows Installers. + +The binaries must be copied next to other Orthanc plugins (Windows: `C:\\Program Files\\Orthanc Server\\Plugins`, ubuntu: `/usr/share/orthanc/plugins/`). You should also possible make sure that the plugin is listed in the `"Plugins"` Orthanc configuration. + +## Configuration + +Like any other Orthanc plugins, Orthanc Explorer 2 is configured through +a json [configuration file](Plugin/DefaultConfiguration.json) that is provided to Orthanc at startup. + +At minimum, you should provide this configuration file: +```json +{ + "Plugins": [ "." ], + "OrthancExplorer2": { + "Enable": true, + "IsDefaultOrthancUI": false + } +} +``` + +Using this minimal configuration file, Orthanc Explorer 2 is +accessible at http://localhost:8042/ui/app/ . If `IsDefaultOrthancUI` +is set to `true`, Orthanc Explorer 2 will replace the built-in Orthanc +Explorer. + +If you are using Docker, the easiest way to try the new Orthanc Explorer 2 is to run this command and then open a browser in http://localhost:8042/ui/app/ (login: orthanc, pwd: orthanc) + +```shell +docker pull orthancteam/orthanc:latest +docker run -p 8042:8042 orthancteam/orthanc:latest +``` + + +## Front-end development + +Prerequisites: + +- install `nodejs` version 14 or higher and `npm` version 6 or higher +- install nginx + +Then, to continuously build and serve the front-end code on your machine, in a shell, type: + +```shell +cd WebApplication +npm install +npm run dev +``` +Npm will then serve the `/ui/app/` static code (HTML/JS). + +Then, launch an Orthanc with the OE2 plugin already enabled and listening on localhost:8043. This can be achieved with by typing this command in another shell: + +```shell +docker run -p 8043:8042 -e ORTHANC__AUTHENTICATION_ENABLED=false orthancteam/orthanc:24.6.2 +``` +This Orthanc will serve the `/ui/api/` routes. + +In third shell, type: + +```shell +sudo ./scripts/start-nginx.sh +``` +This will launch an nginx server that will implement reverse proxies to serve both the static code and the Orthanc Rest API on a single endpoind. +You may then open http://localhost:9999/ui/app/ to view and debug your current front-end code. As soon as you modify a `WebApplication` source file, the UI shall reload automatically in the browser. + +Edit scripts/nginx-dev.conf if needed. + + +## Compilation + +Prerequisites to build the frontend: you need `nodejs` version 14 or higher and `npm` version 6 or higher. + +To build the frontend: + +```shell +cd WebApplication +npm install +npm run build +``` + +And then, to build the C++ plugin: +``` +cd /build +cmake -DWEBAPP_DIST_SOURCE=LOCAL -DALLOW_DOWNLOADS=ON -DCMAKE_BUILD_TYPE:STRING=Release -DUSE_SYSTEM_ORTHANC_SDK=OFF /sources/orthanc-explorer-2/ +make -j 4 +``` + +### LSB (Linux Standard Base) + +Here are the build instructions for LSB: + +``` +cd WebApplication +npm install +npm run build +cd .. +mkdir build-lsb +cd build-lsb +LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake -DCMAKE_TOOLCHAIN_FILE=../Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake -DALLOW_DOWNLOADS=ON -DSTATIC_BUILD=ON -DUSE_LEGACY_JSONCPP=ON -DWEBAPP_DIST_SOURCE=WEB .. +make -j4 +``` + +Pre-compiled LSB binaries can be found at: https://orthanc.uclouvain.be/downloads/linux-standard-base/orthanc-explorer-2/index.html + +### Linking against system-wide Orthanc Framework + +Here are the build instructions to dynamic link against the +system-wide Orthanc Framework: + +``` +cd WebApplication +npm install +npm run build +cd .. +mkdir build-system +cd build-system +cmake .. -DCMAKE_BUILD_TYPE=Debug -DORTHANC_FRAMEWORK_SOURCE=system -DUSE_SYSTEM_GOOGLE_TEST=OFF -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_ORTHANC_SDK=OFF -DWEBAPP_DIST_SOURCE=LOCAL +make -j4 +``` + + +## Releasing + +``` +git tag -a 0.2.0 -m 0.2.0 +git push --tags +git push +``` + +### Contributions + +Feel free to fork this project, modify it and submit pull requests. diff --git a/Resources/CreateDistPackage.sh b/Resources/CreateDistPackage.sh new file mode 100644 index 0000000..08077b2 --- /dev/null +++ b/Resources/CreateDistPackage.sh @@ -0,0 +1,55 @@ +#!/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-2024 Osimis S.A., Belgium +# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero 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 +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +# This command-line script uses the "npm" tool to populate the "dist" +# folder of Orthanc Explorer 2. It uses Docker to this end, in order +# to be usable on our CIS. + +set -ex + +if [ -t 1 ]; then + # TTY is available => use interactive mode + DOCKER_FLAGS='-i' +fi + +ROOT_DIR=`dirname $(readlink -f $0)`/.. +IMAGE=orthanc-explorer-2-npm + +echo "Creating the distribution of Orthanc Explorer 2" + +if [ -e "${ROOT_DIR}/WebApplication/dist/" ]; then + echo "Target folder is already existing, aborting" + exit -1 +fi + +mkdir -p ${ROOT_DIR}/WebApplication/dist/ + +( cd ${ROOT_DIR}/Resources/CreateDistPackage && \ + docker build --no-cache -t ${IMAGE} . ) + +docker run -t ${DOCKER_FLAGS} --rm \ + --user $(id -u):$(id -g) \ + -v ${ROOT_DIR}/Resources/CreateDistPackage/build.sh:/source/build.sh:ro \ + -v ${ROOT_DIR}/WebApplication:/source/WebApplication:ro \ + -v ${ROOT_DIR}/WebApplication/dist/:/target:rw \ + ${IMAGE} \ + bash /source/build.sh diff --git a/Resources/CreateDistPackage/Dockerfile b/Resources/CreateDistPackage/Dockerfile new file mode 100644 index 0000000..b437bfb --- /dev/null +++ b/Resources/CreateDistPackage/Dockerfile @@ -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-2022 Osimis S.A., Belgium +# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero 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 +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +FROM node:19.7.0-bullseye-slim + +MAINTAINER Sebastien Jodogne +LABEL Description="Orthanc, free DICOM server" Vendor="The Orthanc project" + +RUN apt-get -y clean && apt-get -y update && \ + DEBIAN_FRONTEND=noninteractive apt-get -y install patch && \ + apt-get clean && rm -rf /var/lib/apt/lists/* diff --git a/Resources/CreateDistPackage/build.sh b/Resources/CreateDistPackage/build.sh new file mode 100644 index 0000000..9361c96 --- /dev/null +++ b/Resources/CreateDistPackage/build.sh @@ -0,0 +1,30 @@ +#!/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-2024 Osimis S.A., Belgium +# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero 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 +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +set -ex + +cp -r /source/WebApplication /tmp/ + +cd /tmp/WebApplication +npm install --cache /tmp/npm-cache +npm run build --cache /tmp/npm-cache + +cp -r /tmp/WebApplication/dist/* /target diff --git a/Resources/MoveTranslations.py b/Resources/MoveTranslations.py new file mode 100644 index 0000000..8d16817 --- /dev/null +++ b/Resources/MoveTranslations.py @@ -0,0 +1,38 @@ +import json +import os +import shutil +import pathlib + +here = pathlib.Path(__file__).parent.resolve() / "../WebApplication/src/locales/" + +for p in here.glob("*.json"): + with open(p, "r", encoding="utf-8") as f: + translations = json.load(f) + + if "settings" in translations and isinstance(translations["settings"], str): + title = translations["settings"] + del translations["settings"] + translations["settings"] = {} + + for i in ["dicom_AET", + "dicom_port", + "ingest_transcoding", + "installed_plugins", + "orthanc_name", + "orthanc_system_info", + "orthanc_version", + "overwrite_instances", + "plugins_not_enabled", + "striked_through", + "ingest_transcoding", + "verbosity_level", + "storage_compression", + "storage_size", + "statistics" + ]: + if i in translations: + translations["settings"][i] = translations[i] + del translations[i] + + with open(p, "w", encoding="utf-8") as f: + data = json.dump(translations, f, ensure_ascii=False, indent=4, sort_keys=True) diff --git a/Resources/Orthanc/CMake/AutoGeneratedCode.cmake b/Resources/Orthanc/CMake/AutoGeneratedCode.cmake new file mode 100644 index 0000000..d5fcd8e --- /dev/null +++ b/Resources/Orthanc/CMake/AutoGeneratedCode.cmake @@ -0,0 +1,80 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +set(EMBED_RESOURCES_PYTHON "${CMAKE_CURRENT_LIST_DIR}/../EmbedResources.py" + CACHE INTERNAL "Path to the EmbedResources.py script from Orthanc") +set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED") +set(AUTOGENERATED_SOURCES) + +file(MAKE_DIRECTORY ${AUTOGENERATED_DIR}) +include_directories(${AUTOGENERATED_DIR}) + +macro(EmbedResources) + # Convert a semicolon separated list to a whitespace separated string + set(SCRIPT_OPTIONS) + set(SCRIPT_ARGUMENTS) + set(DEPENDENCIES) + set(IS_PATH_NAME false) + + set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources") + + # Loop over the arguments of the function + foreach(arg ${ARGN}) + # Extract the first character of the argument + string(SUBSTRING "${arg}" 0 1 FIRST_CHAR) + if (${FIRST_CHAR} STREQUAL "-") + # If the argument starts with a dash "-", this is an option to + # EmbedResources.py + if (${arg} MATCHES "--target=.*") + # Does the argument starts with "--target="? + string(SUBSTRING "${arg}" 9 -1 TARGET) # 9 is the length of "--target=" + set(TARGET_BASE "${AUTOGENERATED_DIR}/${TARGET}") + else() + list(APPEND SCRIPT_OPTIONS ${arg}) + endif() + else() + if (${IS_PATH_NAME}) + list(APPEND SCRIPT_ARGUMENTS "${arg}") + list(APPEND DEPENDENCIES "${arg}") + set(IS_PATH_NAME false) + else() + list(APPEND SCRIPT_ARGUMENTS "${arg}") + set(IS_PATH_NAME true) + endif() + endif() + endforeach() + + add_custom_command( + OUTPUT + "${TARGET_BASE}.h" + "${TARGET_BASE}.cpp" + COMMAND ${PYTHON_EXECUTABLE} ${EMBED_RESOURCES_PYTHON} + ${SCRIPT_OPTIONS} "${TARGET_BASE}" ${SCRIPT_ARGUMENTS} + DEPENDS + ${EMBED_RESOURCES_PYTHON} + ${DEPENDENCIES} + ) + + list(APPEND AUTOGENERATED_SOURCES + "${TARGET_BASE}.cpp" + ) +endmacro() diff --git a/Resources/Orthanc/CMake/Compiler.cmake b/Resources/Orthanc/CMake/Compiler.cmake new file mode 100644 index 0000000..d03f739 --- /dev/null +++ b/Resources/Orthanc/CMake/Compiler.cmake @@ -0,0 +1,303 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +# This file sets all the compiler-related flags + + +# Save the current compiler flags to the cache every time cmake configures the project +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "compiler flags" FORCE) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}" CACHE STRING "compiler flags" FORCE) + + +include(CheckLibraryExists) + +if ((CMAKE_CROSSCOMPILING AND NOT + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR + "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Cross-compilation necessarily implies standalone and static build + SET(STATIC_BUILD ON) + SET(STANDALONE_BUILD ON) +endif() + + +if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Cache the environment variables "LSB_CC" and "LSB_CXX" for further + # use by "ExternalProject" in CMake + SET(CMAKE_LSB_CC $ENV{LSB_CC} CACHE STRING "") + SET(CMAKE_LSB_CXX $ENV{LSB_CXX} CACHE STRING "") + + # This is necessary to build "Orthanc mainline - Framework LSB + # Release" on "buildbot-worker-debian11" + set(LSB_PTHREAD_NONSHARED "${LSB_PATH}/lib64-${LSB_TARGET_VERSION}/libpthread_nonshared.a") + if (EXISTS ${LSB_PTHREAD_NONSHARED}) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LSB_PTHREAD_NONSHARED}") + endif() +endif() + + +if (CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long") + + # --std=c99 makes libcurl not to compile + # -pedantic gives a lot of warnings on OpenSSL + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -Wno-variadic-macros") + + if (CMAKE_CROSSCOMPILING) + # http://stackoverflow.com/a/3543845/881731 + set(CMAKE_RC_COMPILE_OBJECT " -O coff -I ") + endif() + +elseif (MSVC) + # Use static runtime under Visual Studio + # http://www.cmake.org/Wiki/CMake_FAQ#Dynamic_Replace + # http://stackoverflow.com/a/6510446 + foreach(flag_var + CMAKE_C_FLAGS_DEBUG + CMAKE_CXX_FLAGS_DEBUG + CMAKE_C_FLAGS_RELEASE + CMAKE_CXX_FLAGS_RELEASE + CMAKE_C_FLAGS_MINSIZEREL + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_C_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS_RELWITHDEBINFO) + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}") + endforeach(flag_var) + + # Add /Zm256 compiler option to Visual Studio to fix PCH errors + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256") + + # New in Orthanc 1.5.5 + if (MSVC_MULTIPLE_PROCESSES) + # "If you omit the processMax argument in the /MP option, the + # compiler obtains the number of effective processors from the + # operating system, and then creates one process per effective + # processor" + # https://blog.kitware.com/cmake-building-with-all-your-cores/ + # https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") + endif() + + add_definitions( + -D_CRT_SECURE_NO_WARNINGS=1 + -D_CRT_SECURE_NO_DEPRECATE=1 + ) + + if (MSVC_VERSION LESS 1600) + # Starting with Visual Studio >= 2010 (i.e. macro _MSC_VER >= + # 1600), Microsoft ships a standard-compliant + # header. For earlier versions of Visual Studio, give access to a + # compatibility header. + # http://stackoverflow.com/a/70630/881731 + # https://en.wikibooks.org/wiki/C_Programming/C_Reference/stdint.h#External_links + include_directories(${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/VisualStudio) + endif() + + link_libraries(netapi32) +endif() + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # In FreeBSD/OpenBSD, the "/usr/local/" folder contains the ports and need to be imported + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I/usr/local/include") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L/usr/local/lib") +endif() + + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + + if (# NOT ${CMAKE_SYSTEM_VERSION} STREQUAL "LinuxStandardBase" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + # The "--no-undefined" linker flag makes the shared libraries + # (plugins ModalityWorklists and ServeFolders) fail to compile on + # OpenBSD, and make the PostgreSQL plugin complain about missing + # "environ" global variable in FreeBSD. + # + # TODO - Furthermore, on Linux Standard Base running on Debian 12, + # the "-Wl,--no-undefined" seems to break the compilation (added + # after Orthanc 1.12.2). This is disabled for now. + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") + endif() + + # Remove the "-rdynamic" option + # http://www.mail-archive.com/cmake@cmake.org/msg08837.html + set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") + link_libraries(pthread) + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + link_libraries(rt) + endif() + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + link_libraries(dl) + endif() + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # The "--as-needed" linker flag is not available on FreeBSD and OpenBSD + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--as-needed") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--as-needed") + endif() + + if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND + NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # FreeBSD/OpenBSD have just one single interface for file + # handling, which is 64bit clean, so there is no need to define macro + # for LFS (Large File Support). + # https://ohse.de/uwe/articles/lfs.html + add_definitions( + -D_LARGEFILE64_SOURCE=1 + -D_FILE_OFFSET_BITS=64 + ) + endif() + +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + if (MSVC) + message("MSVC compiler version = " ${MSVC_VERSION} "\n") + # Starting Visual Studio 2013 (version 1800), it is not possible + # to target Windows XP anymore + if (MSVC_VERSION LESS 1800) + add_definitions( + -DWINVER=0x0501 + -D_WIN32_WINNT=0x0501 + ) + endif() + else() + add_definitions( + -DWINVER=0x0501 + -D_WIN32_WINNT=0x0501 + ) + endif() + + add_definitions( + -D_CRT_SECURE_NO_WARNINGS=1 + ) + link_libraries(rpcrt4 ws2_32 iphlpapi) # "iphlpapi" is for "SystemToolbox::GetMacAddresses()" + + if (CMAKE_COMPILER_IS_GNUCXX) + # Some additional C/C++ compiler flags for MinGW + SET(MINGW_NO_WARNINGS "-Wno-unused-function -Wno-unused-variable") + SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MINGW_NO_WARNINGS} -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MINGW_NO_WARNINGS}") + + if (DYNAMIC_MINGW_STDLIB) + else() + # This is a patch for MinGW64 + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++") + endif() + + CHECK_LIBRARY_EXISTS(winpthread pthread_create "" HAVE_WIN_PTHREAD) + if (HAVE_WIN_PTHREAD) + if (DYNAMIC_MINGW_STDLIB) + else() + # This line is necessary to compile with recent versions of MinGW, + # otherwise "libwinpthread-1.dll" is not statically linked. + SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic") + endif() + add_definitions(-DHAVE_WIN_PTHREAD=1) + else() + add_definitions(-DHAVE_WIN_PTHREAD=0) + endif() + endif() + +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + + # fix this error that appears with recent compilers on MacOS: boost/mpl/aux_/integral_wrapper.hpp:73:31: error: integer value -1 is outside the valid range of values [0, 3] for this enumeration type [-Wenum-constexpr-conversion] + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-enum-constexpr-conversion") + + add_definitions( + -D_XOPEN_SOURCE=1 + ) + link_libraries(iconv) + +elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + message("Building using Emscripten (for WebAssembly or asm.js targets)") + include(${CMAKE_CURRENT_LIST_DIR}/EmscriptenParameters.cmake) + +elseif (CMAKE_SYSTEM_NAME STREQUAL "Android") + +else() + message("Unknown target platform: ${CMAKE_SYSTEM_NAME}") + message(FATAL_ERROR "Support your platform here") +endif() + + +if (DEFINED ENABLE_PROFILING AND ENABLE_PROFILING) + if (CMAKE_COMPILER_IS_GNUCXX OR + CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pg") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -pg") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pg") + else() + message(FATAL_ERROR "Don't know how to enable profiling on your configuration") + endif() +endif() + + +if (CMAKE_COMPILER_IS_GNUCXX) + # "When creating a static library using binutils (ar) and there + # exist a duplicate object name (e.g. a/Foo.cpp.o, b/Foo.cpp.o), the + # resulting static library can end up having only one of the + # duplicate objects. [...] This bug only happens if there are many + # objects." The trick consists in replacing the "r" argument + # ("replace") provided to "ar" (as used in CMake < 3.1) by the "q" + # argument ("quick append"). This is because of the fact that CMake + # will invoke "ar" several times with several batches of ".o" + # objects, and using "r" would overwrite symbols defined in + # preceding batches. https://cmake.org/Bug/view.php?id=14874 + set(CMAKE_CXX_ARCHIVE_APPEND " q ") +endif() + + +# This function defines macro "__ORTHANC_FILE__" as a replacement to +# macro "__FILE__", as the latter leaks the full path of the source +# files in the binaries +# https://stackoverflow.com/questions/8487986/file-macro-shows-full-path +# https://twitter.com/wget42/status/1676877802375634944?s=20 +function(DefineSourceBasenameForTarget targetname) + # Microsoft Visual Studio is extremely slow if using + # "set_property()", we only enable this feature for gcc and clang + if (CMAKE_COMPILER_IS_GNUCXX OR + CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + get_target_property(source_files "${targetname}" SOURCES) + foreach(sourcefile ${source_files}) + get_filename_component(basename "${sourcefile}" NAME) + set_property( + SOURCE "${sourcefile}" APPEND + PROPERTY COMPILE_DEFINITIONS "__ORTHANC_FILE__=\"${basename}\"") + endforeach() + endif() +endfunction() diff --git a/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake new file mode 100644 index 0000000..088baa4 --- /dev/null +++ b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake @@ -0,0 +1,578 @@ +# 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-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + + +## +## Check whether the parent script sets the mandatory variables +## + +if (NOT DEFINED ORTHANC_FRAMEWORK_SOURCE OR + (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "web" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" AND + NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "path")) + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_SOURCE must be set to \"system\", \"hg\", \"web\", \"archive\" or \"path\"") +endif() + + +## +## Detection of the requested version +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if (NOT DEFINED ORTHANC_FRAMEWORK_VERSION) + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_VERSION must be set") + endif() + + if (DEFINED ORTHANC_FRAMEWORK_MAJOR OR + DEFINED ORTHANC_FRAMEWORK_MINOR OR + DEFINED ORTHANC_FRAMEWORK_REVISION OR + DEFINED ORTHANC_FRAMEWORK_MD5) + message(FATAL_ERROR "Some internal variable has been set") + endif() + + set(ORTHANC_FRAMEWORK_MD5 "") + + if (NOT DEFINED ORTHANC_FRAMEWORK_BRANCH) + if (ORTHANC_FRAMEWORK_VERSION STREQUAL "mainline") + set(ORTHANC_FRAMEWORK_BRANCH "default") + set(ORTHANC_FRAMEWORK_MAJOR 999) + set(ORTHANC_FRAMEWORK_MINOR 999) + set(ORTHANC_FRAMEWORK_REVISION 999) + + else() + set(ORTHANC_FRAMEWORK_BRANCH "Orthanc-${ORTHANC_FRAMEWORK_VERSION}") + + set(RE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$") + string(REGEX REPLACE ${RE} "\\1" ORTHANC_FRAMEWORK_MAJOR ${ORTHANC_FRAMEWORK_VERSION}) + string(REGEX REPLACE ${RE} "\\2" ORTHANC_FRAMEWORK_MINOR ${ORTHANC_FRAMEWORK_VERSION}) + string(REGEX REPLACE ${RE} "\\3" ORTHANC_FRAMEWORK_REVISION ${ORTHANC_FRAMEWORK_VERSION}) + + if (NOT ORTHANC_FRAMEWORK_MAJOR MATCHES "^[0-9]+$" OR + NOT ORTHANC_FRAMEWORK_MINOR MATCHES "^[0-9]+$" OR + NOT ORTHANC_FRAMEWORK_REVISION MATCHES "^[0-9]+$") + message("Bad version of the Orthanc framework, assuming a pre-release: ${ORTHANC_FRAMEWORK_VERSION}") + set(ORTHANC_FRAMEWORK_MAJOR 999) + set(ORTHANC_FRAMEWORK_MINOR 999) + set(ORTHANC_FRAMEWORK_REVISION 999) + endif() + + if (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.1") + set(ORTHANC_FRAMEWORK_MD5 "dac95bd6cf86fb19deaf4e612961f378") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.2") + set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0") + set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1") + set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2") + set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.0") + set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1") + set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2") + set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3") + set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.4") + set(ORTHANC_FRAMEWORK_MD5 "404baef5d4c43e7c5d9410edda8ef5a5") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.5") + set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6") + set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7") + set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8") + set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0") + set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1") + set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.0") + set(ORTHANC_FRAMEWORK_MD5 "ce5f689e852b01d3672bd3d2f952a5ef") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.1") + set(ORTHANC_FRAMEWORK_MD5 "3c171217f930abe80246997bdbcaf7cc") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.2") + set(ORTHANC_FRAMEWORK_MD5 "328f94dcbd78c169655a13f7ad58a2c2") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.3") + set(ORTHANC_FRAMEWORK_MD5 "3f1ba9502ec7c5449971d3b56087bcde") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.4") + set(ORTHANC_FRAMEWORK_MD5 "19fcb7c21876af86546baa048a22c6c0") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.0") + set(ORTHANC_FRAMEWORK_MD5 "f8ec7554ef5d23ea4ce474b1e8214de9") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.1") + set(ORTHANC_FRAMEWORK_MD5 "db094f96399cbe8b9bbdbce34884c220") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.2") + set(ORTHANC_FRAMEWORK_MD5 "8bfa10e66c9931e74111be0bfb1f4548") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.0") + set(ORTHANC_FRAMEWORK_MD5 "cea0b02ce184671eaf1bd668beefbf28") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.1") + set(ORTHANC_FRAMEWORK_MD5 "08eebc66ef93c3b40115c38501db5fbd") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.2") + set(ORTHANC_FRAMEWORK_MD5 "3ea66c09f64aca990016683b6375734e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.3") + set(ORTHANC_FRAMEWORK_MD5 "9b86e6f00e03278293cd15643cc0233f") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.4") + set(ORTHANC_FRAMEWORK_MD5 "6d5ca4a73ac7d42445041ca79de1624d") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.5") + set(ORTHANC_FRAMEWORK_MD5 "10fc64de1254a095e5d3ed3931f0cfbb") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.6") + set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7") + set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.0") + set(ORTHANC_FRAMEWORK_MD5 "8610c82d9153f22e929f2110f8f60279") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.1") + set(ORTHANC_FRAMEWORK_MD5 "caf667fc5ea452b3d0c2f70bfd02599c") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.0") + set(ORTHANC_FRAMEWORK_MD5 "962c4a4a706a2ef28b390d8515dd7091") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.1") + set(ORTHANC_FRAMEWORK_MD5 "a39661c406adf22cf574fde290cf4bbf") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.2") + set(ORTHANC_FRAMEWORK_MD5 "ede3de356493a8868545f8cb4b8bc8b5") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.3") + set(ORTHANC_FRAMEWORK_MD5 "e48fc0cb09c4856803791a1be28c07dc") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.0") + set(ORTHANC_FRAMEWORK_MD5 "d32a0cde03b6eb603d8dd2b33d38bf1b") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.1") + set(ORTHANC_FRAMEWORK_MD5 "8a435140efc8ff4a01d8242f092f21de") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.2") + set(ORTHANC_FRAMEWORK_MD5 "d2476b9e796e339ac320b5333489bdb3") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.3") + set(ORTHANC_FRAMEWORK_MD5 "975f5bf2142c22cb1777b4f6a0a614c5") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.4") + set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73") + + # Below this point are development snapshots that were used to + # release some plugin, before an official release of the Orthanc + # framework was available. Here is the command to be used to + # generate a proper archive: + # + # $ hg archive /tmp/Orthanc-`hg id -i | sed 's/\+//'`.tar.gz + # + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "ae0e3fd609df") + # DICOMweb 1.1 (framework pre-1.6.0) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "7e09e9b530a2f527854f0b782d7e0645") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "82652c5fc04f") + # Stone Web viewer 1.0 (framework pre-1.8.1) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "d77331d68917e66a3f4f9b807bbdab7f") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "4a3ba4bf4ba7") + # PostgreSQL 3.3 (framework pre-1.8.2) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "2d82bddf06f9cfe82095495cb3b8abde") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "23ad1b9c7800") + # For "Toolbox::ReadJson()" and "Toolbox::Write{...}Json()" (pre-1.9.0) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "9af92080e57c60dd288eba46ce606c00") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "b2e08d83e21d") + # WSI 1.1 (framework pre-1.10.0), to remove "-std=c++11" + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "2eaa073cbb4b44ffba199ad93393b2b1") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "daf4807631c5") + # DICOMweb 1.15 (framework pre-1.12.2) + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "ebe8bdf388319f1c9536b2b680451848") + endif() + endif() + endif() + +elseif (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path") + message("Using the Orthanc framework from a path of the filesystem. Assuming mainline version.") + set(ORTHANC_FRAMEWORK_MAJOR 999) + set(ORTHANC_FRAMEWORK_MINOR 999) + set(ORTHANC_FRAMEWORK_REVISION 999) +endif() + + + +## +## Detection of the third-party software +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg") + find_program(ORTHANC_FRAMEWORK_HG hg) + + if (${ORTHANC_FRAMEWORK_HG} MATCHES "ORTHANC_FRAMEWORK_HG-NOTFOUND") + message(FATAL_ERROR "Please install Mercurial") + endif() +endif() + + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + find_program(ORTHANC_FRAMEWORK_7ZIP 7z + PATHS + "$ENV{ProgramFiles}/7-Zip" + "$ENV{ProgramW6432}/7-Zip" + ) + + if (${ORTHANC_FRAMEWORK_7ZIP} MATCHES "ORTHANC_FRAMEWORK_7ZIP-NOTFOUND") + message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)") + endif() + + else() + find_program(ORTHANC_FRAMEWORK_TAR tar) + if (${ORTHANC_FRAMEWORK_TAR} MATCHES "ORTHANC_FRAMEWORK_TAR-NOTFOUND") + message(FATAL_ERROR "Please install the 'tar' package") + endif() + endif() +endif() + + + +## +## Case of the Orthanc framework specified as a path on the filesystem +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path") + if (NOT DEFINED ORTHANC_FRAMEWORK_ROOT OR + ORTHANC_FRAMEWORK_ROOT STREQUAL "") + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ROOT must provide the path to the sources of Orthanc") + endif() + + if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}) + message(FATAL_ERROR "Non-existing directory: ${ORTHANC_FRAMEWORK_ROOT}") + endif() +endif() + + + +## +## Case of the Orthanc framework cloned using Mercurial +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg") + if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS) + message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON") + endif() + + set(ORTHANC_ROOT ${CMAKE_BINARY_DIR}/orthanc) + + if (EXISTS ${ORTHANC_ROOT}) + message("Updating the Orthanc source repository using Mercurial") + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_HG} pull + WORKING_DIRECTORY ${ORTHANC_ROOT} + RESULT_VARIABLE Failure + ) + else() + message("Forking the Orthanc source repository using Mercurial") + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://orthanc.uclouvain.be/hg/orthanc/" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + endif() + + if (Failure OR NOT EXISTS ${ORTHANC_ROOT}) + message(FATAL_ERROR "Cannot fork the Orthanc repository") + endif() + + message("Setting branch of the Orthanc repository to: ${ORTHANC_FRAMEWORK_BRANCH}") + + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_HG} update -c ${ORTHANC_FRAMEWORK_BRANCH} + WORKING_DIRECTORY ${ORTHANC_ROOT} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while running Mercurial") + endif() +endif() + + + +## +## Case of the Orthanc framework provided as a source archive on the +## filesystem +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive") + if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR + ORTHANC_FRAMEWORK_ARCHIVE STREQUAL "") + message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ARCHIVE must provide the path to the sources of Orthanc") + endif() +endif() + + + +## +## Case of the Orthanc framework downloaded from the Web +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if (DEFINED ORTHANC_FRAMEWORK_URL) + string(REGEX REPLACE "^.*/" "" ORTHANC_FRAMEMORK_FILENAME "${ORTHANC_FRAMEWORK_URL}") + else() + # Default case: Download from the official Web site + set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz) + if (ORTHANC_FRAMEWORK_PRE_RELEASE) + set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}") + else() + set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/sources/orthanc/${ORTHANC_FRAMEMORK_FILENAME}") + endif() + endif() + + set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}") + + if (NOT EXISTS "${ORTHANC_FRAMEWORK_ARCHIVE}") + if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS) + message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON") + endif() + + message("Downloading: ${ORTHANC_FRAMEWORK_URL}") + + file(DOWNLOAD + "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}" + SHOW_PROGRESS EXPECTED_MD5 "${ORTHANC_FRAMEWORK_MD5}" + TIMEOUT 60 + INACTIVITY_TIMEOUT 60 + ) + else() + message("Using local copy of: ${ORTHANC_FRAMEWORK_URL}") + endif() +endif() + + + + +## +## Uncompressing the Orthanc framework, if it was retrieved from a +## source archive on the filesystem, or from the official Web site +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + + if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR + NOT DEFINED ORTHANC_FRAMEWORK_VERSION OR + NOT DEFINED ORTHANC_FRAMEWORK_MD5) + message(FATAL_ERROR "Internal error") + endif() + + if (ORTHANC_FRAMEWORK_MD5 STREQUAL "") + message(FATAL_ERROR "Unknown release of Orthanc: ${ORTHANC_FRAMEWORK_VERSION}") + endif() + + file(MD5 ${ORTHANC_FRAMEWORK_ARCHIVE} ActualMD5) + + if (NOT "${ActualMD5}" STREQUAL "${ORTHANC_FRAMEWORK_MD5}") + message(FATAL_ERROR "The MD5 hash of the Orthanc archive is invalid: ${ORTHANC_FRAMEWORK_ARCHIVE}") + endif() + + set(ORTHANC_ROOT "${CMAKE_BINARY_DIR}/Orthanc-${ORTHANC_FRAMEWORK_VERSION}") + + if (NOT IS_DIRECTORY "${ORTHANC_ROOT}") + if (NOT ORTHANC_FRAMEWORK_ARCHIVE MATCHES ".tar.gz$") + message(FATAL_ERROR "Archive should have the \".tar.gz\" extension: ${ORTHANC_FRAMEWORK_ARCHIVE}") + endif() + + message("Uncompressing: ${ORTHANC_FRAMEWORK_ARCHIVE}") + + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + # How to silently extract files using 7-zip + # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly + + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_7ZIP} e -y ${ORTHANC_FRAMEWORK_ARCHIVE} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + get_filename_component(TMP_FILENAME "${ORTHANC_FRAMEWORK_ARCHIVE}" NAME) + string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}") + + execute_process( + COMMAND ${ORTHANC_FRAMEWORK_7ZIP} x -y ${TMP_FILENAME2} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + else() + execute_process( + COMMAND sh -c "${ORTHANC_FRAMEWORK_TAR} xfz ${ORTHANC_FRAMEWORK_ARCHIVE}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + endif() + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if (NOT IS_DIRECTORY "${ORTHANC_ROOT}") + message(FATAL_ERROR "The Orthanc framework was not uncompressed at the proper location. Check the CMake instructions.") + endif() + endif() +endif() + + + +## +## Determine the path to the sources of the Orthanc framework +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR + ORTHANC_FRAMEWORK_SOURCE STREQUAL "web") + if (NOT DEFINED ORTHANC_ROOT OR + NOT DEFINED ORTHANC_FRAMEWORK_MAJOR OR + NOT DEFINED ORTHANC_FRAMEWORK_MINOR OR + NOT DEFINED ORTHANC_FRAMEWORK_REVISION) + message(FATAL_ERROR "Internal error in the DownloadOrthancFramework.cmake file") + endif() + + unset(ORTHANC_FRAMEWORK_ROOT CACHE) + + if ("${ORTHANC_FRAMEWORK_MAJOR}.${ORTHANC_FRAMEWORK_MINOR}.${ORTHANC_FRAMEWORK_REVISION}" VERSION_LESS "1.7.2") + set(ORTHANC_FRAMEWORK_ROOT "${ORTHANC_ROOT}/Core" CACHE + STRING "Path to the Orthanc framework source directory") + set(ENABLE_PLUGINS_VERSION_SCRIPT OFF) + else() + set(ORTHANC_FRAMEWORK_ROOT "${ORTHANC_ROOT}/OrthancFramework/Sources" CACHE + STRING "Path to the Orthanc framework source directory") + endif() + + unset(ORTHANC_ROOT) +endif() + +if (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system") + if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/OrthancException.h OR + NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake) + message(FATAL_ERROR "Directory not containing the source code of the Orthanc framework: ${ORTHANC_FRAMEWORK_ROOT}") + endif() +endif() + + + +## +## Case of the Orthanc framework installed as a shared library in a +## GNU/Linux distribution (typically Debian). New in Orthanc 1.7.2. +## + +if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system") + set(ORTHANC_FRAMEWORK_LIBDIR "" CACHE PATH "") + set(ORTHANC_FRAMEWORK_USE_SHARED ON CACHE BOOL "Whether to use the shared library or the static library") + set(ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES "" CACHE STRING "Additional libraries to link against, separated by whitespaces, typically needed if using the static library (a common minimal value is \"boost_filesystem boost_iostreams boost_locale boost_regex boost_thread jsoncpp pugixml uuid\")") + + if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND + CMAKE_COMPILER_IS_GNUCXX) # MinGW + set(DYNAMIC_MINGW_STDLIB ON) # Disable static linking against libc (to throw exceptions) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libstdc++") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++") + endif() + + include(CheckIncludeFile) + include(CheckIncludeFileCXX) + include(FindPythonInterp) + include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake) + include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake) + include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake) + set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py) + + if (ORTHANC_FRAMEWORK_USE_SHARED) + list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) + list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix) + else() + list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) + list(GET CMAKE_FIND_LIBRARY_SUFFIXES 1 Suffix) + endif() + + # The "OrthancFramework" library must be the first one to be included + if ("${ORTHANC_FRAMEWORK_LIBDIR}" STREQUAL "") + set(ORTHANC_FRAMEWORK_LIBRARIES ${Prefix}OrthancFramework${Suffix}) + else () + set(ORTHANC_FRAMEWORK_LIBRARIES ${ORTHANC_FRAMEWORK_LIBDIR}/${Prefix}OrthancFramework${Suffix}) + endif() + + if (NOT ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES STREQUAL "") + # https://stackoverflow.com/a/5272993/881731 + string(REPLACE " " ";" tmp ${ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES}) + list(APPEND ORTHANC_FRAMEWORK_LIBRARIES ${tmp}) + endif() + + # Look for the version of the mandatory dependency JsonCpp (cf. JsonCppConfiguration.cmake) + if (CMAKE_CROSSCOMPILING) + set(JSONCPP_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT}/..) + else() + find_path(JSONCPP_INCLUDE_DIR json/reader.h + ${ORTHANC_FRAMEWORK_ROOT}/.. + /usr/include/jsoncpp + /usr/local/include/jsoncpp + ) + endif() + + message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}") + include_directories(${JSONCPP_INCLUDE_DIR}) + + CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H) + if (NOT HAVE_JSONCPP_H) + message(FATAL_ERROR "Please install the libjsoncpp-dev package") + endif() + + # Look for Orthanc framework shared library + include(CheckCXXSymbolExists) + + if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows") + set(ORTHANC_FRAMEWORK_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT}) + else() + find_path(ORTHANC_FRAMEWORK_INCLUDE_DIR OrthancFramework.h + /usr/include/orthanc-framework + /usr/local/include/orthanc-framework + ${ORTHANC_FRAMEWORK_ROOT} + ) + endif() + + if (${ORTHANC_FRAMEWORK_INCLUDE_DIR} STREQUAL "ORTHANC_FRAMEWORK_INCLUDE_DIR-NOTFOUND") + message(FATAL_ERROR "Cannot locate the OrthancFramework.h header") + endif() + + message("Orthanc framework include dir: ${ORTHANC_FRAMEWORK_INCLUDE_DIR}") + include_directories(${ORTHANC_FRAMEWORK_INCLUDE_DIR}) + + if (ORTHANC_FRAMEWORK_USE_SHARED) + set(CMAKE_REQUIRED_INCLUDES "${ORTHANC_FRAMEWORK_INCLUDE_DIR}") + set(CMAKE_REQUIRED_LIBRARIES "${ORTHANC_FRAMEWORK_LIBRARIES}") + + check_cxx_symbol_exists("Orthanc::InitializeFramework" "OrthancFramework.h" HAVE_ORTHANC_FRAMEWORK) + if (NOT HAVE_ORTHANC_FRAMEWORK) + message(FATAL_ERROR "Cannot find the Orthanc framework") + endif() + + unset(CMAKE_REQUIRED_INCLUDES) + unset(CMAKE_REQUIRED_LIBRARIES) + endif() +endif() diff --git a/Resources/Orthanc/CMake/DownloadPackage.cmake b/Resources/Orthanc/CMake/DownloadPackage.cmake new file mode 100644 index 0000000..20ea041 --- /dev/null +++ b/Resources/Orthanc/CMake/DownloadPackage.cmake @@ -0,0 +1,287 @@ +# 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-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +macro(GetUrlFilename TargetVariable Url) + string(REGEX REPLACE "^.*/" "" ${TargetVariable} "${Url}") +endmacro() + + +macro(GetUrlExtension TargetVariable Url) + #string(REGEX REPLACE "^.*/[^.]*\\." "" TMP "${Url}") + string(REGEX REPLACE "^.*\\." "" TMP "${Url}") + string(TOLOWER "${TMP}" "${TargetVariable}") +endmacro() + + + +## +## Setup the patch command-line tool +## + +if (NOT ORTHANC_DISABLE_PATCH) + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + set(PATCH_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/patch/patch.exe) + if (NOT EXISTS ${PATCH_EXECUTABLE}) + message(FATAL_ERROR "Unable to find the patch.exe tool that is shipped with Orthanc") + endif() + + else () + find_program(PATCH_EXECUTABLE patch) + if (${PATCH_EXECUTABLE} MATCHES "PATCH_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'patch' standard command-line tool") + endif() + endif() +endif() + + + +## +## Check the existence of the required decompression tools +## + +if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + find_program(ZIP_EXECUTABLE 7z + PATHS + "$ENV{ProgramFiles}/7-Zip" + "$ENV{ProgramW6432}/7-Zip" + ) + + if (${ZIP_EXECUTABLE} MATCHES "ZIP_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)") + endif() + +else() + find_program(UNZIP_EXECUTABLE unzip) + if (${UNZIP_EXECUTABLE} MATCHES "UNZIP_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'unzip' package") + endif() + + find_program(TAR_EXECUTABLE tar) + if (${TAR_EXECUTABLE} MATCHES "TAR_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'tar' package") + endif() + + find_program(GUNZIP_EXECUTABLE gunzip) + if (${GUNZIP_EXECUTABLE} MATCHES "GUNZIP_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'gzip' package") + endif() +endif() + + +macro(DownloadFile MD5 Url) + GetUrlFilename(TMP_FILENAME "${Url}") + + set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}") + if (NOT EXISTS "${TMP_PATH}") + message("Downloading ${Url}") + + # This fixes issue 6: "I think cmake shouldn't download the + # packages which are not in the system, it should stop and let + # user know." + # https://code.google.com/p/orthanc/issues/detail?id=6 + if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS) + message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON") + endif() + + foreach (retry RANGE 1 5) # Retries 5 times + if ("${MD5}" STREQUAL "no-check") + message(WARNING "Not checking the MD5 of: ${Url}") + file(DOWNLOAD "${Url}" "${TMP_PATH}" + SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10 + STATUS Failure) + else() + file(DOWNLOAD "${Url}" "${TMP_PATH}" + SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10 + EXPECTED_MD5 "${MD5}" STATUS Failure) + endif() + + list(GET Failure 0 Status) + if (Status EQUAL 0) + break() # Successful download + endif() + endforeach() + + if (NOT Status EQUAL 0) + file(REMOVE ${TMP_PATH}) + message(FATAL_ERROR "Cannot download file: ${Url}") + endif() + + else() + message("Using local copy of ${Url}") + + if ("${MD5}" STREQUAL "no-check") + message(WARNING "Not checking the MD5 of: ${Url}") + else() + file(MD5 ${TMP_PATH} ActualMD5) + if (NOT "${ActualMD5}" STREQUAL "${MD5}") + message(FATAL_ERROR "The MD5 hash of a previously download file is invalid: ${TMP_PATH}") + endif() + endif() + endif() +endmacro() + + +macro(DownloadPackage MD5 Url TargetDirectory) + if (NOT IS_DIRECTORY "${TargetDirectory}") + DownloadFile("${MD5}" "${Url}") + + GetUrlExtension(TMP_EXTENSION "${Url}") + #message(${TMP_EXTENSION}) + message("Uncompressing ${TMP_FILENAME}") + + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + # How to silently extract files using 7-zip + # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly + + if (("${TMP_EXTENSION}" STREQUAL "gz") OR + ("${TMP_EXTENSION}" STREQUAL "tgz") OR + ("${TMP_EXTENSION}" STREQUAL "xz")) + execute_process( + COMMAND ${ZIP_EXECUTABLE} e -y ${TMP_PATH} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if ("${TMP_EXTENSION}" STREQUAL "tgz") + string(REGEX REPLACE ".tgz$" ".tar" TMP_FILENAME2 "${TMP_FILENAME}") + elseif ("${TMP_EXTENSION}" STREQUAL "gz") + string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}") + elseif ("${TMP_EXTENSION}" STREQUAL "xz") + string(REGEX REPLACE ".xz" "" TMP_FILENAME2 "${TMP_FILENAME}") + endif() + + execute_process( + COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_FILENAME2} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + elseif ("${TMP_EXTENSION}" STREQUAL "zip") + execute_process( + COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_PATH} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + else() + message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}") + endif() + + else() + if ("${TMP_EXTENSION}" STREQUAL "zip") + execute_process( + COMMAND sh -c "${UNZIP_EXECUTABLE} -q ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + elseif (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz")) + #message("tar xvfz ${TMP_PATH}") + execute_process( + COMMAND sh -c "${TAR_EXECUTABLE} xfz ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + elseif ("${TMP_EXTENSION}" STREQUAL "bz2") + execute_process( + COMMAND sh -c "${TAR_EXECUTABLE} xfj ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + elseif ("${TMP_EXTENSION}" STREQUAL "xz") + execute_process( + COMMAND sh -c "${TAR_EXECUTABLE} xf ${TMP_PATH}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + else() + message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}") + endif() + endif() + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if (NOT IS_DIRECTORY "${TargetDirectory}") + message(FATAL_ERROR "The package was not uncompressed at the proper location. Check the CMake instructions.") + endif() + endif() +endmacro() + + + +macro(DownloadCompressedFile MD5 Url TargetFile) + if (NOT EXISTS "${TargetFile}") + DownloadFile("${MD5}" "${Url}") + + GetUrlExtension(TMP_EXTENSION "${Url}") + #message(${TMP_EXTENSION}) + message("Uncompressing ${TMP_FILENAME}") + + if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") + # How to silently extract files using 7-zip + # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly + + if ("${TMP_EXTENSION}" STREQUAL "gz") + execute_process( + # "-so" writes uncompressed file to stdout + COMMAND ${ZIP_EXECUTABLE} e -so -y ${TMP_PATH} + OUTPUT_FILE "${TargetFile}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + else() + message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}") + endif() + + else() + if ("${TMP_EXTENSION}" STREQUAL "gz") + execute_process( + COMMAND sh -c "${GUNZIP_EXECUTABLE} -c ${TMP_PATH}" + OUTPUT_FILE "${TargetFile}" + RESULT_VARIABLE Failure + ) + else() + message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}") + endif() + endif() + + if (Failure) + message(FATAL_ERROR "Error while running the uncompression tool") + endif() + + if (NOT EXISTS "${TargetFile}") + message(FATAL_ERROR "The file was not uncompressed at the proper location. Check the CMake instructions.") + endif() + endif() +endmacro() diff --git a/Resources/Orthanc/CMake/EmbedResources.py b/Resources/Orthanc/CMake/EmbedResources.py new file mode 100644 index 0000000..9e033e9 --- /dev/null +++ b/Resources/Orthanc/CMake/EmbedResources.py @@ -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-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +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=] [ ]*' % 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 +#include + +#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& 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 ') +elif FRAMEWORK_PATH != None: + cpp.write('#include "%s/OrthancException.h"' % FRAMEWORK_PATH) +else: + cpp.write('#include ') + +cpp.write(""" +#include +#include + +""") + +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& 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() diff --git a/Resources/Orthanc/CMake/GoogleTestConfiguration.cmake b/Resources/Orthanc/CMake/GoogleTestConfiguration.cmake new file mode 100644 index 0000000..6013f04 --- /dev/null +++ b/Resources/Orthanc/CMake/GoogleTestConfiguration.cmake @@ -0,0 +1,91 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +if (USE_GOOGLE_TEST_DEBIAN_PACKAGE) + find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR + NAMES src/gtest-all.cc + PATHS + ${CROSSTOOL_NG_IMAGE}/usr/src/gtest + ${CROSSTOOL_NG_IMAGE}/usr/src/googletest/googletest + PATH_SUFFIXES src + ) + + find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR + NAMES gtest.h + PATHS + ${CROSSTOOL_NG_IMAGE}/usr/include/gtest + ) + + message("Path to the Debian Google Test sources: ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}") + message("Path to the Debian Google Test includes: ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}") + + set(GOOGLE_TEST_SOURCES + ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}/src/gtest-all.cc + ) + + include_directories(${GOOGLE_TEST_DEBIAN_SOURCES_DIR}) + + if (NOT EXISTS ${GOOGLE_TEST_SOURCES} OR + NOT EXISTS ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}/gtest.h) + message(FATAL_ERROR "Please install the libgtest-dev package") + endif() + +elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST) + set(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/googletest-release-1.8.1) + set(GOOGLE_TEST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/gtest-1.8.1.tar.gz") + set(GOOGLE_TEST_MD5 "2e6fbeb6a91310a16efe181886c59596") + + DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}") + + include_directories( + ${GOOGLE_TEST_SOURCES_DIR}/googletest + ${GOOGLE_TEST_SOURCES_DIR}/googletest/include + ${GOOGLE_TEST_SOURCES_DIR} + ) + + set(GOOGLE_TEST_SOURCES + ${GOOGLE_TEST_SOURCES_DIR}/googletest/src/gtest-all.cc + ) + + # https://code.google.com/p/googletest/issues/detail?id=412 + if (MSVC) # VS2012 does not support tuples correctly yet + add_definitions(/D _VARIADIC_MAX=10) + endif() + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + add_definitions(-DGTEST_HAS_CLONE=0) + endif() + + source_group(ThirdParty\\GoogleTest REGULAR_EXPRESSION ${GOOGLE_TEST_SOURCES_DIR}/.*) + +else() + include(FindGTest) + if (NOT GTEST_FOUND) + message(FATAL_ERROR "Unable to find GoogleTest") + endif() + + include_directories(${GTEST_INCLUDE_DIRS}) + + # The variable GTEST_LIBRARIES contains the shared library of + # Google Test, create an alias for more uniformity + set(GOOGLE_TEST_LIBRARIES ${GTEST_LIBRARIES}) +endif() diff --git a/Resources/Orthanc/Plugins/ExportedSymbolsPlugins.list b/Resources/Orthanc/Plugins/ExportedSymbolsPlugins.list new file mode 100644 index 0000000..f76e0e8 --- /dev/null +++ b/Resources/Orthanc/Plugins/ExportedSymbolsPlugins.list @@ -0,0 +1,7 @@ +# This is the list of the symbols that must be exported by Orthanc +# plugins, if targeting OS X + +_OrthancPluginInitialize +_OrthancPluginFinalize +_OrthancPluginGetName +_OrthancPluginGetVersion diff --git a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp new file mode 100644 index 0000000..df40e00 --- /dev/null +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp @@ -0,0 +1,4117 @@ +/** + * 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-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 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 . + **/ + + +#include "OrthancPluginCppWrapper.h" + +#include +#include +#include + + +#include +#include +#include + +#if !defined(JSONCPP_VERSION_MAJOR) || !defined(JSONCPP_VERSION_MINOR) +# error Cannot access the version of JsonCpp +#endif + + +/** + * We use deprecated "Json::Reader", "Json::StyledWriter" and + * "Json::FastWriter" if JsonCpp < 1.7.0. This choice is rather + * arbitrary, but if Json >= 1.9.0, gcc generates explicit deprecation + * warnings (clang was warning in earlier versions). For reference, + * these classes seem to have been deprecated since JsonCpp 1.4.0 (on + * February 2015) by the following changeset: + * https://github.com/open-source-parsers/jsoncpp/commit/8df98f6112890d6272734975dd6d70cf8999bb22 + **/ +#if (JSONCPP_VERSION_MAJOR >= 2 || \ + (JSONCPP_VERSION_MAJOR == 1 && JSONCPP_VERSION_MINOR >= 8)) +# define JSONCPP_USE_DEPRECATED 0 +#else +# define JSONCPP_USE_DEPRECATED 1 +#endif + + +#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0) +static const OrthancPluginErrorCode OrthancPluginErrorCode_NullPointer = OrthancPluginErrorCode_Plugin; +#endif + + +namespace OrthancPlugins +{ + static OrthancPluginContext* globalContext_ = NULL; + static std::string pluginName_; + + + void SetGlobalContext(OrthancPluginContext* context) + { + if (context == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer); + } + else if (globalContext_ == NULL) + { + globalContext_ = context; + } + else + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + } + + + void SetGlobalContext(OrthancPluginContext* context, + const char* pluginName) + { + SetGlobalContext(context); + pluginName_ = pluginName; + } + + + void ResetGlobalContext() + { + globalContext_ = NULL; + pluginName_.clear(); + } + + bool HasGlobalContext() + { + return globalContext_ != NULL; + } + + + OrthancPluginContext* GetGlobalContext() + { + if (globalContext_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls); + } + else + { + return globalContext_; + } + } + + +#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 + void LogMessage(OrthancPluginLogLevel level, + const char* file, + uint32_t line, + const std::string& message) + { + if (HasGlobalContext()) + { +#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 + const char* pluginName = (pluginName_.empty() ? NULL : pluginName_.c_str()); + OrthancPluginLogMessage(GetGlobalContext(), message.c_str(), pluginName, file, line, OrthancPluginLogCategory_Generic, level); +#else + switch (level) + { + case OrthancPluginLogLevel_Error: + OrthancPluginLogError(GetGlobalContext(), message.c_str()); + break; + + case OrthancPluginLogLevel_Warning: + OrthancPluginLogWarning(GetGlobalContext(), message.c_str()); + break; + + case OrthancPluginLogLevel_Info: + OrthancPluginLogInfo(GetGlobalContext(), message.c_str()); + break; + + default: + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } +#endif + } + } +#endif + + + void LogError(const std::string& message) + { + if (HasGlobalContext()) + { + OrthancPluginLogError(GetGlobalContext(), message.c_str()); + } + } + + void LogWarning(const std::string& message) + { + if (HasGlobalContext()) + { + OrthancPluginLogWarning(GetGlobalContext(), message.c_str()); + } + } + + void LogInfo(const std::string& message) + { + if (HasGlobalContext()) + { + OrthancPluginLogInfo(GetGlobalContext(), message.c_str()); + } + } + + + void MemoryBuffer::Check(OrthancPluginErrorCode code) + { + if (code != OrthancPluginErrorCode_Success) + { + // Prevent using garbage information + buffer_.data = NULL; + buffer_.size = 0; + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } + + + bool MemoryBuffer::CheckHttp(OrthancPluginErrorCode code) + { + if (code != OrthancPluginErrorCode_Success) + { + // Prevent using garbage information + buffer_.data = NULL; + buffer_.size = 0; + } + + if (code == OrthancPluginErrorCode_Success) + { + return true; + } + else if (code == OrthancPluginErrorCode_UnknownResource || + code == OrthancPluginErrorCode_InexistentItem) + { + return false; + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } + + + MemoryBuffer::MemoryBuffer() + { + buffer_.data = NULL; + buffer_.size = 0; + } + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + MemoryBuffer::MemoryBuffer(const void* buffer, + size_t size) + { + uint32_t s = static_cast(size); + if (static_cast(s) != size) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != + OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else + { + memcpy(buffer_.data, buffer, size); + } + } +#endif + + + void MemoryBuffer::Clear() + { + if (buffer_.data != NULL) + { + OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &buffer_); + buffer_.data = NULL; + buffer_.size = 0; + } + } + + + void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other) + { + Clear(); + + buffer_.data = other.data; + buffer_.size = other.size; + + other.data = NULL; + other.size = 0; + } + + + void MemoryBuffer::Swap(MemoryBuffer& other) + { + std::swap(buffer_.data, other.buffer_.data); + std::swap(buffer_.size, other.buffer_.size); + } + + + OrthancPluginMemoryBuffer MemoryBuffer::Release() + { + OrthancPluginMemoryBuffer result = buffer_; + + buffer_.data = NULL; + buffer_.size = 0; + + return result; + } + + + void MemoryBuffer::ToString(std::string& target) const + { + if (buffer_.size == 0) + { + target.clear(); + } + else + { + target.assign(reinterpret_cast(buffer_.data), buffer_.size); + } + } + + + void MemoryBuffer::ToJson(Json::Value& target) const + { + if (buffer_.data == NULL || + buffer_.size == 0) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + if (!ReadJson(target, buffer_.data, buffer_.size)) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot convert some memory buffer to JSON"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + } + + + bool MemoryBuffer::RestApiGet(const std::string& uri, + bool applyPlugins) + { + Clear(); + + if (applyPlugins) + { + return CheckHttp(OrthancPluginRestApiGetAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str())); + } + else + { + return CheckHttp(OrthancPluginRestApiGet(GetGlobalContext(), &buffer_, uri.c_str())); + } + } + + // helper class to convert std::map of headers to the plugin SDK C structure + class PluginHttpHeaders + { + private: + std::vector headersKeys_; + std::vector headersValues_; + + public: + explicit PluginHttpHeaders(const std::map& httpHeaders) + { + for (std::map::const_iterator + it = httpHeaders.begin(); it != httpHeaders.end(); ++it) + { + headersKeys_.push_back(it->first.c_str()); + headersValues_.push_back(it->second.c_str()); + } + } + + const char* const* GetKeys() + { + return (headersKeys_.empty() ? NULL : &headersKeys_[0]); + } + + const char* const* GetValues() + { + return (headersValues_.empty() ? NULL : &headersValues_[0]); + } + + uint32_t GetSize() + { + return static_cast(headersKeys_.size()); + } + }; + + bool MemoryBuffer::RestApiGet(const std::string& uri, + const std::map& httpHeaders, + bool applyPlugins) + { + Clear(); + + PluginHttpHeaders headers(httpHeaders); + + return CheckHttp(OrthancPluginRestApiGet2( + GetGlobalContext(), &buffer_, uri.c_str(), + headers.GetSize(), + headers.GetKeys(), + headers.GetValues(), applyPlugins)); + } + + bool MemoryBuffer::RestApiPost(const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins) + { + Clear(); + + // Cast for compatibility with Orthanc SDK <= 1.5.6 + const char* b = reinterpret_cast(body); + + if (applyPlugins) + { + return CheckHttp(OrthancPluginRestApiPostAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize)); + } + else + { + return CheckHttp(OrthancPluginRestApiPost(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize)); + } + } + +#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 + + bool MemoryBuffer::RestApiPost(const std::string& uri, + const void* body, + size_t bodySize, + const std::map& httpHeaders, + bool applyPlugins) + { + MemoryBuffer answerHeaders; + uint16_t httpStatus; + + PluginHttpHeaders headers(httpHeaders); + + return CheckHttp(OrthancPluginCallRestApi(GetGlobalContext(), + &buffer_, + *answerHeaders, + &httpStatus, + OrthancPluginHttpMethod_Post, + uri.c_str(), + headers.GetSize(), headers.GetKeys(), headers.GetValues(), + body, bodySize, + applyPlugins)); + } + + + bool MemoryBuffer::RestApiPost(const std::string& uri, + const Json::Value& body, + const std::map& httpHeaders, + bool applyPlugins) + { + std::string s; + WriteFastJson(s, body); + return RestApiPost(uri, s.c_str(), s.size(), httpHeaders, applyPlugins); + } +#endif + + bool MemoryBuffer::RestApiPut(const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins) + { + Clear(); + + // Cast for compatibility with Orthanc SDK <= 1.5.6 + const char* b = reinterpret_cast(body); + + if (applyPlugins) + { + return CheckHttp(OrthancPluginRestApiPutAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize)); + } + else + { + return CheckHttp(OrthancPluginRestApiPut(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize)); + } + } + + + static bool ReadJsonInternal(Json::Value& target, + const void* buffer, + size_t size, + bool collectComments) + { +#if JSONCPP_USE_DEPRECATED == 1 + Json::Reader reader; + return reader.parse(reinterpret_cast(buffer), + reinterpret_cast(buffer) + size, target, collectComments); +#else + Json::CharReaderBuilder builder; + builder.settings_["collectComments"] = collectComments; + + const std::unique_ptr reader(builder.newCharReader()); + assert(reader.get() != NULL); + + JSONCPP_STRING err; + if (reader->parse(reinterpret_cast(buffer), + reinterpret_cast(buffer) + size, &target, &err)) + { + return true; + } + else + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot parse JSON: " + std::string(err)); + return false; + } +#endif + } + + + bool ReadJson(Json::Value& target, + const std::string& source) + { + return ReadJson(target, source.empty() ? NULL : source.c_str(), source.size()); + } + + + bool ReadJson(Json::Value& target, + const void* buffer, + size_t size) + { + return ReadJsonInternal(target, buffer, size, true); + } + + + bool ReadJsonWithoutComments(Json::Value& target, + const std::string& source) + { + return ReadJsonWithoutComments(target, source.empty() ? NULL : source.c_str(), source.size()); + } + + + bool ReadJsonWithoutComments(Json::Value& target, + const void* buffer, + size_t size) + { + return ReadJsonInternal(target, buffer, size, false); + } + + + void WriteFastJson(std::string& target, + const Json::Value& source) + { +#if JSONCPP_USE_DEPRECATED == 1 + Json::FastWriter writer; + target = writer.write(source); +#else + Json::StreamWriterBuilder builder; + builder.settings_["indentation"] = ""; + target = Json::writeString(builder, source); +#endif + } + + + void WriteStyledJson(std::string& target, + const Json::Value& source) + { +#if JSONCPP_USE_DEPRECATED == 1 + Json::StyledWriter writer; + target = writer.write(source); +#else + Json::StreamWriterBuilder builder; + builder.settings_["indentation"] = " "; + target = Json::writeString(builder, source); +#endif + } + + + bool MemoryBuffer::RestApiPost(const std::string& uri, + const Json::Value& body, + bool applyPlugins) + { + std::string s; + WriteFastJson(s, body); + return RestApiPost(uri, s, applyPlugins); + } + + + bool MemoryBuffer::RestApiPut(const std::string& uri, + const Json::Value& body, + bool applyPlugins) + { + std::string s; + WriteFastJson(s, body); + return RestApiPut(uri, s, applyPlugins); + } + + + void MemoryBuffer::CreateDicom(const Json::Value& tags, + OrthancPluginCreateDicomFlags flags) + { + Clear(); + + std::string s; + WriteFastJson(s, tags); + + Check(OrthancPluginCreateDicom(GetGlobalContext(), &buffer_, s.c_str(), NULL, flags)); + } + + void MemoryBuffer::CreateDicom(const Json::Value& tags, + const OrthancImage& pixelData, + OrthancPluginCreateDicomFlags flags) + { + Clear(); + + std::string s; + WriteFastJson(s, tags); + + Check(OrthancPluginCreateDicom(GetGlobalContext(), &buffer_, s.c_str(), pixelData.GetObject(), flags)); + } + + + void MemoryBuffer::ReadFile(const std::string& path) + { + Clear(); + Check(OrthancPluginReadFile(GetGlobalContext(), &buffer_, path.c_str())); + } + + + void MemoryBuffer::GetDicomQuery(const OrthancPluginWorklistQuery* query) + { + Clear(); + Check(OrthancPluginWorklistGetDicomQuery(GetGlobalContext(), &buffer_, query)); + } + + + void OrthancString::Assign(char* str) + { + Clear(); + + if (str != NULL) + { + str_ = str; + } + } + + + void OrthancString::Clear() + { + if (str_ != NULL) + { + OrthancPluginFreeString(GetGlobalContext(), str_); + str_ = NULL; + } + } + + + void OrthancString::ToString(std::string& target) const + { + if (str_ == NULL) + { + target.clear(); + } + else + { + target.assign(str_); + } + } + + + void OrthancString::ToJson(Json::Value& target) const + { + if (str_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot convert an empty memory buffer to JSON"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + if (!ReadJson(target, str_)) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot convert some memory buffer to JSON"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + } + + + void OrthancString::ToJsonWithoutComments(Json::Value& target) const + { + if (str_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot convert an empty memory buffer to JSON"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + if (!ReadJsonWithoutComments(target, str_)) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot convert some memory buffer to JSON"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + } + + + void MemoryBuffer::DicomToJson(Json::Value& target, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + OrthancString str; + str.Assign(OrthancPluginDicomBufferToJson + (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength)); + str.ToJson(target); + } + + + bool MemoryBuffer::HttpGet(const std::string& url, + const std::string& username, + const std::string& password) + { + Clear(); + return CheckHttp(OrthancPluginHttpGet(GetGlobalContext(), &buffer_, url.c_str(), + username.empty() ? NULL : username.c_str(), + password.empty() ? NULL : password.c_str())); + } + + + bool MemoryBuffer::HttpPost(const std::string& url, + const std::string& body, + const std::string& username, + const std::string& password) + { + Clear(); + + if (body.size() > 0xffffffffu) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + return CheckHttp(OrthancPluginHttpPost(GetGlobalContext(), &buffer_, url.c_str(), + body.c_str(), body.size(), + username.empty() ? NULL : username.c_str(), + password.empty() ? NULL : password.c_str())); + } + + + bool MemoryBuffer::HttpPut(const std::string& url, + const std::string& body, + const std::string& username, + const std::string& password) + { + Clear(); + + if (body.size() > 0xffffffffu) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + return CheckHttp(OrthancPluginHttpPut(GetGlobalContext(), &buffer_, url.c_str(), + body.empty() ? NULL : body.c_str(), + body.size(), + username.empty() ? NULL : username.c_str(), + password.empty() ? NULL : password.c_str())); + } + + + void MemoryBuffer::GetDicomInstance(const std::string& instanceId) + { + Clear(); + Check(OrthancPluginGetDicomForInstance(GetGlobalContext(), &buffer_, instanceId.c_str())); + } + + + bool HttpDelete(const std::string& url, + const std::string& username, + const std::string& password) + { + OrthancPluginErrorCode error = OrthancPluginHttpDelete + (GetGlobalContext(), url.c_str(), + username.empty() ? NULL : username.c_str(), + password.empty() ? NULL : password.c_str()); + + if (error == OrthancPluginErrorCode_Success) + { + return true; + } + else if (error == OrthancPluginErrorCode_UnknownResource || + error == OrthancPluginErrorCode_InexistentItem) + { + return false; + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); + } + } + + void OrthancConfiguration::LoadConfiguration() + { + OrthancString str; + str.Assign(OrthancPluginGetConfiguration(GetGlobalContext())); + + if (str.GetContent() == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot access the Orthanc configuration"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + str.ToJsonWithoutComments(configuration_); + + if (configuration_.type() != Json::objectValue) + { + ORTHANC_PLUGINS_LOG_ERROR("Unable to read the Orthanc configuration"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } + + + OrthancConfiguration::OrthancConfiguration() + { + LoadConfiguration(); + } + + + OrthancConfiguration::OrthancConfiguration(bool loadConfiguration) + { + if (loadConfiguration) + { + LoadConfiguration(); + } + else + { + configuration_ = Json::objectValue; + } + } + + OrthancConfiguration::OrthancConfiguration(const Json::Value& configuration, const std::string& path) : + configuration_(configuration), + path_(path) + { + } + + + std::string OrthancConfiguration::GetPath(const std::string& key) const + { + if (path_.empty()) + { + return key; + } + else + { + return path_ + "." + key; + } + } + + + bool OrthancConfiguration::IsSection(const std::string& key) const + { + assert(configuration_.type() == Json::objectValue); + + return (configuration_.isMember(key) && + configuration_[key].type() == Json::objectValue); + } + + + void OrthancConfiguration::GetSection(OrthancConfiguration& target, + const std::string& key) const + { + assert(configuration_.type() == Json::objectValue); + + target.path_ = GetPath(key); + + if (!configuration_.isMember(key)) + { + target.configuration_ = Json::objectValue; + } + else + { + if (configuration_[key].type() != Json::objectValue) + { + ORTHANC_PLUGINS_LOG_ERROR("The configuration section \"" + target.path_ + + "\" is not an associative array as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + + target.configuration_ = configuration_[key]; + } + } + + + bool OrthancConfiguration::LookupStringValue(std::string& target, + const std::string& key) const + { + assert(configuration_.type() == Json::objectValue); + + if (!configuration_.isMember(key)) + { + return false; + } + + if (configuration_[key].type() != Json::stringValue) + { + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not a string as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + + target = configuration_[key].asString(); + return true; + } + + + bool OrthancConfiguration::LookupIntegerValue(int& target, + const std::string& key) const + { + assert(configuration_.type() == Json::objectValue); + + if (!configuration_.isMember(key)) + { + return false; + } + + switch (configuration_[key].type()) + { + case Json::intValue: + target = configuration_[key].asInt(); + return true; + + case Json::uintValue: + target = configuration_[key].asUInt(); + return true; + + default: + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not an integer as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + } + + + bool OrthancConfiguration::LookupUnsignedIntegerValue(unsigned int& target, + const std::string& key) const + { + int tmp; + if (!LookupIntegerValue(tmp, key)) + { + return false; + } + + if (tmp < 0) + { + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not a positive integer as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + else + { + target = static_cast(tmp); + return true; + } + } + + + bool OrthancConfiguration::LookupBooleanValue(bool& target, + const std::string& key) const + { + assert(configuration_.type() == Json::objectValue); + + if (!configuration_.isMember(key)) + { + return false; + } + + if (configuration_[key].type() != Json::booleanValue) + { + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not a Boolean as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + + target = configuration_[key].asBool(); + return true; + } + + + bool OrthancConfiguration::LookupFloatValue(float& target, + const std::string& key) const + { + assert(configuration_.type() == Json::objectValue); + + if (!configuration_.isMember(key)) + { + return false; + } + + switch (configuration_[key].type()) + { + case Json::realValue: + target = configuration_[key].asFloat(); + return true; + + case Json::intValue: + target = static_cast(configuration_[key].asInt()); + return true; + + case Json::uintValue: + target = static_cast(configuration_[key].asUInt()); + return true; + + default: + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not an integer as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + } + + + bool OrthancConfiguration::LookupListOfStrings(std::list& target, + const std::string& key, + bool allowSingleString) const + { + assert(configuration_.type() == Json::objectValue); + + target.clear(); + + if (!configuration_.isMember(key)) + { + return false; + } + + switch (configuration_[key].type()) + { + case Json::arrayValue: + { + bool ok = true; + + for (Json::Value::ArrayIndex i = 0; ok && i < configuration_[key].size(); i++) + { + if (configuration_[key][i].type() == Json::stringValue) + { + target.push_back(configuration_[key][i].asString()); + } + else + { + ok = false; + } + } + + if (ok) + { + return true; + } + + break; + } + + case Json::stringValue: + if (allowSingleString) + { + target.push_back(configuration_[key].asString()); + return true; + } + + break; + + default: + break; + } + + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not a list of strings as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + + + bool OrthancConfiguration::LookupSetOfStrings(std::set& target, + const std::string& key, + bool allowSingleString) const + { + std::list lst; + + if (LookupListOfStrings(lst, key, allowSingleString)) + { + target.clear(); + + for (std::list::const_iterator + it = lst.begin(); it != lst.end(); ++it) + { + target.insert(*it); + } + + return true; + } + else + { + return false; + } + } + + + std::string OrthancConfiguration::GetStringValue(const std::string& key, + const std::string& defaultValue) const + { + std::string tmp; + if (LookupStringValue(tmp, key)) + { + return tmp; + } + else + { + return defaultValue; + } + } + + + int OrthancConfiguration::GetIntegerValue(const std::string& key, + int defaultValue) const + { + int tmp; + if (LookupIntegerValue(tmp, key)) + { + return tmp; + } + else + { + return defaultValue; + } + } + + + unsigned int OrthancConfiguration::GetUnsignedIntegerValue(const std::string& key, + unsigned int defaultValue) const + { + unsigned int tmp; + if (LookupUnsignedIntegerValue(tmp, key)) + { + return tmp; + } + else + { + return defaultValue; + } + } + + + bool OrthancConfiguration::GetBooleanValue(const std::string& key, + bool defaultValue) const + { + bool tmp; + if (LookupBooleanValue(tmp, key)) + { + return tmp; + } + else + { + return defaultValue; + } + } + + + float OrthancConfiguration::GetFloatValue(const std::string& key, + float defaultValue) const + { + float tmp; + if (LookupFloatValue(tmp, key)) + { + return tmp; + } + else + { + return defaultValue; + } + } + + + void OrthancConfiguration::GetDictionary(std::map& target, + const std::string& key) const + { + assert(configuration_.type() == Json::objectValue); + + target.clear(); + + if (!configuration_.isMember(key)) + { + return; + } + + if (configuration_[key].type() != Json::objectValue) + { + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not an object as expected"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + + Json::Value::Members members = configuration_[key].getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& value = configuration_[key][members[i]]; + + if (value.type() == Json::stringValue) + { + target[members[i]] = value.asString(); + } + else + { + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is not a dictionary mapping strings to strings"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + } + } + + + void OrthancImage::Clear() + { + if (image_ != NULL) + { + OrthancPluginFreeImage(GetGlobalContext(), image_); + image_ = NULL; + } + } + + + void OrthancImage::CheckImageAvailable() const + { + if (image_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Trying to access a NULL image"); + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + } + + + OrthancImage::OrthancImage() : + image_(NULL) + { + } + + + OrthancImage::OrthancImage(OrthancPluginImage* image) : + image_(image) + { + } + + + OrthancImage::OrthancImage(OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height) + { + image_ = OrthancPluginCreateImage(GetGlobalContext(), format, width, height); + + if (image_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot create an image"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } + + + OrthancImage::OrthancImage(OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + void* buffer) + { + image_ = OrthancPluginCreateImageAccessor + (GetGlobalContext(), format, width, height, pitch, buffer); + + if (image_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot create an image accessor"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } + + void OrthancImage::UncompressPngImage(const void* data, + size_t size) + { + Clear(); + + image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Png); + + if (image_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot uncompress a PNG image"); + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + } + + + void OrthancImage::UncompressJpegImage(const void* data, + size_t size) + { + Clear(); + image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Jpeg); + if (image_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot uncompress a JPEG image"); + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + } + + + void OrthancImage::DecodeDicomImage(const void* data, + size_t size, + unsigned int frame) + { + Clear(); + image_ = OrthancPluginDecodeDicomImage(GetGlobalContext(), data, size, frame); + if (image_ == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot uncompress a DICOM image"); + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + } + + + OrthancPluginPixelFormat OrthancImage::GetPixelFormat() const + { + CheckImageAvailable(); + return OrthancPluginGetImagePixelFormat(GetGlobalContext(), image_); + } + + + unsigned int OrthancImage::GetWidth() const + { + CheckImageAvailable(); + return OrthancPluginGetImageWidth(GetGlobalContext(), image_); + } + + + unsigned int OrthancImage::GetHeight() const + { + CheckImageAvailable(); + return OrthancPluginGetImageHeight(GetGlobalContext(), image_); + } + + + unsigned int OrthancImage::GetPitch() const + { + CheckImageAvailable(); + return OrthancPluginGetImagePitch(GetGlobalContext(), image_); + } + + + void* OrthancImage::GetBuffer() const + { + CheckImageAvailable(); + return OrthancPluginGetImageBuffer(GetGlobalContext(), image_); + } + + + void OrthancImage::CompressPngImage(MemoryBuffer& target) const + { + CheckImageAvailable(); + + OrthancPlugins::MemoryBuffer answer; + OrthancPluginCompressPngImage(GetGlobalContext(), *answer, GetPixelFormat(), + GetWidth(), GetHeight(), GetPitch(), GetBuffer()); + + target.Swap(answer); + } + + + void OrthancImage::CompressJpegImage(MemoryBuffer& target, + uint8_t quality) const + { + CheckImageAvailable(); + + OrthancPlugins::MemoryBuffer answer; + OrthancPluginCompressJpegImage(GetGlobalContext(), *answer, GetPixelFormat(), + GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality); + + target.Swap(answer); + } + + + void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output) const + { + CheckImageAvailable(); + OrthancPluginCompressAndAnswerPngImage(GetGlobalContext(), output, GetPixelFormat(), + GetWidth(), GetHeight(), GetPitch(), GetBuffer()); + } + + + void OrthancImage::AnswerJpegImage(OrthancPluginRestOutput* output, + uint8_t quality) const + { + CheckImageAvailable(); + OrthancPluginCompressAndAnswerJpegImage(GetGlobalContext(), output, GetPixelFormat(), + GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality); + } + + + OrthancPluginImage* OrthancImage::Release() + { + CheckImageAvailable(); + OrthancPluginImage* tmp = image_; + image_ = NULL; + return tmp; + } + + +#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 + FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) : + matcher_(NULL), + worklist_(worklist) + { + if (worklist_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange); + } + } + + + void FindMatcher::SetupDicom(const void* query, + uint32_t size) + { + worklist_ = NULL; + + matcher_ = OrthancPluginCreateFindMatcher(GetGlobalContext(), query, size); + if (matcher_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } + + + FindMatcher::~FindMatcher() + { + // The "worklist_" field + + if (matcher_ != NULL) + { + OrthancPluginFreeFindMatcher(GetGlobalContext(), matcher_); + } + } + + + + bool FindMatcher::IsMatch(const void* dicom, + uint32_t size) const + { + int32_t result; + + if (matcher_ != NULL) + { + result = OrthancPluginFindMatcherIsMatch(GetGlobalContext(), matcher_, dicom, size); + } + else if (worklist_ != NULL) + { + result = OrthancPluginWorklistIsMatch(GetGlobalContext(), worklist_, dicom, size); + } + else + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + if (result == 0) + { + return false; + } + else if (result == 1) + { + return true; + } + else + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + } + +#endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */ + + void AnswerJson(const Json::Value& value, + OrthancPluginRestOutput* output) + { + std::string bodyString; + WriteStyledJson(bodyString, value); + OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json"); + } + + void AnswerString(const std::string& answer, + const char* mimeType, + OrthancPluginRestOutput* output) + { + OrthancPluginAnswerBuffer(GetGlobalContext(), output, answer.c_str(), answer.size(), mimeType); + } + + void AnswerHttpError(uint16_t httpError, OrthancPluginRestOutput *output) + { + OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError); + } + + void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods) + { + OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods); + } + + bool RestApiGetString(std::string& result, + const std::string& uri, + bool applyPlugins) + { + MemoryBuffer answer; + if (!answer.RestApiGet(uri, applyPlugins)) + { + return false; + } + else + { + answer.ToString(result); + return true; + } + } + + bool RestApiGetString(std::string& result, + const std::string& uri, + const std::map& httpHeaders, + bool applyPlugins) + { + MemoryBuffer answer; + if (!answer.RestApiGet(uri, httpHeaders, applyPlugins)) + { + return false; + } + else + { + answer.ToString(result); + return true; + } + } + + + bool RestApiGet(Json::Value& result, + const std::string& uri, + const std::map& httpHeaders, + bool applyPlugins) + { + MemoryBuffer answer; + + if (!answer.RestApiGet(uri, httpHeaders, applyPlugins)) + { + return false; + } + else + { + if (!answer.IsEmpty()) + { + answer.ToJson(result); + } + return true; + } + } + + + bool RestApiGet(Json::Value& result, + const std::string& uri, + bool applyPlugins) + { + MemoryBuffer answer; + + if (!answer.RestApiGet(uri, applyPlugins)) + { + return false; + } + else + { + if (!answer.IsEmpty()) + { + answer.ToJson(result); + } + return true; + } + } + + + bool RestApiPost(std::string& result, + const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins) + { + MemoryBuffer answer; + + if (!answer.RestApiPost(uri, body, bodySize, applyPlugins)) + { + return false; + } + else + { + if (!answer.IsEmpty()) + { + result.assign(answer.GetData(), answer.GetSize()); + } + return true; + } + } + + + bool RestApiPost(Json::Value& result, + const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins) + { + MemoryBuffer answer; + + if (!answer.RestApiPost(uri, body, bodySize, applyPlugins)) + { + return false; + } + else + { + if (!answer.IsEmpty()) + { + answer.ToJson(result); + } + return true; + } + } + +#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 + bool RestApiPost(Json::Value& result, + const std::string& uri, + const Json::Value& body, + const std::map& httpHeaders, + bool applyPlugins) + { + MemoryBuffer answer; + + if (!answer.RestApiPost(uri, body, httpHeaders, applyPlugins)) + { + return false; + } + else + { + if (!answer.IsEmpty()) + { + answer.ToJson(result); + } + return true; + } + } +#endif + + + bool RestApiPost(Json::Value& result, + const std::string& uri, + const Json::Value& body, + bool applyPlugins) + { + std::string s; + WriteFastJson(s, body); + return RestApiPost(result, uri, s, applyPlugins); + } + + + bool RestApiPut(Json::Value& result, + const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins) + { + MemoryBuffer answer; + + if (!answer.RestApiPut(uri, body, bodySize, applyPlugins)) + { + return false; + } + else + { + if (!answer.IsEmpty()) // i.e, on a PUT to metadata/..., orthanc returns an empty response + { + answer.ToJson(result); + } + return true; + } + } + + + bool RestApiPut(Json::Value& result, + const std::string& uri, + const Json::Value& body, + bool applyPlugins) + { + std::string s; + WriteFastJson(s, body); + return RestApiPut(result, uri, s, applyPlugins); + } + + + bool RestApiDelete(const std::string& uri, + bool applyPlugins) + { + OrthancPluginErrorCode error; + + if (applyPlugins) + { + error = OrthancPluginRestApiDeleteAfterPlugins(GetGlobalContext(), uri.c_str()); + } + else + { + error = OrthancPluginRestApiDelete(GetGlobalContext(), uri.c_str()); + } + + if (error == OrthancPluginErrorCode_Success) + { + return true; + } + else if (error == OrthancPluginErrorCode_UnknownResource || + error == OrthancPluginErrorCode_InexistentItem) + { + return false; + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); + } + } + + + void ReportMinimalOrthancVersion(unsigned int major, + unsigned int minor, + unsigned int revision) + { + ORTHANC_PLUGINS_LOG_ERROR("Your version of the Orthanc core (" + + std::string(GetGlobalContext()->orthancVersion) + + ") is too old to run this plugin (version " + + boost::lexical_cast(major) + "." + + boost::lexical_cast(minor) + "." + + boost::lexical_cast(revision) + + " is required)"); + } + + bool CheckMinimalVersion(const char* version, + unsigned int major, + unsigned int minor, + unsigned int revision) + { + if (!strcmp(version, "mainline")) + { + // Assume compatibility with the mainline + return true; + } + +#ifdef _MSC_VER +#define ORTHANC_SCANF sscanf_s +#else +#define ORTHANC_SCANF sscanf +#endif + + // Parse the version + int aa, bb, cc = 0; + if ((ORTHANC_SCANF(version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 && + ORTHANC_SCANF(version, "%4d.%4d", &aa, &bb) != 2) || + aa < 0 || + bb < 0 || + cc < 0) + { + return false; + } + + unsigned int a = static_cast(aa); + unsigned int b = static_cast(bb); + unsigned int c = static_cast(cc); + + // Check the major version number + + if (a > major) + { + return true; + } + + if (a < major) + { + return false; + } + + // Check the minor version number + assert(a == major); + + if (b > minor) + { + return true; + } + + if (b < minor) + { + return false; + } + + // Check the patch level version number + assert(a == major && b == minor); + + if (c >= revision) + { + return true; + } + else + { + return false; + } + } + + + bool CheckMinimalOrthancVersion(unsigned int major, + unsigned int minor, + unsigned int revision) + { + if (!HasGlobalContext()) + { + ORTHANC_PLUGINS_LOG_ERROR("Bad Orthanc context in the plugin"); + return false; + } + + return CheckMinimalVersion(GetGlobalContext()->orthancVersion, + major, minor, revision); + } + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0) + const char* AutodetectMimeType(const std::string& path) + { + const char* mime = OrthancPluginAutodetectMimeType(GetGlobalContext(), path.c_str()); + + if (mime == NULL) + { + // Should never happen, just for safety + return "application/octet-stream"; + } + else + { + return mime; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_PEERS == 1 + size_t OrthancPeers::GetPeerIndex(const std::string& name) const + { + size_t index; + if (LookupName(index, name)) + { + return index; + } + else + { + ORTHANC_PLUGINS_LOG_ERROR("Inexistent peer: " + name); + ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource); + } + } + + + OrthancPeers::OrthancPeers() : + peers_(NULL), + timeout_(0) + { + peers_ = OrthancPluginGetPeers(GetGlobalContext()); + + if (peers_ == NULL) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + } + + uint32_t count = OrthancPluginGetPeersCount(GetGlobalContext(), peers_); + + for (uint32_t i = 0; i < count; i++) + { + const char* name = OrthancPluginGetPeerName(GetGlobalContext(), peers_, i); + if (name == NULL) + { + OrthancPluginFreePeers(GetGlobalContext(), peers_); + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + } + + index_[name] = i; + } + } + + + OrthancPeers::~OrthancPeers() + { + if (peers_ != NULL) + { + OrthancPluginFreePeers(GetGlobalContext(), peers_); + } + } + + + bool OrthancPeers::LookupName(size_t& target, + const std::string& name) const + { + Index::const_iterator found = index_.find(name); + + if (found == index_.end()) + { + return false; + } + else + { + target = found->second; + return true; + } + } + + + std::string OrthancPeers::GetPeerName(size_t index) const + { + if (index >= index_.size()) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + else + { + const char* s = OrthancPluginGetPeerName(GetGlobalContext(), peers_, static_cast(index)); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + } + else + { + return s; + } + } + } + + + std::string OrthancPeers::GetPeerUrl(size_t index) const + { + if (index >= index_.size()) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + else + { + const char* s = OrthancPluginGetPeerUrl(GetGlobalContext(), peers_, static_cast(index)); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + } + else + { + return s; + } + } + } + + + std::string OrthancPeers::GetPeerUrl(const std::string& name) const + { + return GetPeerUrl(GetPeerIndex(name)); + } + + + bool OrthancPeers::LookupUserProperty(std::string& value, + size_t index, + const std::string& key) const + { + if (index >= index_.size()) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + else + { + const char* s = OrthancPluginGetPeerUserProperty(GetGlobalContext(), peers_, static_cast(index), key.c_str()); + if (s == NULL) + { + return false; + } + else + { + value.assign(s); + return true; + } + } + } + + + bool OrthancPeers::LookupUserProperty(std::string& value, + const std::string& peer, + const std::string& key) const + { + return LookupUserProperty(value, GetPeerIndex(peer), key); + } + + + bool OrthancPeers::DoGet(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::map& headers) const + { + if (index >= index_.size()) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + + OrthancPlugins::MemoryBuffer answer; + uint16_t status; + PluginHttpHeaders pluginHeaders(headers); + + OrthancPluginErrorCode code = OrthancPluginCallPeerApi + (GetGlobalContext(), *answer, NULL, &status, peers_, + static_cast(index), OrthancPluginHttpMethod_Get, uri.c_str(), + pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), NULL, 0, timeout_); + + if (code == OrthancPluginErrorCode_Success) + { + target.Swap(answer); + return (status == 200); + } + else + { + return false; + } + } + + + bool OrthancPeers::DoGet(MemoryBuffer& target, + const std::string& name, + const std::string& uri, + const std::map& headers) const + { + size_t index; + return (LookupName(index, name) && + DoGet(target, index, uri, headers)); + } + + + bool OrthancPeers::DoGet(Json::Value& target, + size_t index, + const std::string& uri, + const std::map& headers) const + { + MemoryBuffer buffer; + + if (DoGet(buffer, index, uri, headers)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + + + bool OrthancPeers::DoGet(Json::Value& target, + const std::string& name, + const std::string& uri, + const std::map& headers) const + { + MemoryBuffer buffer; + + if (DoGet(buffer, name, uri, headers)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + + + bool OrthancPeers::DoPost(MemoryBuffer& target, + const std::string& name, + const std::string& uri, + const std::string& body, + const std::map& headers) const + { + size_t index; + return (LookupName(index, name) && + DoPost(target, index, uri, body, headers)); + } + + + bool OrthancPeers::DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const std::map& headers) const + { + MemoryBuffer buffer; + + if (DoPost(buffer, index, uri, body, headers)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + + + bool OrthancPeers::DoPost(Json::Value& target, + const std::string& name, + const std::string& uri, + const std::string& body, + const std::map& headers) const + { + MemoryBuffer buffer; + + if (DoPost(buffer, name, uri, body, headers)) + { + buffer.ToJson(target); + return true; + } + else + { + return false; + } + } + + + bool OrthancPeers::DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const std::map& headers) const + { + if (index >= index_.size()) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + + if (body.size() > 0xffffffffu) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + OrthancPlugins::MemoryBuffer answer; + uint16_t status; + PluginHttpHeaders pluginHeaders(headers); + + OrthancPluginErrorCode code = OrthancPluginCallPeerApi + (GetGlobalContext(), *answer, NULL, &status, peers_, + static_cast(index), OrthancPluginHttpMethod_Post, uri.c_str(), + pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_); + + if (code == OrthancPluginErrorCode_Success) + { + target.Swap(answer); + return (status == 200); + } + else + { + return false; + } + } + + + bool OrthancPeers::DoPut(size_t index, + const std::string& uri, + const std::string& body, + const std::map& headers) const + { + if (index >= index_.size()) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + + if (body.size() > 0xffffffffu) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + OrthancPlugins::MemoryBuffer answer; + uint16_t status; + PluginHttpHeaders pluginHeaders(headers); + + OrthancPluginErrorCode code = OrthancPluginCallPeerApi + (GetGlobalContext(), *answer, NULL, &status, peers_, + static_cast(index), OrthancPluginHttpMethod_Put, uri.c_str(), + pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), body.empty() ? NULL : body.c_str(), body.size(), timeout_); + + if (code == OrthancPluginErrorCode_Success) + { + return (status == 200); + } + else + { + return false; + } + } + + + bool OrthancPeers::DoPut(const std::string& name, + const std::string& uri, + const std::string& body, + const std::map& headers) const + { + size_t index; + return (LookupName(index, name) && + DoPut(index, uri, body, headers)); + } + + + bool OrthancPeers::DoDelete(size_t index, + const std::string& uri, + const std::map& headers) const + { + if (index >= index_.size()) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + + OrthancPlugins::MemoryBuffer answer; + uint16_t status; + PluginHttpHeaders pluginHeaders(headers); + + OrthancPluginErrorCode code = OrthancPluginCallPeerApi + (GetGlobalContext(), *answer, NULL, &status, peers_, + static_cast(index), OrthancPluginHttpMethod_Delete, uri.c_str(), + pluginHeaders.GetSize(), pluginHeaders.GetKeys(), pluginHeaders.GetValues(), NULL, 0, timeout_); + + if (code == OrthancPluginErrorCode_Success) + { + return (status == 200); + } + else + { + return false; + } + } + + + bool OrthancPeers::DoDelete(const std::string& name, + const std::string& uri, + const std::map& headers) const + { + size_t index; + return (LookupName(index, name) && + DoDelete(index, uri, headers)); + } +#endif + + + + + + /****************************************************************** + ** JOBS + ******************************************************************/ + +#if HAS_ORTHANC_PLUGIN_JOB == 1 + void OrthancJob::CallbackFinalize(void* job) + { + if (job != NULL) + { + delete reinterpret_cast(job); + } + } + + + float OrthancJob::CallbackGetProgress(void* job) + { + assert(job != NULL); + + try + { + return reinterpret_cast(job)->progress_; + } + catch (...) + { + return 0; + } + } + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3) + static OrthancPluginErrorCode CopyStringToMemoryBuffer(OrthancPluginMemoryBuffer* target, + const std::string& source) + { + if (OrthancPluginCreateMemoryBuffer(globalContext_, target, source.size()) != OrthancPluginErrorCode_Success) + { + return OrthancPluginErrorCode_NotEnoughMemory; + } + else + { + if (!source.empty()) + { + memcpy(target->data, source.c_str(), source.size()); + } + + return OrthancPluginErrorCode_Success; + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3) + OrthancPluginErrorCode OrthancJob::CallbackGetContent(OrthancPluginMemoryBuffer* target, + void* job) + { + assert(job != NULL); + OrthancJob& that = *reinterpret_cast(job); + return CopyStringToMemoryBuffer(target, that.content_); + } +#else + const char* OrthancJob::CallbackGetContent(void* job) + { + assert(job != NULL); + + try + { + return reinterpret_cast(job)->content_.c_str(); + } + catch (...) + { + return 0; + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3) + int32_t OrthancJob::CallbackGetSerialized(OrthancPluginMemoryBuffer* target, + void* job) + { + assert(job != NULL); + OrthancJob& that = *reinterpret_cast(job); + + if (that.hasSerialized_) + { + if (CopyStringToMemoryBuffer(target, that.serialized_) == OrthancPluginErrorCode_Success) + { + return 1; + } + else + { + return -1; + } + } + else + { + return 0; + } + } +#else + const char* OrthancJob::CallbackGetSerialized(void* job) + { + assert(job != NULL); + + try + { + const OrthancJob& tmp = *reinterpret_cast(job); + + if (tmp.hasSerialized_) + { + return tmp.serialized_.c_str(); + } + else + { + return NULL; + } + } + catch (...) + { + return 0; + } + } +#endif + + + OrthancPluginJobStepStatus OrthancJob::CallbackStep(void* job) + { + assert(job != NULL); + + try + { + return reinterpret_cast(job)->Step(); + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) + { + return OrthancPluginJobStepStatus_Failure; + } + catch (...) + { + return OrthancPluginJobStepStatus_Failure; + } + } + + + OrthancPluginErrorCode OrthancJob::CallbackStop(void* job, + OrthancPluginJobStopReason reason) + { + assert(job != NULL); + + try + { + reinterpret_cast(job)->Stop(reason); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } + + + OrthancPluginErrorCode OrthancJob::CallbackReset(void* job) + { + assert(job != NULL); + + try + { + reinterpret_cast(job)->Reset(); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } + + + void OrthancJob::ClearContent() + { + Json::Value empty = Json::objectValue; + UpdateContent(empty); + } + + + void OrthancJob::UpdateContent(const Json::Value& content) + { + if (content.type() != Json::objectValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat); + } + else + { + WriteFastJson(content_, content); + } + } + + + void OrthancJob::ClearSerialized() + { + hasSerialized_ = false; + serialized_.clear(); + } + + + void OrthancJob::UpdateSerialized(const Json::Value& serialized) + { + if (serialized.type() != Json::objectValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat); + } + else + { + WriteFastJson(serialized_, serialized); + hasSerialized_ = true; + } + } + + + void OrthancJob::UpdateProgress(float progress) + { + if (progress < 0 || + progress > 1) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + } + + progress_ = progress; + } + + + OrthancJob::OrthancJob(const std::string& jobType) : + jobType_(jobType), + progress_(0) + { + ClearContent(); + ClearSerialized(); + } + + + OrthancPluginJob* OrthancJob::Create(OrthancJob* job) + { + if (job == NULL) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer); + } + + OrthancPluginJob* orthanc = +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3) + OrthancPluginCreateJob2 +#else + OrthancPluginCreateJob +#endif + (GetGlobalContext(), job, CallbackFinalize, job->jobType_.c_str(), + CallbackGetProgress, CallbackGetContent, CallbackGetSerialized, + CallbackStep, CallbackStop, CallbackReset); + + if (orthanc == NULL) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + } + else + { + return orthanc; + } + } + + + std::string OrthancJob::Submit(OrthancJob* job, + int priority) + { + if (job == NULL) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer); + } + + OrthancPluginJob* orthanc = Create(job); + + char* id = OrthancPluginSubmitJob(GetGlobalContext(), orthanc, priority); + + if (id == NULL) + { + ORTHANC_PLUGINS_LOG_ERROR("Plugin cannot submit job"); + OrthancPluginFreeJob(GetGlobalContext(), orthanc); + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + } + else + { + std::string tmp(id); + tmp.assign(id); + OrthancPluginFreeString(GetGlobalContext(), id); + + return tmp; + } + } + + + void OrthancJob::SubmitAndWait(Json::Value& result, + OrthancJob* job /* takes ownership */, + int priority) + { + std::string id = Submit(job, priority); + + for (;;) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + + Json::Value status; + if (!RestApiGet(status, "/jobs/" + id, false) || + !status.isMember("State") || + status["State"].type() != Json::stringValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InexistentItem); + } + + const std::string state = status["State"].asString(); + if (state == "Success") + { + if (status.isMember("Content")) + { + result = status["Content"]; + } + else + { + result = Json::objectValue; + } + + return; + } + else if (state == "Running") + { + continue; + } + else if (!status.isMember("ErrorCode") || + status["ErrorCode"].type() != Json::intValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError); + } + else + { + if (!status.isMember("ErrorDescription") || + status["ErrorDescription"].type() != Json::stringValue) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt()); + } + else + { +#if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(static_cast(status["ErrorCode"].asInt()), + status["ErrorDescription"].asString()); +#else + ORTHANC_PLUGINS_LOG_ERROR("Exception while executing the job: " + status["ErrorDescription"].asString()); + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt()); +#endif + } + } + } + } + + + void OrthancJob::SubmitFromRestApiPost(OrthancPluginRestOutput* output, + const Json::Value& body, + OrthancJob* job) + { + static const char* KEY_SYNCHRONOUS = "Synchronous"; + static const char* KEY_ASYNCHRONOUS = "Asynchronous"; + static const char* KEY_PRIORITY = "Priority"; + + boost::movelib::unique_ptr protection(job); + + if (body.type() != Json::objectValue) + { +#if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Expected a JSON object in the body"); +#else + ORTHANC_PLUGINS_LOG_ERROR("Expected a JSON object in the body"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); +#endif + } + + bool synchronous = true; + + if (body.isMember(KEY_SYNCHRONOUS)) + { + if (body[KEY_SYNCHRONOUS].type() != Json::booleanValue) + { +#if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Option \"" + std::string(KEY_SYNCHRONOUS) + + "\" must be Boolean"); +#else + ORTHANC_PLUGINS_LOG_ERROR("Option \"" + std::string(KEY_SYNCHRONOUS) + "\" must be Boolean"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); +#endif + } + else + { + synchronous = body[KEY_SYNCHRONOUS].asBool(); + } + } + + if (body.isMember(KEY_ASYNCHRONOUS)) + { + if (body[KEY_ASYNCHRONOUS].type() != Json::booleanValue) + { +#if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Option \"" + std::string(KEY_ASYNCHRONOUS) + + "\" must be Boolean"); +#else + ORTHANC_PLUGINS_LOG_ERROR("Option \"" + std::string(KEY_ASYNCHRONOUS) + "\" must be Boolean"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); +#endif + } + else + { + synchronous = !body[KEY_ASYNCHRONOUS].asBool(); + } + } + + int priority = 0; + + if (body.isMember(KEY_PRIORITY)) + { + if (body[KEY_PRIORITY].type() != Json::intValue) + { +#if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Option \"" + std::string(KEY_PRIORITY) + + "\" must be an integer"); +#else + ORTHANC_PLUGINS_LOG_ERROR("Option \"" + std::string(KEY_PRIORITY) + "\" must be an integer"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); +#endif + } + else + { + priority = !body[KEY_PRIORITY].asInt(); + } + } + + Json::Value result; + + if (synchronous) + { + OrthancPlugins::OrthancJob::SubmitAndWait(result, protection.release(), priority); + } + else + { + std::string id = OrthancPlugins::OrthancJob::Submit(protection.release(), priority); + + result = Json::objectValue; + result["ID"] = id; + result["Path"] = "/jobs/" + id; + } + + std::string s = result.toStyledString(); + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), + s.size(), "application/json"); + } + +#endif + + + + + /****************************************************************** + ** METRICS + ******************************************************************/ + +#if HAS_ORTHANC_PLUGIN_METRICS == 1 + MetricsTimer::MetricsTimer(const char* name) : + name_(name) + { + start_ = boost::posix_time::microsec_clock::universal_time(); + } + + MetricsTimer::~MetricsTimer() + { + const boost::posix_time::ptime stop = boost::posix_time::microsec_clock::universal_time(); + const boost::posix_time::time_duration diff = stop - start_; + OrthancPluginSetMetricsValue(GetGlobalContext(), name_.c_str(), static_cast(diff.total_milliseconds()), + OrthancPluginMetricsType_Timer); + } +#endif + + + + + /****************************************************************** + ** HTTP CLIENT + ******************************************************************/ + +#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 + class HttpClient::RequestBodyWrapper : public boost::noncopyable + { + private: + static RequestBodyWrapper& GetObject(void* body) + { + assert(body != NULL); + return *reinterpret_cast(body); + } + + IRequestBody& body_; + bool done_; + std::string chunk_; + + public: + RequestBodyWrapper(IRequestBody& body) : + body_(body), + done_(false) + { + } + + static uint8_t IsDone(void* body) + { + return GetObject(body).done_; + } + + static const void* GetChunkData(void* body) + { + return GetObject(body).chunk_.c_str(); + } + + static uint32_t GetChunkSize(void* body) + { + return static_cast(GetObject(body).chunk_.size()); + } + + static OrthancPluginErrorCode Next(void* body) + { + RequestBodyWrapper& that = GetObject(body); + + if (that.done_) + { + return OrthancPluginErrorCode_BadSequenceOfCalls; + } + else + { + try + { + that.done_ = !that.body_.ReadNextChunk(that.chunk_); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } + } + }; + + +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + static OrthancPluginErrorCode AnswerAddHeaderCallback(void* answer, + const char* key, + const char* value) + { + assert(answer != NULL && key != NULL && value != NULL); + + try + { + reinterpret_cast(answer)->AddHeader(key, value); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + static OrthancPluginErrorCode AnswerAddChunkCallback(void* answer, + const void* data, + uint32_t size) + { + assert(answer != NULL); + + try + { + reinterpret_cast(answer)->AddChunk(data, size); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + + HttpClient::HttpClient() : + httpStatus_(0), + method_(OrthancPluginHttpMethod_Get), + timeout_(0), + pkcs11_(false), + chunkedBody_(NULL), + allowChunkedTransfers_(true) + { + } + + + void HttpClient::AddHeaders(const HttpHeaders& headers) + { + for (HttpHeaders::const_iterator it = headers.begin(); + it != headers.end(); ++it) + { + headers_[it->first] = it->second; + } + } + + + void HttpClient::SetCredentials(const std::string& username, + const std::string& password) + { + username_ = username; + password_ = password; + } + + + void HttpClient::ClearCredentials() + { + username_.clear(); + password_.clear(); + } + + + void HttpClient::SetCertificate(const std::string& certificateFile, + const std::string& keyFile, + const std::string& keyPassword) + { + certificateFile_ = certificateFile; + certificateKeyFile_ = keyFile; + certificateKeyPassword_ = keyPassword; + } + + + void HttpClient::ClearCertificate() + { + certificateFile_.clear(); + certificateKeyFile_.clear(); + certificateKeyPassword_.clear(); + } + + + void HttpClient::ClearBody() + { + fullBody_.clear(); + chunkedBody_ = NULL; + } + + + void HttpClient::SwapBody(std::string& body) + { + fullBody_.swap(body); + chunkedBody_ = NULL; + } + + + void HttpClient::SetBody(const std::string& body) + { + fullBody_ = body; + chunkedBody_ = NULL; + } + + + void HttpClient::SetBody(IRequestBody& body) + { + fullBody_.clear(); + chunkedBody_ = &body; + } + + + namespace + { + class HeadersWrapper : public boost::noncopyable + { + private: + std::vector headersKeys_; + std::vector headersValues_; + + public: + HeadersWrapper(const HttpClient::HttpHeaders& headers) + { + headersKeys_.reserve(headers.size()); + headersValues_.reserve(headers.size()); + + for (HttpClient::HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it) + { + headersKeys_.push_back(it->first.c_str()); + headersValues_.push_back(it->second.c_str()); + } + } + + void AddStaticString(const char* key, + const char* value) + { + headersKeys_.push_back(key); + headersValues_.push_back(value); + } + + uint32_t GetCount() const + { + return headersKeys_.size(); + } + + const char* const* GetKeys() const + { + return headersKeys_.empty() ? NULL : &headersKeys_[0]; + } + + const char* const* GetValues() const + { + return headersValues_.empty() ? NULL : &headersValues_[0]; + } + }; + + + class MemoryRequestBody : public HttpClient::IRequestBody + { + private: + std::string body_; + bool done_; + + public: + MemoryRequestBody(const std::string& body) : + body_(body), + done_(false) + { + if (body_.empty()) + { + done_ = true; + } + } + + virtual bool ReadNextChunk(std::string& chunk) + { + if (done_) + { + return false; + } + else + { + chunk.swap(body_); + done_ = true; + return true; + } + } + }; + + + // This class mimics Orthanc::ChunkedBuffer + class ChunkedBuffer : public boost::noncopyable + { + private: + typedef std::list Content; + + Content content_; + size_t size_; + + public: + ChunkedBuffer() : + size_(0) + { + } + + ~ChunkedBuffer() + { + Clear(); + } + + void Clear() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + + size_ = 0; + content_.clear(); + } + + /** + * Since Orthanc 1.9.3, this function also clears the content of + * the ChunkedBuffer in order to mimic the behavior of the + * original class "Orthanc::ChunkedBuffer". This prevents the + * forgetting of calling "Clear()" in order to reduce memory + * consumption. + **/ + void Flatten(std::string& target) + { + target.resize(size_); + + size_t pos = 0; + + for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(*it != NULL); + size_t s = (*it)->size(); + + if (s != 0) + { + memcpy(&target[pos], (*it)->c_str(), s); + pos += s; + } + + delete *it; + } + + assert(pos == target.size()); + + size_ = 0; + content_.clear(); + } + + void AddChunk(const void* data, + size_t size) + { + content_.push_back(new std::string(reinterpret_cast(data), size)); + size_ += size; + } + + void AddChunk(const std::string& chunk) + { + content_.push_back(new std::string(chunk)); + size_ += chunk.size(); + } + }; + + +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + class MemoryAnswer : public HttpClient::IAnswer + { + private: + HttpClient::HttpHeaders headers_; + ChunkedBuffer body_; + + public: + const HttpClient::HttpHeaders& GetHeaders() const + { + return headers_; + } + + ChunkedBuffer& GetBody() + { + return body_; + } + + virtual void AddHeader(const std::string& key, + const std::string& value) + { + headers_[key] = value; + } + + virtual void AddChunk(const void* data, + size_t size) + { + body_.AddChunk(data, size); + } + }; +#endif + } + + +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + void HttpClient::ExecuteWithStream(uint16_t& httpStatus, + IAnswer& answer, + IRequestBody& body) const + { + HeadersWrapper h(headers_); + + if (method_ == OrthancPluginHttpMethod_Post || + method_ == OrthancPluginHttpMethod_Put) + { + // Automatically set the "Transfer-Encoding" header if absent + bool found = false; + + for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it) + { + if (boost::iequals(it->first, "Transfer-Encoding")) + { + found = true; + break; + } + } + + if (!found) + { + h.AddStaticString("Transfer-Encoding", "chunked"); + } + } + + RequestBodyWrapper request(body); + + OrthancPluginErrorCode error = OrthancPluginChunkedHttpClient( + GetGlobalContext(), + &answer, + AnswerAddChunkCallback, + AnswerAddHeaderCallback, + &httpStatus, + method_, + url_.c_str(), + h.GetCount(), + h.GetKeys(), + h.GetValues(), + &request, + RequestBodyWrapper::IsDone, + RequestBodyWrapper::GetChunkData, + RequestBodyWrapper::GetChunkSize, + RequestBodyWrapper::Next, + username_.empty() ? NULL : username_.c_str(), + password_.empty() ? NULL : password_.c_str(), + timeout_, + certificateFile_.empty() ? NULL : certificateFile_.c_str(), + certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(), + certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(), + pkcs11_ ? 1 : 0); + + if (error != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); + } + } +#endif + + + void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus, + HttpHeaders& answerHeaders, + std::string& answerBody, + const std::string& body) const + { + HeadersWrapper headers(headers_); + + MemoryBuffer answerBodyBuffer, answerHeadersBuffer; + + if (body.size() > 0xffffffffu) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + OrthancPluginErrorCode error = OrthancPluginHttpClient( + GetGlobalContext(), + *answerBodyBuffer, + *answerHeadersBuffer, + &httpStatus, + method_, + url_.c_str(), + headers.GetCount(), + headers.GetKeys(), + headers.GetValues(), + body.empty() ? NULL : body.c_str(), + body.size(), + username_.empty() ? NULL : username_.c_str(), + password_.empty() ? NULL : password_.c_str(), + timeout_, + certificateFile_.empty() ? NULL : certificateFile_.c_str(), + certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(), + certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(), + pkcs11_ ? 1 : 0); + + if (error != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); + } + + Json::Value v; + answerHeadersBuffer.ToJson(v); + + if (v.type() != Json::objectValue) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + Json::Value::Members members = v.getMemberNames(); + answerHeaders.clear(); + + for (size_t i = 0; i < members.size(); i++) + { + const Json::Value& h = v[members[i]]; + if (h.type() != Json::stringValue) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + else + { + answerHeaders[members[i]] = h.asString(); + } + } + + answerBodyBuffer.ToString(answerBody); + } + + + void HttpClient::Execute(IAnswer& answer) + { +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + if (allowChunkedTransfers_) + { + if (chunkedBody_ != NULL) + { + ExecuteWithStream(httpStatus_, answer, *chunkedBody_); + } + else + { + MemoryRequestBody wrapper(fullBody_); + ExecuteWithStream(httpStatus_, answer, wrapper); + } + + return; + } +#endif + + // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked + // transfers are disabled. This results in higher memory usage + // (all chunks from the answer body are sent at once) + + HttpHeaders answerHeaders; + std::string answerBody; + Execute(answerHeaders, answerBody); + + for (HttpHeaders::const_iterator it = answerHeaders.begin(); + it != answerHeaders.end(); ++it) + { + answer.AddHeader(it->first, it->second); + } + + if (!answerBody.empty()) + { + answer.AddChunk(answerBody.c_str(), answerBody.size()); + } + } + + + void HttpClient::Execute(HttpHeaders& answerHeaders /* out */, + std::string& answerBody /* out */) + { +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + if (allowChunkedTransfers_) + { + MemoryAnswer answer; + Execute(answer); + answerHeaders = answer.GetHeaders(); + answer.GetBody().Flatten(answerBody); + return; + } +#endif + + // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked + // transfers are disabled. This results in higher memory usage + // (all chunks from the request body are sent at once) + + if (chunkedBody_ != NULL) + { + ChunkedBuffer buffer; + + std::string chunk; + while (chunkedBody_->ReadNextChunk(chunk)) + { + buffer.AddChunk(chunk); + } + + std::string body; + buffer.Flatten(body); + + ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, body); + } + else + { + ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, fullBody_); + } + } + + + void HttpClient::Execute(HttpHeaders& answerHeaders /* out */, + Json::Value& answerBody /* out */) + { + std::string body; + Execute(answerHeaders, body); + + if (!ReadJson(answerBody, body)) + { + ORTHANC_PLUGINS_LOG_ERROR("Cannot convert HTTP answer body to JSON"); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + } + + + void HttpClient::Execute() + { + HttpHeaders answerHeaders; + std::string body; + Execute(answerHeaders, body); + } + +#endif /* HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 */ + + + + + + /****************************************************************** + ** CHUNKED HTTP SERVER + ******************************************************************/ + + namespace Internals + { + void NullRestCallback(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + } + + IChunkedRequestReader *NullChunkedRestCallback(const char* url, + const OrthancPluginHttpRequest* request) + { + return NULL; + } + + +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1 + + OrthancPluginErrorCode ChunkedRequestReaderAddChunk( + OrthancPluginServerChunkedRequestReader* reader, + const void* data, + uint32_t size) + { + try + { + if (reader == NULL) + { + return OrthancPluginErrorCode_InternalError; + } + + reinterpret_cast(reader)->AddChunk(data, size); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + return OrthancPluginErrorCode_BadFileFormat; + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } + + + OrthancPluginErrorCode ChunkedRequestReaderExecute( + OrthancPluginServerChunkedRequestReader* reader, + OrthancPluginRestOutput* output) + { + try + { + if (reader == NULL) + { + return OrthancPluginErrorCode_InternalError; + } + + reinterpret_cast(reader)->Execute(output); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + return OrthancPluginErrorCode_BadFileFormat; + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } + + + void ChunkedRequestReaderFinalize( + OrthancPluginServerChunkedRequestReader* reader) + { + if (reader != NULL) + { + delete reinterpret_cast(reader); + } + } + +#else + + OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request, + RestCallback GetHandler, + ChunkedRestCallback PostHandler, + RestCallback DeleteHandler, + ChunkedRestCallback PutHandler) + { + try + { + std::string allowed; + + if (GetHandler != Internals::NullRestCallback) + { + allowed += "GET"; + } + + if (PostHandler != Internals::NullChunkedRestCallback) + { + if (!allowed.empty()) + { + allowed += ","; + } + + allowed += "POST"; + } + + if (DeleteHandler != Internals::NullRestCallback) + { + if (!allowed.empty()) + { + allowed += ","; + } + + allowed += "DELETE"; + } + + if (PutHandler != Internals::NullChunkedRestCallback) + { + if (!allowed.empty()) + { + allowed += ","; + } + + allowed += "PUT"; + } + + switch (request->method) + { + case OrthancPluginHttpMethod_Get: + if (GetHandler == Internals::NullRestCallback) + { + OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str()); + } + else + { + GetHandler(output, url, request); + } + + break; + + case OrthancPluginHttpMethod_Post: + if (PostHandler == Internals::NullChunkedRestCallback) + { + OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str()); + } + else + { + boost::movelib::unique_ptr reader(PostHandler(url, request)); + if (reader.get() == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + reader->AddChunk(request->body, request->bodySize); + reader->Execute(output); + } + } + + break; + + case OrthancPluginHttpMethod_Delete: + if (DeleteHandler == Internals::NullRestCallback) + { + OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str()); + } + else + { + DeleteHandler(output, url, request); + } + + break; + + case OrthancPluginHttpMethod_Put: + if (PutHandler == Internals::NullChunkedRestCallback) + { + OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str()); + } + else + { + boost::movelib::unique_ptr reader(PutHandler(url, request)); + if (reader.get() == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + reader->AddChunk(request->body, request->bodySize); + reader->Execute(output); + } + } + + break; + + default: + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { +#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1 + if (HasGlobalContext() && + e.HasDetails()) + { + // The "false" instructs Orthanc not to log the detailed + // error message. This is to avoid duplicating the details, + // because "OrthancException" already does it on construction. + OrthancPluginSetHttpErrorDetails + (GetGlobalContext(), output, e.GetDetails(), false); + } +#endif + + return static_cast(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + return OrthancPluginErrorCode_BadFileFormat; + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + } + + +#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 + OrthancPluginErrorCode IStorageCommitmentScpHandler::Lookup( + OrthancPluginStorageCommitmentFailureReason* target, + void* rawHandler, + const char* sopClassUid, + const char* sopInstanceUid) + { + assert(target != NULL && + rawHandler != NULL); + + try + { + IStorageCommitmentScpHandler& handler = *reinterpret_cast(rawHandler); + *target = handler.Lookup(sopClassUid, sopInstanceUid); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 + void IStorageCommitmentScpHandler::Destructor(void* rawHandler) + { + assert(rawHandler != NULL); + delete reinterpret_cast(rawHandler); + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + DicomInstance::DicomInstance(const OrthancPluginDicomInstance* instance) : + toFree_(false), + instance_(instance) + { + } +#else + DicomInstance::DicomInstance(OrthancPluginDicomInstance* instance) : + toFree_(false), + instance_(instance) + { + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance::DicomInstance(const void* buffer, + size_t size) : + toFree_(true), + instance_(OrthancPluginCreateDicomInstance(GetGlobalContext(), buffer, size)) + { + if (instance_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer); + } + } +#endif + + + DicomInstance::~DicomInstance() + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + if (toFree_ && + instance_ != NULL) + { + OrthancPluginFreeDicomInstance( + GetGlobalContext(), const_cast(instance_)); + } +#endif + } + + + std::string DicomInstance::GetRemoteAet() const + { + const char* s = OrthancPluginGetInstanceRemoteAet(GetGlobalContext(), instance_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return std::string(s); + } + } + + + void DicomInstance::GetJson(Json::Value& target) const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceJson(GetGlobalContext(), instance_)); + s.ToJson(target); + } + + + void DicomInstance::GetSimplifiedJson(Json::Value& target) const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceSimplifiedJson(GetGlobalContext(), instance_)); + s.ToJson(target); + } + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + std::string DicomInstance::GetTransferSyntaxUid() const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceTransferSyntaxUid(GetGlobalContext(), instance_)); + + std::string result; + s.ToString(result); + return result; + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + bool DicomInstance::HasPixelData() const + { + int32_t result = OrthancPluginHasInstancePixelData(GetGlobalContext(), instance_); + if (result < 0) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return (result != 0); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void DicomInstance::GetRawFrame(std::string& target, + unsigned int frameIndex) const + { + MemoryBuffer buffer; + OrthancPluginErrorCode code = OrthancPluginGetInstanceRawFrame( + GetGlobalContext(), *buffer, instance_, frameIndex); + + if (code == OrthancPluginErrorCode_Success) + { + buffer.ToString(target); + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + OrthancImage* DicomInstance::GetDecodedFrame(unsigned int frameIndex) const + { + OrthancPluginImage* image = OrthancPluginGetInstanceDecodedFrame( + GetGlobalContext(), instance_, frameIndex); + + if (image == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return new OrthancImage(image); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void DicomInstance::Serialize(std::string& target) const + { + MemoryBuffer buffer; + OrthancPluginErrorCode code = OrthancPluginSerializeDicomInstance( + GetGlobalContext(), *buffer, instance_); + + if (code == OrthancPluginErrorCode_Success) + { + buffer.ToString(target); + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance* DicomInstance::Transcode(const void* buffer, + size_t size, + const std::string& transferSyntax) + { + OrthancPluginDicomInstance* instance = OrthancPluginTranscodeDicomInstance( + GetGlobalContext(), buffer, size, transferSyntax.c_str()); + + if (instance == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + boost::movelib::unique_ptr result(new DicomInstance(instance)); + result->toFree_ = true; + return result.release(); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1) + DicomInstance* DicomInstance::Load(const std::string& instanceId, + OrthancPluginLoadDicomInstanceMode mode) + { + OrthancPluginDicomInstance* instance = OrthancPluginLoadDicomInstance( + GetGlobalContext(), instanceId.c_str(), mode); + + if (instance == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + boost::movelib::unique_ptr result(new DicomInstance(instance)); + result->toFree_ = true; + return result.release(); + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static std::vector WebDavConvertPath(uint32_t pathSize, + const char* const* pathItems) + { + std::vector result(pathSize); + + for (uint32_t i = 0; i < pathSize; i++) + { + result[i] = pathItems[i]; + } + + return result; + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavIsExistingFolder(uint8_t* isExisting, + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + *isExisting = (that.IsExistingFolder(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavListFolder(uint8_t* isExisting, + OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavAddFile addFile, + OrthancPluginWebDavAddFolder addFolder, + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + std::list files; + std::list subfolders; + + if (!that.ListFolder(files, subfolders, WebDavConvertPath(pathSize, pathItems))) + { + *isExisting = 0; + } + else + { + *isExisting = 1; + + for (std::list::const_iterator + it = files.begin(); it != files.end(); ++it) + { + OrthancPluginErrorCode code = addFile( + collection, it->GetName().c_str(), it->GetContentSize(), + it->GetMimeType().c_str(), it->GetDateTime().c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + return code; + } + } + + for (std::list::const_iterator it = + subfolders.begin(); it != subfolders.end(); ++it) + { + OrthancPluginErrorCode code = addFolder( + collection, it->GetName().c_str(), it->GetDateTime().c_str()); + + if (code != OrthancPluginErrorCode_Success) + { + return code; + } + } + } + + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavRetrieveFile(OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavRetrieveFile retrieveFile, + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + std::string content, mime, dateTime; + + if (that.GetFile(content, mime, dateTime, WebDavConvertPath(pathSize, pathItems))) + { + return retrieveFile(collection, content.empty() ? NULL : content.c_str(), + content.size(), mime.c_str(), dateTime.c_str()); + } + else + { + // Inexisting file + return OrthancPluginErrorCode_Success; + } + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavStoreFileCallback(uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + const void* data, + uint64_t size, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + if (static_cast(static_cast(size)) != size) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + + *isReadOnly = (that.StoreFile(WebDavConvertPath(pathSize, pathItems), data, + static_cast(size)) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavCreateFolderCallback(uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + *isReadOnly = (that.CreateFolder(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + static OrthancPluginErrorCode WebDavDeleteItemCallback(uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload) + { + IWebDavCollection& that = *reinterpret_cast(payload); + + try + { + *isReadOnly = (that.DeleteItem(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (...) + { + return OrthancPluginErrorCode_InternalError; + } + } +#endif + + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + void IWebDavCollection::Register(const std::string& uri, + IWebDavCollection& collection) + { + OrthancPluginErrorCode code = OrthancPluginRegisterWebDavCollection( + GetGlobalContext(), uri.c_str(), WebDavIsExistingFolder, WebDavListFolder, WebDavRetrieveFile, + WebDavStoreFileCallback, WebDavCreateFolderCallback, WebDavDeleteItemCallback, &collection); + + if (code != OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + void GetHttpHeaders(std::map& result, const OrthancPluginHttpRequest* request) + { + result.clear(); + + for (uint32_t i = 0; i < request->headersCount; ++i) + { + result[request->headersKeys[i]] = request->headersValues[i]; + } + } + +#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) + static void SetPluginProperty(const std::string& pluginIdentifier, + _OrthancPluginProperty property, + const std::string& value) + { + _OrthancPluginSetPluginProperty params; + params.plugin = pluginIdentifier.c_str(); + params.property = property; + params.value = value.c_str(); + + GetGlobalContext()->InvokeService(GetGlobalContext(), _OrthancPluginService_SetPluginProperty, ¶ms); + } +#endif + + void SetRootUri(const std::string& pluginIdentifier, + const std::string& uri) + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) + OrthancPluginSetRootUri2(GetGlobalContext(), pluginIdentifier.c_str(), uri.c_str()); +#else + SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_RootUri, uri); +#endif + } + + void SetDescription(const std::string& pluginIdentifier, + const std::string& description) + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) + OrthancPluginSetDescription2(GetGlobalContext(), pluginIdentifier.c_str(), description.c_str()); +#else + SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_Description, description); +#endif + } + + void ExtendOrthancExplorer(const std::string& pluginIdentifier, + const std::string& javascript) + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) + OrthancPluginExtendOrthancExplorer2(GetGlobalContext(), pluginIdentifier.c_str(), javascript.c_str()); +#else + SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_OrthancExplorer, javascript); +#endif + } +} diff --git a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h new file mode 100644 index 0000000..3656efa --- /dev/null +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h @@ -0,0 +1,1511 @@ +/** + * 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-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 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 . + **/ + + +#pragma once + +#include "OrthancPluginException.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +/** + * The definition of ORTHANC_PLUGINS_VERSION_IS_ABOVE below is for + * backward compatibility with Orthanc SDK <= 1.3.0. + * + * $ hg diff -r Orthanc-1.3.0:Orthanc-1.3.1 ../../../Plugins/Include/orthanc/OrthancCPlugin.h + * + **/ +#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) +#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ + (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ + (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision)))) +#endif + + +#if !defined(ORTHANC_FRAMEWORK_VERSION_IS_ABOVE) +#define ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(major, minor, revision) \ + (ORTHANC_VERSION_MAJOR > major || \ + (ORTHANC_VERSION_MAJOR == major && \ + (ORTHANC_VERSION_MINOR > minor || \ + (ORTHANC_VERSION_MINOR == minor && \ + ORTHANC_VERSION_REVISION >= revision)))) +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0) +// The "OrthancPluginFindMatcher()" primitive was introduced in Orthanc 1.2.0 +# define HAS_ORTHANC_PLUGIN_FIND_MATCHER 1 +#else +# define HAS_ORTHANC_PLUGIN_FIND_MATCHER 0 +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 4, 2) +# define HAS_ORTHANC_PLUGIN_PEERS 1 +# define HAS_ORTHANC_PLUGIN_JOB 1 +#else +# define HAS_ORTHANC_PLUGIN_PEERS 0 +# define HAS_ORTHANC_PLUGIN_JOB 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0) +# define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS 1 +#else +# define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 4) +# define HAS_ORTHANC_PLUGIN_METRICS 1 +#else +# define HAS_ORTHANC_PLUGIN_METRICS 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 1, 0) +# define HAS_ORTHANC_PLUGIN_HTTP_CLIENT 1 +#else +# define HAS_ORTHANC_PLUGIN_HTTP_CLIENT 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7) +# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT 1 +#else +# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7) +# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER 1 +#else +# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 0) +# define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP 1 +#else +# define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2) +# define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API 1 +#else +# define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 10, 1) +# define HAS_ORTHANC_PLUGIN_WEBDAV 1 +#else +# define HAS_ORTHANC_PLUGIN_WEBDAV 0 +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) +# define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 1 +#else +# define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 0 +#endif + + +// Macro to tag a function as having been deprecated +#if (__cplusplus >= 201402L) // C++14 +# define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) [[deprecated]] f +#elif defined(__GNUC__) || defined(__clang__) +# define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) f __attribute__((deprecated)) +#elif defined(_MSC_VER) +# define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) __declspec(deprecated) f +#else +# define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED +#endif + + +#if !defined(__ORTHANC_FILE__) +# if defined(_MSC_VER) +# pragma message("Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries") +# else +# warning Warning: Macro __ORTHANC_FILE__ is not defined, this will leak the full path of the source files in the binaries +# endif +# define __ORTHANC_FILE__ __FILE__ +#endif + + +#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 +# define ORTHANC_PLUGINS_LOG_ERROR(msg) ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Error, __ORTHANC_FILE__, __LINE__, msg) +# define ORTHANC_PLUGINS_LOG_WARNING(msg) ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Warning, __ORTHANC_FILE__, __LINE__, msg) +# define ORTHANC_PLUGINS_LOG_INFO(msg) ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Info, __ORTHANC_FILE__, __LINE__, msg) +#else +# define ORTHANC_PLUGINS_LOG_ERROR(msg) ::OrthancPlugins::LogError(msg) +# define ORTHANC_PLUGINS_LOG_WARNING(msg) ::OrthancPlugins::LogWarning(msg) +# define ORTHANC_PLUGINS_LOG_INFO(msg) ::OrthancPlugins::LogInfo(msg) +#endif + + +namespace OrthancPlugins +{ + typedef void (*RestCallback) (OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request); + + void SetGlobalContext(OrthancPluginContext* context); + + void SetGlobalContext(OrthancPluginContext* context, + const char* pluginName); + + void ResetGlobalContext(); + + bool HasGlobalContext(); + + OrthancPluginContext* GetGlobalContext(); + + + class OrthancImage; + + + class MemoryBuffer : public boost::noncopyable + { + private: + OrthancPluginMemoryBuffer buffer_; + + void Check(OrthancPluginErrorCode code); + + bool CheckHttp(OrthancPluginErrorCode code); + + public: + MemoryBuffer(); + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + // This constructor makes a copy of the given buffer in the memory + // handled by the Orthanc core + MemoryBuffer(const void* buffer, + size_t size); +#endif + + ~MemoryBuffer() + { + Clear(); + } + + OrthancPluginMemoryBuffer* operator*() + { + return &buffer_; + } + + // This transfers ownership from "other" to "this" + void Assign(OrthancPluginMemoryBuffer& other); + + void Swap(MemoryBuffer& other); + + OrthancPluginMemoryBuffer Release(); + + const char* GetData() const + { + if (buffer_.size > 0) + { + return reinterpret_cast(buffer_.data); + } + else + { + return NULL; + } + } + + size_t GetSize() const + { + return buffer_.size; + } + + bool IsEmpty() const + { + return GetSize() == 0 || GetData() == NULL; + } + + void Clear(); + + void ToString(std::string& target) const; + + void ToJson(Json::Value& target) const; + + bool RestApiGet(const std::string& uri, + bool applyPlugins); + + bool RestApiGet(const std::string& uri, + const std::map& httpHeaders, + bool applyPlugins); + + bool RestApiPost(const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins); + + bool RestApiPut(const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins); + + bool RestApiPost(const std::string& uri, + const Json::Value& body, + bool applyPlugins); + +#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 + bool RestApiPost(const std::string& uri, + const Json::Value& body, + const std::map& httpHeaders, + bool applyPlugins); + + bool RestApiPost(const std::string& uri, + const void* body, + size_t bodySize, + const std::map& httpHeaders, + bool applyPlugins); +#endif + + bool RestApiPut(const std::string& uri, + const Json::Value& body, + bool applyPlugins); + + bool RestApiPost(const std::string& uri, + const std::string& body, + bool applyPlugins) + { + return RestApiPost(uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins); + } + + bool RestApiPut(const std::string& uri, + const std::string& body, + bool applyPlugins) + { + return RestApiPut(uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins); + } + + void CreateDicom(const Json::Value& tags, + OrthancPluginCreateDicomFlags flags); + + void CreateDicom(const Json::Value& tags, + const OrthancImage& pixelData, + OrthancPluginCreateDicomFlags flags); + + void ReadFile(const std::string& path); + + void GetDicomQuery(const OrthancPluginWorklistQuery* query); + + void DicomToJson(Json::Value& target, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength); + + bool HttpGet(const std::string& url, + const std::string& username, + const std::string& password); + + bool HttpPost(const std::string& url, + const std::string& body, + const std::string& username, + const std::string& password); + + bool HttpPut(const std::string& url, + const std::string& body, + const std::string& username, + const std::string& password); + + void GetDicomInstance(const std::string& instanceId); + }; + + + class OrthancString : public boost::noncopyable + { + private: + char* str_; + + void Clear(); + + public: + OrthancString() : + str_(NULL) + { + } + + ~OrthancString() + { + Clear(); + } + + // This transfers ownership, warning: The string must have been + // allocated by the Orthanc core + void Assign(char* str); + + const char* GetContent() const + { + return str_; + } + + bool IsNullOrEmpty() const + { + return str_ == NULL || str_[0] == 0; + } + + void ToString(std::string& target) const; + + void ToJson(Json::Value& target) const; + + void ToJsonWithoutComments(Json::Value& target) const; +}; + + + class OrthancConfiguration : public boost::noncopyable + { + private: + Json::Value configuration_; // Necessarily a Json::objectValue + std::string path_; + + std::string GetPath(const std::string& key) const; + + void LoadConfiguration(); + + public: + OrthancConfiguration(); // loads the full Orthanc configuration + + explicit OrthancConfiguration(bool load); + + explicit OrthancConfiguration(const Json::Value& configuration, const std::string& path); // e.g. to load a section from a default json content + + const Json::Value& GetJson() const + { + return configuration_; + } + + bool IsSection(const std::string& key) const; + + void GetSection(OrthancConfiguration& target, + const std::string& key) const; + + bool LookupStringValue(std::string& target, + const std::string& key) const; + + bool LookupIntegerValue(int& target, + const std::string& key) const; + + bool LookupUnsignedIntegerValue(unsigned int& target, + const std::string& key) const; + + bool LookupBooleanValue(bool& target, + const std::string& key) const; + + bool LookupFloatValue(float& target, + const std::string& key) const; + + bool LookupListOfStrings(std::list& target, + const std::string& key, + bool allowSingleString) const; + + bool LookupSetOfStrings(std::set& target, + const std::string& key, + bool allowSingleString) const; + + std::string GetStringValue(const std::string& key, + const std::string& defaultValue) const; + + int GetIntegerValue(const std::string& key, + int defaultValue) const; + + unsigned int GetUnsignedIntegerValue(const std::string& key, + unsigned int defaultValue) const; + + bool GetBooleanValue(const std::string& key, + bool defaultValue) const; + + float GetFloatValue(const std::string& key, + float defaultValue) const; + + void GetDictionary(std::map& target, + const std::string& key) const; + }; + + class OrthancImage : public boost::noncopyable + { + private: + OrthancPluginImage* image_; + + void Clear(); + + void CheckImageAvailable() const; + + public: + OrthancImage(); + + explicit OrthancImage(OrthancPluginImage* image); + + OrthancImage(OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height); + + OrthancImage(OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + void* buffer); + + ~OrthancImage() + { + Clear(); + } + + void UncompressPngImage(const void* data, + size_t size); + + void UncompressJpegImage(const void* data, + size_t size); + + void DecodeDicomImage(const void* data, + size_t size, + unsigned int frame); + + OrthancPluginPixelFormat GetPixelFormat() const; + + unsigned int GetWidth() const; + + unsigned int GetHeight() const; + + unsigned int GetPitch() const; + + void* GetBuffer() const; + + const OrthancPluginImage* GetObject() const + { + return image_; + } + + void CompressPngImage(MemoryBuffer& target) const; + + void CompressJpegImage(MemoryBuffer& target, + uint8_t quality) const; + + void AnswerPngImage(OrthancPluginRestOutput* output) const; + + void AnswerJpegImage(OrthancPluginRestOutput* output, + uint8_t quality) const; + + void* GetWriteableBuffer(); + + OrthancPluginImage* Release(); + }; + + +#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 + class FindMatcher : public boost::noncopyable + { + private: + OrthancPluginFindMatcher* matcher_; + const OrthancPluginWorklistQuery* worklist_; + + void SetupDicom(const void* query, + uint32_t size); + + public: + explicit FindMatcher(const OrthancPluginWorklistQuery* worklist); + + FindMatcher(const void* query, + uint32_t size) + { + SetupDicom(query, size); + } + + explicit FindMatcher(const MemoryBuffer& dicom) + { + SetupDicom(dicom.GetData(), dicom.GetSize()); + } + + ~FindMatcher(); + + bool IsMatch(const void* dicom, + uint32_t size) const; + + bool IsMatch(const MemoryBuffer& dicom) const + { + return IsMatch(dicom.GetData(), dicom.GetSize()); + } + }; +#endif + + + bool ReadJson(Json::Value& target, + const std::string& source); + + bool ReadJson(Json::Value& target, + const void* buffer, + size_t size); + + bool ReadJsonWithoutComments(Json::Value& target, + const std::string& source); + + bool ReadJsonWithoutComments(Json::Value& target, + const void* buffer, + size_t size); + + void WriteFastJson(std::string& target, + const Json::Value& source); + + void WriteStyledJson(std::string& target, + const Json::Value& source); + + bool RestApiGet(Json::Value& result, + const std::string& uri, + bool applyPlugins); + + bool RestApiGet(Json::Value& result, + const std::string& uri, + const std::map& httpHeaders, + bool applyPlugins); + + bool RestApiGetString(std::string& result, + const std::string& uri, + bool applyPlugins); + + bool RestApiGetString(std::string& result, + const std::string& uri, + const std::map& httpHeaders, + bool applyPlugins); + + bool RestApiPost(std::string& result, + const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins); + + bool RestApiPost(Json::Value& result, + const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins); + +#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 + bool RestApiPost(Json::Value& result, + const std::string& uri, + const Json::Value& body, + const std::map& httpHeaders, + bool applyPlugins); +#endif + + bool RestApiPost(Json::Value& result, + const std::string& uri, + const Json::Value& body, + bool applyPlugins); + + inline bool RestApiPost(Json::Value& result, + const std::string& uri, + const std::string& body, + bool applyPlugins) + { + return RestApiPost(result, uri, body.empty() ? NULL : body.c_str(), + body.size(), applyPlugins); + } + + inline bool RestApiPost(Json::Value& result, + const std::string& uri, + const MemoryBuffer& body, + bool applyPlugins) + { + return RestApiPost(result, uri, body.GetData(), + body.GetSize(), applyPlugins); + } + + bool RestApiPut(Json::Value& result, + const std::string& uri, + const void* body, + size_t bodySize, + bool applyPlugins); + + bool RestApiPut(Json::Value& result, + const std::string& uri, + const Json::Value& body, + bool applyPlugins); + + inline bool RestApiPut(Json::Value& result, + const std::string& uri, + const std::string& body, + bool applyPlugins) + { + return RestApiPut(result, uri, body.empty() ? NULL : body.c_str(), + body.size(), applyPlugins); + } + + bool RestApiDelete(const std::string& uri, + bool applyPlugins); + + bool HttpDelete(const std::string& url, + const std::string& username, + const std::string& password); + + void AnswerJson(const Json::Value& value, + OrthancPluginRestOutput* output); + + void AnswerString(const std::string& answer, + const char* mimeType, + OrthancPluginRestOutput* output); + + void AnswerHttpError(uint16_t httpError, + OrthancPluginRestOutput* output); + + void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods); + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0) + const char* AutodetectMimeType(const std::string& path); +#endif + +#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 + void LogMessage(OrthancPluginLogLevel level, + const char* file, + uint32_t line, + const std::string& message); +#endif + +#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 + // Use macro ORTHANC_PLUGINS_LOG_ERROR() instead + ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogError(const std::string& message)); +#else + void LogError(const std::string& message); +#endif + +#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 + // Use macro ORTHANC_PLUGINS_LOG_WARNING() instead + ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogWarning(const std::string& message)); +#else + void LogWarning(const std::string& message); +#endif + +#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 + // Use macro ORTHANC_PLUGINS_LOG_INFO() instead + ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogInfo(const std::string& message)); +#else + void LogInfo(const std::string& message); +#endif + + void ReportMinimalOrthancVersion(unsigned int major, + unsigned int minor, + unsigned int revision); + + bool CheckMinimalOrthancVersion(unsigned int major, + unsigned int minor, + unsigned int revision); + + bool CheckMinimalVersion(const char* version, + unsigned int major, + unsigned int minor, + unsigned int revision); + + namespace Internals + { + template + static OrthancPluginErrorCode Protect(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + try + { + Callback(output, url, request); + return OrthancPluginErrorCode_Success; + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { +#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1 + if (HasGlobalContext() && + e.HasDetails()) + { + // The "false" instructs Orthanc not to log the detailed + // error message. This is to avoid duplicating the details, + // because "OrthancException" already does it on construction. + OrthancPluginSetHttpErrorDetails + (GetGlobalContext(), output, e.GetDetails(), false); + } +#endif + + return static_cast(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + return OrthancPluginErrorCode_BadFileFormat; + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } + } + + + template + void RegisterRestCallback(const std::string& uri, + bool isThreadSafe) + { + if (isThreadSafe) + { + OrthancPluginRegisterRestCallbackNoLock + (GetGlobalContext(), uri.c_str(), Internals::Protect); + } + else + { + OrthancPluginRegisterRestCallback + (GetGlobalContext(), uri.c_str(), Internals::Protect); + } + } + + +#if HAS_ORTHANC_PLUGIN_PEERS == 1 + class OrthancPeers : public boost::noncopyable + { + private: + typedef std::map Index; + + OrthancPluginPeers *peers_; + Index index_; + uint32_t timeout_; + + size_t GetPeerIndex(const std::string& name) const; + + public: + OrthancPeers(); + + ~OrthancPeers(); + + uint32_t GetTimeout() const + { + return timeout_; + } + + void SetTimeout(uint32_t timeout) + { + timeout_ = timeout; + } + + bool LookupName(size_t& target, + const std::string& name) const; + + std::string GetPeerName(size_t index) const; + + std::string GetPeerUrl(size_t index) const; + + std::string GetPeerUrl(const std::string& name) const; + + size_t GetPeersCount() const + { + return index_.size(); + } + + bool LookupUserProperty(std::string& value, + size_t index, + const std::string& key) const; + + bool LookupUserProperty(std::string& value, + const std::string& peer, + const std::string& key) const; + + bool DoGet(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::map& headers) const; + + bool DoGet(MemoryBuffer& target, + const std::string& name, + const std::string& uri, + const std::map& headers) const; + + bool DoGet(Json::Value& target, + size_t index, + const std::string& uri, + const std::map& headers) const; + + bool DoGet(Json::Value& target, + const std::string& name, + const std::string& uri, + const std::map& headers) const; + + bool DoPost(MemoryBuffer& target, + size_t index, + const std::string& uri, + const std::string& body, + const std::map& headers) const; + + bool DoPost(MemoryBuffer& target, + const std::string& name, + const std::string& uri, + const std::string& body, + const std::map& headers) const; + + bool DoPost(Json::Value& target, + size_t index, + const std::string& uri, + const std::string& body, + const std::map& headers) const; + + bool DoPost(Json::Value& target, + const std::string& name, + const std::string& uri, + const std::string& body, + const std::map& headers) const; + + bool DoPut(size_t index, + const std::string& uri, + const std::string& body, + const std::map& headers) const; + + bool DoPut(const std::string& name, + const std::string& uri, + const std::string& body, + const std::map& headers) const; + + bool DoDelete(size_t index, + const std::string& uri, + const std::map& headers) const; + + bool DoDelete(const std::string& name, + const std::string& uri, + const std::map& headers) const; + }; +#endif + + + +#if HAS_ORTHANC_PLUGIN_JOB == 1 + class OrthancJob : public boost::noncopyable + { + private: + std::string jobType_; + std::string content_; + bool hasSerialized_; + std::string serialized_; + float progress_; + + static void CallbackFinalize(void* job); + + static float CallbackGetProgress(void* job); + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3) + static OrthancPluginErrorCode CallbackGetContent(OrthancPluginMemoryBuffer* target, + void* job); +#else + static const char* CallbackGetContent(void* job); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 11, 3) + static int32_t CallbackGetSerialized(OrthancPluginMemoryBuffer* target, + void* job); +#else + static const char* CallbackGetSerialized(void* job); +#endif + + static OrthancPluginJobStepStatus CallbackStep(void* job); + + static OrthancPluginErrorCode CallbackStop(void* job, + OrthancPluginJobStopReason reason); + + static OrthancPluginErrorCode CallbackReset(void* job); + + protected: + void ClearContent(); + + void UpdateContent(const Json::Value& content); + + void ClearSerialized(); + + void UpdateSerialized(const Json::Value& serialized); + + void UpdateProgress(float progress); + + public: + explicit OrthancJob(const std::string& jobType); + + virtual ~OrthancJob() + { + } + + virtual OrthancPluginJobStepStatus Step() = 0; + + virtual void Stop(OrthancPluginJobStopReason reason) = 0; + + virtual void Reset() = 0; + + static OrthancPluginJob* Create(OrthancJob* job /* takes ownership */); + + static std::string Submit(OrthancJob* job /* takes ownership */, + int priority); + + static void SubmitAndWait(Json::Value& result, + OrthancJob* job /* takes ownership */, + int priority); + + // Submit a job from a POST on the REST API with the same + // conventions as in the Orthanc core (according to the + // "Synchronous" and "Priority" options) + static void SubmitFromRestApiPost(OrthancPluginRestOutput* output, + const Json::Value& body, + OrthancJob* job); + }; +#endif + + +#if HAS_ORTHANC_PLUGIN_METRICS == 1 + inline void SetMetricsValue(char* name, + float value) + { + OrthancPluginSetMetricsValue(GetGlobalContext(), name, + value, OrthancPluginMetricsType_Default); + } + + class MetricsTimer : public boost::noncopyable + { + private: + std::string name_; + boost::posix_time::ptime start_; + + public: + explicit MetricsTimer(const char* name); + + ~MetricsTimer(); + }; +#endif + + +#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 + class HttpClient : public boost::noncopyable + { + public: + typedef std::map HttpHeaders; + + class IRequestBody : public boost::noncopyable + { + public: + virtual ~IRequestBody() + { + } + + virtual bool ReadNextChunk(std::string& chunk) = 0; + }; + + + class IAnswer : public boost::noncopyable + { + public: + virtual ~IAnswer() + { + } + + virtual void AddHeader(const std::string& key, + const std::string& value) = 0; + + virtual void AddChunk(const void* data, + size_t size) = 0; + }; + + + private: + class RequestBodyWrapper; + + uint16_t httpStatus_; + OrthancPluginHttpMethod method_; + std::string url_; + HttpHeaders headers_; + std::string username_; + std::string password_; + uint32_t timeout_; + std::string certificateFile_; + std::string certificateKeyFile_; + std::string certificateKeyPassword_; + bool pkcs11_; + std::string fullBody_; + IRequestBody* chunkedBody_; + bool allowChunkedTransfers_; + +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 + void ExecuteWithStream(uint16_t& httpStatus, // out + IAnswer& answer, // out + IRequestBody& body) const; +#endif + + void ExecuteWithoutStream(uint16_t& httpStatus, // out + HttpHeaders& answerHeaders, // out + std::string& answerBody, // out + const std::string& body) const; + + public: + HttpClient(); + + uint16_t GetHttpStatus() const + { + return httpStatus_; + } + + void SetMethod(OrthancPluginHttpMethod method) + { + method_ = method; + } + + const std::string& GetUrl() const + { + return url_; + } + + void SetUrl(const std::string& url) + { + url_ = url; + } + + void SetHeaders(const HttpHeaders& headers) + { + headers_ = headers; + } + + void AddHeader(const std::string& key, + const std::string& value) + { + headers_[key] = value; + } + + void AddHeaders(const HttpHeaders& headers); + + void SetCredentials(const std::string& username, + const std::string& password); + + void ClearCredentials(); + + void SetTimeout(unsigned int timeout) // 0 for default timeout + { + timeout_ = timeout; + } + + void SetCertificate(const std::string& certificateFile, + const std::string& keyFile, + const std::string& keyPassword); + + void ClearCertificate(); + + void SetPkcs11(bool pkcs11) + { + pkcs11_ = pkcs11; + } + + void ClearBody(); + + void SwapBody(std::string& body); + + void SetBody(const std::string& body); + + void SetBody(IRequestBody& body); + + // This function can be used to disable chunked transfers if the + // remote server is Orthanc with a version <= 1.5.6. + void SetChunkedTransfersAllowed(bool allow) + { + allowChunkedTransfers_ = allow; + } + + bool IsChunkedTransfersAllowed() const + { + return allowChunkedTransfers_; + } + + void Execute(IAnswer& answer); + + void Execute(HttpHeaders& answerHeaders /* out */, + std::string& answerBody /* out */); + + void Execute(HttpHeaders& answerHeaders /* out */, + Json::Value& answerBody /* out */); + + void Execute(); + }; +#endif + + + + class IChunkedRequestReader : public boost::noncopyable + { + public: + virtual ~IChunkedRequestReader() + { + } + + virtual void AddChunk(const void* data, + size_t size) = 0; + + virtual void Execute(OrthancPluginRestOutput* output) = 0; + }; + + + typedef IChunkedRequestReader* (*ChunkedRestCallback) (const char* url, + const OrthancPluginHttpRequest* request); + + + namespace Internals + { + void NullRestCallback(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request); + + IChunkedRequestReader *NullChunkedRestCallback(const char* url, + const OrthancPluginHttpRequest* request); + + +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1 + template + static OrthancPluginErrorCode ChunkedProtect(OrthancPluginServerChunkedRequestReader** reader, + const char* url, + const OrthancPluginHttpRequest* request) + { + try + { + if (reader == NULL) + { + return OrthancPluginErrorCode_InternalError; + } + else + { + *reader = reinterpret_cast(Callback(url, request)); + if (*reader == NULL) + { + return OrthancPluginErrorCode_Plugin; + } + else + { + return OrthancPluginErrorCode_Success; + } + } + } + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) + { + return static_cast(e.GetErrorCode()); + } + catch (boost::bad_lexical_cast&) + { + return OrthancPluginErrorCode_BadFileFormat; + } + catch (...) + { + return OrthancPluginErrorCode_Plugin; + } + } + + OrthancPluginErrorCode ChunkedRequestReaderAddChunk( + OrthancPluginServerChunkedRequestReader* reader, + const void* data, + uint32_t size); + + OrthancPluginErrorCode ChunkedRequestReaderExecute( + OrthancPluginServerChunkedRequestReader* reader, + OrthancPluginRestOutput* output); + + void ChunkedRequestReaderFinalize( + OrthancPluginServerChunkedRequestReader* reader); + +#else + + OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request, + RestCallback GetHandler, + ChunkedRestCallback PostHandler, + RestCallback DeleteHandler, + ChunkedRestCallback PutHandler); + + template< + RestCallback GetHandler, + ChunkedRestCallback PostHandler, + RestCallback DeleteHandler, + ChunkedRestCallback PutHandler + > + inline OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + return ChunkedRestCompatibility(output, url, request, GetHandler, + PostHandler, DeleteHandler, PutHandler); + } +#endif + } + + + + // NB: We use a templated class instead of a templated function, because + // default values are only available in functions since C++11 + template< + RestCallback GetHandler = Internals::NullRestCallback, + ChunkedRestCallback PostHandler = Internals::NullChunkedRestCallback, + RestCallback DeleteHandler = Internals::NullRestCallback, + ChunkedRestCallback PutHandler = Internals::NullChunkedRestCallback + > + class ChunkedRestRegistration : public boost::noncopyable + { + public: + static void Apply(const std::string& uri) + { +#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1 + OrthancPluginRegisterChunkedRestCallback( + GetGlobalContext(), uri.c_str(), + GetHandler == Internals::NullRestCallback ? NULL : Internals::Protect, + PostHandler == Internals::NullChunkedRestCallback ? NULL : Internals::ChunkedProtect, + DeleteHandler == Internals::NullRestCallback ? NULL : Internals::Protect, + PutHandler == Internals::NullChunkedRestCallback ? NULL : Internals::ChunkedProtect, + Internals::ChunkedRequestReaderAddChunk, + Internals::ChunkedRequestReaderExecute, + Internals::ChunkedRequestReaderFinalize); +#else + OrthancPluginRegisterRestCallbackNoLock( + GetGlobalContext(), uri.c_str(), + Internals::ChunkedRestCompatibility); +#endif + } + }; + + + +#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 + class IStorageCommitmentScpHandler : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentScpHandler() + { + } + + virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid, + const std::string& sopInstanceUid) = 0; + + static OrthancPluginErrorCode Lookup(OrthancPluginStorageCommitmentFailureReason* target, + void* rawHandler, + const char* sopClassUid, + const char* sopInstanceUid); + + static void Destructor(void* rawHandler); + }; +#endif + + + class DicomInstance : public boost::noncopyable + { + private: + bool toFree_; + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + const OrthancPluginDicomInstance* instance_; +#else + OrthancPluginDicomInstance* instance_; +#endif + + public: +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + explicit DicomInstance(const OrthancPluginDicomInstance* instance); +#else + explicit DicomInstance(OrthancPluginDicomInstance* instance); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance(const void* buffer, + size_t size); +#endif + + ~DicomInstance(); + + const OrthancPluginDicomInstance* GetObject() const + { + return instance_; + } + + std::string GetRemoteAet() const; + + const void* GetBuffer() const + { + return OrthancPluginGetInstanceData(GetGlobalContext(), instance_); + } + + size_t GetSize() const + { + return static_cast(OrthancPluginGetInstanceSize(GetGlobalContext(), instance_)); + } + + void GetJson(Json::Value& target) const; + + void GetSimplifiedJson(Json::Value& target) const; + + OrthancPluginInstanceOrigin GetOrigin() const + { + return OrthancPluginGetInstanceOrigin(GetGlobalContext(), instance_); + } + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + std::string GetTransferSyntaxUid() const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + bool HasPixelData() const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + unsigned int GetFramesCount() const + { + return OrthancPluginGetInstanceFramesCount(GetGlobalContext(), instance_); + } +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void GetRawFrame(std::string& target, + unsigned int frameIndex) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + OrthancImage* GetDecodedFrame(unsigned int frameIndex) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void Serialize(std::string& target) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + static DicomInstance* Transcode(const void* buffer, + size_t size, + const std::string& transferSyntax); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1) + static DicomInstance* Load(const std::string& instanceId, + OrthancPluginLoadDicomInstanceMode mode); +#endif + }; + +// helper method to convert Http headers from the plugin SDK to a std::map +void GetHttpHeaders(std::map& result, const OrthancPluginHttpRequest* request); + +#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + class IWebDavCollection : public boost::noncopyable + { + public: + class FileInfo + { + private: + std::string name_; + uint64_t contentSize_; + std::string mime_; + std::string dateTime_; + + public: + FileInfo(const std::string& name, + uint64_t contentSize, + const std::string& dateTime) : + name_(name), + contentSize_(contentSize), + dateTime_(dateTime) + { + } + + const std::string& GetName() const + { + return name_; + } + + uint64_t GetContentSize() const + { + return contentSize_; + } + + void SetMimeType(const std::string& mime) + { + mime_ = mime; + } + + const std::string& GetMimeType() const + { + return mime_; + } + + const std::string& GetDateTime() const + { + return dateTime_; + } + }; + + class FolderInfo + { + private: + std::string name_; + std::string dateTime_; + + public: + FolderInfo(const std::string& name, + const std::string& dateTime) : + name_(name), + dateTime_(dateTime) + { + } + + const std::string& GetName() const + { + return name_; + } + + const std::string& GetDateTime() const + { + return dateTime_; + } + }; + + virtual ~IWebDavCollection() + { + } + + virtual bool IsExistingFolder(const std::vector& path) = 0; + + virtual bool ListFolder(std::list& files, + std::list& subfolders, + const std::vector& path) = 0; + + virtual bool GetFile(std::string& content /* out */, + std::string& mime /* out */, + std::string& dateTime /* out */, + const std::vector& path) = 0; + + virtual bool StoreFile(const std::vector& path, + const void* data, + size_t size) = 0; + + virtual bool CreateFolder(const std::vector& path) = 0; + + virtual bool DeleteItem(const std::vector& path) = 0; + + static void Register(const std::string& uri, + IWebDavCollection& collection); + }; +#endif + + void SetRootUri(const std::string& pluginIdentifier, + const std::string& uri); + + void SetDescription(const std::string& pluginIdentifier, + const std::string& description); + + void ExtendOrthancExplorer(const std::string& pluginIdentifier, + const std::string& javascript); +} diff --git a/Resources/Orthanc/Plugins/OrthancPluginException.h b/Resources/Orthanc/Plugins/OrthancPluginException.h new file mode 100644 index 0000000..eac5b13 --- /dev/null +++ b/Resources/Orthanc/Plugins/OrthancPluginException.h @@ -0,0 +1,91 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 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 . + **/ + + +#pragma once + +#if !defined(HAS_ORTHANC_EXCEPTION) +# error The macro HAS_ORTHANC_EXCEPTION must be defined +#endif + + +#if HAS_ORTHANC_EXCEPTION == 1 +# include +# define ORTHANC_PLUGINS_ERROR_ENUMERATION ::Orthanc::ErrorCode +# define ORTHANC_PLUGINS_EXCEPTION_CLASS ::Orthanc::OrthancException +# define ORTHANC_PLUGINS_GET_ERROR_CODE(code) ::Orthanc::ErrorCode_ ## code +#else +# include +# define ORTHANC_PLUGINS_ERROR_ENUMERATION ::OrthancPluginErrorCode +# define ORTHANC_PLUGINS_EXCEPTION_CLASS ::OrthancPlugins::PluginException +# define ORTHANC_PLUGINS_GET_ERROR_CODE(code) ::OrthancPluginErrorCode_ ## code +#endif + + +#define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code) \ + throw ORTHANC_PLUGINS_EXCEPTION_CLASS(static_cast(code)); + + +#define ORTHANC_PLUGINS_THROW_EXCEPTION(code) \ + throw ORTHANC_PLUGINS_EXCEPTION_CLASS(ORTHANC_PLUGINS_GET_ERROR_CODE(code)); + + +#define ORTHANC_PLUGINS_CHECK_ERROR(code) \ + if (code != ORTHANC_PLUGINS_GET_ERROR_CODE(Success)) \ + { \ + ORTHANC_PLUGINS_THROW_EXCEPTION(code); \ + } + + +namespace OrthancPlugins +{ +#if HAS_ORTHANC_EXCEPTION == 0 + class PluginException + { + private: + OrthancPluginErrorCode code_; + + public: + explicit PluginException(OrthancPluginErrorCode code) : code_(code) + { + } + + OrthancPluginErrorCode GetErrorCode() const + { + return code_; + } + + const char* What(OrthancPluginContext* context) const + { + const char* description = OrthancPluginGetErrorDescription(context, code_); + if (description) + { + return description; + } + else + { + return "No description available"; + } + } + }; +#endif +} diff --git a/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake b/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake new file mode 100644 index 0000000..4f46d8c --- /dev/null +++ b/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake @@ -0,0 +1,33 @@ +# 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-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 . + + +# In Orthanc <= 1.7.1, the instructions below were part of +# "Compiler.cmake", and were protected by the (now unused) option +# "ENABLE_PLUGINS_VERSION_SCRIPT" in CMake + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${CMAKE_CURRENT_LIST_DIR}/VersionScriptPlugins.map") +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -exported_symbols_list ${CMAKE_CURRENT_LIST_DIR}/ExportedSymbolsPlugins.list") +endif() diff --git a/Resources/Orthanc/Plugins/VersionScriptPlugins.map b/Resources/Orthanc/Plugins/VersionScriptPlugins.map new file mode 100644 index 0000000..f36033b --- /dev/null +++ b/Resources/Orthanc/Plugins/VersionScriptPlugins.map @@ -0,0 +1,12 @@ +# This is a version-script for Orthanc plugins + +{ +global: + OrthancPluginInitialize; + OrthancPluginFinalize; + OrthancPluginGetName; + OrthancPluginGetVersion; + +local: + *; +}; diff --git a/Resources/Orthanc/README.txt b/Resources/Orthanc/README.txt new file mode 100644 index 0000000..0162afe --- /dev/null +++ b/Resources/Orthanc/README.txt @@ -0,0 +1,2 @@ +This folder contains an excerpt of the source code of Orthanc. It is +automatically generated using the "../SyncOrthancFolder.py" script. diff --git a/Resources/Orthanc/Sdk-1.11.2/orthanc/OrthancCPlugin.h b/Resources/Orthanc/Sdk-1.11.2/orthanc/OrthancCPlugin.h new file mode 100644 index 0000000..9424959 --- /dev/null +++ b/Resources/Orthanc/Sdk-1.11.2/orthanc/OrthancCPlugin.h @@ -0,0 +1,9043 @@ +/** + * \mainpage + * + * This C/C++ SDK allows external developers to create plugins that + * can be loaded into Orthanc to extend its functionality. Each + * Orthanc plugin must expose 4 public functions with the following + * signatures: + * + * -# int32_t OrthancPluginInitialize(const OrthancPluginContext* context): + * This function is invoked by Orthanc when it loads the plugin on startup. + * The plugin must: + * - Check its compatibility with the Orthanc version using + * ::OrthancPluginCheckVersion(). + * - Store the context pointer so that it can use the plugin + * services of Orthanc. + * - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback(). + * - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback(). + * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). + * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea2(). + * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV3(). + * - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback(). + * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). + * - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback(). + * - Possibly register a custom decoder for DICOM images using OrthancPluginRegisterDecodeImageCallback(). + * - Possibly register a callback to filter incoming HTTP requests using OrthancPluginRegisterIncomingHttpRequestFilter2(). + * - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer(). + * - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback(). + * - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback(). + * - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback(). + * - Possibly register a callback to keep/discard/modify incoming DICOM instances using OrthancPluginRegisterReceivedInstanceCallback(). + * - 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(). + * -# void OrthancPluginFinalize(): + * This function is invoked by Orthanc during its shutdown. The plugin + * must free all its memory. + * -# const char* OrthancPluginGetName(): + * The plugin must return a short string to identify itself. + * -# const char* OrthancPluginGetVersion(): + * The plugin must return a string containing its version number. + * + * The name and the version of a plugin is only used to prevent it + * from being loaded twice. Note that, in C++, it is mandatory to + * declare these functions within an extern "C" section. + * + * To ensure multi-threading safety, the various REST callbacks are + * guaranteed to be executed in mutual exclusion since Orthanc + * 0.8.5. If this feature is undesired (notably when developing + * high-performance plugins handling simultaneous requests), use + * ::OrthancPluginRegisterRestCallbackNoLock(). + **/ + + + +/** + * @defgroup Images Images and compression + * @brief Functions to deal with images and compressed buffers. + * + * @defgroup REST REST + * @brief Functions to answer REST requests in a callback. + * + * @defgroup Callbacks Callbacks + * @brief Functions to register and manage callbacks by the plugins. + * + * @defgroup DicomCallbacks DicomCallbacks + * @brief Functions to register and manage DICOM callbacks (worklists, C-FIND, C-MOVE, storage commitment). + * + * @defgroup Orthanc Orthanc + * @brief Functions to access the content of the Orthanc server. + * + * @defgroup DicomInstance DicomInstance + * @brief Functions to access DICOM images that are managed by the Orthanc core. + **/ + + + +/** + * @defgroup Toolbox Toolbox + * @brief Generic functions to help with the creation of plugins. + **/ + + + +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2022 Osimis S.A., Belgium + * Copyright (C) 2021-2022 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 . + **/ + + + +#pragma once + + +#include +#include + +#ifdef WIN32 +# define ORTHANC_PLUGINS_API __declspec(dllexport) +#elif __GNUC__ >= 4 +# define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default"))) +#else +# define ORTHANC_PLUGINS_API +#endif + +#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 +#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 11 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 2 + + +#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) +#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ + (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ + (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ + (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision)))) +#endif + + + +/******************************************************************** + ** Check that function inlining is properly supported. The use of + ** inlining is required, to avoid the duplication of object code + ** between two compilation modules that would use the Orthanc Plugin + ** API. + ********************************************************************/ + +/* If the auto-detection of the "inline" keyword below does not work + automatically and that your compiler is known to properly support + inlining, uncomment the following #define and adapt the definition + of "static inline". */ + +/* #define ORTHANC_PLUGIN_INLINE static inline */ + +#ifndef ORTHANC_PLUGIN_INLINE +# if __STDC_VERSION__ >= 199901L +/* This is C99 or above: http://predef.sourceforge.net/prestd.html */ +# define ORTHANC_PLUGIN_INLINE static inline +# elif defined(__cplusplus) +/* This is C++ */ +# define ORTHANC_PLUGIN_INLINE static inline +# elif defined(__GNUC__) +/* This is GCC running in C89 mode */ +# define ORTHANC_PLUGIN_INLINE static __inline +# elif defined(_MSC_VER) +/* This is Visual Studio running in C89 mode */ +# define ORTHANC_PLUGIN_INLINE static __inline +# else +# error Your compiler is not known to support the "inline" keyword +# endif +#endif + + + +/******************************************************************** + ** Inclusion of standard libraries. + ********************************************************************/ + +/** + * For Microsoft Visual Studio, a compatibility "stdint.h" can be + * downloaded at the following URL: + * https://hg.orthanc-server.com/orthanc/raw-file/tip/Resources/ThirdParty/VisualStudio/stdint.h + **/ +#include + +#include + + + +/******************************************************************** + ** Definition of the Orthanc Plugin API. + ********************************************************************/ + +/** @{ */ + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * The various error codes that can be returned by the Orthanc core. + **/ + typedef enum + { + OrthancPluginErrorCode_InternalError = -1 /*!< Internal error */, + OrthancPluginErrorCode_Success = 0 /*!< Success */, + OrthancPluginErrorCode_Plugin = 1 /*!< Error encountered within the plugin engine */, + OrthancPluginErrorCode_NotImplemented = 2 /*!< Not implemented yet */, + OrthancPluginErrorCode_ParameterOutOfRange = 3 /*!< Parameter out of range */, + OrthancPluginErrorCode_NotEnoughMemory = 4 /*!< The server hosting Orthanc is running out of memory */, + OrthancPluginErrorCode_BadParameterType = 5 /*!< Bad type for a parameter */, + OrthancPluginErrorCode_BadSequenceOfCalls = 6 /*!< Bad sequence of calls */, + OrthancPluginErrorCode_InexistentItem = 7 /*!< Accessing an inexistent item */, + OrthancPluginErrorCode_BadRequest = 8 /*!< Bad request */, + OrthancPluginErrorCode_NetworkProtocol = 9 /*!< Error in the network protocol */, + OrthancPluginErrorCode_SystemCommand = 10 /*!< Error while calling a system command */, + OrthancPluginErrorCode_Database = 11 /*!< Error with the database engine */, + OrthancPluginErrorCode_UriSyntax = 12 /*!< Badly formatted URI */, + OrthancPluginErrorCode_InexistentFile = 13 /*!< Inexistent file */, + OrthancPluginErrorCode_CannotWriteFile = 14 /*!< Cannot write to file */, + OrthancPluginErrorCode_BadFileFormat = 15 /*!< Bad file format */, + OrthancPluginErrorCode_Timeout = 16 /*!< Timeout */, + OrthancPluginErrorCode_UnknownResource = 17 /*!< Unknown resource */, + OrthancPluginErrorCode_IncompatibleDatabaseVersion = 18 /*!< Incompatible version of the database */, + OrthancPluginErrorCode_FullStorage = 19 /*!< The file storage is full */, + OrthancPluginErrorCode_CorruptedFile = 20 /*!< Corrupted file (e.g. inconsistent MD5 hash) */, + OrthancPluginErrorCode_InexistentTag = 21 /*!< Inexistent tag */, + OrthancPluginErrorCode_ReadOnly = 22 /*!< Cannot modify a read-only data structure */, + OrthancPluginErrorCode_IncompatibleImageFormat = 23 /*!< Incompatible format of the images */, + OrthancPluginErrorCode_IncompatibleImageSize = 24 /*!< Incompatible size of the images */, + OrthancPluginErrorCode_SharedLibrary = 25 /*!< Error while using a shared library (plugin) */, + OrthancPluginErrorCode_UnknownPluginService = 26 /*!< Plugin invoking an unknown service */, + OrthancPluginErrorCode_UnknownDicomTag = 27 /*!< Unknown DICOM tag */, + OrthancPluginErrorCode_BadJson = 28 /*!< Cannot parse a JSON document */, + OrthancPluginErrorCode_Unauthorized = 29 /*!< Bad credentials were provided to an HTTP request */, + OrthancPluginErrorCode_BadFont = 30 /*!< Badly formatted font file */, + OrthancPluginErrorCode_DatabasePlugin = 31 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */, + OrthancPluginErrorCode_StorageAreaPlugin = 32 /*!< Error in the plugin implementing a custom storage area */, + OrthancPluginErrorCode_EmptyRequest = 33 /*!< The request is empty */, + OrthancPluginErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, + OrthancPluginErrorCode_NullPointer = 35 /*!< Cannot handle a NULL pointer */, + OrthancPluginErrorCode_DatabaseUnavailable = 36 /*!< The database is currently not available (probably a transient situation) */, + OrthancPluginErrorCode_CanceledJob = 37 /*!< This job was canceled */, + OrthancPluginErrorCode_BadGeometry = 38 /*!< Geometry error encountered in Stone */, + OrthancPluginErrorCode_SslInitialization = 39 /*!< Cannot initialize SSL encryption, check out your certificates */, + OrthancPluginErrorCode_DiscontinuedAbi = 40 /*!< Calling a function that has been removed from the Orthanc Framework */, + OrthancPluginErrorCode_BadRange = 41 /*!< Incorrect range request */, + OrthancPluginErrorCode_DatabaseCannotSerialize = 42 /*!< Database could not serialize access due to concurrent update, the transaction should be retried */, + OrthancPluginErrorCode_Revision = 43 /*!< A bad revision number was provided, which might indicate conflict between multiple writers */, + OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44 /*!< A main DICOM Tag has been defined multiple times for the same resource level */, + OrthancPluginErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, + OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, + OrthancPluginErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, + OrthancPluginErrorCode_SQLiteStatementAlreadyUsed = 1003 /*!< SQLite: This cached statement is already being referred to */, + OrthancPluginErrorCode_SQLiteExecute = 1004 /*!< SQLite: Cannot execute a command */, + OrthancPluginErrorCode_SQLiteRollbackWithoutTransaction = 1005 /*!< SQLite: Rolling back a nonexistent transaction (have you called Begin()?) */, + OrthancPluginErrorCode_SQLiteCommitWithoutTransaction = 1006 /*!< SQLite: Committing a nonexistent transaction */, + OrthancPluginErrorCode_SQLiteRegisterFunction = 1007 /*!< SQLite: Unable to register a function */, + OrthancPluginErrorCode_SQLiteFlush = 1008 /*!< SQLite: Unable to flush the database */, + OrthancPluginErrorCode_SQLiteCannotRun = 1009 /*!< SQLite: Cannot run a cached statement */, + OrthancPluginErrorCode_SQLiteCannotStep = 1010 /*!< SQLite: Cannot step over a cached statement */, + OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011 /*!< SQLite: Bing a value while out of range (serious error) */, + OrthancPluginErrorCode_SQLitePrepareStatement = 1012 /*!< SQLite: Cannot prepare a cached statement */, + OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013 /*!< SQLite: Beginning the same transaction twice */, + OrthancPluginErrorCode_SQLiteTransactionCommit = 1014 /*!< SQLite: Failure when committing the transaction */, + OrthancPluginErrorCode_SQLiteTransactionBegin = 1015 /*!< SQLite: Cannot start a transaction */, + OrthancPluginErrorCode_DirectoryOverFile = 2000 /*!< The directory to be created is already occupied by a regular file */, + OrthancPluginErrorCode_FileStorageCannotWrite = 2001 /*!< Unable to create a subdirectory or a file in the file storage */, + OrthancPluginErrorCode_DirectoryExpected = 2002 /*!< The specified path does not point to a directory */, + OrthancPluginErrorCode_HttpPortInUse = 2003 /*!< The TCP port of the HTTP server is privileged or already in use */, + OrthancPluginErrorCode_DicomPortInUse = 2004 /*!< The TCP port of the DICOM server is privileged or already in use */, + OrthancPluginErrorCode_BadHttpStatusInRest = 2005 /*!< This HTTP status is not allowed in a REST API */, + OrthancPluginErrorCode_RegularFileExpected = 2006 /*!< The specified path does not point to a regular file */, + OrthancPluginErrorCode_PathToExecutable = 2007 /*!< Unable to get the path to the executable */, + OrthancPluginErrorCode_MakeDirectory = 2008 /*!< Cannot create a directory */, + OrthancPluginErrorCode_BadApplicationEntityTitle = 2009 /*!< An application entity title (AET) cannot be empty or be longer than 16 characters */, + OrthancPluginErrorCode_NoCFindHandler = 2010 /*!< No request handler factory for DICOM C-FIND SCP */, + OrthancPluginErrorCode_NoCMoveHandler = 2011 /*!< No request handler factory for DICOM C-MOVE SCP */, + OrthancPluginErrorCode_NoCStoreHandler = 2012 /*!< No request handler factory for DICOM C-STORE SCP */, + OrthancPluginErrorCode_NoApplicationEntityFilter = 2013 /*!< No application entity filter */, + OrthancPluginErrorCode_NoSopClassOrInstance = 2014 /*!< DicomUserConnection: Unable to find the SOP class and instance */, + OrthancPluginErrorCode_NoPresentationContext = 2015 /*!< DicomUserConnection: No acceptable presentation context for modality */, + OrthancPluginErrorCode_DicomFindUnavailable = 2016 /*!< DicomUserConnection: The C-FIND command is not supported by the remote SCP */, + OrthancPluginErrorCode_DicomMoveUnavailable = 2017 /*!< DicomUserConnection: The C-MOVE command is not supported by the remote SCP */, + OrthancPluginErrorCode_CannotStoreInstance = 2018 /*!< Cannot store an instance */, + OrthancPluginErrorCode_CreateDicomNotString = 2019 /*!< Only string values are supported when creating DICOM instances */, + OrthancPluginErrorCode_CreateDicomOverrideTag = 2020 /*!< Trying to override a value inherited from a parent module */, + OrthancPluginErrorCode_CreateDicomUseContent = 2021 /*!< Use \"Content\" to inject an image into a new DICOM instance */, + OrthancPluginErrorCode_CreateDicomNoPayload = 2022 /*!< No payload is present for one instance in the series */, + OrthancPluginErrorCode_CreateDicomUseDataUriScheme = 2023 /*!< The payload of the DICOM instance must be specified according to Data URI scheme */, + OrthancPluginErrorCode_CreateDicomBadParent = 2024 /*!< Trying to attach a new DICOM instance to an inexistent resource */, + OrthancPluginErrorCode_CreateDicomParentIsInstance = 2025 /*!< Trying to attach a new DICOM instance to an instance (must be a series, study or patient) */, + OrthancPluginErrorCode_CreateDicomParentEncoding = 2026 /*!< Unable to get the encoding of the parent resource */, + OrthancPluginErrorCode_UnknownModality = 2027 /*!< Unknown modality */, + OrthancPluginErrorCode_BadJobOrdering = 2028 /*!< Bad ordering of filters in a job */, + OrthancPluginErrorCode_JsonToLuaTable = 2029 /*!< Cannot convert the given JSON object to a Lua table */, + OrthancPluginErrorCode_CannotCreateLua = 2030 /*!< Cannot create the Lua context */, + OrthancPluginErrorCode_CannotExecuteLua = 2031 /*!< Cannot execute a Lua command */, + OrthancPluginErrorCode_LuaAlreadyExecuted = 2032 /*!< Arguments cannot be pushed after the Lua function is executed */, + OrthancPluginErrorCode_LuaBadOutput = 2033 /*!< The Lua function does not give the expected number of outputs */, + OrthancPluginErrorCode_NotLuaPredicate = 2034 /*!< The Lua function is not a predicate (only true/false outputs allowed) */, + OrthancPluginErrorCode_LuaReturnsNoString = 2035 /*!< The Lua function does not return a string */, + OrthancPluginErrorCode_StorageAreaAlreadyRegistered = 2036 /*!< Another plugin has already registered a custom storage area */, + OrthancPluginErrorCode_DatabaseBackendAlreadyRegistered = 2037 /*!< Another plugin has already registered a custom database back-end */, + OrthancPluginErrorCode_DatabaseNotInitialized = 2038 /*!< Plugin trying to call the database during its initialization */, + OrthancPluginErrorCode_SslDisabled = 2039 /*!< Orthanc has been built without SSL support */, + OrthancPluginErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, + OrthancPluginErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, + OrthancPluginErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, + OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, + OrthancPluginErrorCode_NoCGetHandler = 2044 /*!< No request handler factory for DICOM C-GET SCP */, + OrthancPluginErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, + + _OrthancPluginErrorCode_INTERNAL = 0x7fffffff + } OrthancPluginErrorCode; + + + /** + * Forward declaration of one of the mandatory functions for Orthanc + * plugins. + **/ + ORTHANC_PLUGINS_API const char* OrthancPluginGetName(); + + + /** + * The various HTTP methods for a REST call. + **/ + typedef enum + { + OrthancPluginHttpMethod_Get = 1, /*!< GET request */ + OrthancPluginHttpMethod_Post = 2, /*!< POST request */ + OrthancPluginHttpMethod_Put = 3, /*!< PUT request */ + OrthancPluginHttpMethod_Delete = 4, /*!< DELETE request */ + + _OrthancPluginHttpMethod_INTERNAL = 0x7fffffff + } OrthancPluginHttpMethod; + + + /** + * @brief The parameters of a REST request. + * @ingroup Callbacks + **/ + typedef struct + { + /** + * @brief The HTTP method. + **/ + OrthancPluginHttpMethod method; + + /** + * @brief The number of groups of the regular expression. + **/ + uint32_t groupsCount; + + /** + * @brief The matched values for the groups of the regular expression. + **/ + const char* const* groups; + + /** + * @brief For a GET request, the number of GET parameters. + **/ + uint32_t getCount; + + /** + * @brief For a GET request, the keys of the GET parameters. + **/ + const char* const* getKeys; + + /** + * @brief For a GET request, the values of the GET parameters. + **/ + const char* const* getValues; + + /** + * @brief For a PUT or POST request, the content of the body. + **/ + const void* body; + + /** + * @brief For a PUT or POST request, the number of bytes of the body. + **/ + uint32_t bodySize; + + + /* -------------------------------------------------- + New in version 0.8.1 + -------------------------------------------------- */ + + /** + * @brief The number of HTTP headers. + **/ + uint32_t headersCount; + + /** + * @brief The keys of the HTTP headers (always converted to low-case). + **/ + const char* const* headersKeys; + + /** + * @brief The values of the HTTP headers. + **/ + const char* const* headersValues; + + } OrthancPluginHttpRequest; + + + typedef enum + { + /* Generic services */ + _OrthancPluginService_LogInfo = 1, + _OrthancPluginService_LogWarning = 2, + _OrthancPluginService_LogError = 3, + _OrthancPluginService_GetOrthancPath = 4, + _OrthancPluginService_GetOrthancDirectory = 5, + _OrthancPluginService_GetConfigurationPath = 6, + _OrthancPluginService_SetPluginProperty = 7, + _OrthancPluginService_GetGlobalProperty = 8, + _OrthancPluginService_SetGlobalProperty = 9, + _OrthancPluginService_GetCommandLineArgumentsCount = 10, + _OrthancPluginService_GetCommandLineArgument = 11, + _OrthancPluginService_GetExpectedDatabaseVersion = 12, + _OrthancPluginService_GetConfiguration = 13, + _OrthancPluginService_BufferCompression = 14, + _OrthancPluginService_ReadFile = 15, + _OrthancPluginService_WriteFile = 16, + _OrthancPluginService_GetErrorDescription = 17, + _OrthancPluginService_CallHttpClient = 18, + _OrthancPluginService_RegisterErrorCode = 19, + _OrthancPluginService_RegisterDictionaryTag = 20, + _OrthancPluginService_DicomBufferToJson = 21, + _OrthancPluginService_DicomInstanceToJson = 22, + _OrthancPluginService_CreateDicom = 23, + _OrthancPluginService_ComputeMd5 = 24, + _OrthancPluginService_ComputeSha1 = 25, + _OrthancPluginService_LookupDictionary = 26, + _OrthancPluginService_CallHttpClient2 = 27, + _OrthancPluginService_GenerateUuid = 28, + _OrthancPluginService_RegisterPrivateDictionaryTag = 29, + _OrthancPluginService_AutodetectMimeType = 30, + _OrthancPluginService_SetMetricsValue = 31, + _OrthancPluginService_EncodeDicomWebJson = 32, + _OrthancPluginService_EncodeDicomWebXml = 33, + _OrthancPluginService_ChunkedHttpClient = 34, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_GetTagName = 35, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_EncodeDicomWebJson2 = 36, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_EncodeDicomWebXml2 = 37, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_CreateMemoryBuffer = 38, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GenerateRestApiAuthorizationToken = 39, /* New in Orthanc 1.8.1 */ + _OrthancPluginService_CreateMemoryBuffer64 = 40, /* New in Orthanc 1.9.0 */ + _OrthancPluginService_CreateDicom2 = 41, /* New in Orthanc 1.9.0 */ + _OrthancPluginService_GetDatabaseServerIdentifier = 42, /* New in Orthanc 1.11.1 */ + + /* Registration of callbacks */ + _OrthancPluginService_RegisterRestCallback = 1000, + _OrthancPluginService_RegisterOnStoredInstanceCallback = 1001, + _OrthancPluginService_RegisterStorageArea = 1002, + _OrthancPluginService_RegisterOnChangeCallback = 1003, + _OrthancPluginService_RegisterRestCallbackNoLock = 1004, + _OrthancPluginService_RegisterWorklistCallback = 1005, + _OrthancPluginService_RegisterDecodeImageCallback = 1006, + _OrthancPluginService_RegisterIncomingHttpRequestFilter = 1007, + _OrthancPluginService_RegisterFindCallback = 1008, + _OrthancPluginService_RegisterMoveCallback = 1009, + _OrthancPluginService_RegisterIncomingHttpRequestFilter2 = 1010, + _OrthancPluginService_RegisterRefreshMetricsCallback = 1011, + _OrthancPluginService_RegisterChunkedRestCallback = 1012, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, + _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014, + _OrthancPluginService_RegisterTranscoderCallback = 1015, /* New in Orthanc 1.7.0 */ + _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 */ + + /* Sending answers to REST calls */ + _OrthancPluginService_AnswerBuffer = 2000, + _OrthancPluginService_CompressAndAnswerPngImage = 2001, /* Unused as of Orthanc 0.9.4 */ + _OrthancPluginService_Redirect = 2002, + _OrthancPluginService_SendHttpStatusCode = 2003, + _OrthancPluginService_SendUnauthorized = 2004, + _OrthancPluginService_SendMethodNotAllowed = 2005, + _OrthancPluginService_SetCookie = 2006, + _OrthancPluginService_SetHttpHeader = 2007, + _OrthancPluginService_StartMultipartAnswer = 2008, + _OrthancPluginService_SendMultipartItem = 2009, + _OrthancPluginService_SendHttpStatus = 2010, + _OrthancPluginService_CompressAndAnswerImage = 2011, + _OrthancPluginService_SendMultipartItem2 = 2012, + _OrthancPluginService_SetHttpErrorDetails = 2013, + + /* Access to the Orthanc database and API */ + _OrthancPluginService_GetDicomForInstance = 3000, + _OrthancPluginService_RestApiGet = 3001, + _OrthancPluginService_RestApiPost = 3002, + _OrthancPluginService_RestApiDelete = 3003, + _OrthancPluginService_RestApiPut = 3004, + _OrthancPluginService_LookupPatient = 3005, + _OrthancPluginService_LookupStudy = 3006, + _OrthancPluginService_LookupSeries = 3007, + _OrthancPluginService_LookupInstance = 3008, + _OrthancPluginService_LookupStudyWithAccessionNumber = 3009, + _OrthancPluginService_RestApiGetAfterPlugins = 3010, + _OrthancPluginService_RestApiPostAfterPlugins = 3011, + _OrthancPluginService_RestApiDeleteAfterPlugins = 3012, + _OrthancPluginService_RestApiPutAfterPlugins = 3013, + _OrthancPluginService_ReconstructMainDicomTags = 3014, + _OrthancPluginService_RestApiGet2 = 3015, + _OrthancPluginService_CallRestApi = 3016, /* New in Orthanc 1.9.2 */ + + /* Access to DICOM instances */ + _OrthancPluginService_GetInstanceRemoteAet = 4000, + _OrthancPluginService_GetInstanceSize = 4001, + _OrthancPluginService_GetInstanceData = 4002, + _OrthancPluginService_GetInstanceJson = 4003, + _OrthancPluginService_GetInstanceSimplifiedJson = 4004, + _OrthancPluginService_HasInstanceMetadata = 4005, + _OrthancPluginService_GetInstanceMetadata = 4006, + _OrthancPluginService_GetInstanceOrigin = 4007, + _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008, + _OrthancPluginService_HasInstancePixelData = 4009, + _OrthancPluginService_CreateDicomInstance = 4010, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_FreeDicomInstance = 4011, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceFramesCount = 4012, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceRawFrame = 4013, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDecodedFrame = 4014, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_TranscodeDicomInstance = 4015, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_SerializeDicomInstance = 4016, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceAdvancedJson = 4017, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDicomWebJson = 4018, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDicomWebXml = 4019, /* New in Orthanc 1.7.0 */ + + /* Services for plugins implementing a database back-end */ + _OrthancPluginService_RegisterDatabaseBackend = 5000, /* New in Orthanc 0.8.6 */ + _OrthancPluginService_DatabaseAnswer = 5001, + _OrthancPluginService_RegisterDatabaseBackendV2 = 5002, /* New in Orthanc 0.9.4 */ + _OrthancPluginService_StorageAreaCreate = 5003, + _OrthancPluginService_StorageAreaRead = 5004, + _OrthancPluginService_StorageAreaRemove = 5005, + _OrthancPluginService_RegisterDatabaseBackendV3 = 5006, /* New in Orthanc 1.9.2 */ + + /* Primitives for handling images */ + _OrthancPluginService_GetImagePixelFormat = 6000, + _OrthancPluginService_GetImageWidth = 6001, + _OrthancPluginService_GetImageHeight = 6002, + _OrthancPluginService_GetImagePitch = 6003, + _OrthancPluginService_GetImageBuffer = 6004, + _OrthancPluginService_UncompressImage = 6005, + _OrthancPluginService_FreeImage = 6006, + _OrthancPluginService_CompressImage = 6007, + _OrthancPluginService_ConvertPixelFormat = 6008, + _OrthancPluginService_GetFontsCount = 6009, + _OrthancPluginService_GetFontInfo = 6010, + _OrthancPluginService_DrawText = 6011, + _OrthancPluginService_CreateImage = 6012, + _OrthancPluginService_CreateImageAccessor = 6013, + _OrthancPluginService_DecodeDicomImage = 6014, + + /* Primitives for handling C-Find, C-Move and worklists */ + _OrthancPluginService_WorklistAddAnswer = 7000, + _OrthancPluginService_WorklistMarkIncomplete = 7001, + _OrthancPluginService_WorklistIsMatch = 7002, + _OrthancPluginService_WorklistGetDicomQuery = 7003, + _OrthancPluginService_FindAddAnswer = 7004, + _OrthancPluginService_FindMarkIncomplete = 7005, + _OrthancPluginService_GetFindQuerySize = 7006, + _OrthancPluginService_GetFindQueryTag = 7007, + _OrthancPluginService_GetFindQueryTagName = 7008, + _OrthancPluginService_GetFindQueryValue = 7009, + _OrthancPluginService_CreateFindMatcher = 7010, + _OrthancPluginService_FreeFindMatcher = 7011, + _OrthancPluginService_FindMatcherIsMatch = 7012, + + /* Primitives for accessing Orthanc Peers (new in 1.4.2) */ + _OrthancPluginService_GetPeers = 8000, + _OrthancPluginService_FreePeers = 8001, + _OrthancPluginService_GetPeersCount = 8003, + _OrthancPluginService_GetPeerName = 8004, + _OrthancPluginService_GetPeerUrl = 8005, + _OrthancPluginService_CallPeerApi = 8006, + _OrthancPluginService_GetPeerUserProperty = 8007, + + /* Primitives for handling jobs (new in 1.4.2) */ + _OrthancPluginService_CreateJob = 9000, + _OrthancPluginService_FreeJob = 9001, + _OrthancPluginService_SubmitJob = 9002, + _OrthancPluginService_RegisterJobsUnserializer = 9003, + + _OrthancPluginService_INTERNAL = 0x7fffffff + } _OrthancPluginService; + + + typedef enum + { + _OrthancPluginProperty_Description = 1, + _OrthancPluginProperty_RootUri = 2, + _OrthancPluginProperty_OrthancExplorer = 3, + + _OrthancPluginProperty_INTERNAL = 0x7fffffff + } _OrthancPluginProperty; + + + + /** + * The memory layout of the pixels of an image. + * @ingroup Images + **/ + typedef enum + { + /** + * @brief Graylevel 8bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * one byte. + **/ + OrthancPluginPixelFormat_Grayscale8 = 1, + + /** + * @brief Graylevel, unsigned 16bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * two bytes. + **/ + OrthancPluginPixelFormat_Grayscale16 = 2, + + /** + * @brief Graylevel, signed 16bpp image. + * + * The image is graylevel. Each pixel is signed and stored in two + * bytes. + **/ + OrthancPluginPixelFormat_SignedGrayscale16 = 3, + + /** + * @brief Color image in RGB24 format. + * + * This format describes a color image. The pixels are stored in 3 + * consecutive bytes. The memory layout is RGB. + **/ + OrthancPluginPixelFormat_RGB24 = 4, + + /** + * @brief Color image in RGBA32 format. + * + * This format describes a color image. The pixels are stored in 4 + * consecutive bytes. The memory layout is RGBA. + **/ + OrthancPluginPixelFormat_RGBA32 = 5, + + OrthancPluginPixelFormat_Unknown = 6, /*!< Unknown pixel format */ + + /** + * @brief Color image in RGB48 format. + * + * This format describes a color image. The pixels are stored in 6 + * consecutive bytes. The memory layout is RRGGBB. + **/ + OrthancPluginPixelFormat_RGB48 = 7, + + /** + * @brief Graylevel, unsigned 32bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * four bytes. + **/ + OrthancPluginPixelFormat_Grayscale32 = 8, + + /** + * @brief Graylevel, floating-point 32bpp image. + * + * The image is graylevel. Each pixel is floating-point and stored + * in four bytes. + **/ + OrthancPluginPixelFormat_Float32 = 9, + + /** + * @brief Color image in BGRA32 format. + * + * This format describes a color image. The pixels are stored in 4 + * consecutive bytes. The memory layout is BGRA. + **/ + OrthancPluginPixelFormat_BGRA32 = 10, + + /** + * @brief Graylevel, unsigned 64bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * eight bytes. + **/ + OrthancPluginPixelFormat_Grayscale64 = 11, + + _OrthancPluginPixelFormat_INTERNAL = 0x7fffffff + } OrthancPluginPixelFormat; + + + + /** + * The content types that are supported by Orthanc plugins. + **/ + typedef enum + { + OrthancPluginContentType_Unknown = 0, /*!< Unknown content type */ + OrthancPluginContentType_Dicom = 1, /*!< DICOM */ + OrthancPluginContentType_DicomAsJson = 2, /*!< JSON summary of a DICOM file */ + OrthancPluginContentType_DicomUntilPixelData = 3, /*!< DICOM Header till pixel data */ + + _OrthancPluginContentType_INTERNAL = 0x7fffffff + } OrthancPluginContentType; + + + + /** + * The supported types of DICOM resources. + **/ + typedef enum + { + OrthancPluginResourceType_Patient = 0, /*!< Patient */ + OrthancPluginResourceType_Study = 1, /*!< Study */ + OrthancPluginResourceType_Series = 2, /*!< Series */ + OrthancPluginResourceType_Instance = 3, /*!< Instance */ + OrthancPluginResourceType_None = 4, /*!< Unavailable resource type */ + + _OrthancPluginResourceType_INTERNAL = 0x7fffffff + } OrthancPluginResourceType; + + + + /** + * The supported types of changes that can be signaled to the change callback. + * @ingroup Callbacks + **/ + typedef enum + { + OrthancPluginChangeType_CompletedSeries = 0, /*!< Series is now complete */ + OrthancPluginChangeType_Deleted = 1, /*!< Deleted resource */ + OrthancPluginChangeType_NewChildInstance = 2, /*!< A new instance was added to this resource */ + OrthancPluginChangeType_NewInstance = 3, /*!< New instance received */ + OrthancPluginChangeType_NewPatient = 4, /*!< New patient created */ + OrthancPluginChangeType_NewSeries = 5, /*!< New series created */ + OrthancPluginChangeType_NewStudy = 6, /*!< New study created */ + OrthancPluginChangeType_StablePatient = 7, /*!< Timeout: No new instance in this patient */ + OrthancPluginChangeType_StableSeries = 8, /*!< Timeout: No new instance in this series */ + OrthancPluginChangeType_StableStudy = 9, /*!< Timeout: No new instance in this study */ + OrthancPluginChangeType_OrthancStarted = 10, /*!< Orthanc has started */ + OrthancPluginChangeType_OrthancStopped = 11, /*!< Orthanc is stopping */ + OrthancPluginChangeType_UpdatedAttachment = 12, /*!< Some user-defined attachment has changed for this resource */ + OrthancPluginChangeType_UpdatedMetadata = 13, /*!< Some user-defined metadata has changed for this resource */ + OrthancPluginChangeType_UpdatedPeers = 14, /*!< The list of Orthanc peers has changed */ + OrthancPluginChangeType_UpdatedModalities = 15, /*!< The list of DICOM modalities has changed */ + OrthancPluginChangeType_JobSubmitted = 16, /*!< New Job submitted */ + OrthancPluginChangeType_JobSuccess = 17, /*!< A Job has completed successfully */ + OrthancPluginChangeType_JobFailure = 18, /*!< A Job has failed */ + + _OrthancPluginChangeType_INTERNAL = 0x7fffffff + } OrthancPluginChangeType; + + + /** + * The compression algorithms that are supported by the Orthanc core. + * @ingroup Images + **/ + typedef enum + { + OrthancPluginCompressionType_Zlib = 0, /*!< Standard zlib compression */ + OrthancPluginCompressionType_ZlibWithSize = 1, /*!< zlib, prefixed with uncompressed size (uint64_t) */ + OrthancPluginCompressionType_Gzip = 2, /*!< Standard gzip compression */ + OrthancPluginCompressionType_GzipWithSize = 3, /*!< gzip, prefixed with uncompressed size (uint64_t) */ + + _OrthancPluginCompressionType_INTERNAL = 0x7fffffff + } OrthancPluginCompressionType; + + + /** + * The image formats that are supported by the Orthanc core. + * @ingroup Images + **/ + typedef enum + { + OrthancPluginImageFormat_Png = 0, /*!< Image compressed using PNG */ + OrthancPluginImageFormat_Jpeg = 1, /*!< Image compressed using JPEG */ + OrthancPluginImageFormat_Dicom = 2, /*!< Image compressed using DICOM */ + + _OrthancPluginImageFormat_INTERNAL = 0x7fffffff + } OrthancPluginImageFormat; + + + /** + * The value representations present in the DICOM standard (version 2013). + * @ingroup Toolbox + **/ + typedef enum + { + OrthancPluginValueRepresentation_AE = 1, /*!< Application Entity */ + OrthancPluginValueRepresentation_AS = 2, /*!< Age String */ + OrthancPluginValueRepresentation_AT = 3, /*!< Attribute Tag */ + OrthancPluginValueRepresentation_CS = 4, /*!< Code String */ + OrthancPluginValueRepresentation_DA = 5, /*!< Date */ + OrthancPluginValueRepresentation_DS = 6, /*!< Decimal String */ + OrthancPluginValueRepresentation_DT = 7, /*!< Date Time */ + OrthancPluginValueRepresentation_FD = 8, /*!< Floating Point Double */ + OrthancPluginValueRepresentation_FL = 9, /*!< Floating Point Single */ + OrthancPluginValueRepresentation_IS = 10, /*!< Integer String */ + OrthancPluginValueRepresentation_LO = 11, /*!< Long String */ + OrthancPluginValueRepresentation_LT = 12, /*!< Long Text */ + OrthancPluginValueRepresentation_OB = 13, /*!< Other Byte String */ + OrthancPluginValueRepresentation_OF = 14, /*!< Other Float String */ + OrthancPluginValueRepresentation_OW = 15, /*!< Other Word String */ + OrthancPluginValueRepresentation_PN = 16, /*!< Person Name */ + OrthancPluginValueRepresentation_SH = 17, /*!< Short String */ + OrthancPluginValueRepresentation_SL = 18, /*!< Signed Long */ + OrthancPluginValueRepresentation_SQ = 19, /*!< Sequence of Items */ + OrthancPluginValueRepresentation_SS = 20, /*!< Signed Short */ + OrthancPluginValueRepresentation_ST = 21, /*!< Short Text */ + OrthancPluginValueRepresentation_TM = 22, /*!< Time */ + OrthancPluginValueRepresentation_UI = 23, /*!< Unique Identifier (UID) */ + OrthancPluginValueRepresentation_UL = 24, /*!< Unsigned Long */ + OrthancPluginValueRepresentation_UN = 25, /*!< Unknown */ + OrthancPluginValueRepresentation_US = 26, /*!< Unsigned Short */ + OrthancPluginValueRepresentation_UT = 27, /*!< Unlimited Text */ + + _OrthancPluginValueRepresentation_INTERNAL = 0x7fffffff + } OrthancPluginValueRepresentation; + + + /** + * The possible output formats for a DICOM-to-JSON conversion. + * @ingroup Toolbox + * @see OrthancPluginDicomToJson() + **/ + typedef enum + { + OrthancPluginDicomToJsonFormat_Full = 1, /*!< Full output, with most details */ + OrthancPluginDicomToJsonFormat_Short = 2, /*!< Tags output as hexadecimal numbers */ + OrthancPluginDicomToJsonFormat_Human = 3, /*!< Human-readable JSON */ + + _OrthancPluginDicomToJsonFormat_INTERNAL = 0x7fffffff + } OrthancPluginDicomToJsonFormat; + + + /** + * Flags to customize a DICOM-to-JSON conversion. By default, binary + * tags are formatted using Data URI scheme. + * @ingroup Toolbox + **/ + typedef enum + { + OrthancPluginDicomToJsonFlags_None = 0, + OrthancPluginDicomToJsonFlags_IncludeBinary = (1 << 0), /*!< Include the binary tags */ + OrthancPluginDicomToJsonFlags_IncludePrivateTags = (1 << 1), /*!< Include the private tags */ + OrthancPluginDicomToJsonFlags_IncludeUnknownTags = (1 << 2), /*!< Include the tags unknown by the dictionary */ + OrthancPluginDicomToJsonFlags_IncludePixelData = (1 << 3), /*!< Include the pixel data */ + OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii = (1 << 4), /*!< Output binary tags as-is, dropping non-ASCII */ + OrthancPluginDicomToJsonFlags_ConvertBinaryToNull = (1 << 5), /*!< Signal binary tags as null values */ + OrthancPluginDicomToJsonFlags_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_INTERNAL = 0x7fffffff + } OrthancPluginDicomToJsonFlags; + + + /** + * Flags to the creation of a DICOM file. + * @ingroup Toolbox + * @see OrthancPluginCreateDicom() + **/ + typedef enum + { + OrthancPluginCreateDicomFlags_None = 0, + OrthancPluginCreateDicomFlags_DecodeDataUriScheme = (1 << 0), /*!< Decode fields encoded using data URI scheme */ + OrthancPluginCreateDicomFlags_GenerateIdentifiers = (1 << 1), /*!< Automatically generate DICOM identifiers */ + + _OrthancPluginCreateDicomFlags_INTERNAL = 0x7fffffff + } OrthancPluginCreateDicomFlags; + + + /** + * The constraints on the DICOM identifiers that must be supported + * by the database plugins. + * @deprecated Plugins using OrthancPluginConstraintType will be faster + **/ + typedef enum + { + OrthancPluginIdentifierConstraint_Equal = 1, /*!< Equal */ + OrthancPluginIdentifierConstraint_SmallerOrEqual = 2, /*!< Less or equal */ + OrthancPluginIdentifierConstraint_GreaterOrEqual = 3, /*!< More or equal */ + OrthancPluginIdentifierConstraint_Wildcard = 4, /*!< Case-sensitive wildcard matching (with * and ?) */ + + _OrthancPluginIdentifierConstraint_INTERNAL = 0x7fffffff + } OrthancPluginIdentifierConstraint; + + + /** + * The constraints on the tags (main DICOM tags and identifier tags) + * that must be supported by the database plugins. + **/ + typedef enum + { + OrthancPluginConstraintType_Equal = 1, /*!< Equal */ + OrthancPluginConstraintType_SmallerOrEqual = 2, /*!< Less or equal */ + OrthancPluginConstraintType_GreaterOrEqual = 3, /*!< More or equal */ + OrthancPluginConstraintType_Wildcard = 4, /*!< Wildcard matching */ + OrthancPluginConstraintType_List = 5, /*!< List of values */ + + _OrthancPluginConstraintType_INTERNAL = 0x7fffffff + } OrthancPluginConstraintType; + + + /** + * The origin of a DICOM instance that has been received by Orthanc. + **/ + typedef enum + { + OrthancPluginInstanceOrigin_Unknown = 1, /*!< Unknown origin */ + OrthancPluginInstanceOrigin_DicomProtocol = 2, /*!< Instance received through DICOM protocol */ + OrthancPluginInstanceOrigin_RestApi = 3, /*!< Instance received through REST API of Orthanc */ + OrthancPluginInstanceOrigin_Plugin = 4, /*!< Instance added to Orthanc by a plugin */ + OrthancPluginInstanceOrigin_Lua = 5, /*!< Instance added to Orthanc by a Lua script */ + OrthancPluginInstanceOrigin_WebDav = 6, /*!< Instance received through WebDAV (new in 1.8.0) */ + + _OrthancPluginInstanceOrigin_INTERNAL = 0x7fffffff + } OrthancPluginInstanceOrigin; + + + /** + * The possible status for one single step of a job. + **/ + typedef enum + { + OrthancPluginJobStepStatus_Success = 1, /*!< The job has successfully executed all its steps */ + OrthancPluginJobStepStatus_Failure = 2, /*!< The job has failed while executing this step */ + OrthancPluginJobStepStatus_Continue = 3 /*!< The job has still data to process after this step */ + } OrthancPluginJobStepStatus; + + + /** + * Explains why the job should stop and release the resources it has + * allocated. This is especially important to disambiguate between + * the "paused" condition and the "final" conditions (success, + * failure, or canceled). + **/ + typedef enum + { + OrthancPluginJobStopReason_Success = 1, /*!< The job has succeeded */ + OrthancPluginJobStopReason_Paused = 2, /*!< The job was paused, and will be resumed later */ + OrthancPluginJobStopReason_Failure = 3, /*!< The job has failed, and might be resubmitted later */ + OrthancPluginJobStopReason_Canceled = 4 /*!< The job was canceled, and might be resubmitted later */ + } OrthancPluginJobStopReason; + + + /** + * The available types of metrics. + **/ + typedef enum + { + OrthancPluginMetricsType_Default = 0, /*!< Default metrics */ + + /** + * This metrics represents a time duration. Orthanc will keep the + * maximum value of the metrics over a sliding window of ten + * seconds, which is useful if the metrics is sampled frequently. + **/ + OrthancPluginMetricsType_Timer = 1 + } OrthancPluginMetricsType; + + + /** + * The available modes to export a binary DICOM tag into a DICOMweb + * JSON or XML document. + **/ + typedef enum + { + OrthancPluginDicomWebBinaryMode_Ignore = 0, /*!< Don't include binary tags */ + OrthancPluginDicomWebBinaryMode_InlineBinary = 1, /*!< Inline encoding using Base64 */ + OrthancPluginDicomWebBinaryMode_BulkDataUri = 2 /*!< Use a bulk data URI field */ + } OrthancPluginDicomWebBinaryMode; + + + /** + * The available values for the Failure Reason (0008,1197) during + * storage commitment. + * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 + **/ + typedef enum + { + OrthancPluginStorageCommitmentFailureReason_Success = 0, + /*!< Success: The DICOM instance is properly stored in the SCP */ + + OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1, + /*!< 0110H: A general failure in processing the operation was encountered */ + + OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2, + /*!< 0112H: One or more of the elements in the Referenced SOP + Instance Sequence was not available */ + + OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3, + /*!< 0213H: The SCP does not currently have enough resources to + store the requested SOP Instance(s) */ + + OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4, + /*!< 0122H: Storage Commitment has been requested for a SOP + Instance with a SOP Class that is not supported by the SCP */ + + OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5, + /*!< 0119H: The SOP Class of an element in the Referenced SOP + Instance Sequence did not correspond to the SOP class registered + for this SOP Instance at the SCP */ + + OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6 + /*!< 0131H: The Transaction UID of the Storage Commitment Request + is already in use */ + } OrthancPluginStorageCommitmentFailureReason; + + + /** + * The action to be taken after ReceivedInstanceCallback is triggered + **/ + typedef enum + { + OrthancPluginReceivedInstanceAction_KeepAsIs = 1, /*!< Keep the instance as is */ + OrthancPluginReceivedInstanceAction_Modify = 2, /*!< Modify the instance */ + OrthancPluginReceivedInstanceAction_Discard = 3, /*!< Discard the instance */ + + _OrthancPluginReceivedInstanceAction_INTERNAL = 0x7fffffff + } OrthancPluginReceivedInstanceAction; + + + /** + * @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 + * content of the buffer is not useful anymore, it must be free by a + * call to ::OrthancPluginFreeMemoryBuffer(). + **/ + typedef struct + { + /** + * @brief The content of the buffer. + **/ + void* data; + + /** + * @brief The number of bytes in the buffer. + **/ + uint32_t size; + } OrthancPluginMemoryBuffer; + + + + /** + * @brief A 64-bit memory buffer allocated by the core system of Orthanc. + * + * A memory buffer allocated by the core system of Orthanc. When the + * content of the buffer is not useful anymore, it must be free by a + * call to ::OrthancPluginFreeMemoryBuffer64(). + **/ + typedef struct + { + /** + * @brief The content of the buffer. + **/ + void* data; + + /** + * @brief The number of bytes in the buffer. + **/ + uint64_t size; + } OrthancPluginMemoryBuffer64; + + + + + /** + * @brief Opaque structure that represents the HTTP connection to the client application. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput; + + + + /** + * @brief Opaque structure that represents a DICOM instance that is managed by the Orthanc core. + * @ingroup DicomInstance + **/ + typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance; + + + + /** + * @brief Opaque structure that represents an image that is uncompressed in memory. + * @ingroup Images + **/ + typedef struct _OrthancPluginImage_t OrthancPluginImage; + + + + /** + * @brief Opaque structure that represents the storage area that is actually used by Orthanc. + * @ingroup Images + **/ + typedef struct _OrthancPluginStorageArea_t OrthancPluginStorageArea; + + + + /** + * @brief Opaque structure to an object that represents a C-Find query for worklists. + * @ingroup DicomCallbacks + **/ + typedef struct _OrthancPluginWorklistQuery_t OrthancPluginWorklistQuery; + + + + /** + * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists. + * @ingroup DicomCallbacks + **/ + typedef struct _OrthancPluginWorklistAnswers_t OrthancPluginWorklistAnswers; + + + + /** + * @brief Opaque structure to an object that represents a C-Find query. + * @ingroup DicomCallbacks + **/ + typedef struct _OrthancPluginFindQuery_t OrthancPluginFindQuery; + + + + /** + * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists. + * @ingroup DicomCallbacks + **/ + typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindAnswers; + + + + /** + * @brief Opaque structure to an object that can be used to check whether a DICOM instance matches a C-Find query. + * @ingroup Toolbox + **/ + typedef struct _OrthancPluginFindMatcher_t OrthancPluginFindMatcher; + + + + /** + * @brief Opaque structure to the set of remote Orthanc Peers that are known to the local Orthanc server. + * @ingroup Toolbox + **/ + typedef struct _OrthancPluginPeers_t OrthancPluginPeers; + + + + /** + * @brief Opaque structure to a job to be executed by Orthanc. + * @ingroup Toolbox + **/ + typedef struct _OrthancPluginJob_t OrthancPluginJob; + + + + /** + * @brief Opaque structure that represents a node in a JSON or XML + * document used in DICOMweb. + * @ingroup Toolbox + **/ + typedef struct _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode; + + + + /** + * @brief Signature of a callback function that answers to a REST request. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginRestCallback) ( + OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request); + + + + /** + * @brief Signature of a callback function that is triggered when Orthanc stores a new DICOM instance. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginOnStoredInstanceCallback) ( + const OrthancPluginDicomInstance* instance, + const char* instanceId); + + + + /** + * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginOnChangeCallback) ( + OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId); + + + + /** + * @brief Signature of a callback function to decode a DICOM instance as an image. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginDecodeImageCallback) ( + OrthancPluginImage** target, + const void* dicom, + const uint32_t size, + uint32_t frameIndex); + + + + /** + * @brief Signature of a function to free dynamic memory. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginFree) (void* buffer); + + + + /** + * @brief Signature of a function to set the content of a node + * encoding a binary DICOM tag, into a JSON or XML document + * generated for DICOMweb. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginDicomWebSetBinaryNode) ( + OrthancPluginDicomWebNode* node, + OrthancPluginDicomWebBinaryMode mode, + const char* bulkDataUri); + + + + /** + * @brief Callback for writing to the storage area. + * + * Signature of a callback function that is triggered when Orthanc writes a file to the storage area. + * + * @param uuid The UUID of the file. + * @param content The content of the file. + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate) ( + const char* uuid, + const void* content, + int64_t size, + OrthancPluginContentType type); + + + + /** + * @brief Callback for reading from the storage area. + * + * Signature of a callback function that is triggered when Orthanc reads a file from the storage area. + * + * @param content The content of the file (output). + * @param size The size of the file (output). + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + * @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 + * your C standard library will automatically be invoked on the + * "content" pointer. + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRead) ( + void** content, + int64_t* size, + const char* uuid, + OrthancPluginContentType type); + + + + /** + * @brief Callback for reading a whole file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a whole file from the storage area. + * + * @param target Memory buffer where to store the content of the file. It must be allocated by the + * plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it. + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadWhole) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type); + + + + /** + * @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. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart); + + + + /** + * @brief Callback for removing a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area. + * + * @param uuid The UUID of the file to be removed. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove) ( + const char* uuid, + OrthancPluginContentType type); + + + + /** + * @brief Callback to handle the C-Find SCP requests for worklists. + * + * Signature of a callback function that is triggered when Orthanc + * receives a C-Find SCP request against modality worklists. + * + * @param answers The target structure where answers must be stored. + * @param query The worklist query. + * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates. + * @param calledAet The Application Entity Title (AET) of the modality that is called by the request. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWorklistCallback) ( + OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* issuerAet, + const char* calledAet); + + + + /** + * @brief Callback to filter incoming HTTP requests received by Orthanc. + * + * Signature of a callback function that is triggered whenever + * Orthanc receives an HTTP/REST request, and that answers whether + * this request should be allowed. If the callback returns "0" + * ("false"), the server answers with HTTP status code 403 + * (Forbidden). + * + * Pay attention to the fact that this function may be invoked + * concurrently by different threads of the Web server of + * Orthanc. You must implement proper locking if applicable. + * + * @param method The HTTP method used by the request. + * @param uri The URI of interest. + * @param ip The IP address of the HTTP client. + * @param headersCount The number of HTTP headers. + * @param headersKeys The keys of the HTTP headers (always converted to low-case). + * @param headersValues The values of the HTTP headers. + * @return 0 if forbidden access, 1 if allowed access, -1 if error. + * @ingroup Callbacks + * @deprecated Please instead use OrthancPluginIncomingHttpRequestFilter2() + **/ + typedef int32_t (*OrthancPluginIncomingHttpRequestFilter) ( + OrthancPluginHttpMethod method, + const char* uri, + const char* ip, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues); + + + + /** + * @brief Callback to filter incoming HTTP requests received by Orthanc. + * + * Signature of a callback function that is triggered whenever + * Orthanc receives an HTTP/REST request, and that answers whether + * this request should be allowed. If the callback returns "0" + * ("false"), the server answers with HTTP status code 403 + * (Forbidden). + * + * Pay attention to the fact that this function may be invoked + * concurrently by different threads of the Web server of + * Orthanc. You must implement proper locking if applicable. + * + * @param method The HTTP method used by the request. + * @param uri The URI of interest. + * @param ip The IP address of the HTTP client. + * @param headersCount The number of HTTP headers. + * @param headersKeys The keys of the HTTP headers (always converted to low-case). + * @param headersValues The values of the HTTP headers. + * @param getArgumentsCount The number of GET arguments (only for the GET HTTP method). + * @param getArgumentsKeys The keys of the GET arguments (only for the GET HTTP method). + * @param getArgumentsValues The values of the GET arguments (only for the GET HTTP method). + * @return 0 if forbidden access, 1 if allowed access, -1 if error. + * @ingroup Callbacks + **/ + typedef int32_t (*OrthancPluginIncomingHttpRequestFilter2) ( + OrthancPluginHttpMethod method, + const char* uri, + const char* ip, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + uint32_t getArgumentsCount, + const char* const* getArgumentsKeys, + const char* const* getArgumentsValues); + + + + /** + * @brief Callback to handle incoming C-Find SCP requests. + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a C-Find SCP request not concerning modality + * worklists. + * + * @param answers The target structure where answers must be stored. + * @param query The worklist query. + * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates. + * @param calledAet The Application Entity Title (AET) of the modality that is called by the request. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginFindCallback) ( + OrthancPluginFindAnswers* answers, + const OrthancPluginFindQuery* query, + const char* issuerAet, + const char* calledAet); + + + + /** + * @brief Callback to handle incoming C-Move SCP requests. + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a C-Move SCP request. The callback receives the + * type of the resource of interest (study, series, instance...) + * together with the DICOM tags containing its identifiers. In turn, + * the plugin must create a driver object that will be responsible + * for driving the successive move suboperations. + * + * @param resourceType The type of the resource of interest. Note + * that this might be set to ResourceType_None if the + * QueryRetrieveLevel (0008,0052) tag was not provided by the + * issuer (i.e. the originator modality). + * @param patientId Content of the PatientID (0x0010, 0x0020) tag of the resource of interest. Might be NULL. + * @param accessionNumber Content of the AccessionNumber (0x0008, 0x0050) tag. Might be NULL. + * @param studyInstanceUid Content of the StudyInstanceUID (0x0020, 0x000d) tag. Might be NULL. + * @param seriesInstanceUid Content of the SeriesInstanceUID (0x0020, 0x000e) tag. Might be NULL. + * @param sopInstanceUid Content of the SOPInstanceUID (0x0008, 0x0018) tag. Might be NULL. + * @param originatorAet The Application Entity Title (AET) of the + * modality from which the request originates. + * @param sourceAet The Application Entity Title (AET) of the + * modality that should send its DICOM files to another modality. + * @param targetAet The Application Entity Title (AET) of the + * modality that should receive the DICOM files. + * @param originatorId The Message ID issued by the originator modality, + * as found in tag (0000,0110) of the DICOM query emitted by the issuer. + * + * @return The NULL value if the plugin cannot deal with this query, + * or a pointer to the driver object that is responsible for + * handling the successive move suboperations. + * + * @note If targetAet equals sourceAet, this is actually a query/retrieve operation. + * @ingroup DicomCallbacks + **/ + typedef void* (*OrthancPluginMoveCallback) ( + OrthancPluginResourceType resourceType, + const char* patientId, + const char* accessionNumber, + const char* studyInstanceUid, + const char* seriesInstanceUid, + const char* sopInstanceUid, + const char* originatorAet, + const char* sourceAet, + const char* targetAet, + uint16_t originatorId); + + + /** + * @brief Callback to read the size of a C-Move driver. + * + * Signature of a callback function that returns the number of + * C-Move suboperations that are to be achieved by the given C-Move + * driver. This driver is the return value of a previous call to the + * OrthancPluginMoveCallback() callback. + * + * @param moveDriver The C-Move driver of interest. + * @return The number of suboperations. + * @ingroup DicomCallbacks + **/ + typedef uint32_t (*OrthancPluginGetMoveSize) (void* moveDriver); + + + /** + * @brief Callback to apply one C-Move suboperation. + * + * Signature of a callback function that applies the next C-Move + * suboperation that os to be achieved by the given C-Move + * driver. This driver is the return value of a previous call to the + * OrthancPluginMoveCallback() callback. + * + * @param moveDriver The C-Move driver of interest. + * @return 0 if success, or the error code if failure. + * @ingroup DicomCallbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginApplyMove) (void* moveDriver); + + + /** + * @brief Callback to free one C-Move driver. + * + * Signature of a callback function that releases the resources + * allocated by the given C-Move driver. This driver is the return + * value of a previous call to the OrthancPluginMoveCallback() + * callback. + * + * @param moveDriver The C-Move driver of interest. + * @ingroup DicomCallbacks + **/ + typedef void (*OrthancPluginFreeMove) (void* moveDriver); + + + /** + * @brief Callback to finalize one custom job. + * + * Signature of a callback function that releases all the resources + * allocated by the given job. This job is the argument provided to + * OrthancPluginCreateJob(). + * + * @param job The job of interest. + * @ingroup Toolbox + **/ + typedef void (*OrthancPluginJobFinalize) (void* job); + + + /** + * @brief Callback to check the progress of one custom job. + * + * Signature of a callback function that returns the progress of the + * job. + * + * @param job The job of interest. + * @return The progress, as a floating-point number ranging from 0 to 1. + * @ingroup Toolbox + **/ + typedef float (*OrthancPluginJobGetProgress) (void* job); + + + /** + * @brief Callback to retrieve the content of one custom job. + * + * Signature of a callback function that returns human-readable + * statistics about the job. This statistics must be formatted as a + * JSON object. This information is notably displayed in the "Jobs" + * tab of "Orthanc Explorer". + * + * @param job The job of interest. + * @return The statistics, as a JSON object encoded as a string. + * @ingroup Toolbox + **/ + typedef const char* (*OrthancPluginJobGetContent) (void* job); + + + /** + * @brief Callback to serialize one custom job. + * + * Signature of a callback function that returns a serialized + * version of the job, formatted as a JSON object. This + * serialization is stored in the Orthanc database, and is used to + * reload the job on the restart of Orthanc. The "unserialization" + * callback (with OrthancPluginJobsUnserializer signature) will + * receive this serialized object. + * + * @param job The job of interest. + * @return The serialized job, as a JSON object encoded as a string. + * @see OrthancPluginRegisterJobsUnserializer() + * @ingroup Toolbox + **/ + typedef const char* (*OrthancPluginJobGetSerialized) (void* job); + + + /** + * @brief Callback to execute one step of a custom job. + * + * Signature of a callback function that executes one step in the + * job. The jobs engine of Orthanc will make successive calls to + * this method, as long as it returns + * OrthancPluginJobStepStatus_Continue. + * + * @param job The job of interest. + * @return The status of execution. + * @ingroup Toolbox + **/ + typedef OrthancPluginJobStepStatus (*OrthancPluginJobStep) (void* job); + + + /** + * @brief Callback executed once one custom job leaves the "running" state. + * + * Signature of a callback function that is invoked once a job + * leaves the "running" state. This can happen if the previous call + * to OrthancPluginJobStep has failed/succeeded, if the host Orthanc + * server is being stopped, or if the user manually tags the job as + * paused/canceled. This callback allows the plugin to free + * resources allocated for running this custom job (e.g. to stop + * threads, or to remove temporary files). + * + * Note that handling pauses might involves a specific treatment + * (such a stopping threads, but keeping temporary files on the + * disk). This "paused" situation can be checked by looking at the + * "reason" parameter. + * + * @param job The job of interest. + * @param reason The reason for leaving the "running" state. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginJobStop) (void* job, + OrthancPluginJobStopReason reason); + + + /** + * @brief Callback executed once one stopped custom job is started again. + * + * Signature of a callback function that is invoked once a job + * leaves the "failure/canceled" state, to be started again. This + * function will typically reset the progress to zero. Note that + * before being actually executed, the job would first be tagged as + * "pending" in the Orthanc jobs engine. + * + * @param job The job of interest. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginJobReset) (void* job); + + + /** + * @brief Callback executed to unserialize a custom job. + * + * Signature of a callback function that unserializes a job that was + * saved in the Orthanc database. + * + * @param jobType The type of the job, as provided to OrthancPluginCreateJob(). + * @param serialized The serialization of the job, as provided by OrthancPluginJobGetSerialized. + * @return The unserialized job (as created by OrthancPluginCreateJob()), or NULL + * if this unserializer cannot handle this job type. + * @see OrthancPluginRegisterJobsUnserializer() + * @ingroup Callbacks + **/ + typedef OrthancPluginJob* (*OrthancPluginJobsUnserializer) (const char* jobType, + const char* serialized); + + + + /** + * @brief Callback executed to update the metrics of the plugin. + * + * Signature of a callback function that is called by Orthanc + * whenever a monitoring tool (such as Prometheus) asks the current + * values of the metrics. This callback gives the plugin a chance to + * update its metrics, by calling OrthancPluginSetMetricsValue(). + * This is typically useful for metrics that are expensive to + * acquire. + * + * @see OrthancPluginRegisterRefreshMetrics() + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginRefreshMetricsCallback) (); + + + + /** + * @brief Callback executed to encode a binary tag in DICOMweb. + * + * Signature of a callback function that is called by Orthanc + * whenever a DICOM tag that contains a binary value must be written + * to a JSON or XML node, while a DICOMweb document is being + * generated. The value representation (VR) of the DICOM tag can be + * OB, OD, OF, OL, OW, or UN. + * + * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml() + * @param node The node being generated, as provided by Orthanc. + * @param setter The setter to be used to encode the content of the node. If + * the setter is not called, the binary tag is not written to the output document. + * @param levelDepth The depth of the node in the DICOM hierarchy of sequences. + * This parameter gives the number of elements in the "levelTagGroup", + * "levelTagElement", and "levelIndex" arrays. + * @param levelTagGroup The group of the parent DICOM tags in the hierarchy. + * @param levelTagElement The element of the parent DICOM tags in the hierarchy. + * @param levelIndex The index of the node in the parent sequences of the hierarchy. + * @param tagGroup The group of the DICOM tag of interest. + * @param tagElement The element of the DICOM tag of interest. + * @param vr The value representation of the binary DICOM node. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginDicomWebBinaryCallback) ( + OrthancPluginDicomWebNode* node, + OrthancPluginDicomWebSetBinaryNode setter, + uint32_t levelDepth, + const uint16_t* levelTagGroup, + const uint16_t* levelTagElement, + const uint32_t* levelIndex, + uint16_t tagGroup, + uint16_t tagElement, + OrthancPluginValueRepresentation vr); + + + + /** + * @brief Callback executed to encode a binary tag in DICOMweb. + * + * Signature of a callback function that is called by Orthanc + * whenever a DICOM tag that contains a binary value must be written + * to a JSON or XML node, while a DICOMweb document is being + * generated. The value representation (VR) of the DICOM tag can be + * OB, OD, OF, OL, OW, or UN. + * + * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml() + * @param node The node being generated, as provided by Orthanc. + * @param setter The setter to be used to encode the content of the node. If + * the setter is not called, the binary tag is not written to the output document. + * @param levelDepth The depth of the node in the DICOM hierarchy of sequences. + * This parameter gives the number of elements in the "levelTagGroup", + * "levelTagElement", and "levelIndex" arrays. + * @param levelTagGroup The group of the parent DICOM tags in the hierarchy. + * @param levelTagElement The element of the parent DICOM tags in the hierarchy. + * @param levelIndex The index of the node in the parent sequences of the hierarchy. + * @param tagGroup The group of the DICOM tag of interest. + * @param tagElement The element of the DICOM tag of interest. + * @param vr The value representation of the binary DICOM node. + * @param payload The user payload. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginDicomWebBinaryCallback2) ( + OrthancPluginDicomWebNode* node, + OrthancPluginDicomWebSetBinaryNode setter, + uint32_t levelDepth, + const uint16_t* levelTagGroup, + const uint16_t* levelTagElement, + const uint32_t* levelIndex, + uint16_t tagGroup, + uint16_t tagElement, + OrthancPluginValueRepresentation vr, + void* payload); + + + + /** + * @brief Data structure that contains information about the Orthanc core. + **/ + typedef struct _OrthancPluginContext_t + { + void* pluginsManager; + const char* orthancVersion; + OrthancPluginFree Free; + OrthancPluginErrorCode (*InvokeService) (struct _OrthancPluginContext_t* context, + _OrthancPluginService service, + const void* params); + } OrthancPluginContext; + + + + /** + * @brief An entry in the dictionary of DICOM tags. + **/ + typedef struct + { + uint16_t group; /*!< The group of the tag */ + uint16_t element; /*!< The element of the tag */ + OrthancPluginValueRepresentation vr; /*!< The value representation of the tag */ + uint32_t minMultiplicity; /*!< The minimum multiplicity of the tag */ + uint32_t maxMultiplicity; /*!< The maximum multiplicity of the tag (0 means arbitrary) */ + } OrthancPluginDictionaryEntry; + + + + /** + * @brief Free a string. + * + * Free a string that was allocated by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param str The string to be freed. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeString( + OrthancPluginContext* context, + char* str) + { + if (str != NULL) + { + context->Free(str); + } + } + + + /** + * @brief Check that the version of the hosting Orthanc is above a given version. + * + * This function checks whether the version of the Orthanc server + * running this plugin, is above the given version. Contrarily to + * OrthancPluginCheckVersion(), it is up to the developer of the + * plugin to make sure that all the Orthanc SDK services called by + * the plugin are actually implemented in the given version of + * Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param expectedMajor Expected major version. + * @param expectedMinor Expected minor version. + * @param expectedRevision Expected revision. + * @return 1 if and only if the versions are compatible. If the + * result is 0, the initialization of the plugin should fail. + * @see OrthancPluginCheckVersion + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginCheckVersionAdvanced( + OrthancPluginContext* context, + int expectedMajor, + int expectedMinor, + int expectedRevision) + { + int major, minor, revision; + + if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) || + sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) || + sizeof(int32_t) != sizeof(_OrthancPluginService) || + sizeof(int32_t) != sizeof(_OrthancPluginProperty) || + sizeof(int32_t) != sizeof(OrthancPluginPixelFormat) || + sizeof(int32_t) != sizeof(OrthancPluginContentType) || + sizeof(int32_t) != sizeof(OrthancPluginResourceType) || + sizeof(int32_t) != sizeof(OrthancPluginChangeType) || + sizeof(int32_t) != sizeof(OrthancPluginCompressionType) || + sizeof(int32_t) != sizeof(OrthancPluginImageFormat) || + sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) || + sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) || + sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) || + sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) || + sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || + sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) || + sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) || + sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || + sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || + sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || + sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || + sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction)) + { + /* Mismatch in the size of the enumerations */ + return 0; + } + + /* Assume compatibility with the mainline */ + if (!strcmp(context->orthancVersion, "mainline")) + { + return 1; + } + + /* Parse the version of the Orthanc core */ + if ( +#ifdef _MSC_VER + sscanf_s +#else + sscanf +#endif + (context->orthancVersion, "%4d.%4d.%4d", &major, &minor, &revision) != 3) + { + return 0; + } + + /* Check the major number of the version */ + + if (major > expectedMajor) + { + return 1; + } + + if (major < expectedMajor) + { + return 0; + } + + /* Check the minor number of the version */ + + if (minor > expectedMinor) + { + return 1; + } + + if (minor < expectedMinor) + { + return 0; + } + + /* Check the revision number of the version */ + + if (revision >= expectedRevision) + { + return 1; + } + else + { + return 0; + } + } + + + /** + * @brief Check the compatibility of the plugin wrt. the version of its hosting Orthanc. + * + * This function checks whether the version of the Orthanc server + * running this plugin, is above the version of the current Orthanc + * SDK header. This guarantees that the plugin is compatible with + * the hosting Orthanc (i.e. it will not call unavailable services). + * The result of this function should always be checked in the + * OrthancPluginInitialize() entry point of the plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return 1 if and only if the versions are compatible. If the + * result is 0, the initialization of the plugin should fail. + * @see OrthancPluginCheckVersionAdvanced + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginCheckVersion( + OrthancPluginContext* context) + { + return OrthancPluginCheckVersionAdvanced( + context, + ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + } + + + /** + * @brief Free a memory buffer. + * + * Free a memory buffer that was allocated by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer to release. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeMemoryBuffer( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* buffer) + { + context->Free(buffer->data); + } + + + /** + * @brief Free a memory buffer. + * + * Free a memory buffer that was allocated by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer to release. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeMemoryBuffer64( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer64* buffer) + { + context->Free(buffer->data); + } + + + /** + * @brief Log an error. + * + * Log an error message using the Orthanc logging system. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param message The message to be logged. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginLogError( + OrthancPluginContext* context, + const char* message) + { + context->InvokeService(context, _OrthancPluginService_LogError, message); + } + + + /** + * @brief Log a warning. + * + * Log a warning message using the Orthanc logging system. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param message The message to be logged. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginLogWarning( + OrthancPluginContext* context, + const char* message) + { + context->InvokeService(context, _OrthancPluginService_LogWarning, message); + } + + + /** + * @brief Log an information. + * + * Log an information message using the Orthanc logging system. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param message The message to be logged. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginLogInfo( + OrthancPluginContext* context, + const char* message) + { + context->InvokeService(context, _OrthancPluginService_LogInfo, message); + } + + + + typedef struct + { + const char* pathRegularExpression; + OrthancPluginRestCallback callback; + } _OrthancPluginRestCallback; + + /** + * @brief Register a REST callback. + * + * This function registers a REST callback against a regular + * expression for a URI. This function must be called during the + * initialization of the plugin, i.e. inside the + * OrthancPluginInitialize() public function. + * + * Each REST callback is guaranteed to run in mutual exclusion. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param pathRegularExpression Regular expression for the URI. May contain groups. + * @param callback The callback function to handle the REST call. + * @see OrthancPluginRegisterRestCallbackNoLock() + * + * @note + * The regular expression is case sensitive and must follow the + * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html). + * + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback( + OrthancPluginContext* context, + const char* pathRegularExpression, + OrthancPluginRestCallback callback) + { + _OrthancPluginRestCallback params; + params.pathRegularExpression = pathRegularExpression; + params.callback = callback; + context->InvokeService(context, _OrthancPluginService_RegisterRestCallback, ¶ms); + } + + + + /** + * @brief Register a REST callback, without locking. + * + * This function registers a REST callback against a regular + * expression for a URI. This function must be called during the + * initialization of the plugin, i.e. inside the + * OrthancPluginInitialize() public function. + * + * Contrarily to OrthancPluginRegisterRestCallback(), the callback + * will NOT be invoked in mutual exclusion. This can be useful for + * high-performance plugins that must handle concurrent requests + * (Orthanc uses a pool of threads, one thread being assigned to + * each incoming HTTP request). Of course, if using this function, + * it is up to the plugin to implement the required locking + * mechanisms. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param pathRegularExpression Regular expression for the URI. May contain groups. + * @param callback The callback function to handle the REST call. + * @see OrthancPluginRegisterRestCallback() + * + * @note + * The regular expression is case sensitive and must follow the + * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html). + * + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallbackNoLock( + OrthancPluginContext* context, + const char* pathRegularExpression, + OrthancPluginRestCallback callback) + { + _OrthancPluginRestCallback params; + params.pathRegularExpression = pathRegularExpression; + params.callback = callback; + context->InvokeService(context, _OrthancPluginService_RegisterRestCallbackNoLock, ¶ms); + } + + + + typedef struct + { + OrthancPluginOnStoredInstanceCallback callback; + } _OrthancPluginOnStoredInstanceCallback; + + /** + * @brief Register a callback for received instances. + * + * This function registers a callback function that is called + * whenever a new DICOM instance is stored into the Orthanc core. + * + * @warning Your callback function will be called synchronously with + * the core of Orthanc. This implies that deadlocks might emerge if + * you call other core primitives of Orthanc in your callback (such + * deadlocks are particularly visible in the presence of other plugins + * or Lua scripts). It is thus strongly advised to avoid any call to + * the REST API of Orthanc in the callback. If you have to call + * other primitives of Orthanc, you should make these calls in a + * separate thread, passing the pending events to be processed + * through a message queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback function. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnStoredInstanceCallback( + OrthancPluginContext* context, + OrthancPluginOnStoredInstanceCallback callback) + { + _OrthancPluginOnStoredInstanceCallback params; + params.callback = callback; + + context->InvokeService(context, _OrthancPluginService_RegisterOnStoredInstanceCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + const void* answer; + uint32_t answerSize; + const char* mimeType; + } _OrthancPluginAnswerBuffer; + + /** + * @brief Answer to a REST request. + * + * This function answers to a REST request with the content of a memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param answer Pointer to the memory buffer containing the answer. + * @param answerSize Number of bytes of the answer. + * @param mimeType The MIME type of the answer. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginAnswerBuffer( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const void* answer, + uint32_t answerSize, + const char* mimeType) + { + _OrthancPluginAnswerBuffer params; + params.output = output; + params.answer = answer; + params.answerSize = answerSize; + params.mimeType = mimeType; + context->InvokeService(context, _OrthancPluginService_AnswerBuffer, ¶ms); + } + + + typedef struct + { + OrthancPluginRestOutput* output; + OrthancPluginPixelFormat format; + uint32_t width; + uint32_t height; + uint32_t pitch; + const void* buffer; + } _OrthancPluginCompressAndAnswerPngImage; + + typedef struct + { + OrthancPluginRestOutput* output; + OrthancPluginImageFormat imageFormat; + OrthancPluginPixelFormat pixelFormat; + uint32_t width; + uint32_t height; + uint32_t pitch; + const void* buffer; + uint8_t quality; + } _OrthancPluginCompressAndAnswerImage; + + + /** + * @brief Answer to a REST request with a PNG image. + * + * This function answers to a REST request with a PNG image. The + * parameters of this function describe a memory buffer that + * contains an uncompressed image. The image will be automatically compressed + * as a PNG image by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param format The memory layout of the uncompressed image. + * @param width The width of the image. + * @param height The height of the image. + * @param pitch The pitch of the image (i.e. the number of bytes + * between 2 successive lines of the image in the memory buffer). + * @param buffer The memory buffer containing the uncompressed image. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerPngImage( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + const void* buffer) + { + _OrthancPluginCompressAndAnswerImage params; + params.output = output; + params.imageFormat = OrthancPluginImageFormat_Png; + params.pixelFormat = format; + params.width = width; + params.height = height; + params.pitch = pitch; + params.buffer = buffer; + params.quality = 0; /* No quality for PNG */ + context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* instanceId; + } _OrthancPluginGetDicomForInstance; + + /** + * @brief Retrieve a DICOM instance using its Orthanc identifier. + * + * Retrieve a DICOM instance using its Orthanc identifier. The DICOM + * file is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param instanceId The Orthanc identifier of the DICOM instance of interest. + * @return 0 if success, or the error code if failure. + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetDicomForInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* instanceId) + { + _OrthancPluginGetDicomForInstance params; + params.target = target; + params.instanceId = instanceId; + return context->InvokeService(context, _OrthancPluginService_GetDicomForInstance, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* uri; + } _OrthancPluginRestApiGet; + + /** + * @brief Make a GET call to the built-in Orthanc REST API. + * + * Make a GET call to the built-in Orthanc REST API. The result to + * the query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param uri The URI in the built-in Orthanc API. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiGetAfterPlugins + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGet( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri) + { + _OrthancPluginRestApiGet params; + params.target = target; + params.uri = uri; + return context->InvokeService(context, _OrthancPluginService_RestApiGet, ¶ms); + } + + + + /** + * @brief Make a GET call to the REST API, as tainted by the plugins. + * + * Make a GET call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. The result to the + * query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param uri The URI in the built-in Orthanc API. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiGet + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGetAfterPlugins( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri) + { + _OrthancPluginRestApiGet params; + params.target = target; + params.uri = uri; + return context->InvokeService(context, _OrthancPluginService_RestApiGetAfterPlugins, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* uri; + const void* body; + uint32_t bodySize; + } _OrthancPluginRestApiPostPut; + + /** + * @brief Make a POST call to the built-in Orthanc REST API. + * + * Make a POST call to the built-in Orthanc REST API. The result to + * the query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the POST request. + * @param bodySize The size of the body. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiPostAfterPlugins + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPost( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const void* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPost, ¶ms); + } + + + /** + * @brief Make a POST call to the REST API, as tainted by the plugins. + * + * Make a POST call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. The result to the + * query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the POST request. + * @param bodySize The size of the body. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiPost + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPostAfterPlugins( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const void* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPostAfterPlugins, ¶ms); + } + + + + /** + * @brief Make a DELETE call to the built-in Orthanc REST API. + * + * Make a DELETE call to the built-in Orthanc REST API. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param uri The URI to delete in the built-in Orthanc API. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiDeleteAfterPlugins + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiDelete( + OrthancPluginContext* context, + const char* uri) + { + return context->InvokeService(context, _OrthancPluginService_RestApiDelete, uri); + } + + + /** + * @brief Make a DELETE call to the REST API, as tainted by the plugins. + * + * Make a DELETE call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param uri The URI to delete in the built-in Orthanc API. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiDelete + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiDeleteAfterPlugins( + OrthancPluginContext* context, + const char* uri) + { + return context->InvokeService(context, _OrthancPluginService_RestApiDeleteAfterPlugins, uri); + } + + + + /** + * @brief Make a PUT call to the built-in Orthanc REST API. + * + * Make a PUT call to the built-in Orthanc REST API. The result to + * the query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the PUT request. + * @param bodySize The size of the body. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiPutAfterPlugins + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPut( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const void* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPut, ¶ms); + } + + + + /** + * @brief Make a PUT call to the REST API, as tainted by the plugins. + * + * Make a PUT call to the Orthanc REST API, after all the plugins + * are applied. In other words, if some plugin overrides or adds the + * called URI to the built-in Orthanc REST API, this call will + * return the result provided by this plugin. The result to the + * query is stored into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param uri The URI in the built-in Orthanc API. + * @param body The body of the PUT request. + * @param bodySize The size of the body. + * @return 0 if success, or the error code if failure. + * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource. + * @see OrthancPluginRestApiPut + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiPutAfterPlugins( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + const void* body, + uint32_t bodySize) + { + _OrthancPluginRestApiPostPut params; + params.target = target; + params.uri = uri; + params.body = body; + params.bodySize = bodySize; + return context->InvokeService(context, _OrthancPluginService_RestApiPutAfterPlugins, ¶ms); + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* argument; + } _OrthancPluginOutputPlusArgument; + + /** + * @brief Redirect a REST request. + * + * This function answers to a REST request by redirecting the user + * to another URI using HTTP status 301. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param redirection Where to redirect. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRedirect( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* redirection) + { + _OrthancPluginOutputPlusArgument params; + params.output = output; + params.argument = redirection; + context->InvokeService(context, _OrthancPluginService_Redirect, ¶ms); + } + + + + typedef struct + { + char** result; + const char* argument; + } _OrthancPluginRetrieveDynamicString; + + /** + * @brief Look for a patient. + * + * Look for a patient stored in Orthanc, using its Patient ID tag (0x0010, 0x0020). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored patients). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param patientID The Patient ID of interest. + * @return The NULL value if the patient is non-existent, or a string containing the + * Orthanc ID of the patient. This string must be freed by OrthancPluginFreeString(). + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupPatient( + OrthancPluginContext* context, + const char* patientID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = patientID; + + if (context->InvokeService(context, _OrthancPluginService_LookupPatient, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for a study. + * + * Look for a study stored in Orthanc, using its Study Instance UID tag (0x0020, 0x000d). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored studies). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param studyUID The Study Instance UID of interest. + * @return The NULL value if the study is non-existent, or a string containing the + * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString(). + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudy( + OrthancPluginContext* context, + const char* studyUID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = studyUID; + + if (context->InvokeService(context, _OrthancPluginService_LookupStudy, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for a study, using the accession number. + * + * Look for a study stored in Orthanc, using its Accession Number tag (0x0008, 0x0050). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored studies). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param accessionNumber The Accession Number of interest. + * @return The NULL value if the study is non-existent, or a string containing the + * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString(). + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudyWithAccessionNumber( + OrthancPluginContext* context, + const char* accessionNumber) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = accessionNumber; + + if (context->InvokeService(context, _OrthancPluginService_LookupStudyWithAccessionNumber, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for a series. + * + * Look for a series stored in Orthanc, using its Series Instance UID tag (0x0020, 0x000e). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored series). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param seriesUID The Series Instance UID of interest. + * @return The NULL value if the series is non-existent, or a string containing the + * Orthanc ID of the series. This string must be freed by OrthancPluginFreeString(). + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupSeries( + OrthancPluginContext* context, + const char* seriesUID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = seriesUID; + + if (context->InvokeService(context, _OrthancPluginService_LookupSeries, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Look for an instance. + * + * Look for an instance stored in Orthanc, using its SOP Instance UID tag (0x0008, 0x0018). + * This function uses the database index to run as fast as possible (it does not loop + * over all the stored instances). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param sopInstanceUID The SOP Instance UID of interest. + * @return The NULL value if the instance is non-existent, or a string containing the + * Orthanc ID of the instance. This string must be freed by OrthancPluginFreeString(). + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupInstance( + OrthancPluginContext* context, + const char* sopInstanceUID) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = sopInstanceUID; + + if (context->InvokeService(context, _OrthancPluginService_LookupInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + uint16_t status; + } _OrthancPluginSendHttpStatusCode; + + /** + * @brief Send a HTTP status code. + * + * This function answers to a REST request by sending a HTTP status + * code (such as "400 - Bad Request"). Note that: + * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer(). + * - Redirections (status 301) must use ::OrthancPluginRedirect(). + * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized(). + * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param status The HTTP status code to be sent. + * @ingroup REST + * @see OrthancPluginSendHttpStatus() + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatusCode( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + uint16_t status) + { + _OrthancPluginSendHttpStatusCode params; + params.output = output; + params.status = status; + context->InvokeService(context, _OrthancPluginService_SendHttpStatusCode, ¶ms); + } + + + /** + * @brief Signal that a REST request is not authorized. + * + * This function answers to a REST request by signaling that it is + * not authorized. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param realm The realm for the authorization process. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSendUnauthorized( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* realm) + { + _OrthancPluginOutputPlusArgument params; + params.output = output; + params.argument = realm; + context->InvokeService(context, _OrthancPluginService_SendUnauthorized, ¶ms); + } + + + /** + * @brief Signal that this URI does not support this HTTP method. + * + * This function answers to a REST request by signaling that the + * queried URI does not support this method. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param allowedMethods The allowed methods for this URI (e.g. "GET,POST" after a PUT or a POST request). + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSendMethodNotAllowed( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* allowedMethods) + { + _OrthancPluginOutputPlusArgument params; + params.output = output; + params.argument = allowedMethods; + context->InvokeService(context, _OrthancPluginService_SendMethodNotAllowed, ¶ms); + } + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* key; + const char* value; + } _OrthancPluginSetHttpHeader; + + /** + * @brief Set a cookie. + * + * This function sets a cookie in the HTTP client. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param cookie The cookie to be set. + * @param value The value of the cookie. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetCookie( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* cookie, + const char* value) + { + _OrthancPluginSetHttpHeader params; + params.output = output; + params.key = cookie; + params.value = value; + context->InvokeService(context, _OrthancPluginService_SetCookie, ¶ms); + } + + + /** + * @brief Set some HTTP header. + * + * This function sets a HTTP header in the HTTP answer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param key The HTTP header to be set. + * @param value The value of the HTTP header. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpHeader( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* key, + const char* value) + { + _OrthancPluginSetHttpHeader params; + params.output = output; + params.key = key; + params.value = value; + context->InvokeService(context, _OrthancPluginService_SetHttpHeader, ¶ms); + } + + + typedef struct + { + char** resultStringToFree; + const char** resultString; + int64_t* resultInt64; + const char* key; + const OrthancPluginDicomInstance* instance; + OrthancPluginInstanceOrigin* resultOrigin; /* New in Orthanc 0.9.5 SDK */ + } _OrthancPluginAccessDicomInstance; + + + /** + * @brief Get the AET of a DICOM instance. + * + * This function returns the Application Entity Title (AET) of the + * DICOM modality from which a DICOM instance originates. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The AET if success, NULL if error. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + const char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultString = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceRemoteAet, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the size of a DICOM file. + * + * This function returns the number of bytes of the given DICOM instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The size of the file, -1 in case of error. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + int64_t size; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultInt64 = &size; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceSize, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return -1; + } + else + { + return size; + } + } + + + /** + * @brief Get the data of a DICOM file. + * + * This function returns a pointer to the content of the given DICOM instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The pointer to the DICOM data, NULL in case of error. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + const char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultString = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceData, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the DICOM tag hierarchy as a JSON file. + * + * This function returns a pointer to a newly created string + * containing a JSON file. This JSON file encodes the tag hierarchy + * of the given DICOM instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The NULL value in case of error, or a string containing the JSON file. + * This string must be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultStringToFree = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the DICOM tag hierarchy as a JSON file (with simplification). + * + * This function returns a pointer to a newly created string + * containing a JSON file. This JSON file encodes the tag hierarchy + * of the given DICOM instance. In contrast with + * ::OrthancPluginGetInstanceJson(), the returned JSON file is in + * its simplified version. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The NULL value in case of error, or a string containing the JSON file. + * This string must be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultStringToFree = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceSimplifiedJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Check whether a DICOM instance is associated with some metadata. + * + * This function checks whether the DICOM instance of interest is + * associated with some metadata. As of Orthanc 0.8.1, in the + * callbacks registered by + * ::OrthancPluginRegisterOnStoredInstanceCallback(), the only + * possibly available metadata are "ReceptionDate", "RemoteAET" and + * "IndexInSeries". + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @param metadata The metadata of interest. + * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE int OrthancPluginHasInstanceMetadata( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + const char* metadata) + { + int64_t result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultInt64 = &result; + params.instance = instance; + params.key = metadata; + + if (context->InvokeService(context, _OrthancPluginService_HasInstanceMetadata, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return -1; + } + else + { + return (result != 0); + } + } + + + /** + * @brief Get the value of some metadata associated with a given DICOM instance. + * + * This functions returns the value of some metadata that is associated with the DICOM instance of interest. + * Before calling this function, the existence of the metadata must have been checked with + * ::OrthancPluginHasInstanceMetadata(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @param metadata The metadata of interest. + * @return The metadata value if success, NULL if error. Please note that the + * returned string belongs to the instance object and must NOT be + * deallocated. Please make a copy of the string if you wish to access + * it later. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + const char* metadata) + { + const char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultString = &result; + params.instance = instance; + params.key = metadata; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceMetadata, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginStorageCreate create; + OrthancPluginStorageRead read; + OrthancPluginStorageRemove remove; + OrthancPluginFree free; + } _OrthancPluginRegisterStorageArea; + + /** + * @brief Register a custom storage area. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param read The callback function to read a file from the custom storage area. + * @param remove The callback function to remove a file from the custom storage area. + * @ingroup Callbacks + * @deprecated Please use OrthancPluginRegisterStorageArea2() + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea( + OrthancPluginContext* context, + OrthancPluginStorageCreate create, + OrthancPluginStorageRead read, + OrthancPluginStorageRemove remove) + { + _OrthancPluginRegisterStorageArea params; + params.create = create; + params.read = read; + params.remove = remove; + +#ifdef __cplusplus + params.free = ::free; +#else + params.free = free; +#endif + + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea, ¶ms); + } + + + + /** + * @brief Return the path to the Orthanc executable. + * + * This function returns the path to the Orthanc executable. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancPath(OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetOrthancPath, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Return the directory containing the Orthanc. + * + * This function returns the path to the directory containing the Orthanc executable. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancDirectory(OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetOrthancDirectory, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Return the path to the configuration file(s). + * + * This function returns the path to the configuration file(s) that + * was specified when starting Orthanc. Since version 0.9.1, this + * path can refer to a folder that stores a set of configuration + * files. This function is deprecated in favor of + * OrthancPluginGetConfiguration(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + * @see OrthancPluginGetConfiguration() + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetConfigurationPath, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginOnChangeCallback callback; + } _OrthancPluginOnChangeCallback; + + /** + * @brief Register a callback to monitor changes. + * + * This function registers a callback function that is called + * whenever a change happens to some DICOM resource. + * + * @warning Your callback function will be called synchronously with + * the core of Orthanc. This implies that deadlocks might emerge if + * you call other core primitives of Orthanc in your callback (such + * deadlocks are particularly visible in the presence of other plugins + * or Lua scripts). It is thus strongly advised to avoid any call to + * the REST API of Orthanc in the callback. If you have to call + * other primitives of Orthanc, you should make these calls in a + * separate thread, passing the pending events to be processed + * through a message queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback function. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnChangeCallback( + OrthancPluginContext* context, + OrthancPluginOnChangeCallback callback) + { + _OrthancPluginOnChangeCallback params; + params.callback = callback; + + context->InvokeService(context, _OrthancPluginService_RegisterOnChangeCallback, ¶ms); + } + + + + typedef struct + { + const char* plugin; + _OrthancPluginProperty property; + const char* value; + } _OrthancPluginSetPluginProperty; + + + /** + * @brief Set the URI where the plugin provides its Web interface. + * + * For plugins that come with a Web interface, this function + * declares the entry path where to find this interface. This + * information is notably used in the "Plugins" page of Orthanc + * Explorer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param uri The root URI for this plugin. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri( + OrthancPluginContext* context, + const char* uri) + { + _OrthancPluginSetPluginProperty params; + params.plugin = OrthancPluginGetName(); + params.property = _OrthancPluginProperty_RootUri; + params.value = uri; + + context->InvokeService(context, _OrthancPluginService_SetPluginProperty, ¶ms); + } + + + /** + * @brief Set a description for this plugin. + * + * Set a description for this plugin. It is displayed in the + * "Plugins" page of Orthanc Explorer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param description The description. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription( + OrthancPluginContext* context, + const char* description) + { + _OrthancPluginSetPluginProperty params; + params.plugin = OrthancPluginGetName(); + params.property = _OrthancPluginProperty_Description; + params.value = description; + + context->InvokeService(context, _OrthancPluginService_SetPluginProperty, ¶ms); + } + + + /** + * @brief Extend the JavaScript code of Orthanc Explorer. + * + * Add JavaScript code to customize the default behavior of Orthanc + * Explorer. This can for instance be used to add new buttons. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param javascript The custom JavaScript code. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer( + OrthancPluginContext* context, + const char* javascript) + { + _OrthancPluginSetPluginProperty params; + params.plugin = OrthancPluginGetName(); + params.property = _OrthancPluginProperty_OrthancExplorer; + params.value = javascript; + + context->InvokeService(context, _OrthancPluginService_SetPluginProperty, ¶ms); + } + + + typedef struct + { + char** result; + int32_t property; + const char* value; + } _OrthancPluginGlobalProperty; + + + /** + * @brief Get the value of a global property. + * + * Get the value of a global property that is stored in the Orthanc database. Global + * properties whose index is below 1024 are reserved by Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param property The global property of interest. + * @param defaultValue The value to return, if the global property is unset. + * @return The value of the global property, or NULL in the case of an error. This + * string must be freed by OrthancPluginFreeString(). + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetGlobalProperty( + OrthancPluginContext* context, + int32_t property, + const char* defaultValue) + { + char* result; + + _OrthancPluginGlobalProperty params; + params.result = &result; + params.property = property; + params.value = defaultValue; + + if (context->InvokeService(context, _OrthancPluginService_GetGlobalProperty, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Set the value of a global property. + * + * Set the value of a global property into the Orthanc + * database. Setting a global property can be used by plugins to + * save their internal parameters. Plugins are only allowed to set + * properties whose index are above or equal to 1024 (properties + * below 1024 are read-only and reserved by Orthanc). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param property The global property of interest. + * @param value The value to be set in the global property. + * @return 0 if success, or the error code if failure. + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetGlobalProperty( + OrthancPluginContext* context, + int32_t property, + const char* value) + { + _OrthancPluginGlobalProperty params; + params.result = NULL; + params.property = property; + params.value = value; + + return context->InvokeService(context, _OrthancPluginService_SetGlobalProperty, ¶ms); + } + + + + typedef struct + { + int32_t *resultInt32; + uint32_t *resultUint32; + int64_t *resultInt64; + uint64_t *resultUint64; + } _OrthancPluginReturnSingleValue; + + /** + * @brief Get the number of command-line arguments. + * + * Retrieve the number of command-line arguments that were used to launch Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return The number of arguments. + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetCommandLineArgumentsCount( + OrthancPluginContext* context) + { + uint32_t count = 0; + + _OrthancPluginReturnSingleValue params; + memset(¶ms, 0, sizeof(params)); + params.resultUint32 = &count; + + if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgumentsCount, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + + + /** + * @brief Get the value of a command-line argument. + * + * Get the value of one of the command-line arguments that were used + * to launch Orthanc. The number of available arguments can be + * retrieved by OrthancPluginGetCommandLineArgumentsCount(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param argument The index of the argument. + * @return The value of the argument, or NULL in the case of an error. This + * string must be freed by OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetCommandLineArgument( + OrthancPluginContext* context, + uint32_t argument) + { + char* result; + + _OrthancPluginGlobalProperty params; + params.result = &result; + params.property = (int32_t) argument; + params.value = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgument, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the expected version of the database schema. + * + * Retrieve the expected version of the database schema. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return The version. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetExpectedDatabaseVersion( + OrthancPluginContext* context) + { + uint32_t count = 0; + + _OrthancPluginReturnSingleValue params; + memset(¶ms, 0, sizeof(params)); + params.resultUint32 = &count; + + if (context->InvokeService(context, _OrthancPluginService_GetExpectedDatabaseVersion, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + + + /** + * @brief Return the content of the configuration file(s). + * + * This function returns the content of the configuration that is + * used by Orthanc, formatted as a JSON string. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the configuration. This string must be freed by + * OrthancPluginFreeString(). + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfiguration(OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetConfiguration, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* subType; + const char* contentType; + } _OrthancPluginStartMultipartAnswer; + + /** + * @brief Start an HTTP multipart answer. + * + * Initiates a HTTP multipart answer, as the result of a REST request. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param subType The sub-type of the multipart answer ("mixed" or "related"). + * @param contentType The MIME type of the items in the multipart answer. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginSendMultipartItem(), OrthancPluginSendMultipartItem2() + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartMultipartAnswer( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* subType, + const char* contentType) + { + _OrthancPluginStartMultipartAnswer params; + params.output = output; + params.subType = subType; + params.contentType = contentType; + return context->InvokeService(context, _OrthancPluginService_StartMultipartAnswer, ¶ms); + } + + + /** + * @brief Send an item as a part of some HTTP multipart answer. + * + * This function sends an item as a part of some HTTP multipart + * answer that was initiated by OrthancPluginStartMultipartAnswer(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param answer Pointer to the memory buffer containing the item. + * @param answerSize Number of bytes of the item. + * @return 0 if success, or the error code if failure (this notably happens + * if the connection is closed by the client). + * @see OrthancPluginSendMultipartItem2() + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const void* answer, + uint32_t answerSize) + { + _OrthancPluginAnswerBuffer params; + params.output = output; + params.answer = answer; + params.answerSize = answerSize; + params.mimeType = NULL; + return context->InvokeService(context, _OrthancPluginService_SendMultipartItem, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const void* source; + uint32_t size; + OrthancPluginCompressionType compression; + uint8_t uncompress; + } _OrthancPluginBufferCompression; + + + /** + * @brief Compress or decompress a buffer. + * + * This function compresses or decompresses a buffer, using the + * version of the zlib library that is used by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param source The source buffer. + * @param size The size in bytes of the source buffer. + * @param compression The compression algorithm. + * @param uncompress If set to "0", the buffer must be compressed. + * If set to "1", the buffer must be uncompressed. + * @return 0 if success, or the error code if failure. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginBufferCompression( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const void* source, + uint32_t size, + OrthancPluginCompressionType compression, + uint8_t uncompress) + { + _OrthancPluginBufferCompression params; + params.target = target; + params.source = source; + params.size = size; + params.compression = compression; + params.uncompress = uncompress; + + return context->InvokeService(context, _OrthancPluginService_BufferCompression, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* path; + } _OrthancPluginReadFile; + + /** + * @brief Read a file. + * + * Read the content of a file on the filesystem, and returns it into + * a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param path The path of the file to be read. + * @return 0 if success, or the error code if failure. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginReadFile( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* path) + { + _OrthancPluginReadFile params; + params.target = target; + params.path = path; + return context->InvokeService(context, _OrthancPluginService_ReadFile, ¶ms); + } + + + + typedef struct + { + const char* path; + const void* data; + uint32_t size; + } _OrthancPluginWriteFile; + + /** + * @brief Write a file. + * + * Write the content of a memory buffer to the filesystem. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param path The path of the file to be written. + * @param data The content of the memory buffer. + * @param size The size of the memory buffer. + * @return 0 if success, or the error code if failure. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWriteFile( + OrthancPluginContext* context, + const char* path, + const void* data, + uint32_t size) + { + _OrthancPluginWriteFile params; + params.path = path; + params.data = data; + params.size = size; + return context->InvokeService(context, _OrthancPluginService_WriteFile, ¶ms); + } + + + + typedef struct + { + const char** target; + OrthancPluginErrorCode error; + } _OrthancPluginGetErrorDescription; + + /** + * @brief Get the description of a given error code. + * + * This function returns the description of a given error code. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param error The error code of interest. + * @return The error description. This is a statically-allocated + * string, do not free it. + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetErrorDescription( + OrthancPluginContext* context, + OrthancPluginErrorCode error) + { + const char* result = NULL; + + _OrthancPluginGetErrorDescription params; + params.target = &result; + params.error = error; + + if (context->InvokeService(context, _OrthancPluginService_GetErrorDescription, ¶ms) != OrthancPluginErrorCode_Success || + result == NULL) + { + return "Unknown error code"; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + uint16_t status; + const void* body; + uint32_t bodySize; + } _OrthancPluginSendHttpStatus; + + /** + * @brief Send a HTTP status, with a custom body. + * + * This function answers to a HTTP request by sending a HTTP status + * code (such as "400 - Bad Request"), together with a body + * describing the error. The body will only be returned if the + * configuration option "HttpDescribeErrors" of Orthanc is set to "true". + * + * Note that: + * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer(). + * - Redirections (status 301) must use ::OrthancPluginRedirect(). + * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized(). + * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param status The HTTP status code to be sent. + * @param body The body of the answer. + * @param bodySize The size of the body. + * @see OrthancPluginSendHttpStatusCode() + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatus( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + uint16_t status, + const void* body, + uint32_t bodySize) + { + _OrthancPluginSendHttpStatus params; + params.output = output; + params.status = status; + params.body = body; + params.bodySize = bodySize; + context->InvokeService(context, _OrthancPluginService_SendHttpStatus, ¶ms); + } + + + + typedef struct + { + const OrthancPluginImage* image; + uint32_t* resultUint32; + OrthancPluginPixelFormat* resultPixelFormat; + void** resultBuffer; + } _OrthancPluginGetImageInfo; + + + /** + * @brief Return the pixel format of an image. + * + * This function returns the type of memory layout for the pixels of the given image. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param image The image of interest. + * @return The pixel format. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginPixelFormat OrthancPluginGetImagePixelFormat( + OrthancPluginContext* context, + const OrthancPluginImage* image) + { + OrthancPluginPixelFormat target; + + _OrthancPluginGetImageInfo params; + memset(¶ms, 0, sizeof(params)); + params.image = image; + params.resultPixelFormat = ⌖ + + if (context->InvokeService(context, _OrthancPluginService_GetImagePixelFormat, ¶ms) != OrthancPluginErrorCode_Success) + { + return OrthancPluginPixelFormat_Unknown; + } + else + { + return (OrthancPluginPixelFormat) target; + } + } + + + + /** + * @brief Return the width of an image. + * + * This function returns the width of the given image. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param image The image of interest. + * @return The width. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetImageWidth( + OrthancPluginContext* context, + const OrthancPluginImage* image) + { + uint32_t width; + + _OrthancPluginGetImageInfo params; + memset(¶ms, 0, sizeof(params)); + params.image = image; + params.resultUint32 = &width; + + if (context->InvokeService(context, _OrthancPluginService_GetImageWidth, ¶ms) != OrthancPluginErrorCode_Success) + { + return 0; + } + else + { + return width; + } + } + + + + /** + * @brief Return the height of an image. + * + * This function returns the height of the given image. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param image The image of interest. + * @return The height. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetImageHeight( + OrthancPluginContext* context, + const OrthancPluginImage* image) + { + uint32_t height; + + _OrthancPluginGetImageInfo params; + memset(¶ms, 0, sizeof(params)); + params.image = image; + params.resultUint32 = &height; + + if (context->InvokeService(context, _OrthancPluginService_GetImageHeight, ¶ms) != OrthancPluginErrorCode_Success) + { + return 0; + } + else + { + return height; + } + } + + + + /** + * @brief Return the pitch of an image. + * + * This function returns the pitch of the given image. The pitch is + * defined as the number of bytes between 2 successive lines of the + * image in the memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param image The image of interest. + * @return The pitch. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetImagePitch( + OrthancPluginContext* context, + const OrthancPluginImage* image) + { + uint32_t pitch; + + _OrthancPluginGetImageInfo params; + memset(¶ms, 0, sizeof(params)); + params.image = image; + params.resultUint32 = &pitch; + + if (context->InvokeService(context, _OrthancPluginService_GetImagePitch, ¶ms) != OrthancPluginErrorCode_Success) + { + return 0; + } + else + { + return pitch; + } + } + + + + /** + * @brief Return a pointer to the content of an image. + * + * This function returns a pointer to the memory buffer that + * contains the pixels of the image. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param image The image of interest. + * @return The pointer. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE void* OrthancPluginGetImageBuffer( + OrthancPluginContext* context, + const OrthancPluginImage* image) + { + void* target = NULL; + + _OrthancPluginGetImageInfo params; + memset(¶ms, 0, sizeof(params)); + params.resultBuffer = ⌖ + params.image = image; + + if (context->InvokeService(context, _OrthancPluginService_GetImageBuffer, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginImage** target; + const void* data; + uint32_t size; + OrthancPluginImageFormat format; + } _OrthancPluginUncompressImage; + + + /** + * @brief Decode a compressed image. + * + * This function decodes a compressed image from a memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param data Pointer to a memory buffer containing the compressed image. + * @param size Size of the memory buffer containing the compressed image. + * @param format The file format of the compressed image. + * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginUncompressImage( + OrthancPluginContext* context, + const void* data, + uint32_t size, + OrthancPluginImageFormat format) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginUncompressImage params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.data = data; + params.size = size; + params.format = format; + + if (context->InvokeService(context, _OrthancPluginService_UncompressImage, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + + + typedef struct + { + OrthancPluginImage* image; + } _OrthancPluginFreeImage; + + /** + * @brief Free an image. + * + * This function frees an image that was decoded with OrthancPluginUncompressImage(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param image The image. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeImage( + OrthancPluginContext* context, + OrthancPluginImage* image) + { + _OrthancPluginFreeImage params; + params.image = image; + + context->InvokeService(context, _OrthancPluginService_FreeImage, ¶ms); + } + + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + OrthancPluginImageFormat imageFormat; + OrthancPluginPixelFormat pixelFormat; + uint32_t width; + uint32_t height; + uint32_t pitch; + const void* buffer; + uint8_t quality; + } _OrthancPluginCompressImage; + + + /** + * @brief Encode a PNG image. + * + * This function compresses the given memory buffer containing an + * image using the PNG specification, and stores the result of the + * compression into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param format The memory layout of the uncompressed image. + * @param width The width of the image. + * @param height The height of the image. + * @param pitch The pitch of the image (i.e. the number of bytes + * between 2 successive lines of the image in the memory buffer). + * @param buffer The memory buffer containing the uncompressed image. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginCompressAndAnswerPngImage() + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressPngImage( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + const void* buffer) + { + _OrthancPluginCompressImage params; + memset(¶ms, 0, sizeof(params)); + params.target = target; + params.imageFormat = OrthancPluginImageFormat_Png; + params.pixelFormat = format; + params.width = width; + params.height = height; + params.pitch = pitch; + params.buffer = buffer; + params.quality = 0; /* Unused for PNG */ + + return context->InvokeService(context, _OrthancPluginService_CompressImage, ¶ms); + } + + + /** + * @brief Encode a JPEG image. + * + * This function compresses the given memory buffer containing an + * image using the JPEG specification, and stores the result of the + * compression into a newly allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param format The memory layout of the uncompressed image. + * @param width The width of the image. + * @param height The height of the image. + * @param pitch The pitch of the image (i.e. the number of bytes + * between 2 successive lines of the image in the memory buffer). + * @param buffer The memory buffer containing the uncompressed image. + * @param quality The quality of the JPEG encoding, between 1 (worst + * quality, best compression) and 100 (best quality, worst + * compression). + * @return 0 if success, or the error code if failure. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressJpegImage( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + const void* buffer, + uint8_t quality) + { + _OrthancPluginCompressImage params; + memset(¶ms, 0, sizeof(params)); + params.target = target; + params.imageFormat = OrthancPluginImageFormat_Jpeg; + params.pixelFormat = format; + params.width = width; + params.height = height; + params.pitch = pitch; + params.buffer = buffer; + params.quality = quality; + + return context->InvokeService(context, _OrthancPluginService_CompressImage, ¶ms); + } + + + + /** + * @brief Answer to a REST request with a JPEG image. + * + * This function answers to a REST request with a JPEG image. The + * parameters of this function describe a memory buffer that + * contains an uncompressed image. The image will be automatically compressed + * as a JPEG image by the core system of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param format The memory layout of the uncompressed image. + * @param width The width of the image. + * @param height The height of the image. + * @param pitch The pitch of the image (i.e. the number of bytes + * between 2 successive lines of the image in the memory buffer). + * @param buffer The memory buffer containing the uncompressed image. + * @param quality The quality of the JPEG encoding, between 1 (worst + * quality, best compression) and 100 (best quality, worst + * compression). + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerJpegImage( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + const void* buffer, + uint8_t quality) + { + _OrthancPluginCompressAndAnswerImage params; + params.output = output; + params.imageFormat = OrthancPluginImageFormat_Jpeg; + params.pixelFormat = format; + params.width = width; + params.height = height; + params.pitch = pitch; + params.buffer = buffer; + params.quality = quality; + context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, ¶ms); + } + + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + OrthancPluginHttpMethod method; + const char* url; + const char* username; + const char* password; + const void* body; + uint32_t bodySize; + } _OrthancPluginCallHttpClient; + + + /** + * @brief Issue a HTTP GET call. + * + * Make a HTTP GET call to the given URL. The result to the query is + * stored into a newly allocated memory buffer. Favor + * OrthancPluginRestApiGet() if calling the built-in REST API of the + * Orthanc instance that hosts this plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param url The URL of interest. + * @param username The username (can be NULL if no password protection). + * @param password The password (can be NULL if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpGet( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* url, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶ms, 0, sizeof(params)); + + params.target = target; + params.method = OrthancPluginHttpMethod_Get; + params.url = url; + params.username = username; + params.password = password; + + return context->InvokeService(context, _OrthancPluginService_CallHttpClient, ¶ms); + } + + + /** + * @brief Issue a HTTP POST call. + * + * Make a HTTP POST call to the given URL. The result to the query + * is stored into a newly allocated memory buffer. Favor + * OrthancPluginRestApiPost() if calling the built-in REST API of + * the Orthanc instance that hosts this plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param url The URL of interest. + * @param body The content of the body of the request. + * @param bodySize The size of the body of the request. + * @param username The username (can be NULL if no password protection). + * @param password The password (can be NULL if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpPost( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* url, + const void* body, + uint32_t bodySize, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶ms, 0, sizeof(params)); + + params.target = target; + params.method = OrthancPluginHttpMethod_Post; + params.url = url; + params.body = body; + params.bodySize = bodySize; + params.username = username; + params.password = password; + + return context->InvokeService(context, _OrthancPluginService_CallHttpClient, ¶ms); + } + + + /** + * @brief Issue a HTTP PUT call. + * + * Make a HTTP PUT call to the given URL. The result to the query is + * stored into a newly allocated memory buffer. Favor + * OrthancPluginRestApiPut() if calling the built-in REST API of the + * Orthanc instance that hosts this plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param url The URL of interest. + * @param body The content of the body of the request. + * @param bodySize The size of the body of the request. + * @param username The username (can be NULL if no password protection). + * @param password The password (can be NULL if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpPut( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* url, + const void* body, + uint32_t bodySize, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶ms, 0, sizeof(params)); + + params.target = target; + params.method = OrthancPluginHttpMethod_Put; + params.url = url; + params.body = body; + params.bodySize = bodySize; + params.username = username; + params.password = password; + + return context->InvokeService(context, _OrthancPluginService_CallHttpClient, ¶ms); + } + + + /** + * @brief Issue a HTTP DELETE call. + * + * Make a HTTP DELETE call to the given URL. Favor + * OrthancPluginRestApiDelete() if calling the built-in REST API of + * the Orthanc instance that hosts this plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param url The URL of interest. + * @param username The username (can be NULL if no password protection). + * @param password The password (can be NULL if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpDelete( + OrthancPluginContext* context, + const char* url, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶ms, 0, sizeof(params)); + + params.method = OrthancPluginHttpMethod_Delete; + params.url = url; + params.username = username; + params.password = password; + + return context->InvokeService(context, _OrthancPluginService_CallHttpClient, ¶ms); + } + + + + typedef struct + { + OrthancPluginImage** target; + const OrthancPluginImage* source; + OrthancPluginPixelFormat targetFormat; + } _OrthancPluginConvertPixelFormat; + + + /** + * @brief Change the pixel format of an image. + * + * This function creates a new image, changing the memory layout of the pixels. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param source The source image. + * @param targetFormat The target pixel format. + * @return The resulting image. It must be freed with OrthancPluginFreeImage(). + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginConvertPixelFormat( + OrthancPluginContext* context, + const OrthancPluginImage* source, + OrthancPluginPixelFormat targetFormat) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginConvertPixelFormat params; + params.target = ⌖ + params.source = source; + params.targetFormat = targetFormat; + + if (context->InvokeService(context, _OrthancPluginService_ConvertPixelFormat, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Return the number of available fonts. + * + * This function returns the number of fonts that are built in the + * Orthanc core. These fonts can be used to draw texts on images + * through OrthancPluginDrawText(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return The number of fonts. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontsCount( + OrthancPluginContext* context) + { + uint32_t count = 0; + + _OrthancPluginReturnSingleValue params; + memset(¶ms, 0, sizeof(params)); + params.resultUint32 = &count; + + if (context->InvokeService(context, _OrthancPluginService_GetFontsCount, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + + + + typedef struct + { + uint32_t fontIndex; /* in */ + const char** name; /* out */ + uint32_t* size; /* out */ + } _OrthancPluginGetFontInfo; + + /** + * @brief Return the name of a font. + * + * This function returns the name of a font that is built in the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount(). + * @return The font name. This is a statically-allocated string, do not free it. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetFontName( + OrthancPluginContext* context, + uint32_t fontIndex) + { + const char* result = NULL; + + _OrthancPluginGetFontInfo params; + memset(¶ms, 0, sizeof(params)); + params.name = &result; + params.fontIndex = fontIndex; + + if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Return the size of a font. + * + * This function returns the size of a font that is built in the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount(). + * @return The font size. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontSize( + OrthancPluginContext* context, + uint32_t fontIndex) + { + uint32_t result; + + _OrthancPluginGetFontInfo params; + memset(¶ms, 0, sizeof(params)); + params.size = &result; + params.fontIndex = fontIndex; + + if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, ¶ms) != OrthancPluginErrorCode_Success) + { + return 0; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginImage* image; + uint32_t fontIndex; + const char* utf8Text; + int32_t x; + int32_t y; + uint8_t r; + uint8_t g; + uint8_t b; + } _OrthancPluginDrawText; + + + /** + * @brief Draw text on an image. + * + * This function draws some text on some image. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param image The image upon which to draw the text. + * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount(). + * @param utf8Text The text to be drawn, encoded as an UTF-8 zero-terminated string. + * @param x The X position of the text over the image. + * @param y The Y position of the text over the image. + * @param r The value of the red color channel of the text. + * @param g The value of the green color channel of the text. + * @param b The value of the blue color channel of the text. + * @return 0 if success, other value if error. + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDrawText( + OrthancPluginContext* context, + OrthancPluginImage* image, + uint32_t fontIndex, + const char* utf8Text, + int32_t x, + int32_t y, + uint8_t r, + uint8_t g, + uint8_t b) + { + _OrthancPluginDrawText params; + memset(¶ms, 0, sizeof(params)); + params.image = image; + params.fontIndex = fontIndex; + params.utf8Text = utf8Text; + params.x = x; + params.y = y; + params.r = r; + params.g = g; + params.b = b; + + return context->InvokeService(context, _OrthancPluginService_DrawText, ¶ms); + } + + + + typedef struct + { + OrthancPluginStorageArea* storageArea; + const char* uuid; + const void* content; + uint64_t size; + OrthancPluginContentType type; + } _OrthancPluginStorageAreaCreate; + + + /** + * @brief Create a file inside the storage area. + * + * This function creates a new file inside the storage area that is + * currently used by Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storageArea The storage area. + * @param uuid The identifier of the file to be created. + * @param content The content to store in the newly created file. + * @param size The size of the content. + * @param type The type of the file content. + * @return 0 if success, other value if error. + * @ingroup Callbacks + * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiPut()" on + * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaCreate( + OrthancPluginContext* context, + OrthancPluginStorageArea* storageArea, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type) + { + _OrthancPluginStorageAreaCreate params; + params.storageArea = storageArea; + params.uuid = uuid; + params.content = content; + params.size = size; + params.type = type; + + return context->InvokeService(context, _OrthancPluginService_StorageAreaCreate, ¶ms); + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + OrthancPluginStorageArea* storageArea; + const char* uuid; + OrthancPluginContentType type; + } _OrthancPluginStorageAreaRead; + + + /** + * @brief Read a file from the storage area. + * + * This function reads the content of a given file from the storage + * area that is currently used by Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param storageArea The storage area. + * @param uuid The identifier of the file to be read. + * @param type The type of the file content. + * @return 0 if success, other value if error. + * @ingroup Callbacks + * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiGet()" on + * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRead( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + OrthancPluginStorageArea* storageArea, + const char* uuid, + OrthancPluginContentType type) + { + _OrthancPluginStorageAreaRead params; + params.target = target; + params.storageArea = storageArea; + params.uuid = uuid; + params.type = type; + + return context->InvokeService(context, _OrthancPluginService_StorageAreaRead, ¶ms); + } + + + typedef struct + { + OrthancPluginStorageArea* storageArea; + const char* uuid; + OrthancPluginContentType type; + } _OrthancPluginStorageAreaRemove; + + /** + * @brief Remove a file from the storage area. + * + * This function removes a given file from the storage area that is + * currently used by Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storageArea The storage area. + * @param uuid The identifier of the file to be removed. + * @param type The type of the file content. + * @return 0 if success, other value if error. + * @ingroup Callbacks + * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiDelete()" on + * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead. + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRemove( + OrthancPluginContext* context, + OrthancPluginStorageArea* storageArea, + const char* uuid, + OrthancPluginContentType type) + { + _OrthancPluginStorageAreaRemove params; + params.storageArea = storageArea; + params.uuid = uuid; + params.type = type; + + return context->InvokeService(context, _OrthancPluginService_StorageAreaRemove, ¶ms); + } + + + + typedef struct + { + OrthancPluginErrorCode* target; + int32_t code; + uint16_t httpStatus; + const char* message; + } _OrthancPluginRegisterErrorCode; + + /** + * @brief Declare a custom error code for this plugin. + * + * This function declares a custom error code that can be generated + * by this plugin. This declaration is used to enrich the body of + * the HTTP answer in the case of an error, and to set the proper + * HTTP status code. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param code The error code that is internal to this plugin. + * @param httpStatus The HTTP status corresponding to this error. + * @param message The description of the error. + * @return The error code that has been assigned inside the Orthanc core. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterErrorCode( + OrthancPluginContext* context, + int32_t code, + uint16_t httpStatus, + const char* message) + { + OrthancPluginErrorCode target; + + _OrthancPluginRegisterErrorCode params; + params.target = ⌖ + params.code = code; + params.httpStatus = httpStatus; + params.message = message; + + if (context->InvokeService(context, _OrthancPluginService_RegisterErrorCode, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + /* There was an error while assigned the error. Use a generic code. */ + return OrthancPluginErrorCode_Plugin; + } + } + + + + typedef struct + { + uint16_t group; + uint16_t element; + OrthancPluginValueRepresentation vr; + const char* name; + uint32_t minMultiplicity; + uint32_t maxMultiplicity; + } _OrthancPluginRegisterDictionaryTag; + + /** + * @brief Register a new tag into the DICOM dictionary. + * + * This function declares a new public tag in the dictionary of + * DICOM tags that are known to Orthanc. This function should be + * used in the OrthancPluginInitialize() callback. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param group The group of the tag. + * @param element The element of the tag. + * @param vr The value representation of the tag. + * @param name The nickname of the tag. + * @param minMultiplicity The minimum multiplicity of the tag (must be above 0). + * @param maxMultiplicity The maximum multiplicity of the tag. A value of 0 means + * an arbitrary multiplicity ("n"). + * @return 0 if success, other value if error. + * @see OrthancPluginRegisterPrivateDictionaryTag() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDictionaryTag( + OrthancPluginContext* context, + uint16_t group, + uint16_t element, + OrthancPluginValueRepresentation vr, + const char* name, + uint32_t minMultiplicity, + uint32_t maxMultiplicity) + { + _OrthancPluginRegisterDictionaryTag params; + params.group = group; + params.element = element; + params.vr = vr; + params.name = name; + params.minMultiplicity = minMultiplicity; + params.maxMultiplicity = maxMultiplicity; + + return context->InvokeService(context, _OrthancPluginService_RegisterDictionaryTag, ¶ms); + } + + + + typedef struct + { + uint16_t group; + uint16_t element; + OrthancPluginValueRepresentation vr; + const char* name; + uint32_t minMultiplicity; + uint32_t maxMultiplicity; + const char* privateCreator; + } _OrthancPluginRegisterPrivateDictionaryTag; + + /** + * @brief Register a new private tag into the DICOM dictionary. + * + * This function declares a new private tag in the dictionary of + * DICOM tags that are known to Orthanc. This function should be + * used in the OrthancPluginInitialize() callback. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param group The group of the tag. + * @param element The element of the tag. + * @param vr The value representation of the tag. + * @param name The nickname of the tag. + * @param minMultiplicity The minimum multiplicity of the tag (must be above 0). + * @param maxMultiplicity The maximum multiplicity of the tag. A value of 0 means + * an arbitrary multiplicity ("n"). + * @param privateCreator The private creator of this private tag. + * @return 0 if success, other value if error. + * @see OrthancPluginRegisterDictionaryTag() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterPrivateDictionaryTag( + OrthancPluginContext* context, + uint16_t group, + uint16_t element, + OrthancPluginValueRepresentation vr, + const char* name, + uint32_t minMultiplicity, + uint32_t maxMultiplicity, + const char* privateCreator) + { + _OrthancPluginRegisterPrivateDictionaryTag params; + params.group = group; + params.element = element; + params.vr = vr; + params.name = name; + params.minMultiplicity = minMultiplicity; + params.maxMultiplicity = maxMultiplicity; + params.privateCreator = privateCreator; + + return context->InvokeService(context, _OrthancPluginService_RegisterPrivateDictionaryTag, ¶ms); + } + + + + typedef struct + { + OrthancPluginStorageArea* storageArea; + OrthancPluginResourceType level; + } _OrthancPluginReconstructMainDicomTags; + + /** + * @brief Reconstruct the main DICOM tags. + * + * This function requests the Orthanc core to reconstruct the main + * DICOM tags of all the resources of the given type. This function + * can only be used as a part of the upgrade of a custom database + * back-end. A database transaction will be automatically setup. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storageArea The storage area. + * @param level The type of the resources of interest. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginReconstructMainDicomTags( + OrthancPluginContext* context, + OrthancPluginStorageArea* storageArea, + OrthancPluginResourceType level) + { + _OrthancPluginReconstructMainDicomTags params; + params.level = level; + params.storageArea = storageArea; + + return context->InvokeService(context, _OrthancPluginService_ReconstructMainDicomTags, ¶ms); + } + + + typedef struct + { + char** result; + const char* instanceId; + const void* buffer; + uint32_t size; + OrthancPluginDicomToJsonFormat format; + OrthancPluginDicomToJsonFlags flags; + uint32_t maxStringLength; + } _OrthancPluginDicomToJson; + + + /** + * @brief Format a DICOM memory buffer as a JSON string. + * + * This function takes as input a memory buffer containing a DICOM + * file, and outputs a JSON string representing the tags of this + * DICOM file. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM file. + * @param size The size of the memory buffer. + * @param format The output format. + * @param flags Flags governing the output. + * @param maxStringLength The maximum length of a field. Too long fields will + * be output as "null". The 0 value means no maximum length. + * @return The NULL value if the case of an error, or the JSON + * string. This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + * @see OrthancPluginDicomInstanceToJson + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson( + OrthancPluginContext* context, + const void* buffer, + uint32_t size, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + char* result; + + _OrthancPluginDicomToJson params; + memset(¶ms, 0, sizeof(params)); + params.result = &result; + params.buffer = buffer; + params.size = size; + params.format = format; + params.flags = flags; + params.maxStringLength = maxStringLength; + + if (context->InvokeService(context, _OrthancPluginService_DicomBufferToJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Format a DICOM instance as a JSON string. + * + * This function formats a DICOM instance that is stored in Orthanc, + * and outputs a JSON string representing the tags of this DICOM + * instance. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The Orthanc identifier of the instance. + * @param format The output format. + * @param flags Flags governing the output. + * @param maxStringLength The maximum length of a field. Too long fields will + * be output as "null". The 0 value means no maximum length. + * @return The NULL value if the case of an error, or the JSON + * string. This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + * @see OrthancPluginDicomInstanceToJson + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomInstanceToJson( + OrthancPluginContext* context, + const char* instanceId, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + char* result; + + _OrthancPluginDicomToJson params; + memset(¶ms, 0, sizeof(params)); + params.result = &result; + params.instanceId = instanceId; + params.format = format; + params.flags = flags; + params.maxStringLength = maxStringLength; + + if (context->InvokeService(context, _OrthancPluginService_DicomInstanceToJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* uri; + uint32_t headersCount; + const char* const* headersKeys; + const char* const* headersValues; + int32_t afterPlugins; + } _OrthancPluginRestApiGet2; + + /** + * @brief Make a GET call to the Orthanc REST API, with custom HTTP headers. + * + * Make a GET call to the Orthanc REST API with extended + * parameters. The result to the query is stored into a newly + * allocated memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param uri The URI in the built-in Orthanc API. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers (can be NULL if no header). + * @param headersValues Array containing the values of the HTTP headers (can be NULL if no header). + * @param afterPlugins If 0, the built-in API of Orthanc is used. + * If 1, the API is tainted by the plugins. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginRestApiGet, OrthancPluginRestApiGetAfterPlugins + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGet2( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + int32_t afterPlugins) + { + _OrthancPluginRestApiGet2 params; + params.target = target; + params.uri = uri; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + params.afterPlugins = afterPlugins; + + return context->InvokeService(context, _OrthancPluginService_RestApiGet2, ¶ms); + } + + + + typedef struct + { + OrthancPluginWorklistCallback callback; + } _OrthancPluginWorklistCallback; + + /** + * @brief Register a callback to handle modality worklists requests. + * + * This function registers a callback to handle C-Find SCP requests + * on modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWorklistCallback( + OrthancPluginContext* context, + OrthancPluginWorklistCallback callback) + { + _OrthancPluginWorklistCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterWorklistCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginWorklistAnswers* answers; + const OrthancPluginWorklistQuery* query; + const void* dicom; + uint32_t size; + } _OrthancPluginWorklistAnswersOperation; + + /** + * @brief Add one answer to some modality worklist request. + * + * This function adds one worklist (encoded as a DICOM file) to the + * set of answers corresponding to some C-Find SCP request against + * modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @param query The worklist query, as received by the callback. + * @param dicom The worklist to answer, encoded as a DICOM file. + * @param size The size of the DICOM file. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + * @see OrthancPluginCreateDicom() + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWorklistAddAnswer( + OrthancPluginContext* context, + OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const void* dicom, + uint32_t size) + { + _OrthancPluginWorklistAnswersOperation params; + params.answers = answers; + params.query = query; + params.dicom = dicom; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_WorklistAddAnswer, ¶ms); + } + + + /** + * @brief Mark the set of worklist answers as incomplete. + * + * This function marks as incomplete the set of answers + * corresponding to some C-Find SCP request against modality + * worklists. This must be used if canceling the handling of a + * request when too many answers are to be returned. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWorklistMarkIncomplete( + OrthancPluginContext* context, + OrthancPluginWorklistAnswers* answers) + { + _OrthancPluginWorklistAnswersOperation params; + params.answers = answers; + params.query = NULL; + params.dicom = NULL; + params.size = 0; + + return context->InvokeService(context, _OrthancPluginService_WorklistMarkIncomplete, ¶ms); + } + + + typedef struct + { + const OrthancPluginWorklistQuery* query; + const void* dicom; + uint32_t size; + int32_t* isMatch; + OrthancPluginMemoryBuffer* target; + } _OrthancPluginWorklistQueryOperation; + + /** + * @brief Test whether a worklist matches the query. + * + * This function checks whether one worklist (encoded as a DICOM + * file) matches the C-Find SCP query against modality + * worklists. This function must be called before adding the + * worklist as an answer through OrthancPluginWorklistAddAnswer(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param query The worklist query, as received by the callback. + * @param dicom The worklist to answer, encoded as a DICOM file. + * @param size The size of the DICOM file. + * @return 1 if the worklist matches the query, 0 otherwise. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginWorklistIsMatch( + OrthancPluginContext* context, + const OrthancPluginWorklistQuery* query, + const void* dicom, + uint32_t size) + { + int32_t isMatch = 0; + + _OrthancPluginWorklistQueryOperation params; + params.query = query; + params.dicom = dicom; + params.size = size; + params.isMatch = &isMatch; + params.target = NULL; + + if (context->InvokeService(context, _OrthancPluginService_WorklistIsMatch, ¶ms) == OrthancPluginErrorCode_Success) + { + return isMatch; + } + else + { + /* Error: Assume non-match */ + return 0; + } + } + + + /** + * @brief Retrieve the worklist query as a DICOM file. + * + * This function retrieves the DICOM file that underlies a C-Find + * SCP query against modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param query The worklist query, as received by the callback. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWorklistGetDicomQuery( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginWorklistQuery* query) + { + _OrthancPluginWorklistQueryOperation params; + params.query = query; + params.dicom = NULL; + params.size = 0; + params.isMatch = NULL; + params.target = target; + + return context->InvokeService(context, _OrthancPluginService_WorklistGetDicomQuery, ¶ms); + } + + + /** + * @brief Get the origin of a DICOM file. + * + * This function returns the origin of a DICOM instance that has been received by Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The origin of the instance. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + OrthancPluginInstanceOrigin origin; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultOrigin = &origin; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceOrigin, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return OrthancPluginInstanceOrigin_Unknown; + } + else + { + return origin; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* json; + const OrthancPluginImage* pixelData; + OrthancPluginCreateDicomFlags flags; + } _OrthancPluginCreateDicom; + + /** + * @brief Create a DICOM instance from a JSON string and an image. + * + * This function takes as input a string containing a JSON file + * describing the content of a DICOM instance. As an output, it + * writes the corresponding DICOM instance to a newly allocated + * memory buffer. Additionally, an image to be encoded within the + * DICOM instance can also be provided. + * + * 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. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param json The input JSON file. + * @param pixelData The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme. + * @param flags Flags governing the output. + * @return 0 if success, other value if error. + * @ingroup Toolbox + * @see OrthancPluginCreateDicom2 + * @see OrthancPluginDicomBufferToJson + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* json, + const OrthancPluginImage* pixelData, + OrthancPluginCreateDicomFlags flags) + { + _OrthancPluginCreateDicom params; + params.target = target; + params.json = json; + params.pixelData = pixelData; + params.flags = flags; + + return context->InvokeService(context, _OrthancPluginService_CreateDicom, ¶ms); + } + + + typedef struct + { + OrthancPluginDecodeImageCallback callback; + } _OrthancPluginDecodeImageCallback; + + /** + * @brief Register a callback to handle the decoding of DICOM images. + * + * This function registers a custom callback to decode DICOM images, + * extending the built-in decoder of Orthanc that uses + * DCMTK. Starting with Orthanc 1.7.0, the exact behavior is + * affected by the configuration option + * "BuiltinDecoderTranscoderOrder" of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDecodeImageCallback( + OrthancPluginContext* context, + OrthancPluginDecodeImageCallback callback) + { + _OrthancPluginDecodeImageCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterDecodeImageCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginImage** target; + OrthancPluginPixelFormat format; + uint32_t width; + uint32_t height; + uint32_t pitch; + void* buffer; + const void* constBuffer; + uint32_t bufferSize; + uint32_t frameIndex; + } _OrthancPluginCreateImage; + + + /** + * @brief Create an image. + * + * This function creates an image of given size and format. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param format The format of the pixels. + * @param width The width of the image. + * @param height The height of the image. + * @return The newly allocated image. It must be freed with OrthancPluginFreeImage(). + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImage( + OrthancPluginContext* context, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginCreateImage params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.format = format; + params.width = width; + params.height = height; + + if (context->InvokeService(context, _OrthancPluginService_CreateImage, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Create an image pointing to a memory buffer. + * + * This function creates an image whose content points to a memory + * buffer managed by the plugin. Note that the buffer is directly + * accessed, no memory is allocated and no data is copied. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param format The format of the pixels. + * @param width The width of the image. + * @param height The height of the image. + * @param pitch The pitch of the image (i.e. the number of bytes + * between 2 successive lines of the image in the memory buffer). + * @param buffer The memory buffer. + * @return The newly allocated image. It must be freed with OrthancPluginFreeImage(). + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImageAccessor( + OrthancPluginContext* context, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + void* buffer) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginCreateImage params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.format = format; + params.width = width; + params.height = height; + params.pitch = pitch; + params.buffer = buffer; + + if (context->InvokeService(context, _OrthancPluginService_CreateImageAccessor, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Decode one frame from a DICOM instance. + * + * This function decodes one frame of a DICOM image that is stored + * in a memory buffer. This function will give the same result as + * OrthancPluginUncompressImage() for single-frame DICOM images. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer Pointer to a memory buffer containing the DICOM image. + * @param bufferSize Size of the memory buffer containing the DICOM image. + * @param frameIndex The index of the frame of interest in a multi-frame image. + * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). + * @ingroup Images + * @see OrthancPluginGetInstanceDecodedFrame() + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginDecodeDicomImage( + OrthancPluginContext* context, + const void* buffer, + uint32_t bufferSize, + uint32_t frameIndex) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginCreateImage params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.constBuffer = buffer; + params.bufferSize = bufferSize; + params.frameIndex = frameIndex; + + if (context->InvokeService(context, _OrthancPluginService_DecodeDicomImage, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + + typedef struct + { + char** result; + const void* buffer; + uint32_t size; + } _OrthancPluginComputeHash; + + /** + * @brief Compute an MD5 hash. + * + * This functions computes the MD5 cryptographic hash of the given memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The source memory buffer. + * @param size The size in bytes of the source buffer. + * @return The NULL value in case of error, or a string containing the cryptographic hash. + * This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeMd5( + OrthancPluginContext* context, + const void* buffer, + uint32_t size) + { + char* result; + + _OrthancPluginComputeHash params; + params.result = &result; + params.buffer = buffer; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_ComputeMd5, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Compute a SHA-1 hash. + * + * This functions computes the SHA-1 cryptographic hash of the given memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The source memory buffer. + * @param size The size in bytes of the source buffer. + * @return The NULL value in case of error, or a string containing the cryptographic hash. + * This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeSha1( + OrthancPluginContext* context, + const void* buffer, + uint32_t size) + { + char* result; + + _OrthancPluginComputeHash params; + params.result = &result; + params.buffer = buffer; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_ComputeSha1, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginDictionaryEntry* target; + const char* name; + } _OrthancPluginLookupDictionary; + + /** + * @brief Get information about the given DICOM tag. + * + * This functions makes a lookup in the dictionary of DICOM tags + * that are known to Orthanc, and returns information about this + * tag. The tag can be specified using its human-readable name + * (e.g. "PatientName") or a set of two hexadecimal numbers + * (e.g. "0010-0020"). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Where to store the information about the tag. + * @param name The name of the DICOM tag. + * @return 0 if success, other value if error. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginLookupDictionary( + OrthancPluginContext* context, + OrthancPluginDictionaryEntry* target, + const char* name) + { + _OrthancPluginLookupDictionary params; + params.target = target; + params.name = name; + return context->InvokeService(context, _OrthancPluginService_LookupDictionary, ¶ms); + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + const void* answer; + uint32_t answerSize; + uint32_t headersCount; + const char* const* headersKeys; + const char* const* headersValues; + } _OrthancPluginSendMultipartItem2; + + /** + * @brief Send an item as a part of some HTTP multipart answer, with custom headers. + * + * This function sends an item as a part of some HTTP multipart + * answer that was initiated by OrthancPluginStartMultipartAnswer(). In addition to + * OrthancPluginSendMultipartItem(), this function will set HTTP header associated + * with the item. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param answer Pointer to the memory buffer containing the item. + * @param answerSize Number of bytes of the item. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers. + * @param headersValues Array containing the values of the HTTP headers. + * @return 0 if success, or the error code if failure (this notably happens + * if the connection is closed by the client). + * @see OrthancPluginSendMultipartItem() + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem2( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const void* answer, + uint32_t answerSize, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues) + { + _OrthancPluginSendMultipartItem2 params; + params.output = output; + params.answer = answer; + params.answerSize = answerSize; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + + return context->InvokeService(context, _OrthancPluginService_SendMultipartItem2, ¶ms); + } + + + typedef struct + { + OrthancPluginIncomingHttpRequestFilter callback; + } _OrthancPluginIncomingHttpRequestFilter; + + /** + * @brief Register a callback to filter incoming HTTP requests. + * + * This function registers a custom callback to filter incoming HTTP/REST + * requests received by the HTTP server of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + * @deprecated Please instead use OrthancPluginRegisterIncomingHttpRequestFilter2() + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter( + OrthancPluginContext* context, + OrthancPluginIncomingHttpRequestFilter callback) + { + _OrthancPluginIncomingHttpRequestFilter params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* answerBody; + OrthancPluginMemoryBuffer* answerHeaders; + uint16_t* httpStatus; + OrthancPluginHttpMethod method; + const char* url; + uint32_t headersCount; + const char* const* headersKeys; + const char* const* headersValues; + const void* body; + uint32_t bodySize; + const char* username; + const char* password; + uint32_t timeout; + const char* certificateFile; + const char* certificateKeyFile; + const char* certificateKeyPassword; + uint8_t pkcs11; + } _OrthancPluginCallHttpClient2; + + + + /** + * @brief Issue a HTTP call with full flexibility. + * + * Make a HTTP call to the given URL. The result to the query is + * stored into a newly allocated memory buffer. The HTTP request + * will be done accordingly to the global configuration of Orthanc + * (in particular, the options "HttpProxy", "HttpTimeout", + * "HttpsVerifyPeers", "HttpsCACertificates", and "Pkcs11" will be + * taken into account). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answerBody The target memory buffer (out argument). + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * The value of this argument is ignored if the HTTP method is DELETE. + * @param answerHeaders The target memory buffer for the HTTP headers in the answers (out argument). + * The answer headers are formatted as a JSON object (associative array). + * The buffer must be freed with OrthancPluginFreeMemoryBuffer(). + * This argument can be set to NULL if the plugin has no interest in the HTTP headers. + * @param httpStatus The HTTP status after the execution of the request (out argument). + * @param method HTTP method to be used. + * @param url The URL of interest. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers (can be NULL if no header). + * @param headersValues Array containing the values of the HTTP headers (can be NULL if no header). + * @param username The username (can be NULL if no password protection). + * @param password The password (can be NULL if no password protection). + * @param body The HTTP body for a POST or PUT request. + * @param bodySize The size of the body. + * @param timeout Timeout in seconds (0 for default timeout). + * @param certificateFile Path to the client certificate for HTTPS, in PEM format + * (can be NULL if no client certificate or if not using HTTPS). + * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format + * (can be NULL if no client certificate or if not using HTTPS). + * @param certificateKeyPassword Password to unlock the key of the client certificate + * (can be NULL if no client certificate or if not using HTTPS). + * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginCallPeerApi() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpClient( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* answerBody, + OrthancPluginMemoryBuffer* answerHeaders, + uint16_t* httpStatus, + OrthancPluginHttpMethod method, + const char* url, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + const void* body, + uint32_t bodySize, + const char* username, + const char* password, + uint32_t timeout, + const char* certificateFile, + const char* certificateKeyFile, + const char* certificateKeyPassword, + uint8_t pkcs11) + { + _OrthancPluginCallHttpClient2 params; + memset(¶ms, 0, sizeof(params)); + + params.answerBody = answerBody; + params.answerHeaders = answerHeaders; + params.httpStatus = httpStatus; + params.method = method; + params.url = url; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + params.body = body; + params.bodySize = bodySize; + params.username = username; + params.password = password; + params.timeout = timeout; + params.certificateFile = certificateFile; + params.certificateKeyFile = certificateKeyFile; + params.certificateKeyPassword = certificateKeyPassword; + params.pkcs11 = pkcs11; + + return context->InvokeService(context, _OrthancPluginService_CallHttpClient2, ¶ms); + } + + + /** + * @brief Generate an UUID. + * + * Generate a random GUID/UUID (globally unique identifier). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL in the case of an error, or a newly allocated string + * containing the UUID. This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateUuid( + OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GenerateUuid, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + + typedef struct + { + OrthancPluginFindCallback callback; + } _OrthancPluginFindCallback; + + /** + * @brief Register a callback to handle C-Find requests. + * + * This function registers a callback to handle C-Find SCP requests + * that are not related to modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterFindCallback( + OrthancPluginContext* context, + OrthancPluginFindCallback callback) + { + _OrthancPluginFindCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterFindCallback, ¶ms); + } + + + typedef struct + { + OrthancPluginFindAnswers *answers; + const OrthancPluginFindQuery *query; + const void *dicom; + uint32_t size; + uint32_t index; + uint32_t *resultUint32; + uint16_t *resultGroup; + uint16_t *resultElement; + char **resultString; + } _OrthancPluginFindOperation; + + /** + * @brief Add one answer to some C-Find request. + * + * This function adds one answer (encoded as a DICOM file) to the + * set of answers corresponding to some C-Find SCP request that is + * not related to modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @param dicom The answer to be added, encoded as a DICOM file. + * @param size The size of the DICOM file. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + * @see OrthancPluginCreateDicom() + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginFindAddAnswer( + OrthancPluginContext* context, + OrthancPluginFindAnswers* answers, + const void* dicom, + uint32_t size) + { + _OrthancPluginFindOperation params; + memset(¶ms, 0, sizeof(params)); + params.answers = answers; + params.dicom = dicom; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_FindAddAnswer, ¶ms); + } + + + /** + * @brief Mark the set of C-Find answers as incomplete. + * + * This function marks as incomplete the set of answers + * corresponding to some C-Find SCP request that is not related to + * modality worklists. This must be used if canceling the handling + * of a request when too many answers are to be returned. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginFindMarkIncomplete( + OrthancPluginContext* context, + OrthancPluginFindAnswers* answers) + { + _OrthancPluginFindOperation params; + memset(¶ms, 0, sizeof(params)); + params.answers = answers; + + return context->InvokeService(context, _OrthancPluginService_FindMarkIncomplete, ¶ms); + } + + + + /** + * @brief Get the number of tags in a C-Find query. + * + * This function returns the number of tags that are contained in + * the given C-Find query. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param query The C-Find query. + * @return The number of tags. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFindQuerySize( + OrthancPluginContext* context, + const OrthancPluginFindQuery* query) + { + uint32_t count = 0; + + _OrthancPluginFindOperation params; + memset(¶ms, 0, sizeof(params)); + params.query = query; + params.resultUint32 = &count; + + if (context->InvokeService(context, _OrthancPluginService_GetFindQuerySize, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + + /** + * @brief Get one tag in a C-Find query. + * + * This function returns the group and the element of one DICOM tag + * in the given C-Find query. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param group The group of the tag (output). + * @param element The element of the tag (output). + * @param query The C-Find query. + * @param index The index of the tag of interest. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetFindQueryTag( + OrthancPluginContext* context, + uint16_t* group, + uint16_t* element, + const OrthancPluginFindQuery* query, + uint32_t index) + { + _OrthancPluginFindOperation params; + memset(¶ms, 0, sizeof(params)); + params.query = query; + params.index = index; + params.resultGroup = group; + params.resultElement = element; + + return context->InvokeService(context, _OrthancPluginService_GetFindQueryTag, ¶ms); + } + + + /** + * @brief Get the symbolic name of one tag in a C-Find query. + * + * This function returns the symbolic name of one DICOM tag in the + * given C-Find query. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param query The C-Find query. + * @param index The index of the tag of interest. + * @return The NULL value in case of error, or a string containing the name of the tag. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetFindQueryTagName( + OrthancPluginContext* context, + const OrthancPluginFindQuery* query, + uint32_t index) + { + char* result; + + _OrthancPluginFindOperation params; + memset(¶ms, 0, sizeof(params)); + params.query = query; + params.index = index; + params.resultString = &result; + + if (context->InvokeService(context, _OrthancPluginService_GetFindQueryTagName, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Get the value associated with one tag in a C-Find query. + * + * This function returns the value associated with one tag in the + * given C-Find query. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param query The C-Find query. + * @param index The index of the tag of interest. + * @return The NULL value in case of error, or a string containing the value of the tag. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetFindQueryValue( + OrthancPluginContext* context, + const OrthancPluginFindQuery* query, + uint32_t index) + { + char* result; + + _OrthancPluginFindOperation params; + memset(¶ms, 0, sizeof(params)); + params.query = query; + params.index = index; + params.resultString = &result; + + if (context->InvokeService(context, _OrthancPluginService_GetFindQueryValue, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + + typedef struct + { + OrthancPluginMoveCallback callback; + OrthancPluginGetMoveSize getMoveSize; + OrthancPluginApplyMove applyMove; + OrthancPluginFreeMove freeMove; + } _OrthancPluginMoveCallback; + + /** + * @brief Register a callback to handle C-Move requests. + * + * This function registers a callback to handle C-Move SCP requests. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The main callback. + * @param getMoveSize Callback to read the number of C-Move suboperations. + * @param applyMove Callback to apply one C-Move suboperation. + * @param freeMove Callback to free the C-Move driver. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterMoveCallback( + OrthancPluginContext* context, + OrthancPluginMoveCallback callback, + OrthancPluginGetMoveSize getMoveSize, + OrthancPluginApplyMove applyMove, + OrthancPluginFreeMove freeMove) + { + _OrthancPluginMoveCallback params; + params.callback = callback; + params.getMoveSize = getMoveSize; + params.applyMove = applyMove; + params.freeMove = freeMove; + + return context->InvokeService(context, _OrthancPluginService_RegisterMoveCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginFindMatcher** target; + const void* query; + uint32_t size; + } _OrthancPluginCreateFindMatcher; + + + /** + * @brief Create a C-Find matcher. + * + * This function creates a "matcher" object that can be used to + * check whether a DICOM instance matches a C-Find query. The C-Find + * query must be expressed as a DICOM buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param query The C-Find DICOM query. + * @param size The size of the DICOM query. + * @return The newly allocated matcher. It must be freed with OrthancPluginFreeFindMatcher(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginFindMatcher* OrthancPluginCreateFindMatcher( + OrthancPluginContext* context, + const void* query, + uint32_t size) + { + OrthancPluginFindMatcher* target = NULL; + + _OrthancPluginCreateFindMatcher params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.query = query; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_CreateFindMatcher, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginFindMatcher* matcher; + } _OrthancPluginFreeFindMatcher; + + /** + * @brief Free a C-Find matcher. + * + * This function frees a matcher that was created using OrthancPluginCreateFindMatcher(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param matcher The matcher of interest. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeFindMatcher( + OrthancPluginContext* context, + OrthancPluginFindMatcher* matcher) + { + _OrthancPluginFreeFindMatcher params; + params.matcher = matcher; + + context->InvokeService(context, _OrthancPluginService_FreeFindMatcher, ¶ms); + } + + + typedef struct + { + const OrthancPluginFindMatcher* matcher; + const void* dicom; + uint32_t size; + int32_t* isMatch; + } _OrthancPluginFindMatcherIsMatch; + + /** + * @brief Test whether a DICOM instance matches a C-Find query. + * + * This function checks whether one DICOM instance matches C-Find + * matcher that was previously allocated using + * OrthancPluginCreateFindMatcher(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param matcher The matcher of interest. + * @param dicom The DICOM instance to be matched. + * @param size The size of the DICOM instance. + * @return 1 if the DICOM instance matches the query, 0 otherwise. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginFindMatcherIsMatch( + OrthancPluginContext* context, + const OrthancPluginFindMatcher* matcher, + const void* dicom, + uint32_t size) + { + int32_t isMatch = 0; + + _OrthancPluginFindMatcherIsMatch params; + params.matcher = matcher; + params.dicom = dicom; + params.size = size; + params.isMatch = &isMatch; + + if (context->InvokeService(context, _OrthancPluginService_FindMatcherIsMatch, ¶ms) == OrthancPluginErrorCode_Success) + { + return isMatch; + } + else + { + /* Error: Assume non-match */ + return 0; + } + } + + + typedef struct + { + OrthancPluginIncomingHttpRequestFilter2 callback; + } _OrthancPluginIncomingHttpRequestFilter2; + + /** + * @brief Register a callback to filter incoming HTTP requests. + * + * This function registers a custom callback to filter incoming HTTP/REST + * requests received by the HTTP server of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter2( + OrthancPluginContext* context, + OrthancPluginIncomingHttpRequestFilter2 callback) + { + _OrthancPluginIncomingHttpRequestFilter2 params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter2, ¶ms); + } + + + + typedef struct + { + OrthancPluginPeers** peers; + } _OrthancPluginGetPeers; + + /** + * @brief Return the list of available Orthanc peers. + * + * This function returns the parameters of the Orthanc peers that are known to + * the Orthanc server hosting the plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return NULL if error, or a newly allocated opaque data structure containing the peers. + * This structure must be freed with OrthancPluginFreePeers(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginPeers* OrthancPluginGetPeers( + OrthancPluginContext* context) + { + OrthancPluginPeers* peers = NULL; + + _OrthancPluginGetPeers params; + memset(¶ms, 0, sizeof(params)); + params.peers = &peers; + + if (context->InvokeService(context, _OrthancPluginService_GetPeers, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return peers; + } + } + + + typedef struct + { + OrthancPluginPeers* peers; + } _OrthancPluginFreePeers; + + /** + * @brief Free the list of available Orthanc peers. + * + * This function frees the data structure returned by OrthancPluginGetPeers(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param peers The data structure describing the Orthanc peers. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreePeers( + OrthancPluginContext* context, + OrthancPluginPeers* peers) + { + _OrthancPluginFreePeers params; + params.peers = peers; + + context->InvokeService(context, _OrthancPluginService_FreePeers, ¶ms); + } + + + typedef struct + { + uint32_t* target; + const OrthancPluginPeers* peers; + } _OrthancPluginGetPeersCount; + + /** + * @brief Get the number of Orthanc peers. + * + * This function returns the number of Orthanc peers. + * + * This function is thread-safe: Several threads sharing the same + * OrthancPluginPeers object can simultaneously call this function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param peers The data structure describing the Orthanc peers. + * @result The number of peers. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetPeersCount( + OrthancPluginContext* context, + const OrthancPluginPeers* peers) + { + uint32_t target = 0; + + _OrthancPluginGetPeersCount params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.peers = peers; + + if (context->InvokeService(context, _OrthancPluginService_GetPeersCount, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return target; + } + } + + + typedef struct + { + const char** target; + const OrthancPluginPeers* peers; + uint32_t peerIndex; + const char* userProperty; + } _OrthancPluginGetPeerProperty; + + /** + * @brief Get the symbolic name of an Orthanc peer. + * + * This function returns the symbolic name of the Orthanc peer, + * which corresponds to the key of the "OrthancPeers" configuration + * option of Orthanc. + * + * This function is thread-safe: Several threads sharing the same + * OrthancPluginPeers object can simultaneously call this function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param peers The data structure describing the Orthanc peers. + * @param peerIndex The index of the peer of interest. + * This value must be lower than OrthancPluginGetPeersCount(). + * @result The symbolic name, or NULL in the case of an error. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerName( + OrthancPluginContext* context, + const OrthancPluginPeers* peers, + uint32_t peerIndex) + { + const char* target = NULL; + + _OrthancPluginGetPeerProperty params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.peers = peers; + params.peerIndex = peerIndex; + params.userProperty = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetPeerName, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Get the base URL of an Orthanc peer. + * + * This function returns the base URL to the REST API of some Orthanc peer. + * + * This function is thread-safe: Several threads sharing the same + * OrthancPluginPeers object can simultaneously call this function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param peers The data structure describing the Orthanc peers. + * @param peerIndex The index of the peer of interest. + * This value must be lower than OrthancPluginGetPeersCount(). + * @result The URL, or NULL in the case of an error. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUrl( + OrthancPluginContext* context, + const OrthancPluginPeers* peers, + uint32_t peerIndex) + { + const char* target = NULL; + + _OrthancPluginGetPeerProperty params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.peers = peers; + params.peerIndex = peerIndex; + params.userProperty = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetPeerUrl, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Get some user-defined property of an Orthanc peer. + * + * This function returns some user-defined property of some Orthanc + * peer. An user-defined property is a property that is associated + * with the peer in the Orthanc configuration file, but that is not + * recognized by the Orthanc core. + * + * This function is thread-safe: Several threads sharing the same + * OrthancPluginPeers object can simultaneously call this function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param peers The data structure describing the Orthanc peers. + * @param peerIndex The index of the peer of interest. + * This value must be lower than OrthancPluginGetPeersCount(). + * @param userProperty The user property of interest. + * @result The value of the user property, or NULL if it is not defined. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUserProperty( + OrthancPluginContext* context, + const OrthancPluginPeers* peers, + uint32_t peerIndex, + const char* userProperty) + { + const char* target = NULL; + + _OrthancPluginGetPeerProperty params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.peers = peers; + params.peerIndex = peerIndex; + params.userProperty = userProperty; + + if (context->InvokeService(context, _OrthancPluginService_GetPeerUserProperty, ¶ms) != OrthancPluginErrorCode_Success) + { + /* No such user property */ + return NULL; + } + else + { + return target; + } + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* answerBody; + OrthancPluginMemoryBuffer* answerHeaders; + uint16_t* httpStatus; + const OrthancPluginPeers* peers; + uint32_t peerIndex; + OrthancPluginHttpMethod method; + const char* uri; + uint32_t additionalHeadersCount; + const char* const* additionalHeadersKeys; + const char* const* additionalHeadersValues; + const void* body; + uint32_t bodySize; + uint32_t timeout; + } _OrthancPluginCallPeerApi; + + /** + * @brief Call the REST API of an Orthanc peer. + * + * Make a REST call to the given URI in the REST API of a remote + * Orthanc peer. The result to the query is stored into a newly + * allocated memory buffer. The HTTP request will be done according + * to the "OrthancPeers" configuration option of Orthanc. + * + * This function is thread-safe: Several threads sharing the same + * OrthancPluginPeers object can simultaneously call this function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answerBody The target memory buffer (out argument). + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * The value of this argument is ignored if the HTTP method is DELETE. + * @param answerHeaders The target memory buffer for the HTTP headers in the answers (out argument). + * The answer headers are formatted as a JSON object (associative array). + * The buffer must be freed with OrthancPluginFreeMemoryBuffer(). + * This argument can be set to NULL if the plugin has no interest in the HTTP headers. + * @param httpStatus The HTTP status after the execution of the request (out argument). + * @param peers The data structure describing the Orthanc peers. + * @param peerIndex The index of the peer of interest. + * This value must be lower than OrthancPluginGetPeersCount(). + * @param method HTTP method to be used. + * @param uri The URI of interest in the REST API. + * @param additionalHeadersCount The number of HTTP headers to be added to the + * HTTP headers provided in the global configuration of Orthanc. + * @param additionalHeadersKeys Array containing the keys of the HTTP headers (can be NULL if no header). + * @param additionalHeadersValues Array containing the values of the HTTP headers (can be NULL if no header). + * @param body The HTTP body for a POST or PUT request. + * @param bodySize The size of the body. + * @param timeout Timeout in seconds (0 for default timeout). + * @return 0 if success, or the error code if failure. + * @see OrthancPluginHttpClient() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCallPeerApi( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* answerBody, + OrthancPluginMemoryBuffer* answerHeaders, + uint16_t* httpStatus, + const OrthancPluginPeers* peers, + uint32_t peerIndex, + OrthancPluginHttpMethod method, + const char* uri, + uint32_t additionalHeadersCount, + const char* const* additionalHeadersKeys, + const char* const* additionalHeadersValues, + const void* body, + uint32_t bodySize, + uint32_t timeout) + { + _OrthancPluginCallPeerApi params; + memset(¶ms, 0, sizeof(params)); + + params.answerBody = answerBody; + params.answerHeaders = answerHeaders; + params.httpStatus = httpStatus; + params.peers = peers; + params.peerIndex = peerIndex; + params.method = method; + params.uri = uri; + params.additionalHeadersCount = additionalHeadersCount; + params.additionalHeadersKeys = additionalHeadersKeys; + params.additionalHeadersValues = additionalHeadersValues; + params.body = body; + params.bodySize = bodySize; + params.timeout = timeout; + + return context->InvokeService(context, _OrthancPluginService_CallPeerApi, ¶ms); + } + + + + + + typedef struct + { + OrthancPluginJob** target; + void *job; + OrthancPluginJobFinalize finalize; + const char *type; + OrthancPluginJobGetProgress getProgress; + OrthancPluginJobGetContent getContent; + OrthancPluginJobGetSerialized getSerialized; + OrthancPluginJobStep step; + OrthancPluginJobStop stop; + OrthancPluginJobReset reset; + } _OrthancPluginCreateJob; + + /** + * @brief Create a custom job. + * + * This function creates a custom job to be run by the jobs engine + * of Orthanc. + * + * Orthanc starts one dedicated thread per custom job that is + * running. It is guaranteed that all the callbacks will only be + * called from this single dedicated thread, in mutual exclusion: As + * a consequence, it is *not* mandatory to protect the various + * callbacks by mutexes. + * + * The custom job can nonetheless launch its own processing threads + * on the first call to the "step()" callback, and stop them once + * the "stop()" callback is called. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param job The job to be executed. + * @param finalize The finalization callback. + * @param type The type of the job, provided to the job unserializer. + * See OrthancPluginRegisterJobsUnserializer(). + * @param getProgress The progress callback. + * @param getContent The content callback. + * @param getSerialized The serialization callback. + * @param step The callback to execute the individual steps of the job. + * @param stop The callback that is invoked once the job leaves the "running" state. + * @param reset The callback that is invoked if a stopped job is started again. + * @return The newly allocated job. It must be freed with OrthancPluginFreeJob(), + * as long as it is not submitted with OrthancPluginSubmitJob(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob( + OrthancPluginContext *context, + void *job, + OrthancPluginJobFinalize finalize, + const char *type, + OrthancPluginJobGetProgress getProgress, + OrthancPluginJobGetContent getContent, + OrthancPluginJobGetSerialized getSerialized, + OrthancPluginJobStep step, + OrthancPluginJobStop stop, + OrthancPluginJobReset reset) + { + OrthancPluginJob* target = NULL; + + _OrthancPluginCreateJob params; + memset(¶ms, 0, sizeof(params)); + + params.target = ⌖ + params.job = job; + params.finalize = finalize; + params.type = type; + params.getProgress = getProgress; + params.getContent = getContent; + params.getSerialized = getSerialized; + params.step = step; + params.stop = stop; + params.reset = reset; + + if (context->InvokeService(context, _OrthancPluginService_CreateJob, ¶ms) != OrthancPluginErrorCode_Success || + target == NULL) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginJob* job; + } _OrthancPluginFreeJob; + + /** + * @brief Free a custom job. + * + * This function frees an image that was created with OrthancPluginCreateJob(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param job The job. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeJob( + OrthancPluginContext* context, + OrthancPluginJob* job) + { + _OrthancPluginFreeJob params; + params.job = job; + + context->InvokeService(context, _OrthancPluginService_FreeJob, ¶ms); + } + + + + typedef struct + { + char** resultId; + OrthancPluginJob *job; + int priority; + } _OrthancPluginSubmitJob; + + /** + * @brief Submit a new job to the jobs engine of Orthanc. + * + * This function adds the given job to the pending jobs of + * Orthanc. Orthanc will take take of freeing it by invoking the + * finalization callback provided to OrthancPluginCreateJob(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param job The job, as received by OrthancPluginCreateJob(). + * @param priority The priority of the job. + * @return ID of the newly-submitted job. This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char *OrthancPluginSubmitJob( + OrthancPluginContext *context, + OrthancPluginJob *job, + int priority) + { + char* resultId = NULL; + + _OrthancPluginSubmitJob params; + memset(¶ms, 0, sizeof(params)); + + params.resultId = &resultId; + params.job = job; + params.priority = priority; + + if (context->InvokeService(context, _OrthancPluginService_SubmitJob, ¶ms) != OrthancPluginErrorCode_Success || + resultId == NULL) + { + /* Error */ + return NULL; + } + else + { + return resultId; + } + } + + + + typedef struct + { + OrthancPluginJobsUnserializer unserializer; + } _OrthancPluginJobsUnserializer; + + /** + * @brief Register an unserializer for custom jobs. + * + * This function registers an unserializer that decodes custom jobs + * from a JSON string. This callback is invoked when the jobs engine + * of Orthanc is started (on Orthanc initialization), for each job + * that is stored in the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param unserializer The job unserializer. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterJobsUnserializer( + OrthancPluginContext* context, + OrthancPluginJobsUnserializer unserializer) + { + _OrthancPluginJobsUnserializer params; + params.unserializer = unserializer; + + context->InvokeService(context, _OrthancPluginService_RegisterJobsUnserializer, ¶ms); + } + + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* details; + uint8_t log; + } _OrthancPluginSetHttpErrorDetails; + + /** + * @brief Provide a detailed description for an HTTP error. + * + * This function sets the detailed description associated with an + * HTTP error. This description will be displayed in the "Details" + * field of the JSON body of the HTTP answer. It is only taken into + * consideration if the REST callback returns an error code that is + * different from "OrthancPluginErrorCode_Success", and if the + * "HttpDescribeErrors" configuration option of Orthanc is set to + * "true". + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param details The details of the error message. + * @param log Whether to also write the detailed error to the Orthanc logs. + * @ingroup REST + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpErrorDetails( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* details, + uint8_t log) + { + _OrthancPluginSetHttpErrorDetails params; + params.output = output; + params.details = details; + params.log = log; + context->InvokeService(context, _OrthancPluginService_SetHttpErrorDetails, ¶ms); + } + + + + typedef struct + { + const char** result; + const char* argument; + } _OrthancPluginRetrieveStaticString; + + /** + * @brief Detect the MIME type of a file. + * + * This function returns the MIME type of a file by inspecting its extension. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param path Path to the file. + * @return The MIME type. This is a statically-allocated + * string, do not free it. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginAutodetectMimeType( + OrthancPluginContext* context, + const char* path) + { + const char* result = NULL; + + _OrthancPluginRetrieveStaticString params; + params.result = &result; + params.argument = path; + + if (context->InvokeService(context, _OrthancPluginService_AutodetectMimeType, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + const char* name; + float value; + OrthancPluginMetricsType type; + } _OrthancPluginSetMetricsValue; + + /** + * @brief Set the value of a metrics. + * + * This function sets the value of a metrics to monitor the behavior + * of the plugin through tools such as Prometheus. The values of all + * the metrics are stored within the Orthanc context. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param name The name of the metrics to be set. + * @param value The value of the metrics. + * @param type The type of the metrics. This parameter is only taken into consideration + * the first time this metrics is set. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue( + OrthancPluginContext* context, + const char* name, + float value, + OrthancPluginMetricsType type) + { + _OrthancPluginSetMetricsValue params; + params.name = name; + params.value = value; + params.type = type; + context->InvokeService(context, _OrthancPluginService_SetMetricsValue, ¶ms); + } + + + + typedef struct + { + OrthancPluginRefreshMetricsCallback callback; + } _OrthancPluginRegisterRefreshMetricsCallback; + + /** + * @brief Register a callback to refresh the metrics. + * + * This function registers a callback to refresh the metrics. The + * callback must make calls to OrthancPluginSetMetricsValue(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback function to handle the refresh. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRefreshMetricsCallback( + OrthancPluginContext* context, + OrthancPluginRefreshMetricsCallback callback) + { + _OrthancPluginRegisterRefreshMetricsCallback params; + params.callback = callback; + context->InvokeService(context, _OrthancPluginService_RegisterRefreshMetricsCallback, ¶ms); + } + + + + + typedef struct + { + char** target; + const void* dicom; + uint32_t dicomSize; + OrthancPluginDicomWebBinaryCallback callback; + } _OrthancPluginEncodeDicomWeb; + + /** + * @brief Convert a DICOM instance to DICOMweb JSON. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb JSON representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @see OrthancPluginCreateDicom() + * @return The NULL value in case of error, or the JSON document. This string must + * be freed by OrthancPluginFreeString(). + * @deprecated OrthancPluginEncodeDicomWebJson2() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback callback) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb XML. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb XML representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @return The NULL value in case of error, or the XML document. This string must + * be freed by OrthancPluginFreeString(). + * @see OrthancPluginCreateDicom() + * @deprecated OrthancPluginEncodeDicomWebXml2() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback callback) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + + typedef struct + { + char** target; + const void* dicom; + uint32_t dicomSize; + OrthancPluginDicomWebBinaryCallback2 callback; + void* payload; + } _OrthancPluginEncodeDicomWeb2; + + /** + * @brief Convert a DICOM instance to DICOMweb JSON. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb JSON representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the JSON document. This string must + * be freed by OrthancPluginFreeString(). + * @see OrthancPluginCreateDicom() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson2( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb2 params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + params.payload = payload; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson2, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb XML. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb XML representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the XML document. This string must + * be freed by OrthancPluginFreeString(). + * @see OrthancPluginCreateDicom() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb2 params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + params.payload = payload; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml2, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Callback executed when a HTTP header is received during a chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, as soon as it + * receives one HTTP header from the answer of the remote HTTP + * server. + * + * @see OrthancPluginChunkedHttpClient() + * @param answer The user payload, as provided by the calling plugin. + * @param key The key of the HTTP header. + * @param value The value of the HTTP header. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) ( + void* answer, + const char* key, + const char* value); + + + /** + * @brief Callback executed when an answer chunk is received during a chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, as soon as it + * receives one data chunk from the answer of the remote HTTP + * server. + * + * @see OrthancPluginChunkedHttpClient() + * @param answer The user payload, as provided by the calling plugin. + * @param data The content of the data chunk. + * @param size The size of the data chunk. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) ( + void* answer, + const void* data, + uint32_t size); + + + /** + * @brief Callback to know whether the request body is entirely read during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. The plugin must answer "1" as + * soon as the body is entirely read: The "request" data structure + * must act as an iterator. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return "1" if the body is over, or "0" if there is still data to be read. + * @ingroup Toolbox + **/ + typedef uint8_t (*OrthancPluginChunkedClientRequestIsDone) (void* request); + + + /** + * @brief Callback to advance in the request body during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. This function asks the plugin + * to advance to the next chunk of data of the request body: The + * "request" data structure must act as an iterator. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientRequestNext) (void* request); + + + /** + * @brief Callback to read the current chunk of the request body during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. The plugin must provide the + * content of the current chunk of data of the request body. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return The content of the current request chunk. + * @ingroup Toolbox + **/ + typedef const void* (*OrthancPluginChunkedClientRequestGetChunkData) (void* request); + + + /** + * @brief Callback to read the size of the current request chunk during a chunked transfer + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP client during a chunked HTTP transfer, while reading + * the body of a POST or PUT request. The plugin must provide the + * size of the current chunk of data of the request body. + * + * @see OrthancPluginChunkedHttpClient() + * @param request The user payload, as provided by the calling plugin. + * @return The size of the current request chunk. + * @ingroup Toolbox + **/ + typedef uint32_t (*OrthancPluginChunkedClientRequestGetChunkSize) (void* request); + + + typedef struct + { + void* answer; + OrthancPluginChunkedClientAnswerAddChunk answerAddChunk; + OrthancPluginChunkedClientAnswerAddHeader answerAddHeader; + uint16_t* httpStatus; + OrthancPluginHttpMethod method; + const char* url; + uint32_t headersCount; + const char* const* headersKeys; + const char* const* headersValues; + void* request; + OrthancPluginChunkedClientRequestIsDone requestIsDone; + OrthancPluginChunkedClientRequestGetChunkData requestChunkData; + OrthancPluginChunkedClientRequestGetChunkSize requestChunkSize; + OrthancPluginChunkedClientRequestNext requestNext; + const char* username; + const char* password; + uint32_t timeout; + const char* certificateFile; + const char* certificateKeyFile; + const char* certificateKeyPassword; + uint8_t pkcs11; + } _OrthancPluginChunkedHttpClient; + + + /** + * @brief Issue a HTTP call, using chunked HTTP transfers. + * + * Make a HTTP call to the given URL using chunked HTTP + * transfers. The request body is provided as an iterator over data + * chunks. The answer is provided as a sequence of function calls + * with the individual HTTP headers and answer chunks. + * + * Contrarily to OrthancPluginHttpClient() that entirely stores the + * request body and the answer body in memory buffers, this function + * uses chunked HTTP transfers. This results in a lower memory + * consumption. Pay attention to the fact that Orthanc servers with + * version <= 1.5.6 do not support chunked transfers: You must use + * OrthancPluginHttpClient() if contacting such older servers. + * + * The HTTP request will be done accordingly to the global + * configuration of Orthanc (in particular, the options "HttpProxy", + * "HttpTimeout", "HttpsVerifyPeers", "HttpsCACertificates", and + * "Pkcs11" will be taken into account). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answer The user payload for the answer body. It will be provided to the callbacks for the answer. + * @param answerAddChunk Callback function to report a data chunk from the answer body. + * @param answerAddHeader Callback function to report an HTTP header sent by the remote server. + * @param httpStatus The HTTP status after the execution of the request (out argument). + * @param method HTTP method to be used. + * @param url The URL of interest. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers (can be NULL if no header). + * @param headersValues Array containing the values of the HTTP headers (can be NULL if no header). + * @param request The user payload containing the request body, and acting as an iterator. + * It will be provided to the callbacks for the request. + * @param requestIsDone Callback function to tell whether the request body is entirely read. + * @param requestChunkData Callback function to get the content of the current data chunk of the request body. + * @param requestChunkSize Callback function to get the size of the current data chunk of the request body. + * @param requestNext Callback function to advance to the next data chunk of the request body. + * @param username The username (can be NULL if no password protection). + * @param password The password (can be NULL if no password protection). + * @param timeout Timeout in seconds (0 for default timeout). + * @param certificateFile Path to the client certificate for HTTPS, in PEM format + * (can be NULL if no client certificate or if not using HTTPS). + * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format + * (can be NULL if no client certificate or if not using HTTPS). + * @param certificateKeyPassword Password to unlock the key of the client certificate + * (can be NULL if no client certificate or if not using HTTPS). + * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginHttpClient() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginChunkedHttpClient( + OrthancPluginContext* context, + void* answer, + OrthancPluginChunkedClientAnswerAddChunk answerAddChunk, + OrthancPluginChunkedClientAnswerAddHeader answerAddHeader, + uint16_t* httpStatus, + OrthancPluginHttpMethod method, + const char* url, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + void* request, + OrthancPluginChunkedClientRequestIsDone requestIsDone, + OrthancPluginChunkedClientRequestGetChunkData requestChunkData, + OrthancPluginChunkedClientRequestGetChunkSize requestChunkSize, + OrthancPluginChunkedClientRequestNext requestNext, + const char* username, + const char* password, + uint32_t timeout, + const char* certificateFile, + const char* certificateKeyFile, + const char* certificateKeyPassword, + uint8_t pkcs11) + { + _OrthancPluginChunkedHttpClient params; + memset(¶ms, 0, sizeof(params)); + + /* In common with OrthancPluginHttpClient() */ + params.httpStatus = httpStatus; + params.method = method; + params.url = url; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + params.username = username; + params.password = password; + params.timeout = timeout; + params.certificateFile = certificateFile; + params.certificateKeyFile = certificateKeyFile; + params.certificateKeyPassword = certificateKeyPassword; + params.pkcs11 = pkcs11; + + /* For chunked body/answer */ + params.answer = answer; + params.answerAddChunk = answerAddChunk; + params.answerAddHeader = answerAddHeader; + params.request = request; + params.requestIsDone = requestIsDone; + params.requestChunkData = requestChunkData; + params.requestChunkSize = requestChunkSize; + params.requestNext = requestNext; + + return context->InvokeService(context, _OrthancPluginService_ChunkedHttpClient, ¶ms); + } + + + + /** + * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader; + + + + /** + * @brief Callback to create a reader to handle incoming chunked HTTP transfers. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This + * callback is only invoked if the HTTP method is POST or PUT. The + * callback must create an user-specific "reader" object that will + * be fed with the body of the incoming body. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader Memory location that must be filled with the newly-created reader. + * @param url The URI that is accessed. + * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used. + * @return 0 if success, or the error code if failure. + **/ + typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) ( + OrthancPluginServerChunkedRequestReader** reader, + const char* url, + const OrthancPluginHttpRequest* request); + + + /** + * @brief Callback invoked whenever a new data chunk is available during a chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This callback + * is invoked as soon as a new data chunk is available for the request body. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback. + * @param data The content of the data chunk. + * @param size The size of the data chunk. + * @return 0 if success, or the error code if failure. + **/ + typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderAddChunk) ( + OrthancPluginServerChunkedRequestReader* reader, + const void* data, + uint32_t size); + + + /** + * @brief Callback invoked whenever the request body is entirely received. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This + * callback is invoked as soon as the full body of the HTTP request + * is available. The plugin can then send its answer thanks to the + * provided "output" object. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback. + * @param output The HTTP connection to the client application. + * @return 0 if success, or the error code if failure. + **/ + typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderExecute) ( + OrthancPluginServerChunkedRequestReader* reader, + OrthancPluginRestOutput* output); + + + /** + * @brief Callback invoked to release the resources associated with an incoming HTTP chunked transfer. + * + * Signature of a callback function that is called by Orthanc acting + * as a HTTP server that supports chunked HTTP transfers. This + * callback is invoked to release all the resources allocated by the + * given reader. Note that this function might be invoked even if + * the entire body was not read, to deal with client error or + * disconnection. + * + * @see OrthancPluginRegisterChunkedRestCallback() + * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback. + **/ + typedef void (*OrthancPluginServerChunkedRequestReaderFinalize) ( + OrthancPluginServerChunkedRequestReader* reader); + + typedef struct + { + const char* pathRegularExpression; + OrthancPluginRestCallback getHandler; + OrthancPluginServerChunkedRequestReaderFactory postHandler; + OrthancPluginRestCallback deleteHandler; + OrthancPluginServerChunkedRequestReaderFactory putHandler; + OrthancPluginServerChunkedRequestReaderAddChunk addChunk; + OrthancPluginServerChunkedRequestReaderExecute execute; + OrthancPluginServerChunkedRequestReaderFinalize finalize; + } _OrthancPluginChunkedRestCallback; + + + /** + * @brief Register a REST callback to handle chunked HTTP transfers. + * + * This function registers a REST callback against a regular + * expression for a URI. This function must be called during the + * initialization of the plugin, i.e. inside the + * OrthancPluginInitialize() public function. + * + * Contrarily to OrthancPluginRegisterRestCallback(), the callbacks + * will NOT be invoked in mutual exclusion, so it is up to the + * plugin to implement the required locking mechanisms. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param pathRegularExpression Regular expression for the URI. May contain groups. + * @param getHandler The callback function to handle REST calls using the GET HTTP method. + * @param postHandler The callback function to handle REST calls using the POST HTTP method. + * @param deleteHandler The callback function to handle REST calls using the DELETE HTTP method. + * @param putHandler The callback function to handle REST calls using the PUT HTTP method. + * @param addChunk The callback invoked when a new chunk is available for the request body of a POST or PUT call. + * @param execute The callback invoked once the entire body of a POST or PUT call is read. + * @param finalize The callback invoked to release the resources associated with a POST or PUT call. + * @see OrthancPluginRegisterRestCallbackNoLock() + * + * @note + * The regular expression is case sensitive and must follow the + * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html). + * + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterChunkedRestCallback( + OrthancPluginContext* context, + const char* pathRegularExpression, + OrthancPluginRestCallback getHandler, + OrthancPluginServerChunkedRequestReaderFactory postHandler, + OrthancPluginRestCallback deleteHandler, + OrthancPluginServerChunkedRequestReaderFactory putHandler, + OrthancPluginServerChunkedRequestReaderAddChunk addChunk, + OrthancPluginServerChunkedRequestReaderExecute execute, + OrthancPluginServerChunkedRequestReaderFinalize finalize) + { + _OrthancPluginChunkedRestCallback params; + params.pathRegularExpression = pathRegularExpression; + params.getHandler = getHandler; + params.postHandler = postHandler; + params.deleteHandler = deleteHandler; + params.putHandler = putHandler; + params.addChunk = addChunk; + params.execute = execute; + params.finalize = finalize; + + context->InvokeService(context, _OrthancPluginService_RegisterChunkedRestCallback, ¶ms); + } + + + + + + typedef struct + { + char** result; + uint16_t group; + uint16_t element; + const char* privateCreator; + } _OrthancPluginGetTagName; + + /** + * @brief Returns the symbolic name of a DICOM tag. + * + * This function makes a lookup to the dictionary of DICOM tags that + * are known to Orthanc, and returns the symbolic name of a DICOM tag. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param group The group of the tag. + * @param element The element of the tag. + * @param privateCreator For private tags, the name of the private creator (can be NULL). + * @return NULL in the case of an error, or a newly allocated string + * containing the path. This string must be freed by + * OrthancPluginFreeString(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetTagName( + OrthancPluginContext* context, + uint16_t group, + uint16_t element, + const char* privateCreator) + { + char* result; + + _OrthancPluginGetTagName params; + params.result = &result; + params.group = group; + params.element = element; + params.privateCreator = privateCreator; + + if (context->InvokeService(context, _OrthancPluginService_GetTagName, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + /** + * @brief Callback executed by the storage commitment SCP. + * + * Signature of a factory function that creates an object to handle + * one incoming storage commitment request. + * + * @remark The factory receives the list of the SOP class/instance + * UIDs of interest to the remote storage commitment SCU. This gives + * the factory the possibility to start some prefetch process + * upfront in the background, before the handler object is actually + * queried about the status of these DICOM instances. + * + * @param handler Output variable where the factory puts the handler object it created. + * @param jobId ID of the Orthanc job that is responsible for handling + * the storage commitment request. This job will successively look for the + * status of all the individual queried DICOM instances. + * @param transactionUid UID of the storage commitment transaction + * provided by the storage commitment SCU. It contains the value of the + * (0008,1195) DICOM tag. + * @param sopClassUids Array of the SOP class UIDs (0008,0016) that are queried by the SCU. + * @param sopInstanceUids Array of the SOP instance UIDs (0008,0018) that are queried by the SCU. + * @param countInstances Number of DICOM instances that are queried. This is the size + * of the `sopClassUids` and `sopInstanceUids` arrays. + * @param remoteAet The AET of the storage commitment SCU. + * @param calledAet The AET used by the SCU to contact the storage commitment SCP (i.e. Orthanc). + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentFactory) ( + void** handler /* out */, + const char* jobId, + const char* transactionUid, + const char* const* sopClassUids, + const char* const* sopInstanceUids, + uint32_t countInstances, + const char* remoteAet, + const char* calledAet); + + + /** + * @brief Callback to free one storage commitment SCP handler. + * + * Signature of a callback function that releases the resources + * allocated by the factory of the storage commitment SCP. The + * handler is the return value of a previous call to the + * OrthancPluginStorageCommitmentFactory() callback. + * + * @param handler The handler object to be destructed. + * @ingroup DicomCallbacks + **/ + typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler); + + + /** + * @brief Callback to get the status of one DICOM instance in the + * storage commitment SCP. + * + * Signature of a callback function that is successively invoked for + * each DICOM instance that is queried by the remote storage + * commitment SCU. The function must be tought of as a method of + * the handler object that was created by a previous call to the + * OrthancPluginStorageCommitmentFactory() callback. After each call + * to this method, the progress of the associated Orthanc job is + * updated. + * + * @param target Output variable where to put the status for the queried instance. + * @param handler The handler object associated with this storage commitment request. + * @param sopClassUid The SOP class UID (0008,0016) of interest. + * @param sopInstanceUid The SOP instance UID (0008,0018) of interest. + * @ingroup DicomCallbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) ( + OrthancPluginStorageCommitmentFailureReason* target, + void* handler, + const char* sopClassUid, + const char* sopInstanceUid); + + + typedef struct + { + OrthancPluginStorageCommitmentFactory factory; + OrthancPluginStorageCommitmentDestructor destructor; + OrthancPluginStorageCommitmentLookup lookup; + } _OrthancPluginRegisterStorageCommitmentScpCallback; + + /** + * @brief Register a callback to handle incoming requests to the storage commitment SCP. + * + * This function registers a callback to handle storage commitment SCP requests. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param factory Factory function that creates the handler object + * for incoming storage commitment requests. + * @param destructor Destructor function to destroy the handler object. + * @param lookup Callback function to get the status of one DICOM instance. + * @return 0 if success, other value if error. + * @ingroup DicomCallbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterStorageCommitmentScpCallback( + OrthancPluginContext* context, + OrthancPluginStorageCommitmentFactory factory, + OrthancPluginStorageCommitmentDestructor destructor, + OrthancPluginStorageCommitmentLookup lookup) + { + _OrthancPluginRegisterStorageCommitmentScpCallback params; + params.factory = factory; + params.destructor = destructor; + params.lookup = lookup; + return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, ¶ms); + } + + + + /** + * @brief Callback to filter incoming DICOM instances received by Orthanc. + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a new DICOM instance (e.g. through REST API or + * DICOM protocol), and that answers whether this DICOM instance + * should be accepted or discarded by Orthanc. + * + * Note that the metadata information is not available + * (i.e. GetInstanceMetadata() should not be used on "instance"). + * + * @warning Your callback function will be called synchronously with + * the core of Orthanc. This implies that deadlocks might emerge if + * you call other core primitives of Orthanc in your callback (such + * deadlocks are particularly visible in the presence of other plugins + * or Lua scripts). It is thus strongly advised to avoid any call to + * the REST API of Orthanc in the callback. If you have to call + * other primitives of Orthanc, you should make these calls in a + * separate thread, passing the pending events to be processed + * through a message queue. + * + * @param instance The received DICOM instance. + * @return 0 to discard the instance, 1 to store the instance, -1 if error. + * @ingroup Callbacks + **/ + typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) ( + const OrthancPluginDicomInstance* instance); + + + typedef struct + { + OrthancPluginIncomingDicomInstanceFilter callback; + } _OrthancPluginIncomingDicomInstanceFilter; + + /** + * @brief Register a callback to filter incoming DICOM instances. + * + * This function registers a custom callback to filter incoming + * DICOM instances received by Orthanc (either through the REST API + * or through the DICOM protocol). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingDicomInstanceFilter( + OrthancPluginContext* context, + OrthancPluginIncomingDicomInstanceFilter callback) + { + _OrthancPluginIncomingDicomInstanceFilter params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterIncomingDicomInstanceFilter, ¶ms); + } + + + /** + * @brief Callback to filter incoming DICOM instances received by + * Orthanc through C-STORE. + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a new DICOM instance using DICOM C-STORE, and + * that answers whether this DICOM instance should be accepted or + * discarded by Orthanc. If the instance is discarded, the callback + * can specify the DIMSE error code answered by the Orthanc C-STORE + * SCP. + * + * Note that the metadata information is not available + * (i.e. GetInstanceMetadata() should not be used on "instance"). + * + * @warning Your callback function will be called synchronously with + * the core of Orthanc. This implies that deadlocks might emerge if + * you call other core primitives of Orthanc in your callback (such + * deadlocks are particularly visible in the presence of other plugins + * or Lua scripts). It is thus strongly advised to avoid any call to + * the REST API of Orthanc in the callback. If you have to call + * other primitives of Orthanc, you should make these calls in a + * separate thread, passing the pending events to be processed + * through a message queue. + * + * @param dimseStatus If the DICOM instance is discarded, + * DIMSE status to be sent by the C-STORE SCP of Orthanc + * @param instance The received DICOM instance. + * @return 0 to discard the instance, 1 to store the instance, -1 if error. + * @ingroup Callbacks + **/ + typedef int32_t (*OrthancPluginIncomingCStoreInstanceFilter) ( + uint16_t* dimseStatus /* out */, + const OrthancPluginDicomInstance* instance); + + + typedef struct + { + OrthancPluginIncomingCStoreInstanceFilter callback; + } _OrthancPluginIncomingCStoreInstanceFilter; + + /** + * @brief Register a callback to filter incoming DICOM instances + * received by Orthanc through C-STORE. + * + * This function registers a custom callback to filter incoming + * DICOM instances received by Orthanc through the DICOM protocol. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingCStoreInstanceFilter( + OrthancPluginContext* context, + OrthancPluginIncomingCStoreInstanceFilter callback) + { + _OrthancPluginIncomingCStoreInstanceFilter params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterIncomingCStoreInstanceFilter, ¶ms); + } + + /** + * @brief Callback to keep/discard/modify a DICOM instance received + * by Orthanc from any source (C-STORE or REST API) + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a new DICOM instance (through DICOM protocol or + * REST API), and that specifies an action to be applied to this + * newly received instance. The instance can be kept as it is, can + * be modified by the plugin, or can be discarded. + * + * This callback is invoked immediately after reception, i.e. before + * transcoding and before filtering + * (cf. OrthancPluginRegisterIncomingDicomInstanceFilter()). + * + * @warning Your callback function will be called synchronously with + * the core of Orthanc. This implies that deadlocks might emerge if + * you call other core primitives of Orthanc in your callback (such + * deadlocks are particularly visible in the presence of other plugins + * or Lua scripts). It is thus strongly advised to avoid any call to + * the REST API of Orthanc in the callback. If you have to call + * other primitives of Orthanc, you should make these calls in a + * separate thread, passing the pending events to be processed + * through a message queue. + * + * @param modifiedDicomBuffer A buffer containing the modified DICOM (output). + * This buffer must be allocated using OrthancPluginCreateMemoryBuffer64() + * and will be freed by the Orthanc core. + * @param receivedDicomBuffer A buffer containing the received DICOM (input). + * @param receivedDicomBufferSize The size of the received DICOM (input). + * @param origin The origin of the DICOM instance (input). + * @return `OrthancPluginReceivedInstanceAction_KeepAsIs` to accept the instance as is,
+ * `OrthancPluginReceivedInstanceAction_Modify` to store the modified DICOM contained in `modifiedDicomBuffer`,
+ * `OrthancPluginReceivedInstanceAction_Discard` to tell Orthanc to discard the instance. + * @ingroup Callbacks + **/ + typedef OrthancPluginReceivedInstanceAction (*OrthancPluginReceivedInstanceCallback) ( + OrthancPluginMemoryBuffer64* modifiedDicomBuffer, + const void* receivedDicomBuffer, + uint64_t receivedDicomBufferSize, + OrthancPluginInstanceOrigin origin); + + + typedef struct + { + OrthancPluginReceivedInstanceCallback callback; + } _OrthancPluginReceivedInstanceCallback; + + /** + * @brief Register a callback to keep/discard/modify a DICOM instance received + * by Orthanc from any source (C-STORE or REST API) + * + * This function registers a custom callback to keep/discard/modify + * incoming DICOM instances received by Orthanc from any source + * (C-STORE or REST API). + * + * @warning Contrarily to + * OrthancPluginRegisterIncomingCStoreInstanceFilter() and + * OrthancPluginRegisterIncomingDicomInstanceFilter() that can be + * called by multiple plugins, + * OrthancPluginRegisterReceivedInstanceCallback() can only be used + * by one single plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterReceivedInstanceCallback( + OrthancPluginContext* context, + OrthancPluginReceivedInstanceCallback callback) + { + _OrthancPluginReceivedInstanceCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterReceivedInstanceCallback, ¶ms); + } + + /** + * @brief Get the transfer syntax of a DICOM file. + * + * This function returns a pointer to a newly created string that + * contains the transfer syntax UID of the DICOM instance. The empty + * string might be returned if this information is unknown. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The NULL value in case of error, or a string containing the + * transfer syntax UID. This string must be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + char* result; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultStringToFree = &result; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceTransferSyntaxUid, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Check whether the DICOM file has pixel data. + * + * This function returns a Boolean value indicating whether the + * DICOM instance contains the pixel data (7FE0,0010) tag. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return "1" if the DICOM instance contains pixel data, or "0" if + * the tag is missing, or "-1" in the case of an error. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + int64_t hasPixelData; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultInt64 = &hasPixelData; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_HasInstancePixelData, ¶ms) != OrthancPluginErrorCode_Success || + hasPixelData < 0 || + hasPixelData > 1) + { + /* Error */ + return -1; + } + else + { + return (hasPixelData != 0); + } + } + + + + + + + typedef struct + { + OrthancPluginDicomInstance** target; + const void* buffer; + uint32_t size; + const char* transferSyntax; + } _OrthancPluginCreateDicomInstance; + + /** + * @brief Parse a DICOM instance. + * + * This function parses a memory buffer that contains a DICOM + * file. The function returns a new pointer to a data structure that + * is managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM instance. + * @param size The size of the memory buffer. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance( + OrthancPluginContext* context, + const void* buffer, + uint32_t size) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginCreateDicomInstance params; + params.target = ⌖ + params.buffer = buffer; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_CreateDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + typedef struct + { + OrthancPluginDicomInstance* dicom; + } _OrthancPluginFreeDicomInstance; + + /** + * @brief Free a DICOM instance. + * + * This function frees a DICOM instance that was parsed using + * OrthancPluginCreateDicomInstance(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom The DICOM instance. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeDicomInstance( + OrthancPluginContext* context, + OrthancPluginDicomInstance* dicom) + { + _OrthancPluginFreeDicomInstance params; + params.dicom = dicom; + + context->InvokeService(context, _OrthancPluginService_FreeDicomInstance, ¶ms); + } + + + typedef struct + { + uint32_t* targetUint32; + OrthancPluginMemoryBuffer* targetBuffer; + OrthancPluginImage** targetImage; + char** targetStringToFree; + const OrthancPluginDicomInstance* instance; + uint32_t frameIndex; + OrthancPluginDicomToJsonFormat format; + OrthancPluginDicomToJsonFlags flags; + uint32_t maxStringLength; + OrthancPluginDicomWebBinaryCallback2 dicomWebCallback; + void* dicomWebPayload; + } _OrthancPluginAccessDicomInstance2; + + /** + * @brief Get the number of frames in a DICOM instance. + * + * This function returns the number of frames that are part of a + * DICOM image managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The number of frames (will be zero in the case of an error). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + uint32_t count; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetUint32 = &count; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceFramesCount, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + + /** + * @brief Get the raw content of a frame in a DICOM instance. + * + * This function returns a memory buffer containing the raw content + * of a frame in a DICOM instance that is managed by the Orthanc + * core. This is notably useful for compressed transfer syntaxes, as + * it gives access to the embedded files (such as JPEG, JPEG-LS or + * JPEG2k). The Orthanc core transparently reassembles the fragments + * to extract the raw frame. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param instance The instance of interest. + * @param frameIndex The index of the frame of interest. + * @return 0 if success, or the error code if failure. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginDicomInstance* instance, + uint32_t frameIndex) + { + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetBuffer = target; + params.instance = instance; + params.frameIndex = frameIndex; + + return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, ¶ms); + } + + + /** + * @brief Decode one frame from a DICOM instance. + * + * This function decodes one frame of a DICOM image that is managed + * by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @param frameIndex The index of the frame of interest. + * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + uint32_t frameIndex) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetImage = ⌖ + params.instance = instance; + params.frameIndex = frameIndex; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDecodedFrame, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Parse and transcode a DICOM instance. + * + * This function parses a memory buffer that contains a DICOM file, + * then transcodes it to the given transfer syntax. The function + * returns a new pointer to a data structure that is managed by the + * Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM instance. + * @param size The size of the memory buffer. + * @param transferSyntax The transfer syntax UID for the transcoding. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance( + OrthancPluginContext* context, + const void* buffer, + uint32_t size, + const char* transferSyntax) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginCreateDicomInstance params; + params.target = ⌖ + params.buffer = buffer; + params.size = size; + params.transferSyntax = transferSyntax; + + if (context->InvokeService(context, _OrthancPluginService_TranscodeDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + /** + * @brief Writes a DICOM instance to a memory buffer. + * + * This function returns a memory buffer containing the + * serialization of a DICOM instance that is managed by the Orthanc + * core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param instance The instance of interest. + * @return 0 if success, or the error code if failure. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginDicomInstance* instance) + { + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetBuffer = target; + params.instance = instance; + + return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, ¶ms); + } + + + /** + * @brief Format a DICOM memory buffer as a JSON string. + * + * This function takes as DICOM instance managed by the Orthanc + * core, and outputs a JSON string representing the tags of this + * DICOM file. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param format The output format. + * @param flags Flags governing the output. + * @param maxStringLength The maximum length of a field. Too long fields will + * be output as "null". The 0 value means no maximum length. + * @return The NULL value if the case of an error, or the JSON + * string. This string must be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + * @see OrthancPluginDicomBufferToJson + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + char* result = NULL; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetStringToFree = &result; + params.instance = instance; + params.format = format; + params.flags = flags; + params.maxStringLength = maxStringLength; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceAdvancedJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb JSON. + * + * This function converts a DICOM instance that is managed by the + * Orthanc core, into its DICOMweb JSON representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the JSON document. This string must + * be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + params.targetStringToFree = ⌖ + params.instance = instance; + params.dicomWebCallback = callback; + params.dicomWebPayload = payload; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb XML. + * + * This function converts a DICOM instance that is managed by the + * Orthanc core, into its DICOMweb XML representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the XML document. This string must + * be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + params.targetStringToFree = ⌖ + params.instance = instance; + params.dicomWebCallback = callback; + params.dicomWebPayload = payload; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebXml, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Signature of a callback function to transcode a DICOM instance. + * @param transcoded Target memory buffer. It must be allocated by the + * plugin using OrthancPluginCreateMemoryBuffer(). + * @param buffer Memory buffer containing the source DICOM instance. + * @param size Size of the source memory buffer. + * @param allowedSyntaxes A C array of possible transfer syntaxes UIDs for the + * result of the transcoding. The plugin must choose by itself the + * transfer syntax that will be used for the resulting DICOM image. + * @param countSyntaxes The number of transfer syntaxes that are contained + * in the "allowedSyntaxes" array. + * @param allowNewSopInstanceUid Whether the transcoding plugin can select + * a transfer syntax that will change the SOP instance UID (or, in other + * terms, whether the plugin can transcode using lossy compression). + * @return 0 if success (i.e. image successfully transcoded and stored into + * "transcoded"), or the error code if failure. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginTranscoderCallback) ( + OrthancPluginMemoryBuffer* transcoded /* out */, + const void* buffer, + uint64_t size, + const char* const* allowedSyntaxes, + uint32_t countSyntaxes, + uint8_t allowNewSopInstanceUid); + + + typedef struct + { + OrthancPluginTranscoderCallback callback; + } _OrthancPluginTranscoderCallback; + + /** + * @brief Register a callback to handle the transcoding of DICOM images. + * + * This function registers a custom callback to transcode DICOM + * images, extending the built-in transcoder of Orthanc that uses + * DCMTK. The exact behavior is affected by the configuration option + * "BuiltinDecoderTranscoderOrder" of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterTranscoderCallback( + OrthancPluginContext* context, + OrthancPluginTranscoderCallback callback) + { + _OrthancPluginTranscoderCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterTranscoderCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + uint32_t size; + } _OrthancPluginCreateMemoryBuffer; + + /** + * @brief Create a 32-bit memory buffer. + * + * This function creates a memory buffer that is managed by the + * Orthanc core. The main use case of this function is for plugins + * that act as DICOM transcoders. + * + * Your plugin should never call "free()" on the resulting memory + * buffer, as the C library that is used by the plugin is in general + * not the same as the one used by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param size Size of the memory buffer to be created. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + uint32_t size) + { + _OrthancPluginCreateMemoryBuffer params; + params.target = target; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer, ¶ms); + } + + + /** + * @brief Generate a token to grant full access to the REST API of Orthanc. + * + * 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. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return The authorization token, or NULL value in the case of an error. + * This string must be freed by OrthancPluginFreeString(). + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateRestApiAuthorizationToken( + OrthancPluginContext* context) + { + char* result; + + _OrthancPluginRetrieveDynamicString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GenerateRestApiAuthorizationToken, + ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + + typedef struct + { + OrthancPluginMemoryBuffer64* target; + uint64_t size; + } _OrthancPluginCreateMemoryBuffer64; + + /** + * @brief Create a 64-bit memory buffer. + * + * This function creates a 64-bit memory buffer that is managed by + * the Orthanc core. The main use case of this function is for + * plugins that read files from the storage area. + * + * Your plugin should never call "free()" on the resulting memory + * buffer, as the C library that is used by the plugin is in general + * not the same as the one used by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param size Size of the memory buffer to be created. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer64( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer64* target, + uint64_t size) + { + _OrthancPluginCreateMemoryBuffer64 params; + params.target = target; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer64, ¶ms); + } + + + typedef struct + { + OrthancPluginStorageCreate create; + OrthancPluginStorageReadWhole readWhole; + OrthancPluginStorageReadRange readRange; + OrthancPluginStorageRemove remove; + } _OrthancPluginRegisterStorageArea2; + + /** + * @brief Register a custom storage area, with support for range request. + * + * 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 readWhole The callback function to read a whole file from the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * 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 + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea2( + OrthancPluginContext* context, + OrthancPluginStorageCreate create, + OrthancPluginStorageReadWhole readWhole, + OrthancPluginStorageReadRange readRange, + OrthancPluginStorageRemove remove) + { + _OrthancPluginRegisterStorageArea2 params; + params.create = create; + params.readWhole = readWhole; + params.readRange = readRange; + params.remove = remove; + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea2, ¶ms); + } + + + + typedef struct + { + _OrthancPluginCreateDicom createDicom; + const char* privateCreator; + } _OrthancPluginCreateDicom2; + + /** + * @brief Create a DICOM instance from a JSON string and an image, with a private creator. + * + * 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. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param json The input JSON file. + * @param pixelData The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme. + * @param flags Flags governing the output. + * @param privateCreator The private creator to be used for the private DICOM tags. + * Check out the global configuration option "Dictionary" of Orthanc. + * @return 0 if success, other value if error. + * @ingroup Toolbox + * @see OrthancPluginCreateDicom + * @see OrthancPluginDicomBufferToJson + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom2( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* json, + const OrthancPluginImage* pixelData, + OrthancPluginCreateDicomFlags flags, + const char* privateCreator) + { + _OrthancPluginCreateDicom2 params; + params.createDicom.target = target; + params.createDicom.json = json; + params.createDicom.pixelData = pixelData; + params.createDicom.flags = flags; + params.privateCreator = privateCreator; + + return context->InvokeService(context, _OrthancPluginService_CreateDicom2, ¶ms); + } + + + + + + + typedef struct + { + OrthancPluginMemoryBuffer* answerBody; + OrthancPluginMemoryBuffer* answerHeaders; + uint16_t* httpStatus; + OrthancPluginHttpMethod method; + const char* uri; + uint32_t headersCount; + const char* const* headersKeys; + const char* const* headersValues; + const void* body; + uint32_t bodySize; + uint8_t afterPlugins; + } _OrthancPluginCallRestApi; + + /** + * @brief Call the REST API of Orthanc with full flexibility. + * + * Make a call to the given URI in the REST API of Orthanc. The + * result to the query is stored into a newly allocated memory + * buffer. This function is always granted full access to the REST + * API (no credentials, nor security token is needed). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answerBody The target memory buffer (out argument). + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * The value of this argument is ignored if the HTTP method is DELETE. + * @param answerHeaders The target memory buffer for the HTTP headers in the answer (out argument). + * The answer headers are formatted as a JSON object (associative array). + * The buffer must be freed with OrthancPluginFreeMemoryBuffer(). + * This argument can be set to NULL if the plugin has no interest in the answer HTTP headers. + * @param httpStatus The HTTP status after the execution of the request (out argument). + * @param method HTTP method to be used. + * @param uri The URI of interest. + * @param headersCount The number of HTTP headers. + * @param headersKeys Array containing the keys of the HTTP headers (can be NULL if no header). + * @param headersValues Array containing the values of the HTTP headers (can be NULL if no header). + * @param body The HTTP body for a POST or PUT request. + * @param bodySize The size of the body. + * @param afterPlugins If 0, the built-in API of Orthanc is used. + * If 1, the API is tainted by the plugins. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginRestApiGet2, OrthancPluginRestApiPost, OrthancPluginRestApiPut, OrthancPluginRestApiDelete + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCallRestApi( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* answerBody, + OrthancPluginMemoryBuffer* answerHeaders, + uint16_t* httpStatus, + OrthancPluginHttpMethod method, + const char* uri, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + const void* body, + uint32_t bodySize, + uint8_t afterPlugins) + { + _OrthancPluginCallRestApi params; + memset(¶ms, 0, sizeof(params)); + + params.answerBody = answerBody; + params.answerHeaders = answerHeaders; + params.httpStatus = httpStatus; + params.method = method; + params.uri = uri; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + params.body = body; + params.bodySize = bodySize; + params.afterPlugins = afterPlugins; + + return context->InvokeService(context, _OrthancPluginService_CallRestApi, ¶ms); + } + + + + /** + * @brief Opaque structure that represents a WebDAV collection. + * @ingroup Callbacks + **/ + typedef struct _OrthancPluginWebDavCollection_t OrthancPluginWebDavCollection; + + + /** + * @brief Declare a file while returning the content of a folder. + * + * This function declares a file while returning the content of a + * WebDAV folder. + * + * @param collection Context of the collection. + * @param name Base name of the file. + * @param dateTime The date and time of creation of the file. + * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information. + * @param size Size of the file. + * @param mimeType The MIME type of the file. If empty or set to `NULL`, + * Orthanc will do a best guess depending on the file extension. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFile) ( + OrthancPluginWebDavCollection* collection, + const char* name, + uint64_t size, + const char* mimeType, + const char* dateTime); + + + /** + * @brief Declare a subfolder while returning the content of a folder. + * + * This function declares a subfolder while returning the content of a + * WebDAV folder. + * + * @param collection Context of the collection. + * @param name Base name of the subfolder. + * @param dateTime The date and time of creation of the subfolder. + * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFolder) ( + OrthancPluginWebDavCollection* collection, + const char* name, + const char* dateTime); + + + /** + * @brief Retrieve the content of a file. + * + * This function is used to forward the content of a file from a + * WebDAV collection, to the core of Orthanc. + * + * @param collection Context of the collection. + * @param data Content of the file. + * @param size Size of the file. + * @param mimeType The MIME type of the file. If empty or set to `NULL`, + * Orthanc will do a best guess depending on the file extension. + * @param dateTime The date and time of creation of the file. + * It must be formatted as an ISO string of form + * `YYYYMMDDTHHMMSS,fffffffff` where T is the date-time + * separator. It must be expressed in UTC (it is the responsibility + * of the plugin to do the possible timezone + * conversions). Internally, this string will be parsed using + * `boost::posix_time::from_iso_string()`. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFile) ( + OrthancPluginWebDavCollection* collection, + const void* data, + uint64_t size, + const char* mimeType, + const char* dateTime); + + + /** + * @brief Callback for testing the existence of a folder. + * + * Signature of a callback function that tests whether the given + * path in the WebDAV collection exists and corresponds to a folder. + * + * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavIsExistingFolderCallback) ( + uint8_t* isExisting, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback for listing the content of a folder. + * + * Signature of a callback function that lists the content of a + * folder in the WebDAV collection. The callback must call the + * provided `addFile()` and `addFolder()` functions to emit the + * content of the folder. + * + * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise. + * @param collection Context to be provided to `addFile()` and `addFolder()` functions. + * @param addFile Function to add a file to the list. + * @param addFolder Function to add a folder to the list. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavListFolderCallback) ( + uint8_t* isExisting, /* out */ + OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavAddFile addFile, + OrthancPluginWebDavAddFolder addFolder, + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback for retrieving the content of a file. + * + * Signature of a callback function that retrieves the content of a + * file in the WebDAV collection. The callback must call the + * provided `retrieveFile()` function to emit the actual content of + * the file. + * + * @param collection Context to be provided to `retrieveFile()` function. + * @param retrieveFile Function to return the content of the file. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFileCallback) ( + OrthancPluginWebDavCollection* collection, + OrthancPluginWebDavRetrieveFile retrieveFile, + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback to store a file. + * + * Signature of a callback function that stores a file into the + * WebDAV collection. + * + * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param data Content of the file to be stored. + * @param size Size of the file to be stored. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavStoreFileCallback) ( + uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + const void* data, + uint64_t size, + void* payload); + + + /** + * @brief Callback to create a folder. + * + * Signature of a callback function that creates a folder in the + * WebDAV collection. + * + * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavCreateFolderCallback) ( + uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + /** + * @brief Callback to remove a file or a folder. + * + * Signature of a callback function that removes a file or a folder + * from the WebDAV collection. + * + * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise. + * @param pathSize Number of levels in the path. + * @param pathItems Items making the path. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWebDavDeleteItemCallback) ( + uint8_t* isReadOnly, /* out */ + uint32_t pathSize, + const char* const* pathItems, + void* payload); + + + typedef struct + { + const char* uri; + OrthancPluginWebDavIsExistingFolderCallback isExistingFolder; + OrthancPluginWebDavListFolderCallback listFolder; + OrthancPluginWebDavRetrieveFileCallback retrieveFile; + OrthancPluginWebDavStoreFileCallback storeFile; + OrthancPluginWebDavCreateFolderCallback createFolder; + OrthancPluginWebDavDeleteItemCallback deleteItem; + void* payload; + } _OrthancPluginRegisterWebDavCollection; + + /** + * @brief Register a WebDAV virtual filesystem. + * + * This function maps a WebDAV collection onto the given URI in the + * REST API of Orthanc. 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 uri URI where to map the WebDAV collection (must start with a `/` character). + * @param isExistingFolder Callback method to test for the existence of a folder. + * @param listFolder Callback method to list the content of a folder. + * @param retrieveFile Callback method to retrieve the content of a file. + * @param storeFile Callback method to store a file into the WebDAV collection. + * @param createFolder Callback method to create a folder. + * @param deleteItem Callback method to delete a file or a folder. + * @param payload The user payload. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWebDavCollection( + OrthancPluginContext* context, + const char* uri, + OrthancPluginWebDavIsExistingFolderCallback isExistingFolder, + OrthancPluginWebDavListFolderCallback listFolder, + OrthancPluginWebDavRetrieveFileCallback retrieveFile, + OrthancPluginWebDavStoreFileCallback storeFile, + OrthancPluginWebDavCreateFolderCallback createFolder, + OrthancPluginWebDavDeleteItemCallback deleteItem, + void* payload) + { + _OrthancPluginRegisterWebDavCollection params; + params.uri = uri; + params.isExistingFolder = isExistingFolder; + params.listFolder = listFolder; + params.retrieveFile = retrieveFile; + params.storeFile = storeFile; + params.createFolder = createFolder; + params.deleteItem = deleteItem; + params.payload = payload; + + return context->InvokeService(context, _OrthancPluginService_RegisterWebDavCollection, ¶ms); + } + + + /** + * @brief Gets the DatabaseServerIdentifier. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @return the database server identifier. This is a statically-allocated + * string, do not free it. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetDatabaseServerIdentifier( + OrthancPluginContext* context) + { + const char* result; + + _OrthancPluginRetrieveStaticString params; + params.result = &result; + params.argument = NULL; + + if (context->InvokeService(context, _OrthancPluginService_GetDatabaseServerIdentifier, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + +#ifdef __cplusplus +} +#endif + + +/** @} */ + diff --git a/Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake b/Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake new file mode 100644 index 0000000..1a81990 --- /dev/null +++ b/Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake @@ -0,0 +1,101 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +# +# Full build, as used on the BuildBot CIS: +# +# $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DUSE_LEGACY_BOOST=ON -DBOOST_LOCALE_BACKEND=icu -DENABLE_PKCS11=ON -G Ninja +# +# Or, more lightweight version (without libp11 and ICU): +# +# $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G Ninja +# + +INCLUDE(CMakeForceCompiler) + +SET(LSB_PATH $ENV{LSB_PATH} CACHE STRING "") +SET(LSB_CC $ENV{LSB_CC} CACHE STRING "") +SET(LSB_CXX $ENV{LSB_CXX} CACHE STRING "") +SET(LSB_TARGET_VERSION "4.0" CACHE STRING "") + +IF ("${LSB_PATH}" STREQUAL "") + SET(LSB_PATH "/opt/lsb") +ENDIF() + +IF (EXISTS ${LSB_PATH}/lib64) + SET(LSB_TARGET_PROCESSOR "x86_64") + SET(LSB_LIBPATH ${LSB_PATH}/lib64-${LSB_TARGET_VERSION}) +ELSEIF (EXISTS ${LSB_PATH}/lib) + SET(LSB_TARGET_PROCESSOR "x86") + SET(LSB_LIBPATH ${LSB_PATH}/lib-${LSB_TARGET_VERSION}) +ELSE() + MESSAGE(FATAL_ERROR "Unable to detect the target processor architecture. Check the LSB_PATH environment variable.") +ENDIF() + +SET(LSB_CPPPATH ${LSB_PATH}/include) +SET(PKG_CONFIG_PATH ${LSB_LIBPATH}/pkgconfig/) + +# the name of the target operating system +SET(CMAKE_SYSTEM_NAME Linux) +SET(CMAKE_SYSTEM_VERSION LinuxStandardBase) +SET(CMAKE_SYSTEM_PROCESSOR ${LSB_TARGET_PROCESSOR}) + +# which compilers to use for C and C++ +SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc) + +if (${CMAKE_VERSION} VERSION_LESS "3.6.0") + CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU) +else() + SET(CMAKE_CXX_COMPILER ${LSB_PATH}/bin/lsbc++) +endif() + +# here is the target environment located +SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH}) + +# adjust the default behaviour of the FIND_XXX() commands: +# search headers and libraries in the target environment, search +# programs in the host environment +SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER) +SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER) + +SET(CMAKE_CROSSCOMPILING OFF) + + +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include" CACHE INTERNAL "" FORCE) +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward" CACHE INTERNAL "" FORCE) +SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE) +SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE) + +if (NOT "${LSB_CXX}" STREQUAL "") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cxx=${LSB_CXX}") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}") +endif() + +if (NOT "${LSB_CC}" STREQUAL "") + SET(CMAKE_C_FLAGS "${CMAKE_CC_FLAGS} --lsb-cc=${LSB_CC}") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cc=${LSB_CC}") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cc=${LSB_CC}") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cc=${LSB_CC}") +endif() + diff --git a/Resources/Orthanc/Toolchains/MinGW-W64-Toolchain32.cmake b/Resources/Orthanc/Toolchains/MinGW-W64-Toolchain32.cmake new file mode 100644 index 0000000..61ae88f --- /dev/null +++ b/Resources/Orthanc/Toolchains/MinGW-W64-Toolchain32.cmake @@ -0,0 +1,39 @@ +# 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-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +# the name of the target operating system +set(CMAKE_SYSTEM_NAME Windows) + +# which compilers to use for C and C++ +set(CMAKE_C_COMPILER i686-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++) +set(CMAKE_RC_COMPILER i686-w64-mingw32-windres) + +# here is the target environment located +set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32) + +# adjust the default behaviour of the FIND_XXX() commands: +# search headers and libraries in the target environment, search +# programs in the host environment +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/Resources/Orthanc/Toolchains/MinGW-W64-Toolchain64.cmake b/Resources/Orthanc/Toolchains/MinGW-W64-Toolchain64.cmake new file mode 100644 index 0000000..888ac6f --- /dev/null +++ b/Resources/Orthanc/Toolchains/MinGW-W64-Toolchain64.cmake @@ -0,0 +1,39 @@ +# 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-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +# the name of the target operating system +set(CMAKE_SYSTEM_NAME Windows) + +# which compilers to use for C and C++ +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +# here is the target environment located +set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32) + +# adjust the default behaviour of the FIND_XXX() commands: +# search headers and libraries in the target environment, search +# programs in the host environment +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/Resources/Orthanc/Toolchains/MinGWToolchain.cmake b/Resources/Orthanc/Toolchains/MinGWToolchain.cmake new file mode 100644 index 0000000..b98ecdf --- /dev/null +++ b/Resources/Orthanc/Toolchains/MinGWToolchain.cmake @@ -0,0 +1,42 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2024 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 +# . + + +# the name of the target operating system +set(CMAKE_SYSTEM_NAME Windows) + +# which compilers to use for C and C++ +set(CMAKE_C_COMPILER i586-mingw32msvc-gcc) +set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++) +set(CMAKE_RC_COMPILER i586-mingw32msvc-windres) + +# here is the target environment located +set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc) + +# adjust the default behaviour of the FIND_XXX() commands: +# search headers and libraries in the target environment, search +# programs in the host environment +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE) diff --git a/Resources/SyncOrthancFolder.py b/Resources/SyncOrthancFolder.py new file mode 100644 index 0000000..abf50dc --- /dev/null +++ b/Resources/SyncOrthancFolder.py @@ -0,0 +1,78 @@ +#!/usr/bin/python3 + +# +# 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.11.2' +REPOSITORY = 'https://orthanc.uclouvain.be/hg/orthanc/raw-file' + +FILES = [ + ('OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake', 'CMake'), + ('OrthancFramework/Resources/CMake/Compiler.cmake', 'CMake'), + ('OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake', 'CMake'), + ('OrthancFramework/Resources/CMake/DownloadPackage.cmake', 'CMake'), + ('OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake', 'CMake'), + ('OrthancFramework/Resources/EmbedResources.py', 'CMake'), + ('OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake', 'Toolchains'), + ('OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake', 'Toolchains'), + ('OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake', 'Toolchains'), + ('OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake', 'Toolchains'), + ('OrthancServer/Plugins/Samples/Common/ExportedSymbolsPlugins.list', 'Plugins'), + ('OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp', 'Plugins'), + ('OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h', 'Plugins'), + ('OrthancServer/Plugins/Samples/Common/OrthancPluginException.h', 'Plugins'), + ('OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake', 'Plugins'), + ('OrthancServer/Plugins/Samples/Common/VersionScriptPlugins.map', 'Plugins'), +] + +SDK = [ + 'orthanc/OrthancCPlugin.h', +] + + +def Download(x): + branch = x[0] + source = x[1] + target = os.path.join(TARGET, x[2]) + print(target) + + try: + os.makedirs(os.path.dirname(target)) + except: + pass + + url = '%s/%s/%s' % (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([ 'default', + f[0], + os.path.join(f[1], os.path.basename(f[0])) ]) + +for f in SDK: + commands.append([ + 'Orthanc-%s' % PLUGIN_SDK_VERSION, + 'OrthancServer/Plugins/Include/%s' % f, + 'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f) + ]) + + +pool = multiprocessing.Pool(10) # simultaneous downloads +pool.map(Download, commands) diff --git a/TODO b/TODO new file mode 100644 index 0000000..19f7949 --- /dev/null +++ b/TODO @@ -0,0 +1,98 @@ +- implement pagination to fetch studies 100 -> 200 when we scroll to the end of the study-list +- implement pagination to fetch instances 100 -> 200 when we scroll to the end of the instance-list (displaying 2000 instances in one go sometimes takes 10 seconds !) + + +- add a "reset" button (on Windows, to reload the config after you have changed it without going to the Services -> Orthanc -> Restart) +- show ohif-vr and ohif-tmtv buttons only when relevant (analyse the content of the study) + +- predefined filters in config file to display below Studies: + - "Today": {"StudyDate": "$today"} + - "CT Last month": {"ModalitiesInStudy": "CT", "StudyDate": "$oneMonthAgo-"} + predefined keywords: $today, $yesterday, $oneWeekAgo, $oneMonthAgo, $oneYearAgo + +UI improvements: +- Admin theme including responsive tables (including multiline): https://github.com/lekoala/admini +- tags: https://github.com/lekoala/bootstrap5-tags +- when opening the series view I would prefer to see an image and it’s labels over a list of instance numbers and paired SOPInstanceUIDs: + https://discourse.orthanc-server.org/t/beginner-questions-from-horos-user/5322 + +- modification: + - configuration to hide DICOM UID options and select the right one directly for a given setup. + - add a Series Description renamer (one dialog to edit SeriesNumber + SeriesDescription of a study) + +- settings: + - show the list of /tools/accepted-transfer-syntaxes + - show the list of /tools/accepted-sop-classes (in 1.12.6) + +- show attachments + +- support neuro plugin (download nifti) + +- TagsTree: allow click on "null" tags to open /instances/../content/group,element in new window + +- show job details (need improvement in Orthanc API itself) +- include an "all jobs" panel and not only our jobs (https://discourse.orthanc-server.org/t/oe2-inclusion-of-jobs-panel-in-explorer-2/3708) + +- list last studies received (through DICOM or upload) + +- UI customization: + - add custom actions per study/series/instances: + - configuration suggestion: + { + "CustomButtons": [ + { + "Name": "Process study", // the text will appear in the button tooltip + "Icon": "bi bi-glass", + "Level": "Study", + "ActionUrl": "../../my-python-plugin/my-route", // this route can be templated with e.g: ../../my-python-plugin/my-route?StudyInstanceUID=$resource-dicom-id$&orthanc-id=$resource-orthanc-id$&level=$resource-level$" + "ActionMethod": "POST", + "ActionPostPayloadTemplate": { + "dicom-id": "$resource-dicom-id$", + "orthanc-id": "$resource-orthanc-id$" + } + "EnabledUrl": "../../my-python-plugin/my-route-enabled", // optional: a GET route that returns true/false if the custom button must be enabled/disabled for this resource. It shall be templated as well + + } + ] + } + - configure other viewers url (ex: radiant://?n=pstv&v=0020000D&v=%22StudyInstanceUID%22 or osirix or horos ...) + +- orthanc-share should generate QR code with publication links + +- Q&R on multiple modalities at a same time (select the modalities you want to Q&R and display the modality in the study list) + (same with Q&R for dicom-web and peers) + +- in local study list, display the number of studies that are present on a remote modality for this patient (e.g: a cold archive) + - Add a button to fetch all these data (todo: find a way to delete them after a while ?) + - same with dicom-web and peers ? + + +Configuration management (ideas + implementation notes): +------------------------------- + +- allow Orthanc to store anything in Global Properties. Maybe not through the API but only through the SDK. + Or, only for an "admin" user ? + +- Edit configuration through the oe2 UI: + - Some immutable configuration in a config file: + - DB + storage + - Admin user ! + - HttpPort + - Check if we could store a config in Global Properties (probably too early in the Orthanc init process). We would merge the file config with the config in DB + - Otherwise, store the config in a file and restart Orthanc + -> in Windows Service: how to tell Orthanc to use another config file ? + -> would be nice to have a SDK route to implement /tools/reboot with a given config file + - Could be a combination of 2: + - start Orthanc with config from file + - plugin reads the config in DB, generates a tmp config file -> /tools/reboot with this file + - that's a bit "shaky" since each cold start is made of 2 starts + +Ideas bag: +********* + +- allow users to choose the columns in the interface (store in browser LocalStorage ?) +- browse orthanc peer (probably need to extend the Orthanc API to avoid CORS issues) +- show statistics/event logs: e.g: would be nice to see how many instances + have been received recently (from where) +- add a button to export a whole series to jpeg (Chamrousse): we could reuse [this python code](https://orthanc.uclouvain.be/book/plugins/python.html#generating-a-mosaic-for-a-dicom-series) and trigger it from a [custom button](https://github.com/orthanc-server/orthanc-explorer-2/issues/18) (once we have implemented them !!!) +- add a text editor to associate a radiology report or note with a study, storing it as an attachment (cf. https://discourse.orthanc-server.org/t/oe2-inclusion-of-reporting-note/) diff --git a/WebApplication/index.html b/WebApplication/index.html new file mode 100644 index 0000000..624e575 --- /dev/null +++ b/WebApplication/index.html @@ -0,0 +1,24 @@ + + + + + + + ... + + + +
+ + + diff --git a/WebApplication/package-lock.json b/WebApplication/package-lock.json new file mode 100644 index 0000000..fcda460 --- /dev/null +++ b/WebApplication/package-lock.json @@ -0,0 +1,1341 @@ +{ + "name": "orthanc-explorer-2", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "orthanc-explorer-2", + "version": "0.0.0", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.1", + "@popperjs/core": "^2.11.8", + "@vuepic/vue-datepicker": "^8.3.1", + "axios": "^1.7.4", + "bootstrap": "5.3.3", + "bootstrap-icons": "^1.11.3", + "bootstrap5-tags": "^1.7.2", + "jquery": "^3.7.1", + "keycloak-js": "^24.0.5", + "mitt": "^3.0.1", + "uppie": "^4.0.0", + "uuid": "^9.0.1", + "vue": "^3.4.21", + "vue-i18n": "^9.10.2", + "vue-router": "^4.3.0", + "vue3-observe-visibility": "^1.0.2", + "vuex": "^4.1.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@intlify/core-base": { + "version": "9.14.2", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.2.tgz", + "integrity": "sha512-DZyQ4Hk22sC81MP4qiCDuU+LdaYW91A6lCjq8AWPvY3+mGMzhGDfOCzvyR6YBQxtlPjFqMoFk9ylnNYRAQwXtQ==", + "dependencies": { + "@intlify/message-compiler": "9.14.2", + "@intlify/shared": "9.14.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.2", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.2.tgz", + "integrity": "sha512-YsKKuV4Qv4wrLNsvgWbTf0E40uRv+Qiw1BeLQ0LAxifQuhiMe+hfTIzOMdWj/ZpnTDj4RSZtkXjJM7JDiiB5LQ==", + "dependencies": { + "@intlify/shared": "9.14.2", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.2", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.2.tgz", + "integrity": "sha512-uRAHAxYPeF+G5DBIboKpPgC/Waecd4Jz8ihtkpJQD5ycb5PwXp0k/+hBGl5dAjwF7w+l74kz/PKA8r8OK//RUw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.23.0.tgz", + "integrity": "sha512-8OR+Ok3SGEMsAZispLx8jruuXw0HVF16k+ub2eNXKHDmdxL4cf9NlNpAzhlOhNyXzKDEJuFeq0nZm+XlNb1IFw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.23.0.tgz", + "integrity": "sha512-rEFtX1nP8gqmLmPZsXRMoLVNB5JBwOzIAk/XAcEPuKrPa2nPJ+DuGGpfQUR0XjRm8KjHfTZLpWbKXkA5BoFL3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.23.0.tgz", + "integrity": "sha512-ZbqlMkJRMMPeapfaU4drYHns7Q5MIxjM/QeOO62qQZGPh9XWziap+NF9fsqPHT0KzEL6HaPspC7sOwpgyA3J9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.23.0.tgz", + "integrity": "sha512-PfmgQp78xx5rBCgn2oYPQ1rQTtOaQCna0kRaBlc5w7RlA3TDGGo7m3XaptgitUZ54US9915i7KeVPHoy3/W8tA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.23.0.tgz", + "integrity": "sha512-WAeZfAAPus56eQgBioezXRRzArAjWJGjNo/M+BHZygUcs9EePIuGI1Wfc6U/Ki+tMW17FFGvhCfYnfcKPh18SA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.23.0.tgz", + "integrity": "sha512-v7PGcp1O5XKZxKX8phTXtmJDVpE20Ub1eF6w9iMmI3qrrPak6yR9/5eeq7ziLMrMTjppkkskXyxnmm00HdtXjA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.23.0.tgz", + "integrity": "sha512-nAbWsDZ9UkU6xQiXEyXBNHAKbzSAi95H3gTStJq9UGiS1v+YVXwRHcQOQEF/3CHuhX5BVhShKoeOf6Q/1M+Zhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.23.0.tgz", + "integrity": "sha512-5QT/Di5FbGNPaVw8hHO1wETunwkPuZBIu6W+5GNArlKHD9fkMHy7vS8zGHJk38oObXfWdsuLMogD4sBySLJ54g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.23.0.tgz", + "integrity": "sha512-Sefl6vPyn5axzCsO13r1sHLcmPuiSOrKIImnq34CBurntcJ+lkQgAaTt/9JkgGmaZJ+OkaHmAJl4Bfd0DmdtOQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.23.0.tgz", + "integrity": "sha512-o4QI2KU/QbP7ZExMse6ULotdV3oJUYMrdx3rBZCgUF3ur3gJPfe8Fuasn6tia16c5kZBBw0aTmaUygad6VB/hQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.23.0.tgz", + "integrity": "sha512-+bxqx+V/D4FGrpXzPGKp/SEZIZ8cIW3K7wOtcJAoCrmXvzRtmdUhYNbgd+RztLzfDEfA2WtKj5F4tcbNPuqgeg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.23.0.tgz", + "integrity": "sha512-I/eXsdVoCKtSgK9OwyQKPAfricWKUMNCwJKtatRYMmDo5N859tbO3UsBw5kT3dU1n6ZcM1JDzPRSGhAUkxfLxw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.23.0.tgz", + "integrity": "sha512-4ZoDZy5ShLbbe1KPSafbFh1vbl0asTVfkABC7eWqIs01+66ncM82YJxV2VtV3YVJTqq2P8HMx3DCoRSWB/N3rw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.23.0.tgz", + "integrity": "sha512-+5Ky8dhft4STaOEbZu3/NU4QIyYssKO+r1cD3FzuusA0vO5gso15on7qGzKdNXnc1gOrsgCqZjRw1w+zL4y4hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.23.0.tgz", + "integrity": "sha512-0SPJk4cPZQhq9qA1UhIRumSE3+JJIBBjtlGl5PNC///BoaByckNZd53rOYD0glpTkYFBQSt7AkMeLVPfx65+BQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz", + "integrity": "sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.10.tgz", + "integrity": "sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.10", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.10.tgz", + "integrity": "sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==", + "dependencies": { + "@vue/compiler-core": "3.5.10", + "@vue/shared": "3.5.10" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.10.tgz", + "integrity": "sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.10", + "@vue/compiler-dom": "3.5.10", + "@vue/compiler-ssr": "3.5.10", + "@vue/shared": "3.5.10", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.10.tgz", + "integrity": "sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==", + "dependencies": { + "@vue/compiler-dom": "3.5.10", + "@vue/shared": "3.5.10" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.10.tgz", + "integrity": "sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==", + "dependencies": { + "@vue/shared": "3.5.10" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.10.tgz", + "integrity": "sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==", + "dependencies": { + "@vue/reactivity": "3.5.10", + "@vue/shared": "3.5.10" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.10.tgz", + "integrity": "sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==", + "dependencies": { + "@vue/reactivity": "3.5.10", + "@vue/runtime-core": "3.5.10", + "@vue/shared": "3.5.10", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.10.tgz", + "integrity": "sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==", + "dependencies": { + "@vue/compiler-ssr": "3.5.10", + "@vue/shared": "3.5.10" + }, + "peerDependencies": { + "vue": "3.5.10" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.10.tgz", + "integrity": "sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==" + }, + "node_modules/@vuepic/vue-datepicker": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-8.8.1.tgz", + "integrity": "sha512-8ehfUz1m69Vuc16Pm4ukgb3Mg1VT14x4EsG1ag4O/qbSNRWztTo+pUV4JnFt0FGLl5gGb6NXlxIvR7EjLgD7Gg==", + "dependencies": { + "date-fns": "^3.6.0" + }, + "peerDependencies": { + "vue": ">=3.2.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, + "node_modules/bootstrap5-tags": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/bootstrap5-tags/-/bootstrap5-tags-1.7.5.tgz", + "integrity": "sha512-EqCpdjD/UdZVYdlgcWfQKU68x7AtUs3lSnl9/u1xW2kVc193nqE4QOyJ5fAvc4i4t365LerRG87kptMpsD1PlQ==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "node_modules/js-sha256": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", + "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==" + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/keycloak-js": { + "version": "24.0.5", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-24.0.5.tgz", + "integrity": "sha512-VQOSn3j13DPB6OuavKAq+sRjDERhIKrXgBzekoHRstifPuyULILguugX6yxRUYFSpn3OMYUXmSX++tkdCupOjA==", + "dependencies": { + "js-sha256": "^0.11.0", + "jwt-decode": "^4.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/rollup": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.23.0.tgz", + "integrity": "sha512-vXB4IT9/KLDrS2WRXmY22sVB2wTsTwkpxjB8Q3mnakTENcYw3FRmfdYDy/acNmls+lHmDazgrRjK/yQ6hQAtwA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.23.0", + "@rollup/rollup-android-arm64": "4.23.0", + "@rollup/rollup-darwin-arm64": "4.23.0", + "@rollup/rollup-darwin-x64": "4.23.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.23.0", + "@rollup/rollup-linux-arm-musleabihf": "4.23.0", + "@rollup/rollup-linux-arm64-gnu": "4.23.0", + "@rollup/rollup-linux-arm64-musl": "4.23.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.23.0", + "@rollup/rollup-linux-riscv64-gnu": "4.23.0", + "@rollup/rollup-linux-s390x-gnu": "4.23.0", + "@rollup/rollup-linux-x64-gnu": "4.23.0", + "@rollup/rollup-linux-x64-musl": "4.23.0", + "@rollup/rollup-win32-arm64-msvc": "4.23.0", + "@rollup/rollup-win32-ia32-msvc": "4.23.0", + "@rollup/rollup-win32-x64-msvc": "4.23.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/uppie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/uppie/-/uppie-4.0.0.tgz", + "integrity": "sha512-AiV1n8tFfsNOmLDB547P1os56p6NMDsgtMBJ73BIGBlzVgvNZuqbZGMsU7OLZCeMNiSAEdBbeu9KR4TK0TpMMw==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.10.tgz", + "integrity": "sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==", + "dependencies": { + "@vue/compiler-dom": "3.5.10", + "@vue/compiler-sfc": "3.5.10", + "@vue/runtime-dom": "3.5.10", + "@vue/server-renderer": "3.5.10", + "@vue/shared": "3.5.10" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-i18n": { + "version": "9.14.2", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.2.tgz", + "integrity": "sha512-JK9Pm80OqssGJU2Y6F7DcM8RFHqVG4WkuCqOZTVsXkEzZME7ABejAUqUdA931zEBedc4thBgSUWxeQh4uocJAQ==", + "dependencies": { + "@intlify/core-base": "9.14.2", + "@intlify/shared": "9.14.2", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", + "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue3-observe-visibility": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vue3-observe-visibility/-/vue3-observe-visibility-1.0.2.tgz", + "integrity": "sha512-PmNmvLezogMAgwix6VPnkkrx7sj834GblvqCCDI2iySpohTQvCm1j2q89aTmyS0XzG4sWBK2Izypejf4IpNFig==", + "peerDependencies": { + "vue": "^3.x.x" + } + }, + "node_modules/vuex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", + "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", + "dependencies": { + "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + } + } +} diff --git a/WebApplication/package.json b/WebApplication/package.json new file mode 100644 index 0000000..f1516d0 --- /dev/null +++ b/WebApplication/package.json @@ -0,0 +1,33 @@ +{ + "name": "orthanc-explorer-2", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.5.1", + "@popperjs/core": "^2.11.8", + "@vuepic/vue-datepicker": "^8.3.1", + "axios": "^1.7.4", + "bootstrap": "5.3.3", + "bootstrap-icons": "^1.11.3", + "bootstrap5-tags": "^1.7.2", + "jquery": "^3.7.1", + "keycloak-js": "^24.0.5", + "mitt": "^3.0.1", + "uppie": "^4.0.0", + "uuid": "^9.0.1", + "vue": "^3.4.21", + "vue-i18n": "^9.10.2", + "vue-router": "^4.3.0", + "vuex": "^4.1.0", + "vue3-observe-visibility": "^1.0.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.4.8" + } +} diff --git a/WebApplication/public/favicon.ico b/WebApplication/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..529c09d56d2610567058b37b1111fd708c64504c GIT binary patch literal 4286 zcmeI$K}#D!7{>7lHF_vSL!lH(K|GY6^kUH3YBn{wi3r|$sr>@Il=jj?11f_04O)7x zicv%9*>BOSr=m$CL>e1nNcw-09SD)xq&4j+kzZ!nnY_>J?(8OLOo+eHQKNVBdBm8z z#+XN7m+_$ejTe1)ZZ|b|G~d5{N*RPPhb?4~4RA=`3{1lFWEuy^2FTF2jX1(+gsTQ3 zSVIo=E>sz_44c11y(`qakY~&~B2e}KR-u~RWNXY#@-@e*KL=&c;o)&_?I~sZ9?(9;6W?Q6t<`H&(>{zqW9)c+_C9RB&XKN> z%|D_qDE|%pzwpzi`$M%|`MPdD{IU9s&gUz*@^!6)^54;K&y8BGOFbi8`MP$RZ|i4X zYdps+?Z@BG@1Or2)<*?p$ajm~e)?f~0RIta`aioxh z?w?_}^7U-i{q^0UzcF!OJ4@smjLDmi`C?k&;T={?f{3A>G zlgOcr3iim#pzgb%ox;D$kC9nK-FHYG2KneE#+b3rOHeapoNj(Sk910SpUo|BPo!cOB`ZKWEYB#Adi&NM+ z)YvYe7~q_~McDiWY89Yoi;IrC2AhAzcAOdmSVa|f4xL(!U53qHqSjSQZJXh0tBhTT zvURN^SVIn-vekc+{Jfv7@1n*Mqn_pv#vHbwXL2@x*2oO(@lR9h0NDV#_S=Xfj7Iix vD19p(O7ky-H2)}Q8miY%28T{ooQzo+M9rA@qHX6`zm96!`fAf;9+UkW(^Ll8 literal 0 HcmV?d00001 diff --git a/WebApplication/retrieve-and-view.html b/WebApplication/retrieve-and-view.html new file mode 100644 index 0000000..a31cc00 --- /dev/null +++ b/WebApplication/retrieve-and-view.html @@ -0,0 +1,13 @@ + + + + + + + OE2 Retrieve and view + + +
+ + + diff --git a/WebApplication/src/App.vue b/WebApplication/src/App.vue new file mode 100644 index 0000000..083b80d --- /dev/null +++ b/WebApplication/src/App.vue @@ -0,0 +1,84 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/AppLanding.vue b/WebApplication/src/AppLanding.vue new file mode 100644 index 0000000..8fd59b0 --- /dev/null +++ b/WebApplication/src/AppLanding.vue @@ -0,0 +1,47 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/AppRetrieveAndView.vue b/WebApplication/src/AppRetrieveAndView.vue new file mode 100644 index 0000000..699151a --- /dev/null +++ b/WebApplication/src/AppRetrieveAndView.vue @@ -0,0 +1,33 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/assets/css/common.css b/WebApplication/src/assets/css/common.css new file mode 100644 index 0000000..b3ede3c --- /dev/null +++ b/WebApplication/src/assets/css/common.css @@ -0,0 +1,75 @@ +.cut-text { + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; /* preserve double white spaces (https://github.com/orthanc-server/orthanc-explorer-2/issues/30) */ + max-width: 0; +} + + /* override CSS from datepicker to avoid the close button to overflow over the text */ +.dp__input_icon_pad { + padding-right: 30px !important; +} + +.dp__preset_ranges { + width: 300px !important; +} + +.dp__range_end, .dp__range_start, .dp__active_date { + background: var(--dp-primary-color) !important; +} + + + +/* for the StudyItem */ +.label { + margin-left: 2px; + margin-left: 2px; + background-color: var(--label-bg-color); +} + +/* for the tag editor */ +.tags-badge.bg-info { + background-color: var(--label-bg-color) !important; +} + +.form-control::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: var(--form-placeholder-color); + opacity: 1; /* Firefox */ +} + +.job-card { + margin-bottom: 2px; +} + +.job-card a { + /* color: black; */ +} + +.job-card-close { + position: absolute; + top: 5px; + right: 5px; +} + +.job-pct-pill { + width: 50px; + margin-right: 5px; +} + +.jobs-header { + text-align: left; + padding-top: 2px; + padding-bottom: 2px; + line-height: 1.2rem; +} + +.jobs-body { + padding-top: 2px; + padding-bottom: 2px; + line-height: 1.6; + text-align: left; +} + +.jobs-body a { + text-decoration: underline; +} diff --git a/WebApplication/src/assets/css/defaults-dark.css b/WebApplication/src/assets/css/defaults-dark.css new file mode 100644 index 0000000..bbbe492 --- /dev/null +++ b/WebApplication/src/assets/css/defaults-dark.css @@ -0,0 +1,67 @@ +/* Customized CSS. +This CSS is loaded after all other CSS files have been loaded +therefore, it overrides all other definitions */ +:root { + --bootstrap-theme: dark; + + --study-even-bg-color: rgb(55, 55, 60); + --study-odd-bg-color: rgb(45, 45, 50); + --study-hover-color: rgb(100, 98, 95); + --study-selected-color: rgb(40, 40, 45); + --study-details-bg-color: var(--study-selected-color); + --study-list-remote-bg-color: var(--study-hover-color); + + --series-even-bg-color: rgb(45, 45, 50); + --series-odd-bg-color: rgb(35, 35, 40); + --series-hover-color: var(--study-hover-color); + --series-selected-color: rgb(30, 30, 35); + --series-details-bg-color: var(--series-selected-color); + + --instance-odd-bg-color: rgb(35, 35, 40); + --instance-even-bg-color: rgb(30, 30, 35); + --instance-hover-color: var(--study-hover-color); + --instance-selected-color: rgb(20, 20, 25); + --instance-details-bg-color: var(--instance-selected-color); + + --study-table-header-bg-color: var(--study-even-bg-color); + --study-table-filter-bg-color: rgb(60, 60, 59); + --study-table-actions-bg-color: var(--study-table-filter-bg-color); + + --table-filters-is-searching-color: #fda90d86; + --table-filters-is-not-searching-color: rgb(30, 150, 255); + + --details-top-margin: 25px; + + --nav-bar-width: 260px; + + --nav-side-color: rgb(255, 255, 255); + --nav-side-bg-color: rgb(30, 36, 42); + --nav-side-sub-bg-color: #181c20; + --nav-side-selected-bg-color: #4f5b69; + --nav-side-hover-bg-color: #3e454d; + --nav-side-submenu-color: rgb(255, 255, 255); + --nav-side-active-border-color: #d19b3d; + + --label-bg-color: rgb(61, 116, 141); + + --form-placeholder-color: #888; + + /* you may also customize global bootstrap variables */ + --bs-border-color: rgb(110, 160, 200); + /* --bs-info-rgb: 255, 26, 35; */ + +} + +/* datepicker variables */ +.dp__theme_dark { + --dp-border-color: var(--bs-border-color); +} + +/* optional, you may e.g. customize bootstrap buttons or any other class that is declared globally */ + +/* +.btn-primary { + --bs-btn-color: white; + --bs-btn-bg: red; +} */ + diff --git a/WebApplication/src/assets/css/defaults-light.css b/WebApplication/src/assets/css/defaults-light.css new file mode 100644 index 0000000..ec1e13a --- /dev/null +++ b/WebApplication/src/assets/css/defaults-light.css @@ -0,0 +1,72 @@ +/* Default CSS. +This CSS is loaded after all other CSS files have been loaded +therefore, it overrides all other definitions. However, if you provide +a CustomCssPath, the custom file will be loaded after this one*/ +:root { + --bootstrap-theme: light; + + --study-even-bg-color: rgb(250, 250, 250); + --study-odd-bg-color: rgb(240, 240, 240); + --study-hover-color: rgb(60, 180, 255); + --study-selected-color: rgb(235, 235, 235); + --study-details-bg-color: var(--study-selected-color); + --study-list-remote-bg-color: var(--study-hover-color); + + --series-even-bg-color: rgb(240, 240, 240); + --series-odd-bg-color: rgb(235, 235, 235); + --series-hover-color: var(--study-hover-color); + --series-selected-color: rgb(225, 225, 225); + --series-details-bg-color: var(--series-selected-color); + + --instance-odd-bg-color: rgb(230, 230, 230); + --instance-even-bg-color: rgb(225, 225, 225); + --instance-hover-color: var(--study-hover-color); + --instance-selected-color: rgb(215, 215, 215); + --instance-details-bg-color: var(--instance-selected-color); + + --study-table-header-bg-color: var(--study-even-bg-color); + --study-table-filter-bg-color: rgb(220, 220, 220); + --study-table-actions-bg-color: var(--study-table-filter-bg-color); + + --table-filters-is-searching-color: #fda90d86; + --table-filters-is-not-searching-color: rgb(30, 150, 255); + + --details-top-margin: 25px; + + --nav-bar-width: 260px; + + --nav-side-color: rgb(255, 255, 255); + --nav-side-bg-color: #2e353d; + --nav-side-sub-bg-color: #181c20; + --nav-side-selected-bg-color: #4f5b69; + --nav-side-hover-bg-color: #3e454d; + --nav-side-submenu-color: rgb(255, 255, 255); + --nav-side-active-border-color: #d19b3d; + + --label-bg-color: rgb(10, 200, 240); + + --form-placeholder-color: #CCC; + + /* you may also customize global bootstrap variables */ + /* --bs-border-color: rgb(110, 160, 200); */ + /* --bs-info-rgb: 255, 26, 35; */ +} + +/* override the custom logo class */ +/* .custom-logo { + height: 150px; +} */ + + +/* datepicker variables */ +/* .dp__theme_dark { + --dp-border-color: var(--bs-border-color); +} */ + +/* optional, you may e.g. customize bootstrap buttons or any other class that is declared globally */ + +/* +.btn-primary { + --bs-btn-color: white; + --bs-btn-bg: red; +} */ diff --git a/WebApplication/src/assets/css/layout.css b/WebApplication/src/assets/css/layout.css new file mode 100644 index 0000000..e6c10ae --- /dev/null +++ b/WebApplication/src/assets/css/layout.css @@ -0,0 +1,3 @@ +.content { + margin-left: var(--nav-bar-width); +} \ No newline at end of file diff --git a/WebApplication/src/assets/images/orthanc.png b/WebApplication/src/assets/images/orthanc.png new file mode 100644 index 0000000000000000000000000000000000000000..816a4297e6aba644f5e2d9d9d49d7774cd0aa1f1 GIT binary patch literal 35026 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 +import { uppie } from "uppie" +import { mapState } from "vuex" +import resourceHelpers from "../helpers/resource-helpers" +import dateHelpers from "../helpers/date-helpers" +import api from "../orthancApi" +import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js" + + + +export default { + props: ["orthancStudyId", "studyMainDicomTags", "patientMainDicomTags"], + data() { + return { + seriesTags: {}, // the series tag values entered in the dialog + seriesDateTags: {}, // same as tags but only for dates (for the DatePicker) + errorMessageId: null, + warningMessageId: null, + uploadedFileType: null, + uploadedFileDate: null, + uploadedFileBase64Content: null, + uploadedFileHumanSize: null, + step: 'prepare', // allowed values: 'prepare', 'error' + } + }, + async mounted() { + this.$refs['modal-main-div'].addEventListener('show.bs.modal', (e) => { + // move the modal to body to avoid z-index issues: https://weblog.west-wind.com/posts/2016/sep/14/bootstrap-modal-dialog-showing-under-modal-background + document.querySelector('body').appendChild(e.target); + this.reset(); + }); + uppie(document.querySelector("#addSeriesFileUpload-" + this.orthancStudyId), this.uppieUploadHandler); + }, + methods: { + async reset() { + this.step = 'prepare'; + this.uploadedFileType = null; + this.uploadedFileDate = null; + this.uploadedFileHumanSize = null; + this.uploadedFileName = null; + this.uploadedFileBase64Content = null; + this.warningMessageId = null; + this.errorMessageId = null; + this.seriesTags = {}; + document.getElementById("addSeriesFileUpload-" + this.orthancStudyId).value = null; + }, + setError(errorMessageId) { + this.step = 'error'; + this.errorMessageId = errorMessageId; + }, + async addSeries() { + try { + let tags = {}; + + for (const [k,] of Object.entries(this.seriesTags)) { + if (dateHelpers.isDateTag(k)) { + tags[k] = dateHelpers.dicomDateFromDatePicker(this.seriesDateTags[k]); + } else { + tags[k] = this.seriesTags[k]; + } + } + + + // if no AcquisitionDate and ContentDate, copy it from SeriesDate ... + if (!("AcquisitionDate" in tags)) { + tags["AcquisitionDate"] = tags["SeriesDate"] + } + if (!("ContentDate" in tags)) { + tags["ContentDate"] = tags["SeriesDate"] + } + + await api.createDicom(this.orthancStudyId, this.uploadedFileBase64Content, tags); + let closeButton = document.getElementById('add-series-close-'+ this.orthancStudyId); + closeButton.click(); + this.messageBus.emit('added-series-to-study-' + this.orthancStudyId); + } catch (err) { + this.setError('add_series.error_add_series_unexpected_error_html'); + } + }, + isDateTag(tag) { + return dateHelpers.isDateTag(tag); + }, + onDrop(ev) { + ev.preventDefault(); + this.analyzeFile(ev.dataTransfer.items[0].getAsFile()); + }, + onDragOver(event) { + event.preventDefault(); + }, + getHumanSize(sizeInBytes) { + if (sizeInBytes > 1024*1024) { + return (Math.round(sizeInBytes/(1024*1024)*100) / 100) + " MB"; + } else { + return (Math.round(sizeInBytes/1024*100) / 100) + " kB"; + } + }, + async uppieUploadHandler(event, formData, files) { + event.preventDefault(); + this.analyzeFile(event.target.files[0]); + }, + async analyzeFile(file) { + console.log("analyzeFile", file); + this.uploadedFileDate = file.lastModifiedDate; + this.uploadedFileHumanSize = this.getHumanSize(file.size); + this.uploadedFileName = file.name; + this.warningMessageId = null; + + if (file.type == 'application/pdf') { + this.uploadedFileType = "pdf"; + } else if (file.type.startsWith('image/')) { + this.uploadedFileType = "image"; + } else if (file.type == "" && file.name.endsWith ) { + this.uploadedFileType = "stl" + } else { + this.uploadedFileType = null; + this.warningMessageId = "add_series.unrecognized_file_format" + return; + } + + for (const [k, v] of Object.entries(this.uiOptions.AddSeriesDefaultTags[this.uploadedFileType])) { + this.seriesTags[k] = v; + if (dateHelpers.isDateTag(k)) { + if (v == '$TODAY$') { + this.seriesDateTags[k] = new Date(); + } else if (v == '$STUDY_DATE$') { + this.seriesDateTags[k] = dateHelpers.fromDicomDate(this.studyMainDicomTags["StudyDate"]); + } else if (v == '$FILE_DATE$') { + this.seriesDateTags[k] = this.uploadedFileDate; + } else { + this.seriesDateTags[k] = dateHelpers.fromDicomDate(v); + } + } + } + + let reader = new FileReader(); + let that = this; + reader.onload = function (event) { + that.uploadedFileBase64Content = event.target.result; + } + reader.readAsDataURL(file); + } + }, + computed: { + ...mapState({ + uiOptions: state => state.configuration.uiOptions + }), + resourceTitle() { + return resourceHelpers.getResourceTitle('study', this.patientMainDicomTags, this.studyMainDicomTags, null, null); + }, + addSeriesButtonEnabled() { + return this.uploadedFileType != null; + }, + isDarkMode() { + // hack to switch the theme: get the value from our custom css + let bootstrapTheme = document.documentElement.getAttribute("data-bs-theme"); // for production + bootstrapTheme = getComputedStyle(document.documentElement).getPropertyValue('--bootstrap-theme'); // for dev + console.log("DatePicker color mode is ", bootstrapTheme); + return bootstrapTheme == "dark"; + }, + datePickerFormat() { + return this.uiOptions.DateFormat; + } + + }, + components: {} + +}; + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/BulkLabelsModal.vue b/WebApplication/src/components/BulkLabelsModal.vue new file mode 100644 index 0000000..3be10e6 --- /dev/null +++ b/WebApplication/src/components/BulkLabelsModal.vue @@ -0,0 +1,225 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/CopyToClipboardButton.vue b/WebApplication/src/components/CopyToClipboardButton.vue new file mode 100644 index 0000000..1e67a31 --- /dev/null +++ b/WebApplication/src/components/CopyToClipboardButton.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/WebApplication/src/components/InstanceDetails.vue b/WebApplication/src/components/InstanceDetails.vue new file mode 100644 index 0000000..2f2a6ad --- /dev/null +++ b/WebApplication/src/components/InstanceDetails.vue @@ -0,0 +1,127 @@ + + + + + + + + diff --git a/WebApplication/src/components/InstanceItem.vue b/WebApplication/src/components/InstanceItem.vue new file mode 100644 index 0000000..bfae735 --- /dev/null +++ b/WebApplication/src/components/InstanceItem.vue @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/InstanceList.vue b/WebApplication/src/components/InstanceList.vue new file mode 100644 index 0000000..b0ced62 --- /dev/null +++ b/WebApplication/src/components/InstanceList.vue @@ -0,0 +1,109 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/InstanceListExtended.vue b/WebApplication/src/components/InstanceListExtended.vue new file mode 100644 index 0000000..473928b --- /dev/null +++ b/WebApplication/src/components/InstanceListExtended.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/WebApplication/src/components/JobItem.vue b/WebApplication/src/components/JobItem.vue new file mode 100644 index 0000000..dcbbf64 --- /dev/null +++ b/WebApplication/src/components/JobItem.vue @@ -0,0 +1,99 @@ + + + + \ No newline at end of file diff --git a/WebApplication/src/components/JobsList.vue b/WebApplication/src/components/JobsList.vue new file mode 100644 index 0000000..1e0512c --- /dev/null +++ b/WebApplication/src/components/JobsList.vue @@ -0,0 +1,46 @@ + + + + \ No newline at end of file diff --git a/WebApplication/src/components/LabelsEditor.vue b/WebApplication/src/components/LabelsEditor.vue new file mode 100644 index 0000000..cf7741e --- /dev/null +++ b/WebApplication/src/components/LabelsEditor.vue @@ -0,0 +1,116 @@ + + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/LanguagePicker.vue b/WebApplication/src/components/LanguagePicker.vue new file mode 100644 index 0000000..5814487 --- /dev/null +++ b/WebApplication/src/components/LanguagePicker.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/WebApplication/src/components/Modal.vue b/WebApplication/src/components/Modal.vue new file mode 100644 index 0000000..f565c2d --- /dev/null +++ b/WebApplication/src/components/Modal.vue @@ -0,0 +1,45 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/ModifyModal.vue b/WebApplication/src/components/ModifyModal.vue new file mode 100644 index 0000000..187a8d0 --- /dev/null +++ b/WebApplication/src/components/ModifyModal.vue @@ -0,0 +1,998 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/NotFound.vue b/WebApplication/src/components/NotFound.vue new file mode 100644 index 0000000..1c7188d --- /dev/null +++ b/WebApplication/src/components/NotFound.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/WebApplication/src/components/ResourceButtonGroup.vue b/WebApplication/src/components/ResourceButtonGroup.vue new file mode 100644 index 0000000..988e69a --- /dev/null +++ b/WebApplication/src/components/ResourceButtonGroup.vue @@ -0,0 +1,1006 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/ResourceDetailText.vue b/WebApplication/src/components/ResourceDetailText.vue new file mode 100644 index 0000000..5e986a1 --- /dev/null +++ b/WebApplication/src/components/ResourceDetailText.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/WebApplication/src/components/RetrieveAndView.vue b/WebApplication/src/components/RetrieveAndView.vue new file mode 100644 index 0000000..0c40faf --- /dev/null +++ b/WebApplication/src/components/RetrieveAndView.vue @@ -0,0 +1,151 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/SeriesDetails.vue b/WebApplication/src/components/SeriesDetails.vue new file mode 100644 index 0000000..63e2126 --- /dev/null +++ b/WebApplication/src/components/SeriesDetails.vue @@ -0,0 +1,165 @@ + + + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/SeriesItem.vue b/WebApplication/src/components/SeriesItem.vue new file mode 100644 index 0000000..7ecd655 --- /dev/null +++ b/WebApplication/src/components/SeriesItem.vue @@ -0,0 +1,140 @@ + + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/SeriesList.vue b/WebApplication/src/components/SeriesList.vue new file mode 100644 index 0000000..8da43f7 --- /dev/null +++ b/WebApplication/src/components/SeriesList.vue @@ -0,0 +1,149 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/Settings.vue b/WebApplication/src/components/Settings.vue new file mode 100644 index 0000000..e886ec2 --- /dev/null +++ b/WebApplication/src/components/Settings.vue @@ -0,0 +1,221 @@ + + + \ No newline at end of file diff --git a/WebApplication/src/components/SettingsLabels.vue b/WebApplication/src/components/SettingsLabels.vue new file mode 100644 index 0000000..c2d0b8e --- /dev/null +++ b/WebApplication/src/components/SettingsLabels.vue @@ -0,0 +1,222 @@ + + + \ No newline at end of file diff --git a/WebApplication/src/components/SettingsPermissions.vue b/WebApplication/src/components/SettingsPermissions.vue new file mode 100644 index 0000000..7c8db11 --- /dev/null +++ b/WebApplication/src/components/SettingsPermissions.vue @@ -0,0 +1,427 @@ + + + \ No newline at end of file diff --git a/WebApplication/src/components/ShareModal.vue b/WebApplication/src/components/ShareModal.vue new file mode 100644 index 0000000..c93d1c1 --- /dev/null +++ b/WebApplication/src/components/ShareModal.vue @@ -0,0 +1,200 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/SideBar.vue b/WebApplication/src/components/SideBar.vue new file mode 100644 index 0000000..ad7a02c --- /dev/null +++ b/WebApplication/src/components/SideBar.vue @@ -0,0 +1,488 @@ + + + \ No newline at end of file diff --git a/WebApplication/src/components/StudyDetails.vue b/WebApplication/src/components/StudyDetails.vue new file mode 100644 index 0000000..99b0a69 --- /dev/null +++ b/WebApplication/src/components/StudyDetails.vue @@ -0,0 +1,194 @@ + + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/StudyItem.vue b/WebApplication/src/components/StudyItem.vue new file mode 100644 index 0000000..0cb0949 --- /dev/null +++ b/WebApplication/src/components/StudyItem.vue @@ -0,0 +1,328 @@ + + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/StudyList.vue b/WebApplication/src/components/StudyList.vue new file mode 100644 index 0000000..4b8c244 --- /dev/null +++ b/WebApplication/src/components/StudyList.vue @@ -0,0 +1,1232 @@ + + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/TagsTree.vue b/WebApplication/src/components/TagsTree.vue new file mode 100644 index 0000000..2cf557a --- /dev/null +++ b/WebApplication/src/components/TagsTree.vue @@ -0,0 +1,84 @@ + + + + + + diff --git a/WebApplication/src/components/TokenLanding.vue b/WebApplication/src/components/TokenLanding.vue new file mode 100644 index 0000000..ace1dcc --- /dev/null +++ b/WebApplication/src/components/TokenLanding.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/TokenLinkButton.vue b/WebApplication/src/components/TokenLinkButton.vue new file mode 100644 index 0000000..c2ce3ac --- /dev/null +++ b/WebApplication/src/components/TokenLinkButton.vue @@ -0,0 +1,142 @@ + + + + + \ No newline at end of file diff --git a/WebApplication/src/components/UploadHandler.vue b/WebApplication/src/components/UploadHandler.vue new file mode 100644 index 0000000..e32abeb --- /dev/null +++ b/WebApplication/src/components/UploadHandler.vue @@ -0,0 +1,216 @@ + + + + \ No newline at end of file diff --git a/WebApplication/src/components/UploadReport.vue b/WebApplication/src/components/UploadReport.vue new file mode 100644 index 0000000..e7b3ac0 --- /dev/null +++ b/WebApplication/src/components/UploadReport.vue @@ -0,0 +1,109 @@ + + + + \ No newline at end of file diff --git a/WebApplication/src/globalConfigurations.js b/WebApplication/src/globalConfigurations.js new file mode 100644 index 0000000..1d9e55c --- /dev/null +++ b/WebApplication/src/globalConfigurations.js @@ -0,0 +1,3 @@ +export var baseOe2Url = window.location.pathname.slice(0, -1); +export var orthancApiUrl = '../../' +export var oe2ApiUrl = '../api/'; \ No newline at end of file diff --git a/WebApplication/src/helpers/clipboard-helpers.js b/WebApplication/src/helpers/clipboard-helpers.js new file mode 100644 index 0000000..62a0f48 --- /dev/null +++ b/WebApplication/src/helpers/clipboard-helpers.js @@ -0,0 +1,39 @@ +export default { + copyToClipboard(text, onSuccess, onFailure) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text).then( + () => { + if (onSuccess !== undefined) { + onSuccess(); + } else { + console.log("copied to clipboard: ", text) + } + }, + () => { + if (onFailure !== undefined) { + onFailure(); + } else { + console.log("failed to copy to clipboard") + } + } + ) + } else { // this happens, e.g in unsecure contexts (https://discourse.orthanc-server.org/t/copy-icon-oe2-problem/3958/4) + const tmp = document.createElement('TEXTAREA'); + const focus = document.activeElement; + + tmp.value = text; + + document.body.appendChild(tmp); + tmp.select(); + document.execCommand('copy'); // note: this is deprecated but we use this code only as a fallback + document.body.removeChild(tmp); + focus.focus(); + + if (onSuccess !== undefined) { + onSuccess(); + } else { + console.log("hopefully copied to clipboard: ", text) + } + } + }, +} diff --git a/WebApplication/src/helpers/date-helpers.js b/WebApplication/src/helpers/date-helpers.js new file mode 100644 index 0000000..5399d33 --- /dev/null +++ b/WebApplication/src/helpers/date-helpers.js @@ -0,0 +1,91 @@ +import { parse, format, endOfMonth, endOfYear, startOfMonth, startOfYear, subMonths, subDays, startOfWeek, endOfWeek, subYears } from 'date-fns'; + +document._datePickerPresetRanges = [ + { tLabel: 'date_picker.today', value: [new Date(), new Date()] }, + { tLabel: 'date_picker.yesterday', value: [subDays(new Date(), 1), subDays(new Date(), 1)] }, + { tLabel: 'date_picker.this_week', value: [startOfWeek(new Date(), { weekStartsOn: 1 }), new Date()] }, + { tLabel: 'date_picker.last_week', value: [startOfWeek(subDays(new Date(), 7), { weekStartsOn: 1 }), endOfWeek(subDays(new Date(), 7), { weekStartsOn: 1 })] }, + { tLabel: 'date_picker.this_month', value: [startOfMonth(new Date()), endOfMonth(new Date())] }, + { tLabel: 'date_picker.last_month', value: [startOfMonth(subMonths(new Date(), 1)), endOfMonth(subMonths(new Date(), 1))] }, + { tLabel: 'date_picker.last_12_months', value: [subYears(new Date(), 1), new Date()] }, +]; + +export default { + parse(dateStr, format) { + return parse(dateStr, format, 0); + }, + toDicomDate(date) { + return (date.getFullYear() * 10000 + (date.getMonth()+1) * 100 + date.getDate()).toString(); + }, + fromDicomDate(dateStr) { + let match = null; + match = dateStr.match(/^(\d{4})(\d{1,2})(\d{1,2})$/); // yyyymmdd (DICOM) + if (match) { + return new Date(match[1], match[2]-1, match[3]); + } + return null; + }, + parseDateForDatePicker(str) { + let match = null; + match = str.match(/^(\d{4})(\d{1,2})(\d{1,2})$/); // yyyymmdd (DICOM) + if (match) { + return [new Date(match[1], match[2]-1, match[3]), null]; + } + match = str.match(/^(\d{4})(\d{1,2})(\d{1,2})\-(\d{4})(\d{1,2})(\d{1,2})$/); // yyyymmdd-yyyymmdd (DICOM range) + if (match) { + return [new Date(match[1], match[2]-1, match[3]), new Date(match[4], match[5]-1, match[6])]; + } + // match = str.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/); // dd/mm/yyyy or dd-mm-yyyy + // if (match) { + // return [new Date(match[3], match[2]-1, match[1]), null]; + // } + // match = str.match(/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/); // yyyy/mm/dd or yyyy-mm-dd + // if (match) { + // return [new Date(match[1], match[2]-1, match[3]), null]; + // } + return null; + }, + dicomDateFromDatePicker(dates) { + let output = ""; + if (dates == null) { + output = null; + } + else if (dates instanceof Date) { + output = this.toDicomDate(dates); + } + else if (dates instanceof Array) { + if (dates.length == 2 && (dates[0] != null && dates[0].getFullYear != undefined) && (dates[1] != null && dates[1].getFullYear != undefined)) { + if (this.toDicomDate(dates[0]) == this.toDicomDate(dates[1])) { + output = this.toDicomDate(dates[0]); + } else { + output = this.toDicomDate(dates[0]) + "-" + this.toDicomDate(dates[1]); + } + } else if (dates.length >= 1 && dates[0].getFullYear != undefined) { + output = this.toDicomDate(dates[0]); + } else if (dates.length == 2 && typeof dates[0] == 'string' && typeof dates[1] == 'string') { + output = dates[0] + "-" + dates[1]; + } else if (dates.length == 1 && typeof dates[0] == 'string') { + output = dates[0]; + } + } + return output; + }, + formatDateForDisplay(dicomDate, dateFormat) { + if (!dicomDate) { + return ""; + } + if (dicomDate && dicomDate.length == 8) { + let d = parse(dicomDate, "yyyyMMdd", new Date()); + if (!isNaN(d.getDate())) { + return format(d, dateFormat); + } + } + if (dicomDate.length > 0) { + return dicomDate; + } + return ""; + }, + isDateTag(tagName) { + return ["StudyDate", "PatientBirthDate", "SeriesDate", "AcquisitionDate", "ContentDate"].indexOf(tagName) != -1; + } +} diff --git a/WebApplication/src/helpers/label-helpers.js b/WebApplication/src/helpers/label-helpers.js new file mode 100644 index 0000000..7ac2356 --- /dev/null +++ b/WebApplication/src/helpers/label-helpers.js @@ -0,0 +1,22 @@ +export default { + filterLabel(str) { + const regexLabel = new RegExp("^[0-9\-\_a-zA-Z]+$"); + if (str && str.length > 0 && !regexLabel.test(str)) { + console.log("invalid label: ", str); + const invalidLabelTips = document.querySelectorAll('.invalid-label-tips'); + invalidLabelTips.forEach(element => { + element.classList.remove('invalid-label-tips-hidden'); + }) + setTimeout(() => { + const invalidLabelTips = document.querySelectorAll('.invalid-label-tips'); + invalidLabelTips.forEach(element => { + element.classList.add('invalid-label-tips-hidden'); + }) + }, 8000); + } else { + // console.log("valid '" + str + "'"); + } + + return str.replace(/[^0-9\-\_a-zA-Z]/gi, ''); + } +} \ No newline at end of file diff --git a/WebApplication/src/helpers/resource-helpers.js b/WebApplication/src/helpers/resource-helpers.js new file mode 100644 index 0000000..e226960 --- /dev/null +++ b/WebApplication/src/helpers/resource-helpers.js @@ -0,0 +1,89 @@ +import store from "../store" +import api from "../orthancApi" + +export default { + getResourceTitle(resourceType, patientMainDicomTags, studyMainDicomTags, seriesMainDicomTags, instanceTags) { + let title = []; + + function addIfExists(title, key, dico) { + if (key in dico) { + title.push(dico[key]); + return true; + } + return false; + }; + + if (resourceType == "study" || resourceType == "series") { + addIfExists(title, "PatientID", patientMainDicomTags) + addIfExists(title, "StudyID", studyMainDicomTags) + addIfExists(title, "StudyDate", studyMainDicomTags) + addIfExists(title, "StudyDescription", studyMainDicomTags) + } + if (resourceType == "series" || resourceType == "instance") { + addIfExists(title, "SeriesNumber", seriesMainDicomTags); + addIfExists(title, "SeriesDescription", seriesMainDicomTags); + } + if (resourceType == "instance") { + if ("0020,0013" in instanceTags) { + title.push("instance # " + instanceTags["0020,0013"]["Value"]); + } else { + title.push("instance"); + } + } + + return title.join(" | "); + }, + + patientNameCapture : "([^\\^]+)\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?", + patientNameFormatting : null, + formatPatientName(originalPatientName) { + if (originalPatientName && this.patientNameFormatting && this.patientNameCapture) { + return originalPatientName.replace(new RegExp(this.patientNameCapture), this.patientNameFormatting); + } else { + return originalPatientName; + } + }, + + getPrimaryViewerUrl(level, orthancId, dicomId) { + if (store.state.configuration.uiOptions.ViewersOrdering.length > 0) { + for (let viewer of store.state.configuration.uiOptions.ViewersOrdering) { + if ((["osimis-web-viewer", "stone-webviewer", "volview", "wsi"].indexOf(viewer) != -1 && viewer in store.state.configuration.installedPlugins) || + (viewer.startsWith("ohif") && viewer in store.state.configuration.installedPlugins) || + (viewer.startsWith("ohif") && store.state.configuration.uiOptions.EnableOpenInOhifViewer3) || + (viewer == "meddream" && store.state.configuration.uiOptions.EnableOpenInMedDreamViewer)) + { + return this.getViewerUrl(level, orthancId, dicomId, viewer); + } + } + } + return null; + }, + + getViewerUrl(level, orthancId, dicomId, viewer) { + if (viewer == 'osimis-web-viewer') { + return api.getOsimisViewerUrl(level, orthancId); + } else if (viewer == 'stone-webviewer') { + return api.getStoneViewerUrl(level, dicomId) + } else if (viewer == 'volview') { + return api.getVolViewUrl(level, orthancId); + } else if (viewer == 'ohif') { + if (store.state.configuration.ohifDataSource == 'dicom-web') { + return api.getOhifViewerUrlForDicomWeb('basic', dicomId); + } else { + return api.getOhifViewerUrlForDicomJson('basic', dicomId); + } + } else if (viewer == 'ohif-vr') { + return api.getOhifViewerUrl('vr'); + } else if (viewer == 'ohif-tmtv') { + return api.getOhifViewerUrl('tmtv'); + } else if (viewer == 'ohif-seg') { + return api.getOhifViewerUrl('seg'); + } else if (viewer == 'ohif-micro') { + return api.getOhifViewerUrl('microscopy'); + } else if (viewer == 'wsi') { + return api.getWsiViewerUrl(orthancId); // note: this must be a series ID ! + } else if (viewer == 'meddream') { + return store.state.configuration.uiOptions.MedDreamViewerPublicRoot + "?study=" + dicomId; + } + } +} diff --git a/WebApplication/src/helpers/source-type.js b/WebApplication/src/helpers/source-type.js new file mode 100644 index 0000000..6e18788 --- /dev/null +++ b/WebApplication/src/helpers/source-type.js @@ -0,0 +1,7 @@ +const SourceType = Object.freeze({ + LOCAL_ORTHANC: 0, + REMOTE_DICOM: 1, + REMOTE_DICOM_WEB: 2 +}); + +export default SourceType; \ No newline at end of file diff --git a/WebApplication/src/locales/de.json b/WebApplication/src/locales/de.json new file mode 100644 index 0000000..381a362 --- /dev/null +++ b/WebApplication/src/locales/de.json @@ -0,0 +1,213 @@ +{ + "all_modalities": "Alle", + "anonymize": "Anonymisieren", + "cancel": "Abbrechen", + "change_password": "Passwort ändern", + "close": "Schließen", + "copy_orthanc_id": "Orthanc-ID kopieren", + "date_picker": { + "last_12_months": "Letzten 12 Monate", + "last_month": "Letzten Monat", + "last_week": "Letzte Woche", + "this_month": "Diesen Monat", + "this_week": "Diese Woche", + "today": "Heute", + "yesterday": "Gestern" + }, + "default": "Standard", + "delete": "Löschen", + "delete_instance_body": "Sind Sie sicher, dass Sie diese Instanz löschen möchten?
Diese Aktion kann nicht rückgängig gemacht werden!", + "delete_instance_title": "Instanz löschen?", + "delete_series_body": "Sind Sie sicher, dass Sie diese Serie löschen möchten?
Diese Aktion kann nicht rückgängig gemacht werden!", + "delete_series_title": "Serien löschen?", + "delete_studies_body": "Sind Sie sicher, dass Sie diese Studien löschen möchten?
Diese Aktion kann nicht rückgängig gemacht werden!", + "delete_studies_title": "Studien löschen?", + "delete_study_body": "Sind Sie sicher, dass Sie diese Studie löschen möchten?
Diese Aktion kann nicht rückgängig gemacht werden!", + "delete_study_title": "Studie löschen?", + "description": "Beschreibung", + "dicom_modalities": "DICOM Modalitäten", + "dicom_tags": { + "AccessionNumber": "Zugangsnummer", + "BodyPartExamined": "Untersuchungsregion", + "InstanceNumber": "Instanznummer", + "InstitutionName": "Institutionsname", + "ModalitiesInStudy": "Modalitäten in der Studie", + "Modality": "Modalität", + "NumberOfFrames": "Anzahl der Frames", + "NumberOfStudyRelatedSeries": "Anzahl der studienbezogenen Serien", + "PatientBirthDate": "Geburtsdatum des Patienten", + "PatientID": "Patienten-ID", + "PatientName": "Patientenname", + "PatientSex": "Geschlecht des Patienten", + "ProtocolName": "Protokollname", + "ReferringPhysicianName": "Überweisender Arzt", + "RequestingPhysician": "Anfordernder Arzt", + "SOPInstanceUID": "SOP-Instanz-UID", + "SeriesDate": "Serien-Datum", + "SeriesDescription": "Serienbeschreibung", + "SeriesInstanceUID": "Serieninstanz-UID", + "SeriesNumber": "Seriennummer", + "SeriesTime": "Serienzeit", + "StudyDate": "Studiendatum", + "StudyDescription": "Studienbeschreibung", + "StudyID": "Studien-ID", + "StudyInstanceUID": "Studieninstanz-UID", + "StudyTime": "Studienzeit", + "seriesCount": "Anzahl der Serien" + }, + "dicom_web_servers": "DICOM-Webserver", + "displaying_latest_studies": "Anzeige der neuesten Studien", + "download_dicom_file": "DICOM-Datei herunterladen", + "download_dicomdir": "DICOMDIR herunterladen", + "download_zip": "ZIP herunterladen", + "drop_files": "Dateien hier ablegen", + "enter_search": "Geben Sie Suchkriterien ein, um Ergebnisse anzuzeigen!", + "error": "Fehler", + "file": "Datei", + "files": "Dateien", + "frames": "Frames", + "instance": "Instanz", + "instances": "Instanzen", + "instances_number": "Anzahl der Instanzen", + "labels": { + "add_button": "Hinzufügen", + "add_labels_message_html": "Hinzufügen dieser Labels zu allen ausgewählten Ressourcen", + "add_labels_placeholder": "Labels hinzufügen, drücken Sie Enter, um ein neues Label zu erstellen", + "added_labels_message_part_1_html": "Hinzugefügte Labels ", + "added_labels_message_part_2_html": " zu {count} Studien", + "clear_all_button": "Alle entfernen", + "clear_all_labels_message_html": "Entferne alle Labels in allen ausgewählten Ressourcen", + "cleared_labels_message_part_1_html": "Entfernte Labels ", + "cleared_labels_message_part_2_html": " von {count} Studien", + "edit_labels_button": "Labels bearbeiten", + "modal_title": "Labels in ausgewählten Ressourcen bearbeiten", + "remove_button": "Entfernen", + "remove_labels_message_html": "Entfernen dieser Labels aus allen ausgewählten Ressourcen", + "remove_labels_placeholder": "Labels entfernen", + "removed_labels_message_part_1_html": "Entfernte Labels ", + "removed_labels_message_part_2_html": " von {count} Studien", + "study_details_title": "Labels" + }, + "legacy_ui": "Alte Benutzeroberfläche", + "loading_latest_studies": "Laden der neuesten Studien", + "local_studies": "Alle lokalen Studien", + "logout": "Abmelden", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "Modalitäten in der Studie", + "modify": { + "anonymize_button_title": "Anonymisieren", + "anonymize_modal_title": "Anonymisieren", + "autogenerated_dicom_uid": "Automatisch generiert", + "back_button_title": "Zurück", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Fehler beim Ändern der Serie, keine Studie mit dieser Studieninstanz-UID gefunden.", + "error_attach_series_to_existing_study_target_unchanged_html": "Fehler beim Ändern der Serie, die Zielstudie ist dieselbe wie die aktuelle Serienstudie.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Fehler beim Ändern des Patienten, kein Patient mit dieser Patienten-ID gefunden. Bitte verwenden Sie die Ändern von Studien-Tags Funktion.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Fehler beim Ändern von Studien-Tags: Dieser Patient hat bereits andere Studien. Bitte verwenden Sie die Patient-Tags ändern Funktion.", + "error_modify_any_study_tags_patient_exists_html": "Fehler beim Ändern von Studien-Tags: Ein anderer Patient mit derselben Patienten-ID existiert bereits. Bitte verwenden Sie die Patient ändern Funktion.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Fehler beim Ändern von Patienten-Tags: Ein anderer Patient mit derselben Patienten-ID existiert bereits. Bitte verwenden Sie die Patient-Tags ändern Funktion.", + "error_modify_unexpected_error_html": "Unerwarteter Fehler während der Änderung, überprüfen Sie die Orthanc-Protokolle", + "insert_tag": "DICOM-Tag einfügen", + "job_in_progress": "Änderung wird verarbeitet", + "modify_button_title": "Ändern", + "modify_modal_title": "Ändern", + "modify_study_mode_duplicate_html": "Erstellen Sie eine Kopie der Originalstudie mit Ihren Änderungen. (generiert neue DICOM-UIDs und behält die Originalstudie)", + "modify_study_mode_keep_uids_html": "Originalstudie verändern. (behalten die originalen DICOM-UIDs)", + "modify_study_mode_new_uids_html": "Originalstudie verändern. (generiert neue DICOM-UIDs)", + "remove_tag_tooltip": "Tag entfernen", + "remove_tag_undo_tooltip": "Tag entfernen rückgängig machen", + "series_step_0_attach_series_to_existing_study_button_title_html": "Studie ändern", + "series_step_0_attach_series_to_existing_study_html": "Diese Serie zu einer vorhandenen Studie verschieben.", + "series_step_0_create_new_study_button_title_html": "Neue Studie erstellen", + "series_step_0_create_new_study_html": "Diese Serie in eine neue Studie verschieben.", + "series_step_0_modify_series_button_title_html": "Seriendaten ändern", + "series_step_0_modify_series_html": "Seriendaten in dieser Serie ändern?", + "show_modified_resources": "Modifizierte Ressourcen anzeigen", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Patient ändern", + "study_step_0_attach_study_to_existing_patient_html": "Diese Studie an einen anderen vorhandenen Patienten anhängen.", + "study_step_0_modify_study_button_title_html": "Studien-Tags ändern", + "study_step_0_modify_study_html": "Patienten-/Studien-Tags in dieser Studie ändern?", + "study_step_0_patient_has_other_studies_button_title_html": "Patienten-Tags ändern", + "study_step_0_patient_has_other_studies_html": "Dieser Patient hat insgesamt {count} Studien.
Möchten Sie den Patienten in all diesen Studien ändern?", + "warning_create_new_study_from_series_html": "Ein Patient mit derselben Patienten-ID existiert bereits mit unterschiedlichen Tags. Möchten Sie den vorhandenen Patientennamen, das Geschlecht und das Geburtsdatum verwenden?" + }, + "my_jobs": "Meine Aufträge", + "no_modalities": "Keine", + "no_result_found": "Kein Ergebnis gefunden!", + "not_showing_all_results": "Es können nicht alle Ergebnisse angezeigt werden. Sie sollten Ihre Suchkriterien verfeinern", + "open": "Öffnen", + "page_not_found": "Seite nicht gefunden!", + "plugin": "Plugin", + "preview": "Vorschau", + "profile": "Profil", + "retrieve_and_view": { + "finding_locally": "Überprüfen, ob die Studie bereits lokal verfügbar ist.", + "finding_remotely": "Suchen der Studie in Remote-DICOM-Knoten.", + "not_found": "Die Studie wurde weder lokal noch in Remote-DICOM-Knoten gefunden.", + "retrieved_html": "Abgerufene {count} Instanzen.", + "retrieving": "Studie abrufen." + }, + "searching": "Suchen...", + "select_files": "Dateien auswählen", + "select_folder": "Ordner auswählen", + "send_to": { + "button_title": "Senden", + "dicom": "An DICOM-Knoten senden", + "dicom_web": "An DICOMWeb-Server senden", + "orthanc_peer": "An Orthanc Peer senden", + "transfers": "An Orthanc Peer senden (erweiterte Übertragungen)" + }, + "series": "Serie", + "series_count_header": "# Serien", + "series_plural": "Serien", + "settings": { + "dicom_AET": "DICOM AET", + "dicom_port": "DICOM-Port", + "ingest_transcoding": "Transcoding übernehmen", + "installed_plugins": "Installierte Plugins", + "orthanc_name": "Orthanc-Name", + "orthanc_system_info": "Orthanc-Systeminformationen", + "orthanc_version": "Orthanc-Version", + "overwrite_instances": "Instanzen überschreiben", + "plugins_not_enabled": "Geladene, aber nicht aktivierte oder nicht korrekt konfigurierte Plugins sind ", + "statistics": "Statistiken", + "storage_compression": "Speicherkompression", + "storage_size": "Speichergröße", + "striked_through": "durchgestrichen", + "title": "Einstellungen", + "verbosity_level": "Ausführlichkeitsstufe" + }, + "share": { + "anonymize": "Anonymisieren?", + "button_title": "Teilen", + "copy_and_close": "Kopieren und Schließen", + "days": "Tage", + "expires_in": "Läuft ab in", + "link": "Link", + "modal_title": "Studie teilen", + "never": "niemals" + }, + "show_errors": "Fehler anzeigen", + "studies": "Studien", + "study": "Studie", + "this_patient_has_no_other_studies": "Dieser Patient hat keine anderen Studien.", + "this_patient_has_other_studies": "Dieser Patient hat insgesamt {count} Studien.", + "this_patient_has_other_studies_show": "Andere Studien anzeigen!", + "token": { + "error_token_expired_html": "Ihr Token ist abgelaufen.", + "error_token_invalid_html": "Ihr Token ist ungültig.
Überprüfen Sie, ob Sie es vollständig eingefügt haben, oder kontaktieren Sie die Person, die es Ihnen zur Verfügung gestellt hat.", + "error_token_unknown_html": "Ihr Token ist ungültig.
Überprüfen Sie, ob Sie es vollständig eingefügt haben, oder kontaktieren Sie die Person, die es Ihnen zur Verfügung gestellt hat.", + "token_being_checked_html": "Ihr Token wird überprüft." + }, + "trace": "Trace", + "upload": "Hochladen", + "uploaded_studies": "Hochgeladene Studien", + "verbose": "Ausführlich", + "view_in_meddream": "In MedDream anzeigen", + "view_in_ohif": "In OHIF anzeigen", + "view_in_ohif_tmtv": "In OHIF Total Metabolic Tumor Volume-Modus anzeigen", + "view_in_ohif_vr": "In OHIF Volumenrendering-Modus anzeigen", + "view_in_osimis": "In OsimisViewer anzeigen", + "view_in_stone": "In StoneViewer anzeigen", + "view_in_volview": "In VolView anzeigen", + "view_in_wsi_viewer": "In Whole Slide Imaging Viewer anzeigen" +} \ No newline at end of file diff --git a/WebApplication/src/locales/en.json b/WebApplication/src/locales/en.json new file mode 100644 index 0000000..2f5fa25 --- /dev/null +++ b/WebApplication/src/locales/en.json @@ -0,0 +1,270 @@ +{ + "add_series": { + "button_title": "Add a series", + "drop_file": "Drop a single PDF, image or STL file here or", + "error_add_series_unexpected_error_html": "Unexpected error while adding a series, check Orthanc logs", + "modal_title": "Add a new series to study", + "select_file": "Select a file", + "unrecognized_file_format": "Unrecognized file format. Only PDF, image files and STL files are supported", + "uploaded_file": "The file {name} ({size}) is of {type} type." + }, + "all_modalities": "All", + "anonymize": "Anonymize", + "cancel": "Cancel", + "change_password": "Change password", + "close": "Close", + "copy_orthanc_id": "Copy Orthanc ID", + "date_picker": { + "last_12_months": "Last 12 months", + "last_month": "Last month", + "last_week": "Last week", + "this_month": "This month", + "this_week": "This week", + "today": "Today", + "yesterday": "Yesterday" + }, + "default": "Default", + "delete": "Delete", + "delete_instance_body": "Are you sure you want to delete this instance?
This action can not be undone!", + "delete_instance_title": "Delete instance?", + "delete_series_body": "Are you sure you want to delete this series?
This action can not be undone!", + "delete_series_title": "Delete series?", + "delete_studies_body": "Are you sure you want to delete these studies?
This action can not be undone!", + "delete_studies_title": "Delete studies?", + "delete_study_body": "Are you sure you want to delete this study?
This action can not be undone!", + "delete_study_title": "Delete study?", + "description": "Description", + "dicom_modalities": "DICOM Modalities", + "dicom_tags": { + "AccessionNumber": "Accession number", + "BodyPartExamined": "Body part examined", + "InstanceNumber": "Instance number", + "InstitutionName": "Institution Name", + "ModalitiesInStudy": "Modalities in Study", + "Modality": "Modality", + "NumberOfFrames": "Number of frames", + "NumberOfStudyRelatedSeries": "Number of series", + "OtherPatientIDs": "Patient Other IDs", + "PatientBirthDate": "Patient Birth Date", + "PatientID": "Patient ID", + "PatientName": "Patient Name", + "PatientSex": "Patient sex", + "ProtocolName": "Protocol Name", + "ReferringPhysicianName": "Referring Physician Name", + "RequestingPhysician": "Requesting Physician", + "SOPInstanceUID": "SOP Instance UID", + "SeriesDate": "Series Date", + "SeriesDescription": "Series Description", + "SeriesInstanceUID": "Series Instance UID", + "SeriesNumber": "Series number", + "SeriesTime": "Series Time", + "StudyDate": "Study Date", + "StudyDescription": "Study Description", + "StudyID": "Study ID", + "StudyInstanceUID": "Study Instance UID", + "StudyTime": "Study Time", + "seriesCount": "Number of series" + }, + "dicom_web_servers": "DICOM-Web Servers", + "displaying_latest_studies": "Displaying most recent studies", + "download_dicom_file": "Download DICOM file", + "download_dicomdir": "Download DICOMDIR", + "download_zip": "Download ZIP", + "drop_files": "Drop files here or", + "enter_search": "Enter a search criteria to show results!", + "error": "Error", + "file": "File", + "files": "files", + "frames": "frames", + "instance": "Instance", + "instances": "Instances", + "instances_count_header": "# instances", + "instances_number": "Number of instances", + "job_success": "Success", + "job_failure": "Failure", + "labels": { + "add_button": "Add", + "add_labels_message_html": "Add these labels to all selected resources", + "add_labels_placeholder": "Labels to add, press Enter to create or add a new one", + "add_labels_placeholder_no_create": "Labels to add", + "added_labels_message_part_1_html": "Added labels ", + "added_labels_message_part_2_html": " to {count} studies", + "clear_all_button": "Remove all", + "clear_all_labels_message_html": "Remove all labels in all selected resources", + "cleared_labels_message_part_1_html": "Removed labels ", + "cleared_labels_message_part_2_html": " from {count} studies", + "edit_labels_button": "Edit labels", + "modal_title": "Modify labels in selected resources", + "remove_button": "Remove", + "remove_labels_message_html": "Remove these labels from all selected resources", + "remove_labels_placeholder": "Labels to remove", + "removed_labels_message_part_1_html": "Removed labels ", + "removed_labels_message_part_2_html": " from {count} studies", + "study_details_title": "Labels", + "valid_alphabet_warning": "Only allowed characters in labels are: latin letters, numbers, - and _" + }, + "legacy_ui": "Legacy UI", + "loading_latest_studies": "Loading most recent studies", + "local_studies": "All local Studies", + "logout": "Logout", + "mammography_plugin": "Start mammography AI", + "modalities_in_study": "Modalities in Study", + "modify": { + "anonymize_button_title": "Anonymize", + "anonymize_modal_title": "Anonymize", + "autogenerated_dicom_uid": "Auto-generated", + "back_button_title": "Back", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Error while changing series, no study found with this StudyInstanceUID.", + "error_attach_series_to_existing_study_target_unchanged_html": "Error while changing series, the target study is the same as the current series study.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Error while changing patient, no patient found with this PatientID. You should probably use the Modify Study tags function instead.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Error while modifyting study tags: this patient has other studies. You should use the Modify Patient tags function instead.", + "error_modify_any_study_tags_patient_exists_html": "Error while modifying study tags: another patient with the same PatientID already exists. You should use the Change patient function instead.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Error while modifying a patient tags: another patient with the same PatientID already exists. You should use the Modify Patient tags function instead.", + "error_modify_unexpected_error_html": "Unexpected error during modification, check Orthanc logs", + "insert_tag": "Insert a DICOM Tag", + "job_in_progress": "Modification is being processed", + "modify_button_title": "Modify", + "modify_modal_title": "Modify", + "modify_series_mode_duplicate_html": "Create a modified copy of the original series. (generating new DICOM UIDs and keeping the original series)", + "modify_series_mode_keep_uids_html": "Modify the original series. (keeping the original DICOM UIDs)", + "modify_series_mode_new_uids_html": "Modify the original series. (generating new DICOM UIDs)", + "modify_study_mode_duplicate_html": "Create a modified copy of the original study. (generating new DICOM UIDs and keeping the original study)", + "modify_study_mode_keep_uids_html": "Modify the original study. (keeping the original DICOM UIDs)", + "modify_study_mode_new_uids_html": "Modify the original study. (generating new DICOM UIDs)", + "remove_tag_tooltip": "Remove tag", + "remove_tag_undo_tooltip": "Undo remove tag", + "series_step_0_attach_series_to_existing_study_button_title_html": "Change study", + "series_step_0_attach_series_to_existing_study_html": "Move this series to an existing Study.", + "series_step_0_create_new_study_button_title_html": "Create new study", + "series_step_0_create_new_study_html": "Move this series to a new study.", + "series_step_0_modify_series_button_title_html": "Modify series tags", + "series_step_0_modify_series_html": "Modify Series tags in this series only?", + "show_modified_resources": "Show modified resources", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Change patient", + "study_step_0_attach_study_to_existing_patient_html": "Attach this study to another existing Patient.", + "study_step_0_modify_study_button_title_html": "Modify Study tags", + "study_step_0_modify_study_html": "Modify Patient/Study tags in this study only?", + "study_step_0_patient_has_other_studies_button_title_html": "Modify Patient tags", + "study_step_0_patient_has_other_studies_html": "This patient has {count} studies in total.
Do you want to modify the patient in all these studies?", + "warning_create_new_study_from_series_html": "A patient with the same PatientID already exists with differing tags. Do you want to use the existing PatientName, PatientSex and PatientBirthDate?" + }, + "my_jobs": "My jobs", + "no_modalities": "None", + "no_result_found": "No result found!", + "not_showing_all_results": "Not showing all results. You should refine your search criteria", + "open": "Open", + "page_not_found": "Page not found!", + "patients": "Patients", + "plugin": "plugin", + "plugins": { + "delayed_deletion": { + "pending_files_count": "Delayed Deletion plugin: Files to delete" + } + }, + "preview": "Preview", + "profile": "Profile", + "retrieve": "Retrieve", + "retrieve_and_view": { + "finding_locally": "Checking if the study is already available locally.", + "finding_remotely": "Searching for the study in remote DICOM nodes.", + "not_found": "The study was not found neither locally nor in remote DICOM nodes.", + "retrieved_html": "Retrieved {count} instances.", + "retrieving": "Retrieving study." + }, + "remote_dicom_browsing": "You are currently browsing the {source} remote DICOM node", + "remote_dicom_web_browsing": "You are currently browsing the {source} remote DICOMWeb server", + "save": "Save", + "searching": "Searching...", + "select_files": "Select Files", + "select_folder": "Select Folder", + "send_to": { + "button_title": "Send", + "dicom": "Send to DICOM node", + "dicom_web": "Send to DICOMWeb server", + "orthanc_peer": "Send to Orthanc Peer", + "transfers": "Send to Orthanc Peer (advanced-transfers)" + }, + "series": "Series", + "series_count_header": "# series", + "series_and_instances_count_header": "# Ser/Inst", + "series_plural": "Series", + "settings": { + "allow_all_currents_and_futures_labels": "All current and future labels", + "available_labels_global_instructions_html": "Since you have enabled permissions and you are an administrator, this page enables you to create/delete the available labels. Users won't be able to create other labels.
Once you have updated this list, don't forget to click Save to apply your changes.", + "available_labels_title": "Available labels", + "create_new_label": "Create new label", + "dicom_AET": "DICOM AET", + "dicom_port": "DICOM Port", + "ingest_transcoding": "Ingest transcoding", + "installed_plugins": "Installed Plugins", + "labels_clear_selection": "Clear selection", + "labels_description_html": "Labels are created/deleted in the settings/Labels page.", + "labels_select_all_currents": "Select all currently defined labels", + "labels_limit_available_labels": "Limit available labels to a list", + "labels_list_empty": "The labels list is currently empty", + "labels_based_permissions_title": "Labels based permissions", + "labels_title": "Labels", + "global_permissions_title": "Global permissions", + "orthanc_name": "Orthanc Name", + "orthanc_system_info": "Orthanc System Info", + "orthanc_version": "Orthanc Version", + "overwrite_instances": "Overwrite instances", + "permissions": "Permissions", + "permissions_clear_selection": "Clear selection", + "permissions_global_instructions_html": "Edit the permissions below and click Save to apply your modifications.
For each role, select the labels whose access is allowed and the allowed permissions.", + "permissions_instructions": "The permissions are statically defined in the authorization-plugin configuration.", + "read_only": "Read only system", + "select_all_label_permissions": "Select all label permissions", + "select_all_global_permissions": "Select all global permissions", + "plugins_not_enabled": "Plugins that are loaded but not enabled or not configured correctly are ", + "roles_title": "Roles", + "roles_description_html": "The roles are created/deleted in Keycloak", + "statistics": "Statistics", + "storage_compression": "Storage Compression", + "storage_size": "Storage Size", + "striked_through": "striked-through", + "system_info": "System info", + "title": "Settings", + "verbosity_level": "Verbosity Level" + }, + "share": { + "anonymize": "Anonymize?", + "button_title": "Share", + "copy_and_close": "Copy and close", + "days": "days", + "expires_in": "Expires in", + "link": "Link", + "modal_title": "Share study", + "modal_title_multiple_studies": "Share {count} studies", + "never": "never" + }, + "show_errors": "Show errors", + "studies": "Studies", + "study": "Study", + "this_patient_has_no_other_studies": "This patient has no other studies.", + "this_patient_has_other_studies": "This patient has {count} studies in total.", + "this_patient_has_other_studies_show": "Show them!", + "this_remote_patient_has_local_studies": "This patient has {count} studies already stored locally.", + "this_study_is_already_stored_locally": "This study is already stored locally.", + "this_study_is_already_stored_locally_show": "Show it!", + "token": { + "error_token_expired_html": "Your token has expired.", + "error_token_invalid_html": "Your token is invalid.
Check if you have pasted it completely, or contact the person who has provided it to you.", + "error_token_unknown_html": "Your token is invalid.
Check if you have pasted it completely, or contact the person who has provided it to you.", + "token_being_checked_html": "Your token is being checked." + }, + "trace": "Trace", + "upload": "Upload", + "uploaded_studies": "Uploaded studies", + "verbose": "Verbose", + "view_in_meddream": "View in MedDream", + "view_in_ohif": "View in OHIF", + "view_in_ohif_microscopy": "View in OHIF Microscopy mode", + "view_in_ohif_seg": "View in OHIF Segmentation mode", + "view_in_ohif_tmtv": "View in OHIF Total Metabolic Tumor Volume mode", + "view_in_ohif_vr": "View in OHIF Volume Rendering mode", + "view_in_osimis": "View in OsimisViewer", + "view_in_stone": "View in StoneViewer", + "view_in_volview": "View in VolView", + "view_in_wsi_viewer": "View in Whole Slide Imaging Viewer" +} \ No newline at end of file diff --git a/WebApplication/src/locales/es.json b/WebApplication/src/locales/es.json new file mode 100644 index 0000000..abd4dce --- /dev/null +++ b/WebApplication/src/locales/es.json @@ -0,0 +1,190 @@ +{ + "all_modalities": "Todos", + "anonymize": "Anonimizar", + "cancel": "Cancelar", + "change_password": "Cambiar la contraseña", + "close": "Cerrar", + "copy_orthanc_id": "Copiar Orthanc ID", + "date_picker": { + "last_12_months": "Últimos 12 meses", + "last_month": "El mes pasado", + "last_week": "La semana pasada", + "this_month": "Este mes", + "this_week": "Este semana", + "today": "Hoy", + "yesterday": "Ayer" + }, + "default": "Predeterminado", + "delete": "Eliminar", + "delete_instance_body": "¿Estás seguro de que quieres eliminar este elemento?
Esta acción no se puede deshacer!", + "delete_instance_title": "¿Eliminar elemento?", + "delete_series_body": "¿Estás seguro de que quieres eliminar esta serie?
Esta acción no se puede deshacer!", + "delete_series_title": "¿Eliminar serie?", + "delete_studies_body": "¿Estás seguro de que quieres eliminar estos estudios?
Esta acción no se puede deshacer!", + "delete_studies_title": "¿Eliminar estudios?", + "delete_study_body": "¿Estás seguro de que quieres eliminar este estudio?
Esta acción no se puede deshacer!", + "delete_study_title": "¿Eliminar estudio?", + "description": "Descripción", + "dicom_modalities": "Modalidades DICOM", + "dicom_tags": { + "AccessionNumber": "Número de acceso", + "BodyPartExamined": "Parte del cuerpo examinada", + "InstanceNumber": "Número de elemento", + "InstitutionName": "Nombre de la institucion", + "ModalitiesInStudy": "Modalidades en Estudio", + "Modality": "Modalidad", + "NumberOfFrames": "Número de cuadros", + "NumberOfStudyRelatedSeries": "Recuento de serie", + "PatientBirthDate": "Fecha de nacimiento del paciente", + "PatientID": "ID del paciente", + "PatientName": "Nombre del paciente", + "PatientSex": "Sexo del paciente", + "ProtocolName": "Nombre del protocolo", + "ReferringPhysicianName": "Nombre del médico de referencia", + "RequestingPhysician": "Médico solicitante", + "SeriesDate": "Fecha de la serie", + "SeriesDescription": "Descripción de la serie", + "SeriesInstanceUID": "Series Instance UID", + "SeriesNumber": "Número de serie", + "SeriesTime": "Hora de la serie", + "StudyDate": "Fecha del estudio", + "StudyDescription": "Descripción del estudio", + "StudyID": "Identificación del estudio", + "StudyInstanceUID": "Study Instance UID", + "StudyTime": "Hora de estudio", + "seriesCount": "Recuento de serie" + }, + "dicom_web_servers": "Servidores DICOM-Web", + "displaying_latest_studies": "Se muestran los estudios más recientes", + "download_dicom_file": "Descargar archivo DICOM", + "download_dicomdir": "Descargar DICOMDIR", + "download_zip": "Descargar ZIP", + "drop_files": "Soltar archivos aquí o", + "enter_search": "¡Ingrese un criterio de búsqueda para mostrar resultados!", + "error": "Error", + "file": "Archivo", + "files": "archivos", + "frames": "cuadros", + "instance": "Elemento", + "instances": "Elementos", + "instances_number": "Número de elementos", + "legacy_ui": "IU heredada", + "loading_latest_studies": "Cargando estudios más recientes", + "local_studies": "Estudios locales", + "logout": "Cerrar sesión", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "Modalidades en Estudio", + "modify": { + "anonymize_button_title": "Anonimizar", + "anonymize_modal_title": "Anonimizar", + "autogenerated_dicom_uid": "Auto generado", + "back_button_title": "Atrás", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Error al cambiar de serie, no se encontró ningún estudio con este StudyInstanceUID.", + "error_attach_series_to_existing_study_target_unchanged_html": "Error al cambiar de serie, el estudio de destino es el mismo que el estudio de la serie actual.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Error al cambiar de paciente, no se encontró ningún paciente con este ID de paciente. Modificar etiquetas de estudio función en su lugar.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Error al modificar etiquetas de estudio: este paciente tiene otros estudios. Modificar etiquetas de pacientes función en su lugar.", + "error_modify_any_study_tags_patient_exists_html": "Error al modificar las etiquetas del estudio: ya existe otro paciente con el mismo ID de paciente. Cambiar de paciente función en su lugar.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Error al modificar las etiquetas de un paciente: ya existe otro paciente con el mismo ID de paciente. Modificar etiquetas de pacientes función en su lugar.", + "error_modify_unexpected_error_html": "Error inesperado durante la modificación, verifique los registros de Orthanc", + "insert_tag": "Insertar una etiqueta DICOM", + "job_in_progress": "Se está procesando la modificación", + "modify_button_title": "Modificar", + "modify_modal_title": "Modificar", + "modify_study_mode_duplicate_html": "Crear una copia modificada del estudio original. (generando nuevos DICOM UIDs y manteniendo el estudio original)", + "modify_study_mode_keep_uids_html": "Modificar el estudio original. (manteniendo los DICOM UIDs originales)", + "modify_study_mode_new_uids_html": "Modificar el estudio original. (generando nuevos DICOM UIDs)", + "remove_tag_tooltip": "Eliminar etiqueta", + "remove_tag_undo_tooltip": "Deshacer eliminar etiqueta", + "series_step_0_attach_series_to_existing_study_button_title_html": "Cambiar de estudio", + "series_step_0_attach_series_to_existing_study_html": "Mover esta serie a un estudio existente.", + "series_step_0_create_new_study_button_title_html": "Crear nuevo estudio", + "series_step_0_create_new_study_html": "Mover esta serie a un nuevo estudio.", + "series_step_0_modify_series_button_title_html": "Modificar etiquetas de serie", + "series_step_0_modify_series_html": "¿Modificar etiquetas de serie solo en esta serie?", + "show_modified_resources": "Mostrar recursos modificados", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Cambiar de paciente", + "study_step_0_attach_study_to_existing_patient_html": "Adjuntar este estudio a otro paciente existente.", + "study_step_0_modify_study_button_title_html": "Modificar etiquetas de estudio", + "study_step_0_modify_study_html": "¿Modificar etiquetas de paciente/estudio solo en este estudio?", + "study_step_0_patient_has_other_studies_button_title_html": "Modificar etiquetas de pacientes", + "study_step_0_patient_has_other_studies_html": "Este paciente tiene {count} estudios en total.
¿Quieres modificar al paciente en todos estos estudios?", + "warning_create_new_study_from_series_html": "Ya existe un paciente con el mismo ID de paciente con etiquetas diferentes. ¿Desea utilizar las etiquetas PatientName, PatientSex and PatientBirthDate existentes?" + }, + "my_jobs": "Mis trabajos", + "no_modalities": "Ninguno", + "no_result_found": "¡No se encontraron resultados!", + "not_showing_all_results": "No se muestran todos los resultados. Debes afinar tus criterios de búsqueda. ", + "open": "Abrir", + "page_not_found": "¡Página no encontrada!", + "plugin": "Complemento", + "preview": "Vista previa", + "profile": "Perfil", + "retrieve_and_view": { + "finding_locally": "Verificando si el estudio está disponible localmente.", + "finding_remotely": "Buscando el estudio en nodos DICOM remotos.", + "not_found": "El estudio no se encontró ni localmente ni en nodos DICOM remotos.", + "retrieved_html": "Se recuperaron {count} elementos.", + "retrieving": "Recuperando estudio." + }, + "searching": "Buscando...", + "select_files": "Seleccionar Archivos", + "select_folder": "Seleccionar Carpeta", + "send_to": { + "button_title": "Enviar", + "dicom": "Enviar a nodo DICOM", + "dicom_web": "Enviar a DICOM-Web", + "orthanc_peer": "Enviar a un par", + "transfers": "Enviar a un par (Transferencias avanzadas)" + }, + "series": "Serie", + "series_count_header": "# serie", + "series_plural": "Series", + "settings": { + "dicom_AET": "DICOM AET", + "dicom_port": "Puerto DICOM", + "ingest_transcoding": "Transcodificación adaptable", + "installed_plugins": "Complementos instalados", + "orthanc_name": "Nombre original", + "orthanc_system_info": "Información del sistema Orthanc", + "orthanc_version": "Versión original", + "overwrite_instances": "Sobrescribir elementos", + "plugins_not_enabled": "Los complementos que están cargados pero no habilitados o configurados correctamente están ", + "statistics": "Estadísticas", + "storage_compression": "Compresión de almacenamiento", + "storage_size": "Tamaño de almacenamiento", + "striked_through": "tachado", + "title": "Configuraciones", + "verbosity_level": "Nivel de Verbosidad" + }, + "share": { + "anonymize": "¿Anonimizar?", + "button_title": "Compartir", + "copy_and_close": "Copiar y cerrar", + "days": "días", + "expires_in": "Expira en", + "link": "Enlace", + "modal_title": "Compartir estudio", + "never": "nunca" + }, + "show_errors": "Mostrar errores", + "studies": "Estudios", + "study": "Estudio", + "this_patient_has_no_other_studies": "Este paciente no tiene otros estudios.", + "this_patient_has_other_studies": "Este paciente tiene {count} estudios en total.", + "this_patient_has_other_studies_show": "Mostrarlos!", + "token": { + "error_token_expired_html": "Tu token ha caducado.", + "error_token_invalid_html": "Tu token es inválido.
Comprueba que lo has pegado por completo o contacta a la persona que te lo ha facilitado.", + "error_token_unknown_html": "Tu token es inválido.
Comprueba que lo has pegado por completo o contacta a la persona que te lo ha facilitado.", + "token_being_checked_html": "Tu token está siendo verificado." + }, + "trace": "Rastreo", + "upload": "Cargar", + "uploaded_studies": "Estudios cargados", + "verbose": "Detallado", + "view_in_meddream": "Ver en MedDream", + "view_in_ohif": "Ver en OHIF", + "view_in_osimis": "Ver en OsimisViewer", + "view_in_stone": "Ver en StoneViewer", + "view_in_volview": "Ver en VolView" +} \ No newline at end of file diff --git a/WebApplication/src/locales/fr.json b/WebApplication/src/locales/fr.json new file mode 100644 index 0000000..26d3b02 --- /dev/null +++ b/WebApplication/src/locales/fr.json @@ -0,0 +1,270 @@ +{ + "add_series": { + "button_title": "Ajouter une série", + "drop_file": "Déposez ici un fichier PDF, image ou STL", + "error_add_series_unexpected_error_html": "Erreur inattendue lors de l'ajout de la sérier', vérifiez les logs d'Orthanc", + "modal_title": "Ajouter une nouvelle série à l'examen", + "select_file": "Choisir un fichier", + "unrecognized_file_format": "Format de fichier non reconnu. Seuls les format PDF, images (JPG et PNG) et STL sont supportés.", + "uploaded_file": "Le fichier {name} ({size}) est de type {type}." + }, + "all_modalities": "Toutes", + "anonymize": "Anonymiser", + "cancel": "Annuler", + "change_password": "Changer le MDP", + "close": "Fermer", + "copy_orthanc_id": "Copier l'identifiant Orthanc", + "date_picker": { + "last_12_months": "Les 12 derniers mois", + "last_month": "Le mois dernier", + "last_week": "La semaine dernière", + "this_month": "Ce mois-ci", + "this_week": "Cette semaine", + "today": "Aujourd'hui", + "yesterday": "Hier" + }, + "default": "Par défaut", + "delete": "Supprimer", + "delete_instance_body": "Êtes vous sûr de vouloir supprimer cette instance ?
Cette opération est définitive.", + "delete_instance_title": "Supprimer l'instance ?", + "delete_series_body": "Êtes vous sûr de vouloir supprimer cette série ?
Cette opération est définitive.", + "delete_series_title": "Supprimer la série ?", + "delete_studies_body": "Êtes vous sûr de vouloir supprimer ces examens ?
Cette opération est définitive.", + "delete_studies_title": "Supprimer les examens ?", + "delete_study_body": "Êtes vous sûr de vouloir supprimer cet examen ?
Cette opération est définitive.", + "delete_study_title": "Supprimer l'examen ?", + "description": "Description", + "dicom_modalities": "Modalités DICOM", + "dicom_tags": { + "AccessionNumber": "Numéro d'accès", + "BodyPartExamined": "Partie du corps examinée", + "InstanceNumber": "Numéro d'instance", + "InstitutionName": "Nom de l'établissement", + "ModalitiesInStudy": "Modalités", + "Modality": "Modalité", + "NumberOfFrames": "Nombre d'images", + "NumberOfStudyRelatedSeries": "Nombre de séries", + "OtherPatientIDs": "Autres ID Patient", + "PatientBirthDate": "Date de naissance", + "PatientID": "ID Patient", + "PatientName": "Nom", + "PatientSex": "Sexe", + "ProtocolName": "Nom du protocole", + "ReferringPhysicianName": "Nom du médecin référent", + "RequestingPhysician": "Médecin demandeur", + "SOPInstanceUID": "SOP Instance UID", + "SeriesDate": "Date de la série", + "SeriesDescription": "Description", + "SeriesInstanceUID": "Series Instance UID", + "SeriesNumber": "Numéro de série", + "SeriesTime": "Heure de la série", + "StudyDate": "Date de l'examen", + "StudyDescription": "Description", + "StudyID": "ID de l'examen", + "StudyInstanceUID": "Study Instance UID", + "StudyTime": "Heure de l'examen", + "seriesCount": "Nombre de séries" + }, + "dicom_web_servers": "Serveurs Web DICOM", + "displaying_latest_studies": "Liste des examens les plus récents", + "download_dicom_file": "Télécharger le fichier DICOM", + "download_dicomdir": "Télécharger DICOMDIR", + "download_zip": "Télécharger ZIP", + "drop_files": "Déposez les fichiers ici ou", + "enter_search": "Entrez un critère de recherche pour afficher les résultats !", + "error": "Erreur", + "file": "Fichier", + "files": "fichiers", + "frames": "Frames", + "instance": "Élément", + "instances": "Instances", + "instances_count_header": "# instances", + "instances_number": "Nombre d'instances", + "job_success": "Succès", + "job_failure": "Échec", + "labels": { + "add_button": "Ajouter", + "add_labels_message_html": "Ajouter ces étiquettes aux examens sélectionnés", + "add_labels_placeholder": "Ajouter des étiquettes, appuyer sur Enter pour en créer de nouvelles", + "add_labels_placeholder_no_create": "Ajouter des étiquettes", + "added_labels_message_part_1_html": "Les étiquettes ", + "added_labels_message_part_2_html": " ont été ajoutées aux {count} examens", + "clear_all_button": "Tout effacer", + "clear_all_labels_message_html": "Effacer toutes les étiquettes des examens sélectionnés", + "cleared_labels_message_part_1_html": "Les étiquettes ", + "cleared_labels_message_part_2_html": " ont été supprimées de {count} examens", + "edit_labels_button": "Modifier les étiquettes", + "modal_title": "Modifier les étiquettes dans les examens sélectionnés", + "remove_button": "Supprimer", + "remove_labels_message_html": "Supprimer ces étiquettes des examens sélectionnés", + "remove_labels_placeholder": "Étiquettes à supprimer", + "removed_labels_message_part_1_html": "Les étiquettes ", + "removed_labels_message_part_2_html": " ont été supprimées de {count} examens", + "study_details_title": "Étiquettes", + "valid_alphabet_warning": "Seuls les caractères suivant sont autorisé : lettres sans accents, chiffres, - et _" + }, + "legacy_ui": "Ancienne interface", + "loading_latest_studies": "Chargement des examens les plus récents", + "local_studies": "Tous les examens", + "logout": "Se déconnecter", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "Modalités dans l'examen", + "modify": { + "anonymize_button_title": "Anonymiser", + "anonymize_modal_title": "Anonymiser", + "autogenerated_dicom_uid": "DICOM UID autogénéré", + "back_button_title": "Précédent", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Impossible de modifier la série, aucun examen trouvé avec ce StudyInstanceUID.", + "error_attach_series_to_existing_study_target_unchanged_html": "Impossible de modifier la série, l'examen de destination est le même que l'examen de la série actuelle.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Erreur lors de la modification du patient, aucun patient trouvé avec cet identifiant. Utilisez plutôt la fonction Modifier les tags de l'examen.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Erreur lors de la modification des tags de l'examen : ce patient possède d'autres examens. Utilisez plutôt la fonction Modifier les tags du Patient.", + "error_modify_any_study_tags_patient_exists_html": "Erreur lors de la modification des tags de l'examen : un autre patient existe déjà avec le même identifiant. Utilisez plutôt la fonction Changer de patient.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Erreur lors de la modification des tags du patient : un autre patient existe déjà avec le même identifiant. Utilisez plutôt la fonction Modifier les tags du Patient.", + "error_modify_unexpected_error_html": "Erreur inattendue lors de la modification, vérifiez les logs d'Orthanc", + "insert_tag": "Insérer un tag DICOM", + "job_in_progress": "La modification est en cours de traitement", + "modify_button_title": "Modifier", + "modify_modal_title": "Modifier", + "modify_series_mode_duplicate_html": "Créer une copie modifiée de la série originale. (en générant de nouveaux UID DICOM et en conservant la série originale)", + "modify_series_mode_keep_uids_html": "Modifier la série originale. (en conservant les UID DICOM d'origine)", + "modify_series_mode_new_uids_html": "Modifier la série originale. (en générant de nouveaux UID DICOM)", + "modify_study_mode_duplicate_html": "Créer une copie modifiée de l'examen original. (en générant de nouveaux UID DICOM et en conservant l'examen original)", + "modify_study_mode_keep_uids_html": "Modifier l'examen original. (en conservant les UID DICOM d'origine)", + "modify_study_mode_new_uids_html": "Modifier l'examen original. (en générant de nouveaux UID DICOM)", + "remove_tag_tooltip": "Supprimer le tag", + "remove_tag_undo_tooltip": "Annuler la suppression du tag", + "series_step_0_attach_series_to_existing_study_button_title_html": "Déplacez dans un examen existant", + "series_step_0_attach_series_to_existing_study_html": "Déplacez cette série vers un examen existant.", + "series_step_0_create_new_study_button_title_html": "Créer un nouvel examen", + "series_step_0_create_new_study_html": "Déplacez cette série dans un nouvel examen.", + "series_step_0_modify_series_button_title_html": "Modifier les tags de la série", + "series_step_0_modify_series_html": "Modifier les tags relatifs à la série uniquement dans cette série ?", + "show_modified_resources": "Afficher les ressources modifiées", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Changer de patient", + "study_step_0_attach_study_to_existing_patient_html": "Attachez cet examen à un autre patient existant.", + "study_step_0_modify_study_button_title_html": "Modifier les tags de l'examen", + "study_step_0_modify_study_html": "Modifier les tags relatif au patient et à l'examen uniquement dans cet examen ?", + "study_step_0_patient_has_other_studies_button_title_html": "Modifier les tags du Patient", + "study_step_0_patient_has_other_studies_html": "Ce patient a {count} examen au total.
Voulez-vous modifier le patient dans tous ces examens ?", + "warning_create_new_study_from_series_html": "Un patient avec le même identifiant existe déjà avec des tags différents. Voulez-vous utiliser les nom, sexe et date de naissance du patient existant ?" + }, + "my_jobs": "Mes jobs", + "no_modalities": "Aucune", + "no_result_found": "Aucun résultat trouvé !", + "not_showing_all_results": "Tous les résultats ne sont pas affichés. Veuillez affiner vos critères de recherche", + "open": "Ouvrir", + "page_not_found": "Page introuvable !", + "patients": "Patients", + "plugin": "Plugin", + "plugins": { + "delayed_deletion": { + "pending_files_count": "Delayed Deletion plugin: Fichiers à effacer" + } + }, + "preview": "Aperçu", + "profile": "Profil", + "retrieve": "Rapatrier", + "retrieve_and_view": { + "finding_locally": "Recherche de l'examen en local.", + "finding_remotely": "Recherche de l'examen dans les modalités DICOM.", + "not_found": "Cet examen n'a pu être trouvé ni en local ni dans les modalités DICOM.", + "retrieved_html": "{count} instances récupérées.", + "retrieving": "L'examen est en cours de récupération." + }, + "remote_dicom_browsing": "Vous explorez actuellement le contenu du noeud DICOM {source}", + "remote_dicom_web_browsing": "Vous explorez actuellement le contenu du serveur DICOMWeb {source}", + "save": "Enregistrer", + "searching": "Recherche...", + "select_files": "Choisir des fichiers", + "select_folder": "Choisir un dossier", + "send_to": { + "button_title": "Envoyer", + "dicom": "Envoyer vers un noeud DICOM", + "dicom_web": "Envoyer vers un serveur DICOMWeb", + "orthanc_peer": "Envoyer à un peer Orthanc", + "transfers": "Envoyer à un peer Orthanc (advanced-transfers)" + }, + "series": "Série", + "series_count_header": "# séries", + "series_and_instances_count_header": "# Ser/Inst", + "series_plural": "Séries", + "settings": { + "allow_all_currents_and_futures_labels": "Toutes les étiquettes actuelles et futures", + "available_labels_global_instructions_html": "Les permissions étant activées, cette page vous permet de modifier la liste des étiquettes disponibles. Les autres utilisateurs ne pourront pas créer de nouvelles étiquettes.
N'oubliez pas d'enregistrer vos modifications.", + "available_labels_title": "Étiquettes disponibles", + "create_new_label": "Créer une étiquette", + "dicom_AET": "DICOM AET", + "dicom_port": "Port DICOM", + "ingest_transcoding": "Transcodage à la réception", + "installed_plugins": "Plugins installés", + "labels_clear_selection": "Vider la sélection", + "labels_description_html": "Les étiquettes sont créées dans la pages Paramètres/Étiquettes.", + "labels_select_all_currents": "Sélectionner toutes les étiquettes actuellement disponibles.", + "labels_limit_available_labels": "Restreindre les étiquettes à une liste", + "labels_list_empty": "La liste des étiquettes est actuellement vide", + "labels_based_permissions_title": "Permissions basées sur les étiquettes", + "labels_title": "Étiquettes", + "global_permissions_title": "Permissions globales", + "orthanc_name": "Nom de l'Orthanc", + "orthanc_system_info": "Informations système Orthanc", + "orthanc_version": "Version d'Orthanc", + "overwrite_instances": "Écraser les instances", + "permissions": "Permissions", + "permissions_clear_selection": "Vider la sélection", + "permissions_global_instructions_html": "Éditer les permissions ci-dessous et cliquez sur Enregistrer pour appliquer les modifications.
Pour chaque rôle, sélectionnez les étiquettes qui seront accessibles et les actions autorisées sur ces examens.", + "permissions_instructions": "La liste de permission ne peut être modifiée; elle est définie dans le plugin authorization.", + "plugins_not_enabled": "Les plugins qui sont chargés mais non-activés ou configurés correctement sont ", + "read_only": "Système en lecture seule", + "roles_title": "Rôles", + "roles_description_html": "Les rôles sont créés et modifiés dans Keycloak", + "select_all_label_permissions": "Sélectionner toutes les permissions basées sur les étiquettes", + "select_all_global_permissions": "Sélectionner toutes les permissions globales", + "system_info": "Système", + "statistics": "Statistiques", + "storage_compression": "Compression du stockage", + "storage_size": "Espace utilisé", + "striked_through": "barrés", + "title": "Paramètres", + "verbosity_level": "Niveau de verbosité des logs" + }, + "share": { + "anonymize": "Anonymiser ?", + "button_title": "Partager", + "copy_and_close": "Copier et fermer", + "days": "jours", + "expires_in": "Expire dans", + "link": "Lien", + "modal_title": "Partager l'examen", + "modal_title_multiple_studies": "Partager {count} examens", + "never": "jamais" + }, + "show_errors": "Afficher les erreurs", + "studies": "Examens", + "study": "Examen", + "this_patient_has_no_other_studies": "Ce patient n'a pas d'autres examens.", + "this_patient_has_other_studies": "Ce patient a {count} examens au total.", + "this_patient_has_other_studies_show": "Les afficher !", + "this_remote_patient_has_local_studies": "Ce patient a déjà {count} examens stockés en local.", + "this_study_is_already_stored_locally": "Cet examen est déjà stocké en local.", + "this_study_is_already_stored_locally_show": "L'afficher !", + "token": { + "error_token_expired_html": "Votre lien a expiré.", + "error_token_invalid_html": "Votre lien n'est pas valide.
Vérifiez que vous l'avez collé complètement ou contactez la personne qui vous l'a fourni.", + "error_token_unknown_html": "Votre lien n'est pas valide.
Vérifiez que vous l'avez collé complètement ou contactez la personne qui vous l'a fourni.", + "token_being_checked_html": "Votre lien est en cours de vérification." + }, + "trace": "Trace", + "upload": "Importer", + "uploaded_studies": "Examens importés", + "verbose": "Verbose", + "view_in_meddream": "Visualiser avec MedDream", + "view_in_ohif": "Visualiser avec OHIF", + "view_in_ohif_microscopy": "Visualiser avec OHIF en mode Microscopie", + "view_in_ohif_seg": "Visualiser avec OHIF en mode Segmentation", + "view_in_ohif_tmtv": "Visualiser avec OHIF en mode Total Metabolic Tumor Volume", + "view_in_ohif_vr": "Visualiser avec OHIF en mode Volume Rendering", + "view_in_osimis": "Visualiser avec le OsimisViewer", + "view_in_stone": "Visualiser avec le StoneViewer", + "view_in_volview": "Visualiser avec VolView", + "view_in_wsi_viewer": "Visualiser avec le Whole Slide Imaging Viewer" +} \ No newline at end of file diff --git a/WebApplication/src/locales/i18n.js b/WebApplication/src/locales/i18n.js new file mode 100644 index 0000000..3f5ecd5 --- /dev/null +++ b/WebApplication/src/locales/i18n.js @@ -0,0 +1,48 @@ +import { createI18n } from "vue-i18n"; +import en from "./en.json"; +import de from "./de.json"; +import es from "./es.json"; +import fr from "./fr.json"; +import it from "./it.json"; +import ka from "./ka.json"; +import ru from "./ru.json"; +import si from "./si.json"; +import uk from "./uk.json"; +import zh from "./zh.json"; + +const i18n = createI18n({ + warnHtmlInMessage: 'off', + locale: 'en', // when the list of availableLanguages is loaded, this value is updated in LanguagePicker.isConfigurationLoaded + fallbackLocale: 'en', + messages: { + en, + de, + es, + fr, + it, + ka, + ru, + si, + uk, + zh + }, +}); + +document._mustTranslateDicomTags = false; + +function translateDicomTag($t, $te, tagName) { + if (document._mustTranslateDicomTags) { + if ($te('dicom_tags.' + tagName)) { + return $t('dicom_tags.' + tagName) + } else { + return tagName; + } + } else { + return tagName; + } +} + +export { + i18n as default, + translateDicomTag +}; diff --git a/WebApplication/src/locales/it.json b/WebApplication/src/locales/it.json new file mode 100644 index 0000000..3781487 --- /dev/null +++ b/WebApplication/src/locales/it.json @@ -0,0 +1,190 @@ +{ + "all_modalities": "Tutte le modalità", + "anonymize": "Anonimizzare", + "cancel": "Annulla", + "change_password": "Cambia password", + "close": "Chiudi", + "copy_orthanc_id": "Copia Orthanc ID", + "date_picker": { + "last_12_months": "12 Mesi precedenti ", + "last_month": "Mese Precedente", + "last_week": "Scorsa settimana", + "this_month": "Questo mese", + "this_week": "Questa settimana", + "today": "Oggi", + "yesterday": "Ieri" + }, + "default": "Predefinito", + "delete": "Cancella", + "delete_instance_body": "Sei sicuro di voler cancellare questa istanza?
Questa azione non potrà essere annullata!", + "delete_instance_title": "Cancellare istanza?", + "delete_series_body": "Sei sicuro di voler cancellare la serie?
Questa azione non potrà essere annullata!", + "delete_series_title": "Cancellare la serie?", + "delete_studies_body": "Sei sicuro di voler cancellare questi studi?
Questa azione non potrà essere annullata!", + "delete_studies_title": "Cancellare gli studi?", + "delete_study_body": "Sei sicuro di voler cancellare lo studio?
Questa azione non potrà essere annullata!", + "delete_study_title": "Cancellare lo Studio?", + "description": "Descrizione", + "dicom_modalities": "DICOM Modalità", + "dicom_tags": { + "AccessionNumber": "Numero Accesso", + "BodyPartExamined": "Parte del corpo esaminata", + "InstanceNumber": "Numero Istanza", + "InstitutionName": "Nome Istituto", + "ModalitiesInStudy": "Modalità nello Studio", + "Modality": "Modalità", + "NumberOfFrames": "Numero immagini", + "NumberOfStudyRelatedSeries": "Numero serie", + "PatientBirthDate": "Data di Nascita Paziente", + "PatientID": "ID Paziente", + "PatientName": "Nome Paziente", + "PatientSex": "Sesso Paziente", + "ProtocolName": "Nome Protocollo", + "ReferringPhysicianName": "Nome Medico Referimento", + "RequestingPhysician": "Medico Richiedente", + "SeriesDate": "Data Serie", + "SeriesDescription": "Descrizione Serie", + "SeriesInstanceUID": "Instanza UID Serie", + "SeriesNumber": "Numero Serie", + "SeriesTime": "Ora Serie", + "StudyDate": "Data Studio", + "StudyDescription": "Descrizione Studio", + "StudyID": "ID Studio", + "StudyInstanceUID": "Instanza UID Studio", + "StudyTime": "Ora Studio", + "seriesCount": "Numero di serie" + }, + "dicom_web_servers": "DICOM-Web Servers", + "displaying_latest_studies": "Visualizza studio più recente", + "download_dicom_file": "Scarica DICOM file", + "download_dicomdir": "Scarica DICOMDIR", + "download_zip": "Scarica ZIP", + "drop_files": "Trascina files qui oppure", + "enter_search": "Inserisci un criterio di ricerca per mostrare i risultati!", + "error": "Errore", + "file": "File", + "files": "files", + "frames": "immagini", + "instance": "Istanza", + "instances": "Istanze", + "instances_number": "Numero di istanze", + "legacy_ui": "Storica UI", + "loading_latest_studies": "Caricamento studio più recente", + "local_studies": "Studi Locali", + "logout": "Disconnettersi", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "Modalità nello Studio", + "modify": { + "anonymize_button_title": "Anonimizzare", + "anonymize_modal_title": "Anonimizzare", + "autogenerated_dicom_uid": "Generato automaticamente", + "back_button_title": "Indietro", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Errore nel cambiare la serie, nessun studio trovato con questa Instanza UID Studio.", + "error_attach_series_to_existing_study_target_unchanged_html": "Errore nel cambiare la serie, lo studio in oggetto è identico alla serie studio corrente.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Errore nel cambiare il paziente, nessun paziente trovato con questo ID Paziente. Pobabilmente dovresti usare la funzione Modifica per le Etichette dello Studio.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Errore nella modifica delle etichette Studio: ci sono altri studi per questo paziente. Dovresti usare la funzione Modifica etichette Paziente.", + "error_modify_any_study_tags_patient_exists_html": "Errore nella modifica delle etichette studio: esiste già un altro paziente con lo stesso ID Paziente. Dovresti usare la funzione Cambia paziente.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Errore nella modifica delle etichette Paziente: esiste un altro paziente con lo stesso ID Paziente. Dovresti usare la funzione Modifica etichette Paziente.", + "error_modify_unexpected_error_html": "Errore inaspettato durante la modifica, verifica i log di Orthanc", + "insert_tag": "Inserisci un'etichetta DICOM", + "job_in_progress": "La modifica è in fase di elaborazione", + "modify_button_title": "Modifica", + "modify_modal_title": "Modifica", + "modify_study_mode_duplicate_html": "Creare una copia modificata dello studio originale. (generazione di un nuovo DICOM UIDs e mantenendo lo studio originale)", + "modify_study_mode_keep_uids_html": "Modifica lo studio originale. (mantenendo l'originale DICOM UIDs)", + "modify_study_mode_new_uids_html": "Modifica lo studio originale. (generazione di un nuovo DICOM UIDs)", + "remove_tag_tooltip": "Rimuovi etichetta", + "remove_tag_undo_tooltip": "Annulla rimuovi etichetta", + "series_step_0_attach_series_to_existing_study_button_title_html": "Cambia studio", + "series_step_0_attach_series_to_existing_study_html": "Sposta questa serie in uno studio esistente.", + "series_step_0_create_new_study_button_title_html": "Crea nuovo studio", + "series_step_0_create_new_study_html": "Sposta questa serie in un nuovo studio.", + "series_step_0_modify_series_button_title_html": "Modifica le etichette di serie", + "series_step_0_modify_series_html": "Modifica le etichette di serie solo in questa serie?", + "show_modified_resources": "Mostra risorse modificate", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Cambia paziente", + "study_step_0_attach_study_to_existing_patient_html": "Allegare questo studio a un altro paziente esistente.", + "study_step_0_modify_study_button_title_html": "Modifica etichette Studio", + "study_step_0_modify_study_html": "Modifica etichette Paziente/Studio solo in questo studio?", + "study_step_0_patient_has_other_studies_button_title_html": "Modifica etichette Paziente", + "study_step_0_patient_has_other_studies_html": "Questo paziente ha {count} studi in totale.
vuoi moficare il paziente in tutti questi studi?", + "warning_create_new_study_from_series_html": "Un paziente con lo stesso ID Paziente esiste già con delle etichette differenti. Vuoi usare Nome Paziente, Sesso Paziente e Data di Nascita Paziente esistente?" + }, + "my_jobs": "I miei Lavori", + "no_modalities": "Nessuna", + "no_result_found": "Nessun risultato trovato!", + "not_showing_all_results": "Non vengono visualizzati tutti i risultati. Dovresti affinare i tuoi criteri di ricerca", + "open": "Apri", + "page_not_found": "Pagina non trovata!", + "plugin": "plugin", + "preview": "Anteprima", + "profile": "Profilo", + "retrieve_and_view": { + "finding_locally": "Verificare se lo studio è già disponibile localmente.", + "finding_remotely": "Ricerca dello studio nei nodi DICOM remoti.", + "not_found": "Lo studio non è stato trovato né localmente né in nodi DICOM remoti.", + "retrieved_html": "Recuperate {count} istanze.", + "retrieving": "Recupero dello studio." + }, + "searching": "Cercando...", + "select_files": "Seleziona Files", + "select_folder": "Seleziona Cartella", + "send_to": { + "button_title": "Invia", + "dicom": "Invia ad un nodo DICOM", + "dicom_web": "Invia ad un server DICOMWeb", + "orthanc_peer": "Invia ad un Peer Orthanc", + "transfers": "Invia ad un Peer Orthanc (trasferimento-avanzato)" + }, + "series": "Serie", + "series_count_header": "# serie", + "series_plural": "Serie", + "settings": { + "dicom_AET": "DICOM AET", + "dicom_port": "DICOM Porta", + "ingest_transcoding": "Transcodifica Ingest", + "installed_plugins": "Plugins Installato", + "orthanc_name": "Nome Orthanc", + "orthanc_system_info": "Informazioni di sistema Orthanc", + "orthanc_version": "Versione Orthanc", + "overwrite_instances": "Sovrascrivi istanze", + "plugins_not_enabled": "Plugins caricato non è abilitato oppure non è configurato correttamente", + "statistics": "Statistiche", + "storage_compression": "Compressione di archiviazione", + "storage_size": "Dimensioni di archiviazione", + "striked_through": "Colpito", + "title": "Impostazioni", + "verbosity_level": "Livello Prolisso" + }, + "share": { + "anonymize": "Anonimizzare?", + "button_title": "Condividi", + "copy_and_close": "Copia e Chiudi", + "days": "giorni", + "expires_in": "Scade tra", + "link": "Collegamento", + "modal_title": "Condividi studio", + "never": "mai" + }, + "show_errors": "mostra errori", + "studies": "Studi", + "study": "Studio", + "this_patient_has_no_other_studies": "Questo paziente non ha altri studi.", + "this_patient_has_other_studies": "Questo paziente ha {count} studi in totale.", + "this_patient_has_other_studies_show": "Mostra altri!", + "token": { + "error_token_expired_html": "Il tuo token è scaduto.", + "error_token_invalid_html": "Il tuo token non è valido.
Controlla di averlo incollato completamente o contatta la persona che te lo ha fornito.", + "error_token_unknown_html": "Il tuo token è sconosciuto.
Controlla di averlo incollato completamente o contatta la persona che te lo ha fornito.", + "token_being_checked_html": "Il tuo token è in fase di verifica." + }, + "trace": "Traccia", + "upload": "caricare", + "uploaded_studies": "Studi caricati", + "verbose": "Prolisso", + "view_in_meddream": "Visualizza con MedDream", + "view_in_ohif": "Visualizza con OHIF", + "view_in_osimis": "Visualizza con OsimisViewer", + "view_in_stone": "Visualizza con StoneViewer", + "view_in_volview": "Visualizza con VolView" +} \ No newline at end of file diff --git a/WebApplication/src/locales/ka.json b/WebApplication/src/locales/ka.json new file mode 100644 index 0000000..08dc1b4 --- /dev/null +++ b/WebApplication/src/locales/ka.json @@ -0,0 +1,190 @@ +{ + "all_modalities": "ყველა", + "anonymize": "ანონიმიზაცია", + "cancel": "გაუქმება", + "change_password": "პაროლის შეცვლა", + "close": "დახურვა", + "copy_orthanc_id": "Orthanc ID-ს კოპირება", + "date_picker": { + "last_12_months": "ბოლო 12 თვე", + "last_month": "წინა თვე", + "last_week": "წინა კვირა", + "this_month": "ეს თვე", + "this_week": "ეს კვირა", + "today": "დღეს", + "yesterday": "გუშინ" + }, + "default": "სტანდარტული", + "delete": "წაშლა", + "delete_instance_body": "ნამდვილად გსურთ ობიექტის წაშლა?
სურათის დაბრუნება შეუძლებელი იქნება", + "delete_instance_title": "ობიექტის წაშლა", + "delete_series_body": "ნამდვილად გსურთ სერიის წაშლა?
სერიის დაბრუნება შეუძლებელი იქნება", + "delete_series_title": "სერიის წაშლა", + "delete_studies_body": "ნამდვილად გსურთ კვლევების წაშლა?
კვლევის დაბრუნება შეუძლებელი იქნება", + "delete_studies_title": "კვლევების წაშლა", + "delete_study_body": "ნამდვილად გსურთ კვლევის წაშლა?
კვლევის დაბრუნება შეუძლებელი იქნება", + "delete_study_title": "კვლევის წაშლა", + "description": "აღწერა", + "dicom_modalities": "DICOM აპარატები", + "dicom_tags": { + "AccessionNumber": "რიგითობის ID", + "BodyPartExamined": "გამოკვლეული სხეულის ნაწილი", + "InstanceNumber": "ობიექტების #", + "InstitutionName": "დაწესებულება", + "ModalitiesInStudy": "აპარატები", + "Modality": "აპარატი", + "NumberOfFrames": "კადრები", + "NumberOfStudyRelatedSeries": "სერიების რაოდენობა", + "PatientBirthDate": "პაციენტის დ.თ.", + "PatientID": "პაციენტის ID", + "PatientName": "სახელი", + "PatientSex": "სქესი", + "ProtocolName": "პროტოკოლი", + "ReferringPhysicianName": "გამომგზავნი", + "RequestingPhysician": "დამკვეთი", + "SeriesDate": "სერიის თარიღი", + "SeriesDescription": "სერიის აღწერა", + "SeriesInstanceUID": "სერიის ობიექტის UID", + "SeriesNumber": "სერიის #", + "SeriesTime": "სერიის დრო", + "StudyDate": "თარიღი", + "StudyDescription": "კვლევის აღწერა", + "StudyID": "კვლევის ID", + "StudyInstanceUID": "კვლევის ობიექტის UID", + "StudyTime": "დრო", + "seriesCount": "სერიები" + }, + "dicom_web_servers": "DICOM-Web სერვერები", + "displaying_latest_studies": "ნაჩვენებია უახლესი კვლევები", + "download_dicom_file": "DICOM ფაილის გადმოწერა", + "download_dicomdir": "DICOMDIR გადმოწერა", + "download_zip": "ZIP გადმოწერა", + "drop_files": "დაყარეთ ფაილები აქ ან", + "enter_search": "რეზულტატის სანახავად შეიყვანეთ ძიების კრიტერიუმები", + "error": "შეცდომა", + "file": "ფაილი", + "files": "ფაილები", + "frames": "კადრები", + "instance": "სურათი", + "instances": "სურათები", + "instances_number": "სურათების რაოდენობა", + "legacy_ui": "ძველი ინტერფეისი", + "loading_latest_studies": "იტვირთება უახლესი კვლევები", + "local_studies": "ადგილობრივი კვლევები", + "logout": "გამოსვლა", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "კვლევაში გამოყენებული აპარატები", + "modify": { + "anonymize_button_title": "ანონიმიზაცია", + "anonymize_modal_title": "ანონიმიზაცია", + "autogenerated_dicom_uid": "ავტომატურად გენერირებული", + "back_button_title": "უკან", + "error_attach_series_to_existing_study_target_does_not_exist_html": "დაფიქსირდა შეცდომა სერიის ცვლილების დროს, ამ კვლევის UID-თ კვლევა არ იძებნება.", + "error_attach_series_to_existing_study_target_unchanged_html": "დაფიქსირდა შეცდომა სერიის ცვლილებისას, სამიზნე კვლევა იგივეა რაც არსებული სერიის კვლევა.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "დაფიქსირდა შეცდომა პაციენტის ცვლილების დროს, პაციენტი ამ ID-თ არ იძებნება. სავარაუდოდ უნდა გამოიყენოთ კვლევის თეგების შეცვლის ფუნქცია.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "დაფიქსირდა შეცდომა კვლევის თეგების მოდიფიკაციის დროს: ამ პაციენტს უკვე აქვს სხვა კვლევები. უნდა გამოიყენოთ პაციენტის თეგების ცვლილების ფუნქცია.", + "error_modify_any_study_tags_patient_exists_html": "დაფიქსირდა შეცდომა კვლევის თეგების ცვლილების დროს. არსებობს სხვა პაციენტი იგივე პაციენტის ID-თ. სავარაუდოდ უნდა გამოიყენოთ პაციენტის ცვლილების ფუნქცია.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "დაფიქსირდა შეცდომა პაციენტის თეგების მოდიფიკაციის დროს: არსებობს სხვა პაციენტი იგივე პაციენტის ID-თ. უნდა გამოიყენოთ პაციენტის თეგების ცვლილების ფუნქცია.", + "error_modify_unexpected_error_html": "შეცდომა მოდიფიკაციის დროს, შეამოწმეთ Orthanc ლოგები", + "insert_tag": "DICOM თეგის დამატება", + "job_in_progress": "მოდიფიკაცია მუშავდება", + "modify_button_title": "ცვლილება", + "modify_modal_title": "ცვლილება", + "modify_study_mode_duplicate_html": "შეიქმნას საწყისი კვლევის მოდიფიცირებული ასლი. ( დაგენერირდეს ახალი DICOM UID-ები და შეინახოს საწყისი კვლევა)", + "modify_study_mode_keep_uids_html": "შეიცვალოს საწყისი კვლევა (დარჩეს საწყისი DICOM UID-ები)", + "modify_study_mode_new_uids_html": "შეიცვალოს საწყისი კვლევა (დაგენერირდეს ახალი DICOM UID-ებს)", + "remove_tag_tooltip": "თეგის წაშლა", + "remove_tag_undo_tooltip": "წაშლილი თეგის დაბრუნება", + "series_step_0_attach_series_to_existing_study_button_title_html": "კვლევის ცვლილება", + "series_step_0_attach_series_to_existing_study_html": "ამ სერიის არსებულ კვლევაში გადატანა.", + "series_step_0_create_new_study_button_title_html": "ახალი კვლევის შექმნა", + "series_step_0_create_new_study_html": "ამ სერიის ახალ კვლევაში გადატანა.", + "series_step_0_modify_series_button_title_html": "სერიის თეგების ცვლილება", + "series_step_0_modify_series_html": "შეიცვალოს სერიის თეგები მხოლოდ ამ სერიისთვის?", + "show_modified_resources": "მოდიფიცირებული რესურსების ნახვა", + "study_step_0_attach_study_to_existing_patient_button_title_html": "პაციენტის ცვლილება", + "study_step_0_attach_study_to_existing_patient_html": "ამ კვლევის სხვა, არსებულ პაციენტზე მიბმა.", + "study_step_0_modify_study_button_title_html": "კვლევის თეგების ცვლილება", + "study_step_0_modify_study_html": "შეიცვალოს პაციენტის/კვლევის თეგები მხოლოდ ამ კვლევისთვის?", + "study_step_0_patient_has_other_studies_button_title_html": "პაციენტის თეგების ცვლილება", + "study_step_0_patient_has_other_studies_html": "ამ პაციენტს ჯამში აქვს{count} კვლევა.
გსურთ პაციენტის ცვლილება ყველა კვლევაში?", + "warning_create_new_study_from_series_html": "უკვე არსებობს პაციენტი იგივე ID-თ და განსხვავებული თეგებით. გსურთ გამოიყენოთ არსებული სახელი, სქესი და დაბადების თარიღი?" + }, + "my_jobs": "ჩემი დავალებები", + "no_modalities": "არცერთი", + "no_result_found": "რეზულტატები ვერ მოიძებნა", + "not_showing_all_results": "ყველა კვლევა არ არის ნაჩვენები. დააზუსტეთ ძიების კრიტერიუმები.", + "open": "გახსნა", + "page_not_found": "გვერდი ვერ მოიძებნა", + "plugin": "ფლაგინი", + "preview": "ნახვა", + "profile": "პროფილი", + "retrieve_and_view": { + "finding_locally": "მოწმდება თუ კვლევა უკვე ლოკალურად არის ხელმისაწვდომი.", + "finding_remotely": "იძებნება კვლევა დისტანციურ DICOM აპარატებში.", + "not_found": "კვლევა არ იძებნება არც ლოკალურად და არც დისტანციურ DICOM აპარატებში.", + "retrieved_html": "გამოთხოვილია {count} ობიექტი.", + "retrieving": "კვლეევის გამოთხოვა." + }, + "searching": "ძიება...", + "select_files": "აირჩიეთ ფაილები", + "select_folder": "აირჩიეთ ფოლდერი", + "send_to": { + "button_title": "გაგზავნა", + "dicom": "DICOM აპარატში გაგზავნა", + "dicom_web": "DICOMWeb სერვერზე გაგზავნა", + "orthanc_peer": "გაგზავნა სხვა Orthanc სისტემაში", + "transfers": "სხვა Orthanc სისტემაში გაგზავნა (ოპტიმიზირებული)" + }, + "series": "სერია", + "series_count_header": "სერიები", + "series_plural": "სერიები", + "settings": { + "dicom_AET": "DICOM AET", + "dicom_port": "DICOM პორტი", + "ingest_transcoding": "შემოსული სურათების ტრანსკოდირება", + "installed_plugins": "დაინსტალირებული ფლაგინები", + "orthanc_name": "Orthanc სახელი", + "orthanc_system_info": "Orthanc სისტემის ინფორმაცია", + "orthanc_version": "Orthanc ვერსია", + "overwrite_instances": "სურათების გადაწერა", + "plugins_not_enabled": "ფლაგინები რომლებიც ჩაიტვირთა, მაგრამ არ არის ჩართული ან სწორად კონფიგურირებული: ", + "statistics": "სტატისტიკა", + "storage_compression": "ფაილების საცავის კომპრესია", + "storage_size": "ფაილების საცავის ზომა", + "striked_through": "გახაზულია", + "title": "პარამეტრები", + "verbosity_level": "ლოგირების დონე" + }, + "share": { + "anonymize": "მოხდეს ანონიმიზირება?", + "button_title": "გაზიარება", + "copy_and_close": "დაკოპირება და დახურვა", + "days": "დღეში", + "expires_in": "ვადა გასდის", + "link": "ბმული", + "modal_title": "კვლევის გაზიარება", + "never": "არასდროს" + }, + "show_errors": "აჩვენე შეცდომები", + "studies": "კვლევები", + "study": "კვლევა", + "this_patient_has_no_other_studies": "ამ პაციენტს სხვა კვლევები არ აქვს.", + "this_patient_has_other_studies": "ამ პაციენტს ჯამში აქვს {count} კვლევა.", + "this_patient_has_other_studies_show": "ჩვენება", + "token": { + "error_token_expired_html": "თქვენი დაშვების კოდის ვადა ამოწურულია.", + "error_token_invalid_html": "თქვენი დაშვების კოდი არ არის ძალაში.
გადაამოწმეთ რომ სწორად გაქვთ დაკოპირებული, ან დაეკონტაქტეთ ადამიანს ვინც გამოგიგზავნათ.", + "error_token_unknown_html": "თქვენი დაშვების კოდი არ არის ძალაში.
გადაამოწმეთ რომ სწორად გაქვთ დაკოპირებული, ან დაეკონტაქტეთ ადამიანს ვინც გამოგიგზავნათ.", + "token_being_checked_html": "თქვენი დაშვების კოდი მოწმდება." + }, + "trace": "ყველაზე დეტალური", + "upload": "ატვირთვა", + "uploaded_studies": "ატვირთული კვლევები", + "verbose": "ვრცელი", + "view_in_meddream": "MedDream Viewer-ში ნახვა", + "view_in_ohif": "OHIF Viewer-ში ნახვა", + "view_in_osimis": "OsimisViewer-ში ნახვა", + "view_in_stone": "StoneViewer-ში ნახვა", + "view_in_volview": "VolView-ში ნახვა" +} \ No newline at end of file diff --git a/WebApplication/src/locales/ru.json b/WebApplication/src/locales/ru.json new file mode 100644 index 0000000..71384de --- /dev/null +++ b/WebApplication/src/locales/ru.json @@ -0,0 +1,213 @@ +{ + "all_modalities": "Все", + "anonymize": "Анонимизировать", + "cancel": "Отменить", + "change_password": "Изменить пароль", + "close": "Закрыть", + "copy_orthanc_id": "Копировать Orthanc ID ", + "date_picker": { + "last_12_months": "За последние 12 месяцев", + "last_month": "В прошлом месяце", + "last_week": "На прошлой неделе", + "this_month": "В этом месяце", + "this_week": "На этой неделе", + "today": "Сегодня", + "yesterday": "Вчера" + }, + "default": "По умолчанию", + "delete": "Удалить", + "delete_instance_body": "Вы уверены, что хотите удалить этот экземпляр?
Это действие нельзя отменить!", + "delete_instance_title": "Удалить экземпляр?", + "delete_series_body": "Вы уверены, что хотите удалить эту серию?
Это действие нельзя отменить!", + "delete_series_title": "Удалить серию?", + "delete_studies_body": "Вы уверены, что хотите удалить эти исследования?
Это действие нельзя отменить!", + "delete_studies_title": "Удалить исследования?", + "delete_study_body": "Вы уверены, что хотите удалить это исследование?
Это действие нельзя отменить!", + "delete_study_title": "Удалить исследование?", + "description": "Описание", + "dicom_modalities": "Модальности DICOM", + "dicom_tags": { + "AccessionNumber": "Регистрационный номер", + "BodyPartExamined": "Обследованная часть тела", + "InstanceNumber": "Номер экземпляра", + "InstitutionName": "Название учреждения", + "ModalitiesInStudy": "Модальности в исследовании", + "Modality": "Модальность", + "NumberOfFrames": "Количество кадров", + "NumberOfStudyRelatedSeries": "Количество серий, связанных с исследованием", + "PatientBirthDate": "Дата рождения", + "PatientID": "ID пациента", + "PatientName": "Имя пациента", + "PatientSex": "Пол пациента", + "ProtocolName": "Название протокола", + "ReferringPhysicianName": "Направляющий врач", + "RequestingPhysician": "Запрашивающий врач", + "SOPInstanceUID": "UID экземпляра SOP", + "SeriesDate": "Дата серии", + "SeriesDescription": "Описание серии", + "SeriesInstanceUID": "UID экземпляра серии", + "SeriesNumber": "Номер серии", + "SeriesTime": "Время серии", + "StudyDate": "Дата исследования", + "StudyDescription": "Описание исследования", + "StudyID": "ID исследования", + "StudyInstanceUID": "UID экземпляра исследования", + "StudyTime": "Время исследования", + "seriesCount": "Количество серий" + }, + "dicom_web_servers": "Серверы DICOM-Web", + "displaying_latest_studies": "Отображение последних исследований", + "download_dicom_file": "Загрузить файл DICOM", + "download_dicomdir": "Загрузить DICOMDIR", + "download_zip": "Загрузить ZIP-архив", + "drop_files": "Перетащите файлы сюда или", + "enter_search": "Введите критерии поиска для отображения результатов!", + "error": "Ошибка", + "file": "Файл", + "files": "файлы", + "frames": "кадры", + "instance": "Экземпляр", + "instances": "Экземпляры", + "instances_number": "Количество экземпляров", + "labels": { + "add_button": "Добавить", + "add_labels_message_html": "Добавить эти метки всем выбранным ресурсам", + "add_labels_placeholder": "Метки, которые нужно добавить. Нажмите Enter, чтобы создать новый", + "added_labels_message_part_1_html": "Добавленные метки ", + "added_labels_message_part_2_html": " до {count} исследований", + "clear_all_button": "Удалить все", + "clear_all_labels_message_html": "Удалить все метки во всех выделенных ресурсах", + "cleared_labels_message_part_1_html": "Удаленные метки ", + "cleared_labels_message_part_2_html": " из {count} исследований", + "edit_labels_button": "Редактировать метки", + "modal_title": "Редактирование меток в выделенных ресурсах", + "remove_button": "Удалить", + "remove_labels_message_html": "Удалить эти метки из всех выбранных ресурсов", + "remove_labels_placeholder": "Метки, которые будут удалены", + "removed_labels_message_part_1_html": "Удаленные метки ", + "removed_labels_message_part_2_html": " из {count} исследований", + "study_details_title": "Метки" + }, + "legacy_ui": "Старый интерфейс", + "loading_latest_studies": "Загрузка последних исследований", + "local_studies": "Все локальные исследования", + "logout": "Выход", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "Модальности в исследовании", + "modify": { + "anonymize_button_title": "Анонимизировать", + "anonymize_modal_title": "Анонимизировать", + "autogenerated_dicom_uid": "Автоматически сгенерированные", + "back_button_title": "Назад", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Ошибка при смене серии, исследование с этим StudyInstanceUID не найдено.", + "error_attach_series_to_existing_study_target_unchanged_html": "Ошибка при смене серии, целевое исследование такое же, как текущее исследование серии.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Ошибка при смене пациента, пациента с этим ID не найдено. Возможно, вместо этого нужно использоватьИзменить теги исследования.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Ошибка при смене тегов исследования: этот пациент имеет другие исследования. Вместо этого следует использовать функцию Изменить теги пациента .", + "error_modify_any_study_tags_patient_exists_html": "Ошибка при смене тегов исследования: уже существует другой пациент с таким же ID. Вместо этого нужно использовать Изменить пациента.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Ошибка при смене тегов пациента: уже существует другой пациент с таким же ID. Вместо этого следует использовать функцию < strong > Изменить теги пациента .", + "error_modify_unexpected_error_html": "Неожиданная ошибка при модификации, проверьте журналы Orthanc", + "insert_tag": "Вставить тег DICOM", + "job_in_progress": "Модификация обрабатывается", + "modify_button_title": "Модифицировать", + "modify_modal_title": "Модифицировать", + "modify_study_mode_duplicate_html": "Создание модифицированной копии исследования. (с генерацией новых DICOM UIDов и сохранением оригинального исследования)", + "modify_study_mode_keep_uids_html": "Модификация оригинального исследования. (с сохранением исходных DICOM UIDов)", + "modify_study_mode_new_uids_html": "Модификация оригинального исследования. (с генерацией новых DICOM UIDов)", + "remove_tag_tooltip": "Удалить тег", + "remove_tag_undo_tooltip": "Отменить удаление тегов", + "series_step_0_attach_series_to_existing_study_button_title_html": "Изменить исследование", + "series_step_0_attach_series_to_existing_study_html": "Переместить эту серию в существующее исследование.", + "series_step_0_create_new_study_button_title_html": "Создать новое исследование", + "series_step_0_create_new_study_html": "Переместить эту серию в новое исследование.", + "series_step_0_modify_series_button_title_html": "Модифицировать теги серии", + "series_step_0_modify_series_html": "Модифицировать теги только в этой серии?", + "show_modified_resources": "Показать модифицированные ресурсы", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Сменить пациента", + "study_step_0_attach_study_to_existing_patient_html": "Присоединить это исследование к другому пациенту.", + "study_step_0_modify_study_button_title_html": "Изменить теги исследования", + "study_step_0_modify_study_html": "Изменить теги пациента/исследования только в этом исследовании?", + "study_step_0_patient_has_other_studies_button_title_html": "Изменить теги пациента", + "study_step_0_patient_has_other_studies_html": "Общее количество исследований этого пациента: {count} . < br/> Вы хотите изменить пациента во всех этих исследованиях?", + "warning_create_new_study_from_series_html": "Уже существует пациент с другими тегами и с таким же ID. Хотите ли вы использовать существующие имя пациента, пол пациента и дату рождения пациента? " + }, + "my_jobs": "Мои задачи", + "no_modalities": "Ничего", + "no_result_found": "Результатов нет!", + "not_showing_all_results": "Не показаны все результаты. Вы должны уточнить критерии поиска", + "open": "Открыть", + "page_not_found": "Такой страницы нет!", + "plugin": "дополнительный модуль", + "preview": "Предварительный просмотр", + "profile": "Профиль", + "retrieve_and_view": { + "finding_locally": "Проверка наличия исследования локально.", + "finding_remotely": "Поиск исследования в удаленных узлах DICOM.", + "not_found": "Исследование не было найдено ни локально, ни в удаленных узлах DICOM.", + "retrieved_html": "Получено {count} экземпляров.", + "retrieving": "Получение исследования." + }, + "searching": "Поиск...", + "select_files": "Выберите файлы", + "select_folder": "Выберите папку", + "send_to": { + "button_title": "Отправить", + "dicom": "Отправить в узел DICOM", + "dicom_web": "Отправить на сервер DICOMWeb", + "orthanc_peer": "Отправить в другой Orthanc", + "transfers": "Отправить в другой Orthanc (расширенные параметры передачи)" + }, + "series": "Серия", + "series_count_header": "# серий", + "series_plural": "Серии", + "settings": { + "dicom_AET": "DICOM AET", + "dicom_port": "Порт DICON", + "ingest_transcoding": "Транскодирование поступающих данных", + "installed_plugins": "Установлены дополнительные модули", + "orthanc_name": "Имя сервера", + "orthanc_system_info": "Информация о системе", + "orthanc_version": "Версия", + "overwrite_instances": "Заменить экземпляры", + "plugins_not_enabled": "Загруженные дополнительные модули, которые не включены или неверно настроены", + "statistics": "Статистика", + "storage_compression": "Сжатие хранилища", + "storage_size": "Размер хранилища", + "striked_through": "зачеркнутые", + "title": "Настройка", + "verbosity_level": "Уровень подробности" + }, + "share": { + "anonymize": "Анонимизировать?", + "button_title": "Поделиться", + "copy_and_close": "Копировать и закрыть", + "days": "дни", + "expires_in": "Срок действия заканчивается через", + "link": "Ссылка", + "modal_title": "Поделиться исследованием", + "never": "никогда" + }, + "show_errors": "Показать ошибки", + "studies": "Исследования", + "study": "Исследование", + "this_patient_has_no_other_studies": "У этого пациента нет больше исследований.", + "this_patient_has_other_studies": "У этого пациента в целом {count} исследований.", + "this_patient_has_other_studies_show": "Показать их!", + "token": { + "error_token_expired_html": "Ваш токен просрочен.", + "error_token_invalid_html": "Ваш токен недействителен.
Проверьте, вставили ли его полностью, или обратитесь к человеку, который вам его предоставил.", + "error_token_unknown_html": "Ваш токен недействителен.
Проверьте, вставили ли его полностью, или обратитесь к человеку, который вам его предоставил.", + "token_being_checked_html": "Ваш токен проверяется." + }, + "trace": "Трассировка", + "upload": "Загрузить", + "uploaded_studies": "Загруженные исследования", + "verbose": "Подробнее", + "view_in_meddream": "Посмотреть в MedDream", + "view_in_ohif": "Посмотреть в OHIF", + "view_in_ohif_tmtv": "Посмотреть в OHIF в режиме Total Metabolic Tumor Volume", + "view_in_ohif_vr": "Посмотреть в OHIF в режиме Volume Rendering", + "view_in_osimis": "Посмотреть в OsimisViewer", + "view_in_stone": "Посмотреть в StoneViewer", + "view_in_volview": "Посмотреть в VolView", + "view_in_wsi_viewer": "Посмотреть в Whole Slide Imaging Viewer" +} \ No newline at end of file diff --git a/WebApplication/src/locales/si.json b/WebApplication/src/locales/si.json new file mode 100644 index 0000000..2e2d7fd --- /dev/null +++ b/WebApplication/src/locales/si.json @@ -0,0 +1,217 @@ +{ + "all_modalities": "Vse", + "anonymize": "Anonimiziraj", + "cancel": "Prekliči", + "change_password": "Spremeni geslo", + "close": "Zapri", + "copy_orthanc_id": "Kopiraj Orthanc ID", + "date_picker": { + "last_12_months": "Zadnjih 12 mesecev", + "last_month": "Prejšnji mesec", + "last_week": "Prejšnji teden", + "this_month": "Ta mesec", + "this_week": "Ta teden", + "today": "Danes", + "yesterday": "Včeraj" + }, + "default": "Privzeto", + "delete": "Izbriši", + "delete_instance_body": "Ste prepričani, da želite izbrisati ta primerek?
Tega dejanja se ne da razveljaviti!", + "delete_instance_title": "Izbriši primerek?", + "delete_series_body": "Ste prepričani, da želite izbrisati to serijo?
Tega dejanja se ne da razveljaviti!", + "delete_series_title": "Izbriši serijo?", + "delete_studies_body": "Ste prepričani, da želite izbrisati te študije?
Tega dejanja se ne da razveljaviti!", + "delete_studies_title": "Izbriši študije?", + "delete_study_body": "Ste prepričani, da želite izbrisati to študijo?
Tega dejanja se ne da razveljaviti!", + "delete_study_title": "Izbriši študijo?", + "description": "Opis", + "dicom_modalities": "DICOM modalnosti", + "dicom_tags": { + "AccessionNumber": "Pristopna številka", + "BodyPartExamined": "Pregledan del telesa", + "InstanceNumber": "Številka primerka", + "InstitutionName": "Ime ustanove", + "ModalitiesInStudy": "Modalitete v študiji", + "Modality": "Modalnost", + "NumberOfFrames": "Število slik", + "NumberOfStudyRelatedSeries": "Število serij", + "PatientBirthDate": "Datum rojstva pacienta", + "PatientID": "ID pacienta", + "PatientName": "Ime pacienta", + "PatientSex": "Spol pacienta", + "ProtocolName": "Ime protokola", + "ReferringPhysicianName": "Ime napotnega zdravnika", + "RequestingPhysician": "Zdravnik prosilec", + "SOPInstanceUID": "UID primerka SOP", + "SeriesDate": "Datum serije", + "SeriesDescription": "Opis serije", + "SeriesInstanceUID": "UID primerka serije", + "SeriesNumber": "Številka serije", + "SeriesTime": "Čas serije", + "StudyDate": "Datum študije", + "StudyDescription": "Opis študije", + "StudyID": "ID študije", + "StudyInstanceUID": "UID primerka študije", + "StudyTime": "Čas študije", + "seriesCount": "Število serij" + }, + "dicom_web_servers": "DICOM-Web strežnik", + "displaying_latest_studies": "Prikaz najnovejših študij", + "download_dicom_file": "Prenos datoteke DICOM", + "download_dicomdir": "Prenesi datoteko DICOMDIR", + "download_zip": "Prenesi datoteko ZIP", + "drop_files": "Datoteke odvrzi sem ali pa...", + "enter_search": "Vnesi kriterij za iskanje za prikaz rezultatov!", + "error": "Napaka", + "file": "Datoteka", + "files": "datoteke", + "frames": "slike", + "instance": "Primerek", + "instances": "Primerki", + "instances_number": "Število primerkov", + "labels": { + "add_button": "Dodaj", + "add_labels_message_html": "Dodajte te oznake vsem izbranim virom", + "add_labels_placeholder": "Oznake, ki jih želite dodati. Pritisnite Enter, da ustvarite novo oznako.", + "added_labels_message_part_1_html": "Dodane oznake", + "added_labels_message_part_2_html": " do {count} študij", + "clear_all_button": "Odstrani vse", + "clear_all_labels_message_html": "Odstrani vse oznake v vseh izbranih virih", + "cleared_labels_message_part_1_html": "Odstranjene oznake", + "cleared_labels_message_part_2_html": " iz {count} študij", + "edit_labels_button": "Uredi oznake", + "modal_title": "Spremeni oznake v izbranih virih", + "remove_button": "Odstrani", + "remove_labels_message_html": "Odstranite te oznake iz vseh izbranih virov", + "remove_labels_placeholder": "Oznake za odstranitev", + "removed_labels_message_part_1_html": "Odstranjene oznake", + "removed_labels_message_part_2_html": " iz {count} študij", + "study_details_title": "Oznake" + }, + "legacy_ui": "Starejši uporabniški vmesnik", + "loading_latest_studies": "Nalaganje najnovejših študij", + "local_studies": "Vse lokalne študije", + "logout": "Odjava", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "Modalitete v študiju", + "modify": { + "anonymize_button_title": "Anonimiziraj", + "anonymize_modal_title": "Anonimiziraj", + "autogenerated_dicom_uid": "Samodejno ustvarjeno", + "back_button_title": "Nazaj", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Napaka pri spreminjanju serije, s tem \"StudyInstanceUID\" ni bilo mogoče najti študije.", + "error_attach_series_to_existing_study_target_unchanged_html": "Napaka pri spreminjanju serije, ciljna študija je enaka trenutni študiji serije.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Napaka pri menjavi pacienta. Pacient s tem ID-jem ne obstaja. Poizkusite s Spremenite študijske oznake funkcijo.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Napaka pri spreminjanju oznak študije: ta pacient ima druge študije. Namesto tega uporabite funkcijo Spremenite oznake pacienta.", + "error_modify_any_study_tags_patient_exists_html": "Napaka pri spreminjanju oznak študije: pacient s tem ID-jem že obstaja. Namesto tega uporabite funkcijo Spremeni pacienta/strong>.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Napaka pri spreminjanju oznak pacienta: drug pacient z istim ID-jem že obstaja. Namesto tega uporabite funkcijo Spremenite oznake pacienta.", + "error_modify_unexpected_error_html": "Nepričakovana napaka med spreminjanjem, preverite Orthanc-ove dnevnike", + "insert_tag": "Vstavi DICOM oznako", + "job_in_progress": "Sprememba je v obdelavi", + "modify_button_title": "Spremeni", + "modify_modal_title": "Spremeni", + "modify_series_mode_duplicate_html": "Ustvari spremenjeno kopijo izvirne serije. (ustvarjanje novih DICOM UID-jev in ohranjanje izvirne serije)", + "modify_series_mode_keep_uids_html": "Popravi izvirno serijo. (ohranjanje izvirnih DICOM UID-jev)", + "modify_series_mode_new_uids_html": "Spremeni izvirno serijo. (ustvarjanje novih DICOM UID-jev)", + "modify_study_mode_duplicate_html": "Ustvari spremenjeno kopijo izvirne študije. (ustvarjanje novih DICOM UID-jev in ohranjanje izvirne študije)", + "modify_study_mode_keep_uids_html": "Popravi izvirno študijo. (ohranjanje izvirnih DICOM UID-jev)", + "modify_study_mode_new_uids_html": "Spremeni izvirno študijo. (ustvarjanje novih DICOM UID-jev)", + "remove_tag_tooltip": "Odstrani oznako", + "remove_tag_undo_tooltip": "Razveljavi odstranitev oznake", + "series_step_0_attach_series_to_existing_study_button_title_html": "Spremeni študijo", + "series_step_0_attach_series_to_existing_study_html": "Premakni to serijo v obstoječo študijo.", + "series_step_0_create_new_study_button_title_html": "Ustvari novo študijo", + "series_step_0_create_new_study_html": "Premakni to serijo v novo študijo.", + "series_step_0_modify_series_button_title_html": "Spremeni oznake serije", + "series_step_0_modify_series_html": "Želite spremeniti oznake serije samo v tej seriji?", + "show_modified_resources": "Pokaži spremenjene vire", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Spremeni pacienta", + "study_step_0_attach_study_to_existing_patient_html": "Priloži to študijo drugemu obstoječemu pacientu.", + "study_step_0_modify_study_button_title_html": "Spremeni oznake študije", + "study_step_0_modify_study_html": "Želite spremeniti oznake pacienta/študije samo v tej študiji?", + "study_step_0_patient_has_other_studies_button_title_html": "Spremeni oznake pacientov", + "study_step_0_patient_has_other_studies_html": "Ta pacient ima skupno {count} študij.
Ali želite spremeniti pacienta v vseh teh študijah?", + "warning_create_new_study_from_series_html": "Pacient z istim ID-jem, ampak drugačnimi oznakami, že obstaja. Ali želite uporabiti obstoječe ime pacienta, njegov spol in datum rojstva?" + }, + "my_jobs": "Moja opravila", + "no_modalities": "Noben", + "no_result_found": "Ni rezultatov!", + "not_showing_all_results": "Niso prikazani vsi rezultati. Uporabite bolj specifične iskalne kriterije", + "open": "Odprto", + "page_not_found": "Stran ni najdena!", + "plugin": "vtičnik", + "preview": "Predogled", + "profile": "Profil", + "retrieve_and_view": { + "finding_locally": "Preverjam, ali je študija že na voljo lokalno.", + "finding_remotely": "Iskanje študije v oddaljenih DICOM vozliščih.", + "not_found": "Študija ni bila najdena niti lokalno niti v oddaljenih DICOM vozliščih.", + "retrieved_html": "Pridobljenih {count} primerkov.", + "retrieving": "Pridobivanje študije." + }, + "searching": "Iščem ...", + "select_files": "Izberi datoteke", + "select_folder": "Izberi mapo", + "send_to": { + "button_title": "Pošlji", + "dicom": "Pošlji v DICOM vozlišče", + "dicom_web": "Pošlji na DICOMWeb strežnik", + "orthanc_peer": "Pošlji Orthanc vrstniku", + "transfers": "Pošlji Orthanc vrstniku (napredni prenosi)" + }, + "series": "Serija", + "series_count_header": "# serij", + "series_plural": "Serije", + "settings": { + "dicom_AET": "DICOM AET", + "dicom_port": "DICOM vrata", + "ingest_transcoding": "Prevzemno prekodiranje", + "installed_plugins": "Nameščeni vtičniki", + "orthanc_name": "Ime Orthanc", + "orthanc_system_info": "Informacije o sistemu Orthanc", + "orthanc_version": "Različica Orthanc", + "overwrite_instances": "Prepiši primerke", + "plugins_not_enabled": "Vtičniki, ki so naloženi, vendar niso omogočeni ali niso pravilno konfigurirani, so", + "statistics": "Statistika", + "storage_compression": "Stiskanje shranjevanja", + "storage_size": "Velikost shranjevanja", + "striked_through": "prečrtani", + "title": "Nastavitve", + "verbosity_level": "Nivo beleženja" + }, + "share": { + "anonymize": "Anonimiziraj?", + "button_title": "Deli", + "copy_and_close": "Kopiraj in zapri", + "days": "dni", + "expires_in": "Poteče čez", + "link": "Povezava", + "modal_title": "Deli študijo", + "modal_title_multiple_studies": "Deli {count} študij", + "never": "nikoli" + }, + "show_errors": "Pokaži napake", + "studies": "Študije", + "study": "Študija", + "this_patient_has_no_other_studies": "Ta pacient nima drugih študij.", + "this_patient_has_other_studies": "Ta pacient ima skupno {count} študij.", + "this_patient_has_other_studies_show": "Prikaži jih", + "token": { + "error_token_expired_html": "Vaš žeton je potekel.", + "error_token_invalid_html": "Vaš žeton ni veljaven.
Preverite, ali ste ga prilepili v celoti, ali pa se obrnite na osebo, ki vam ga je posredovala.", + "error_token_unknown_html": "Vaš žeton ni veljaven.
Preverite, ali ste ga prilepili v celoti, ali pa se obrnite na osebo, ki vam ga je posredovala.", + "token_being_checked_html": "Vaš žeton se preverja." + }, + "trace": "Napredno", + "upload": "Naloži", + "uploaded_studies": "Naložene študije", + "verbose": "Podrobno", + "view_in_meddream": "Ogled v MedDream", + "view_in_ohif": "Ogled v OHIF", + "view_in_ohif_tmtv": "Ogled v načinu OHIF Total Metabolic Tumor Volume", + "view_in_ohif_vr": "Ogled v načinu OHIF Volume Rendering", + "view_in_osimis": "Ogled v OsimisViewer", + "view_in_stone": "Ogled v StoneViewer", + "view_in_volview": "Ogled v VolView", + "view_in_wsi_viewer": "Ogled v Whole Slide Imaging Viewer" +} \ No newline at end of file diff --git a/WebApplication/src/locales/uk.json b/WebApplication/src/locales/uk.json new file mode 100644 index 0000000..920f374 --- /dev/null +++ b/WebApplication/src/locales/uk.json @@ -0,0 +1,214 @@ +{ + "all_modalities": "Всі", + "anonymize": "Анонімізувати", + "cancel": "Скасувати", + "change_password": "Змінити пароль", + "close": "Закрити", + "copy_orthanc_id": "Копіювати Orthanc ID", + "date_picker": { + "last_12_months": "За останні 12 місяців", + "last_month": "Минулого місяця", + "last_week": "Минулого тижня", + "this_month": "Цього місяця", + "this_week": "Цього тижня", + "today": "Сьогодні", + "yesterday": "Вчора" + }, + "default": "За замовчуванням", + "delete": "Видалити", + "delete_instance_body": "Ви впевнені, що хочете видалити цей екземпляр?
Цю дію не можна скасувати!", + "delete_instance_title": "Видалити екземпляр?", + "delete_series_body": "Ви впевнені, що хочете видалити ці серії?
Цю дію не можна скасувати!", + "delete_series_title": "Видалити серії?", + "delete_studies_body": "Ви впевнені, що хочете видалити ці дослідження?
Цю дію не можна скасувати!", + "delete_studies_title": "Видалити дослідження?", + "delete_study_body": "Ви впевнені, що хочете видалити це дослідження?
Цю дію не можна скасувати!", + "delete_study_title": "Видалити дослідження?", + "description": "Опис", + "dicom_modalities": "Модальності DICOM", + "dicom_tags": { + "AccessionNumber": "Номер обстеження", + "BodyPartExamined": "Обстежена частина тіла", + "InstanceNumber": "Номер екземпляра", + "InstitutionName": "Назва установи", + "ModalitiesInStudy": "Модальності у дослідженні", + "Modality": "Модальність", + "NumberOfFrames": "Кількість кадрів", + "NumberOfStudyRelatedSeries": "Кількість серій, пов'язаних із дослідженням", + "PatientBirthDate": "Дата народження пацієнта", + "PatientID": "ID пацієнта", + "PatientName": "Ім'я пацієнта", + "PatientSex": "Стать пацієнта", + "ProtocolName": "Назва протоколу", + "ReferringPhysicianName": "Ім'я лікаря, що направив", + "RequestingPhysician": "Лікар, що робить запит", + "SOPInstanceUID": "UID екземпляра SOP", + "SeriesDate": "Дата серії", + "SeriesDescription": "Опис серії", + "SeriesInstanceUID": "UID екземпляра серії", + "SeriesNumber": "Номер серії", + "SeriesTime": "Час серії", + "StudyDate": "Дата дослідження", + "StudyDescription": "Опис дослідження", + "StudyID": "ID дослідження", + "StudyInstanceUID": "UID екземпляра дослідження", + "StudyTime": "Час дослідження", + "seriesCount": "Кількість серій" + }, + "dicom_web_servers": "Сервери DICOM-Web", + "displaying_latest_studies": "Відображення останніх досліджень", + "download_dicom_file": "Завантажити файл DICOM", + "download_dicomdir": "Завантажити DICOMDIR", + "download_zip": "Завантажити ZIP-архів", + "drop_files": "Перетягніть файли сюди або", + "enter_search": "Введіть критерії пошуку, щоб відобразити результати!", + "error": "Помилка", + "file": "Файл", + "files": "файли", + "frames": "кадри", + "instance": "Екземпляр", + "instances": "Екземпляри", + "instances_number": "Кількість екземплярів", + "labels": { + "add_button": "Додати", + "add_labels_message_html": "Додати ці мітки усім вибраним ресурсам", + "add_labels_placeholder": "Мітки, що будуть додані; натисність Enter для створення нової", + "added_labels_message_part_1_html": "Додані мітки ", + "added_labels_message_part_2_html": " до {count} досліджень", + "clear_all_button": "Видалити все", + "clear_all_labels_message_html": "Видалити всі мітки у всіх виділених ресурсах", + "cleared_labels_message_part_1_html": "Видалені мітки ", + "cleared_labels_message_part_2_html": " з {count} досліджень", + "edit_labels_button": "Редагувати мітки", + "modal_title": "Редагування міток у виділених ресурсах", + "remove_button": "Видалити", + "remove_labels_message_html": "Видалити ці мітки з усіх вибраних ресурсів", + "remove_labels_placeholder": "Мітки, що будуть вилучені", + "removed_labels_message_part_1_html": "Видалені мітки ", + "removed_labels_message_part_2_html": " з {count} досліджень", + "study_details_title": "Мітки" + }, + "legacy_ui": "Старий інтерфейс", + "loading_latest_studies": "Завантаження останніх досліджень", + "local_studies": "Усі локальні дослідження", + "logout": "Вихід", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "Модальності у дослідженні", + "modify": { + "anonymize_button_title": "Анонімізувати", + "anonymize_modal_title": "Анонімізувати", + "autogenerated_dicom_uid": "Автоматично згенеровані", + "back_button_title": "Взад", + "error_attach_series_to_existing_study_target_does_not_exist_html": "Помилка під час зміни серії, дослідження з цим StudyInstanceUID не знайдено.", + "error_attach_series_to_existing_study_target_unchanged_html": "Помилка під час зміни серії, цільове дослідження таке ж, як поточне дослідження серії.", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "Помилка під час зміни пацієнта, пацієнта з цим ID пацієнта не знайдено. Можливо, замість цього вам слід скористатися функцією Змінити теги дослідження.", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Помилка під час зміни тегів дослідження: цей пацієнт має інші дослідження. Замість цього слід використовувати функцію Змінити теги пацієнта.", + "error_modify_any_study_tags_patient_exists_html": "Помилка під час зміни тегів дослідження: уже існує інший пацієнт із таким самим ID пацієнта. Замість цього слід скористатися функцією Змінити пацієнта.", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Помилка під час зміни тегів пацієнта: уже існує інший пацієнт із таким самим ID пацієнта. Замість цього слід використовувати функцію Змінити теги пацієнта.", + "error_modify_unexpected_error_html": "Неочікувана помилка під час модифікації, перевірте журнали Orthanc", + "insert_tag": "Вставити тег DICOM", + "job_in_progress": "Модифікація обробляється", + "modify_button_title": "Модифікувати", + "modify_modal_title": "Модифікувати", + "modify_study_mode_duplicate_html": "Створення модифікованої копії дослідження. (з генерацією нових DICOM UIDів ТА збереженням оригінального дослідження)", + "modify_study_mode_keep_uids_html": "Модифікація оригінального дослідження. (зі збереженням оригінальних DICOM UIDів)", + "modify_study_mode_new_uids_html": "Модифікація оригінального дослідження. (з генерацією нових DICOM UIDів)", + "remove_tag_tooltip": "Видалити тег", + "remove_tag_undo_tooltip": "Відмінити видалення тегу", + "series_step_0_attach_series_to_existing_study_button_title_html": "Змінити дослідження", + "series_step_0_attach_series_to_existing_study_html": "Перемістіти цю серію до наявного дослідження.", + "series_step_0_create_new_study_button_title_html": "Створити нове дослідження", + "series_step_0_create_new_study_html": "Перемістити цю серію до нового дослідження.", + "series_step_0_modify_series_button_title_html": "Модифікувати теги серії", + "series_step_0_modify_series_html": "Модифікувати теги серії лише у цій серії?", + "show_modified_resources": "Показати модифіковані ресурси", + "study_step_0_attach_study_to_existing_patient_button_title_html": "Змінити пацієнта", + "study_step_0_attach_study_to_existing_patient_html": "Приєднати це дослідження до іншого наявного пацієнта.", + "study_step_0_modify_study_button_title_html": "Змінити теги дослідження", + "study_step_0_modify_study_html": "Змінити теги пацієнта/дослідження лише в цьому дослідженні?", + "study_step_0_patient_has_other_studies_button_title_html": "Змінити теги пацієнта", + "study_step_0_patient_has_other_studies_html": "Загальна кількість досліджень цього пацієнта: {count}.
Ви бажаєте змінити пацієнта в усіх цих дослідженнях ?", + "warning_create_new_study_from_series_html": "Вже існує пацієнт із іншими тегами і з таким самим ID пацієнта. Чи бажаєте ви використовувати наявні ім'я пацієнта, стать пацієнта і дату народження пацієнта?" + }, + "my_jobs": "Мої завдання", + "no_modalities": "Нічого", + "no_result_found": "Результатів немає!", + "not_showing_all_results": "Не показано всі результати. Ви повинні уточнити критерії пошуку", + "open": "Відкрити", + "page_not_found": "Такої сторінки немає!", + "plugin": "додатковий модуль", + "preview": "Попередній перегляд", + "profile": "Профіль", + "retrieve_and_view": { + "finding_locally": "Перевірка наявності дослідження локально.", + "finding_remotely": "Пошук дослідження у віддалених вузлах DICOM.", + "not_found": "Дослідження не було знайдено ані локально, ані у віддалених вузлах DICOM.", + "retrieved_html": "Отримано {count} екземплярів.", + "retrieving": "Отримання дослідження." + }, + "searching": "Пошук...", + "select_files": "Виберіть файли", + "select_folder": "Виберіть теку", + "send_to": { + "button_title": "Надіслати", + "dicom": "Надіслати до вузла DICOM", + "dicom_web": "Надіслати до сервера DICOMWeb", + "orthanc_peer": "Надіслати до іншого Orthanc", + "transfers": "Надіслати до іншого Orthanc (розширені параметри передавання)" + }, + "series": "Серія", + "series_count_header": "# серій", + "series_plural": "Серії", + "settings": { + "dicom_AET": "DICOM AE Title", + "dicom_port": "Порт DICOM", + "ingest_transcoding": "Транскодування даних, що надходять", + "installed_plugins": "Встановлені додаткові модулі", + "orthanc_name": "Ім'я сервера Orthanc", + "orthanc_system_info": "Інформація про систему Orthanc", + "orthanc_version": "Версія Orthanc", + "overwrite_instances": "Перезаписати екземпляри", + "plugins_not_enabled": "Завантажені додаткові модулі, які не ввімкнені або некоректно налаштовані, ", + "statistics": "Статистика", + "storage_compression": "Стиснення сховища", + "storage_size": "Розмір сховища", + "striked_through": "закреслені", + "title": "Налаштування", + "verbosity_level": "Рівень докладності" + }, + "share": { + "anonymize": "Анонімізувати?", + "button_title": "Поділитись", + "copy_and_close": "Копіювати та закрити", + "days": "дні", + "expires_in": "Строк дії закінчується через", + "link": "Посилання", + "modal_title": "Поділитись дослідженням", + "modal_title_multiple_studies": "Поділитись {count} дослідженнями", + "never": "ніколи" + }, + "show_errors": "Показати помилки", + "studies": "Дослідження", + "study": "Дослідження", + "this_patient_has_no_other_studies": "У цього пацієнта немає більше досліджень.", + "this_patient_has_other_studies": "У цього пацієнта загалом {count} досліджень.", + "this_patient_has_other_studies_show": "Показати їх!", + "token": { + "error_token_expired_html": "Ваш токен прострочений.", + "error_token_invalid_html": "Ваш токен недійсний.
Перевірте, чи вставили його повністю, або зверніться до особи, яка вам його надала.", + "error_token_unknown_html": "Ваш токен недійсний.
Перевірте, чи вставили його повністю, або зверніться до особи, яка вам його надала.", + "token_being_checked_html": "Ваш токен перевіряється." + }, + "trace": "Трасування", + "upload": "Завантажити", + "uploaded_studies": "Завантажені дослідження", + "verbose": "Докладно", + "view_in_meddream": "Подивитись у MedDream", + "view_in_ohif": "Подивитися в OHIF", + "view_in_ohif_tmtv": "Подивитися в OHIF у режимі Total Metabolic Tumor Volume", + "view_in_ohif_vr": "Подивитися в OHIF у режимі Volume Rendering", + "view_in_osimis": "Подивитись у OsimisViewer", + "view_in_stone": "Подивитись у StoneViewer", + "view_in_volview": "Подивитись у VolView", + "view_in_wsi_viewer": "Подивитись у Whole Slide Imaging Viewer" +} \ No newline at end of file diff --git a/WebApplication/src/locales/zh.json b/WebApplication/src/locales/zh.json new file mode 100644 index 0000000..e42c53c --- /dev/null +++ b/WebApplication/src/locales/zh.json @@ -0,0 +1,208 @@ +{ + "all_modalities": "全部", + "anonymize": "匿名化", + "cancel": "取消", + "change_password": "更改密码", + "close": "关闭", + "copy_orthanc_id": "复制Orthanc ID", + "date_picker": { + "last_12_months": "最近12个月", + "last_month": "上个月", + "last_week": "上周", + "this_month": "本月", + "this_week": "本周", + "today": "今天", + "yesterday": "昨天" + }, + "default": "默认", + "delete": "删除", + "delete_instance_body": "您确定要删除此实例吗?
此操作无法撤销!", + "delete_instance_title": "删除实例?", + "delete_series_body": "您确定要删除此系列吗?
此操作无法撤销!", + "delete_series_title": "删除系列?", + "delete_studies_body": "您确定要删除这些研究吗?
此操作无法撤销!", + "delete_studies_title": "删除研究?", + "delete_study_body": "您确定要删除此研究吗?
此操作无法撤销!", + "delete_study_title": "删除研究?", + "description": "描述", + "dicom_modalities": "DICOM模态", + "dicom_tags": { + "AccessionNumber": "访问号", + "BodyPartExamined": "检查的部位", + "InstanceNumber": "实例号", + "InstitutionName": "机构名称", + "ModalitiesInStudy": "研究中的模态", + "Modality": "模态", + "NumberOfFrames": "帧数", + "NumberOfStudyRelatedSeries": "系列数量", + "PatientBirthDate": "患者出生日期", + "PatientID": "患者ID", + "PatientName": "患者姓名", + "PatientSex": "患者性别", + "ProtocolName": "协议名称", + "ReferringPhysicianName": "转诊医师姓名", + "RequestingPhysician": "申请医师", + "SeriesDate": "系列日期", + "SeriesDescription": "系列描述", + "SeriesInstanceUID": "系列实例UID", + "SeriesNumber": "系列号", + "SeriesTime": "系列时间", + "StudyDate": "研究日期", + "StudyDescription": "研究描述", + "StudyID": "研究ID", + "StudyInstanceUID": "研究实例UID", + "StudyTime": "研究时间", + "seriesCount": "系列数量" + }, + "dicom_web_servers": "DICOM-Web服务器", + "displaying_latest_studies": "显示最近的研究", + "download_dicom_file": "下载DICOM文件", + "download_dicomdir": "下载DICOMDIR", + "download_zip": "下载ZIP", + "drop_files": "将文件拖放到此处或", + "enter_search": "输入搜索条件以显示结果!", + "error": "错误", + "file": "文件", + "files": "文件", + "frames": "帧数", + "instance": "实例", + "instances": "实例", + "instances_number": "实例数量", + "labels": { + "add_button": "添加", + "add_labels_message_html": "添加这些标签到所有选定的资源", + "add_labels_placeholder": "要添加的标签,按Enter键创建新标签", + "added_labels_message_part_1_html": "已添加标签", + "added_labels_message_part_2_html": "到{count}个研究", + "clear_all_button": "移除全部", + "clear_all_labels_message_html": "移除全部选定资源的标签", + "cleared_labels_message_part_1_html": "已移除标签", + "cleared_labels_message_part_2_html": "从{count}个研究", + "edit_labels_button": "编辑标签", + "modal_title": "修改选定资源的标签", + "remove_button": "移除", + "remove_labels_message_html": "移除所有选定资源的这些标签", + "remove_labels_placeholder": "要移除的标签", + "removed_labels_message_part_1_html": "已移除标签", + "removed_labels_message_part_2_html": "从{count}个研究" + }, + "legacy_ui": "传统用户界面", + "loading_latest_studies": "正在加载最近的研究", + "local_studies": "所有本地研究", + "logout": "登出", + "mammography_plugin": "Start mammogram AI", + "modalities_in_study": "研究中的模态", + "modify": { + "anonymize_button_title": "匿名化", + "anonymize_modal_title": "匿名化", + "autogenerated_dicom_uid": "自动生成", + "back_button_title": "返回", + "error_attach_series_to_existing_study_target_does_not_exist_html": "更改系列时出错,未找到具有此StudyInstanceUID的研究。", + "error_attach_series_to_existing_study_target_unchanged_html": "更改系列时出错,目标研究与当前系列研究相同。", + "error_attach_study_to_existing_patient_target_does_not_exist_html": "更改患者时出错,未找到具有此PatientID的患者。您应该使用修改研究标签功能。", + "error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "修改研究标签时出错:该患者有其他研究。您应该使用修改患者标签功能。", + "error_modify_any_study_tags_patient_exists_html": "修改研究标签时出错:已存在具有相同PatientID的其他患者。您应该使用更改患者功能。", + "error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "修改患者标签时出错:已存在具有相同PatientID的其他患者。您应该使用修改患者标签功能。", + "error_modify_unexpected_error_html": "修改过程中发生意外错误,请检查Orthanc日志", + "insert_tag": "插入DICOM标签", + "job_in_progress": "正在处理修改", + "modify_button_title": "修改", + "modify_modal_title": "修改", + "modify_study_mode_duplicate_html": "创建原始研究的修改副本。(生成新的DICOM UID并保留原始研究)", + "modify_study_mode_keep_uids_html": "修改原始研究。(保留原始DICOM UID)", + "modify_study_mode_new_uids_html": "修改原始研究。(生成新的DICOM UID)", + "remove_tag_tooltip": "移除标签", + "remove_tag_undo_tooltip": "撤销移除标签", + "series_step_0_attach_series_to_existing_study_button_title_html": "更改研究", + "series_step_0_attach_series_to_existing_study_html": "将此系列移至现有研究。", + "series_step_0_create_new_study_button_title_html": "创建新研究", + "series_step_0_create_new_study_html": "将此系列移至新的研究。", + "series_step_0_modify_series_button_title_html": "修改系列标签", + "series_step_0_modify_series_html": "仅修改此系列的系列标签?", + "show_modified_resources": "显示修改后的资源", + "study_step_0_attach_study_to_existing_patient_button_title_html": "更改患者", + "study_step_0_attach_study_to_existing_patient_html": "将此研究附加到其他现有患者。", + "study_step_0_modify_study_button_title_html": "修改研究标签", + "study_step_0_modify_study_html": "仅修改此研究的患者/研究标签?", + "study_step_0_patient_has_other_studies_button_title_html": "修改患者标签", + "study_step_0_patient_has_other_studies_html": "该患者共有{count}个研究。
您是否要在所有这些研究中修改患者?", + "warning_create_new_study_from_series_html": "已存在具有不同标签的具有相同PatientID的患者。您是否要使用现有的PatientName、PatientSex和PatientBirthDate?" + }, + "my_jobs": "我的作业", + "no_modalities": "无", + "no_result_found": "未找到结果!", + "not_showing_all_results": "未显示所有结果。您应该精确您的搜索条件", + "open": "打开", + "page_not_found": "页面未找到!", + "plugin": "插件", + "preview": "预览", + "profile": "配置文件", + "retrieve_and_view": { + "finding_locally": "检查本地是否已存在该研究。", + "finding_remotely": "在远程DICOM节点中搜索该研究。", + "not_found": "未在本地或远程DICOM节点中找到该研究。", + "retrieved_html": "已检索{count}个实例。", + "retrieving": "检索研究。" + }, + "searching": "搜索中...", + "select_files": "选择文件", + "select_folder": "选择文件夹", + "send_to": { + "button_title": "发送", + "dicom": "发送至DICOM节点", + "dicom_web": "发送至DICOMWeb服务器", + "orthanc_peer": "发送至Orthanc对等节点", + "transfers": "发送至Orthanc对等节点(高级传输)" + }, + "series": "系列", + "series_count_header": "系列数", + "series_plural": "系列", + "settings": { + "dicom_AET": "DICOM AET", + "dicom_port": "DICOM端口", + "ingest_transcoding": "摄取转码", + "installed_plugins": "已安装的插件", + "orthanc_name": "Orthanc名称", + "orthanc_system_info": "Orthanc系统信息", + "orthanc_version": "Orthanc版本", + "overwrite_instances": "覆盖实例", + "plugins_not_enabled": "已加载但未启用或配置不正确的插件为", + "statistics": "统计", + "storage_compression": "存储压缩", + "storage_size": "存储大小", + "striked_through": "删除线", + "title": "设置", + "verbosity_level": "详细级别" + }, + "share": { + "anonymize": "匿名化?", + "button_title": "共享", + "copy_and_close": "复制并关闭", + "days": "天", + "expires_in": "过期时间", + "link": "链接", + "modal_title": "共享研究", + "never": "永不" + }, + "show_errors": "显示错误", + "studies": "研究", + "study": "研究", + "this_patient_has_no_other_studies": "该患者没有其他研究。", + "this_patient_has_other_studies": "该患者共有{count}个研究。", + "this_patient_has_other_studies_show": "显示它们!", + "token": { + "error_token_expired_html": "您的令牌已过期。", + "error_token_invalid_html": "您的令牌无效。
请确保完整粘贴或与提供给您的人员联系。", + "error_token_unknown_html": "您的令牌无效。
请确保完整粘贴或与提供给您的人员联系。", + "token_being_checked_html": "正在验证您的令牌。" + }, + "trace": "跟踪", + "upload": "上传", + "uploaded_studies": "已上传的研究", + "verbose": "详细", + "view_in_meddream": "在MedDream中查看", + "view_in_ohif": "在OHIF中查看", + "view_in_osimis": "在OsimisViewer中查看", + "view_in_stone": "在StoneViewer中查看", + "view_in_volview": "在VolView中查看" +} \ No newline at end of file diff --git a/WebApplication/src/main-landing.js b/WebApplication/src/main-landing.js new file mode 100644 index 0000000..b8788d3 --- /dev/null +++ b/WebApplication/src/main-landing.js @@ -0,0 +1,41 @@ +import { createApp } from 'vue' +import AppLanding from './AppLanding.vue' +import i18n from './locales/i18n' +import "bootstrap/dist/css/bootstrap.min.css" +import "bootstrap/dist/js/bootstrap.bundle.min.js" +import "bootstrap-icons/font/bootstrap-icons.css" +import "@fortawesome/fontawesome-free/css/all.min.css" +import store from "./store" +import orthancApi from './orthancApi' +import axios from 'axios' + + +// Names of the params that can contain an authorization token +// If one of these params contain a token, it will be passed as a header +// with each request to the Orthanc API +const VALID_TOKEN_PARAMS = ["token", "auth-token", "authorization"]; + + +// before initialization, we must load part of the configuration to know if we need to enable Keycloak or not +axios.get('../api/pre-login-configuration').then((config) => { + + const app = createApp(AppLanding) + + app.use(store) + app.use(i18n) + + // If there is a param with a token in the params, use it as a header in subsequent calls to the Orthanc API + const params = new URLSearchParams(window.location.search); + + for (let paramName of VALID_TOKEN_PARAMS) { + const paramValue = params.get(paramName); + + if (!paramValue) continue; + + localStorage.setItem(paramName, paramValue); + orthancApi.updateAuthHeader(paramName); + } + + app.mount('#app-landing') + +}); diff --git a/WebApplication/src/main-retrieve-and-view.js b/WebApplication/src/main-retrieve-and-view.js new file mode 100644 index 0000000..9c7bf58 --- /dev/null +++ b/WebApplication/src/main-retrieve-and-view.js @@ -0,0 +1,41 @@ +import { createApp } from 'vue' +import AppRetrieveAndView from './AppRetrieveAndView.vue' +import i18n from './locales/i18n' +import "bootstrap/dist/css/bootstrap.min.css" +import "bootstrap/dist/js/bootstrap.bundle.min.js" +import "bootstrap-icons/font/bootstrap-icons.css" +import "@fortawesome/fontawesome-free/css/all.min.css" +import store from "./store" +import orthancApi from './orthancApi' +import axios from 'axios' + + +// Names of the params that can contain an authorization token +// If one of these params contain a token, it will be passed as a header +// with each request to the Orthanc API +const VALID_TOKEN_PARAMS = ["token", "auth-token", "authorization"]; + + +// before initialization, we must load part of the configuration to know if we need to enable Keycloak or not +axios.get('../api/pre-login-configuration').then((config) => { + + const app = createApp(AppRetrieveAndView) + + app.use(store) + app.use(i18n) + + // If there is a param with a token in the params, use it as a header in subsequent calls to the Orthanc API + const params = new URLSearchParams(window.location.search); + + for (let paramName of VALID_TOKEN_PARAMS) { + const paramValue = params.get(paramName); + + if (!paramValue) continue; + + localStorage.setItem(paramName, paramValue); + orthancApi.updateAuthHeader(paramName); + } + + app.mount('#app-retrieve-and-view') + +}); diff --git a/WebApplication/src/main.js b/WebApplication/src/main.js new file mode 100644 index 0000000..396f409 --- /dev/null +++ b/WebApplication/src/main.js @@ -0,0 +1,115 @@ +import { createApp } from 'vue' +import App from './App.vue' +import i18n from './locales/i18n' +import "bootstrap/dist/css/bootstrap.min.css" +import "bootstrap/dist/js/bootstrap.bundle.min.js" +import "bootstrap-icons/font/bootstrap-icons.css" +import "@fortawesome/fontawesome-free/css/all.min.css" +import "./assets/css/layout.css" +import "./assets/css/common.css" +import store from "./store" +import { router } from './router' +import Keycloak from "keycloak-js" +import orthancApi from './orthancApi' +import axios from 'axios' +import Datepicker from '@vuepic/vue-datepicker'; +import '@vuepic/vue-datepicker/dist/main.css'; +import mitt from "mitt" +import VueObserveVisibility from 'vue3-observe-visibility' + +// Names of the params that can contain an authorization token +// If one of these params contain a token, it will be passed as a header +// with each request to the Orthanc API +const VALID_TOKEN_PARAMS = ["token", "auth-token", "authorization"]; + +// before initialization, we must load part of the configuration to know if we need to enable Keycloak or not +axios.get('../api/pre-login-configuration').then((config) => { + + const app = createApp(App) + const messageBus = mitt(); + + app.use(router) + app.use(store) + app.use(i18n) + app.use(VueObserveVisibility) + app.component('Datepicker', Datepicker); + + app.config.globalProperties.messageBus = messageBus; + + let keycloackConfig = null; + + if (config.data['Keycloak'] && config.data['Keycloak'] != null && config.data['Keycloak']['Enable']) { + console.log("Keycloak is enabled"); + + keycloackConfig = config.data['Keycloak'] + + let initOptions = { + url: keycloackConfig['Url'], + realm: keycloackConfig['Realm'], + clientId: keycloackConfig['ClientId'], + onLoad: 'login-required' + } + + window.keycloak = new Keycloak(initOptions); + + window.keycloak.init({ onLoad: initOptions.onLoad }).then(async (auth) => { + + if (!auth) { + window.location.reload(); + } else { + console.log("Authenticated"); + } + + localStorage.setItem("vue-token", window.keycloak.token); + localStorage.setItem("vue-refresh-token", window.keycloak.refreshToken); + orthancApi.updateAuthHeader(); + + app.mount('#app'); + console.log("App mounted with keycloak, current route is ", router.currentRoute.value.fullPath); + + // keycloak includes state, code and session_state -> the router does not like them -> remove them + const params = new URLSearchParams(router.currentRoute.value.fullPath); + params.delete('state'); + params.delete('code'); + params.delete('session_state'); + const cleanedRoute = decodeURIComponent(params.toString()).replace('/=', '/'); + console.log("App mounted, moving to cleaned route ", cleanedRoute); + await router.push(cleanedRoute); + + // programm token refresh at regular interval + setInterval(() => { + window.keycloak.updateToken(70).then((refreshed) => { + if (refreshed) { + console.log('Token refreshed'); + localStorage.setItem("vue-token", window.keycloak.token); + localStorage.setItem("vue-refresh-token", window.keycloak.refreshToken); + orthancApi.updateAuthHeader(); + } else { + console.log('Token not refreshed, valid for ' + + Math.round(window.keycloak.tokenParsed.exp + window.keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds'); + } + }).catch(() => { + console.log('Failed to refresh token'); + }); + + }, 60000) + + }).catch(() => { + console.log("Could not connect to Keycloak"); + }); + } else { + // If there is a param with a token in the params, use it as a header in subsequent calls to the Orthanc API + const params = new URLSearchParams(window.location.search); + + for (let paramName of VALID_TOKEN_PARAMS) { + const paramValue = params.get(paramName); + + if (!paramValue) continue; + + localStorage.setItem(paramName, paramValue); + orthancApi.updateAuthHeader(paramName); + } + + app.mount('#app') + } +}); diff --git a/WebApplication/src/orthancApi.js b/WebApplication/src/orthancApi.js new file mode 100644 index 0000000..c7e3033 --- /dev/null +++ b/WebApplication/src/orthancApi.js @@ -0,0 +1,682 @@ +import axios from "axios" +import store from "./store" + +import { orthancApiUrl, oe2ApiUrl } from "./globalConfigurations"; + +export default { + updateAuthHeader(headerKey = null) { + axios.defaults.headers.common[headerKey ?? "token"] = localStorage.getItem(headerKey ?? "vue-token") + }, + async loadOe2Configuration() { + return (await axios.get(oe2ApiUrl + "configuration")).data; + }, + async loadDicomWebServers() { + return (await axios.get(orthancApiUrl + "dicom-web/servers")).data; + }, + async loadOrthancPeers() { + return (await axios.get(orthancApiUrl + "peers")).data; + }, + async loadDicomModalities() { + return (await axios.get(orthancApiUrl + "modalities?expand")).data; + }, + async loadSystem() { + return (await axios.get(orthancApiUrl + "system")).data; + }, + async sendToDicomWebServer(resourcesIds, destination) { + const response = (await axios.post(orthancApiUrl + "dicom-web/servers/" + destination + "/stow", { + "Resources" : resourcesIds, + "Synchronous": false + })); + + return response.data['ID']; + }, + async sendToOrthancPeer(resourcesIds, destination) { + const response = (await axios.post(orthancApiUrl + "peers/" + destination + "/store", { + "Resources" : resourcesIds, + "Synchronous": false + })); + + return response.data['ID']; + }, + async sendToOrthancPeerWithTransfers(resources, destination) { + const response = (await axios.post(orthancApiUrl + "transfers/send", { + "Resources" : resources, + "Compression": "gzip", + "Peer": destination, + "Synchronous": false + })); + + return response.data['ID']; + }, + async sendToDicomModality(resourcesIds, destination) { + const response = (await axios.post(orthancApiUrl + "modalities/" + destination + "/store", { + "Resources" : resourcesIds, + "Synchronous": false + })); + + return response.data['ID']; + }, + async getJobStatus(jobId) { + const response = (await axios.get(orthancApiUrl + "jobs/" + jobId)); + return response.data; + }, + async deleteResource(level, orthancId) { + return axios.delete(orthancApiUrl + this.pluralizeResourceLevel(level) + "/" + orthancId); + }, + async deleteResources(resourcesIds) { + return axios.post(orthancApiUrl + "tools/bulk-delete", { + "Resources": resourcesIds + }); + }, + async cancelFindStudies() { + if (window.axiosFindStudiesAbortController) { + window.axiosFindStudiesAbortController.abort(); + window.axiosFindStudiesAbortController = null; + } + }, + async findStudies(filterQuery, labels, LabelsConstraint, orderBy, since) { + await this.cancelFindStudies(); + window.axiosFindStudiesAbortController = new AbortController(); + + let limit = store.state.configuration.uiOptions.MaxStudiesDisplayed; + if (store.state.configuration.hasExtendedFind) { + limit = store.state.configuration.uiOptions.PageLoadSize; + } + + let payload = { + "Level": "Study", + "Limit": limit, + "Query": filterQuery, + "RequestedTags": store.state.configuration.requestedTagsForStudyList, + "Expand": true + }; + + if (labels && labels.length > 0) { + payload["Labels"] = labels; + payload["LabelsConstraint"] = LabelsConstraint; + } + + if (orderBy && orderBy.length > 0) { + payload["OrderBy"] = orderBy; + } + + if (since) { + payload["Since"] = since; + } + + return (await axios.post(orthancApiUrl + "tools/find", payload, + { + signal: window.axiosFindStudiesAbortController.signal + })).data; + }, + async getMostRecentStudiesExtended(label) { + await this.cancelFindStudies(); + window.axiosFindStudiesAbortController = new AbortController(); + + let payload = { + "Level": "Study", + "Limit": store.state.configuration.uiOptions.PageLoadSize, + "Query": {}, + "RequestedTags": store.state.configuration.requestedTagsForStudyList, + "OrderBy" : [{ + 'Type': 'Metadata', + 'Key': 'LastUpdate', + 'Direction': 'DESC' + }], + "Expand": true + }; + if (label) { + payload["Labels"] = [label]; + payload["LabelsConstraint"] = "All"; + } + + return (await axios.post(orthancApiUrl + "tools/find", payload, + { + signal: window.axiosFindStudiesAbortController.signal + })).data; + }, + async getLastChangeId() { + const response = (await axios.get(orthancApiUrl + "changes?last")); + return response.data["Last"]; + }, + async getChanges(since, limit) { + const response = (await axios.get(orthancApiUrl + "changes?since=" + since + "&limit=" + limit)); + return response.data; + }, + async getChangesExtended(to, limit, filter = []) { + let url = orthancApiUrl + "changes?to=" + to + "&limit=" + limit; + if (filter.length > 0) { + url += "&type=" + filter.join(";") + } + const response = (await axios.get(url)); + return response.data; + }, + async getSamePatientStudies(patientTags, tags) { + if (!tags || tags.length == 0) { + console.error("Unable to getSamePatientStudies if 'tags' is not defined or empty"); + return {}; + } + + let query = {}; + for (let tag of tags) { + if (tag in patientTags) { + query[tag] = patientTags[tag]; + } + } + const response = (await axios.post(orthancApiUrl + "tools/find", { + "Level": "Study", + "Query": query, + "Expand": false + })); + return response.data; + }, + async findPatient(patientId) { + const response = (await axios.post(orthancApiUrl + "tools/lookup", patientId)); + if (response.data.length == 1) { + const patient = (await axios.get(orthancApiUrl + "patients/" + response.data[0]['ID'])); + return patient.data; + } else { + return null; + } + }, + async findStudy(studyInstanceUid) { + const response = (await axios.post(orthancApiUrl + "tools/lookup", studyInstanceUid)); + if (response.data.length == 1) { + const study = (await axios.get(orthancApiUrl + "studies/" + response.data[0]['ID'])); + return study.data; + } else { + return null; + } + }, + async studyExists(studyInstanceUid) { + const response = (await axios.post(orthancApiUrl + "tools/lookup", studyInstanceUid)); + return response.data.length >= 1; + }, + async mergeSeriesInExistingStudy({seriesIds, targetStudyId, keepSource}) { + const response = (await axios.post(orthancApiUrl + "studies/" + targetStudyId + "/merge", { + "Resources": seriesIds, + "KeepSource": keepSource, + "Synchronous": false + })); + return response.data['ID']; + }, + async cancelRemoteDicomFind() { + if (window.axiosRemoteDicomFindAbortController) { + window.axiosRemoteDicomFindAbortController.abort(); + window.axiosRemoteDicomFindAbortController = null; + } + }, + async remoteDicomFind(level, remoteModality, filterQuery, isUnique) { + if (isUnique) { + await this.cancelRemoteDicomFind(); + window.axiosRemoteDicomFindAbortController = new AbortController(); + } + + try { + let axiosOptions = {} + if (isUnique) { + axiosOptions['signal'] = window.axiosRemoteDicomFindAbortController.signal + } + const queryResponse = (await axios.post(orthancApiUrl + "modalities/" + remoteModality + "/query", { + "Level": level, + "Query": filterQuery + }, + axiosOptions + )).data; + // console.log(queryResponse); + const answers = (await axios.get(orthancApiUrl + "queries/" + queryResponse["ID"] + "/answers?expand&simplify")).data; + // console.log(answers); + return answers; + } catch (err) + { + console.log("Error during query:", err); // TODO: display error to user + return {}; + } + }, + async qidoRs(level, remoteServer, filterQuery, isUnique){ + if (isUnique) { + await this.cancelQidoRs(); + window.axiosQidoRsAbortController = new AbortController(); + } + + try { + let axiosOptions = {} + if (isUnique) { + axiosOptions['signal'] = window.axiosQidoRsAbortController.signal + } + let uri = null; + if (level == "Study") { + uri = "/studies"; + } else if (level == "Series") { + uri = "/studies/" + filterQuery["StudyInstanceUID"] + "/series"; + delete filterQuery["StudyInstanceUID"]; // we don't need it the filter since it is in the url + } else if (level == "Instance") { + uri = "/studies/" + filterQuery["StudyInstanceUID"] + "/series/" + filterQuery["SeriesInstanceUID"] + "/instances"; + delete filterQuery["StudyInstanceUID"]; // we don't need it the filter since it is in the url + delete filterQuery["SeriesInstanceUID"]; + } + let args = {...filterQuery}; + args["limit"] = String(store.state.configuration.uiOptions.MaxStudiesDisplayed); + args["fuzzymatching"] = "true"; + + const queryResponse = (await axios.post(orthancApiUrl + "dicom-web/servers/" + remoteServer + "/qido", { + "Uri": uri, + "Arguments": args + }, + axiosOptions + )).data; + // transform the response into something similar to a DICOM C-Find API response + let responses = []; + for (let qr of queryResponse) { + let r = {} + for (const [k, v] of Object.entries(qr)) { + if (v.Value) { + r[v.Name] = v.Value; + } + } + responses.push(r); + } + return responses; + } catch (err) + { + console.log("Error during query:", err); // TODO: display error to user + return {}; + } + }, + async cancelQidoRs() { + if (window.axiosQidoRsAbortController) { + window.axiosQidoRsAbortController.abort(); + window.axiosQidoRsAbortController = null; + } + }, + async wadoRsRetrieve(remoteServer, resources){ + const retrieveJob = (await axios.post(orthancApiUrl + "dicom-web/servers/" + remoteServer + "/retrieve", { + "Resources": resources, + "Asynchronous": true + } + )).data; + return retrieveJob["ID"]; + }, + async remoteDicomRetrieveResource(level, remoteModality, filterQuery, targetAet) { + return this.remoteDicomRetrieveResources(level, remoteModality, [filterQuery], targetAet); + }, + async remoteDicomRetrieveResources(level, remoteModality, filterQueries, targetAet) { + const retrieveMethod = this.getRetrieveMethod(remoteModality); + + let uriSegment = "/get"; + let query = { + "Level": level, + "Resources" : filterQueries, + "Synchronous": false + } + + if (retrieveMethod == "C-MOVE") { + query["TargetAet"] = targetAet; + uriSegment = "/move"; + } + + const response = (await axios.post(orthancApiUrl + "modalities/" + remoteModality + uriSegment, query)); + return response.data['ID']; + }, + async remoteModalityEcho(remoteModality) { + return axios.post(orthancApiUrl + "modalities/" + remoteModality + "/echo", {}); + }, + async uploadFile(filecontent) { + return (await axios.post(orthancApiUrl + "instances", filecontent)).data; + }, + async triggerMammographyPlugin(requestBodyData) { + return (await axios.post(orthancApiUrl + "mammography-apply", requestBodyData)).data; + }, + async createDicom(parentId, content, tags) { + return (await axios.post(orthancApiUrl + "tools/create-dicom", { + "Parent": parentId, + "Tags": tags, + "Content": content + })).data + }, + async getPatient(orthancId) { + return (await axios.get(orthancApiUrl + "patients/" + orthancId)).data; + }, + async getStudy(orthancId) { + // returns the same result as a findStudies (including RequestedTags !) + return (await axios.get(orthancApiUrl + "studies/" + orthancId + "?requestedTags=" + store.state.configuration.requestedTagsForStudyList.join(';'))).data; + }, + async getStudySeries(orthancId) { + return (await axios.get(orthancApiUrl + "studies/" + orthancId + "/series")).data; + }, + async getSeriesInstances(orthancId) { + return (await axios.get(orthancApiUrl + "series/" + orthancId + "/instances")).data; + }, + async getSeriesInstancesExtended(orthancId, since) { + const limit = store.state.configuration.uiOptions.PageLoadSize; + let payload = { + "Level": "Instance", + "Limit": limit, + "ParentSeries": orthancId, + "Query": {}, + "OrderBy" : [ + { "Type": "MetadataAsInt", + "Key": "IndexInSeries", + "Direction": "ASC" + } + ], + "Expand": true + }; + + if (since) { + payload["Since"] = since; + } + + const response = (await axios.post(orthancApiUrl + "tools/find", payload)); + return response.data; + }, + async getStudyInstances(orthancId) { + return (await axios.get(orthancApiUrl + "studies/" + orthancId + "/instances")).data; + }, + async getStudyInstancesExpanded(orthancId, requestedTags) { + let url = orthancApiUrl + "studies/" + orthancId + "/instances?expanded"; + if (requestedTags && requestedTags.length > 0) { + url += "&requestedTags=" + requestedTags.join(";") + } + return (await axios.get(url)).data; + }, + async getSeriesParentStudy(orthancId) { + return (await axios.get(orthancApiUrl + "series/" + orthancId + "/study")).data; + }, + async getInstanceParentStudy(orthancId) { + return (await axios.get(orthancApiUrl + "instances/" + orthancId + "/study")).data; + }, + async getResourceStudy(orthancId, level) { + if (level == "study") { + return (await this.getStudy(orthancId)); + } else if (level == "series") { + return (await this.getSeriesParentStudy(orthancId)); + } else if (level == "instance") { + return (await this.getInstanceParentStudy(orthancId)); + } else { + console.error("unsupported level for getResourceStudyId", level); + } + }, + async getInstanceTags(orthancId) { + return (await axios.get(orthancApiUrl + "instances/" + orthancId + "/tags")).data; + }, + async getSimplifiedInstanceTags(orthancId) { + return (await axios.get(orthancApiUrl + "instances/" + orthancId + "/tags?simplify")).data; + }, + async getInstanceHeader(orthancId) { + return (await axios.get(orthancApiUrl + "instances/" + orthancId + "/header")).data; + }, + async getStatistics() { + return (await axios.get(orthancApiUrl + "statistics")).data; + }, + async getPermissionsList() { + return (await axios.get(orthancApiUrl + "auth/settings/permissions")).data; + }, + async getRolesConfig() { + return (await axios.get(orthancApiUrl + "auth/settings/roles")).data; + }, + async setRolesConfig(roles) { + return (await axios.put(orthancApiUrl + "auth/settings/roles", roles)).data; + }, + async getDelayedDeletionStatus() { + return (await axios.get(orthancApiUrl + "plugins/delayed-deletion/status")).data; + }, + async generateUid(level) { + return (await axios.get(orthancApiUrl + "tools/generate-uid?level=" + level)).data; + }, + async setVerboseLevel(level) { + await axios.put(orthancApiUrl + "tools/log-level", level); + }, + + async getVerboseLevel() { + return (await axios.get(orthancApiUrl + "tools/log-level")).data; + }, + + async anonymizeResource({resourceLevel, orthancId, replaceTags={}, removeTags=[]}) { + const response = (await axios.post(orthancApiUrl + this.pluralizeResourceLevel(resourceLevel) + "/" + orthancId + "/anonymize", { + "Replace": replaceTags, + "Remove": removeTags, + "KeepSource": true, + "Force": true, + "Synchronous": false + })) + + return response.data['ID']; + }, + + async modifyResource({resourceLevel, orthancId, replaceTags={}, removeTags=[], keepTags=[], keepSource}) { + const response = (await axios.post(orthancApiUrl + this.pluralizeResourceLevel(resourceLevel) + "/" + orthancId + "/modify", { + "Replace": replaceTags, + "Remove": removeTags, + "Keep": keepTags, + "KeepSource": keepSource, + "KeepLabels": true, + "Force": true, + "Synchronous": false + })) + + return response.data['ID']; + }, + + async loadAllLabels() { + const response = (await axios.get(orthancApiUrl + "tools/labels")); + return response.data; + }, + + async addLabel({studyId, label}) { + await axios.put(orthancApiUrl + "studies/" + studyId + "/labels/" + label, ""); + return label; + }, + + async removeLabel({studyId, label}) { + await axios.delete(orthancApiUrl + "studies/" + studyId + "/labels/" + label); + return label; + }, + + async removeAllLabels(studyId) { + const labels = await this.getLabels(studyId); + let promises = []; + for (let label of labels) { + promises.push(this.removeLabel({ + studyId: studyId, + label: label + })); + } + await Promise.all(promises); + return labels; + }, + + async getLabels(studyId) { + const response = (await axios.get(orthancApiUrl + "studies/" + studyId + "/labels")); + return response.data; + }, + + async updateLabels({studyId, labels}) { + const currentLabels = await this.getLabels(studyId); + const labelsToRemove = currentLabels.filter(x => !labels.includes(x)); + const labelsToAdd = labels.filter(x => !currentLabels.includes(x)); + let promises = []; + + // console.log("labelsToRemove: ", labelsToRemove); + // console.log("labelsToAdd: ", labelsToAdd); + for (const label of labelsToRemove) { + promises.push(this.removeLabel({ + studyId: studyId, + label: label + })); + } + for (const label of labelsToAdd) { + promises.push(this.addLabel({ + studyId: studyId, + label: label + })); + } + await Promise.all(promises); + return labelsToAdd.length > 0 || labelsToRemove.length > 0; + }, + + + + async createToken({tokenType, resourcesIds, level, validityDuration=null, id=null, expirationDate=null}) { + let body = { + "Resources" : [], + "Type": tokenType + } + + for (let resourceId of resourcesIds) { + // the authorization are performed at study level -> get parent study id if needed + const study = await this.getResourceStudy(resourceId, level); + body["Resources"].push({ + "OrthancId": study["ID"], + "DicomUid": study["MainDicomTags"]["StudyInstanceUID"], + "Level": 'study' + }) + } + + if (validityDuration != null) { + body["ValidityDuration"] = validityDuration; + } + + if (expirationDate != null) { + body["ExpirationDate"] = expirationDate.toJSON(); + } + + if (id != null) { + body["Id"] = id; + } + + const response = (await axios.put(orthancApiUrl + "auth/tokens/" + tokenType, body)); + // console.log(response); + + return response.data; + }, + async parseToken(tokenKey, tokenValue) { + const response = (await axios.post(orthancApiUrl + "auth/tokens/decode", { + "TokenKey": tokenKey, + "TokenValue": tokenValue + })) + + return response.data; + }, + async getLabelStudyCount(label) { + const response = (await axios.post(orthancApiUrl + "tools/count-resources", { + "Level": "Study", + "Query": {}, + "Labels": [label], + "LabelsConstraint" : "All" + })); + return response.data["Count"]; + }, + + ////////////////////////////////////////// HELPERS + getOsimisViewerUrl(level, resourceOrthancId) { + return orthancApiUrl + 'osimis-viewer/app/index.html?' + level + '=' + resourceOrthancId; + }, + getStoneViewerUrl(level, resourceDicomUid) { + return orthancApiUrl + 'stone-webviewer/index.html?' + level + '=' + resourceDicomUid; + }, + getVolViewUrl(level, resourceOrthancId) { + const volViewVersion = store.state.configuration.installedPlugins.volview.Version; + const urls = 'urls=[../' + this.pluralizeResourceLevel(level) + '/' + resourceOrthancId + '/archive]'; + if (volViewVersion == '1.0') { + return orthancApiUrl + 'volview/index.html?' + urls; + } else { + return orthancApiUrl + 'volview/index.html?names=[archive.zip]&' + urls; + } + }, + getWsiViewerUrl(seriesOrthancId) { + return orthancApiUrl + 'wsi/app/viewer.html?series=' + seriesOrthancId; + }, + getStoneViewerUrlForBulkStudies(studiesDicomIds) { + return orthancApiUrl + 'stone-webviewer/index.html?study=' + studiesDicomIds.join(","); + }, + getOhifViewerUrlForDicomJson(mode, resourceOrthancId) { + if (mode == 'basic') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'viewer?url=../studies/' + resourceOrthancId + "/ohif-dicom-json"; + } else if (mode == 'vr') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'viewer?hangingprotocolId=mprAnd3DVolumeViewport&url=../studies/' + resourceOrthancId + "/ohif-dicom-json"; + } else if (mode == 'tmtv') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'tmtv?url=../studies/' + resourceOrthancId + "/ohif-dicom-json"; + } else if (mode == 'seg') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'segmentation?url=../studies/' + resourceOrthancId + "/ohif-dicom-json"; + } else if (mode == 'microscopy') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'microscopy?url=../studies/' + resourceOrthancId + "/ohif-dicom-json"; + } + }, + getOhifViewerUrlForDicomWeb(mode, resourceDicomUid) { + if (store.state.configuration.uiOptions.EnableOpenInOhifViewer3) { + if (mode == 'basic') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'viewer?StudyInstanceUIDs=' + resourceDicomUid; + } else if (mode == 'vr') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'viewer?hangingprotocolId=mprAnd3DVolumeViewport&StudyInstanceUIDs=' + resourceDicomUid; + } else if (mode == 'tmtv') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'tmtv?StudyInstanceUIDs=' + resourceDicomUid; + } else if (mode == 'seg') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'segmentation?StudyInstanceUIDs=' + resourceDicomUid; + } else if (mode == 'microscopy') { + return store.state.configuration.uiOptions.OhifViewer3PublicRoot + 'microscopy?StudyInstanceUIDs=' + resourceDicomUid; + } + } else { + return store.state.configuration.uiOptions.OhifViewerPublicRoot + 'Viewer/' + resourceDicomUid; + } + }, + getOhifViewerUrlForDicomWebBulkStudies(mode, studiesDicomIds) { + if (store.state.configuration.uiOptions.EnableOpenInOhifViewer3) { + return this.getOhifViewerUrlForDicomWeb(mode, studiesDicomIds.join(",")); + } else { + return null; + } + }, + getInstancePreviewUrl(orthancId) { + return orthancApiUrl + "instances/" + orthancId + "/preview"; + }, + getInstancePdfUrl(orthancId) { + return orthancApiUrl + "instances/" + orthancId + "/pdf"; + }, + getInstanceDownloadUrl(orthancId) { + return orthancApiUrl + "instances/" + orthancId + "/file"; + }, + getDownloadZipUrl(level, resourceOrthancId) { + return orthancApiUrl + this.pluralizeResourceLevel(level) + '/' + resourceOrthancId + '/archive'; + }, + getBulkDownloadZipUrl(resourcesOrthancId) { + if (resourcesOrthancId.length > 0) + { + return orthancApiUrl + "tools/create-archive?resources=" + resourcesOrthancId.join(','); + } + return undefined; + }, + getBulkDownloadDicomDirUrl(resourcesOrthancId) { + if (resourcesOrthancId.length > 0) + { + return orthancApiUrl + "tools/create-media?resources=" + resourcesOrthancId.join(','); + } + return undefined; + }, + getDownloadDicomDirUrl(level, resourceOrthancId) { + return orthancApiUrl + this.pluralizeResourceLevel(level) + '/' + resourceOrthancId + '/media'; + }, + getApiUrl(level, resourceOrthancId, subroute) { + return orthancApiUrl + this.pluralizeResourceLevel(level) + '/' + resourceOrthancId + subroute; + }, + getRetrieveMethod(modality) { + const defaultRetrieveMethod = store.state.configuration.system.DefaultRetrieveMethod; + if (store.state.configuration.queryableDicomModalities[modality].RetrieveMethod == "SystemDefault") { + return defaultRetrieveMethod; + } else { + return store.state.configuration.queryableDicomModalities[modality]; + } + }, + + pluralizeResourceLevel(level) { + if (level == "study") { + return "studies" + } else if (level == "instance") { + return "instances" + } else if (level == "patient") { + return "patients" + } else if (level == "series") { + return "series" + } + } +} diff --git a/WebApplication/src/router.js b/WebApplication/src/router.js new file mode 100644 index 0000000..320710a --- /dev/null +++ b/WebApplication/src/router.js @@ -0,0 +1,71 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import Settings from './components/Settings.vue' +import SettingsLabels from './components/SettingsLabels.vue' +import SettingsPermissions from './components/SettingsPermissions.vue' +import StudyList from './components/StudyList.vue' +import SideBar from './components/SideBar.vue' +import NotFound from './components/NotFound.vue' +import { baseOe2Url } from "./globalConfigurations" + +console.log('Base URL for router: ', baseOe2Url); + +function removeKeyCloakStates(to, from, next) { + if (to.path.includes("&state=")) { + // the router does not recognize the &state=... after a redirect from KeyCloak -> simply block it (it works but I don't really understand why !) + console.log("removeKeyCloakStates", to, from); + next(false); + } else { + next(); + } +} + +export const router = createRouter({ + history: createWebHashHistory(baseOe2Url), + routes: [ + { + path: '/', + alias: '/index.html', + components: { + SideBarView: SideBar, + ContentView: StudyList, + }, + }, + { + path: '/filtered-studies', + components: { + SideBarView: SideBar, + ContentView: StudyList, + }, + }, + { + path: '/settings', + components: { + SideBarView: SideBar, + ContentView: Settings, + }, + }, + { + path: '/settings-labels', + components: { + SideBarView: SideBar, + ContentView: SettingsLabels, + }, + }, + { + path: '/settings-permissions', + components: { + SideBarView: SideBar, + ContentView: SettingsPermissions, + }, + }, + { + path: '/:pathMatch(.*)', + beforeEnter: removeKeyCloakStates, + components: { + SideBarView: SideBar, + ContentView: NotFound, + }, + } + + ], +}) diff --git a/WebApplication/src/store/index.js b/WebApplication/src/store/index.js new file mode 100644 index 0000000..2880b8c --- /dev/null +++ b/WebApplication/src/store/index.js @@ -0,0 +1,18 @@ +import { createStore, createLogger } from 'vuex' +import configuration from './modules/configuration' +import studies from './modules/studies' +import jobs from './modules/jobs' +import labels from './modules/labels' + +const debug = process.env.NODE_ENV !== 'production' + +export default createStore({ + modules: { + studies, + configuration, + jobs, + labels + }, + strict: debug, + plugins: debug ? [createLogger()] : [] +}) \ No newline at end of file diff --git a/WebApplication/src/store/modules/configuration.js b/WebApplication/src/store/modules/configuration.js new file mode 100644 index 0000000..95d9b8f --- /dev/null +++ b/WebApplication/src/store/modules/configuration.js @@ -0,0 +1,176 @@ +import api from "../../orthancApi" +import resourceHelpers from "../../helpers/resource-helpers" + +///////////////////////////// STATE +const state = () => ({ + installedPlugins: {}, + orthancPeers: [], + targetDicomWebServers: [], + queryableDicomWebServers: [], + queryableDicomModalities: {}, + targetDicomModalities: {}, + maxStudiesDisplayed: 100, + orthancApiUrl: "../../", + uiOptions: {}, + userProfile: null, + tokens: {}, + loaded: false, + system: {}, + ohifDataSource: "dicom-web", + customLogoUrl: null, + hasCustomLogo: false, + requestedTagsForStudyList: [], + hasExtendedFind: false, + hasExtendedChanges: false, +}) + +///////////////////////////// GETTERS +const getters = { +} + +///////////////////////////// MUTATIONS + +const mutations = { + setUiOptions(state, { uiOptions }) { + state.uiOptions = uiOptions; + + if (uiOptions.StudyListColumns.indexOf('modalities') != -1) { + state.requestedTagsForStudyList.push('ModalitiesInStudy') + } + if (uiOptions.StudyListColumns.indexOf('instancesCount') != -1 || uiOptions.StudyListColumns.indexOf('seriesAndInstancesCount') != -1) { + state.requestedTagsForStudyList.push('NumberOfStudyRelatedInstances') + } + if (uiOptions.StudyListColumns.indexOf('seriesCount') != -1 || uiOptions.StudyListColumns.indexOf('seriesAndInstancesCount') != -1) { + state.requestedTagsForStudyList.push('NumberOfStudyRelatedSeries') + } + if (uiOptions.EnableReportQuickButton) { + state.requestedTagsForStudyList.push('SOPClassesInStudy'); // to detect PDF files + } + }, + setUserProfile(state, { profile }) { + state.userProfile = profile; + }, + setInstalledPlugin(state, { plugin, pluginConfiguration }) { + state.installedPlugins[plugin] = pluginConfiguration; + }, + setDicomWebServers(state, { dicomWebServers }) { // TODO: split in two + state.targetDicomWebServers = dicomWebServers; + state.queryableDicomWebServers = dicomWebServers; + }, + setOhifDataSource(state, { ohifDataSource }) { + state.ohifDataSource = ohifDataSource; + }, + setOrthancPeers(state, { orthancPeers }) { + state.orthancPeers = orthancPeers; + }, + setDicomModalities(state, { dicomModalities }) { // TODO: split in two + state.queryableDicomModalities = dicomModalities; + state.targetDicomModalities = dicomModalities; + }, + setSystem(state, { system }) { + state.system = system; + state.hasExtendedFind = "Capabilities" in state.system && state.system.Capabilities.HasExtendedFind; + state.hasExtendedChanges = "Capabilities" in state.system && state.system.Capabilities.HasExtendedChanges; + }, + setLoaded(state) { + state.loaded = true; + }, + setTokens(state, { tokens }) { + state.tokens = tokens; + }, + setCustomLogo(state, { customLogoUrl, hasCustomLogo }) { + state.hasCustomLogo = hasCustomLogo; + state.customLogoUrl = customLogoUrl; + } + +} + +///////////////////////////// ACTIONS + +const actions = { + async load({ commit, state}) { + await this.dispatch('configuration/loadOe2Configuration'); + + if (state.uiOptions.EnableSendTo) { + try { + const orthancPeers = await api.loadOrthancPeers(); + commit('setOrthancPeers', { orthancPeers: orthancPeers}); + } catch (err) { + console.warn("can not get Orthanc peers - not authorized ?") + } + } + + if (state.uiOptions.EnableSendTo || state.uiOptions.EnableDicomModalities) { + try { + const dicomModalities = await api.loadDicomModalities(); + commit('setDicomModalities', { dicomModalities: dicomModalities}); + } catch (err) { + console.warn("can not get DICOM modalities - not authorized ?") + } + } + + const system = await api.loadSystem(); + commit('setSystem', { system: system}); + + commit('setLoaded'); + }, + async loadOe2Configuration({ commit }) { + const oe2Config = await api.loadOe2Configuration(); + commit('setUiOptions', { uiOptions: oe2Config['UiOptions']}); + commit('setTokens', { tokens: oe2Config['Tokens']}); + + if ('Profile' in oe2Config) { + commit('setUserProfile', { profile: oe2Config['Profile']}); + } + document._mustTranslateDicomTags = oe2Config['UiOptions']['TranslateDicomTags']; + + if ('HasCustomLogo' in oe2Config) { + let customLogoUrl = null; + if ('CustomLogoUrl' in oe2Config) { + customLogoUrl = oe2Config['CustomLogoUrl'] + } + commit('setCustomLogo', { customLogoUrl: customLogoUrl, hasCustomLogo: oe2Config['HasCustomLogo']}); + } + + if ('CustomTitle' in oe2Config) { + document.title = oe2Config['CustomTitle']; + } else { + document.title = "Orthanc Explorer 2"; + } + + if ('UiOptions' in oe2Config && 'PatientNameFormatting' in oe2Config['UiOptions'] && 'PatientNameCapture' in oe2Config['UiOptions']) { + resourceHelpers.patientNameCapture = oe2Config['UiOptions']['PatientNameCapture']; + resourceHelpers.patientNameFormatting = oe2Config['UiOptions']['PatientNameFormatting']; + } + + for (const [pluginName, pluginConfiguration] of Object.entries(oe2Config['Plugins'])) { + + commit('setInstalledPlugin', { plugin: pluginName, pluginConfiguration: pluginConfiguration}) + + if (pluginName === "dicom-web") { + try { + const dicomWebServers = await api.loadDicomWebServers(); + commit('setDicomWebServers', { dicomWebServers: dicomWebServers}); + } catch (err) { + console.warn("can not get DicomWEB servers - not authorized ?") + } + } else if (pluginName === "ohif") { + commit('setOhifDataSource', { ohifDataSource: pluginConfiguration["DataSource"]}) + } + + } + + }, +} + + + +///////////////////////////// EXPORT + +export default { + namespaced: true, + state, + getters, + mutations, + actions, +} \ No newline at end of file diff --git a/WebApplication/src/store/modules/i18n.js b/WebApplication/src/store/modules/i18n.js new file mode 100644 index 0000000..e7af5b4 --- /dev/null +++ b/WebApplication/src/store/modules/i18n.js @@ -0,0 +1,21 @@ +let allLanguages = [ + { name: "English", key: "en" }, + { name: "Deutsch", key: "de" }, + { name: "Español", key: "es" }, + { name: "Français", key: "fr" }, + { name: "Italiano", key: "it" }, + { name: "ქართული", key: "ka" }, + { name: "Русский", key: "ru" }, + { name: "Slovensko", key: "si" }, + { name: "Українська", key: "uk" }, + { name: '中文', key: 'zh' } + // { name: 'Português', key: 'pt' }, + // { name: 'Deutsche', key: 'de' }, + // { name: 'Nederlands', key: 'nl' }, + // { name: '日本語', key: 'jp' }, + // { name: 'বাংলা', key: 'bn' }, + // { name: 'Bahasa', key: 'xn' }, + // { name: 'हिंदू', key: 'hi' }, + // { name: 'Kiswahili', key: 'sw' }, +] +export default allLanguages; \ No newline at end of file diff --git a/WebApplication/src/store/modules/jobs.js b/WebApplication/src/store/modules/jobs.js new file mode 100644 index 0000000..3b5d7c3 --- /dev/null +++ b/WebApplication/src/store/modules/jobs.js @@ -0,0 +1,73 @@ +import api from "../../orthancApi" + +// job = { +// 'id': '123' +// 'name': "Send to DICOM PACS" +// 'isRunning': false, +// 'status': //response from orthanc /jobs/... +// } + +///////////////////////////// STATE +const state = () => ({ + jobs: {}, // map of jobs + jobsIds: [], // jobs ids created by the user during this session + maxJobsInHistory: 5 +}) + +///////////////////////////// GETTERS +const getters = { +} + +///////////////////////////// MUTATIONS + +const mutations = { + addJob(state, { jobId, name }) { + const job = { + 'id': jobId, + 'name': name, + 'isRunning': true, + 'status': null + } + state.jobsIds.push(jobId); + state.jobs[jobId] = job; + }, + removeJob(state, { jobId }) { + const pos = state.jobsIds.indexOf(jobId); + if (pos >= 0) { + state.jobsIds.splice(pos, 1); + } + delete state.jobs[jobId]; + } +} + +///////////////////////////// ACTIONS + +const actions = { + addJob({ commit, state }, payload) { + const jobId = payload['jobId']; + const name = payload['name']; + commit('addJob', { jobId: jobId, name: name }); + + if (this.state.configuration.uiOptions.MaxMyJobsHistorySize > 0) { + while (state.jobsIds.length > this.state.configuration.uiOptions.MaxMyJobsHistorySize) { + commit('removeJob', { jobId: state.jobsIds[0] }) + } + } + }, + removeJob({ commit }, payload) { + const jobId = payload['jobId']; + commit('removeJob', { jobId: jobId }); + }, +} + + + +///////////////////////////// EXPORT + +export default { + namespaced: true, + state, + getters, + mutations, + actions, +} \ No newline at end of file diff --git a/WebApplication/src/store/modules/labels.js b/WebApplication/src/store/modules/labels.js new file mode 100644 index 0000000..067f4e9 --- /dev/null +++ b/WebApplication/src/store/modules/labels.js @@ -0,0 +1,39 @@ +import api from "../../orthancApi" + +///////////////////////////// STATE +const state = () => ({ + allLabels: [] // all labels in Orthanc +}) + +///////////////////////////// GETTERS +const getters = { +} + +///////////////////////////// MUTATIONS + +const mutations = { + setLabels(state, { allLabels }) { + state.allLabels = allLabels; + } +} + +///////////////////////////// ACTIONS + +const actions = { + async refresh({ commit }) { + const allLabels = await api.loadAllLabels(); + commit('setLabels', { allLabels: allLabels }); + } +} + + + +///////////////////////////// EXPORT + +export default { + namespaced: true, + state, + getters, + mutations, + actions, +} \ No newline at end of file diff --git a/WebApplication/src/store/modules/studies.js b/WebApplication/src/store/modules/studies.js new file mode 100644 index 0000000..4f117a3 --- /dev/null +++ b/WebApplication/src/store/modules/studies.js @@ -0,0 +1,369 @@ +import api from "../../orthancApi" +import SourceType from "../../helpers/source-type"; +import store from "../../store" + +const _clearedFilter = { + StudyDate : "", + AccessionNumber: "", + PatientID: "", + PatientName: "", + PatientBirthDate: "", + StudyDescription: "", + StudyInstanceUID: "", + ModalitiesInStudy: "", +} + +///////////////////////////// STATE +const state = () => ({ + studies: [], // studies as returned by tools/find + studiesIds: [], + dicomTagsFilters: {..._clearedFilter}, + labelFilters: [], + orderByFilters: [], + statistics: {}, + isSearching: false, + selectedStudiesIds: [], + selectedStudies: [], + sourceType: SourceType.LOCAL_ORTHANC, + remoteSource: null, +}) + +function insert_wildcards(initialValue) { + // 'filter' -> *filter* (by default, adds the wildcard before and after) + // '"filter' -> filter* (a double quote means "no wildcard") + // 'filter"' -> *filter (a double quote means "no wildcard") + // '"filter"' -> filter (= exact match) + let finalValue = '*' + initialValue.replaceAll('"', '*') + '*'; + return finalValue.replaceAll('**', ''); +} + +async function get_studies_shared(context, append) { + const commit = context.commit; + const state = context.state; + const getters = context.getters; + + if (!append) { + commit('setStudiesIds', { studiesIds: [] }); + commit('setStudies', { studies: [] }); + } + + try { + commit('setIsSearching', { isSearching: true}); + let studies = []; + + if (state.sourceType == SourceType.LOCAL_ORTHANC) { + let orderBy = [...state.orderByFilters]; + if (state.orderByFilters.length == 0) { + orderBy.push({'Type': 'Metadata', 'Key': 'LastUpdate', 'Direction': 'DESC'}) + } + let since = (append ? state.studiesIds.length : null); + + if (!store.state.configuration.hasExtendedFind) { + orderBy = null; + } + studies = (await api.findStudies(getters.filterQuery, state.labelFilters, "All", orderBy, since)); + } else if (state.sourceType == SourceType.REMOTE_DICOM || state.sourceType == SourceType.REMOTE_DICOM_WEB) { + // make sure to fill all columns of the StudyList + let filters = { + "PatientBirthDate": "", + "PatientID": "", + "PatientName": "", + "AccessionNumber": "", + "PatientBirthDate": "", + "StudyDescription": "", + "StudyDate": "" + }; + + // request values for e.g ModalitiesInStudy, NumberOfStudyRelatedSeries + for (let t of store.state.configuration.requestedTagsForStudyList) { + filters[t] = ""; + } + + // overwrite with the filtered values + for (const [k, v] of Object.entries(getters.filterQuery)) { + filters[k] = v; + } + + let remoteStudies; + if (state.sourceType == SourceType.REMOTE_DICOM) { + remoteStudies = (await api.remoteDicomFind("Study", state.remoteSource, filters, true /* isUnique */)); + } else if (state.sourceType == SourceType.REMOTE_DICOM_WEB) { + remoteStudies = (await api.qidoRs("Study", state.remoteSource, filters, true /* isUnique */)); + } + + // copy the tags in MainDicomTags, ... to have a common study structure between local and remote studies + studies = remoteStudies.map(s => { return {"MainDicomTags": s, "PatientMainDicomTags": s, "RequestedTags": s, "ID": s["StudyInstanceUID"]} }); + } + + studies = studies.map(s => {return {...s, "sourceType": state.sourceType} }); + let studiesIds = studies.map(s => s['ID']); + + if (!append) { + commit('setStudiesIds', { studiesIds: studiesIds }); + commit('setStudies', { studies: studies }); + } else { + commit('extendStudiesIds', { studiesIds: studiesIds }); + commit('extendStudies', { studies: studies }); + } + } catch (err) { + console.log("Find studies cancelled", err); + } finally { + commit('setIsSearching', { isSearching: false}); + } +} + +///////////////////////////// GETTERS +const getters = { + filterQuery: (state) => { + let query = {}; + for (const [k, v] of Object.entries(state.dicomTagsFilters)) { + if (v && v.length >= 1 && k != 'labels') { + if (['StudyDate', 'PatientBirthDate'].indexOf(k) != -1) { + // for dates, accept only exactly 8 chars + if (v.length >= 8) { + query[k] = v; + } + } else if (['StudyInstanceUID', 'ModalitiesInStudy'].indexOf(k) != -1) { + // exact match + query[k] = v; + } else { + // wildcard match for all other fields + query[k] = insert_wildcards(v); + } + } + } + return query; + }, + isFilterEmpty: (state) => { + for (const [k, v] of Object.entries(state.dicomTagsFilters)) { + if (k == 'ModalitiesInStudy') { + if (v && v != 'NONE') { + return false; + } + } else { + if (v && v.length >= 1) { + return false; + } + } + } + // dicomTags filter is empty, check the labels + return (!state.labelsFilter || state.labelsFilter.length == 0); + } +} + +///////////////////////////// MUTATIONS + +const mutations = { + setStudiesIds(state, { studiesIds }) { + state.studiesIds = studiesIds; + }, + setStudies(state, { studies }) { + state.studies = studies; + }, + extendStudiesIds(state, { studiesIds }) { + state.studiesIds.push(...studiesIds); + }, + extendStudies(state, { studies }) { + state.studies.push(...studies); + }, + addStudy(state, { studyId, study }) { + if (!state.studiesIds.includes(studyId)) { + state.studiesIds.push(studyId); + state.studies.push(study); + } else { + for (let s in state.studies) { + if (state.studies[s].ID == studyId) { + state.studies[s] = study; + } + } + } + }, + setFilter(state, { dicomTagName, value }) { + state.dicomTagsFilters[dicomTagName] = value; + }, + clearFilter(state) { + state.dicomTagsFilters = {..._clearedFilter}; + state.labelFilters = []; + }, + setLabelFilters(state, { labels }) { + state.labelFilters = []; + for (let f of labels) { + state.labelFilters.push(f); + } + }, + setOrderByFilters(state, { orderBy }) { + console.log("setOrderByFilters"); + state.orderByFilters = []; + for (let f of orderBy) { + state.orderByFilters.push(f); + } + }, + setSource(state, { sourceType, remoteSource }) { + state.sourceType = sourceType; + state.remoteSource = remoteSource; + }, + deleteStudy(state, {studyId}) { + const pos = state.studiesIds.indexOf(studyId); + if (pos >= 0) { + state.studiesIds.splice(pos, 1); + } + state.studies = state.studies.filter(s => s["ID"] != studyId); + + // also delete from selection + const pos2 = state.selectedStudiesIds.indexOf(studyId); + if (pos2 >= 0) { + state.selectedStudiesIds.splice(pos, 1); + } + }, + refreshStudyLabels(state, {studyId, labels}) { + for (const i in state.studies) { + if (state.studies[i].ID == studyId) { + state.studies[i].Labels = labels + } + } + }, + setStatistics(state, {statistics}) { + state.statistics = statistics; + }, + setIsSearching(state, {isSearching}) { + state.isSearching = isSearching; + }, + selectStudy(state, {studyId, isSelected}) { + if (isSelected && !state.selectedStudiesIds.includes(studyId)) { + state.selectedStudiesIds.push(studyId); + state.selectedStudies = state.studies.filter(s => state.selectedStudiesIds.includes(s["ID"])); + } else if (!isSelected) { + const pos = state.selectedStudiesIds.indexOf(studyId); + if (pos >= 0) { + state.selectedStudiesIds.splice(pos, 1); + state.selectedStudies = state.selectedStudies.filter(s => s["ID"] != studyId); + } + } + }, + selectAllStudies(state, {isSelected}) { + if (isSelected) { + state.selectedStudiesIds = [...state.studiesIds]; + state.selectedStudies = [...state.studies]; + } else { + state.selectedStudiesIds = []; + state.selectedStudies = []; + } + + } +} + +///////////////////////////// ACTIONS + +const actions = { + async initialLoad({ commit, state}) { + this.dispatch('studies/loadStatistics'); + }, + async updateFilter({ commit }, payload) { + const dicomTagName = payload['dicomTagName']; + const value = payload['value']; + commit('setFilter', { dicomTagName, value }) + + this.dispatch('studies/reloadFilteredStudies'); + }, + async updateFilterNoReload({ commit }, payload) { + const dicomTagName = payload['dicomTagName']; + const value = payload['value']; + commit('setFilter', { dicomTagName, value }) + }, + async updateLabelFilterNoReload({ commit }, payload) { + const labels = payload['labels']; + commit('setLabelFilters', { labels }) + }, + async updateOrderByNoReload({ commit }, payload) { + const orderBy = payload['orderBy']; + commit('setOrderByFilters', { orderBy }) + }, + async updateOrderBy({ commit }, payload) { + const orderBy = payload['orderBy']; + commit('setOrderByFilters', { orderBy }) + + this.dispatch('studies/reloadFilteredStudies'); + }, + + async updateSource({ commit }, payload) { + const sourceType = payload['source-type']; + const remoteSource = payload['remote-source']; + commit('setSource', { sourceType, remoteSource }); + commit('selectAllStudies', { isSelected: false}); // clear selection when changing source + }, + async clearFilter({ commit, state }) { + commit('clearFilter'); + + this.dispatch('studies/reloadFilteredStudies'); + }, + async clearFilterNoReload({ commit }) { + commit('clearFilter'); + }, + async clearStudies({ commit }) { + commit('setStudiesIds', { studiesIds: [] }); + commit('setStudies', { studies: [] }); + }, + async extendFilteredStudies({ commit, getters, state }) { + get_studies_shared({ commit, getters, state }, true); + }, + async reloadFilteredStudies({ commit, getters, state }) { + get_studies_shared({ commit, getters, state }, false); + }, + async cancelSearch() { + await api.cancelFindStudies(); + }, + async loadStatistics({ commit }) { + const statistics = (await api.getStatistics()); + commit('setStatistics', { statistics: statistics }); + }, + async deleteStudy({ commit }, payload) { + const studyId = payload['studyId']; + commit('deleteStudy', { studyId }); + this.dispatch('studies/loadStatistics'); + }, + async addStudy({ commit }, payload) { + const studyId = payload['studyId']; + const study = payload['study']; + const reloadStats = payload['reloadStats']; + commit('addStudy', { studyId: studyId, study: study }); + if (reloadStats) { + this.dispatch('studies/loadStatistics'); + } + }, + async selectStudy({ commit }, payload) { + const studyId = payload['studyId']; + const isSelected = payload['isSelected']; + commit('selectStudy', { studyId: studyId, isSelected: isSelected}); + }, + async selectAllStudies({ commit }, payload) { + const isSelected = payload['isSelected']; + commit('selectAllStudies', { isSelected: isSelected}); + }, + async reloadStudy({ commit }, payload) { + const studyId = payload['studyId']; + const study = payload['study']; + commit('addStudy', { studyId: studyId, study: study }); + this.dispatch('studies/loadStatistics'); + }, + async refreshStudiesLabels({ commit }, payload) { + const studiesIds = payload['studiesIds']; + + for (const studyId of studiesIds) { + const labels = await api.getLabels(studyId); + + commit('refreshStudyLabels', { studyId: studyId, labels: labels}); + } + }, +} + + + +///////////////////////////// EXPORT + +export default { + namespaced: true, + state, + getters, + mutations, + actions, +} \ No newline at end of file diff --git a/WebApplication/token-landing.html b/WebApplication/token-landing.html new file mode 100644 index 0000000..5cad213 --- /dev/null +++ b/WebApplication/token-landing.html @@ -0,0 +1,13 @@ + + + + + + + OE2 Token landing + + +
+ + + diff --git a/WebApplication/vite.config.js b/WebApplication/vite.config.js new file mode 100644 index 0000000..fa72112 --- /dev/null +++ b/WebApplication/vite.config.js @@ -0,0 +1,40 @@ +import { defineConfig } from 'vite' +import { resolve } from 'path' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + assetsInclude: './src/assets', + base: '', + plugins: [vue()], + server: { + host: true, + port: 3000 + }, + build: { + chunkSizeWarningLimit: 1000, + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + landing: resolve(__dirname, 'token-landing.html'), + retrieve: resolve(__dirname, 'retrieve-and-view.html') + }, + } + }, + css: { + postcss: { // to avoid this warning: https://github.com/vitejs/vite/discussions/5079 + plugins: [ + { + postcssPlugin: 'internal:charset-removal', + AtRule: { + charset: (atRule) => { + if (atRule.name === 'charset') { + atRule.remove(); + } + } + } + } + ] + } + } +}) diff --git a/release-notes.md b/release-notes.md new file mode 100644 index 0000000..4039a77 --- /dev/null +++ b/release-notes.md @@ -0,0 +1,556 @@ +Pending changes +=============== + +Changes: + - Experimental new configuration "Tokens.RequiredForLinks" can be set to false + when using HTTP Basic authentication together with the authorization plugin + (https://discourse.orthanc-server.org/t/user-based-access-control-with-label-based-resource-access/5454). + + +1.7.1 (2025-01-22) +================== + +Fixes: + - Added status text in job progress bar. + - When retrieving a study from a remote DICOM modality, use the default retrieve method: + C-GET or C-MOVE. + - Fixed empty PatientName column when querying a remote DICOM modality. + - Fixed a few small issues when navigating between local and remote studies. + + +1.7.0 (2024-12-19) +================== + +Changes: + - When Orthanc DB supports "ExtendedFind" (SQLite in 1.12.5+ and PosgreSQL 7.0+): + - new features in the local studies list: + - Allow sorting by columns + - Optimized loading of "most-recent" studies + - Load the following studies when scrolling to the bottom of the current list. + - New configuration "EnableLabelsCount" to enable/disable the display of the number of studies with each label. + - The "MaxStudiesDisplayed" configuration is not taken into account anymore for + the local study list since we have implemented "infinite-scroll". However, the option + is still used when performing remote DicomWEB queries. + - New configuration "PageLoadSize" that defines the number of items that are loaded when scrolling the study or instance list. + - The "StudyListContentIfNoSearch" configuration is not taken into account and always considered as "most-recents". + - New "order-by" argument in the url to open the UI directly on a search result, e.g: + http://localhost:8042/ui/app/#/filtered-studies?StudyDate=20231213-20241213&order-by=DicomTag,StudyDate,ASC;DicomTag,PatientName,ASC;Metadata,LastUpdate,DESC + - Disable some UI components on ReadOnly systems. + - The study list header is now sticking on top of the screen. + - Quick report icon: now display the SeriesDate - SeriesDescription in a tooltip. + +Fixes: + - When modifying studies, dates selected from the DatePicker were not always taken into account. + - Fixed the criteria to display the OHIF Segmentation viewer. + - Fixed display of invalid dates like 00000000. + - Fixed compatibility with OHIF 1.4 if OHIF.DataSource is not defined. + + +1.6.4 (2024-10-10) +================== + +Changes: + - new configuration "EnableViewerQuickButton" to enable/disable a button + to open a viewer directly from the study list (default value: true). + - new configuration "EnableReportQuickButton" to enable/disable a button + to open a PDF report directly from the study list if a PDF report isavailable + in the study. (default value: false). + Note that, with Orthanc version up to 1.12.4, this option may slow down the + display of the study list but this will be solved in the next Orthanc version. + +Fixes: + - When modifying studies, dates selected from the DatePicker were not always taken into account. + - Primary viewer icon was not visible when using an external OHIF viewer. + + +1.6.2 (2024-09-23) +================== + +Changes: + - Added a clickable icon to open the primary viewer without expanding the study. + - Added a clickable icon to open a PDF report without expanding the study. + - new configuration "CustomFavIconPath" to customize the FavIcon + - new configuration "CustomTitle" to customize the tab/window title + - new configurations to modify the PatientName display: + - "PatientNameCapture", a Javascript regular expression to capture the words of PatientName + - "PatientNameFormatting", a replacement expression using the captured words + +Fixes: + - Fixed (again) the "UiOptions.EnableApiViewMenu" configuration that was not taken into account. The API View button was never visible. + + +1.6.1 (2024-08-29) +================== + +Changes: + - new configuration values for "UiOptions.StudyListColumns": + - "instancesCount" to show the number of instances in a study + - "seriesAndInstancesCount" to show the number of series/instances in a study. This now replaces "seriesCount" in the default configuration. + +Fixes: + - Fixed the "UiOptions.EnableApiViewMenu" configuration that was not taken into account. The API View button was always visible. + - [Issue #65](https://github.com/orthanc-server/orthanc-explorer-2/issues/65) SeriesCount column is empty. + - [Issue #63](https://github.com/orthanc-server/orthanc-explorer-2/issues/63) This/last week selection in Date picker now starts on monday. + - [Bug #232](https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=232) Clarified compilation instructions and use LOCAL dist folder by default. + + +1.6.0 (2024-08-02) +================== + +Changes: + - POSSIBLE BREAKING CHANGE: the Osimis viewer button is not listed anymore by default. In order + to re-enable it, you must provide it to the "UiOptions.ViewersOrdering" configuration. + - Orthanc Explorer 2 is now the default Orthanc UI when the plugin is installed. + - Refactored the Remote Study List when browsing remote DICOM Modalities. It is now identical to the main + local study list with a reduced list of actions (only the retrieve action is available). + - the /ui/app/#/filtered-remote-studies has been replaced by /ui/app/#/filtered-studies?source-type=dicom&remote-source=... + - Now providing the ability to browse remote DICOMWeb servers. + - Authorization tokens can be provided in the URL as query args e.g: http://localhost:8042/ui/app/?token=my-token + or /ui/app/filtered-studies?StudyDescription=PET&token=my-token and these tokens will be included as HTTP headers + in all requests issued by OE2. Note that the query args must be positioned before the '#' in the URL. + List of valid tokens are "token", "auth-token", "authorization". + - Now sorting the results of a search by StudyDate. + + +Fixes: + - Labels list was not displayed in the "Permissions" edition UI. + - Opening /ui/app now redirects to /ui/app/ instead of failing opening the UI. + + +1.5.1 (2024-07-03) +================== + +Fixes: + - Broken interface when the auth-service was not configured to implement the role/permission API + + +1.5.0 (2024-06-27) +================== + +Changes: + - Implemented a new permission UI in case you are using OE2 together with Keycloak and an authorisation service that implements a role/permission API. + - New configuration "AvailableLabels" to forbid creation of labels that are not defined in this list. If the list is empty, anyone can create any new label. + +Fixes: + - [Issue #57](https://github.com/orthanc-server/orthanc-explorer-2/issues/57) Avoid double calls to tools/find when selecting a label or clicking on `search` button + - In the settings page, the status of the python plugin was not correct. + + +1.4.1 (2024-06-05) +================== + +Changes: +- Settings: if the [Delayed Deletion plugin](https://orthanc.uclouvain.be/book/plugins/delayed-deletion-plugin.html) + is enabled, display the number of files pending deletion. + +Fixes: + - Prevent using invalid characters when adding labels to a study. + - [Issue #58](https://github.com/orthanc-server/orthanc-explorer-2/issues/58) Upload function only uploads the first file. + + +1.4.0 (2024-05-16) +================== + +Changes: +- Added a "Add series" button to the study to create new series from PDF, images or STL files + - New configurations: + - "EnableAddSeries" + - "AddSeriesDefaultTags" + +Fixes: + - [Issue #51](https://github.com/orthanc-server/orthanc-explorer-2/issues/51) Stop calling /changes once all studies are displayed. + - [Issue #52](https://github.com/orthanc-server/orthanc-explorer-2/issues/52) Removed duplicate calls to /studies/../series and /series/../instances. + - [Issue #53](https://github.com/orthanc-server/orthanc-explorer-2/issues/53) Changing selection in Modalities in Study works immediately even if `"StudyListSearchMode": "search-button"`. + - [Issue #54](https://github.com/orthanc-server/orthanc-explorer-2/issues/54) OHIF disappeared after #52 fix + - [Issue #55](https://github.com/orthanc-server/orthanc-explorer-2/issues/55) DateFormat not respected + introduced DatePicker in the Modification Modal. + - Removed duplicate calls to /tools/find in Modification modal + + +1.3.0 (2024-03-25) +================== + +Changes: +- Theming the interface: + - New configurations: + - "Theme" to select the default "light" or "dark" theme. + - "CustomCssPath" to complement the default CSS by a custom one. + - "CustomLogoPath" to provide your own custom logo from disk. + - "CustomLogoUrl" to provide your own custom logo from an external url. + - "DateFormat" to customize the date format in study list and in the date pickers. +- OHIF Integration: + - Added support for `Segmentation` and `Microscopy` modes. The `Microscopy` + mode is disabled by default since it is not stable yet in OHIF. + - You can now enable/disable OHIF viewer modes by including/removing them + from the `ViewersOrdering` configuration. + - OHIF buttons are now visible/hidden depending on the content of the study. +- Configurations: + - Updated default values for `ViewersIcons` and `ViewersOrdering`. +- Added date pickers in the Remote Study List (when performing searches on + remote modalities) +- When editing a study, it is now possible to add an `OtherPatientIDs` DICOM tag. + +Internals: + - Updated all JS libraries. + + +1.2.2 (2024-02-16) +================== + +Changes: +- Now keeping labels when modifying a study or a series. + This fix requires Orthanc 1.12.3. +- Added Slovenian translations +- Now showing the DICOM header content at instance level (TransferSyntax, ...) +- Instance preview now handles PDF correctly. +- Now displaying #Patients and MaximumPatientCount in Settings. + + +Fixes: +- fixed delete on multiple selection in Firefox + + +1.2.1 (2024-01-03) +================== + +Fixes: +- Handle quotes correctly when "PatientBirthDate" is defined in "ShowSamePatientStudiesFilter" +- fix #42: Studies disappear from list when unselecting them. +- fix #34: Enable copy button when context is not secure. + + +1.2.0 (2023-12-19) +================== + +Changes: +- Added a button to download multiple studies at once (only available if running an + Orthanc version > 1.12.1 - not officially released at the time this plugin was + released) +- Bew configuration "UiOptions.ShowSamePatientStudiesFilter" to list the tags that + are used to identify studies belonging to the same patient. +- Added Russian translations +- Added a button to share multiple studies at once. +- Now displaying the MaximumStorageSize in the settings page. +- New configurations "UiOptions.Modifications.SeriesAllowedModes" & "UiOptions.Modifications.SeriesDefaultMode" + to configure how series are modified. If only one option is available, it is not displayed and the + default option is selected. +- In "Modify Patient Tags", allow applying changes even if nothing has changed e.g. to apply all current + Patient tags to all studies with the same Patient ID. + +Fixes: +- fix #43: Missing 'ModalitiesInStudy' in response in remote system QR breaks search result preview. + + +1.1.3 (2023-10-04) +================== + +Fixes: +- fix target of 'open Orthanc Explorer 2' button in legacy OE +- repair tables background color (bug in 1.1.2) + +1.1.2 (2023-10-03) +================== + +Fixes: +- new labels now appears/disappears on the side bar when they are created/deleted from a study +- study labels are listed correctly when reoopening a study + + +1.1.1 (2023-09-18) +================== + +Changes: +- added German translations +- show username in side bar + +Fixes: +- internal: fixed a typo in tools/find request +- fix UI loading when user is not authorized to access to DicomWEB servers +- #30: preserve double spaces in tag values. +- fix labels filtering + +1.1.0 (2023-08-16) +================== + +Fixes: +- fixed an XSS vulnerability (many thanks to Abebe Hailu from kth.se) + +1.0.3 (2023-07-14) +================== + +Changes: +- added support for opening the WSI viewer on series + +1.0.2 (2023-06-27) +================= + +Fixes: +- fix ModalitiesInStudy filtering +- fix links to uploaded studies + + +1.0.1 (2023-06-21) +================= + +Changes: +- added support for OHIF plugin: + - new default value for the 'OhifViewer3PublicRoot' configuration: '/ohif/' + - now displaying 3 buttons to open OHIF in basic mode, Volume Rendering and TMTV modes + - 2 more viewers can be listed in 'ViewersIcons' and in 'ViewersOrdering': "ohif-vr" & "ohif-tmtv" +- added support for labels + - new 'UiOptions.EnableEditLabels' configuration +- added support for VolView 1.1 plugin +- 'StudyListColumns' can now contain any DICOM Tag that is stored as a MainDicomTag or ExtraMainDicomTag +- added Ukrainian and Chinese translations thanks to Stephen D. Scotti, 'Franklin' and Juriy Cherednichock + +Fixes: +- use auth token for /preview if required + + +0.9.3 (2023-05-15) +================== + +Changes: +- allow opening MedDream viewer on multiple studies +- new italian translations thanks to Stefano Feroldi +- new georgian translations thanks to Yomarbuzz + +Fixes: +- improved integration with OHIF v3 and keycloak + + +0.9.2 (2023-04-17) +================== + +Changes: +- new configuration 'StudyListContentIfNoSearch' to replace 'StudyListEmptyIfNoSearch'. Allowed values are + "empty", "most-recents" (default) +- configuration 'StudyListEmptyIfNoSearch' is now deprecated. You should now use "StudyListContentIfNoSearch": "empty". +- now reporting the status of Multitenant DICOM plugin in the settings page +- added button "Open Orthanc Explorer 2" in Orthanc Explorer +- new configuration 'EnableOpenInOhifViewer3' and 'OhifViewer3PublicRoot' + +Fixes: +- In the settings page, the status of the python plugin was not correct. + + +0.8.2 (2023-03-27) +================== + +Fixes: +- If OE2 configuration did not include "UiOptions" section, the UI failed to run correctly. + +Changes: +- Added an icon to open a study or a series in VolView +- In the settings page, do not show buttons to open plugin UI if user-permissions are enabled, these UI + wouldn't work anyway since they do not support tokens. + +0.8.1 (2023-03-24) +================== + +Fixes: +- Since the introduction of multiple selections, is was not possible to open the StoneViewer on a single study +- Auth tokens were not refreshed correctly. + +0.8.0 (2023-03-22) +================== + +Changes: +- Allow actions on multiple studies: + - open StoneViewer + - send to + - delete +- Refactored the Study List headers to show bulk action buttons + +Fixes: +- #9 EnableAnonimization spelled wrong in configuration file: you should now use UiOptions.EnableAnonymization instead. Both spellings + are currently accepted but only the new spelling might be accepted in future versions. + +0.7.0 (2023-03-17) +================== + +Changes: +- Introduced a Date Picker for the StudyDate and PatientBirthDate +- Introduced user-permissions and authorization tokens linked to the authorization plugin and new auth-service API +- Configuration: new "Tokens" section +- new "landing" page at /ui/app/token-landing.html?token=... to validate tokens and display a user friendly message if the token + is invalid. Redirects to the viewer if the token is valid. +- new "landing" page at /ui/app/retrieve-and-view.html?StudyInstanceUID=1.2.3....&modality=pacs&viewer=stone-viewer to open the viewer + on a study that might already be stored in Orthanc or, if not, that can be fetched from a DICOM modality. + +BREAKING CHANGES: +- Shares: removed anonymized shares +- Shares: is now using the authorization plugin to generate tokens. This requires the new auth-service API +- Shares: removed "Shares" configuration section, part of it has been moved to the "Tokens" section + + +0.6.0 (2023-02-03) +================== + +Changes: +- allow modification of series and studies +- allow anonymization of series and studies +- new configuration 'Modifications' +- new configuration 'TranslateDicomTags' +- new configurations 'StudyMainDicomTags', 'PatientMainDicomTags' to define the list of tags to display in the study details +- new link to open all studies from the same patient + +Note: the 'Modify' and 'Anonymization' dialog are only available in English in this version. + +0.5.1 (2022-12-20) +================== + +Changes: +- report correct status for object storage plugins +- improved french translations + +Fixes: +- fix default values for AvailableLanguages to all languages + +0.5.0 (2022-12-16) +================== + +Changes: +- added multi-language support +- new configurations: 'AvailableLanguages', 'DefaultLanguage' +- shares: enable "stone-viewer-publication" type + +Fixes: +- WO-63: instant-link only works if EnableShares is true + +0.4.3 (2022-11-03) +================== + +Changes: +- added 2 configurations to chose the viewer icons and the order in which they appear: + 'ViewersOrdering' and 'ViewersIcons'. The default configuration is identical to the 0.4.1 behaviour. +- adapted orthanc-share API + +0.4.2 (2022-10-28) +================== + +Changes: +- implement study sharing UI for orthanc-share project (still confidential, will be presented at OrthancCon 2022) +- open MedDream with a one time token if connected with orthanc-share project +- reorganized icons for viewers to always use 'eye' and 'eye-fill' for the first 2 enabled viewers. + +Fixes: +- SendTo and ApiView dropdown menu mixed up when both enabled + + +0.4.1 (2022-09-07) +================== + +Changes: +- new `EnableSendTo` option to enable/disable the `SendTo` button + +Fixes: +- SendTo and ApiView dropdown menu were mixed up + + + +0.4.0 (2022-08-30) +================== + +Changes: +- new simplified interface to query DICOM modalities and retrieve study. + It does not allow yet browsing distant series/instances. Still a work in progress ! +- show C-Echo status of DICOM modalities. +- allow controling wildcards in text search to implement exact or partial match. + The default remains a partial match -> if you enter a `filterValue` text in a filter, + Orthanc will search for `*filterValue*`. By adding a `"` at the beginning or at the end of the text, you force an exact match at the beggining or at the end of the text. + + +Fixes: +- improved redirection when running behind a reverse-proxy +- support of dynamic linking against the system-wide Orthanc framework library +- support of LSB (Linux Standard Base) compilation + +Maintenance: +- upgraded all npm dependencies + +0.3.3 (2022-06-09) +================== + +Changes: +- added a button 'open in MedDream Viewer' +- added buttons to change log levels in settings page +- added 'transfer to peer' using the Transfers plugin + +- new configurations: + - "EnableOpenInMedDreamViewer" + - "MedDreamViewerPublicRoot" + +Fixes: +- display ReferringPhyisician, RequestingPhysician and InstitutionName in study list +- fix GDCM plugin status in settings page + +0.3.2 (2022-06-01) +================== + +Changes: +- showing 'searching' status and allow cancelling search + +- new configuration: + - "StudyListEmptyIfNoSearch" + + +0.3.1 (2022-05-31) +================== + +Changes: +- added a button 'open in OHIF Viewer' +- introduced two search modes "search-as-you-type" (suitable for small DBs) or "search-button" (suitable for large DB) + +- new configurations: + - "EnableOpenInOhifViewer" + - "OhifViewerPublicRoot" + - "StudyListSearchMode" + - "StudyListSearchAsYouTypeMinChars" + - "StudyListSearchAsYouTypeDelay" + + +0.3.0 (2022-05-23) +================== + +Changes: +- fixed 'send to peer' +- added 'send to modality' +- now displaying the transfer jobs created by 'send to peer', 'send to modality', 'send to dicom-web' + +- new configuration: + - "MaxMyJobsHistorySize" + +0.2.2 (2022-05-19) +================== + +Changes: +- show Orthanc "Name" in side bar +- changed color of Orthanc logo to white +- improved relative URL computation when running behind a reverse proxy +- added an 'expand' query argument with values to expand studies, series ... to use with only a few resources ! + ``` + http://localhost:8042/ui/app/#/filtered-studies?StudyInstanceUID=1.2.3&expand + http://localhost:8042/ui/app/#/filtered-studies?StudyInstanceUID=1.2.3&expand=study + http://localhost:8042/ui/app/#/filtered-studies?StudyInstanceUID=1.2.3&expand=series + http://localhost:8042/ui/app/#/filtered-studies?StudyInstanceUID=1.2.3&expand=instance + ``` +- new configurations: + - "ShowOrthancName" + - "EnableDownloadZip" + - "EnableDownloadDicomDir" + - "EnableDownloadDicomFile" + +0.2.1 (2022-05-13) +================== + +Changes: +- new default root is now '/ui/' + +Fixes: +- allow display of patient tags in upload report +- upload: avoid calling /statistics for each instance (now only once per study uploaded) + +0.2.0 (2022-05-12) +================== + +initial release diff --git a/scripts/check-translations.py b/scripts/check-translations.py new file mode 100644 index 0000000..d1e1553 --- /dev/null +++ b/scripts/check-translations.py @@ -0,0 +1,82 @@ +#!/usr/bin/python3 + +import pathlib +import json +import glob +import pprint + +here = pathlib.Path(__file__).parent.resolve() + +reference_file_path = str(here / '../WebApplication/src/locales/en.json') +language_files_path = glob.glob(str(here / '../WebApplication/src/locales/*.json')) +source_files_path = glob.glob(str(here / '../WebApplication/src/**/*.vue'), recursive=True) + +with open(reference_file_path, 'rb') as f: + reference_content = json.load(f) + +# check if all entries in the default language file are being used +all_source_files_content = "" +for source_file in source_files_path: + with open(source_file, "rt") as f: + all_source_files_content += f.read() + +# collect all keys +all_keys = [] +def add_keys(node, prefix, all_keys): + for key in node: + if isinstance(node[key], dict): + add_keys(node[key], prefix=f"{key}.", all_keys=all_keys) + else: + all_keys.append(f"{prefix}{key}") + +add_keys(reference_content, "", all_keys=all_keys) + +for key in all_keys: + translations_text = [ + f'"{key}"', + f"'{key}'" + ] + found = False + for translation_text in translations_text: + if translation_text in all_source_files_content: + found = True + break + + if not found: + print(f"translation '{key}' does not seem to be used") + + +def compare_json_node(reference, value, prefix, missings = [], excessives = []): + for key in reference: + if not key in value: + missings.append(f"{prefix}{key}") + + elif isinstance(reference[key], dict): + compare_json_node(reference[key], value[key], prefix=f"{key}.", missings=missings, excessives=excessives) + for key in value: + if not key in reference: + excessives.append(f"{prefix}{key}") + + +for path in language_files_path: + if path != reference_file_path: + with open(path, 'rb') as f: + language_content = json.load(f) + + missings = [] + excessives = [] + + compare_json_node(reference_content, language_content, prefix="", missings=missings, excessives=excessives) + + if len(missings) > 0: + print(f">>> {path}, missing nodes in translation:") + for missing in missings: + print(f" {missing}") + + if len(excessives) > 0: + print(f">>> {path}, unused nodes in translation:") + for excessive in excessives: + print(f" {excessive}") + + if len(missings) == 0 and len(excessives) == 0: + print(f">>> {path}, translation is complete !") diff --git a/scripts/logs/.gitignore b/scripts/logs/.gitignore new file mode 100644 index 0000000..bf0824e --- /dev/null +++ b/scripts/logs/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/scripts/nginx-dev.conf b/scripts/nginx-dev.conf new file mode 100644 index 0000000..5023db7 --- /dev/null +++ b/scripts/nginx-dev.conf @@ -0,0 +1,80 @@ +# Local config to run orthanc and development UI on the same domain +# Uses port 9999 + +# `events` section is mandatory +events { + worker_connections 1024; # Default: 1024 +} + +http { + + # prevent nginx sync issues on OSX + proxy_buffering off; + include /etc/nginx/mime.types; + + server { # on port 9999: real time "run dev" server + listen 9999 default_server; + client_max_body_size 4G; + access_log logs/nginx-access.log; + error_log logs/nginx-error.log; + + # set $orthanc http://192.168.0.8:8042; + set $orthanc 'http://127.0.0.1:8043'; + + # Orthanc when accessed from the 'run dev' server + location ~* ^/(patients|studies|instances|series|plugins|system|tools|statistics|modalities|dicom-web|osimis-viewer|ohif|stone-webviewer|peers|jobs|transfers|queries|auth|app|volview|changes|wsi) { + + proxy_set_header HOST $host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass $orthanc; + } + + location /ui/api/ { + proxy_set_header HOST $host; + proxy_set_header X-Real-IP $remote_addr; + + #rewrite (.*) /ui$1 break; + proxy_pass $orthanc; + } + + location /ui/app/customizable/ { + proxy_set_header HOST $host; + proxy_set_header X-Real-IP $remote_addr; + + #rewrite (.*) /ui$1 break; + proxy_pass $orthanc; + } + + # Frontend development server + location /ui/app/ { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header HOST $host; + rewrite /ui/app(.*) $1 break; + + proxy_pass http://127.0.0.1:3000; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header HOST $host; + + proxy_pass http://127.0.0.1:3000; + } + + location /orthanc/ { + # URL to open in your browser: http://localhost:9999/orthanc/ui/ + + proxy_pass $orthanc; + rewrite /orthanc(.*) $1 break; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_request_buffering off; + proxy_max_temp_file_size 0; + client_max_body_size 0; + } + + } +} diff --git a/scripts/start-nginx.sh b/scripts/start-nginx.sh new file mode 100644 index 0000000..5e00d2c --- /dev/null +++ b/scripts/start-nginx.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -o errexit +set -o xtrace + + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +nginx -p $SCRIPT_DIR -c $SCRIPT_DIR/nginx-dev.conf + + diff --git a/scripts/stop-nginx.sh b/scripts/stop-nginx.sh new file mode 100644 index 0000000..835fd21 --- /dev/null +++ b/scripts/stop-nginx.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -o errexit +set -o xtrace + + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +nginx -p $SCRIPT_DIR -c $SCRIPT_DIR/nginx-dev.conf -s stop + + diff --git a/tests/manual-tests.md b/tests/manual-tests.md new file mode 100644 index 0000000..fcc03ea --- /dev/null +++ b/tests/manual-tests.md @@ -0,0 +1,163 @@ +I know, this is old style manual tests ! Anyone willing to automate this is pretty welcome ! + + +Modifications +============= + +Prerequisites: + +- make sure there are no patients whose `PatientID` starts with `TEST` +- upload the test study from `tests/stimuli/TEST_1` + +Study modification +------------------ + + +Edit study tags to create a modified copy: +- on Study `Test CT`, `Modify Study tags` and change: + - `PatientBirthDate = 19500630` + - `PatientID = TEST_2` + - `PatientName = Test2` + - `AccessionNumber = 2345` + - `StudyDate = 20150101` + - `StudyID = 2` + - Insert `InstitutionName = MY` + - Remove `StudyDescription` + - select "Create a modified copy of the original study" + - Apply + - Check: You should now have 2 studies for `PatientID = TEST*` + - Check the new study has changed its tags + +Edit study tags 'in place': +- on the new Study (the one without `StudyDescription`), first copy-paste the `StudyInstanceUID` somewhere + - click `Modify Study tags`, and change: + - `StudyDate = 20200101` + - select "Modify the original study. (keeping the original DICOM UIDs)" + - Apply + - Check: + - `StudyDate == 20200101` + - `StudyInstanceUID` has not changed + + +Edit study tags, can not be used to edit a patient who already has other studies +- on the new Study (the one whose `PatientID==2`), `Modify Study tags` and change: + - `PatientID = TEST_1` + - don't change other Patient tags + - try all 3 modification mode options + - Apply + - Check: you should get an error telling you to use `Change Patient` + +Edit study tags, can be used to create a new patient +- on the new Study (the one whose `PatientID==2`), `Modify Study tags` and change: + - `PatientID = TEST_3` + - `PatientName = Test3` + - don't change other Patient tags + - select "Create a modified copy of the original study" + - Apply + - Check: you should now have 3 studies and 3 patients + +Attach study to a non existing patient +- on any Study, `Change patient` and set: + - `PatientID = NO_SUCH_PATIENT` + - Apply + - Check: you should get en error telling you to use `Modify Study tags` instead + +Attach study to an existing patient, keep DICOM UIDs +- on Study from `PatientID = TEST_3`, `Change patient` and set: + - `PatientID = TEST_1` + - select "Modify the original study. (keeping the original DICOM UIDs)" + - Apply + - Check: + - Patient `TEST_1` shall now have 2 studies + - the Patient tags shall be identical in both studies + +Edit patient birth date in multiple studies +- on a study from `PatientID = TEST_1`, 'Modify Patient tags' and set: + - `PatientBirthDate = 19100101` + - select "Modify the original study. (keeping the original DICOM UIDs)" + - Apply + - Check: + - `PatientBirthDate` has changed in all studies, + +Edit patient ID in multiple studies +- on a study from `PatientID = TEST_1`, 'Modify Patient tags' and set: + - `PatientID = TEST_4` + - select "Modify the original study. (keeping the original DICOM UIDs)" + - Apply + - Check: + - `PatientID` has changed in all studies, + +Edit patient ID in multiple studies +- on a study from `PatientID = TEST_4`, 'Modify Patient tags' and set: + - `PatientID = TEST_1` + - select "Create a modified copy of the original study" + - Apply + - Check: + - there are duplicate studies for Patient `TEST_1` and `TEST_4` + +Anonymize study +- on any study, 'Anonymize study': + - make sure `PatientID` and `PatientName` have been pre-filled + - set a `StudyDescription` + - Apply + - Check: + - the anonymized study has been created, the original study is still present + + +Series modification +------------------ + +Preparation: +- delete all studies from `TEST*` patient +- upload the test study again +- on Study `Test CT`, `Modify Study tags` and change: + - `AccessionNumber = 2345` + - `StudyDate = 20150101` + - `StudyID = 2` + - `StudyDescription = Test CT 2` + - select "Create a modified copy of the original study" + - Apply +- you should now have 2 studies for the `TEST_1` patient + +Edit series tags to move it to an existing study: +- on the series from `Test CT 2`, `Change study` : + - `StudyInstanceUID = 1.2.4` + - click `Modify` -> you should get an error + - `StudyInstanceUID = 1.2.3` + - click `Modify` + - Check: + - you now have 2 series in the `Test CT` study + - `StudyDate` tags of instances from both series should be set to `20100630` + +Move series to a new study: +- on one of the series, `Create new study` and change: + - `PatientID = TEST_1` + - `StudyDescription = TEST CT 3` + - click `Modify` -> you should have a warning telling you that this patient exists with different tags -> `Modify` + - Check: + - you should now have 2 studies for patient `TEST*` + +Move series to a new study (2): +- on the series from `TEST CT 3`, `Create new study` and change: + - `PatientID = TEST_2` + - `PatientName = Test2` + - `PatientBirthDate = 20000101` + - `StudyDescription = TEST CT 4` + - click `Modify` + - Check: + - you should now have 2 studies for patient `TEST*` + - the `Test CT 3 ` series shall have been deleted + +Edit series tags: +- on the series from `TEST CT 4`, `Modify series tags` and change: + - `Modality = CR` + - click `Modify` + - Check: + - the modality shall have been updated in the series (and in the study list) + +Anonymize series: +- on the series from `TEST CT 4`, `Anonymize series` and change: + - make sure `PatientID` and `PatientName` have been pre-filled + - click `Anonymize` + - Check: + - the anonymized series (and so study) has been created, the original series is still present diff --git a/tests/stimuli/TEST_1/10.dcm b/tests/stimuli/TEST_1/10.dcm new file mode 100644 index 0000000000000000000000000000000000000000..368747f63697b58aec981c62edf90c9ac0c4fbef GIT binary patch literal 297374 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

sqB> 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π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%0D$~^_BjI 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%0AI6gU^%Rv#-Vv3nGT&+VJ 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 literal 0 HcmV?d00001 diff --git a/tests/stimuli/TEST_1/12.dcm b/tests/stimuli/TEST_1/12.dcm new file mode 100644 index 0000000000000000000000000000000000000000..9b52d655e6363be177464589efcfbce4c15777bc GIT binary patch literal 295402 zcmeFa1yGzpw5T~~a19a&uE8=4Fu1!DG`K_X;K75tySux)yIb(!?g=g-*-7qwuU>7v zdRx1pasKo+)t zR$Oc#HZ~A93y6c8jf;g9%*n>h0qNy$1qV9^7}6dK(jNc+G{WEU*jYG%umBv$-M9bK z-v5pOX5obNfD2HO!U8BtNJy*bi;2ps%IdR$ImwX#2mnzP6o9mfqQ0~;Ib={I01~8z z5+Ek7EUhjsrlX`REibIBsU!SRT3Aj;R8?O^QJ#ZbUP&3!7XSakZzKTWf5~qo069QR z7zqG^paO!~AakRF=m5wJfdEx`L;x!Yf*MlG2GRc3DnWdNht#sMBLNH`UcdrGRgnOn z0dk5+02OI*d2t05$gE=kN(zVoF=cWMDM&pK>+fAvQ%5IqQB}xY0f%Wir8qy;q%0Q4{VPyq!LgJE}g%x7||9muVNHk(W7{dRf zh6Pz8IYmT(sr0eSb-2-=-)cXljA_SEQv!%V)`|ln89*e+3Ic%;Ip&|t@lPg)@Wuc*y4jlB{aq$?VRkNILI42!uQeOn zUu!Ig42cLJ7Y7TN5P<#f4hQ$&KFJ|6Ea0yV@IP!gAiEQy31R<>5Dm!q*wT=G{$r#M z|72E95X3?mBEteWSt0Yn{u@~ma>A;FfY1MGL6F**f1^P5A;cDd^PiE*#R_5t5&~dw zROK-M5D07_E|$Mx`p*w692F@&-3H_|h>2$X)S&`UiLU4AgW)h%OQTH@d_i&HqL;AEZQr zWWK+?u(Gm&xml3_un-L%0Ld)mNC0>kIYlCXw74j_h@FWWxtN`!g^j6`gB!WhCqr8& zI~xeucnLW)z||bHV8aMlaP3h*kQ{&x$rX_PAc4vTi7AZ#Mdn~*`Cn&DAf!DOBp1K} zSjou&*pTc73jlGFL-ORm^@P;n{*zdNSUFhO zAhn#_KrVJR9ab(*5DPbmlaq~|jgte?11F>%0|3YZl4UW%Me;q%+ z1pl`_KmM!d-+%G^{V#+Lv?-JTlmL_flmL_flmL_flmL_flmL_flmL_fl)(Rv1h#yz z0E!|2z~5&IzyAQX{`&B@1V6$7{w)950$|F?NJ{`<{?oTVzXAC2(yGeB`eM?eit_p@ zs=^9l!pdTQ1^{FL04yvV%-`!jjS&#xU%|p5!2pm^F#q%eP_bYEumBhsY`~vY020Is zVh8yi0cBiJV5$0j$F)nx%d@(N=d*sc$K{6GWw+OXw_(y|?KNh{G@8S>t)O9$w!r;_ z!!=pw^cm=x@AL39+wj|t`^);x5T&3d zTW#_#lNnN~=V>Br*rFmyj}QHtC7zu3oeB?%d^j6ZrF_ac8|SY_z!?ari4A!^%(BB3 zJb&Xw_MUS1CaRl5F0}dkzU4fp&bpC6nh3rrmq8P53$S3L(r6t)W8(+9p027lPYeER)-HblJc!Nkp7yfmLQBUA99XI~`d=FMQ04nVd$_tEaoBRCPt0&AU`J z+it`*4^+Ib6_#hm2i9XYLou&hi1I~KIVW)`71rt8{{ULe$*eW^%m7I^W)pq;_0nG0 z!OKbPCxM^7ThzlyjX9B@k*AsmeZ0FnBHp-fc1OH7^mr=u1Wb8T&6eRTImmREO0yb~ zrWDm}&8sJ4bcP#{?7t}wS2i+)L7xlQw}59lipu0)*R6$Lely%P3aUR7_@nt&C779Ov`imgMlc9NV7%dY`(&;dvud4FTv)d1 z{QyhY@vjPE?|qO%W=^b|iK>%j;K_ywXB~pG4kow~pN;v`%=d;tO`(o*R6cUeFw90R zuSvC|uqUz{&w3AmgF3~PCa*D+-Q#h}wtYSO!!9!7#|PBxM!9*Nak)AQ_4BSJ`ROlI zn!Z{U4Xq^|nUiFx&f?*l5vl|rd#yLHT4Ol6YTT8vEUbhRFt;Z+U$18KCy7P`O}xxf zU9~jUS*1m5+^ZV|HM*6!$FFg(jK9t-5r)WX_PEZQB2g6~>b>5mv76&n?P5VxjgJLi zOk)D4)wMw@m?S?zoAvx_qEk!RZTu^*RtQh;>}nhLZ@4!_u3f&2$G&>Yy3Q-;xs-?R zKs|iVZ{PW(K3NBPVwcw5er}#*^;(uy=x}~!l<+XRc;*9}6DVBfdrE*Si3>l?vgaH< z=a~~GecwLmw=Rv@ow^ffO+%<1!^7=8BR`OcZ}B8xbb&p|nW>~!mwFHZ(r)b!xE0uR z&vw$d>No9IIuC;q@vWrMzg{qKJRV@or0HO#4#3iJXLS=$IvA)N9^@_mnC@2ndQRCe zpP3P{W$n&}TVm2$kaUhVe`j5`y{rs?)1Gr%TQG)+07VZLhG?E78tj)S7qyb&nq1gn z&?jb8j8sG#_vtjK*#{f(s9X3^j@Oja^&`Kw(I(DeYTwMw89HjW$)n@EmXXI0T7M(T zwS33Xd@v>F=Q|Uct|WEgToTZj6w-Bgil8n77ij${Q)qRmLi9H3aD}9RV)3VgR<|`T zS=Eh0HMRXA*0LHIs)nkHmf!cm?Uk}s+A<`}p;L*t%9(6%*>$1tW1!mQu?Ujq;8>%G z?;TFw7Na~6CIbHJwD1C{fwg8mO!>f!brYr8zV*k!=p1s3FGpP~FBUIvbN&Fh{{Y_o z0q~use7+-H?4;%E%#lHuXr8@?XT=fQ6juxldWkb}D^_fCwHWt)(Y~3&rX!gfexy@e zyEERi(X(60pUnC+?bFuead7S?v6;Ya|7+YfdL9?Vrv`F!2zy(mxs)xC%dooSQC!)7yt1v5FIU6lc4m?E1Auu=g!Z)pNh!qXrG<0yc z#qD%-hB_#FzL9E7$cGbud?oS_Bg|5-BJtgYd&uAw?FbyO?nN5=?BDFX-6xx z6RnYO;|-TZ`>2URJ5;v88=*Dlmva}Y9-TeQ&3^z66vdl(OME{&-zi_mb>&UwX5VkpceT9(Th1F_WVg!| zVJ&<+*rYt((3{Rss{jGk7z^8$ZY6IqQ+&XG06Y(*f&ToKR`epmA!4C-SB4zh3C8Wd zU$kho`)VYYaQQX+<~5ViwxNj|Ek;8!z|W z7R!>J*!AQiae~%oz_pC`t~)r^o0Ml{Z6*=TVnpWaJaod-F1(2RJhs)zg=kbI7K-L# zcdVo3CW(hDnQR_6ibDFjmSHpfDh5yULLFKtr&V`fR(}Pp<1dJ8mz8n#Rk(52WszAL z>?)MzWY4a|%Zm(t&Mw(&>$XnkSICGI zR&pK0tR~!xSy#9_KW0m}2@y4v-Z~N|O$y=aSZI(;kD(b;X&-S&ATK7cqXHMsXE^0^ zzcudCL_MCTFKHFo>AV?jeAS)#eTNJ~63)T#4}e$uR;N+>f+euv5k5;3kIB^(L5tGw z(E7C3^6mV{?TJ-rjx5$NTYE061(K&=%Rs0R3!Eivp2a+TU+|4KXd{fKsK>kO+4VO6 zW-XlmIV zq3!%kYH~zqeiNPJmUY%>b1jidYMO7Tlhd~zH2KA}+Dq{pY|IuK5&@IZK!OeLm!l_QhLj_*fQd#B@t$Om^I=IjP5 z&1&j#C zGih1%WR-2hg+?8r5#-;TRjFu&-l^iK3~oovK{vswuwmrG$`4M`(pcoHCG`_`F8*N` z?&)w%d=(zN9omqql^G*h|r& zRNSbQ8u*Q9o~i<++w2USgJ|6FboDLXy3R?wUvkme`OWx3JfZm0lJ|QJj+m~5kS1;q zySe?r1rmxI(j*EC4XHh13<3e&*S&}a*4Bc##8n=S`EsO>xj!&9aB4~p#_Ga6OWoRi zg^g9zwoehtn&v9XF(UJc?5db}-DZ;#%Y@Rtp~NY2I?4?E);k~~jr?lvod=hw?Lwc? z1NTGD>ByWY@4((l2c|NQ`pxRb#E%g?P-_cQ^&IBP#>K%Oz=Yy32AzWME6#Oi+u)wq z`x72h*_A#H$$nT?z70=JEZ`3cQjFv0M)51~my?wP_k6o#vNr;@^VzFx*JdxQyQA6N za>%iqmcwR8YsMpqccE=LC;he~Yeg1!_YGaZ*bQnYAzPuBfbo`aZV#^)Xf9?Ucz3$bn$u6Q@zcwHw*mL7T zP8ej_c%v;I9(&H(_14@IQ217D$m4w;kNk7P%juo=cNz~vHwC9dZs%#5Tak-D00Z;V zE0lDZTT|LqO?c{3MaGvIIb2U%U+{#6ZZBh(71ysKF0johzM3`Op^LCG(QVE22ccWi zo_PW3z`m(8ll)q^%J+S34tRHc)${X?vYql9F z%Qqy^Ejf~4uX*Didmj_%e6$Xieo>bmv&N9lsUXIUDs`uk$u6_#V*nQlgewxOH(6`3 zI$RZGYBRI@a+-tMRkPFVqB1i3*-7~Ptr)NbT0q*FmX5E>D_BYlCMqY;T&xGB;Z1zO zl61rlYeEI;XKVKjf)+j++~rqmKjFeozf+$C4ZM|BsxY`s79c|>Du|#Wb`$5HjZoAd zR_}Z?y2Z#)$*o~arhEZ8ZDHqC%oaQPU zLS-TR^C!i{y8GQB7x_!z=PUf3n!fWWvsWAVk;G2Z;&Pz%cjn4Ife}=-aEStPUvRI& zCNqycwMlfpwB20OI&0n)xPDFg)SX;`JHCZl8u% z7uoS;0jafO7k!JRdTZv``=J#Od#RGyGn`n*^|hxC)p;_e>gbb!TXLQ2;6iyHuz8<& z3yHQlgzla1&(**)&qjmBhXteV|XzS znqB8jcA+YTnn)qy%6Zwu;nJ#S@=hJf4U&~p!0A;ooQO;G871!|QV}@W zl+i?B-bDd~cbdA+5qa(MwVBnb(tKdThK;eywOiM zEhb9J>~f21(~m+TUqi>3Ez&{4r{losTT#D8KvsXJ-a$8E7s8EYIzy zs>iHWEq2o`vu?`mppSObT#t4`3NJC9OHo<^>pZj-t|u!&509h%8E(pwW@bV*^PT8L z3LkBjP`oGgMIQ=$_#>-**32GJY%{g?2j{QJnbU^%-%qxM3~jKyji=zO#kD1@O&Ou? zsUOPz+Pao3gKVK*b6%Cx<^Jw6U#p6v(XNss5})tsYj)wpimH__^^YVqQ_63Jx{fR< z2=0Bv>fo*wzDLl6x_w9TlGSb!N@Nm;uHR^Zurr6DHul$FTYgewMlN zHuuH>PY8MoMDKq5xRyQH=lss0aIlU*8=Gn=bFiR2(CH-XLe(Hv5NMPnsK5?hZcZIe zVRR@hdvWH_mNlj^>!u&HZF8S&67kPD{gCHvO4`PlQ`R=M-UT|K}_c13hM}AXPZY3_uTX`D`HDfFa+9RnP22SfBvw^OFE^ziE7EAbYZ<~$F zCzmhpm?~#~tdZe(boI;ejYLOYbk5xkE!2Ax(TGpG5Z|8qQcb%^C6gUp<#=W3$*dey zOG}0mOgC99@#?h9aU343yZ;zBdH?yI=~tgzw((iyMSZqd^j)dHt4un9L3ph^N!T!eN_Cr%8 zEQN3r#;r)b(8ev$mbwIH>zpOr;3M979EBIrq(M3J*Y=yzT26+-H6vUmUev6WM$OY@ z-0F%~z_5xY_$r%vQY9k1$=duV6h9N#khv zl5R8;+^vJ0vv^E=VV?XxUV`}Et*wR!Ej)Wi52@&)wIg=z`xa|a*&Xpz%*0nKI0AZ> z@q4}PcaP;H?G8%^f;fGA{Hq;zF4~B0f~@sF!<&yhn#S-dx*mHCPSdMQq~!CP`guZG zROJNTF+UhOAHh%`t@7oPQRikJ&ZB-+>Uj(nR zd+S_LEg;}pT_%nuK}i-d4bX2}vR9OasJ*BG}iyz*@fqxu}|<_b2WMvzDumz7s;*=DX&#Z-(%V*hM<4no*2jIsC=37S7z@ zG6tGm{etY4fCW47j`x#btl`zZ=wL=;>xUWPThrqB-9g&s!2~L|M*gJN;#}6X?9mfZ zp$bfA&9u%msw>Og1^Sg{3hxXK;Oa&kv z&);TkRbR>64!)?bbf0-Z1cr06v@~^^*~*}2PbQQ3p@9)XYAfuxnslX_YwmbzFs>|nRS2s%cG$^KXj55&9R?}=RvIHTfijmNHx2!GG z*nEGTwL{(7cioUzS~5(zrTP|;>FgDwg`|cM3ki48afgw*A1-WU?#$pw)fsJ*Xsv3D z)GFR{X7J=~JqnFkvLSA2i8l7?)4L+-et%7g4=CH|=N`pHykvP(mM7(~38wHt-ws&V zg4vv6(iVO5eCHZu(fv$AQbxsaj=8?CO*bN|yN>CMHFj#W1zk-WDqHVoi8Vf)_#ga! z9SGBbQPMx2ihkvv(QCh#h4yfqPN>eR8UV)*lqVZ_-C^3 z$-G-cc9+z4o9jhpfxrun3?RP7eB-6%4pe%9rB$2A+wNkv9d%2!R9qlr-8Pu9BJ0IcJ zj_8r}y(ywQ|yip-k(va)5w!E-TVIM@u$-y_7i(YpKY-wE)_U2n6CNZ7)!QWlfHKeqRh&q!(Jyu6v(^+NsZ8ncHGn??qJ7j?x@HvJ zdRT_%NycAH8TxeyJ=sp7@9m0b(h$t?9TQb01_=8R3swwt@n$^mH|tXf z<15249n(0z?!G}OnaWu1O3nkfWzRXvEsyo6JaNCCT;8$tY8Bmxo@ zn*K7a*5*Gr@?;AipDdn^7e~pGv%@?CHD2+fO&DA`g{60avmM5K%8Sj!F+{zE^W%Yx z-qDSZ$yw$ZZp*^S7z*6S3-700Kl#_Y*5zM>UaY6TI=_D3>@G@QQ_!DS#WZni7*Lsl zajlt=mokqk7V&EitT1sESf^f%bnZ*6-kG*_zIi-ZB_|!OJ)OHs5aL zVA3Pc>K)m~$5o{5e16`av)xV(GaRoo0Dj-{LHOVo(?Lu2s>pnaI-aY@dKIw+=|}WU zfyd?zJ6Zi)cf%8Hv=9`92)tS4h9U80?DIjXR-I8L z6_^T*Yg1cK`c}IbPMr(dcCSCjEYlr7tA4)`4ybBNn~?yKB*-)z%A?L9q96NGg)X?1%|GY{vuUO#iU?tQeu9oRREsVqTSsY^R+;lnAqWJ+HhXOWlQL~8sk{}{Iz!i1s0u#TnOWmiN~H7c%~6_F#A@b;&&NZAs+ z_HD6&1aFo~Mk5z;HkVYROZ1EunK-#a9uDpYW~~=_{ZI@Rj{WJKTj$~WN6XMd7T(G| ze97K8)x@7Fbc2NK^9AqJ7Uk2(k7ucOR$8fcHqoYW%T&ijR$nk*JTBV)03e?SiDMYC zsrTCH(N=g~>q`y8>VIkNLv_V?DX(P8jwo7=P4s&{`;~4AS|jm?gc79?BL+Q9*+t+%$cpH-U47##lgP7-IzTVl z6$NsdpXa{S;oCSFsnXOD%I1tdhb__XCe&xkPeH$GnX`O5FSuC%ze8J1f3l`6mZJbE z9zR|4lGJl+ud^iloZnH)YY|PYWfyN()hb(ANpoS|TZJ=>E3z**D*@7yRGy<8C6lHwNGLHQgS%}3*F&IHmXM*3W9Nj|QQJu;h8Qy$gR`E+7c(r_yPE-J_C5K2{ zcIdGAr#(LEU!Y{8?zZkD&$aJt-*ZRr+rQD_#Y!K$AIWQFGFHrLi zuj!{+0&my$xommSdGDP}<{U=WrUe!~Q5}brS|&D1vajNd1?*hPwv9<06Jq)cZWgZb z6#|y$EPJ=@;E(DidV?H;NR~R{5!m9h(L{rU7~VKMK?38K#L~^$r6E+wm(_dQ zf_!PVCCA6YGKuASLPd5APa#Q_6{&q=j-gmvyHHZ}0u+c3+R-~o$Fdc;c;bt6dk!L# zh$XQ7E;EJK#PZoyc0CVqJ>@9A>GfzEyUn->SN!kTBrSyVwPup*USqcR5=QP4j2~7U zAFYGier$GuE15`pX}&&qTm2mBGcd9*JDHT-d9yU-m1q57pQ8c(^`?;%$)vMRC!OI9 zMxe+93XBfk+rf_~HZp~r;u!Dczvjjh<2%t*!3u1*b}`#1HZV5j6Rt9#FkOsTgAA$EiCkV-SOdg4JeL+fQ3SbeO(sk1p*RG=AJvPd>t87u!@WOnKnAy%OEV9Hcp zU2T}NzsPblScYcQnW+NRJM0_VVBkjcuDPBwm&^;#zHTEysi}qrPDeLe7NcH5w&ZMw z#>}m%$~iWeWZACJHk{#W@!93LfZ4~x^r4ToDqRiJM_N@q+V~U#S$VCL;oDCzzGl5b zwBm2#*M;_KPUu*=!UMy)(QW3uOX0u+-`u(;9Q7&G+O_qovvYMn7C){%jP^TURdh!7 zIxvwfD@_CtdGrRkD17JFr+hLKqQgZRaFSR*tJIQFctdQvoWA#xfd1rsK}=@Q}7hNKuy#cpF%@9EBwsg$l4_>1k&1ztDI zS=(*u?t_=HqzWJtp}=MZ6{+LsgOQ=nuem0CvH9q-5R&ub(gWxxi1_D^DlqoQOfq@4 zte>>C1N|0##$c>y0BOHKAel|Eb5#n4 zQ|b(u#HsNyf$Sg|&yhrAP7lxBKEb$Sbk=0kCp+UHRm;&1Fc-U&{w*p{ho9x6U6JKi zBS0uk|Hwv@nd)RL&3jFGbOKmQeslAi>|=tX?xQ%n{rjD2st=oZ$sK&lYXJ(;by%rs zG{B)=lO(FCB)!d`GwPQnF=@B4Xeu$*hw=3&oN?n2U6cInJg4oe-CZjB--@AUeD>2g z66B%;NdocdWdflh`^Lp@X7Vr6qc%B)LK7mchD>bUoF`TU&_A&yl1V#iz^vBvR^GRdrhVI^|N<&&8jWkaN@2kEt@7xJ?d9HGO)JpZQ~Bf zD`g}QmkUD22JeSxO5@7gx`!j;u^%lP%#JP}dlMTs-hg%1DYXxy{puTw^{(t)LNVXf zm9!9e{`gQfbTQlRxzIe5gv>s3p{1Pkgjix)ac&t{_yOY}qM9^!WJ;0ZsJ31oI-WO_2yt_po07q4wM zXO0^0K8b4Sb?LU5gWsOt7CjdCSra&g7iEy~Ufky9RE@e6%YM7oM-RWCr|T(aX9_0e z-iOA2?Czk%cP2G{^;@a}e(wA3BT9CKs%d_0h^^Z6&=zmM7x$NEk-3HgN?Ba~33c^H z-4h_+F&RF2L!RZk)@U6rAQ#DH zSPeT56B3`*5qdHv)bVSn;KtXTbv~ok&DKos4G$IGeARM``YA(NJiDk-KYR-?4@&^! zRkS*Ru-+0$=)#@rQpIfv*77oLd9bk-p$6fiB;KI#V3gGDyj7Ae&+(1EiymNL22^|9oM<0z9(E@a;E!TXJM zVvSS9-cWFq%DxGFRmv{%$be?)Z*<7dxLTroGcp`z>tSO@MFa09-Rss+RgnYO7N}U) zC=3hsM*7EWeLL`XNPEK%cGL|+b!Bnn8OQy!cEW)&fQKMDEFFw#m$fD1KLE*F-2#!r zSXItZG%a@O;B4ay_z`tWCFP3C@tgIix+mK9`a>om1Hzu%^CC{i(%EtRuso9GhP+DB zr&6?1Bf)L-Z(fx!*A9?VTm(LM5j;Y8S)``Jc*-H`*&&Yo6XmeV>OP%<7luYYrjn3_ zB)78MCbj6jj8iNli~$+X4;t%vRRz)%HIpDWEQ}uIH-$IHovik8_11n(!#wtX0OWH? zxTdCGIW;(54;PWyV7&{lVRRU8b%!19esXCX)eEb!q01sA>YcnX+8=D7Y9!X8>j(Mlo$Hsdm7-NTw+TU`-VS0k>;Wj8At*E8T>D0B%WN#3QL}3D45o5aSTRm5=XNJ`OPw zn;>0nKay|Mwg5$8&E7t+f*+A`yf$i%u{&Z?+~obc)QL2B z<_1q=+9_W|TZ>2b)2IVg-;hUF;kjwuii!2HLR3>FHd$H*uj-|sE(vMFRNZ{$=$y51 zm5k}sQ8%4tMW(ik^0lI>)45ziu6@(htMU&Dn>ytrl|@(j`;!brjH~J8-~9RWR{j7! zS}tia?hPaS5F}<^bP0=Rgr!F$Mrmm=COaO231x`{H}S<0L9gC{0~;-*t^LQ7kBXp-ulUi!O8k{_1Erp z=?iI%Vqp?oX5x9+PP&7a9&O({F4IK@H+~D$HV21^f0>K7uvT`S>D7u~*pFjrr&e*_ zxNqC`UayXBV2Vg}gbB69E;%1u!ZSTT+b?em@x?EDW`r>%lNC5BtV~;^DQi|-c}OF4 z(A@Vt=$x~A?pYbhF|B^Tg2uQCOsO>~OsG>MPfMR|l#lr7xoSYmsceymU`=8>Ap9zb zg(tC--PTdwnc(vaW@Y;manpHTLN>8I0ykJH5aHRk+a`NyT^mh9=9`~2VyBnLv$Mbl z8Q**r9S`wwQ^c5w%1f~Z$fpq9J3K4-U!tkAS(z#meLSLM;MEXRK{Z0mm+)391FJT;OU#_Iz>%<7Y%073GUb6+)*pcVL&zUMUQhf*>Swm_ zhyxfb7kU4TkwcBqanv^MR*~)4ubdJd`HV(x^ooe@t<974 zB1}0Y5<3%yX)QjDgFt)7Ym5D**PAd6+poM+Do)y^O^JF8*EK76wBAnio!M^$*W}0L zg?yKp;9{Gm8UTnb=r=q*>{SitF3%9 zjrxMASu>xs-{S1{KIJtpZXErYox54>@$4rrq~ySqGfVNtsqXMHwXNl(MvLP%xPl1w zDw+cNis)ZrJ9s=(Jz4}7g-pxgW6>stOA?!RDo!NBC+)E_d~dtH*U&d$>tM25 z{r2Vs(Yqz^m0Lq`w)8B(d*z9JWd0^E3vr(5cZQ^NB(11HKqSxW zVG4?$EJE;hRAauhMk~T{z}p5C@b|l~bcKqi`USDW_}cFK7_c~*w1Tn$Ma%xGL znQyPyEBEMqNu;#l@w%zX>a{~T4^i-B+psSCyQo%@#g3L+foeOHmt2go!-@CHNG$%c zcwa5rz}x(9SxFBu4OSM&U3PB$;PD2>+H{9aau!eUIR~2(jdjC%^`*>QTdJcP8udQs z39@{dwY+&PSk!I%2O!NXvuhZom3+=TLxK{(cEUp$Ba=*Ow;$+QkJg|j1y|ty#H=w_ z?QlemMZck;?jvAR9p`~T)ENpRFwghvp>cxmbc~}=(5%`T5Bp*EUFa!3iP}`Rw~qZG zBh{Ychsd@82~BUfB8>aju7QpCfEwj7JZ9^HnVf zktEa7TNe&TUdI0p1janXdPjDa>M#EonGDbH;ySywe)l0yfsTy1CLeoPy*tl#=Si=@{a*&@5D5Zm z*8;rEOyH-{9A{ho@Kak^tI?5^tiSUaEWuIbIpHJ-prv55@s|r36*M8n2mDG{X^45% zT0mbZw>licZyNN@Pt>&EptltFRK$;vVj#RUQ?9>Qj%$rzu-RLy=^zu^CV*q#XmKox zC?HXw1E`z-6_subFLMRG)FOYsZtp~8%8)q-=ZBb$ZB&W79_)7pL$7doTDD@k?`Jxz z--iil{AjBj?f1KawDE_yi?)TN4Q&ttSH^eVkGV_`t80~cFPHeG$%I(UzL!t%dfomA zfA#;rhVSvE+7kA6zEy3>J@!mD}f}Gx#zvb=c3Qbc-nS|i1Gjq7+ zU%iR~HYuuh_$C3fR6dO=8QWyTzJAQlk(rh|>NdWs;N0J{xiYe13}DVW9ak@jxMm;3 z*9M7F*x-mx&%MxW>W0!;I3Y8Sw~z=#WtOU_8d*H_BHx zU!i)8MI*&b%Eh1FEFH#I`&Q&KoR?hOcmzkNYMJ7V@yqM4naV#CF+I0U3K>z;M16)p z-uS}r;+z{lFL2ySNPpU^fzb;u5=d^gZKt^}yKuGA=-)~IAiQ!kh@_spJX4O*L&3JB zFu8iFdmk*Yig}aD@OX1YlCNANn9W>ULON#*5N-+>Ih-f)(uU2-tB^S#ilTc9#y zzwXz2Nj-6yOPkmM4E-!%mbplB#5F4tq@xk&cpr^NXcKCJEd*n;!a?C8X>@1d)OIyG zumESE{;lD`Wvb2Q$ijUvY)D4-_zMubDs4|U-E_TEc=4s)k^+rwIukVe*~vwFF3pPK z^^Xp>muRU7HV%)~H)*{W5pzCUph8|w(v=U#pMDK2mtm?)kL7vFGQ`6k_lQYs} z$m-#@H2%iX976~+SzO&To;TFX7F6}oXnRg@Bff(3S5lTXSgSD0edvkr*0H&2vigo8 zvAu_c8e05@8*P&pGt|QWCL+gnN+9l<9C=I?7H1nXwn;3a0gYCOeI@PHRAL^V&hD0i zD#ar~ziPIVXq@a(oda>5oTmeZk&c&=vt4$P0w*wFx80nHl8!rn^CYemnA*w0igP)%m04b8+YQ zvRGuTYqU2BGc(WX7Ii$XvrUF%Y7|t7$@PXu+ zx_+s~YSZ5elTX>iT|g5CcO|z~AEv7n?v89 zM;kJNLiAddUXn^$FY0GalSWP)+wv-WRXz*L;uczFEzF#m=dyArR0ssKh1Jpta?j;c z@ABOav*fj~-v@Y%CF^m3{$te7)wMIjzTPbNciv0b78?vnNH?+JR! z^ZuGLelrkFt8hNsBofGJ!8d&J;O*}n{mlQx3#`rxHYQ`9ZJvrI zre85r(BX<}*rjvH{BdJLEGSm$NHs8fFsb5PjZblg<|+|OXBUg0C_p8B{DaC&%%GA- z<}{qqi93xp8gV`+tho4RdZnswNme=rt%AL&M;oz`FmtT-S%Uia@C87+)Fxw$#p=^i z0Fqd04~ExrGD+60EB6hj1d$>_y2uKNQPn_N-hsNZ<1)zLPWtwd(Q5U`5%fahpsUsq z7PnzpS-o{fG&h;XtS~C&Z!K2sw+5(c6KBn=mr)pU8XCpIU3$;rsM#E6`!w&N)l^>8 z-fY3NuwrsnYxUgJ0xHe-^cQJ5mT8hxzt6-o8Mj^eoDQgG=%Hu`onen-_R(48*&oq! z1$!4cT4j+oN8K#pkF~D1)ptD)=Y}wARovcY)(Qvs9kpm1I{X39I8b)87R>YgBx>8e z=xy&gT!eG4FKX=U_^vukrRGbop|bEIcPgq;otBiy#7Np!bRPb@+1Cbja5JsN-lep- z?emL*A3-h&?T34jLY8c$g5UO^FZl?O_wFngzpDtMCq-N-FXe;6VO6<9cg z7_zJ9(};zwH)IGR8p!J5c)xq=QwyB0)2SrfhV$(99;es0iK81$yT^^juqCEEC22oy zdkJ%=o@^7lSjdS&aQ3Jl`N_71$}|%3e*k(wg};Y^h|+ic8`1J3?IS1MVrAhgR#8id z9g*BOY0=!-PMUq8Nb+T#sWkV5rDe!wU?Iy%3a`OFysbDNU1H z&mDvVv_k35jk2CydT*ipxyscmZv|Bwl?}F4MvM&}!v)T}0boB5r21}@d)?((KW1+f zje=dYfikcRI2o(zdqO4BgmnbieXj$Q8-hx&h?mxAF2j3y@@ts|rrENpV7Kf?IrN=! z@ZVvj+f>!lPROgwUFs$4R|@VQKpUaht`KBcoD_@M$d_6Z2U$}!r%UvYQGUp6dRcWQo{7hF4vPde0&GdW}OzOJmfvVrSbF#y`7 zvMRbWXg_s@{{YiD)oTD(d4i*&lJ4zQi>j>)Ap+A;*=;9OHZ1p=TdUUH?0yR89)B!> zU}sD>nM<*V!`_rECp*-OWU!8w1{{T0?J8Ed+-GcyX!@!fEXpui~@h0Xb6(yBcn`1bA-YwNH&5(KAq>$zTGA^om(P&f87!z>yr+5&^#liE*^(0 zM?I0C?wywH-%ni_PdpekkA)*Y1A2O1vf+ZC6)0FzN1}_?M_impV;>Q-ChLwSS)f{` zVInC=`%SOSA@GuewZw%YcrkM3$cZH2F{PeZpu=J}R*N|es>!Xcsv#Uv0-so+(C2l} zRJxRJ?Te-Sn@TvfrUq#4JfG7-s*{$0MKYF69kjii`^deU%$8yWppt5(GS^r#TCS=0 zO7$bu4i!|IYS}MdmAdKaq^V~nLbd*G@u_UCVqzDo%dtwIx@db@5yPcx%5oF^gbuN} z>+$yuL3>zr-#zJVb6HO7u32^UA(};HYTFU1re3ktO0}=vt#v19FY~dQ{*}^c!n-{^ zETySi-VV{U(Mpci*aA#VWv$QCZm4qVOGiMb>oNwnPL1Zu5l$!N_ZU?4A3M?A@RJd1 zq^>D-gktvJaaXpHD$AUXvP9$hd_lT#m7yB9qPiNS4~3T5Tsp8DP3k1dNxx-b`Kt9( zvD!7h8kEBI)imIOq(+41s^juAklcx-5r3rR*y@@`%U}f%u8`Vh!QYfpGY5Jk%e9jF z(4w5u_4>d1N8hv;=eOkN=IwF0%f#(drH=;36G^?R&~%p6IJ93i&z8S3Pn%@Km+n3d zmPbqpljFkOA}on;zQzWfrMJR?vt-<%kHDUd8?^6}sy!$ z&z3rT{UIl@tUdz)81rpaKYU{=;!OI|1L8>r+#Q6Zxj5JU;H@FBoRm8>dSvg$m%xW3jXN;)k$87!T>wtSv|oOdZt(w z+GPXLCznp3+Ui4FZ;l)?Vg#Fwa)6XrZPP+sdTv@ajk_$*&fe|kCaU3!uxixHqm!)a z{{XrY=|_<f(^@rl-Eh~-$TGr(As^O*#_jRZpj?P$7#^N zpO;ljFU%Th>PqClY4CaJ(MYFphvug$h;vJ??9()D;Tj^4%Q1O9&1C2qGeWLd8iqfw zppmnz>l_?(_qT7-KiHn@=P%BW%FoS2P2-Pbsz!WR^452mC8<@EZkRKNON3Z-=@Kzl zhd+F{T5`fBdv{HR8)h0d68lWyvA6lI79=(OCny>TeU9B3@0vVT={o+=?0m~<%0>}o zZHJb~*-nSENm(P}=+n1M!gGf1`^^#e#TT5ty5#*lK0BB~K=7^wW5dr*=s3uijQk-$ zj7lsyTj5M8BvhYg==vw3GI-NpnAmM^OyPDqUcnGr+Hq$@vK7{J71$#T>=94Ygs_vb zpO{{gczCxhJGZVvbw!6-hcw$-)|ZooOfwFK=c-4mi6A?iMIo7BVkcS^eSY%n3azj# zKNOzhG8R#4RqOh512v52JCBTP!O)!O_gjv^s5pzUfmxNSFxuE>M04R9*RE=srr&Q` zc%)n>5v_U>?)=*?*c`3ZbS+_S*)>78nzGKWkzL+L)i%MGDvIJxA9NM0dw?=~>lJ~_ zb!U$W$M-_7{9!ae#bU!}iafyRtS{HIJO_KzfSm87jg-ym0u)14y2O5XR4cp0oS z(`{u|u=0|EEvm0s-!LdBH_#_~+O@FjS_xP1LcXS$3aQyC=r2%8i4CaVp-DmaaYaBb zn&Q8NRFiHr7H6CSBbVz}>mOEC-<2PpznxT`udU=XhG=qB43TbHL}t=R2}pVU6XtPa zO8c1kImwUa=`pnBw|=Oy{@y!JlQY>40~f2p_m_h>^x<%$>5&)Rxe$;VM1M4FleQkO zv5~(#Fw4Q(X0d$&Bz>%a@c3?mQ{l~&B5n}pYVOGp>a(x{vIFNfE}WyvvL(Ojik`xfi))Z{uX+H&j97nMR#`hCveuxf z7m5xyL-G3!!#G%qcc=%}jzO)1Sl!e>M5{}%;d1@sW7!3|r1}jF$`wOplh9QLquw?SXY6-WXp6m>cH@kG~Eo3{Nz-@%}_5K6$_DmDUZZFa(0 z8{1Yg((&h0CM`u-nZ;`Tg#94yy7%&v@`H(L!s$fVBMZ78b48Rc zt(NWNFx9(sn2@(^-Hyn^ipkf$E(V9h*N)iH%R|*3Y{7ZX(Gx6;kJTKGQ6fy2Iz&-^ zd7N@Oee-PPuV);CvNkbgZI|p>c;G86$we2ihI29&T+CtPa3Kss4AEd@$H10xPY?_j z$&v>ZjiPU`r+K~}-4Tw7ni!^JV(9tB1*v}%;zMX5v^hv;N|KQnfw;hRP+OaJ&;Kzsrx0F7g=F=9qP#eoSBE=$~VMeqoI?m#y`(K$; zuUaM6m%FDv0O~qIO}|(&fe^2KxN=3XuRX2LoGwecl37x(I%QW?b#7@+Osl$zM%|<|?(yvbI(4s5D-76-}nQ>Rv$o8txCzaPPXBMc(!? zuddMi(Zufex`0~I^r;u&O1{{2jjeF;-M^=!!OF(z-=}9Bsh=066Pvck-5^I2t2fsL(uwZoRxLPG!bKHq{C6 zrcaPgxUePPsjn2Iz(#J20#HkyCbY)d%RVgT!96uxx7cE--q6#P4)_*@|HH?*8a;4Qa z1&L8DIHPuG_Bf%yUa4uP9%u_5;^dpm^rSjc+Ukmk0>cUbn;tSXgtTyo4{=K-(FrEflhSd;rc?`<~RQU(4IYI=-RI3 zW$4=GMHPQo)So^ana?ytzobCBC}Pd|G+2E08bz$BfbztzXXK7prS5C@ zo)|6HHd{iTuX-<_uWpQK+fJbLeRIrUs-fIR%a{KEBRzBN8PN*04X|*+W0G5ho$6I- z(LJ|L2mJ+Kgj1XQlY{uCDZ@Kz^G=q}Fjzy~8R{j9i-x%8m^q(vp+@yZHm40V<(kt~ zu_qhMA!xEDDa&DB*9ykDSfVS~l!JY_`kngY(UrF}e>;4UScqwj6L+R&*Ksf~A|~T- zwVLvOqapY}42_s-4-JMqtf6a{qZu&@M$r~z*mJgy!bZ=%&J<1Ya+qjX;u((CYj#D3 zMTw44ihMdm--m6DxwBH%!P|Cwqs?EKz^;!r{$s+ zJ{L2Pi9Y5EWVvAQBjET90d zkh1NV(Mw6gTP{^W>oVRyAp*b<06S(hJ;7^J)>^V%vrpl6)RsQ%QU19(w6iumzT(fadAXmJ(f6d{mS#Uf7kW-eu_*y3SkoKif9~_}6|&u&+A)*QR6940fMOSiIp; zMPFpHFU6*tMYQ7gO>{R*PzyaU@ zWqs4L0rO>S$9}<(K0UNNNgj>&a|BV6qr@#d)B)PGVngV`4aSrAxDnSBIgNzj4iaFV z*y8>-BTs{8Xazc8Sss}BF6m<1g2CMokRveXFJe|$T~iI0R)wjWE}-S0O(yil_E>s<=|>su?PNL;sE%FCg>sTogh8O_Ox{k#vjD}@E zz~qZvL1N#9w!$aHy)zcaW=DzQTTm^{nJO_=)|gu=tr$to)_`l60RZ;0#!fk_wRI&* zW2$Zx2<+IRa7E_UA`G?T92Ij^02P&iLiLLr9e{7oqp*?*J)xa zztdBc)s1C#)yrMw+V_Va>02J@`REs%`Jb5H)41OI#p0*411Mmq?_kq4(}!7eHA~cs zwKZA#7ee(-qG$m@FMK=AQaS$1zAD>4o?K2>eG3sFr{(8HepUkf+)`dcvlm?o)^iM& zLeoAjtPC~OF4GstiHop-h&Dx;639d&1NI~IEA=Oy{{T671M+&7gY(eZ{iEV$tCnq$ z^y!(Iy{o{6W_%f76Hl!(O2TYhWsn0iSk2fJkA#14hryU|v-`-nP69EV zcxdKeUgl1{WgJ#qyMLW`$R}eLxw!55 zFIK$4U)#?w%ettIAM$yRn6fVqO&Nj zwNRiRm}x6XxoG|E({iecv!Ls5!+zJG+`8|OEo4b4cFnLmoPE!p_+y3n7N}+b#ZsNN z5lBQG9HtXzEX{bW7q-5ct77`o4$QF>*%?M3Wc^$HKJ1Qv;_u1t&QusOr{s)Ph;}-Q zK!V1*E8<#KAuy0q*i%p$8%pYrw>+ZkhbYKQHv1=NnY*AFnjTAAW%o*;2JgkWvWw@#1+NL7lmK4^W?^Zk?kE(BW>M9#)TJY*Lz5P`Jf$ zi7!MUll-1yP<=7A5gUmJyY*uc@idk z+L`sEdzb7bC|VYo*K#xDMvduF_se#$Z(fMAt}8~}>WbaOFw(&r7hP)0E~A{lz=3up zB9d#{T0cdU;Z|L!eFadgw^n{Pa_xuG73Jk;Q%vA5%96lM#B`QFs&PGU&FZ(amnC$v z8dp><@qzoDYM`{SDarj9#=MzxR(A(S@Q+W?!}>C?rr7$Hw~OjklJmtq6V2VH$8Hw& z4c7`2k=ZHD(!C^f=T0(>>PvMdNE>Am*5~TZ*r|lHEnJ)(U}u~W65nn5FrF7(WOtm| z+c2}#sd>E>*C~IW!MalGYfWfkJNl`nvnAVn=w^fC-6>p?#~`bCb@xmyRPC-nC=i_w zZ}L^1i+b|2ZKvp}A@#zIuvr)SBKdPivBxg{Eiza(xeZ_COdYK_dr|^5Cc0TM@gy}FjwtfPW@3lST(|D}t+fB3G7$!Crx_ ztyVr(N$(k4;7bmZsCXx-I+vZPw+7UVrF17iikqs&ey;xjH2EdY&bZKOyB@3Wo@%jI zX=}2Hbo`|J_T=zcs{|kjJ%Y6LUjTB#ew}c3k2AGWe$lqTs`M93*rEizqTAo6U#?x# z$UGzY=lJzj+Jvj!EW-_iif`bbW9GpIa}l^x_BE+PvNoCtHJo-qqV4pkc!lJyZ<{k@ z0VQDOK3>eW7LGe+xI1-9%05``+D0y&v9#>dI($eQG|iJEUYxgY9UI+|u-!6#0ldyO ze|THZA>v?06ZLB=q&Baz@>cD0G1m_q%ltZa(f4HjrB>Oi7VKM^uMjmqA}1)zvAQLVe)EYdO(7NU zQPG~M#j{Rxfvb*>v5~BXL6lW!{{SPQ>fU^~Jg+ zd*|w>??vg#d?EQu`Q2KNIyZU0qeC(PqN)9g%ozcMMkMn(2L&ysU9$*Ta$`mLw?>%+ zJe>np^278=*9Iq&zL~oi`z8a7Viq3m+p8%vMC;YJZtrFMGwiUH_Sl|8pFB)CTlV69 z4y_}kN*6AE8sm`F7szqTGpS~S(TyVYkD?}@XAY{Pz?Kp7bX4KwI6tm(GTRa*A_ zrO_{zb-tM)*$h9CfE=)E3mqNJV5XY3%IT_UM!2_T*%f_Y^=d;~t-B-hho;wLGN!ZH zHdRXSpg`lCjx4--V~Bf(oG7PMT8D%rA8$^n9U~lzSuk`RCJxYdskGT6-(1l-Yq526 z2TfGHH~IC^cfTbl_AL}D-xX=|DZLdxKwcsh79?&F{x;KRH`WN=9eaOUo&&7cBUsfQd7w)uFRH*4q4c2O&?K~5t`(7 zdANanu?n>f2h?4o`t8|kUzmTCf0@-wmY{68gCP~|F?eCt7%}#XlMKnFx7(jnm&4bkQ9mLfl}_D$G3 z$Y#xojRAmWqH(?Z45W?NOKeCVV>VFD(2s419H9t-wo8RH#1UEGcUF?|W5?P+rU9c& z2`(0`tdbTQA}wst&;*nX=GP?+g)&~5zKV*#$gntV(oB7uNp5Ye2xJ;qW`KiQmoab! zptgc?_FI=7qh)koS^(*rI|}^a8v0?N1?xNDa(wP@{RX8u_rEE`OShfyw;Ym2)tCAW z-aCMEsrn^J!l!0azA31CD$bcJ=Y^rI)vq9u?CSE^Tj~q$ z_i4DIZL=0_WqLj<6&{6J*%l|~2~NZ3)cm?_9Yc@$542Q^SRB#YR{F1I+ZAPVSf|sy z_j+Eo3X-}NeOdEQFV~6`H!ldZYb%W$EsY6Nb;4ceSyJ8mb$$SI6`tnyVw8J&NCj%z^&MAMTKucSKc8I2Dbl+xsK%&9b{Xy( zrgFf5rua}>)Rv z2236e+Evfu1L-qk4Z}I%V$RiuQgVTm{{S(DLumP$WowtG2wJ||D=%Vf`+RK@^V^1) zl6U$$%q2mWJ{)Z4<+x_=H^A6LIeevwnYe61K1~BJ9LIeoY@Pna6S_2vXZ;h+zHJ+f zy>ZO!7EUMHC4CosL;i}74d5n;BXmgA)~rWpqgosgE1aVAjnzz7T|B)?{dH>_PC2^^2PHDiw(29{ zRqTa$l?}&K^v(0tI=UP+gVL57%e@Q8ow>&~3i0QadmpPhBcUgLZpW$V&PAemzg>8fTifb(AeS{t<-r_?l^2ehU!;+& zh3k5Yt3O>oJ?j4e(eKHdit$n_2Bf|Eie*jQh#VooLoSPEguY2;iH*da3FC{L`P-X6 zBB{Nq%j`J84opegGQSReBewYtN2pHj6+o*REJL9OT%8bzUa2kn&H5MEwMXVBE$l?Eo68un_+M2LRLjiPLM z8>8>pln%vI@1tDRkR;eX{8;b@% z#nOrFl{ynX$H03-wyKG3Q-H!a^^5==?#9x$hudkt2+wT=54k=$#Oe|wPvB=DwKM8mX=ytL&sZ^>kYRbQjoVUjt``|-mCoQ>AoEFk_&T<`9Sgo z4c>NLBVV)*{XwOj8g9+w?7sN3D0&N(IZZp0IkW?368JxT^BUo2Vfg2CWx0 zR2|(%O`S(O8C<;iw^oQJ20CkYswt~l+UCXnvr^r%-KbVN=~kG!Q%Ct-UiP;-7!laYJO#u z?EXyUn}U(50P}xTqe%)2k8dl=uL93U^abT-X(Rec(pA7JIROg7=svgUep~$69-+Wo zr(Q@!K&rPi_J258e8tmpYJs!W2ll5GOzB=WeT_23L+Gj*Tcf$EzUyjF>}xBM)fwNE zqQ^K^ba5wn*1}CoXy&=m?^Gukv{Fj{0Gr&6%?j*ni|nIW>-h)9YgadGrK=O#tn9u{ zVwC$c_ERDb*Q$Vfb zqY=k&+_y^_Lqy@*qsEQD10IErN)%?P#(@qipkzHRvRwA6j}qhn=7o6K%M8LkcK9$e zaoJft<8xof?Y@uf1kQxDwXZcTQ9WzVO-#ezR3xI2XOx2cvI@f%5!{G6!>9^W2)|k! zuiUh8g)t=VU2W8tRZ_L=mi9L)f$q1JY@nqjZnxIhYI{_=Ld)o`bLvgZE?O#9zOAb` zSGhC;K8*Z0Q4Vyz!CfNiI@7wBc_*nl7D2+J$jPf?p9S-bd=+XJyH)7kyHfI_KSS}IvCnHr{Z6Y|Hu*}Kw7N5{!L#%wfLeg&-kPuN z*4E0SnW?IGgy_IdHBDh*Q++_!SglzndCBE^)1a=adygEi+81S4a#$6+uRof-Yub+T zUR$qr^%COe={Gc4{IktZbrimc0+`K?j6K*GyONtp{q+X+o= zsz@yb*6<;m+%<2SBuwd^?1wMxwno%bIbghEIsNf8UQhPc0Dr(tmVNqi$_>D%A_ro?T*$v^b4Dm!BjEVjH`~;G z%UtYU2GcEHPEE>%N&w#g;<<926?_h}d$q=IR|vFZYp%E3^kuLjSeCW=wcO^GRR(JL z$`id}vuhyS+IFBq`EtvrEm8`jIx@Cz=MM1=!Uk-{1;#ce>igKh)o^;nxjGeq6*ZA@ z9?)&Nd78_~?#NApTRF9F;Z00&9$XQ?-pDCtod2GFLzgO7&P;S+^ zXs5l(k@*!H~tfCmciUvFXCMhdCF~a@2sIX42QCQ0|*_)sA87VW@+q{j1!f>EDJ?w(ThvDQ|^H+Z16eVC=&gvbM(2 zG-m0(Y>BUVXfbqW^nw0N)%IA9-78$JaY;#wnE`{3$r~;PA4z~qbRVb6)*Mk1@Zn0| z?h>}+2%_$vC*hQ+8^M$Iep~efB?RtaN$!ZM(l$)59Rn@}cPx(#mM|OP3v9?vlCv|p z`L5orZgvPn3isVujq9X%!vw7dFy-zc`P;3%1>2kKOKJ*r-DCDIaTdq1< zv#;(t-oRgbMX>;?F^o{M-}wg4gzUI;5Z8Tejf6Ei#pa^cy?N6V=z9XVU?vD!E={$_PL+F6Dg6XauK4};$#y=&6lng1u3ViIgrdFN+`QnZ$o-`XxVCIc&A!^? z8v-n-%}L!BnQl_Nxp^3ebN-<-^?7=t=<7}zt@bE%NUy(Cs~bbEzaw~l!Gu?#MOYmDKJI0wsos#}%cqpN zQlsHt&-+;I8+;=MZLMsTL_3wHqAb2dX7+Wp`*q0s1W8l~oi=H?--dAr|Ib0@fKFW{fSh0Ag zDPDZF)-+&CVzOa89oDL|24>W=UG(*FSKC9uKU#m8ouOVHsczdBliu!xa`5oQj;L3* znKjdo>YL4UrAgv^Q`T?_puFVvCQC0+P>K&^Mz_wle>r;@r8sav)`eAe02L{yRg*&F zZK`Od1=+neh`fH117N&B4^#H@T~?b|A=qtQbmP*0pmn&ujk@_9FHf>j_9s_R93;|{ z+FUVJ^*X<`-6{g} zs(E7JwyQA;#HWeqVU@eN?h@$P$V?@z^;Un+ZusRZF2v?~u?*A{j|HA*lvMA5};p*EMp6x)3`I@^d zk21n$or>c#v=#%MhZaVL2rO}Dcg%OxWf*o+bWdJt*8E|_U>@UCl{~G|EY@dd zI$ArwL-mTjDa7Z9l8z(Vbfjw_6BerG%+23zd}gsDXhe%*bB^0Nqn9q-)caG|Rj0c1 zrD|dADT?}Clx&vtAnw%R``dkOp^zn%mO)=&=LpVgv>lInuUEG1d1>4Hv{28kN$b|# zIqlI#JKa3v)%0Q7fx_KJ=Vku@(cW|}honWP473jDfy=6-2r-v9< zB`Uzng5t+7bp5wqaT!}boYuNtGKyNXCL4#SEW{S7;l7qrub6Lt%>E^TPty6cRUHN; zm=$967b8IK&RpKtdUBvUmbS4i1(Yq@&`@zcPv#ElXVkJ@uwv7r6l4Vg#hCImt}09Yw>3W#Svt%qr4@@0`6+N zIMro4AE|CH*lzUg1dqrrM*OFv+Xv5J*QF(*QpD_lZE-x2Ge~lJM8uT=lXRFMpvv10 z43E9n_Ik--qunE?ZJE9-OyRTT9t>%o#nCS@KNa_0tY%90>@m^49(envW)USt)$ZN9 z;7HYk{C1ELE+5$DM<{xt?}seBp&PVkuzcwuVB&sY*;&FHzh1w0m>aiXBQvB#8XeT`+rwFHHCs#7XujaM72>#>y4mi)TB9+Pd!f$} zS+^~ofV;0tP=DyfQDa#(-trk%)-YL>8B2TG{{W+S*4KNUG-mwu2b$kQ-5FFa!V_!- zYu>wFeFva@k2a?&y<+61k=B&}iKby_w^KAZC$U#rwpmIg_n4@gSQy~MYEEbJ+^d6_ zB5ExR?kSP2LBKm5LuxDAFA3bJ@Nsr~8zODlSBQE{n?7X5I{0l{(bnftUa`7vUaMoe zIl(sM<@-Ywxc03-QPlbR(ovyXN{^jNhB8B_&lCo0aDQ+G^t6o%_m4>|fhhXM*fqZOHbVxee zK{x7}>t_}|pK^YP{+|AeAbw>2Q*d1ikYnc7i6;^#72+V&Qp{mTj>3NFyLO5ono?dk z%*V)hVfUG3>3HyE5E%n0*b$AG;u~TM_dhn_KMm4x(W5~UQAA}p*l4(WBRKx!8a&+_ zvY7*Bomp@DZgBpTI(|_}N8KAn=*4k??1)=^DXuPB0_NE5_K zHe}IrJ|4&9F}k$wVjc=)iD1|O$j{=X6QnV-V=z$tZH83$iVtO^0LZ+xNF?1fr#A(# zQFaiudinn1yl;z5(RSMxTy!e$GhI94t!|J{b>q)5EchiEsP}l`th&NApC6vezgXPy zIfl}WSh86ZPRn0yL88OfDz196D87^s3j?ndRlSu=m}^P!Su3!qs{94Rr)_Q$e?{~M zRvYOw+Jp1~*gB?=-7lXTm$e*Zt@ig1MY;NJz2-8_fd=0;ojX-hQr`I8>Z0Y&cjUD@ zB7n3dF;l$&{{Z>h(J|;-Ky|TQ_#)AUi61Rj0Ap_2R|(uHL_x>Ta;Px zUvVUZA82v74!6|Sr|Q?~-{>t9`SbZj`Q<}_b|5QGoWy7@zV8(nqc(-@&<`-d5uE)3 zC)-DCz5 z)34T5mL08qchV)D`u57Qb{&|HZF93wksTXQHm#;w$05%~m16Ct7+ETwlCIO7OjnUC z9G6vXi93{XIG2M_PFS#SY8u;ALORf8o+4f0qN(!n=ImB_1Il{Cj3Zr$u;*ZN$4{-C zL!83qugmvuOL4Q|PG7!yTAfr{?M`|*kou9V>>9etv=%8l66c|sTxnZfjeRc0tJfr7 z)(=AULw>eLY1TUCi;ljpRG+$zuhPTZJkm1Y=`bQex3Y3VYl^S7RbO5y*WCWC`B_hd zlRwG2n1NMSPL0cCOSZjxLugx=dvJgi@$#TeremVcaw7;B5o10jknpJ;U*4`F(I5$ z+0G?|2$~9S!e*G#G1V4K9H!{^enX_kD-%g5+CJIlB!NXM>@vB}5d@9}BFWPad|z`R zBv#PVa*kI^knlhZ$KUYz@OQ-qaMX9&=X_5$wW`B)mkgK&SwL6;NBTdSCd6MMhWXPV zkbw+{N6g;3SFDR0>+2HDw`SLfcLj5G@>%QQa2BSu7Km0QXmH=4W@Dg{D(f3|lhb*I zY(Sl5$)iwuN<|58ReJDdTlL{YBZlXKwB+lNSQ}FHHst3B0JaqQW;=1s?S=-i)T_&@9*vRunxk9eN+Ah5h z(|2?iw?mYVop58GivXc|OR1Lf=sF4|gSW5VTHTZtxr3i>_GQ=_DoO{N<^ErBZ2Jf? zhn`}x0#>~=(Lt=(7FBmiPqUbkQf1shWD%roIW3Xt+f#5wNwezDi?rjR0!y0f0}Xw#vl{YK>N61BHBa+% zi^+QcBlqan>3`^-K7Ls6zveAASYzP$8lf00x~gYUt&pG^4;B+p!bWS8i>!ZmzlNDQ zz4t_zm~A*a&0a;5=Ir)siMCWpb96_M@ZJ7li9x$OnU;aM@AFLEI8N8E?fU1$l^YEi zV=7PIK*Bt@v;B=D-BG0ZG-iKm@U}w@htcT_b?%Gw3?*brM!1^H4#QT>kR}w+HfsK7 zVTzFquK1JZiia#wT~cu)hP^u=NHW|Yp#b>IgFYxg3giTHt8Eh!%=xdCE=IL*mDQQTF%Inv58 zCdCv=BE$VX*_{2&9X^b=<-DWpw8!P^e%JiJ=qt{VbT3;X_XjkKRzr7)dV+0FcL{uB z%`Uy?#{K}Z^D7t?L1gieM}Nj|&DI9=BPmRoo%K0-UCcoRcVT4xb`sWyv@dSHi2c3o-F>KWM!{Z%~?T? zZZ55JagFBvzQf(W8Cu{<(K3FyIB0|B&o)Sh_KE)hY#EL_q{sW|P_$~9_T0B^Bp*F~ zL@n=-r;dTN)GZQr>BnY6V-TATWK9zyY~P-ZQ>5#Gq*UoYZu(ApvxOT-*(Hy!NCL}% zs3tYSb-wFjXoEUd=H*{s z>uY}6n2S@l^|O)PHXmNJ&qgaGM;tNK^cOO7XEgIF=QG<+$^|1^QE7W7l=0NeBWpB; z{Y+WWp|WYSdC-$$XX#JnZLjI$sI5Gs`V-8tszeTj$8BxPod)8VwQdon`+J&+v?<%Q zW==fy+y4NaJck>T{{T9wv7s*1!*^{dTfK$YE$M=w!1z){HzM>Jl|26YP}&AIDVr<#2Ptoz;=J?ZeL-^K^{{UuICY2VBNCD$TtI2R#6bo2qHn_rR)n{ z-|5bPVm{O%T;w8AV6=RuyK4c6&^oQ@x!n6>eNAW3`5E;tykNIuH%(Y)npqdOYpVrmF0$ASpmm*E%ze8K|Vt0k5~3H!>Vj|l6b~UZ=9`z*F4<85SYxC z%qEvf6`(5%Ob4}8*p4J+ZdV-9YtdI+%4EH@&m}DjF_PLS*l?^m;@W;Dr$JxGEsfOs z-DlgEtF13&_EnT+176Bjv~=e#Y75Svp8V9MUG%LDXH?xiHh^^1S=Q~wtt6WH%*`=o zOIeO?dnJ3Rd%G&2bn^{~4^2+jZu4{nD>PDyy3Lana#L%HInBPBt*z8o4wn4xwfT#8 z5^!%PRVlJA)l`S_>ETQ}R#7gR$M09}yeha>q($Wy7fU!BnN@4Izdv95tD8SG{{TO~ zDXWl&UDJlK28L(B^Cx2(4#6!twFH2}BPgnK$~NVX+Ofyo-pR~ov+>!KIC5q~ouduJ z1L&2j-!;hFj+sz4>fJ0B`S{0|A{gzn<-zkWb?(RQQ+~@vdxl3Lt~OfnoKhrQ$m!Xm zJ@`V0@1CY zQC!)%37){DP>{FPDly9AwonD&IJ0t$Pr6f*v%4>&f*iCFV(gcHsT6u7QA&(TbXXIT(s0AJ%`l3dGHea z;f1Q@$wbp_D(1J-Ah1i_+NGCeD=4LQ6pOfD^U$n zfXrr&lw4hmC>kqG!gt=1Sg!grsef6&UU-Z1r}I%?y_BndIT;nPW#uIX-@zbJMZdgOzwibEhPu(#!Oy8fvXKC|JIc(djTrDK`>heaZ zdxY!ol>HGh-{;xGg(iN{yZniapM2S?5wkKQwhx>2!!P#8n!NT#jIr}%DJ2KtJW0Jk z@SaZ`{8=*&RC%it7zK_fbxD|Z1M-+;7Mc4p(-E>bNjl`QjoE<)7+S%zcr-@9?bHV&j#ileCTUFy0V97 zr9w@ks??((Iwe+yu{7t-cGy|gAXP0P9OVtrt#W5fIw9(|K7oqkxzxX09towg4>ajr zk(FO>w-bwR(0DS)yA#0C_C9V%Vwxdkt_)hdnTG~J6R`0}o51Ilo^;t z*I%-Q*Vc<}7}FLjBZJUOuckz#Tb(O)Rg~GTju%R()OFQW$DHF_&~M>t@+Jhi&aF#J z0A)a$zgvqXm0>lv>dwfDcL-t}O)W&lquPSbYFQB&tS!{_Z24v)FEXT;uh^W>?aKs8 z&g$2s(`t)Q{{S`_{KU98ck>>AvCb^bvgy+sMGCZ4w_{$|Evv}#jKF?MXj+l2t-Gif zQM#k_L#Dq$Rk$DW&-1pAkN}Y)0>DT+9gr$xnH5Jd!^E$Yn-SSfg*p=q$lD0T(TBVI z)xU3>b!m+zL`@sKogyEaaLR0LUgzY`GQu2PIhe{c{n0Rq?yUqQ$;a=ujMW!rc~8;q z+;mRU8a$nIbnMPQn@43XbQxp)(Z4PJN}DsyFvWWrhg4`j&ex-0!a2&(DOjWC+~Q0Il3AdqNfGLuAjWEQ9Foc-Jh~9akxye3c@4O5v*M_nk?&%%)+EHcdIj zV#za6JqJB`KnC*c>Com$eSBTrKTEPyt!BXud|MA?hx)w}ZU<;l z+p}I$PqbGq%C*xR*o34O-mK!Veqd>ywFBns>4Tq|ExUfDxa=Pn53lEDV^F#^i$=LR z0?@UeL)pWxNQnI!)|Kj|EkV^>na&lJE)#u$OE)z)AX4^<9mh}~JEppT?9&3D)6{jn z4ShG$)*BUS@lIZLe<#=@vk_7|c|sh&Ye?w!8uHTwMysZllem)!f0f{>)*NLEl6g~! z#5I<{&%yFV$=QWERx55_FHR^mnc}gH4h7F*yA#gS{S5so=mb78{7d7tioo8CTY-z7 zQan9F2$BOTA&@)1j^b9TJLEaAw(H*)ciT68Cn#QO-;~%fr$>t>Uz)b#8%(WnC25uR zk`Qu*md%@C9Fz~z0-|P97_&XX9!86=4VN@zh})(V3HQXsW2$$WcKwb!NC(xw2;S|H zvwUrGqeR0DiAcb2lOW9}98o#r;lL;1K)%f1fb3>!pXlLD9z=_Y2zkq#NO>(e2At4I zba_R&5;n$4K2S^((O4Ax8gz)#i*8M~o!@9$H~ouq^sm5TY*snHOm=CNMo$8sYtn|0 z-fRj~*4qo|Ym!<8m%3nNrOybc^$-5qknRupS;q-MY5W=7ahsfYElnA@B*;; zIa6JdgCAu^?h|0>$EGB;${+NqJEH{G41S_?5$I}~b+_dKcaY6d|l6o)cGu zb!*SxrQr`LG08)Vzh;?UL(XhzYy-jjv(GY*1fIWRXD41?Ow+h;narQ`93hvDqYpMg zBle6j7||ElEn>IDk+v|Y(b=bI(<%fxry8q0lJ@~%)|SaIJdkHb;kQ5bV3F*Ck?Pn?kNNMHjm3Okk~_n3o;c1E?(M4;i+<6`^wL zi@sE^YRgS^dS>`8LHvcLAwxP*Tx~j7yBhl}##uUp5aS2QI)SXfa6NK6BjKN=}>xrODv?&k6K<4l+Tu??1wts z>XpKp06U|_c+!!2B30QiePmKP*r^(qvpF61TeWLBx(Y@1gRgz>O~blXYOOrjiOc+7 z&fM<>oi)ot60xUh#xnt$>Uki6*iWvJ9il4~<@Wnz!`jvJ`YZaW(wxo1JQMkgN&|c3`x{o>-e}o|k4j2} zH}8tTm-mK)Z`eOe~~$ObaSp8@opxxyoqtpoS{uT;NEW2aQXq79PWI&RUD zVeHUs1P$i_{R>fiqRtV&@3WZURymCZ)^f+`3%cNG=nG4yiy|J>nRij|Kz`T-rP(bB zzH0(`lfsIHKjzm+5R072P;5oSi(Q7TsBL(*0`U7F0wG8w88s_uJ(!8e)&OU%6v7Ya*3=9<|e$9Se7+DZa?8k@Shd%}u9y`lZDA3( zAD@(#cki)l*Ks~TE}UKqCLx1r9LP{4@$Ds(qsXvmdvQ&Os!&5K%JWE44$GFkVX;r5 zD4SAJHSbHX;`^AvhO*|DYOk6qE(qtESzz+XymOEt(?VP(-vkrU0H2art0zyC0@=1h zBRg$#(qE?EqunE0ai`=r=Bin_DZ0oOWLZLNm=ZY*SC~N+2PDAUft+AvLV+N58(~Y+ zs%F15YmX*xo2Nqd9^A2mvoAjPW;0^w@+91G+4McI_By)JtgTzC;>UeyFi>`zg{#`4dj*Ic2*IayIXU6YY6zi84<80xSVhKOott;bj}ITTXSQaz+ole~W|{s*ho0}F zU{+VLw&{a2M(r9m$@V-Qg(i3}%(C_@*&a%bA2K^Y{l6G9{rWVlyvNPhXNDT`B1Llv z8Sjg#M$J2Q=_48>Uv|ok>=Ob?=Oi?kQpC7MjR=%@nP{6gFj6GrvSWztv0Cs+k-3>X zc`@2&eJmubB*)NP&=}Z9(J1|;b6eL%;nM6K4AL-;QsqYZC8;2tkch;SxyS}O&B|PF zP`J>n9jXR^+tt-kwXi^Mmf-39ZI zGah5~0YRG=rI<>NdO3kX$u@6BwJYI_X2&Mj^@WbLUV#7IO;Fr=51Oj=a+ z#Mj*4YGyfjvxKL}^pFtg^KFO5WAlx4g~W6crK^3?{Q1^=&t;S5dbW?_t-oeDfvUY! zf85VYlZg_*l@7AJDm2FRm$TnQn*?%wjUcQVLdRO?zM@P~ou^xkVR3+gUfcFhEVkH9 zDMT(LnvGoc9Qn(roKeN~lhS^G=eMSqbc+TUk`n^P&j$Iz;htBOCL?`?d4){sUfKOL z?^+**KO_GDJgP5n#!1j|S^|7xS{OGmW=*aHIO?7a=0fnI5ajBWr)a$8P1)=}Gsn#v z{nNkiwad}$oOVaY&vD;|t#P$Zz>X2V-@9J98npYjTz@o>(q{+}x8tmg?GfRSgTG$< z2g{L+Ir_DIEQaw=e%H6`J|;hAsYN4=@>Uw+PSG=H?Rmi1PhMK)!pxVUw`z|!8GvOH z_7*shBCoe&20$AK2`!s0O7Y6q&7fwjF-^d3nd)VCmx@6Qp&*k8G&fxrjkRy|m6K;* zVU$f5$zo`I%dJeF2v|2Xxa`-RbOnU}0JiK$K!=gR&6P>`tBz0+os=M0dtQ*7yD_UBlmD`(66WDWEipR2SLV^BotumfSaS(1m${G%|@~96sMc%&P$!H zw}w{pO+ghvC4bMnW@#{U+*C>9BdG(%!_qxw#P- z5qU(Af!XNeEC@#Hc57OJcyFt%gxf|ILwU4~NzPFJ05&6sUP5(G)ZU!LlYOB^@s*S~Zeyx+euxp^mUxYNhul)Qf3mbfq`ON%eq zxo#c4VCh;spR#OtF$ZOwo$?N^69Poi)^ef5WHhX7riMruLGaAl+1YF0k=rBb7FBG6 z6d=c8#M>e!=LdT$r0s{^kF#ytvsHfREPEhU3h|WsbePV%maIV`h?%L#wy0DMOZO^0jSWE353TjR;@q;Hw4Gp(9b~h&#{fx_Ck~TIpG!-b^f5!{xW@g4>H@bFk>u+tSfCUH&%X*_Qzk=JV*HZ z`Tmg%!QrV4`v?OM_Sl+@6pI>YjD$~*J0N7VYm*XIxp8D@)=LO`BgWe*FWd3U&Hn&z ziI(WTZu>7V+2mcZ{mXP25+ae)6FnsJiR|dGJe}}@Ga>!vdp*^+cf+@AnH${`rHIwH z?7oZk{3mPj43A{(EI&>d>xVaHjCFlGQBC9aLtURBh<&@{#ht!5T)dw$F#XfyikT8f zscai|&5tiLQK3N~WpOri;j(>YL@$WuNbxpH0i?VPUl?w?X3xAY#_oW=VlKR;kYAK) zCaq8((AtHwYFaAbj>KA0&IK$v_1$jG4T)6dv90k_%BwY{8)sX8RV`W`eMW-S9C(M= zh?6#aOwzeXv*+u!3Qkr~u${n-JrYvtQ+zl4mz?XQ{BGKNqpM^g6I<+B(5vXwl;&SI zS^VMDwXJBCcG*e!3%IHrjU-7*c>7+JLM&5=y_G-!p!#~ zii*)uJSSEvEg5HTrGi=azd9IH#%djvV$x1@{ku_EFktzhC4@@2eM+s?!5>|w0V-F2efD$DseBbPavnvCW&JdX#x0XpYHl4M6F#1t%x z8AM@O;)F+Wzu)R6uA=W=_x&oUza@V#xSqCHVPDL=vGQW0pC_{oT4XQ-0X!@4)v0;; zw`p7m=+P2xnKN`@yGY&mZkOaO!^p_bbZ;VIpSq-d@4B;%D<3oaR>-=hm(#256SfTd z9Cl%=Y+bwNVfNTRZ-KI2y%RLTV2|YJ{%nOBXT=A48h&!)dk+b@3HZjnI{waH!ke{0L%anttc;Wtm)^c;thG8x(o1~SoM#@nQXV38q_^eb|k07fk%bdG1< z4pz~n+PWUdx4ItcD|y|HQqyFrn*ziabMor@6ACMoIL+JBa`m8G>lk>ll+>kXoKjnj zllx^WBE?Q9`xrB7$|a5fXe*c&KMK*m<(a0mpufkj?PhHbtT5{vQQrguz7084TB_E1 zu)z&t|Cq9-wf<5ok zsp&T@T`Kn>S2pwyKU~J!Z@|iy1%ayPutxdw@4j69|pxbf`z{uB-hT?rMkgr}D3j zXgp^Vin88ZU{C{!oQ<$!DF_JV@gRfZ)v_HsdSWB;z*^d?k87P0diSa&N{q|;(Ek~cC)rW(6htxsd1hbbv(gFy;z_a|If^}OF} zQ(nz+O4jO%O~s#d^Ru+I$5(Gr)cH<4g~mawSK5}21!5H_c;nDPwa6s8XK>6{n+13S z;jwxJ?t?n|f`?&eTUJ(O(LgoHQe9*a(z&k4c&x23mF$Z;zUa$!8rDe9b2o5QG$gyt zUasXHaXFKo+Ca_v0M$_unCO(784bK#R?q<}JB_26?IO}wHaUE(q{tSkM|Ig`(_%V@ zmuzE_t2-*nqb)juJ(xhC;!^q4cNU^hJe5e)N-j{a(XR=U9)bq&bZ=SCBnvU5fOc3* z+gR+Yq7EHR-hJoU^&jUi<`)pt?g2FH2`X5vSothQ6NeLq?5z*MQrJ^w9{|Qn&pnkM zSpNVd`5OrvHu*|a`}-_%4w0OJw*gg)ht0W+vCFZn6~=(xwRsrcIpMWh-$ffS(nUB9~vWovNiG6 zBNqj^W!)E#MY`ENN~K+{=`y7$%OUNspd?sf_ifsFeXHui;|~tQmIggu7_^P#nu*tq zi`CtV0>py4P%nbdF~w&CsKUc)PQ>_g#Yc3_Ko(HA{3XmKFIJ0%$N%}K6t14xEf zTpAUi6jS-Z1+v8F@JY5Eca=ZWwdkk$w1vqz7VK87Z;Ink2g@scWYNy5yyv6ytCp&? zMH3<{q{x1hae^s+X{F*Lu*2zli))6rDt6AAv_U3Z{{XQ+pjN7P=g;Mr5>qV*6B(i} z=5c>qGZ3{*lch-Ij+z1cn_eVNUV8*W;& z-4ZOFvmi{z?Otw&uNfb2@Mf|(d#hx5k|J*NMyXNs%h~qGJEXz-Z@Obh+4kLeYKbf9 zFeT?7&G^L6NgZ;coE?t2z*&Tj`(()eN)UX0uea%yGXbW=k0ivJeRTM_F{Vf$@`ov( z$g7^8k{3`nl_>1%0GNVqZCRScsf&7Z65fY`@Ak#&=g^n8K|fi96%nN$tsUA$LfXS2 z0P70)wsXpE3!@vt$+gCXnO}QfZL>kcaKUv z&4N?+vqv{#dg3BkHX)1Bw{8b8sYyt?okLAp63eEPy{9@Xi{#FznAolxp+_ z$@hIA(iO(PFvVFI#1soP1wu81lV~p29aw=PpLHW0@XCQWw>d;d4w0@pHFsTN?Q9{_ zyjM~(U5bGtWDSqVtPD%>ccB_tA7U^s(6`0+#TGseOH{qNZ75dC^p8MuwbfOT+VTTr zC9a1^=DkvVibKfTBJPQt&3<8`Ox>QxAF^&gXBRx>M#?VlCg{<(**T5X zz6_jnXTC;baL!KMdv$ApBWamKBl)Y{n<4GfCQkhk^lSPtye8?>8aCr_MQxwAU&SH~ z(>_MM!AxfpNIC4f7iO~s9Sphf=Fa-EaJHcbRmnjKrdUEOp#h(RlOoA4anKlHtMRTUbWP=WJ>tl-9JFH6%Q&n zh7{T@3S7YU8U?HjW84b)l5JAjL_A1s`H!3uP>3?_lIuBx>Z^H|gC zZxwqihq_&I+N~@P78n9Ht0r4&)70D4+btzT4USU;E;g)b7{1*X)hEI0vJ3>d>sHGA z*&*)qUs-cqnQzq1?$*66*}Lg$JwEhbHxNer9>M| zo}sYoE2(dhx+9=@{WboGRg+}b;hk*cI)dlGA!j_(t4op$n+aoC8V_KO3r?oEzm&`< z8!NOx8J7$S{tO8(wPXGO4Gtm!zznsZmJ{h~fRrn@^gHxRtSR0*{Kov!rz8@`*alaJ z>61DPc427tI@5EKZE|gtG;oEa!}oS^;Su*aT6AdoM11s^*gsy_`Xol3u0DFU?Ag2Q zV*4W*S&5PMtvks_(JLQCNtLfw$x*(DQZ~P0?%As%a`lPXhUsxO@AOuQ1ay7mzoP1w zu2ejsO_mx7JZ3`A(gLGSEvgJ{8#WXXwjmD($(So)0!v!GVcNZ%)(0tbq?}mI;p%O2vejPhi*;W<9E~r)VT;vC}?`#I}9% z5b$BL}CHj7vcj_7K*a8n_y-c@JHC;$hUOe5oIdxT=d~tw<0iP(t(gw5OnPo<`T?52#JBtLB*y#0Ze@b)Rl{ zWc?xi2I_jZfOre?p0S*N0_a~eR_x-phxdxar-zf7$9Ox-OYh zVdtwU8l-62y*^gGdUs?(7MQdC^G3W$N(;MI>9AvJog{g7!9XEP%PoHC)r6d3>6BP3 zuXlMj0cVEq*BT9rF(j7rA{w>mfJ|s9^A%^nip7fIutoABz4Hd-?M&brF|!;jJ7VYP z7aQ5q>2!=eCU?6Ahh&N75%Vv}CFq0U?< zKnl+7fj*37FxcdGpqe!gEgt%y;~TV_Bu8yFfWWfPB0gg`FeXYRJSYk7q704?5=+=> z&MntYnso%eQgx3*Goe_z;@PXMH0_fKfqJqow8Ld2<&B44*9<@|dh*RhfI#7T_41`> zvIDN!TVUJJ?MJl6K{D+%RnAZ64U;S*=xi2>e=y$GeLS+MeuF}@YWoFx0~spmr9Y~9 zz9HnJjuZ=AZf)qLbxR|kv|SVj=qEcxbY3SrK(hkvcA8_9>`gochaGU;Q5b!TT=@D{ zzAXt>#Sbh=fuWk=Y1yW9pi(l9-)xk$@wLBKJ4dF{KRkam{{S~>B4XIv5HdSjA2he< z{0@;Fqs#~GXPOB$idhpS$k_F?IG*y{H!u;t(@>9Qkjkr(f?-4Z;xx;Ma^ z1L>O}h$FfoNO{YOP(InAHOTiV@pBiuc7j@P7SpwN=7Hknm4PgPmYAl2jBOy8Jos{Y zRV8_Z$Xg~3BNYCTbHuqRPljwghh{e1mhVAXwCBT|NwHA{+oFvBxNu_VEyM+HXBDWtS{| z!jN?;QAv3z=aR+8wJcK#W`_aa4%k)Rt1{d+s<;LMam8g#*s5KNUdz#f+1CyV)Xj{=CP{dPSjPh zJs+p36Bqpr!}BctFWF;}EiiXyj2)TIjr5&5w#e77VGUw)-}VEw z9^0p1-b2sa{gI~)do^#5FBwpN)L>4jlA?PfaJcd|>6|;`LM6c`BxDUtA*FBE4s@8h zF~)R>n%A9HVDB>v0@_(IC@oE(;1PQWh#;KOx-9(oV+z0_fW~h?GKtv}<~^*J<&$jM z{U2oXZKJpt`u@d?Wkh9}0B8#aw6K~JfU_JCSKqZ3^Zx2~)-8<%cMBYNFcO*}24XO( zq;_COcZbUHA%QT&^1*!^F=%DXdUv?RK`k+dnJ4hz-z0=nCDvw#!CRo?$PJ2j*zh=){@Zo zb0xi0^y5G;sP_(pKv#$Xt0^cE#I{tmWZ0HgV<553Hn2<>`aJ%{w(^ zgx_O%t6o!Zk1K3V+cal}%yEM@X74mjgBN~{*n1`>-E&9#D@0sKc`rsDXY5=G!-My; zwnN_@L>W-z5z!8L;h-~@vuKkb_v_U%8K^Qn*(gyM!CE5u5HTaUp2_2-;#FRB%~=uGz8;z8TP+)s>a#jThf8!cc|w9hukZBX+u0!^SZhW6!b=o5 zhT#mZF38CC6V+^6VuTqZOlkE}$dIw70BMm*GF-_yM1veu89UNtHXBuOu4GvB)FlAzQ{0{VyBf{mo#nBR5P*< z7QZ#nVx3WI)=og~#Okig-mhRyQm*W$x?=KES*~sYVV#s)tKWu8LGgeF7=yxi7uBk=T2;Em^H3}o1lKn3{`E=DM}Y7 zp;B!`04%STFE}M`pt+-wYbq;?iQB`m5Ml_3j|=MW+dym(dK`A$Ito?pI>5O<6|_p9 zwZB3;$MydJm46~XEjSGe++-hFJ`ri*VndyA0f^iUaz@IBJK=Mttwt$e?A@!`yQk}i zwj4duKF7^FG0`s!CdKLs#M3-*6~=i6}bcK-loND&%ul45J$8I&ZzT4d+H zR6H2NFu*MfMwuEWkDffV%?)85S49|rZbEZHMwwdypPsoMAzd?I70g7^K<7utTT7e0 z+hO$8o_ndfOFsnHZm90#3ThKzR!>T5mc~n`Ur-yPDkIv5EAwYGW|!#!*-um?8o*jC zt);B877g~W3=~C$B6VinscYMcAj>Yd(P39wtQ$3yeB$cwsL{PP%o#P)Z?Q%;H`fLM z=T|oG#=wP6+sOrs_>SR*0PhOLP`L5W%zi5;^wRZOc&N5Ip`{g+^<1>^_qPpyUbzg@ zV=ul{b_E@4OBIliHyeeQXB1ZKiF~%(X{0-rS4Iyw}VB0CK8_ zK-W1e%+0r*Q^Oj7y4Qeg+bfDaAZyz9--37$!Wl}S&S(YeHkyguP zG+vWhVFQ^6m_cRqK$_Z(5XRl&_vR#~dg9ouZFk$fv3p!MH$d1cB+-mjPT4S>uB4QF0#K#C@qpCNIm zSV;mMS|r)>GA6p-w;@w=l6jrRDr*@|%VDD~^tBD8QfzirAfC7$NX60+#R#^bidluL zTtmfF?USqZWcPJ#WI=CU+&fC~{{SuYNm<~pWI;@D1A%sNDYjis<8u{y=*Ifa6@&pN ziuw3_&ZtSRu(1-7>S>o-~+C0tuND-&31VL#YSj$8pol)q@ zlQz)g3eu(=u5CQ9p3ThES0QjhlH4*&nGf~7tp*zzQ6?*hZh#BzRCwM$G}-O5VmG!c z*&S#4A^IcwAeZ?;_-n|O&&K>3vm!{`^b=tQuPK5k#*uzxzcAY2V$s9nKXlm9qVL=G zIzH92eC##~k0a6Ycj?MLJ4uZrI81M(!4FJ8!=&y@qs@?&`mGO>3{g{nC*`!kX{R?#u0Y}bYdflD;GA1tQl zD1N+%=09lJk(-5sI9f3o7#?1fCqs<2y+U@w)g~sB>xQAQE9uT|k>rYxofB&W=T4t} zJ<``rt!Q*aw4#*c26M1oH3*6_ZoQBlWX259oGqDaVPL@~AJaV!R&i>vO6afsX(Xa4 zzEB$o)-}T0a&r*yOr8U?RaV(IMpWNwHxqgb;$@@=LZhq5B)ct(p5@aJ?1KqoIZ>xi^moYApw8m=o!{{W2~ z>(LK2!a-+%jp&^%52#Jo7IydxyaihNMTcB;Z*COa9TN~Pn(0OE#YPNTK-*4+ePkqs zOgwb0T8$JM>R<+vI}z=KDHg_xV5dcMExB4X=WAD0Qix?9Kjpp?t#a@{112VG!Aaq! zre+2yl1PZ9{90m=b!uxqh!B`ynd>jv9`Np@FU{}9{spaqn3m05sWo~#c>r5mk+(3X z9+4Lxv~AW*xDjS|>YJkh(xqbxAKuT=vsCTT^=OYGM!5NGh)?Ny^zGHJWMa{v=#i@( zzi!_fDVk$X(GxxT^V6RF+jZ@dG-(ZGA^Rv^zhuveA6|`Q;Y!C;&3{OT1KIg*iI?7e zw;#UICQx4_MxT(zm18>w>>aadoOcG+#S^NLHYPPq~PhC?QBwq_46Qkz!cg4C{4V_f?0`4IyDxs@E!z2eTd~j&>dl@eDMPo1_Cv zASX8}Y)~fQ&){=sRaFqfUYOd==+B4|VmKjwwrhk)exaoEu*0BiE=^iz*2^QX2>{qP zp)*c_a`N?@Y*<%W3(AHpC2%Y5@<8ablrmOzU01QrAg=@ITJ342j94sgs#5Z&b)H`- zHi$cS)>~kNojXHYg-UY0V(YF>Y#~$Mb)n(t)=E9Orqc!24psL=stu3BRf4MdJZ!p7 z2SU@fDZ1Sj&xKsusqfX)CYNwj>zlOt@)@|U)iG8|X$mT%`Re(X#|e9A%NM8k{?()` zO|HRdxRp51&)#09`CHpb5n+)0RpKp6s6<^MXbi!paH%3>O=gA{d|Bk%lWhK={bc-r0ge%{l6UvP`X7{hx%P z9((r96C}mkCM0MwNXUg2G>K6*&WoqXIyVvX$b!u2_ual}_FQc`{*@*)37cR_`(~VG z%MTOH^Y(l%?@7hNPC1Mt7c6xaj~h?YJc}ws?N~+GoH9|o%u8C{TjH4mq>~;1;>yz| z6klgfl)1Bc97&d6Vq0V%9E7M}1@uh3v>F)*pPf{Rc!FhG#JFSaV?Uag3P^(2BN3(W;1GyoY`Cc2Q18tR` zAumZC7Fee#(<->ktjG-bf%w3!B;0Bl&CP@<8x8*eom$A`y56+^ z0PB8T3uo(nwkYc>^E6!P{@CxxmTgZ8mb~C{o#AB55%Ev}Jr@ z!STo!7dJl+=Co=Si_fk90B(D)r1>-RxASlFcDL0(9I=6t^^X)8p)hnoI}>VgqMPCo zfeUs-3MxSGgqUM~od#KG^0Ofi0h%z~FGhQ@fw=#g3CG zc}Z*g{GS#fk_TLR8KtwL?st1zq;18dWr*f7E%$@dUI4kP>1c!+$~^=T;(18HZIKaL z$T3R@v%Jxwa@biAY#?$3lUjK zz^!cdUcNq(*gctUJ93ja5wA)J*VzP?TDMafAb|<%uudHe)>UQ< z;~fmt@~IZ}VN#&4OE;Y0XnZ2XhGD%r;-40P~SXra=d_oIa0H)b1ldB0E6KH4MPq|MkcOvzKVLYEU6x^X4!QMu`pKN#`7oFJSp ziungtnI^Y^j6!s%fXHJT91F-DjGQFmCU>wh-X>(tc}ANMfMeJU)iO~1yCO(AuX8%N zLGyOOF+50^gZ%_&$m9jt%$oph`^X!tGfb9K{keQv7Ok~<*rw{0Gr*>9`G*BE zK%Q5C6Y)y^A$&6!hMnN|;O4s>yuHM{`IXg**fo!)`ckkJ z(9YcZ&J~X=nboGmr)6Ip{bi()wU>aPTNSmz0c^mHRi{VM&URf2c$y`qyrE@ta(#nJ zU5dI@ZKyp1OIDXhKJUyjjknBK<$C%ew ztc7{6Rh(Bj(e`ym>ObuFYiZ+-CH$4)$`L{EisT8ChwCavYLEju;ZG%|AhsO<>`g<_D1O{@O7|UOrD3ObGyQ`jEg8e#E=V$;LDBZvu;OZ%Pa*?^ z@0>;kxEXKN^vFB+Yuy~LqvGejHh*r;41XlbjUy~JV-I0rnnoCrA_Vx`#(BDM?Y@wK z?%$zrEfjZ#@{6Maa3ko1YI3>xOUGem&jf3@NkG|!Mc8Zzq0GaFniY=8gXTuhPKhUW zX-Z;%UU|EG^L5j3bd`^AT=rW{YK{=mw9MMI0yC#9|9>v*^{Az7rY2I zW++m$s+d|jbvnqh)CVs}3MkFHHW!TxVjEY%3iIs^lWZa1U>?nY4bVHNCv9u)p8!1XmfbLqweptu36oh+jkiV%b zMbQYImCHA)MFV1L$Y`fBq9cr3q#n&>uT~bD|sze zZ=el7A!sT^Jhl!s9^DA&KpJt0)I2}bDb`L^W;Qb6gnO|4U0ozlyzR{VvCKhQp{s|B z>k}Smq*)aui)>BJ{b^CzT)0aaS0iihN@NW7H|po=#rF4Jet&*hepYkIIRc);EIMir zB-zy^5(j{7nY?x~z0f6`JYo_B&)Tu9jJRbW!IM8|-(r=~8*UPG=+)=CjB5om?{MGg zkc1`23%gwCMEr-0XvBTX84-@m3@<5IrBFA0s-3wa=R)78#5AjE2DMjwDa`3zEWgq`g|6~;1dChs z{{SR(O>J&Pq0gXkcuq{?zHWs8xCQ;NZyK1M*n>>3L9|svIYJ0;$hsYCGnQ&V$?RG5 zkLz#i4^G0A;spF0@i)G}q9Xy;sXZp^`u>xK-X#WEv9A z$C#?NM}c2rjWn+Puj--$o~A)@%tt!%MVwo6LKr&{hPfxGH`WTW>wM*=xM};Bv{|rx zUlV4rS8OO}1m$tiP1Am179vLUi&jG58!!Tx3x{oVhBvUQTHu`Z(79R&ve_(@pwO9C z;yVNY2+P{l`x?QsFGl51)@Kz}b!w^ua(6^m(%>UFzf)kk&y}WKkZWq!qbV17#JY2v z6d0s$Ayrj&(?=rPD$3U|#YY$GKbw&(Yf`*ax|HClE7!HJJ8%UpQ`OUbOKn6oWuQU6 zV)I*ka=PdIipM+y>cJT>uyb1s)dY`i_jl~qeWKM|Px(#xZ%*cIURi<0A%T%6U~Tbp z_5q#4wa&~odJIkptd7{z!S0(T96a@I$1DmC*lWQs+zTh|kT>nvjL(0XwD}``n6fpnk%WmzP<9#ASAfa($#hvusSV|v{q{{|}D1<1QcXbV48fyz*^}{I&SVcm%@Z#W zB<8D>IJ@S?J$}UAa6YYa;}^9q8<6FnXx`S_jkI&c!>16y~6haBr8YKxf7 zGtLA73RG1H@L6D3qdLyrdVw+_>)~IC5t`1eQ{N&f7AyU{`qjW)jPn~1O5uoM2P4oSd zhs+cQHgmTX5tJpBe_+C~$W91DOcMfwF^HIHG`hnh9)yQ75p=FuKcPKq`zh8(*1s}; zAwNDV^%7Vyfn9y|3)AwU!%=FL0ia0z8!}!ml*pT<;kOLo#FM}53Th5(Xr0F3kMb;X zMFO4PGAjUxC$7-eeAt z#G$Gxypp2MeF9j=hsD$H40UDQd(gbCK8HITEm>Ud+l0aXGY%E3Bi0lKH zhH}8iLEm6kVM_XUSFZhvBG1yd+EX3p zw$p8wy^-?W7Cw73$}x;a zZy+u?EHr7xNv#qx!$BCaif;N2L*?NT6A`=vk?;gOy_CtJb}sBi6WIX62ndoT_`Q)| zhsT0r9nd~xCb|ui2!8ALZoPyflQ|#^Qfwvm0t*zNWm))S!Y?>-X`8=h zCt?nSYj;- zWWCzd909|4K@XSB5e07;MO(*Z$+{=(fw~3@a@e)nQ&|`S&1fvCwH6%vAw^BV*bPiN zL6O!=;U1IL?@MCHT@=Rlg*zU*D8`;@P{T3EZ}$Opqi*=QvwZ>%&8jvHaaW&mb6z;E zYQnBDaaPj;${3Mku-TqIlX|ny*7aA_UoO~bbv5loIQuzms3zwHKNHPZPWAA2ie@Q<~bs)+o0(QlOF{!&x)4OMszugXkkMtQH? zwJqDVMmd^UhG)VUF1nJgNZV;rhkb4JN9kwiZ*AIK$-@5tk{_SdVU$wNDItwSA*X80 zHo=;}k|8VvS@`4OA+n%O)H4l}G!(#Wfq7@MSxlhCCPrBQ078`VC%$`-{hR*#*)if3 z)E+q^diUk0cDbYDeTfYaC(AnwL^5ROrN`g9RK328_@*&8!!sHN!@5gj0(P}8WNDEs z83y&(gZGM+FB!WW2f-}?YhZkshH983=HO+A?I5nl215uK`bjnkHp96DVLuN`S;sF= zuSUiU0{&@*B4$Ju@huFj>`H-fz#9r%u#$60g7lx zsAh@JC0FcZ_{D@3G(y;kWHs#5A4L5cC6$4_lDjXmezpO~k4xha+lh`b8aB5dho_&$ z_Z48K9VTcAvCHR|*lGJb%&^lyfV(Qzy(KiHv6pB=KxR&~7PhgLK`5p{u{BfXB7v1(FwsK!RvanovugO?)P3>echs1&5^ehCKpj%IA%8?^QRkcSO6Cdg+}dz zwp_2F!jQqHGgKfVvXLiDa|Q753^+$Un6oX*R)8z=NXL?2SFq!n5f9yXmLnI@9uxCD z@Fc;JE9d}ZK%2jwNASQ#O%aUcx4~tndVg40bFm1!J7~Q-Fr6bU`EJ8<^oZuX9g!AR ziyF_c?F(MozAX!y$lX^4AU`F=$7XFPO{u4uMuD&loS2HL+-|tI-d@@awJnFFNSi^jmT@SToCJlY7 z$6D3f(#VHZ_>B8nE1crU8slQ5R1Tq7n6>z>?n0+@DIRJZl6zL~>NRT3tu#%2j`hva zt%TUt(%nth{OejfU#oTb=F+%yEn#q421i#e=1>K41tB@yv4mE9>m$TL&Xw|=t63gn zt5`=nPOjjwSc++8!Z`DJX{ur_$|164$64azUPyswHqxZh($J}1M5nnwn)De4vcN)4 zt?;vvy9KKiSS^%_8IB&An_0vHn=t;w*3QZOHvKj2hw7vA)ADcg3aedn^;SbRBYxc+ zCVJ%NT0AkuvR#|EMsudG423&RoqjUlBS}rVC2C(KCmd$!-z*GWGGX{h)gwHj?e`*4 z*q{Xa??>+2?&yW2!p06e%Vj@UvP1R#Do|`8ke8UTYgku3`1#UKLsFJ^c^6Xbz21&1xwm5G z_8C|nRYaKWTs<2F4{GGR)SgRdM=DEZ8B7Sr_T|?YvVsq^lo7z5C5c-4#8UL}Wi;p8 zPHbBNo*=-%o`QJPo_0AwfywEw?+S``Fn6*}T`MRBh>F^}Yax=rgtJyi84CGWvB3?x z)kSTjZHk)OuW@Flylb@xEG4Z3dEVJ!QLYTAz}}@-&P*DtZPD*1UFK=y@S5Xt2Ru4J zAkZvdkqHTo>sLGNvZ(#+h_;04iO6M<8kv3rZPDpz1Q@i}Hwc<*qlcw2I1Q*7PQFU^ zl1t}$`jX(9s-de}0w}f^g``nd6N%yW%4b~Gr2b6=_JR^2xhBDAD!50jKUDt!Tsoz~ zgW?a!zt1{^CSq9O!JZYnC6h~VH!{vsz_`(KFxJPRW_d^rSp*sM#yJbayx!D0FsTwX zOWzaXz@bJcKzw5((X$}viGe8IPuva(y<1>KCprh)W=Mc2{;9$Y zF#?Lb*9_XJ2s2nk6$O;7x=Wg^e)Dl0RrT(bFUSUR^9VCO-=YaZW9G5hH$|e^Xx>(h z>D0_s(Q4RUnMPo16|7^C!9_>yuvCIt2qHF+iGgjT1FshKMspmszZf9kKvvl-xe^pU zba^5m4*86J69!;DA#y3$yI8KMY@RUvu_cY33ImuoJfNFJWv}gujj%L>bmf&OhDRLf>F21(f+d+(tOK%G*tCjSHek& z^>{aUjpk$s!e7u)-qhfxF?a_>fGWReF43_F)^)- zaWR38;i@qh%l^;Nq6S+F%h#p^AGe(J6#oE&&)ptY8};Gt8YV-XA~fkxVfs~n4(>%w@35XW5rKz5YWSq6xM`h-x@KY}gcRKOK_9XS8S-Lh8bbom%{FZGGU4IR zDFlFN(IjZw8bG~swfPEp{{V!KF{gTwcHnEf7&S_WV|;dKn{~t|8V2dhQdgS@h*t1# zhqmh3I80-B4At`#NCir;+IAK0UfRB|8&9SU*8>*@3^-sk5<|+eFn}|rwQMsWlyx}9 zh_1`T;+E5k3odJ0p2w+@X@D|O5pLqlg;6|lRH;O`FN&K84buSj)wQ=PvUy06;R(5G z$PW9!6<)Thfd+%aaCE0^CEtj+)wWnw8BZ1Y8x478VEIwZ%--yy7YQ~F6-hBkcAb6{ z=99v+jIDYvRr^D}gl~IJLt_H*WeU`;x?!H;Of6y$&f%*FE5*A>~<{gj}!wMPOo<|f+tB+C&wlfefYIsHl>5>-5n z%+&7P(o!Z7kP=zoBWBnoanV~J_fT6DpqM&-NBS?G~2AKHAIHZkpL)4@tR z9wrl^eV>~?_buD^g*~WzZQJG?2qeJW&}10_t%Y@kX^VV+2?5viEwKrigdw&-FOTKs zL?y!9j`fPUeDqA#49Fq5cuVP~6QP?M?~IBWxCYf7%IRsb`Uq|H&=yU<((6DW>C!vN zDPfK^FEW~Et-v26wax%>5Pg}lY1D$NZ5XKUYl^qFGggQ>#IWki2zD|(3R7&AWy!aF zayk8QEatW1@rpcaV?X6(Cc*j;_>5LP9d0P@PI3t_pcA=-LbPU&4jVhY9tia#t<|$z zkSv`8X9nBnYlu7ujFm;ZQje3O*H%qyUJ_pm4yqYo4}&sL3w%{hQtud%K>n!mn>&OH zTV9K)e3du@e>wB-T~?$(+Wh>>z6qTqL@V3%AJ&o+Wp`*RvX$m5MT8jm1Hr%^lI&as zZ*QC49Xo5ie#dF<2~JnFcO=$ymnYMkmn97sV>`i|;lBye)VyKB7BO~c-Qi5a%##CQ4Ic={W(GEq4I6Q7LT8N6Et>Q)jL{}W zsF8JT$8Pz-ewmv5f)YWpZWXgo_Lc3%V?mZbI5H>78wHR-gwl~@$TI_Q`MAtqAYdZF zwjt8RGEY@(S%Dd#CX${2KMxaYIL1O{-!`Xt{5745> zI6J`()(yhjYMj3MS@y^1l69WIuG&|z7AH3P`P0LK1lfXHy#R{I3mtlj768b$ETA}6 zNjl0|cq80vdka!~577fOLbc*o39iB1R}5V&6sl~o3n28twkLR;WMZ+!3!$2=zUU1$FqY`bka?!vV6eiW9UL!J zq`6QtE!!B(3upxm8mqn5$@NVuT0UFz7a8u59UabeF&a0QI-Sa=KxffqRx9`AZ3(p9 zC|>KYHzT`z1dDAh$iNcm@lSafRJTLlKa3Yq3QgNixGy(d1^ zsx`b<7&JRpP@Yp*6XzcbIiWmh2E{`m8eolK%`K0*KUIH3IuEDm{ww@l{KZNNSxQ8_ ze%5Ctc(WV;ud@6j&6$Dus7Xy?<*k&3y8>Sb9N(lvIEj#agf|`LV@27Qx&04bKW^K! zH0F6ahl`B^bh#t4mAI@)qhJawU}?_){UvnHh|%*WqI_L@=kD@2Ewbce=Be7fCzLT* zPRvmVGRbJ1me9Ibnf1~8PK1fP`qULDUbZH-t_{Vc z9fKMOR+n!T`U<&Zn!Sj_yBasjp?7RN>-Rk^%Y?vtozy5FHhxSXUZ?&YUBd%PLzj;5>m z^7=VM+cpi=Tii6nX0CqYTV;_M2H{o1(gkXu&Uc*{eIRA((Nfkn?aYhKhAsx`WlqfJ z#VdZ0vg@J`3&w2L_>rV|vmzt`OJmo8BJ8O;M=FqU4gY zs~c=8ty&6u2&J*l2FcgIsQ&=2{RVw2iN7B|CM%*Rf-LN3mkC{hF!v&=;K`1RQ#c!k zxhj?zu-~b6IgccnU&b!Hrbv^73`XjbZU)`3W@IN*EL`)8vJ{!|;Yy8}yhdx1KS4ct zNyliSQsHkEk!a!e`Zw)`zD ztow0_%OO@oeMTiljTNndvfaL}OSsFQL)}&&3j6~kP^XC}NC?tG55WsEF~c%SyvRN! zQS2pstP?f>Ucs$U_N>&3YMM@IN~*`eN`qVQ0U}A~p?<~;?@K-dZ&+f*&4yMD7}qv3 z1%_U^iq-K1$+i`Kt9jJ-cR%%ASp^mHD>|}ms%p8U`^S}TM4H}d>h{~UmOxz9S{pY} z0OiJ$F=>L9Ek+voEl{Ri(-B|kZK(a-&i86?K*~;yj6P zWq@EPnRaw{=zr}GRApH6H{$Q)wQS@*0d=viMG?sam$<&LGVs$nKCc5Cnds=Sw#e5v zM6v&X-k>#DGFfkEtSvxaER>9BNKw0#FRvlUEocC0e6MMnTER3LojZC~ zIwwMqzhLtzyZ-=C26SZEIizen@0vw^p62aE|o0 zCKTmOxx%j-)my}{)`Z)1`9r-U&Q)(kFjRZG;js?yt0)VW-Q)h_=U>v^CQD)|QlVC> z731ebP?Q~mcvYPRU2JEh94!iR0m5->I~~CAepJ3|2}=DB#`XQD>^Jx%vz19p-UkOVvIULRkH~IT>yf?!TA@L6$Nk z$+3(^4KLh|3)=>C?J;qY6VgF|mGrQgn|6wgE^c9Q+H|7)L%IZS^xDSuFf2=~pgkZD z1G<<(8W;?)eT<2PGC3-68Rka`CZgdm5w=S6d}7PG^hGW7sGj1@2tg4feVTE2CezkK zn~y;h=+2g4J$v0?tErI-DR{~&*v$*A5tZV!$=<~%lyo(x^;cwhf#%370s49j%JABY zkyKkGZFbL#V2!)8uWk`jmczTQtZgK`-05AY*|%iTO=g85#dBO{fvD*O1~ILxJsrf) zjjz>;-L20T@-&+aJvA!SB(Hk~;7-2DMWvYScE?m&-0iB`>D~~!Z60-ds%So|VO(gi z`OKAh*UG$tbEEl(tE#@4q!7Bnqmfm=S~=YsuT6KHsQBm z-1Ot&4B+SPlB5L4*X;Nj=K3#ui6f%#!&HIWW_>I-vi#geR6Qc(MV%;d9OFS})F&xo zFx*6#v6*Wtye?VE&^sm_dPSDh7G9HsacClsB$Qk=#e)%yv;ihX?+8P}1a=CFKgJ-Y z%EnxKN+INEa^;bRNHfaBRuEGT{XKhl+Y&`$OGZ(otCAtAK&aBEn_mRY_VXjvqB8?A zU^iJed%sHD1}z(Y;Ax}c zVzf5PuqKy>4sYCKf#Q;|Te9h^hOx>a;m}`c;?BrlbP@Ha{UJ%Y)~>ERnbtLqdn~15 zs%z~N1hLkZv@nttZ5a#052GpCTCJtg)X6Idv3oL#Upc$vV>cE&?`w2#2ZeQU0-ZHK zttz@7vMVXHDlb?!zwl36ZRZ}eon6>A4TD--4m_f>bvEy*?P`L+NQ0h|?GrB0PcCZB>C#wDFR5=#El0og~TRNOWAy=M!uw zTk(}1XPHFQMASJTwUyE5(REArEfKWLk%S*+eViQU==7ek+w{pBv{u41wQnt$s1hOQ zpNzP1^NEHz6?h4Fiv322NNtD=@;u&N-Xav{!^rt}jc(SQqgc%lVeXyiT6=NX)R}AR73;nON2p zp=H=T8+#wKE+Q`LAyt}${URZO!AU~i@AA9U3v^Xt%c*h9F+C^^sOHTkGdFu8gu%tE zKw`qN?q`e9T(3 z@dWcmvij|wc)Ke$wI%2O03_`WQ)I+T66vYA{?HWI7SPnaL-q}0zOT_`*J-wiD&E`X z+ciG-qgr`a^l?`zwLzNabypR5G26W3W5TVi1_{pGH`{AW==DDlRXxzAHse7V%lyy! z&&pQCsmQL5fa&`y%i57;f_gIDtCrkE~ac{43 z5Kv6ELZ?CbHgxZ7BgP;`Ei_;MM;sb%?H3>#lDTVCO~ z?ij$la|_#8S9OrgZo`;H0UmZ!ts!P@7aKi; zqdIkfzettT*IE>XoHD{A&4y1@xlSN<<99qT~HO|;uR_d+4Pyj1q? zb+DTjt?3$vSfVTxr$-H4SuuK4($Or{%`bok$9P?v3|Ccg;=cC4H(9UG>*%E^(696x zw;BGWDa+>PmFvxA#W$b`-<;lUp1sQ0LU`zEym}kYD$+Su-!^pxN~K>L-x(d%&idu* z->P{W`P)dbBx>ei#Vb05u}5<@c7Cej?V&mnoqfLejz?>Lb#>jl%Qx>eS9R|mSKOh= z5l3y_py>iw9W7QTJ&<-pF_i7&05us{*$kQ$geaqGV+MEE^oidXUKJ+*erSGSSE(}C zAg#`sUc2d(XsvoCK`Ry=cF7fUWYp(*(Nx|dZ9Hi!$hd7kxCXey=IYg=Z?Shr(K-en zZi}*JpNvKoDi4#qC<0#im};4(3F2PDfsTe@Q8M_h(RwuJk&Wx~cFW3I_meROK+ci- zBFWu8ILST^Gw^+E!K3H6^8WyW_b`Pj22n%Eds!9@3fbEq*{R3u&wz2lBV6jrn`mLzfzFtHK=2<%(dN+C%Q4o6@kD%i0t3k~a`Z96ll4^u@~ zCq>^saD7o3@`7p~t|vZuKwzcGT1~5U*3<_9jjZa9Nrja(paQh#&4(gLAVDRyV9=Vz za4Hm|T4n*3uv1R=A{HhUrP%7XS~@>7s*mT3s&Ix#@TAH`Lh3qx!>vf-S{scG)I*1a z=bnXVyneBBN+n|sv^`xTRDbH24?RRsn zW!Cmr6m{cu-B#}3RGMcZZ>Bk~Z9Yh(ZV#Cj+?P!56lBeKMh{wsqaZZNHO`%2p{n8G zz`dBVuL8QCl5Twcd)3`b!~X!Lsyx7U)}U<(T_ercFA&lUT-RUzRy`r&>n{J0TJ0d-&8Wp+IF z4J48n0|3}#?+IY3_IqA50S48tf20QCrb%c!HR$okD9oZ~JZbm?w#kVYq@6M&McE%* z=~|%boj!2bdyI$lRKFq3n*eF}IY*!8zK1jE5FTwYC{&oB_sNy+-4a2u3X`yGs1h}j zfJzJrvsWb(8cQ5w?KKcd*`WqW-w*8fdOvrBVq++N{{RYxBG?qTmHf#zP7#Aue#7u4 zA%x&X(6J|G9?BvgQ5=9Ui`q?h{;Q|9nwj;)zGk(mg< zzOV@F*D}XX+^McTqSTs_r0^#z-l&eXVk4`JXviaR`t->N39RpnLF2SICi;U4$r4Nj zUt%3ab=>a01E3y~HX%E7O#XWwFyz|Kecwn=zIaEkdFY2mV?_C5m8^OLI;er#*-75r zpd7^>N-`4K7H@J5s5J`L=zq`3R>A6)s_&j*Yjd$gPY}CiRQ);3;(F7ZZ1qZ8uprl8 zq10rm$@1H%=3lg(vwcw<+%Efr`Ehel{oP!1NE_Dj=T-X_g6(LW{iAi0Z__bc6-^rLhO_XK-TkBbvEL1KALh5^zvc=pk5`WLG|1#NWi%J1 z7Ub_3{t-d@MRZm1dOl0VJoj#?5as=!(qiZF(BpIVPD3#BEhVQxfLnPmV2PN>=bBY> zlayJII75NndV{wsb{ELU-(vw$cZFfl#Hp=VR9{ z1_-YvOAT#U)II=DGbxc|j=JYuRd3HKAn|;y&B+@5;>!-2*rnvMkWnDjiBgu?QQL;s z^Rs>{s>HFXnrgy3YNs()wLOcuhgBKVG%fP~0LV!_sKHLwLT>hK6Pa}-XtAIoMZI~L zyYl^<5wya8J>M7&WHYCoMca1gJer|8wlc+sQ#@-hj9qmvrBkc7xXsfkkJ1X7y;JOG z&RBEk^!h)#`Er`*iUn4bec;Z}NoNc_T^jDcIhoy7qP#7NVU3E?93>YpIf={`t=)8=vN{U+em?GfA6ORT+zJxQA^@Q(p0QdGTvz}^ z3<^`L17&rpE{PhUin3IMBv~_)R;;?}13PtS+`^Zz;PE{lP$)dZSeKw#iuBB;)|dgU zUnRUGIISVrj7~N596?)Y0PK`UTW6YU-iO;@bA)Ebx=pRbI6j8w=*nBI_8M%Zih;kP zYe6FyJAa8J=zm7AL{k%5s=lES)8CtEzoFH|LSb>CPS@wRTV&1K%LkZx9IFmI)PMPaT@Zh3`wr7<1VtI*CP~Z zui>*7N@B5r?@blb7eDIv>eox(+==<^`7`<3O*mGp=kZRLCLr=NB1$?CF!C{H0b$;? zw4@R<2QdsQ26jVa&)zmCqX}giERs<70+%yCiNcE$Y#9t2*5FR}Eqlk!#e_zP$HzvI zyXm?#iIPw`95rE-V-l7Od9PHV+Dby(CL|I+Yu&7vy))sH+^AP9c8{J!Y>ir`kJ&C9 zDT2;y%S^mRZ7vMH?PDcL8ZvB|MMFR+vTiJ?dyMX1Gde!<$@1iGCYlV4Uvhzc!FTmi;H<<(p8N%qn8t$`4HK;sDn-Ku^?_VibYimu z%(#^!Y|Tl%MBmIV@(taeupg{Kva26R#@rb!;k`w~i;NYnQp|RO zL5FKQty3v*H_eJG-I655%zOiLg6qddCA312Mjm@&dGviUa>ryyv!ct}C&M7J@SGBZ z{m~*ROd;U{UV7!p2`}w?_Uj#%?>2jcfbooh1s54vRR_*AX2 zKHVY_xbjG^d{|b@_U06};Do`x7f2FuaPb_(>@DOLY@iaFx=LuBBSM&r3Vw{e+dsm? z#IWK3hQZ`V=*+vD4V`0M_Kn!`{cpD3ey`xWlrg1Uz``;DEEV=UZ7NsW)=ii3YRqF! zkMe9SVq3^Vu52~z*`Y1zIkzP6@NnNVpOWG zin;FwTB6k1U%4bXV8f|wmQ;y9 zW1C+KI$spg9)Y8eu+jZlK+!$QyU1&Oa*1lxR<*NON98W;v+421dbJJfT_?~NUm11k z9Y@uiY0ipPmo0S|H4jZ9Q9UMr;l9b{gVw4}xyd*7^kfA7>f$?vi9?yXZ+9C_R%Yxf zd6J8jbwt}w8tnd0-^n1{IN2BS)oO@kjfD0)5mLS~xYn{N{OrHVEAF!SV%7@0-&ir7 zSxUX+A>Gxn?uh-B{VQwj+`kpSF+VHm_EJKFv~I?7V#BGFYb|vvbOQQ`s6QN7C!|DE zTku*uH`v;AOd;&3k-jIRKXKI*ynVai?SUF3oHZ7TN*J;mB4aaVzr58l1pQIX>IFQO zm;|)Ofj)H567|8DRsea@qr%osF!}qK#$Z3fTQ!paI%60ey1q9IB)q&PBQioqlPikU zlQQ)~*XE-?3Me>PUE1RiIHSD!tl=z00PiV;2blO~w9AaO#Rho5BA5zne1n0?!SYc+ zQpLk+0A*>K)h6R++Z{1vv)Oj@b@nhia~`y^`dP@o@ZDkgw?dcKU09BuVaieAW`t4+8E%Z&XcQt ztyt~KQ8oOd9v`@-tsZ%zSCMr^!+zZvNcVP(iOB{S&dBFzb`#^_3Q*h`fJl}#gf_`JLY_)G_py)Q6G2IPbWcxo2N<@%exD021cQP_Zq~vk zIjF`qrm#VhFxL2bpiK$5jf#gcz$g|5lW2l9F%k$=2f+o(CMqfIdYxB8#oTA$?1N&= zicZ9PupBPSW%}{6-7Wp_4$f*oQonHuOkDW?Cbb`ohm}Sd=wP#>JLtY&NwQ9@;-+pzC=^U1?y7=9&bp za?fRCQkF%z+!qTCV4yk${*hiOp1!{Fh zsuPi~M^_|E?w+k00ko60Iej;_bx}?f$3h!>; z?Dg(!KJ>@FbEN8S4ujM?GR}2htQpQFT)N%~*1At9zd>4odw8?4 zGFXi~LBhSO)}Mn(pO|&AMXpeLSLPMs1}`X3<})5V?fbPm0Pf&ByDQW_ML@ zU#mT9`dQqz%~Sc6`Pca!&rVxhhFAKUqT(W0`xd?SCSIR`3ru|nFewYkp^E(*bhvW! z-;i%N>z9mKg`tdMg@#e#<2Mncc0>Cu_-~P~P_6TIk@PGRaPyD%vNy#fKN2xx==yvp z64^xnIU*)g$3STn5PZ3!nj&kOFJ_oVe(-dxHOAV*pnMXVGM%OzjhKD4j39C6^z9f^ zZizu=5gG}PWO(w3YJ)VCGFqWX#HnCmHn~aHH85|OZ$20v$(|9CBrdt~Bt&yn`Vp7I zy&q#(Hj91FdNx@P3sO|fWd(nAfS}7)vNp6M5?!6W&oumOC_9RY3|C6vA=dI%wry!e zK;<98InGcqFiZkl7=W`6$wM2wXte1rH0CxL6}zs4yye&Ly&uggSG_HHxO1;dBvszH z7ge0B0u`BNz?zJV%jKV~8yf!YBUJ*j*kF|Mhp{c4wNQq-hzDOmkv&vOY^iNpA2?N4B#?NG2iL}&fu8OEwY@Zrxh5K0=vbxN}c&JqP zkFfh^@C3)<(g4#Y8y6>yQejKo7JKj=aoK`NP;=BD7POY2v&WjT>YPOPuk9D;$4^~6 zGseG}JPFR2u9cjm8MMTq$|auByu@obfVhDtVGl;LRWKQ*RuLw^-G`)h!^#%<0rQnV zUiN|Oa&Q|GNoZ#^3YVfLKW~YrEt~N@5M)Lu8{oj-J}29?!k-^*f+TVSuOb?v%EX49 zLP_W+2VVGQBoRN<$KzR)pi-A@+{ zz}EY(SS9_gniQw0S^*|tNKy4i2||4gt8+U6fP}&fIXGXGQ_Mb z)gdgP&nIX;=v&&P6_uWf+T~6GHRcN@dh{5)g2ToFg@-J%`P3Tor|XM8u8-&<23;7r zhTjaq8HFNCO&NFA|t5-(OM*G1ZY;CXOzk#3-$_lsimCn^k#cYhz7(Te81azfO7v=&QKP z@=NmKgiW!m3c4QOGYnL^Y{POBx0Yc^%MCApqB|xin42FgHBQr%oP~T$1kKuXdQ892 zA?WjN5@2s3u09hm@&w#s`Sw^!VN>%40WzU`wm!|jO&ylsGK0CDUv6?Hfx>j}F$Em? z&^djoT!r#-rDY^&Pi1SDhVMsdG!>a|8AG-jjTT&ZyRgb$$>LX?=Lz2$ZYz5-$|l1A zbx1V=D_+^U2pvX>%aH6=w?Qv8a#_)`*T|m>H%uZJYK!I?4;_7Iv8-zb_Of2COHHQ2 z8?+#1C;{i}kS~0^r|Uffbm8zsO0orHS#oxy@`z^|oJ6L)FR)j@9HmM`Zi+ZYWyETQ zCE!FH!nmljO|I{HzPNX`kQ&d`_wd^sT6lX76X;6{n1a%|Xw^y&MlmeQ&Q2X0N2ndU zD@LIxg;wk|euS!!+V$SK(kM4BrRwLQ7xFCW#&n&T*F6dOPx==~tyZ037N^{=&`w;s zaZ=p+p$T;K1&-au&%dAvNQGz%-ZjB>ai>gLR2L?4wFSSW*C$4(UI(Uj7YH$2(-dAI zCZDLSp0r5ia&v0?divyv9?GZ2n-O6ERO{YK0LTNh>D2K(0mqcP4>59==7Ab2$wn0M z5TZ4jk7UE9;GZ^00>~SqJTPfOnjqlT4L(lJ@4vrJy7Tmhq`AYNKLo!t>txGOPh2g~ zw~BsS1q{Izqr#sMj4&+vZAhpo+F@>y@V&aXd%xWT1`^gfPJ6aY52{ws^U4hPptxJ( zPr?yggztqa0gT2;ghPHoW_=b;;&3(nvsTzMMwFgJ5aO9d9Y0=?O#6e%A&7Bo5%e=e z2;wz!D6E+aFCqp|$;0<(4ELCg4>mA@S)+_2?h-|oh8U@^#YMu8o^YBY>XV#?z#F9< zM|PN#K2mhKUcuR-Ed6;4WnX6cB4M4fy@#%QBQtM6SPz{D>T{!R2~AU##S4(hj6-T! zGYXZ148_gP2^I>1q35J&a#q_WSEJ)uY})7x+zBir(t9!rKudMs~k8IVE&x)l^0Sb|arS zbe}!DKcjEzPGL&w8fhqePrc`KW41@x^!B3mSCd|ZLu z6tmjWeX-(z6Cv9G1-=&09JO2hG1|YVAE?KYIGgZ4hq=YHQz|-h*%Z5WtkI0TB6`>f zmH?QhcFR*_PM55dAH0#Qgl#(U!*+Z1eWFo_@xIwIcpo+E&(C1&%Wj<*9|TG#v9@Qg zEoDZ{h*l)bgb~v_c8EG=L*0oiH4odaNU1(CFHgjzQ5vVh$lOi(fV(9ciL|J_aZe9x zQ`IL%z0!iVs!kZk>hl!U1_*O>>om8M1VKbR5%E<=$in{iV;6!OhU2`j;Vcn?=cojo z`mmL#$YwQfq>@>iE&VPiLd=A0euS{y`eEk9w`}RQny@r{8~1%PHTeZo-A+Vxk5t%8 zL|bds*LGwni-y_|7wq=M1K>=tuC0)UMz%DwUXE}v>!UZYQP>O`>hyLcmVVzNR?Ct$|_ zL9V@DxYL94ujx-3KE*(WGo|EmHQ#H~?QWFv19KubQ*fs|cJ;`KU#lBFMh|Wcr)_D@&}x(%&e+(9m1c1#5m=48}&*tzcGsh zlbBT!@Sx0eljH(;VKzW)ChFHC?OSp4cIk^dW&I>Xd88(4jRb)RF^NNm3SOMN)=N1! zj`V;%W@6zuvgf;YNP!7y-1TP&ne47=hO2LRxPn*3#rqDc|pcKHJ2UD*2u zNJh((nJl&mitJ-*;vEDQtl4K(LrApycGKz1bnTyS$3v~{f(he_w6s&|y$C~{E=P`P zJiyl9&cxf=_|Esbd0B@0<36TDt}GV?Lxi6B?>hA(^bWNZ%^#o_ioHkFC+IsY@>ShR zSY74J%>()4ravRh^9=?215T*b2BkVxM=XwPed^9yvqjOJNl3O=)=Y@Ji~0ziCGTY7fFcUYGE6rW2uhQKIn!r8|8bnG@^ z0*q>jz7A&hM$s-{G1G8h5;tK9z+`FJ6^8|sz+dERFm_M+Ah{B&Fj#N+w6{zvA1uhoYKxgH}N%NQ!^p8NaUs2EDO!^ z!{wHZ5rIs}e$93TG7DICX*&M;35IC(`oyM zMcAA5hKYGDCu){qeATB?u`ETNmt5Qh%>{MlYsmG8r&U!~*=6J=t6sZNv@33orfrJb zO?2GeKUGZS(7ZO&)k|F0b=DRdHk*w>g(#!UepewU^b4L4s*llFsC|a*NF6G?=h8fR zVRgl2MW&+npX2u4QjOR{P?n{VRBo?d6)#491nw%wmit&69b(9VIAxFRqfb%RE!L%4 zb6-LsT6C3#YH?N9)qK*=Yt+1oiu*v&KbSnaif*LREW5^_K0hvlz1IMi)6sQUcHMPjh2Y&HrqX(t@lLAT79 zl19U?N9;KYH`=zy+Z=fOnCZo%YOOMg#~ijy2L(a^};2@ zYahQu6m1iI_Fsy{wKatrP-y}G0Bg)?V#qBe2F@H&sO$rY;&q)Tf9vm*)C;-yRI;qNRRQ+4uEVjx*oy~#2kJmoCkSMIA2Y) zoXZNlyQ@Z_XAMgv7MZj6F)6IewrWQbo^tO^lt!LSin?;9t_3=itdk_t{T>B_mV0lK zW?JJ9BG-073WoC=TpPG5Ya5fhW{dLa=Bt!#WgBgW@+0?ARU~hmZt}b<(T+Ci!RpoJ zNmW%ebxXxek20+eceX04F{-^}LowAn2rs89Al;W#*IRAQPwt=3E61hk+jEDB_3eMe z7mB*aP*4b+Bg@@7@|dk*a(@h8j9-%f0G&0rX>snit&}?;8(zP~jqPJ_W$?p%QDQh`=6wSfbPj4JzK)DNSvy%KdqESTorChbx%d#5? z^S4#fy#>vE9wxLWtuA$(J%iN90_-C!d~4VB9*bp~m~k4>7NeoN?`j|sb`^!k7b}_^ zO9!8ev9z14GoU(!3iPV%-f*_t=~ok!pKNh$pHdxtRqIZ8v~~3-H}$s5N1Ncty7ov#{UqSU&Zk(h(Rpf?ccec)Ii9(oSX|gqaG7FAiO^CMOrFq-jxwIcXD#YP zlboP^;V3?6+-ekEnm0|0uiptpA0(zDbQRH#a6H*JDKY(npoXk5+2%g%iIF%u^lbWU zVo>zqCm&;#4Lp<$TDM6PJ5=opsWlJD#_{DC-QRR8AXKRfW4vvCaC)4_@}(vX#y67I#b0aQW6GG#Y5 z%#7FNtPYXwKiNOjCA3Zu{8Ie(s6bi1*1k1-Nlp`B#hnHH#72_kmt&J>#W4DFig(yg z9lKt#o8n;~?V6CMLT(?UGUkASU@Q(i{n|HrK1NXqMhshILmCHW#w>BM zSuuvws|^tMZ1dAmvR=jdC`p^emT?V%IG=r}u>Sywt%l5i&kX9@oH(J55H`6hR&dRN zdN5_W!2FSuZ0Vsju8r%@U@_w}3f zkGwAWtMMO*e3n2=-cFiF=Ff4=-rci^IokQ$_((fi2FBe!gt<47m*1+W)?K^ zJH*b74}YIU(qcqtIqdpRk+I8cpJGegG~TfUbmjKVA4<|{CdiRJi?SgiK__A-e1fpp z2S?C1G9G;>T>Qz*2t#3*S{Nh1jUOlxw2~&h^b!y!%Ja>eiYuFzB)o?!cG0pdd-}>V zXTBe?*9wOZlh85`Dr&R!$HI0}3Jlp{PFMMQ8^t58!HKp7k}kfp5E}7YHmR-21>3?3 zp;-rgkq8szo8rvHP4AChwYzhkW zTWzE71YwgPSTXAWjX-k08(So$ONv!BZ25R_=TFbClI{9s)Wnz!^uc*df-6b4b5AiX z^g$XGTEG)y)os?uNvd`OaLWwMi&xm4fke_5wn*E^`Nq}1mOX7ibcWhi)1@rsxIwz9 zi+=qdGQmD z_#c)Wfw(c%EVFjjh6`8~(yNMHu+`1?wlxDLF;|`0Yq1RFW3|O~=x@+3+&;iN8}l>q zuk#n3E=rKYf`ID;XmE39mWl{z5M?&$lqPKKNP~->?bG8x!!6omVfKu*`}P>^+vHrf zNW)&;pKA6$vOJ3?7@MP(74AEyNXx-ecWTBy&v(b)qtY;<=OSL8wMN2ZoVAn(aOtYsdxTM2E6xo3zHq0~%vtfwkko^^n;FkAzpq$cwAY3W0>x2-FaAG*8PY zg$qUaDe^~EF_u7XrK$R06&P+_u@L4I3piw3?RnI^{ar=qiri&K)2cPITaZoiWK+E!E9#te<3+IVM!z}u zhg#BfZ&{p`Ky*#~lF@Ki`Yi)tsb|f?`OdO~HjHnc!o$wuEMt_twCJ_WS6g84DSbOt z(^viTM^(K|&H*lU8tLt-wyv!1Iqw>`m@ek4fLWg5XRke!K zRyw5T+P%;OH?y62kqA(XDoN)m&ew&unN_> zEUclJ#w=}ny`2x~p{H*VjtwgNcgiUd_ zY4c%RghJH_dj>j+iN_oY4j}`4xJJOtDFw3dv{{_`TEViJ-fR|z_$k}ApN6>4pf2JA zCrz0>TxFGvaHqm~C7PcO_w1SbFh+@JqZpj7Z){yNq=8`%48FC}bD6?;xc*^eQ@C2i z!ei+QlDV{=4cD>{PkXH_MYd4+HlBhd=A>ybI&d(ST0?RJf>8z%6(B2>M6IeKb!Jy> zT-4S^slNknrA%~(A=+xLD@nIgUpc#xeW_~;Es)lU{Lyk>H6;M>ez39ZU8i~5^cbf< zUfpN;{{Yu2vUnf#687g=93OFWhe7hHmGXk_fUfT(omY7Ur8iu!{MNVDD7>xGg;6@D zucX*pBBj^~)aP0A!WEu@Qoc9TonLb~@(<>!<*E7KQFA?dy2(@>vuIBpaMh4+npTmW zW1ZKc#;F!wnS6{kG}{?kw{o#M8}&c+2fsS|^4pC6031Ip*9q7aIM?NAHOW`W*;W>E zkdiY(N&9piQY6WqrGf_lU6%fg=Q+PFeg94AI8=)ARS@>YpHP8WvFb8_}%87-mkG49d2+5tm>G-jdm zCCP(1G;qB|7z_nP8{eHE}z3kGbkp9-~Ia$j6-IySIBoPL5t@GnlYL~;?^akRt&7!sLNqhfYx>z zaqgS_$?x9kuy~%6{7wA2s`t#5?36vF)}OqL4ftZQ1?A$cGA9iWe_EP6b1uAqu22Jxl+O+I}pb~_$ zyMk91MRmp=DYMBqz(+pz!o6C*=wICa6nJ)}SAsD&jNZmXC>x~&4fYzWxRpj)(1KfG#a+DBA;jbg(*6pCUSE~9wwD6RsEZV* zEM|J&V{tm^pX(1zpMxcx#69s_bq^ohi0f|8l?9hg@Xe7R2ni%?NqoU!%1d*ur(JdE zK9Z`d5)+!m*{*zF&?0DVn^u^A^ZU7J2a5esHpBa)Y2eqT6V4Phy;OdAbB7%s(*?&h z9Jdy$TVQOv(!sN>oKu-k(*KC(-U zL;)60V90LLp~lCLD5Q+^&ln>qCgRQ=t6beV&W{>)ox3CXE=G~M`5xFB+KVC^ZH7 zP2ke-C%W{jA`{azK<*eUo3iWj%>YO%JPD7n%S2VyusY(hhK|U)@s)E-6z=Dy(x|I3 zys_Wt)cYxhxpgLi?Wib7I_ISM{oQn(y&g?Yg|=TR_ztnF)uNQ`X{tRY6AvB1rq zvwWy#QVW|X^TiyLKrk@{sVtKElnSeN*AUOFA$7fP<9%geWzE+6bsl>Qi;irevBN7` z(k%3E5xDp(S0L5@0G}My8Mic+UF%j#I(aBIC)iw+9@pxDa_*3rzFE4gbUdvw=DTnw z(I2}%q?aS@LBxNIUz#o*q>8(GDGHzFYzkw}mQ zkDd^OSah!;NXIWU6x|{`)*y$Vjw>HmqqJl;DOg@-5)}UcqO$FWlvzG{ae(`P|pDS8P-jAUD$gn zmaA^7`oqaa{Bh;mHvJBxVp{Yyx<2j#K-3!YBwtG z#@vN{F5_Izkt)SWt{z7XklG+M*Ibr@d-u{X0jM6|a6-VXHmb^~}{TOl{s} z57~ZC!X}(%{r1c;i|nsulkhA}`?T-UGEb9s!}H&DZPmL*sRUySCPYA@a)cO+WHzai zF^M=U99UkIgPudB>|KoOA|^rOyy59VBv}BClQ?T8 z?>1%;XBm-?SB&h5Sx$`}1AKVSD>|3Tl+`m@iRZ-(tD7oposf(Mh*ol$**gFidP6$B zh}^8Xk+pBJl$w+B+qYw4`7*1LHMfLFv!YZk8d=HxTW+;t5L>UDn3uj{KpI)v5t_I) z(3Iz}UmtamxI&$&*;7_5ny9?nD4^p8edEh*dhE*4hZU;u(#s;3kJM$6X`w$+ z6?$DULvxoH)wK@CyF$1WKy{l`4k2RJ;4LDGi?D1!DuhQkubWhCK7wef#gE^gs^6kr zuXTQOaA)N2Dpa()oQm zFe7c-wm!@~AUl4zn`P?qcwD@hE-sy$M*2_aG04e}vn_gc?E6{B`XK1p?zuQ_&tzPmb7A?eODo*DeN~+jMP{>}j}Z9zsa}010`Fkdp|Gle-8+m#1oxvLv~J z`}?dheUh+$ZcYjFMIR9>8gDETBS0g*D1AEtRF zOoxReNfxwCgpj5n&LbDqtp%xZ%NtLXs!>=(SJ~TF8pV`P+;rU3@=e&KF1EFX)e>mQ z#9;hjLmF|TiuKcbj8uDalenlSy9_~oP_V9OhTAz5kJhBrzmzN~+t2g6%SdRzXEs@U z>4lZmG-CCXrt-ikfd zwH#e&+$Kv3xX4v?vN>9#u}oI?%i1*pnuS?bypP4);aj(`%-+0mGbJ}<#@Hfh#sF1u z3F9NU4PYEBBXL#sO$xmo(toF)q&>ONoW1$s_^bK&hb$O&f47H3XgLjNn_Lxk)t!ME zhUjY`IZ(#+30t;B-y=LD$Cnpwi5^am_r7k^hV9-)m!s_UOZGM~e4DG;Bl5~LxcP6M zJ{;U;Mu{_it-IjH*_2&-bKM_aWcNVOSbvgt&et#&yl~Do{aSX)UoVzgbk2=FV5mj@ zAK`f0M`sAO(8dN4+k`(L55D4U3%V`8_R-QBJ$0mjp4ZGLz(Jv&r$SYSU~q7RW6;RW1G zK3L!put|i1F(DrINp!aO*_gyeI^HX0{{WgPwue|7n&PZ%_++fuU~ee6!5|{@jE10X zV&l$SVRaChV=iVzTr}G`bspYYXkFF4y1Z(^V#!U`TpFpSzV^r=^y1GakvY(PE^t!s8^qg{4K6f2b~$ z#VGawu~c78uV}ux#R|%olM1$nTFMoOaEfBDidT0$BRztg%H(+lvZ)rU^lnY!9%oc8 zegi;#OhEt%KlZYo6c?}R`h4>x<=4!Z>m6&=*zdHvfAufD`_9AUo-zDc;2NMAEUW-& zT1#OeO^YefLQakJwpI0w7#he?!m83^NYR#`v|%^QzWOHp+CJGU4Z6Jd7+WRf9$u|F z_DGEyY{+rEjVoDQB}VPrAK2e)!+yNR$Co5ul5sJnTwPnCMwNy+PBW-~gUSfPU&E&1 z^oYL6jqK4SHf9*Mo!H_mo{0M=Xv1_edVR}a@vKv`jTSH4b!DW6ad&8$nVAHn!-&8; z2*O~?5mghoL$0%-*io0z77jSLgg!PEFLuA(+J>R#FOJn;0W$&wEVnq~<7IoV5W zT6BD`;pC=gC^@W>Ub3NPS~CSOcV(AeXt$6o^xzj&7OwQev0@^YYKlc117p#rlE4>`*f;B1PRg&g>l&vUz_~k#hT zidT9gkywRRL&=cT$zJMfNNa`Z1^`4D4uBqN9dOh3b_V1G3s~$uOc2^uArMn-4L$lc zk~J0IS%RM?1ESRZuxzS8CF{KhSyI8kmm*yk0~$x3EEa4u$NU8YcMKZIqb|73Jma?$ zw_Q!`z78NwZq}L!W@e>X1g-Rf7l$zYP_%O;gwxFrjFH#xy>tmM#m}X(RqX|~Idi?X zqtKkw<*+;_rKQvhooh|iC2K%Qqc}R-}e!sraqFy-d@=n-9 z27FX@$g=jr*%E)zoey{E)w-!SHi4@V{T8;PNQh~Uo zz_sOq#rFh9)p=rPW9PQ5G;7`L;+FO8uxnC!zOa_XZ&hSEDK5{_p6gt&4A`-D2S*QP zRnqMt2<@K54#))T0sw<-w)Ix6JKS?$fsv~+1+81S>uTKXY`Q!1JM&BPZkcU3 zi(~;+)ePsl)s4#6>@C|uf<9S(1F-1(<;mTqM0k?0)9#tt^k|!-e2D&IDmF7?;k!K; zP0BQw`tbLD%r#{zbZvT|?;#FesFOC2+)!Mo{(0=c^G1-@hqL_AC(%TG_ju`|32ur$ zT+DFKd?|9rOO^w48AG$r83%@FpBnrsjm<0ZPbd=!qEr`c$q|IJV+RAmjLy=;GZyFz(znkc16VV~1J4tI zVecb$$c-z&l1Kp1a5s^y4cdc?cdf5VcF(Tt1mt_zSiqpJEp-7xQa(SZ99V9UM6+XB z+;#ZX&VI5Mm@9Yd%bnUND|RyFfTYaNR9_sQ z64+p*m2Nsp74*b5e?QY;tI98*{{TPHQ9@?lF*nD^H9JL;rv%EOA6;t^jdKM+Y$~ov zfp}SBMD_lFeyDz^bmG4>ayRCO3tn>G0xExzp86CK`V=KWbfpCUfOI|b^Kv*091?hhM&-*MOG*hh!X zg}z{&F*Ei|kpznC*Q?>AhwYd@Q2}fLAP|=hK~My@44)u$xO$6^FH4gGT#**M79$tdvJnqF`2!t-wzT&bYbhY-tGo`3X zid!W1vT~Z1LAAn$epZ7u-TO<Cu# zx?gObJ*h2_jaJ`2rL$STEZk&`mEQO-@=DV@$*JQqIu)1PHIgP#ER?9N?a83t@!Y#7-(ic%mJprI zg_&sga~7oxHbZP+^Tse2+n^A#hsf^&I>D~twJUvrgMyXYbV6Nfv(e+Ip4=II_iU32 zEtJ8FaTNuo*{=+ggM@m@bsFdEwYQ`ww!rE!wLT^Lln(&aXx%gx4Sj!5nX}>DvyB@EUROnsg zb(3NJ6z#v&ZnU1Y!2bY_Ka)yCH>nu15;Td=Cb_CO>enC<8RLwRU0K*#Ht8|+!1pba z<7tVBDWf;}uT~gJk-j$gdPr^aY&iIRx8J41&wNPuNR_9}bE9p?Pn+WG!ZBzwwQI*; zy7J$mPVIv7!c^$ch{unf*_trJEy6H1GvaZk4%41+OZ|@ce-PE8UeTLdwT2T1*`C8E z7wxmN8-{sF^9dp+&Co1_>Bbk&6CscolW+pLDBjgCS;++R->}n};{>e+>ng@)I>%$& zPgYx$=cW%8hOccWz83si@?T?F+D}fnCCf$GHa3i=gWB%Du-F)Irm%@k-gn?qT1AO$ z0^jihnxp7$7;-T5SN)-nfr!nybZLf|s;8_ib*`eUL**Qd$6Sgn64FicTqymTN!B=%&_Ki?= z)^++e1gG0od`WKXwqmOWr|8_?+t)4i4Qqdaq()Zfs>RC`MRkR*2@gF(l&mECTV-q<##_rBG67|TP1Kk^T;(SS7Uyiq;)eIEsI*Y+>H&^F@Mz zAf$wn2X+gL(q^`ny>A|;d=VzlLAL4U;cy1pY(Q&thQ`f#Nm38d3KY;gnnV_*0SISo zWE!mbJqZVUw&aL zvx6LTHnU5Vozi(m`Mkj z7}Tr{Eb zb!w5P?03bRhPgUrdA1JN<)ucC8a$7*?8gGZ=8TyJM*K$YWz&wxS__ZYM>R)`r9xbg{kcZX&^tZqPZdiJ z(UQx&1+OJBEh=AXZ~_>dc%Z7L^%{?~=fMt=73!S##yvZfo9s$=|NuzVOL+y)z;7?%Q-@*>Ah}qB3<hn^^vLmKpRm~5 zujj(D&q*cZ0UF@yIrL&u!Iv4&vvV37`h}cJ-8wY}*pGY1Le9dQpUG2PzMH3Qg{@O4xAF?NGE|Wv!~! zeJ2`^yNA*D#Y16NtdlV3`~JAOgh*1M!ZX_Z?Q&jJ#12hP6Hk76RovaZ47-T6h<5dG zuHhsJot2Th4yk9_RQiUQhK2`wLkiy@ou}TtQ~Mj#t+giy{vGihEY^>q$#grovZ_Oi z06gPejcGWysB7-p_`D%(A7Qg;2;RrptL>UR%WPdUecLVuJ7Z_NDBowdOs&x4doypN z^&5evOo8!a?A^3ff2C!-Z0~c-cK-l8AU}N3vSa8a5*ckijVdhkL@u5rtn83TSE5y)$S7^Ic; zv@orUm$a-cIWJ`81UHx{9?mlvQ>%qH1GJHhMu5qdt;7gdjUsg%F9bVW82Re=I`!09 zGb0K?5b{R~#N$}xCJd|^;&77-8(R#nN%%k;6hk-3D}4hxh+uX|lU=>n_XL{ecF&}o z=c}CNmP}zM&#$5NKR#K!^LamWJ;|+2Z>T6><`ZqoSD;q93!PQ^dKgt+tCg50LNa8x z((DT~O<6V3DFB7$gHJkI^ppcYI*W=0ZXQK{^8|LzG_Q1pJ ze#UpPb?n)vIao!`CxoR$*XEyo-5`FMQ+JQg&!-A02zi}4vCck4{E4P$!kRU2!$z`t ze%TfYPb0)BqG+jkg)lqs3+ETyGWQ6Feiy6K%d!bU^HDr-*`sWj2DgXHX#69{I2uW? zQnAiHsas-@!(NShwr|At&peZs8uAprMi0c`6f_`IoumnxYIU(}au)W$vw;{oicQ%!+W!D^Q9&_+o*%P0NcN(ohf_{%D4ggBV7qZLyeQTZX+TD5i#I`% z_132)rYKrlBiFYAB=JnkaJQPLF0YU#ldwajL4VmjT+HDu^jN;h;yc<1S+(8F9|e0f za$-|$jZnXmd!q&QltpU<4?tKP-uciObFJ4lfG^L>>o6liJhiO~&t?oLfhD2Jho1mU zt-2GI`V~d;6WdRhjk`7b z^eH$>&_X`Qc*%u~yyeEx0fIJ(S;K^SAFzpQ8b?;b1xEd_KB>?(M`ZYao9Bbqo&_U} zGq^e6B%{TVgt;&@GOUm$CPW`)`I#yYX- zwjfiWK_t5Ro<~X2ifi^D93&bGA)KP`yN9%Dn4P_25%{+$T8o^`CGXgzESg!+q|39HH$5n1f^yP zLbuWZMB=S#{ax5Dl_k=_$2Y>VE0u~Gg>K!sbm+3wj;-QMUYT2&{z|iIb(Ll_~ZwXmktRLvvkDS z+%y~ZY@dvSxdDNnS{iRNC1BOgTF zpkjrEaZCHWOx@cGZ=Wl6_=CPEEKHH3u-Pj(Sd)~-M8jRDV6eKdvsx^M?we9)mo}mk zR56)|Z(G;K@5F$i4m!5PG(bjz#rMI`+wg!y4+h#o6r!L?htksrCDLuf;;}#=LlyQI zZu!>7#W0MZnWdNAel?N*f ze1ry;^_ph>L^#Zl7eo71c87C^RwUahzn*M(7ckO5Z?KdLF$Abq?HM^_>0aMdEj@P^ zrMiU;JEf7G7}D&J*9FmA#j-3aMTUc0DawoeMN3u&+ogHR_l>a*hj9>0Rgk`i>3heW zuUc~B8(k_2Y=HWKVkS}=f#YH`TyvE z+X)Dbrj*)Ot}4XnoeYc5CA)w2>#{nrzJvVu{BHcCPqT`yE0+nCgeJW@OU6aj31xhg z1`*Bs#c$O<8R>rL-M%)ty5sGSCtl4P=KC5nQV{i&%jo;C-4kqmpM9cMofACNWg67B> zHa7M&Q?c@jP(BUAfXmBi4WKub2@udTdj#$yFhtD7@0}^S5gjAv$Cx00kmLc5SJ@cp zJYbW{OY`e<%Xyn!%&nT+X&Ld<9@ThkQUw*7l-KNNI_5+B)>s}9H3m!x7@YKWnY#&& zClJZ>mSN)zdk8ZLuU7Or5f0FuB51b2nF!9)u3-~!Nesst5cAVyjf{9W_H*bm%T^ZD z2+{PAH}z)83~kX*3uTorSTM@{RnKmAg+&d{Mv5Pz(?k|0zEyi~QkK||W?|2x9$lv> z?z2^kQI|E5&k(K@b-TSYQZ1XeVzQ?}a%G%yRhqeE<6V=rWAwE*VH}?Nl^TPiYz}L5 zu<2Ur$&_g+HU0XP-b_iO^+8zBdN#OFvcot*L-wjZ`5u|-?s%z^9IK+)IZWDapP(PA-=RG_FPeA< z^BeM-vU4Jdl<>zS=vrnNpzFC;OO!I=F!a3L`QP+79W`z;%29v3X!OX*JRsBeTKR7b2eM zlN(t>et6daF3IcqwQF46GC-nIi0hpC;k}y4vFw)3w%bdKQ{!tn{MvzONt)3g6B-JN z7G4%ori~l-%=2|_Dm*F4(2fT3BMS~C7n+xkOo1Il^B6tmf_bQ}!su2ic|6CQfvbFh zhDsu6Kdd;TOe&Mgvec37NKlOk$HDCZ84%p!ATpa?J`~7E$LsqEeOmtjM^_-ltZ_LL z5*W;)OWIc=iH3r`(S{o#!pzXs%bGY)tg&ZN{S=<7&))4d_kl)o<{@^?w1${@X44SKj}>`Wf1P7E;;$4wBlPWeto=pIeEP9QR_ zRJSmyucPS-)TZSnn~Xzf^$?unR_Oeqplb!`1z|@WZ`SrY8l$PI+9MRw5Maat+GYSv z*A-?GYc`>cXa4|7e@K61I@^!=(~|!H5x9IX2c1b7ud@&MT=<7}03yT+DYe30hxTyj zr*nx5e$9}w)#)wTv}dDUJLBz}7-2m1!YAnIns>q7Cm%HBvLR0*dG777eIWvFsJ|0~ zv+Y|6h4Xfu1!O^w@fMM5{z{(+Zru?k?mVVTRESaLO%IgVlcMdyRJd`q=c;A`&63bI zK%E5H2Wl=9b7=8;@sw>_W)et<)M9j8WssGe8HeM;IpTaF(z1cKUXxpQN}SB}z~^`~ zntowzb8)aO68o6<;B{X=bJ|W^0wFV(J1@QlKT!kP8;8!_SQ;_msB>pZgviVWYdCVi zbgsF|6fR7a*0s6iqQes+Q#OD)PwWbOC`P62dj{B3WkIg;i@sxeJ6V*CZtJ46pDk)c zM!X~>H*7>PxzT4%8Us0NazO_D1scL|L=xd&SV-1VvXkOu$YgaHB~|p?4s*3@cI{R; z1s#pi@!e@s!un^8>F%atu9_|73$(3jScR6I)5g=+h*N3*H$BX4MA?=y0a(R#B#?}geRauB@j+g|+ z@lR&6=%ik2lO}Rh-Lg>9N6}cLk0&fa8MfbReyD!7_tziu4<`QrK76iCIOd3Z7MVn1 zO;#+HqH0AI2o{RCe1uFzZX&%iEuUeO%@Jf0`&@XL=$|Gpv2i08UVlWxDcUo~cDXRU zlskDXUU?g4zcuSQ8(z5{WfyFJHv62-a&*fLkKapC6(<5$$ zDjtJ!Ax-)L5ngYBFFldA!ZolFDKViFB?2GK>hUrsq(OZUjBVI1XPX#w&&yuYzyjr_ zG{MrQ3?wXr6AQbm(R&iib9BwB0caH;s4kq%*mMoOCb3x@-@1nctkvivmnn@H!=j>4M_yD(B%&+mz$2Rh1-dHLu~_K^Ys8JRyS$Oo9Tv%1737v0?fixcPLL9x5SgBISUdqxYF;vEE3hLRc7Hp~ny}iu+uTxnJw$!TRb?aU9@HY;3wb&I_ zlxo7Y?g(IHS_!p-*VZEUC0chY4Ods*b-^8SOjw-5YsO&!p)(6Eq7QXZuZu$K)rNZl z?n_hS6V_)WaL=EbiE)-rGcQ0SN&>9Om`BZsr9osUxv|oQUeSmNHxxX^Oayd?aekYA zymYNsNN{K2e=$nbp6W8t0>%`1{~uk((3 z1<6OjU}`Rj5^*?ebnm_17ucd9mKJgt`Pz9Q;kiQxCj-1_^x1Y=l8@a79LEOewPRxD7%Op%lktTf8Wfx}rLVMSj~Ppp}M==$DLlnaw9&lGe37?p^xF0C3vX-jefdsO>W4YHfuvFj3VIq2>#oLCAH$>Yb52EeF zq&%f!H(tMrWsIp>MeX!C$Pu9sorayeWyg^-1?+T5%YN9O6ZQ;AEZ_i=p9@)_+6a3X zyj7;qQ{;`G8a6p&tc2hMGZ_3WZgk=LH%^H)cQPe*6J!~rhC$mvz!WQ7`5$8@af!?= zE%qy0<&5IM;M_*kZFFNiSZ1tb0)$`*yBU8MAFFs#4XYY zOCmVQ6|BkOEpANp<~@o9ag!f85D1E$v2$R{Re5%P*V^BEeMsaD%6J`mrYycIoL-YR z#T#P0ZUMG*rMAPtJ*g~r<-zJ?z}fX24Pr*Xg#67jM^}pnRQgbVniaH$)-U z%YvFY;XkH9tp~ZP(3^(ZHDgP4F3bg{!qlzLT>#sKa<2PhQgE~uR<&uW=ypF%l?pix9+UPEZw%lhm(#+?(r@EK#>|N;-7>t@jO=0P*gi>p zm_hnP39)BOhoW!0H2bG`;xLKEgDPgvVX#J&yDMB@BSf7qY%^jYXg?d%fJgZ{TuJLC z^=QD3fv3lo(I+^uXNoHb!p@S=$hKGVG-WVZ$t$@@DFe+OPmCmlQn^9h^fPjVcwktj z0t-_;i-y9?l7`NV#jjYUkEBJ*=sE5>;8Mjl;`n;kvCEldv=3OWY^EDQ{ebB!`?KQ8Zg!!o5IZ-u&St5!^9`4 zpk8$KxQN1HxFo_Xp0GyPQPl1IHPPz*%6lUvVk*X>Z3Vj@Sn(~2m489Mb1>`TGLvnV zcFBg8KF+W~Nv}G-vb$J#%8lr1NrRWz(U*=FinO!PHJm^!Z&j6j)>XpwGj?5d{buN{ zQtjroZiy90OLIzgMDjpd;em?OL=`TFr^RzWH1L%>DS88@^%=A0FK(T$RvLCp*0ve- zsR)q~!?ciz^Ho-<%xm_`N-$Qe>5rQWx5wz!KOX3znd;XPGju zknVv;`yWPwE^ zNA-|gfd2p(WLXWAjHe94d>wK$&xf8d<`eWZIor>U9quGXWf{0o>?e3aHY1DTGXn8- zJ|%4QFS7LOm5x%yP1~wAB;^hFaTyS``YIU%USw>LjkyvNsvI~!7Y&@U&Lu2+NYn0) zf^0U@?_uk*Ee5dC$bGpsK)(;}7JP(o1%jV94kQWh2vKt><_61Ykf&sdnX7Dxu>CSi zT-4tSqrJsr3K_6EbHKL{u!#isCB_#8jAjl5!cV{~T4Z<$V#65T7OZh=nxQ3cVX11( zlHlU0YN>^=>kAWTjZ&bOKrrY~LUS0Vr0MKVXq9c9lt~8>?9esT>lI{mVADS2fC)-T z9Jhtm_IjL@SVM#gx1~bW7A>MUZQ9ei6*1HcZEeQdSfmrzy>ysNdkw%&crEmvS{ z8jaBFH71+loVGb?$-Jw{8fnhKskvU;w*o^D4#TM!@I(}r`ocuw()A@!)6YMdJcUAt zp5w3_Iiv91wX!0{QWjuZD!K^a5`T(CNJPebaxiR<*Wb}j(&-Bim%lJSJHI#SQhVl# z`2Gt9gD~W@TuD6PXcp`@;+o(`lvD;fxBCZKJo1C6ju7FL!$f_vcWj&By;BKH4+|pN zbqwF0&$P;kFvyUz{D4b%z0x3gC&?rH+bmy%F!=bXQy>c8hC4NnHHNY{Eu7Q79A8fVM>`B*d&O7*^ZJDKG&R09>-u7If_>Ev9Ji( zI5YyWLr-E7SgEEqD>>M`n$Xb4W!VIs$+p@}FtzT4xCd#KC!Ipw=7tNb^vv|g6{)0N z!Oj~a8ehP)!rOG}D?S!@V#P0IaKQIRoyCU@v1CDF9ijtBk|M>MI0aro4H`Gfa;{{v ztdWLTE_Z!?iewAnT(m*heRnpbn;1<}-EdJ>Lxajq_R~~Z=vR)_zR;(s zR(ltWF8*e8Q|8ZHIhOS%1&D1ucS8d4Nk6U4h^pS)Dq3@`XVd)rh*sK>Tv!!*ik^Vz zBIdTv5Z!a|+9a+VbmGn)p%)((y)~h4=hVU`^a);aUmG!)B(3YZgjpNb7y%D8C*Gv( z-u?X*>Xz}dC+0l9t;8Zm zmI!}6hvaCmf%}vg1QV!gVgop_A022!> zT6Ge^&O#{#3l(jcM~w=Vr(x^#-;y z`%Dd{KFEw-&v|^M?h7xC)VhbCM^cbkc_<~#OxW31ZmJ|U>GA%pi0lcUn5?U#Vp=Ci zq|K$n%qLdf}8S1qEemmwx0w3=-P`3u)K3}u2m3Eq+B8C9kiWJTfX2vByRgh&yko%g?NJ)r zNV(Vq+8%8;Mx)HE_6xBVX0wJPuI_?7hGV1-K0nz07Oh( zXW<2gIvbLSOp0!f%5JaRM4} zEH$h1;aBqdr&+K;wR(wZ(>vT_tv8Ub&MZ2PIt6TvfQ317#MX%v)bY$N{n`6;Z=yS& zmcKNAF~2?+s~Zy)D1{I205Z!|ix^~~?gIlyXx6nVMkht<^XVJ53c#l6J0nfdB6e}0 z%aeCB3XI8YHW^`^O`)3w%VR?OEvm17~*pl3o{#4y8VEHEmlHVzz zbw_m3juRrXjvFdl7U*X;9*3(xn&9mM!7MpdP>iHT4P%uTRDmH{32aS{{)qJeXktJI zwv5-D-SG_%1o|M!TG-QSxuE`Iq3%sxsJSU(OKnv7!^ODG*b+mW;3b>Xl{6yqA4#s_#l` zt&OMImN+YoE}46&`l{FCb*^E!X$p$dtwlk#K{Ji6?JZowk=sG_5D{{#icn*^S_>4Tx;-7n^%_SzAFVR!&aDkB$4DO zXYpN)VCCR2jr#(VKlKzI~2*cMc<-8EM%g*-?c{`v`<`gPt)xi8!$0akfUA zAhgIkjDd9q~h9Guf?SFH1NTW6$Y~i$$erHc%o2=E0E& zSha`)k#&-3@=adG<5|0bVNZ}&%}fXlgl~>2lD&DWAmZR?Q3$<^j=gWIATa|d$hDgyhSJbNN3o*HuO3@!2Ur$o>4lxzJ+a$V=jQ(a zP@~WwmUEQfiSc;S0-(PBbmj;6&Iybm<{rN`T`EK02rB7-tEP zqLB14f$Tx!Pa-}f%>-zNfcOA}Ou z=GmKNY7%dM*yXG~k+F$L*u`?(g-+Rxrq0?gq}pDaj+~#Uw}2F<$%t5dhH(BIjG|z; z76@galnzY!DO(8;(Ot8=!pM%ug!TxCHy(&VN(>QtELxmbSJwQsd}1aq1A8_`%w zR5k>Wtl^x1!_N>>%7goTj+)XfjQt(74q$5rsKCPKC3%64!&(< zmfbxe!x|_4w%k9B0O&%^_0Q+^r+U}Jqr9A*DWn1kl3t46S}prqfm zFqc3gY=9$-t(dv`xwrMhuzH@c{Pg^!{I^nJ(!%^jU zD2Cr=;7osvNSiW^IJixk$ZI!>{(C<1N1A1xC>f3mukNb3}r6x$jV&3T(k%nY!qk`QaF#NQ@+2(s0Rd-+wiH!t^6V7X4 z_Lsv-+zAi)ZmBDhk;=TFPc<#xN#g+ z3a}#~+Y2peGNh3YEu|5xCidc6m7F=c&J_Wy8AsYpyty>-M*e`$S&;%?Te^w#`&4=- zOj7YfjyrEv{xc1oUN-MG3T5`Ws@rgWrla>)4BNyyP?r^|N7q|b6gwjkYVC>0CgEdW zkyUJ^^NV)PP%PU#5zZA+$c`FP)eG8`^xc>ZSW>e2$EKWXe>v-#P@&X6n_Tvk@KO%w zIq9(}vJ&QwTR7mjNqfUf9yy9zCT#=`L>R-!$%1_L6mvyY$RF~c! z>ZFq_tL0_`JT5UP-eVV07{qjIoEm;!Yn6g~g7n^s0lxKKcgvcZlAUnXvlxSfa~&OtP<5QkSGcZ1%5*4E9+5%Wxyw}c zRP>1ri>taGr>?*AJ;q|FyyeyM$R8(0bOl#sMONO9{k6us>uR3;HiOf(O^z8Z@{}R+ zmkgOh*R{m*iLUF5cUmiFJ#;fS9Q9Rd&eK<-Td^z(O3Ph^n43br31m?A{czn(yRuf2 ztgbWZ;|*y1Rn6AFB7ywT=aQsBGk_v2`=un)Iuf=_XY09E&13=aOn79-m_^;Kh-|0p z6RQ6JS30-!_nE&eKPxzu5t_F6ki+tZ!ClR?kjSFk14NDKYz2^(zqE9woFnZmhozDjO z5`qRU<#2>_4@5(;hDD4urvRIC`)q>|S&HaMgcyobjGC-N0JGbc% zlzsA=RMup?o{IriNpf+F0X{-^u}7Y0LOS6JZs>*lE+}lgbdLH=?Ye{t(qeIa3B`)d zUxZwa%~4mRH3eo4JhL)(-T3M>x;@?uoYIIk1Pf>e4Ge}%uk`oI^VkMLxGNR5QMW&3D)*$;F%~*v*VeB;yG*vL#pxRhrW2KdP#Xw#PHAJT^rt0w_{dY}YO>#@ zOwN48TBmWcP;5=A-=GQ4s-jF?XWi< z+$dalY}+->^dQn2CKN4>h}#F5k3U>ZSe*;EDEj`}H{-c)6NHh%w`5OVoJaV0Hp%+7 zWCV^xVC~xrR?PJK@m&yPeY4%$yc8YGn&yHU&>_-Lr&2d3q-LHS$PK)0GG z)F1{!#1)7NC4xv8VCH7;UYR&?bD@PdtTIliW;#g$v9*1`Tr9@}!GJN`k8G|;*xRbM zR`qBm&DJ)PLQkh_tr)kM~PWv9W8r@n4iZ`B0*{J5gy7Y&ju!YV9iFuk{|{J!HTa!{_g&h_C*=_ z-}tS=YN>nQ(qg_MY?dHB3k1H%y$hes@fN6bu3v(VWx2H z(U-~^uiMEC)>meQzYa$xFc_v%nux#;tI~#dL4sgVsXJl}pEDK^a3+u&8|mAEnVt85 zi9(Mgk?lGUEU?Te1YliZ4ivg92RlyCZjonNrS#d%Z+edEl_zL))?mZ~MK`R-iIeBf z&2l6*O{>g>3q)+uD1sWo zHa0Rc&zPF&kisa8_L<7h}zKD34aXO3AZj&L+uiY z1a#jV;cyJDpVXqMyw|Gi^&_iq)ZcAYT>dYgIexcy;2uO>IUf_X-Jeg{wWH6-mJ1>T za?06U3pLr}%BOUWlF$+!ZZefRXZj`UUXn!MZ_Azo=k@lriB2j7B1IqdnFhTxD`aDl z*I^mmzBpY2N*#$4(iudThRIUpEW?R2J-hU8)hFFT_ioXiGi2(4t7%O?0B42ZrWs;L zM%5b)CM7`A26mwgziy4@v1Z7NX5!9Ynozm`o=9x2-0BbDHko6us{u9PDMB3 zo+lQhl^Dh_G02(?4)Yzmbj{HxPOoChj$Lk1U}Pqan=kK}$pw^DG_~j^f**LXtg8Xi z-C`he>#@YWHd#6hOPsz2t~A!T(6j3VeY?tPhGPO6Vp&N5is18xQ(_yz=~4l8!R}fP zXoo6(-J0=-t!R@7*?QJ2ADZDH>}JZZvUh`#fhHQw%@L4Ri9{#XWtdl5KJ=1MqSjou zh3JHUzY1fm$ob;K!dW&Dk=MpRFDQ0z9?6N(Eq{qB|4Jx~5E^GOSFpYDk=!+;}Ob6Q|kE9SE<}6<6Hox-0 zj0t@>(y8@>aMqY}T4G~|2w@;2-OMDGAh7h4@7i&gGt8NS?w(gzVquE`)#(jCN`dP9 zs|QlFy@$81#k04r{6l*Ftn(h%aJkxzE|)EWAl;>6zBFXcUOQw;^=34O{wtq)v#Z~d?x>B~mFhc`y0nJt9)E%}6(#^{( z>XGYvuDiXdD*E1Pjsyj1(>#Z6o?4+-x-l6sf4A3b5OpsWC0HCE$W`TZGA+%byC*eB zq2)7r(~do@!4e|CDPK-BIfF<~N$VWe4TG=$09}7ee?hsY^Pln8hN|q#t2u5&og;GF z=qla5UKr(pby#hour&a1{qsw}i|KIEq7x|@f8vEa9V=8hA^ns_2eJ&$7&{&21Zv+1 z+T1e(2ujf!Rv$pnK195w3S8RO3~pk#}u|O{Tv**BqL+5 z65#%mwiStAw5hbmR`uCGe(8Bg^oYT_Fz}DjiNM2tQ^I+zr(*W`{`hd_kn*6IANGQc zFD0#)W0vD{=KDPJHJJU0N$bX>RMOxuv#v&M)TT-UlFJgg2bqdP9S4V$Rg!SRP@AMp zhD>fc8UwSUg3$RreJ0G-2!TM;&+4h}Dys_@FPGe_D^>v9WK;%Gy!cb=|;y z6MoiTjf!CCu76Rp8L5yhbq)0Lf+~%?q5Gz51!$*hFM=Y5atJmhX%;$k)GI-Sbx6D& zwH2pmsVbWJu{kQs8&tSmHGNcc02}_ZH)}%HCn}EXT$0-7PQr{*&Z(>Z;^P)Rz2qZ( z`&d)mH22J%2Yq`gLhU^Sh6(O5=y9DtfaxQh6GI+{Jk849K+SD7!t%Qf1%~ z3d60)vw}&u(<3^HSFrm7^`rGOwP=6N&&aROO0+{lGO$An562TSp!&W(Er( zu63PqtdM04KXj3bPKu>xi89)$rFr>q*z;$T%jmEWCPFq@&tx1Ql39frn7$!|YZ8>t zv>7BY;)e2B4qvlArf#Ih-Rl!qnB|X#%~Bt?cGjLqlseXhSAgjHb;8!FEJBMjSrP3E zOdVj_NojRmGqDtF<5YExWo@iaY_I0ieG{jT`Wk&izA_PuKU`&vRzt5hGPRhkbGVLD zjPc>R^*<8AL#2rR>&(^tmk8sLt{p^NkOH#=k!aNyXKjyR><$u+lPAkilW=xd_KVFp0hSH?ma-x@SDb*c2%%tofDi1?UR#pIKlsh30 z5yEUrY~6h+gH4Cbx3Q)`U!PnvWz>Aae1 zoHd|6IjYN-gTF=o!Ty(ijPaid{#yQFriJry;%XalArcf;r6DZw@(6$IhHTh3Y(~r= z49s_g1In9@oFkC(Kb!KKxpsJcxX#@NLpH&1w@LPRjYc(2)H+^~k>q=v=(y~Ot8Tfn zplRGxf}38%BMrJUrtNdcCD4nxY!VIllA*^PuV+w`x=Ouw4%(Kj<)vBI z?3ZK;%waZRH90#Hsay1Y*9R?cuE(DnXQa+hyG;hv$mshZ)-O=tR1!QGiiR<>_4To-w&XrqT{^R=0B z)!(KSHCxU!UL|=YftQ^&^#mtwY>ZE!=n;%Hh2qIdZojF}QX5sDDiRZYVxsLC^(^$D zw(4-sh-m})*-Y1#H3`dHYgt@ks3T0ZJ(5r?JPZkfMW(BC2^GL<WcWc-|!TK;`Ks zB%q;9A&6*^=g`R!T)>9&GRp2*IV1@TNkYZ;{Csk5w5-YYbh+0fGr&^FjtXn0rp;7= zwZlO;L62-126Td;x7#oT%$Wehi7hgc9#Ln#$4gZ%~CmB?#s0f-|A%kVP=c@ z{oXcL;d7<+RwTK+*)%lj7TMYnrGGnGtET&Yu1;@DzB$WJ)sIL?=0f)hU29@<>sw@2 z+t458-!93wIkiLbq>{R$#c&&0djM@t-45n0%XU=i+SRIT^Yw(%iqeKKH`R@{3YIaS z{{T!ooAev>^N9ZdG(RQy;*78t$qFt3arS!7godlOtt^Vklvweg9`-i8;)W$3&qn#2 zty_JzAi&4hCrK{D8Yg*29_i{t?smNHksOBonZgwoMu zxcbGLVjB!$k#ly5#MWY^77FfCdf^9U`GRNoZt1e^C+zGOLN^nK&URE`h6O{CC~Fp? zN(8Je0ChE~d*+rQyR?(AJ;!FTN3on_4EqkantxX5$0KfAA1ZE$+l1Q5>DF6)aRief z-O3veLxvpAxGj>>S8O6nWGqvf7MRbJPk3SH>{_o9P^Zzw-NI#{npE2mom*zq*HDlR zG;Qb0eJNR?pH@(Jr&LtB5}`}6ZieK(WpL_R2`TZNb%|!KX6pVov|&$kw0QM{LjM3k z(n~Yioq59e-3|@GMjt&*^nEf_99LJ?V(>j(^g{>ZC+b&C@%lGWq;!U`w)Lj@%SEle zEVzFBoP7NnDO?Qzanajii*b_wB|pgB5i6&Bk$#Kwf&!iG%4F zreO0KCo%da`au12Qq9Q z21^>lJTHf?Hz_@zP0Xo_AxL?_291N;yuRF+wKHg~-RCWnd#rrhEoj>fZb5{S?$A$~ z!Y8^j1e}!#(@ot}?g6#Vdh5iQ%Y6)%Ju1<$DrNgQfw5toTBeI_Y``g^m1Un@H7{FE z{E|TzrJQI*BtKOh68vANt>(&YxSzIVab3LAkDIPAw3F3a*OkJ6^~=Fu^bhhD2SqCG zj^k@`-B)i2VY`qj`_y3S8>AT1uL_r)EO&b%sp)UcPP*kQvCXo+up&wz&j#J?a1`SZ zq-Aa*F?qn{t47bEDotOb+?*cZ2CGU}-=fLt+Ahv=Go{v#EogIP-o0e^rLL#<~-GH z7TU>u^Rj<&KSp{Us^*X8>yWCQwuKG#>GgFMYSHI9+?dT9d?^5j(_&~#e+VGfc zUKkQPrUZ+t1je1-xge|@Bbx&- zjPFN^mc}q_4F*SREu&`1TO$T~NZLm~KxRYIk~7S1Vz;mY7*T3$=qC5iQq%7F1hawQ zh%HgECNn+rb?Fxl-GM9Qt2DyCiJzoA)eqVN9 za(+DOW+{rV025BhI< zbN4@gGTeBBWqgoz*a)Cj0wWX|@KPI_)0fUMr>s9gjUGwKXkQgS5Ls}TklB#3d5&QZ z^c~-PJf^LlY4m5BJ=uX7l1>R>Oo=vdc^_h8D#=)|jEsm{OfsW&%90(hWJ3YIge8dL znGzsR#2+R$Y-@}W8&^f@1VUEEp+U+I#kLlaM4lbp+cxVpbNziYhg=X}^q`XGSaOYg$>31nVfA|&3}und(l zlc+at&V2S&_TLswsqi*2R_&xq&1USqB7c5${nB0-=RO7mVMTU)6k!dAEkWB z)U-Q$HEKhDU~y+lgG4#KRrazOc~^FIG`Psp}kFFe*-InY1Fz@*L!M#2D75P=q zocLOZQF9Mk+IB5CfzsmZX$QfYR7C84c0{?$(?t(V)jfVS#I^*msr{+@mHM=Gck)N` zfAY?`wCgghN7o=(%bR!bUV=7{i4X>%_U6qe6heEF9gj z&x#gE&<$>YwMrMe!h*{JnjUn$n#e}RHnOk)GOjZEzS#{8yznvJ&DS^BM0T!}ohCN( z%j;$|8o;!Tx=lfh%+ZaTe=s=O?o4>vmeq>cm&Q(&T~J$}m^EA3MtkFysUP1gI{ zW3AY$nsat8z2iw@qM{6RSiyr6wflQ2(5ylnYzr9x{2MjOumsplC39nxR-Vgdp&W^b zc~aY?9WF4{nI3^Nd+y~odp>$h8yi}(G}Pl0){a}&0>w!J#Iopv)$ONMhqEdG=2hz= z@SGC`zUrM?ky`1FZMzU{zDBr^#F^-RZfEK0nvQR}5L9p8HSAtMA&DNb5Ba3>*WVn8 zdq(FXxBRX{?v{Rdr|&cCb+0&d?`2eVm4FY~3X?k8httUY!TKirkNNM^eIY^692z{< z-q+?Wt3RAi z%^%Yq?&6L@<3Q-G48ZE+*Ntmd+kll>H-lVdEI~R8;U$EI%Lqu3*e@Mw)n3{E0IENu zKd4=SMt)KLRsL%%$P=hxY(3h}$y~06xd_td;hS);((cA;S))ueR(5hS;6$JHvXT}c zlQcURe&sJlt=iAW5KmCOdk7uO1dZDF4f8zqu9uP=jIr^4^p;-<#HXUpjms={7SXR( zs8Qh-CI(>wnl>zIDS`cZW5B?M<;E=arN8SZR~@wML7m*#0P4QhV**d{s&6@eL% z;YfPbQBbUUF6OhjilBdP z*I@JqHB?EBSN{MiwDnXDe0sNeHI!z+Rriw%pY1t169PG1rZL2yl1DE<=q(4IG zCmLSEuqw_x<|zzxk9||NkLH&}x7IfgPkw7OD?K=t$A~)DsJV<&T7t(&EUo?|{>t>X zE%QECgSRquRmf}cm-4K4Vz*F`{{S_r%w(1XnepIH#SI0@|)yfDC0xX>e13 zsO)`sVCxRy?~c9wNAFq(^MCPIjB7?5YXW5PI~3DhcV14i9_lLSi)q0+qx&(SGL8(p z$L)~_p3%fKN7oFen_+uCHqpBBDWWbDq(m|^QFZ;2VZtpk4${JB>j5rN1;}Ong*smN z@U^gpj!b9LwvS*08OnNDNHW9DWEL7u)-WUoU`v0o^)e-u5L}OhQYZNeJoX5v#-!)2 zbXZgO&JCoFE^Eu$*g|Yz5^5^Z0_-8CEwuX};5-=Eidj~5tkBVKy|x)GYFSPd_J3?3 zrliDJdt}4|0HrViY=s~J%^<7*JC74)ubJ6lFsC{=4Mp{iNH|Yj`z$Qhd3DGhEMYF<6 zN{{xe^lQ=QlL<%oV`Ny{MGiaUG4E>n%CAMSas?s`qk82rt1AOxffi{00DhA-aFJc` zJzafJa26wf(g!=PYMB@|(&5TNz~0bggzPwHM)wU#r42H*7(094EJPb@(iXd=jXpeG z(DW7@0nKI;oC*2|U{E89!&b#O(d_}oL4g`e0*IM4SGx8*7%FI^bSN#`@Myuv#2Y&fvwj6^q#2qj4kt>G0b|Yy4S6GB?^hGy2xdL`K8bueNrWT zLCUvNSZN3TK<*HR`T4;$g+#;B)ndbKv&eV)b#HNfT>!JAD?AC; z4D#4ZI=+8L3Sg__Jy6ZFzdu+y{{ZzMH}mWH>G{73XSG(sW8Bni^emmcs>awWY>+gU z7o%|9(=~ag5!(un$aK%Ljh6ZP_D{!F$TLTs$Z*U_;iFubnsx0GLvF?)S?c;`xPqs` zDhgz2C8ZA?2YmT!dJMD(j5OXs%M2dPtSs3AVU?8{X+`)aivb$ZA!@-votMH!7t{9m z6J>2Hh-jnA5?qc~WXX$B3Noi+M-v-@wuXREe1h5y40ebtjPj)0?CYxId}L{Vu&id; zGPglrE;O#=;+AF=1j(y;FTRYN=FL^+?DeN%Vf zvtR;H#;~>Vt&rOEvJkep4bPrki5o^f^+TQ_;Y>!kQXy?n>M9m3#ao(05c6WIJ%HxK z9-q3ow$rgVpR8%CZhn9ey-Sca^3A-TTnSG^{a`SD6M_nLsh*YK;$f4;avNF zqtiv6U?kQ50I(}vbIdM{6vsBut}l>y%dOd{Gj*-oqfVr%JF5BvoGGFfsT17j{cTfA zw_geM*F9M^yL|pm=nvBlT;xXo0K)z*J^HuMywCaUE03tV_|-%0qC%yzsv9viiKdk? zchuRjRvTQovZ_>vtgXl22L8qSe9=* zL^4-M;(N1JlzBbGSYi?hZi5q9Y#_wI#(QOm)h{oN2+PgU^?4#l=*8oW*_h3Rg!pR+ zGG&7l(=~Q0`mA%~1=QtT&U{>;;LpL=4-popxJCy zNk_%0IqnvjWli`2JV~MGxE2ee0ksiGSu-r+ThM}8EQwadL8+!uzYtHN{m`}@R4s`v zQehR#4TXxO=6j;!Lt|0r=c$R0@BFeQr;O{w(QJk4xUjW!ZP7gh5~ep37&foEPF2UPJ@5Y5gj8U$~$W+@9`F1F}^ zq0k9i7MWI$_@ZCp)iZZxPzbM8rpE5Y=nv3e(;Fdgg#Q3Mza;3Zp1GZFQC5o;SiNSZ zB(vW&vfZCst9dZ&y|GM;!bpYU(f4gxkvMnQ?_ibaC^PfJURZ7d;yN(NF(>^tFzw{v zPt~G)Lvb({1627#;V{b`uR$$-joT|}C5$JGe_jN4(R0Xa49=sih>(nh`x{S_4nr8~ zi#s2^)?kOBGsY$+V+kf$K9*%!vmoh}23)u_)3 zvE^A7#g1~{u?$XCmjG@N+983FJe_x74jg@6#zK!{_+!w|B93L5^>2x6v(^O77NTJB zGE1{zu;vO*A=6z}~rJ!Lkz;ibRXt)VDoM zyBBFOPfd8mFSN8bStERZK z#R%pb-e|e$7UkE-wDZmF7!_aA8cU_gVb6{Fed?f5w^h*bDsZ-I{Y?jnE!Ns&qHLaG zSLs0J`E^ILDJVC+F3jy-uiv)OS6+tTiIAv}qU>ZdNEaIiGL=2jyv0x6D)#C%575@{ zDQqid1T!Cyx;G^S1Y1i;m+fgcx8k1}QnnvdS&Q`l00i>Oh%!3Xi%Gcow(g;wEE*Uk zwB1tW6qBl=W$c%!a?${+Z>9rcdg@Vsw123*Swj9&eqergP){=1qN6=VT-!CCxz^92 zks@}L^JdM$7NmVir0sGuIK{nGO06 z<4R8k1h=}e*ke#KWYXsl3x@si$nMGz(;Ku)jzml&LdaNcuAgpqr-7C zV@Yxes7}|RAmqo*2m!k`V z{gGkAoxB+T03~RZswO(72p&v3<|3gOV3x)%9QR;_NC7yeO_=;Sdx0k6{eeYh5n3MM zVKiE)R*{-UuyjoDVaA#3uvxa&44nhZa*IrlXx7V#SD^is zCi>*LHt~zr3bOE}TIE9Z2&DACNnJ5x#%`@88N`iB)pK0b!rDCb&&cCw*LSplm7rwsk-U&O`&VkV-jG!JU;QYpM6xz zrmGujwo~YZaQz#|>Hh$yoPW}nr$T;yDRdVt)b+PeHgfWy&Y^SVl{?zn=Gys!*+)z4 zx^tSkJfynAL9vyeLQ*`b-}Fa6UpJ*)XY&W;Wgl3(pXz&$fcaOAjVg3@&Skq&T&O39 z3iqL2IPI}A(L&w!P!2X$c4%bwzz-2uRj#Y_7w;GJC1w6Yap&ZJ=jt=e9qrnZmzZbh z2zWRaMnrE$G!2x(FnZ}%^yh_{{{U(5vd_^7Z}vPQQ#KFVmRjbG*QZ<=#p38O=zWs4 z_&03tG?jrXaIF%2k|4~1dWetO8-AkWW|v((stEF#0X-xs{TU0AJ2Sc<&4N%S{{Xj~ zRc=f_3vm%!2#M9Q9CjiiH2gbD6deqCD>#n;%a*b64@LnzjF21J;KM5+lT^4e9&osB z6L(9ewwt|@+Qn*46ChT>^8rSRaNxkZVgU84Ov6en?S>L8l^YOXwh5%jpfX7+NKgpD zjcJs=^&O}#mc(LB0Hx?!oM6i(d?+cWAzd53s9$%w7|>3XG|57-HbMa6wH=e{kR$WT zYw{+Ekv>vi+Y6zo*`ajA(?X}&biUi(pJZfKE#-$_*GB9bypH9bgyy%mBZy7PjWpA3 z<551+L%KJkAW>Gc4?Q+4St@jcU_$;h^G>dTaO98C-VR~5kU8aprFn8Yh3?Vm{z@S0 z*8c#Uy`@uD4$McZvrjyt-*WROK@lTlG6A&ixxCb)%keB>6PIg-Aw4f#2dnQzCBHAa z?w)y{UR_2{^L;tdst`I;kmFN#6?%>P3;FkTR6l`L6M8>D_w66f{CCPn>R-`7pPn5W zoFcTp<x(>uTuGY&wDxS|KzeVt}W$4_V3s+Ds!s%vjc04MCdIFzqn> z8wVMx2Zo?GYo73A0|6C(Xx2DiP6JSxk}2o z&F)p9`iiY6)w;SWs{Ef7)rNmQq#w8{d;rirW`y=!#vOiqnPdM77h*~ zylm!aQRc%nv<&80FiZfHVN*f!V;Pnq5Qf5~i!z~BWoEJPH5>RV2%R}VkV+P0aAhjB zCp$fy{{TQ)4wu^No9(S&x|H#tcrXIl=LXaCcn)yX7;7m1qvJ8-&U@;pxM-gEz~wG zB=WI@OHP1$wrh7gZtbdXRZl8QIvN$Xb91X1O3tRUc_P4-O26}$OQ2oqbh&a?9{jm* z@919*f#%QCcm&`1nyTvlcr9fcMar=1y6WWO+ds8A8l0bK^hJ+#8nStDBao}xaXysl z5&X?@+0;=tE_c2kRBJw!r>cibEg?~+)NA0Y--^FMXihxu{!Dl3o?p1TJDUFh8M$kX z=iD||Xw)=!<$q6el|as>)fUR3r`|PWc40@<uD%8w zSHUND4;PWFHENgUkv);m4aVS+;RoH$!)Nv#mll<)qyCWLUOY`1zL*H%PWMg0jdht}qfGlz@#=1L3+$ zhA|#k#!K0xC4nbUQ^1Fnjr1eT0hyMPO){idXsqlt$lc{m$VRm;u6Z$YGQEOn=Xd7v zhvh6m8*bN%t_hX>iWa@;_(=(hQrb*-#44(#>Y2?|-GnTN3vobdjh?=BTYIuqac?rn zuAgWN=v?aVK@y}XuDGTri0fAt757zz84nGW=KAjSN`W&_hYHq#Wv7uEQ+kI*Eo8CR zRL5l3^nWsS<>AYZ>KpHSOPQLhp0whwqWXl!(BjY1l^86)pC7k;zsSJ5ahLAXnNP1? zKb(k`7e8-Zv9AskAJ)+2R;Fj$yyow!i7U}oml)X&73v8}Uh-Gx4#5%3fV+0%G0oqO z%iKpn5xBoZdz15L@$B_GFTnA(bk8a3cu*karG}_IDPP9G(q8MgMd@yI`&z>V{{YR5 z1Ut9(U-V-3qJKDl8z_}}MZ4X=+cHM7D+sOP8}uY7PaQVHSu9GtbDWv6)(+*5KADWz zbHiajWZ={HMHeuTo!aDkizva<$c38{1hWGm${27IIkWT>!yL^o#c2S#=B>I|=7__I zj{9I{Ty;u_Am|$fEIpnbNQ|-UN{6=Vm;knnh)2s#h?uj9@DUlnSxO_68rCu%?Yq2! zKy>0-_e6=PF+(|p*ucHx6Q9x&+HQvjyA8U*xGjqbr+d?*I0IG4$H@XKS3)I+t>STc zjJmO8AVsZC*Ihbtn8L-QYvIuIQ7x2qsI@TOmW^2#wrenj zM%-zYRiONbUs!1cJu2ivh5UxvC_R z-iRL+klq-C`&ppz2^a(6$a|xGpkZv`NMclSRDwqhm`EpwipFySZvm1tlaUHnl7XMi zb&Eo|dW8hFbRHxP18$^~Zwjqny9C51jyva*I-%)doK+&W)~Cm~BIc$qL1* zUe96Ws{=x}HmU2`9#&a4FXA_DW12?ETy57Kg_z3v%caLU*0F7edoInfiLqPYpH!SH zQH>k$Nj^D~Qsf7r9}ULXU6IGBp#c1!8`n!0lt4~JM-FwQl z6~_KqCYDd?d{!6_!q*ea_Lp>-15!K2K z6oNaY&QFRzNjZVus&bIeN_2VtfVD(|=h9`lfDO~l$%Temta)*z`kUPR7i`AavZP}o zi)GoOc+T~As%LVG5h&j16X?7PpL^uDDSz$48= zW4h_6ueuqwF;sEwWG6X>4^LUdRRzVg?7LU`7`FP}?EEeK&E%eXhyo3)1Pna)Z71}( z(e

0G|hO88JopS2mg8<>qK6jhM8p#Fal(9h>#*pZ2MVZNYITBn-$!;m!r4E!l1R zW+mXp*B^G0@%QM9naH8ei8-xIlqU;pA8#WNo zT-%hZrTfOisgGL=nZ3g(d3~SVit08Fi>02MRhZUWT=?sioKPj~V^pZBon~Mv{JV6C zYs962a0Wy#L2<8_T$C$~A<0#7l6st+_UU*J^w<4Ox9d`RP3yL&vuu8QL+v%Do++}8 zgJ?6rm)~;oZY5* zx$TPU@>itpX{&xOF@+W1}z3cin%RiZvtsxDiimPi3 z+?@pJ72;^_a3>~d=*rDvYXc=n`zZZv!>i1dnT`IaezJ8hVvPKu{LuV;Opj&^4I50+ z@*-_0+bXqy&lgZ5BY?k`O@{9nT&){?ECNm%=4{JsLzsq@o)9*d-m9iQCCEXZ5@)hA z5tjKyKwxxV(?dwcNK;YvK%1V>MdXakwCM4@Cb12NA>&$CSz-WT%sfV1l_qY7FC%)g z2N;Cm+cA-^FgwWPS~tU*LE{1|?r55}ZuFQ#d$sL_5TS6#gN!sx6sBp3mbJcS(y{E9 zs|w{R^yb9tVb-g&CrhvJJt<~tSyvyS=wlqtGjjl?_S62dl!W_NvKjbCy7e?CSuv3I zxJDw;2AX_4sft`%TTaIY5tA8T>c^pO+5m-QuI*g_jx=jfKN*%5NNJ!MIoUv5%_568 z!Gl=aK%XdUM2Wh7`B|Ayw_E8b*TrFK)L~8A3iVmZ>qz}#x6u6!Yoa*Oyytjyt!d63 zPc|Z_KtfsPWJgDbE@WxxDO{S(*L>+!SycToA*!CiWQ{v+*nGX%icH*W-PkpCQ(fJh z&(OUJq?J#~zAFMmk!*`45u=^D0W)nQ)k{J@Mmhfgyy}YYO=He$#%i93iAUR{xcKKr zSbv=Tspay6M^ry>WovVqHCk0&5Pr`5s_R*`d;2WLMDOS0k!b$_&^q{M>@=Uo zzH6mx-kH~5jtO?tmyHd!>-Lm-n1dZc$u(}#36^HlOE59kwDu=!7%u++{h0j|>+Waa zZ^~cE+WE$Dsf0$>S$uRYbpDaw-+Wv-I1pTq1KPkZ~3T%P? z8JHkEwzP&2@-Qk`Jh&4@Z=0hJ07zto=#m2tv>|TUsuT7JzR&QqABOLbv+NA&)4vFr zxE_p_cV&%Z-R#7=UE8#QAO2q*Ig=;~6F}AgNAmu+SveZj4NY>uaex zkLPG?6)>~)4X*+tRP^V?O7hbhT`JCDuhyNjSIy}^d`l)Gl-l0t362##tnFX*2VC{~ z1qWJftGZ>@ub*68GL$ST>a(`xb4|TsCDYxEZjl;^dn;Gax&5@D>TZP^$E>Y`w!FOr zP|XY-q4{YWsySn#)NF2@;l6YI5ZH%6KeL^t?_8>ea|!l989g=AJwMBdHD^CnwtYV> znQFf3%L|ezZicvcR^8~H5x%Yj^&V`#tqMdvclsH`_4jz9^A+F1x7RCvk@g=3vD$yh zH#-FB(bjKsTA!GlXopZ+w%;#LoU=OSsw3KU$Gf!K)Dm|_D+cgGlEu|mNLN~OvQrf7 zJHM&FUAph0%zl3UGyZL)w2-Sko|$ATk#cCPu~li7pFeaKL86RpZU~3JBHFRvuSD59 zpv0HwvSLmjyLP!6J|1XkK!e#~W;D)lb?PZBG~lzo+CK?d;}xtw!}sDsG_Z_Cp@KfY zmdJArsCs8^jpBEE?C_%=<3WshYtvzy(hC<=WcYS4^$Q)lF z+^HP!omv>8#UmmIrp-+Ut?}XV!)d&)r>X6BHGul)6LOY`WtBdhI6H~~oPxN;&=A?! z#=MTs@AO>Cz`|o_Xr4!NOfKxW5QL-)V>L$w%(Koau={1a9{!2)Qx`kgWIcf@UWt}n zDWEgegI6+fxf#`cHeTmMLo&A`cBXfELbcWw8Ai(P#|2u=daEACSco99a(=7Jgp``# z{F3wAlKL|d+{Iqq)mK4%BD&3DG`bl~PQE9?Jm333hG*@?X*w;%1WA=gb^XdDL z^>f8~)7=5!wPpkOpPWZ3;GTh+~hvZMk zj&y@wb~kWrs}?^#Y6_y!@HTar>m-XsU!=Oz&b9X%s_%ZJk&G}{w9feLv1&Wr`<<=J z-sk|MJ|rjld{J;hmRmITRP z2#jmh{{T+osE@K>L^Srfjg-k{*>|~4foyGPw4JOyp+|$;Gr+@aQ37Izj>i&gp%Qnv zD_AfNHZB_s!hQsQTL}#nrrw|#Nf;2a#IvdLD8Vjb2j3%7cV5P8{)Vef+!Qe;QPL$; z@Z`KyH*gV&5+CBhYZH|1%q>(vZ9BB< zQnHwBE9lLuO4r_4Pholk-z7FO(-9}8;%{EHCSJoYm z#n$)pT$7=23$ID{cQ|q18gun))`dBya>b0S&MMHtmB=N$P%UG=^BY`gero5ds;6oa zuW1$|O!S}Vhw67`*o410{{RO0#47b~k4@)RjnM;G`&O?J(bY<^#yyJSca_ZHVO+AfiZ@a znWq_hHhTWk} z3^j~LyM~<)EtOQJGNfybj*~^V0&8rxZbx@3KI5@Uyj#tM^$lD;ghZ!;NPEA+2$?i4 zHEmuzf9cII4{Dyr1m3V@f{q7Xj%ErBsMzXde<=xWr?X?IY(;1Y|Ax3X?voG4`DGXjA|N@WQCrP z<#OCTEqn5cHCJMX5ayhq`}a|EPOEC9WWF_XQC_vB>CWcmY&w|_o}G?639o>WlrLuz z+8|aavlerW6=DxbxvC528m+Fc=-wT?nyHW$_1$jp+pRayrumxGzv)00WpK;}D_yPJ zalC2^#=H4(O~-o`7SS>dmS(e4vp9S7xiYtUlSFh!0Cqd4D_U|ryZOqX=sF1nwC&Y) zQ#=XGyi?CMaL$6ajF9CH52}N(*cJZ8xf?86tDskD)=0#V*(?+Z2vWYb-?^pbY?OES zi!Q0xujohX4^3Qge?7k;KR;9b(O=yWkV;S1QBnvq*P?}e3gA*gd^(+sZGetzYu$+* zdUDr>B_qV!0D)mTe#7mU5XWI*(04@0e*FBGuN}T)_Ko@?O(Uje*}!7K+dnN)O~xPh z-ftx*SiN#&#uqQb5K2al!8{+ugA~MG`Jg3X5=r(9^G=uurD&+_LKFQYd=`6Ew69xQUQ4bKPQ7HJVuzTXvN8sj4Mgj zoI%ZRoxX33IwfxfDr1nEwSm_)rK?&H32Tk`V8*&X%MRQc$Ko7kS1r1tEuIfJ866=v z)s!1$ICb8tBxOa=P{#ZR0&W;U%tD)@FP4_8qQnt~y_?i!SmBYvU6n`+e1J~oa5+|4 zY`i+s@GiayV-W^o%%;=&vWHjg=@ngnVHU3Y)|Fd6nvinaT%@gX2&QEs(Gb-`p}iDc zJ(>er0y@HY=pH|1M+ z^|Rd{RM4F1%m$lm2|WJo)f|1Rr7C?Rhn%bI@itp8KFE>d0P#|EwGghIAL$N<{Sp0H z{SIHEd1LZ7f~$ycQ^Em+&EyH7lI-s+v6QTqb>fD#YhVO1fE-!6^pwwD$Qj~KC^$-m zJ_fuq@QAs(^COf=gCXa>9$UT7Oz#7`EM-3;u{?rwN5XcP7qUR8tQEbcylsCUdB|XxoyRy{-fyv+%cxS{msS$U77jt+@;pz@v3^7e#Jaw3VtJm{we8rJUC!<@nn{3!)v^ zmlIpsYf_LB*)IxgxvJi8wi7A3Jd}EMD%lZ!S zk=QZ#l)WKkZ315(C&k)OZ`v^X9WuO@1%&#>gP=UZT$s$0VC);WOlkY$PxS1ED@UUo zTyHB4p2WG1GRllN0_2?K86WALK;7eHxuv%u5ZjI|xg9jWl-4|ApBh3jTp+7RFigsr z@n^O|18WzCryj_DgY4NtWRc4~b())IdTNU0Mp+@}mW`BL(nxuc&SxwC05X)JX{8z{ zVK_kNQ#!P-?(}u8ft96P#vnl7QrhUWxE!q}51;Am91OYowV` zt8PcctYlM!I!2M`t~c5G5zyOd8<7D*pyk-z4a|n@O6}x5Uwm#|WT@S7inh^(g>w#{XohOR^P>{R+dN(QtNCAlR973rk9XGG&&L$fXmc$nskgd<3rlrbYFk|U zzZfUeSfZD4qpsn|r`NVw;?^Xay}O6>gTA{1n)nCtBZ>nMoE31)&F6BhgIgQwW+F-x z93=*ROabbnnRNXlV9Lr^xa>MMYSAwbWeVGX$unc<_hHy^=3~3bz`T$uSurv_)9zA4 z$Tc5Fq;+VS98{<>gO=S79i^cNpOj)zAd=iW30ewc2*`cD!PhhB`^x4nGXVbpBImv? z**o!uJ)R=@JPmsbUOV;3(WX?r$zrWm9MV#AcS=JSt^5{%tkm^t| zJMIE;eFGX)*oWchafYhHB3Wr>!#vCkd~p!$K5xQ!pw4Ryp@%YpBd$9+2${3XGPz)G ziVsmlJzz)xf)2j}KVqY$QDTm-3%e;YWhm6&0L);n*x%DxWC78!NtRg2(!@m=$t`}& z7p?ZvO1|@?N7$KLf_$rMR1xh;Vw>iJZHUUIWaXllP;~E04c9OoYTLFw$7WF4k_93- z)}yx9%7&}^IWF?b!RyKCKL2oLn5VoR{m09srDI?%zrTLzIX_2B1 z=@R^mLoxBRZrP{IH|sNy{W+VMw}h=w^?F`cg5nq(6uuKi#XF}D2TzAKG|5BtPM8Z; z>4XJgj8^5$wP=?xln)?9EsisX4Q7_TJ}eSiHAs)Vmy_4PIziAt8zz`~zS%DqN42b1x4ekL*U>nNV8lQPtF2BuCu4o zpC=Jvh?-{%9_5BCs|ywd^6^SYUGcJPNzQfqg9#-&fJmv;v^}bfby&qH1B-_}AJxM; zmW)YUZye@KKI|rqt-3TiC6KlQUbwq`e-x*z)GsVW=k{VLMqaFX$%19aN#?s;SNN2S z2_%#2oMcN5C%U#;nlrX85XNIb^2K6TSG20qG>&;J>Q~7X4j5O@K9xOAR=rE7{*PO_ zl#3&3b*_>s!g;55F;ZPhL!4R{?8=#~%K5a+0qC6+?+O(YUpSAeZQ>paZ;@0K^`^FN zb5EcddefYyC0pR6#E?~Tu?Wq|J&kyC^AO5U@o%1rPOtCJ&}?~3N9F3XvU0OB+p1_1 z&&uhJ6QCa0Ho54flt4TDA8s$oilaET!t4D7`ceAL)`;>sJMg=gs{B|b1qR5lM=&5@pYRU;YQNGy^Mpc+VG_=sO#o|Utf;!_8ihNY55CIz1sFuy- z2S_s%OXR}$P=7B{{U~8C+{c6BRU}>g4`|(rs=X7dLg}%!f`k3 zVj%`C9W*Wp26mVWxG#F?0Pg)p+sVm|4-Na}F{!+Z2{pbD3D7oR9R6?`g6UyyOmFf} zvkS)MJ~)qE#)7b#K5uDZn7uNm^aZ7Looo~|5(_f>)|)TdVAw$g{1u$gb?b(x2Nd<)g2tlkOPEb`kc6>tV*_LGC67bFiVkEu3Q72CDl zRacg6)6=3V?#xM8{LnCQBP>X?1Z6~h3r^hP$kh#Y&pnL0(`$Cz@|=8#WTs`#S+{a2 zXjiVq_z&}jn@C)wI}f-lN@Je?06pmrimH(2S=!a$YL5IFS9ucJh*#K4)WIz>BV_R2 zlT&NSSk2k`U;6>piJyLYejN#;0BStXQ^Fw|L;ZQ@vIJf!b=`^=BTJEDP2ab{;5I>< z=a|OFeB3dG_TO}Dle-9+`yhVE+BeAed^!H-gpFQA-?LxL5Cv!My?ZoDiW5zCGSYpRKu`7azYhxU$ZwkefM$p1)Zs>o%rH7;R%~ z62Qj^S8O+^+WsxAfnG%9{e-rRD+)C2HUW=;%O3`F9(BV+A9seViv>_E18ga|LO7{7 z?4ZHQsNVJYO+XBv-V9l`Rps^(p%Q63LNsbZm9?!3 zIwJ&sfE;3P+r0k(wEL9yQ#R{57g{dD2E{zcj#a&D2ZVPTxk{G>kfdjinRS8ast00^ zNAuybYBklPVAZYXiP9F}eS4!%nVi9BYIhTMNCa%|py=*yrfu^4L0FwIv=;5BtRH2h zu8+R}$gAeMAa5~3Uo}&|MsjW085+9k-a?Z|b*CtG5oLKiS;sV29fZ+&^;cV6L zc3-pp3e}1khf2KTKh=Tk&9PSJfo50f&_PeyrQ|GiO2$kLGhyc=PI6-G`)9uHgFY76 z(d5aTFI<_?Bgxw*UN~ur#5iQAB(_A85;VB_OG0H6ggH#8%#ot|DjsBF&x!$nqNc_W z2fz@6t(Fp@d`t<>AFMOe3*a9qIqbr0EoSr$odX&9eWw{y;BMG6e2j$5#L*3zOSo^# zir9O7wjbT>^BV_(ASWMvlfQ7R$`Z$5ZS9yiiXEFy-KVz>(Kkl+up*iuY1T?PJ*q`@ zO&JM*>$({LtT19^@aI#ehxY-{*N2f;bA~LxK~Pmr+&=JL)D;y8cUAza8l7?xD~I+^govSZ7qTtH&V*5$%>*A=JHm z$?g_l)sA?*%{}thXZ3<}2R}SB@a+TCNwlqTDX0zV+gr11UJvEk2GA6r6DM)&QuIi6Ulgc*)L9&T zK|uSCv~!02t?V}{({ma{r~4uL$y=$|M3Dh3#9+rP-NH^qY_zkzhhjzV+@ zQ4Ye-8CtWCx>Oy6B1I`VVW~{uDCd38P9E#Reu!k}FJMMO%Rj>!Rf`FUk+LC1GZe4p z{jOfE1aTZ^M!aN8jU0hc@MB9ZEMZat$4P4=2PaGJ{{U;aEQbq%!f3dNO-+FeRDyda zhWcimbJ>aq@TP;iAv*aCl+lJIXO5jkm#j$2B$j0s-T>PCcHegBbHqy9+{N|gyoJ=F znq~`JouX>2vvDMP6s~TG1_+CdiI^%*`g{XBM5a}^ZGdD40?-5F4TbHuqGQs9lV*wu{R5mWs37em!e34i5fa=R>`Bv;jwW~sf?Cx~E+;6T^E6^10 zB?y|n%wFl_CI&J!@;5_nrR}4&lRaFcb}VG;(Fh#ic`EtW$(;vUWy>9zUs@N(BXKHE zFn)NTFLW2^7j1rf^RG7Y<(LBjNw#wmbYkLLlf>yGWuQqwy^GON+*W2e%dhnlDyrgM z(ftGcZtYIg<&VkVz{;9MP~c>8uBRVWRu8!($0?kwU_)Wrne3|vL=YNSp#3LjuxH@| zQIwr4Tm2pZ_x=y6|+gGn8VIa+3T}n=bzIBl+_=8-NzIO ziHcurc(NzM-#Bi)!|}1}a~WzCCSEz~p0lz{?U1y|$~l414#FHuz;c?0EVV=1^nCSi zmy8LL_D0Se!+IibvHByb4>{VmC6LEq*;p^YOWH~y;Ii=EcTq_Y9xxTVNm0x$z#$tY z=Y-=4Hx6piS;IEO%&Low4?;M45VHYEA#{xptjwUo5R6UPEK3ORxP4@<icA9qygTt+}DLq`>K>K%C)5O0X8MHJ-FOqCTD;v@=M zOy$h%aTNuP5WLce70*c4ke?Wd*IO*nt(@J}Z@pVu*O40-`D>JOB>wUVJ+db4rMbuSG9B(XIjUD+-Gkt?+N~^fpH=lSLjhy-ytviSEd{+mlrnj=H zg(-I}@E!Y*Fi~m#5Wh0H2^XjKU0)>P~DnFX%VyZ%f$9Kal?bmHan^C`{5P zEkG_S5N+fH?3x67T>Tg?VrVNM4MSva`e6N2{Ewq#oMXtANIW@Y#^~5P*;As<@^Fng zNskxOQT3A%5WI~7qMz6Z?%0yHh?SoI0D){GD{D$?5bc@als^a)94=HMB!bTpZYF@v zx|FAHhHK1aiWE+4Ekn4+5I|KB!hR6&cw1TOk==0voG6LKK~B_G8r+>Y{dqTLG-y8*Cqjg|VjPwCg?B zp?+2KRK%5M>Y8F|*{!(Ep|=^0VK69T4tr{QSZr##$EOt|wZNE~{aKsnE?#_LYt3m| z-EFh~06My&+`hgo%$-qK@qKoVH?6Bqkl>D)s~F`Cr2`VQ%Pb-ABjob}7!pbCpL;5jzMiz8%)C}oo! z-C|xK!05Sw>_Mx3H_UQDNq6kr?GjQK31ly@u5pA;bjMhcXm8dd=YKMK(gr*ElVe z*;48iEGK(w)x{0S_OlhWm0?^4mwyheidgM8uO7>}HP(aZ=d;y8o^+O)BIyt&#;WV5 z#zFcS!fx>34N#7&)K)FaOfOxUj#jR9!8Usti<_H?)cu|cZzlJln*pdm7G8SiisliA zb0GHCS9N|g=-GA^b57c&s4GMu!*7)x@Dy`KHO%VHBt^wL`C+Q-=FPl&$K20DuJyH( zDVKw2U<;M^wgAAgNhq+f3tBg_$>-<0f`m-JmeTK9v~hiLA%&4@r2BRL!uR)fT6}l; zGsWC7U59dfP8+Ae%icA5t@AUw(9#g`#OySNeNdyD&KVuvXx9{uV&zF4r0htf{)--5 zzhY>{evBHh7!ypaF^k-Hm^JEfmW@%mZ#3c{TWND!G5rL*?(z^nc#LC8* z(B$ggS}sxLY}d2lW^oHLR1G9`mXI90`Pb!&>(aK1eZ6g#d}Nm$Cs7Zunm#0^RsrX{ zhlE)zBlol-p4&a3ISF3nt})p9M{jEfAhHGw>iYe{aR~b{%~6wLzKN9K`vktcavm;d z7vK#mOQmyYte?b*`zC;1PXtjrBMERDJuqU*Dn-@>FMOy}q#vx?8oB{>D2cTR0%?2M z2n%d=>h)z(*?L_ozRspiRJT&KPUG{ka-l2aZmdYu*2RCTn5#&0K+g45oHQ0v=~K%t zGgpLN*vC|!wA|GiEvCiI)~msqVJ^k9X?%cm-G0qhSb$!xYNYL!y>64d(CVkEmY(I_ zte|c>^&Bnio^e-Ao-sUWI$j;_fX`Cc7o|62(#9?I)rRBTVFt^wxk}^SLg`i=`HMtj z`Iidtx4V;>_@0JNaH4}*0z!}g^HtdKu^l2VtX@=MMw;F|dwFO;%x*$sXDMiRFY7Pq zcT4_m{#I~j2v%D;Q?XHDw|`f@6yP6A5_6->AGD)3Jf1mcZ@6HO&*Q1oMi_V|8uP zdK6EyJtcl_K_lgp7;zE|#fy)!hfViPc`~+?4lx+t> zY&|%&?Q7k({mo(6O|OkCgwe4QG0R}x$0%XOu1!4z{0$Y{?LrU7~gEDmKI$I+yzy{(TaN(`Bxntp6HlP`XdQ!ynfo)=v zH9N|b^(~;>e{HdKa61V_amK{ym71HR6Do%iJp*+wQen;6N-E)iIA7PtEht40)fbu> zH=v5J9plj~y$ZlTSuP)bsB5~LJA2=B#M3!f=0e(1-BnuSOdnX#K)$6@A#G z{vN)t82OUt2+TRz0rpHQ;-PoH(+mLUm^I?$UN&=FTywJd%v_6*on;__6tRtb^`_m8 zt}avZy?Mp1&!Jf@DSS6CqUt8HiYO27e*~mSuiioK`loBiLT;u7JGTtydfI)Wy6Apw-bc zP6X2yLoOtU7uBS)j)JoekQ?UmSrXYOW(+VB=QWB9&cR;n*u(b3gP7(nFTRM88(;<} zYUQORxNH}!)m`J(hcvLF%RZLr?P^h%1p{+NGnS=#sZB2-ucpOtNwVhG zFGytCW>;q$bQDa7XM(I>6G1#7^?h|uY-~`n2WV`strd|TSi`6r7e%cu!hTIMt6@N3 zw~W%wUP9wh#1I=gpNHKAv4|StIhik6K5wZy)vm5^J{)^3t;ZcUpJJt^Kl5+7+12tz zm24;xsJ|z<^OmYEh3@XY{O9Gg{!}7HG>A$pdI-jsDNT&145C@JWOETT$q{fV*S6@i zmWDh4^uK=oqW7^5UgA&756@^@43SR3vLKy%7GkrPh#1JySM>~V91X%C1ij0U`c!!A^aVny?l zv;mnipzE8cz#;50fWVYPYg0b7|olnzUdw^UY1~&aJssHZ&iKS!0`8D-o8}Y~^FWD^po= zrl%I)D3JikAKRIWFV1@t85*bCjA0F=WycuOgu(^F5*3cvnKb%q8VMgD;8eqk_$t8R zO(m=kq|pr)-!$Avfm-?&nd_KK6z5PDqMSPItDg0eW8z{QnbN8@TO~q;WiQ$E)sI>d zt{ZHcbrQPk4GhH)9II#B#{$kz3wkP&@{s;HG;$8|1D-b$=1(pmph@r;BB>FW*)FEWeYgjoYf>D)vnxmaj13q5(k%&sFg>Nx}Llj3P55?DP>_MOFGTNlxr zhS|4FxM*d~b3|y9NsHYgcG((ZO)C~@RcWS;YBQV`{b-C)%SOvnV)Sf}FJqesxl1w? zXBZYm47qj2T;vAZC1s8t;Ebcb(7>zim_I--J}h8nL5jm^i`Wf@u2&Gvb^cXxkZiUL z+^m47Fn=`FctKP^-pS*c=Yo}`Qn7v;Z%5x=qB#q!##NJiks3j3Njg&XThGUh4PV72 z_~1k3fLTlGMKU5=y#ysv}dkHCNomD%7g22PjNm+=E%5Rsw>VQ^BB@L`G}dyBg^G zC~)nHs)V2}S)`;tIi@K+g~Xyt^DotQT?-?_#SX-$l9E<+Q{^;V(ZtS4_2?jbYjpb+GeX4(To^?&eA+LtMItoc<)b!)H^d zU9ZZl<@Qs{w!L7VjiFpHIZr|JqAOisU+UX1{c_D!rX@B6$E6~n+tv3qaNjY}{NZqS zus1jA&+2vK*)-4N zH|7T-s}mc&scfLE78?LMdax&C*bU>B^O+_tnDg*~yJ5h-5sy1S*~2$Sld>*aeAqxq zh+ANo_;y<(dRTwbvE2jG198=>-QP=?3vR93@r|}oaoAxxG)$oy6QKT&a}H(?kg+qa z30pcu$3nL6{{Kvwu#PY0O}S#X=AY1bFiQwy5j^LgFY zb;E2t+1CW=k_OC*S~4pzeMHRdk|QF^Z&;3D5xA_`BcFsC5@luDS?C6)-& z^|5W*w}Bo*a>VoR_n@57D=v&2E9k?<*c9PS!HH%xNZ#Sl?|QpB-Pt25c=AO^`m#sL?0fbS0fvROf6!;|JSw zSA^wc=UVk`8CqE|qf@Al)(vyao!@73LooWA^Lv_Wer)XCkp(Y`c(T`{ZX`;1f=qa9 zI&I3Xv%58*NNh$vRd9%Av`YdRRq;>tnIpLN7j6ETcMI1r@Ym!&5pGvz?L1G1?e>mOkA+t@Yy8w`=DOQTlD=uSf4W2zDBtuW5e5&mGV$`OVg__ zi-E&*O|$UTsM66HIb=I_k|L+$KS-^68AFaIry019BNNRYVd-5HCTq-Ip9<^M0hdyi z{PLfyX&GX@-Gu$oy*dVbJ#B(VjUqz`8+MWgL(%0N=UF`obHCjWmffUz-*(W3MR?Y> zxI!~%uyWgk;xW|fgDg3ioH`kp^5*#XY?5$B%p{M&hvjw!b1`A6KZ*N#Bc^agvkT8b z;!6ICI=a$0d5sn9=A>B$37RV2EsaXMEY0iaWF|x=H1r3&V^|qG*g>mpLHtP6kX<*ExmX)Ju|wLYQ5_9n~$Jrh7OSY z@UZn%^SCps7=%|7Y`3mwTw-gl^kGL?Q_m+M8-$EF`E=U8r0D|{Mq+aDFCc8KUrfY*#`bY1zK zL&u)R>i+=LKKSV$&u`7Y%q|%kzTRnW5msu{mdU$IV2I5TN=tBk#m4)IA-Ri#mWkT1 z(Ia-a{{U+DT%A&fPLm);{em|ijj|S5>(`XoVE307tWm8|yq&5fjIo4Y!Fo@KnWN8S z=a7P$a)x9KqXFk=(m+Jw19lP@J6s~cA#KZSRw4eAi4@eOS>AgrW{H|-`Zprc;(S1M ze#p-pVzjTx+(T=lEst~AeGP9Pv#hHe_L;zRE;h}D@c|pPFPtmCNMcqUYh*^5ELkBg zc(4d6qJP)4%WQoze8NF7kPOBiN>IqKj870%b%}9|9A;QUibOc6 z^Gp$r2~zXjF?VdTFyI*rvPkKZwU3`K4|GP=nPEC|$6jW>jW}toapsOKqX}oTCfU(< zi{*)IJaiLF10F#j8Vl=T1;IHH7Qr8IsZ(G)Cb@-Z*PLcgzA`Y%%L%2NSgVF&K_zP{ zBUyD_ZQ;BjzBFkXAxs!+dg)xW29jB7cv7nW08Y5+YO!G5tmSSQ!VF|2E}Q?%VYht)$L(xINw zBn@BFb(MbmV)EK48(yy#@n@H>mFXYMeqZFfAG@tP6Q6iakD;I8YBZ|>wT`}URh_b8 z1)j2!-tLMJxp5nvtLV1Y+SI8i;;;VJ{nq^=cl`4Fqx`|+^;?Fw^wjNW2epj&mZ`IT zkA~5u*Cl#wcMWvNKJ!-1Z`s_%kn1p1oaB~RvXB{kCBj&P2s;NdHohVk};=x zFYT;fE6kCsoKSxI37C?rC{MPp9rMr@GrhHET*e#DV5y>9E$(pz!rn#< zjig8EGq*t_HO;M->F_0u-;taN5mL`~p`Mh~4^y-Syo*Q+DqvV7U!bN>ki0OC&GOgO ztnk>^%q_RMQV^@c#a4pumL(Jm^GTKKL5s!P59GnC5VxqfBan`@(dgDAWN=WK4gs9W zNG&eMutWvgf;g6`)(YGp>rjarRR*N+}GXuR7X+P7kWb1ohI+b9ar`{UivJ;9Tga-zcR9>fWzAN*=%Zd5KC3T&VxpM(7*Z8a))$XHYvymt zier(jzNm7#sV*U^>3bTv2=5hkbg3zJ0Tgd_NEZ~-x;#rE6>ia&O@T9o-KIRlU}I0vER?UN#F9HG z+YMqDQBhmSN6{DUj9?3B9lk^NlttYuVpN;8@{V}YQvryY}sIA++=%EwyS@!dfr4r_Dh&G!c%C^39U+af|tu|*fb z%(O>Y2BWgs6nzi1O5jO&a#l3fjR3Ysx2!<4bSx}Y%4!D9DZ(bc>l|r3cFtoBs=REl z7R*V7R@nZ`z%ztUTTC<6wrIGZU*}du>@~P^R_2TQz^swYJi)J4?Pk7MzGChaw*3(v z!lm+#Zp23hh_#n`Gh2?rv*0rFm|l zdVwpU>h%Z2*pf}jtZCcL7PaZ*3d-)fXzBdv_?1@DioNY^Th+${b2#O8La6bx*SvVs zPOucL01;L2vlqiUSMwVa+SQoMhBl8mx39bNy!-RMe=h$35B~r^Xli}9qGp$k>g+4U zm8B8#Ln>&n4`jl@m7a|ojU+MZ$IY>Hn~Mu<>6r!cDGW9HmfUwkkDTuY&HKzWd9bWK zJLU_W?mFO+jjCAig*bfu8|3QI^nIJY zE&zj4%mx85@QJ;i5K@@suK_GaRrc@4T!@mVujMauF-D6^qwb5c$ai79EQ4$Cf(a`r zt#Dxp{=f3z8#tj0?%;?dtHmk=v`hph4Yg<>_wWt(Iw@Y4Dg)@Ru@g@aqNn~EH~ zNqeVm)Dmvxt0JqoSk2e<{ZDb|S9Mi89c8XWGh(*r%@?q)-*;Ln)!n@jAtBS>D#|<{eMpttgNjNC1Iyv`O|{$ z!|8)fjUIefGO~}J@Fw>C8aJgiLyzHn^b?ElbXfL z5acE~D5NnmiY2p5c!l+PR&9EAKteuw@6z+jUO3C!qCOh-M8w(C^k?+lJ~$f z6kMzc5AzuMK8QwMts{zP)e1!D7)f=H>5Hy>D26qZVG_5Lm~!+MhzYAGaiU=--Fcka3mIhKjxgp6WtC~!SAC&!xY&17 zSxHBuSM%gVGpJE`2G~9iENd#oV0GiDO-|JQ0c5f9)wOi=A#L&kT)RxFA;wf7 zN><_3Jua1^s5+EzG=|-BQrev%1%~s5hl<^LZppNZP!OIJ1@eo1K9G~d8Oj>Z z-Bl*s-fOe&N_+FJ4;6lVqRQ%?v{UBKxLrAX-0%Y22Ids4`%YGGEUH2AI!F-Rn>-z3 zj!d~z5!}j=0eSwB{>1);dCA|I{{W1U=h?*9)r5|Es{RAS4M^v z$=*)JBnzzjLg#(4!q`lJ};6_Mt~}Ou)8%&m5$vQ#miLfFbQHlL=sx`#h*Sv zoOT9n1v!NT-q?Eqo0KDe(ewK`yCC>OErSLy3|?@_M31_O#VPC!+lTBU9zI)z$)7oP zOs#(DnX)9QlE%cCaG2B99hG!j*HyxP#I)=WfjDOgPb`Ryh{*`lb1UYgZ1y>ZQXfno zAi?n93U16BL`By4%H_gLVNs2};SXDyuK5`#iDVrzOn(~#bmgmTgQVKUDd1Qvaxbc= zu?$9&fZwbAD=o0mH`z%iZZ3H%ykeD?Fr`jVTocmjC4{x15K9A9G&f;$PJa-I8Q^Km zW3#sIW2>EJwy!Hdu{7z>;;_!0^Nb}+24E59?9H+)?IA1=T-#0DQ0s<1O0gskr2tjt zWK@_I(ObH^{ebQB!PhpScfZkCHx9ZsQ^kz)&bC{f$S$z_ccF|_hu6uS4%(BQge5x5 zuWz1wwd7RtEMP4=>c&mUpe+fMWzZBlVb!ln39lK}t_yD8Rt*RFY{Z8(|hUlv7kf%S6rt@Q9h3Oy~w$ zKhofn07uXgAA(371g?xUXwcd)O#PCD_``&G!m}hcJh7f@m9W3D{fNXwoug-3maXy7 zW?kBB1}}@N1M!>~sC&)^W6`%+=D?wt3a~7j&vtAQ0k1TDvutf*m{=<3)rE#Knxan! zIa|h^kx&V1f_GW#tEQanh1#)DRHn7S475S1rh}Ac#kyBG2(6S%>zgpUGlMl5WGs+S zM}p0%c(EGNnA~Ent83f6X)?qkU089~-bC4@=fr83r&BD+E_>HVmNc4Cs-81q12D^2 zkj`F^Z1LTM^hjm-7IV5|tK*v;VY{PMg`_65*2(heij(F_dHGU2Io1F`WiA{u&BOd{FkXRE1_2#7~(K3cD=F9GcRd;*L1MZ_HqO(!5lp3H+ z(!U|KB=T{W1u-^C(yOFQ4Gx^Zo>s*DYyEcVo}uHL{cySZ=oJl7vv~)dYme2;#t1!DL*%mc-a?>D-v5zomU3)*Y?4W+NDkdkY_}gvRomq2d zIDS`isx7^9+8w#e91l|b6jJM&_*)GD{;;=~ba^!~x&YbmY&GQ9ts>nNXDYaw1*EOJ z-QTwzyJ~WW9sm9icxvUvfSG8DFm{zMo{(6N~bK#s}-IpvnN;UCS? zAWM<91cG};4<9hD&jQV12cXEjB1_Q_#)z1Co?m;jNesl!h4OS=r1-X zp=BJ~+-w`>+G2jSt?QB4EXc-S#1Xev*t5t=0AwNJ`=DgQk)sneOP3Gn8%qh>su1`k zikVdzKYSn)eml5w%-KEyzwWyX$+M}|;Lxpt=LBO9pt}=4k}3ouP+)0=Oxa5Ae`>2R zkZLkwO}G_1d^JkiPPAIy3ObwQfD#-`2ZY6llGiP7We#MWvTWz5kA_vTZkD4yaS`O< z!8rt~*@y%|=(_utXz&p3wUQPru7IdE-Id%r=vLKKKrCeWs=RBFOOnw@wfX6GbwCy# z+C}ujHIaP6%zd$JH`>(jYfr0^X^+YZZ=3s5e=^YgXmVxDT{BP0PeT*%F zNXhF>IWC?D>Cn2g68AyRb#KNgfHK?R!q^3aV!<-fiZNTxS7qGp+hE$(i+0VcY#2um z1}jb=3pb7vgH{GL6J!F*r+a#Q`65o})L~?t>HC*Kx-4v(CKt0wlMP-Tmpb+7Yt&YV zfKD#fs=JvY0WFbJruU)g0+?=KBH>i@JSZvFsWI5~Far~qa3@sMH7TJa$*43suCR%b zt(A>_L%<>Lgi0A%XS84?yf-ngKFS=*weYvV01_lN-2k%Xd+W@l)uIL6* zCAF{m>_vJO^Ltmv7Ml&y>2T=V`L54#Zn@nBX$(d_$P99=OQ;zAnPwNOoOf zAUi#I9bcF`Ns&Y*C`-uTlB=N_llPeP9l6wQx1#dT%^`&tUt6p;yel+4N)Fh&PFT`H zsBPA%=7ofZ1tjYBFy}@vuvMZXk*w*h_^6=_n+7JxWb!Cnyxso*G;7CmQ=CBd3tE|1 zb`A5R`01!dt&Pa#H9$BlBZ&B&dDXU@%72ipjz$j*V24Usw!#cb zMy}qoUYxB}9V>>Jn+IAbI@PWaqt@liMrR(~5P$Hi)RMO*2EafCyn`LMFx9)S_xRC4r0kn5UDFhPPd-jTDDd z_?ELbbMOL!)}~t0)+Jj&F`y%g@e^14^@3g)-`guTIUP4DC=5 zV7~R2_-d@VEfoq_#-KU_?8hBs{U5BRn4T3N5t~FGqzu(APop6FyA3S5SYT^|5=8ww zVGb)bVEoThk7-?)PkDGSCg%?+l5*IfZfey1>y|PeCRAI8$A6faJ%lVck>QgUzCN=e zp%#u-Zp;qBhoo-l@S@KfjN?vG6rgU<7hc&-)2=HD#kVf|M%lQS3|SFEkrF7H{{X&2 zZ1^)gsPfpRr?DCoRL2RI2$kwh}Ek^^J3+QQQW5lI~PWP};Uo&k|C{Pt!fh4y}Z==r~GB z-LWHH{m-CCTUam&VIt-^Xvy~_HC6zCEWn4vDYZ3UeX-GXl?5SZMDuQD-gTCd5c3im z=1szCiBapS!OqD^Yt|(iaNVv?HLlEzosr!wLcH zp%Ti~oy}KOy**l@5z^XLhP+~8uP0UyRPCA@U0-a6Om=ligsoM`w>G3#mPb9`fo>6< z43YW8$-kexjHhcwvwLdy{N+jIj+5k@92kiSu5uB~6HPYhT{OiaIx_2ad9BT!SuV!_ zsM{oYE1&c~_8;_xlY##LIR5}E`3*hs5`Kv@Ry8w2S}4G+sM4hJgockXA7t>7riv;z zeBj%?IviOuOR`~SjO;$w<}rOSEULs(n3L6~FQ)8M@_Br`4l6B@rxqEg#KCa(WwH^m z5c1u!OpQTgpIkqjCgDLh#>?FYc3`sC7#7SjV+S4%8jnm^+2ah1@ZI*4A;-qem@sK2 zip}+QACNZry{5>toGp%sCu!bRA*bfN1dp1r3PCZErkN>>VrF!jkP}R%LvR|`9Tdti zszJjhIAgU$-{kj5D-Cvgt%E+yugAIg=TGB5VIW&=S7 z5L&asTF}@lkW*8V>5U3|7pCK@R8o;!)=U^%-1a4lN&J|LlQ}#~E|$k}vmjfB;4U!} z(3dZ4T^npMCi=gjEE&Am5iWI$to4uMC5h??Hn`JIuSlP|y|ek+pJ_R&vu@E&j1fz3 zHa42SXD*~Sa;b)=z;MuV+P$bg}(Y=19=aMLcy{T}_g>!$uxYv^SIM{N@(E;=AcIla;m&a`2ypz0%;OGS>m zuj704!C?dX_WqkPIl^leBLW2~r(N5z_oJjkNf=5x#_Gi}DKZspRxl!NPj@*PGZxTp z>pAY1;bV=!iC8mb%UrPpnEJ#Hz}*q#MDlTDPEIeVw1#ejzxqP0Ux7{5wjiPo^K4dP zN0MVAGFxNk?0iI?i4J9G-$yh}(d}-qr%xuh$0fN&!ei$qc?K2$6d#+8(lyg`&0!yt z6E04Fu$W$w^<1G)St?G3WTP9bUUw?Qo|R*L>RbBx=5No6(h#Q9*_MR_%D;I~_Zk|Vi7L5bG>I_e z9D|ySt!u4BG<_CKvt5hOKdPUsHs=g}T>fiu6-92#tfWsTGP)_7Ce+PFb;*Vd`4yO^ z35NNjN_33Nz|KTDG52pDuXr;vpz9|;Tp_Xy!h$wT?ZfAlz$A|}69!sjNSdT=mAg{Z z!Wu&2dscG6=RaH-*bZb@A`r~EIBx`bRi8vTS;CboIPv8L)WaouUZ0`rDUO2q2|8#@ z+#(5KgBNc7&mcop$@Dq`>$=pUwVWi33_5Q?s-U-{ zL{<;#$S9PxKV~0|K9ai^2M8f7y=3ZP!4ze$R;cB3(AHxLbk=YM-&^TuO%O^!9njRa z>iF%c3YW11L2<|;QyfnKU(_4@b`tENt{MvT<35@pOeJ0~+}~hW!?alxpXT2Vb5(ai zWMlNU-mN8bw;u-t&MP}#o|;PP6*4)Onq+|$8)~;9ndktln({#Aqbftj%DV5Kk^QIr z9kciY^V9NkhygSD69xz^Rm7P38I}gQVJ$$X%xdKzi8>X;TFwzRV<^XxVtbyLQMQ%9 zZ@Z+$4$LgLVv<4Vk>o7q4q+b`IeF;H4*W&3w5kyq3Pepllh|Qwz}>T8=@{m(xNC%B z8$w5sn2e3Y^_31$h%n3Pi#|H>`Af!~RN`S588VE+I8_pMb7Y5v!I%j@Zj>_?2-}%# z+d}iT80{-){i$)62>=UFeMDw#)D}O)y_>kJOk+H8S&#~SLSV7eP5IsCh4v#OO9QB7oCt}JJ6YhKD?rB_|&A6P2Jm`(<) zqbxw_afKLvQ?8p1PKgbi^TxsK%EPSXmS&cG0}<$Y!MN-R5+0y#o1@|oEN+eHEM}~U z9F0`lb9Pq60~%_O8NlAdmdL!cHSb8Xs%kBpr|L=-xs_=XcxGCy6#(98v#y?CY29DB z*>wvS&p#ikS_ak(`L?aiu{Q1mR(j3%Oe(iHDSXa}zAM0R1P>(5`8v#>l`Q*H+?A8j z6z^Fp_%;$v7u1y!b+8W#y`si(bFX=0fVr2HP^)xij_|kbE?uD#s_Dz{4};S6u}W^Z zgCs;@3S&WqeKtxn0-4l0ske(XWvOClE>-04L01^Ij{#o#8bd$?QT~g03kJB{T zw#M~bVIPpcxy3dvlNrU>6HL#1GXSu!B10vK2%jC89tIigjU-bZ<*Z_v)ab>@>gP2N zHYPKuS(7E6fN}|D9YZXn(M-ddB8v3PuNl)7rZ`^bihzn@G0@UnWsj5%z~apmhrC6I zcFb8TbsR?2vt3CLL?ZaaZ*X+TJ(nKZH%*i)L~`g zS;G(oy}bpRe23OT;?0!@WssgW#cj2ln_dGgO5G=0dM~OGh3AdjnB`dd)I!3sY8zU` zP1y=birtiQ*uWW|g9Oc0O->HHHm|qUglpD4rq!*JtalpgYMw`tW4PJ4=B0A6(+60f z<-WKU?#Wa;{2>!Fi zgXCY%o_D0{3WI&Q(l(vx!?icu=LMe$_57ZJq3sI$ENk*_in|IyAr4%{XwS$bqcxUH zA(rNQTlB~Dqo?}zhy0-YiQ_@Db}6=I0fdEO&7_#v(c}ScSJ=!orC4JKL*opHH>x~i zwg7a<^R{~!upR#Z5`m-5vWDVjP~fKaFYG@K7Q@h2rbqKVaZVEnBWNGwvthS8YJIvWfm}oBmFf74hA)C-6+;ZL8|%m*I~f6 z?1!s-sFJ5+Hr8b58%E!??I+W3&31zN+GVV`u4-85%!~S4n-t|W`{<*LQg@hHAZbyd zW@$-kI#|-5ahnV&C&Y!Gmse++=#`{_R_(qr93`f78P=oG zOX)ZRgGma`>_lR1L``XKLI8mV#W|wVQ7`qwP_dx*43)TWkeG*|9Zf}MjV{^ehWh0+ zP>S`S(lu>rOI2BiXjv5(NK?ro^`G)}Ut`-(V0re=fH}yuY-G9z*0RAR*l}$Xv1OU0 zfv8)a8HChq*PS>4D)g*`^c6{vTHA96EOf4##_X-x?UdIS`p1etCn>rTO&+<}_Y2Ql zaNib-Kw%vi_{A5ccV%RTCe(%|B#cNcmU9|~kYq2WB^lBpjW@U%ec>U_n;Qi)@i-n*KH4>rh>Oz@NbR;!B+ z-H@g7mWc6t9yh6W6;YzSy`QniQ_EQyZIWflDcfQ^3r#EX9mCU(dgxbA2A=U&u%Ryp? z9l$GeqJ^84MwrvK#T=&R6>~jv3Z0V3tR=vBWH=ob*#-yIUH5L z5Ps^3)G`y0?AL^-4XEt&+Rqs23mr%>SriD?zLtuVGn`cmfx?5Z?(u1PSC$-m5|{ZJ z@_wZ?v9u+s-jt(XffMd3!SbZJ>6iN^m9jTdcP(9G50ZwNDXNv~?pU}vcI&cfb@kq< zFvmx8%>ns`#W0lSC`^A-${PDPH6Pf|!e!gk~rFVo=Hj_HR)TJ5TRd^x6)f}U0 z+L3)EbV(~?{)_(AcRNQKescaoa-^}LnF9ct<5)fjghUR)l9ZfuL&PZ(!t|MoJ!p=j zi<2Q~l^Z=1py~9_n{-OoJZRDK%s~=Vfa$rAT9dXeQSmk%pB3T~gh0+IKqpuNubPf} z7QiJ5haSKvy0Q49$>z|%tWk4Nm8H8X?t)qXz4{-5n;xPndqkcD=5<7WT1PGt9(2(Zwj)H?)xqYy zfpx?-PUWra2TMXIt>fK@%whzlp@|VlI19NY*Wv6fCrv_!jgYW*mWfU$Ms7P?Rms(D zK%xNWISQPJLBL|Uz(GTj2X5;EvU%07CHny#xT@qlPY^Y9B*E<%gko6Ds4g3KlkKgjQ@qn&=c(YMEB^$gFu;gVomz=kB&8*^o{ z?~6{+=~l(=gSK~Du`A#*WfII9%Yd@6+6GH;+Pz??*q&-IIIr3LA9NEF9X+%vj)N$p zo10qG;->Jduy5svB49mg!Y+ITiI>i&0And2Z1m zYEpIh%WZHrYx7B@TKx3cu`2ckGJWwQQk;uVO`SthPLnsASk*0HI#M8-rqIT zr#D``A^Ats*R^X;T!eDushITTJu}pul7H)>yBXj z{{Z~w{Qm&MFs$lvNC}G2db4Vh0ygq7fHFoIBsqPCLuxA<%}0aA*i3P)SZR@oJVt1f z8%W|Y_a4!;cOAQ9$Rj^sov|fD-dZ$2h(p3)jRYc2Xv1`3HDS4t^=g9|heq(n#;!7N zj=N@v3dOxuhj0<@gGNjVJ2^qfvP@cBFFQms#f1lF@Q(d}VC*fLk#Bd+T#*FcY!`*Y zbToE5o)=ZN+|&(daIs9A@KbaZIuXfPZjxIwaogrk6Qr{hR9GRRjm*s(80~VEo~ILO z6Q`}&3vNoamMXI4AAxP}S$8m!5VQD*z9>8(z^jICkdUJumC%YP)5IWF?b@M_E|4 zuSj8EEv%56nJT8@I3Xd*OyK&%oYPva3ivgLSG^?=9Kr%arFFTia*msWRcB{>+xt)Y!PYI@U-*?o zaP@Z|X_~=-oK(4i&IOwUN899v+*V{2R>(|<+MEYYjr5;<*94>~KT0v6CRl08QvTBn z07iX0bg?lHmkL}!(^{qKI68cM*XPO|t%wH3*n*agXqT6oq(~BC570@LP{Jn6929mG zVgeIj>TJNUl4xRBaOaji}s)07s;^&KZ&NkRp?a0lh~C*K;qX*etr|Ya;U)a%#qy zE9t9|T5GJ-TI*+xmPL*p4U1P`{xC=o@Rj9tM*&r$cx2YI&Bd}yD z=0s;-PQm&+3y{FzuQ320TP7%kDt_#yEK8h|nYoPQgWYgoSzkC(7G>(5XiYU|mdei^ z`fJ0BW5|@hG-jDb*?W(e0($s66hPR5OmKgTxq_te5xKju=_}Q2E4Tl zrpwmICGueE=Jy(pdDhg=RkL&bQPWkAQZ260t9J!w*PLZws^4oX##?L-4Ctj^!{q{@ z7P@AyrOirasameea+qR8w&Pb#SJyb-yworC6%X>8o+!W1hDvQpojoV2uI`BJkI0@# zZmD1qKy%oQbExAYx_Dw-@OnMjTb~iL3q>wdEnO6ZfVTp+ixz+G4|4bOzbJlB{z7xI zJ(#qmCdiRp*j3wNd`n2ZO$@H>V(~R2F&<9GSZTRw!y=NkLDMXx08^&}l_yLBIKe5j z7~>~FLfa}C-4M2BA2~BLF+-2ACdbebbw$V7K^eqjDn1hCNlPTqw#+AFwM{Z15Qu9a zzG74zyIthN+d;=lr>fKC?#oyS+jJl0ekR!ayesPV|eV);k}cZ6fZNOqElrTDQ&f?+JqoPN!ZVi}yLQ!O&9SbFi=C$2JazhA z!WMMZrMo__r}`#UHfG3T{EzQ}iPn5t5cgTEDA6CITK@nc`IDcx$zsl|IUdPw@g zFYn8gZgUa3-LI=>jXLl>tyqTj$CfU3=ybsO?fOds<858>w1Y1ZthLsk+uqgvB!A*x z&9BC8UjW3%KDM^bdtZreSf!LU4{3;F#!Pk3;JAWu6PcCyj&?Uq zSTDC-tY*l$jlPXwK=o19Z~^|7FP`t1MYe=&XivtnBNH=UW1-BWZl^4}*iRy_XzUf0;F4h+DfltKwCFT$(H zN08(KD5?eMu)!GWKIY$FnucdUSGI#&lC2T9 z-LY^%Acbhj0FfnLIKZq(+_QMvb#E#NO6Nkjb-g*w56@TS z0FBT*-O#o1sTL(Da=S8Cvb}1$N4j}52-m9$O&gn$?uX|5s;wdTI!qC!jpTd+0lzi0`YzFDP4ei4L+Z+n%8W5ueW=z z_XhF&rs9voo>30fAR>!J&s2b0win8`Rp|`vyV5*&S&s0*OxdYVim0Pz$kIyz`g0rP ztR}=JV1L6RFvG*?KMR^bd?}&~$N0t~HVR~eCN|t7>>>t!)#q8rPLN=b8XUi%B~8x6 zsvTfOn5uYjKxJ?bFrdw>iY_SR9u=gPPLN4&GNmyRA*bQqN*kx*T8Y6MFvz`F$Q5Bp zSUA;kUH1dc-)=F>w(LtXC~axNrNq?8v1=k&)1YG;U4fyAVUx-BeOkEa#w8KL`?2;^ zS3%^>Y)ny<8eF$F2up5ApcWw4&YxbV49zS}{eM+@EV`#Qb7RLbQQ+1hsxUwfwmk0i z+d0Aj2Ek%0avStlAn&NiQY5|+VMu!2i+xDy%LYrv!aYM4VY;%>xohUC?$~Y*Y@4rI z`TqdF>)$54Y;4iJD)FyE^33}sNs#9z3qi0q~3Wsgj}alhzq9hmFC%ZQz=#ouJ_Fbz4`BwDQ{U=)hzm+APgYBs6N*6q%Fz!nf)u;3$lh)@88< zU8IRK(AFBfOJRMvA~?u^WnM87o8(v2s*wkUG8tU?Fk6gfcBWTLi`1P7%XFJ{PN%)1 zse=)X9#SQukf&fYCjzu%GXP?ih`S_#l{1jh9~|HtpY@GHxsz)tb|@BTWEeg9?XH^1 z%KkNBrJ>&|FP5BIIa2$2G)YG*c9X9#X_-#Ru0$P_>txvq1;p&<3-#$iI;7CbqC`JG zLP6G9MUb4S%dU!Gj!bHopE}xDwJ5XIZiE@vdHbPk9(xMngpC8WIH#aOYkjQg%i_kU4PW7S29)5x z&L!=24?s~4xMTXY(_CR$(R3KmOiXpli;X($w_>2zT^$~hY>a6^yG-RLqD!f#i&yVV zHT&IX)K=$lewbPP@l^g$e>>GtpmQui=|WBU_K=N{SBO}3qZ#z@1)D=aY=|it&}rjd zjmTul`DG1|s9@4JMkDs&c}ajqFc~Cw3md9ibXD?RY0K>-{2TY$=&oR%&bK6)0m%~xtf)If*} zpe=YzW_KZaRbj$(EDZUU z)J8pb8>IKVT8P}cZn)k&vx^&FJYNaH7i*#YBSm+o6A;5xzYpbb?_Vr_5#p@WR-<6kqpSVT%64t zM#07$Xhh+V)s7b003~VbcNDEramAb!X>7E_F(x_8^Gr3Vsaefh+jO2V& zfbID)q$HmeN-$@*viG4ZrK6UH$zq>xa{23c+!wHt6-bdMEF0gZMsgXyJRiJ@1Mz@ zeWbrTIpD%%gjMysC$lZYwN8ZQerm3ns`m!!(XQI4H&)L$A?~o%EDH0bL{h46uXTww zq-R>U3s6gW`l;LZ5A~kf*46ms_|M2{T^xqFsvWaW8=Oo|%esnNu@Xl`B2}FHg#$_7 z7BH>5T&d%I(ncH>l1gG44Jz!LIACee$@(kV8g|0c%aGio{SdC1xdnl+kDAJcE9Sx8 zp8>I<5d8JRE$Ne^V-OvZH6hwsVZgIJhI4cc6J{uy(;9mi4P$U30Mpl`epi+D9|H?k>`ZwXTOx~WKO zAO)=3U$UKTap2vlOFFOV3er0enxPgnD~aN(MtE)}>demBN6jg z+O40Ut9SaFt`3pOV!;r_d7vDu@1B#b>n@Ar3r?S6I2K@jb_qSsZE`J(b-tm#v}@-& zr*qdWu^p=2*uV^*<%M0P-cB@y1XgIKQ5*Sd!A8q63L!A~~(vHReGcL$bkz zj<`nWEsgRy4xPI(6&$`A8P?*;!(_NkKrr2i-?ft#(5)*W zbIFe-ph?Mt0F|8psFRD59Md39j!GM9_@`m2@<*~r!cLON^4Jz+meHtIxU52_!Z{)| zC^B?TE-PUG`m=}hxyOPmGHSzQ)>p<`UeCL3rk;s(V|iy2=+^40B*~k_SXk!JrLmc= zL}JDEiJt_UKIucc4RHN|^uV+Cqy)NDzG_TWN-65vyl0cntnUtOsp+I^`ahAl{OmK; zu^J*F%QlTI0t%X=(v)fIyF-Y1lTz8{TB`TA%Kim}Z&d6LFf=ZgxK5q8>c%D$!(b6q zTW+ytWrfpSgJ5xUw4=sTvDNN3xm>B$?UU(}>5fqT`h!BUsYNO6_*IsrmW%8tiwk_L zlVZ$=;RtpmJqqHl3R{(u0-tRq&=&=I=!<7lv&6kzsg;dK(Q9=($>F1Ss3Y6AA1>*G zdZ4i6WfIm+H7l+5LHa(A+4d!^}nXLuHm0Ey>RIO2!tCv4gNvuHFms z2BEf!cDW2~72p!jb+=b__wT1`q|~3D{{V_Sw3(f(GM%tZ*Ov`#fzbz7b-hKc7&)kB!)b+?9U5`lC|3OSN#D1@xEk0u zPoi0msD@#Ksg?`~WM(c))rN!d1Q$Pofr&t;89h!#shK(h)(UuJ?s8I7-2d!5?X*Eri<6j(iibq^h|B1I}yF4Xf(UrfCH?!xnx z30MOL7h!e<&`T|vjw6n6$enoQBE5k>Xt5F1mPB9HE#pErSc+Du4ru)2t>jn6kf+r@ zU@j{fWCOK)#K>UV6N)62t}W{enlDM|M@c{hWwyhfqhbYq%XdL(ve5Ls=(n76wChCT zismd^9={>I?Pwcaw=YGNS~c4VdDcB|I;yWSV}kgEp;OjO25L)ASe-6I zv}O}j*E4b}te*mIyKCVI!d)6MsqW=C6Wlw$D+;h5ho`S_NGWqClRCBQA;bHLx zU9Dp&5f4aMDB_vYeP8>r`u$$;=jTV`-#OE>ETI0xTw=8;-PT=g0^O@OM-Ivm^2;m+ z7>E`uZNmghhErD0qHV~CxLz8G3mH4L?A#ACh6M<~oIyk;4c=!|W2#;m4G@I*ac7-f z5X-TDMDI10$Qiv68ivFjG?=XzZhfr&Gb$zrJlgM49{9q-xg!ymT^A+o5?&Pc1Y#1z zV#V|^?HP`T?lT2IJjtv>HH&24$L%hqYqKmHEVd3xkW%e=wR|(<=C-ykMQRzj$8$tc zn4AuWiodJmbcrg}6*eB+wHi2&QOuq+srmqp(-ttG>};IkpRt2nBy zw0s%$Y!S8JLUYSNntsgGmx!kX8SLl}+rw^xRl&`4+ZvV1ooY*iZ3H?o&FZk}pH**g z%q?{rij3Wb%RM7nm1~TqOX58`+nbJznQEm1OID*Tc}T6RZg*Tf)-25rtPMksa9Y=q zB^pD0cX6bjS2ju{?1xhIg=7N5APXRI=LfRa#4Z~cgLYW_`(3?m%EVcxY_`gN!}2nH zhfX)dw@dSz^B%jdpZa1w*Cz!n5$-Ebkelu*@gtvmZ;v`Ax2eiqochOa+SGMAzh(6! z?N|8?U+}jr^N;Decjo=6P~VlX4%7VxBg0VZ9#7qT^A}m%ADeGOdQOoIdQxX)S6vH1 zweJr#nNb4eudSNo_)5I>w<5l}AN!B?!}T-=4Dc7@pD!z|8C7CDJPCs7wxWtnmY7#m zT|!Gmm7&?9xr-=*kq@(v2WtS>X631G$0LEZV#os=JcMQrgL&?X82HSm#U=!Ru#9?1 zuficJ!mDKJco?l9oC7ILqr**+rKm1vplTEByh^HAQNxOFfeoE1^`y2GSCZ2aWT10# zV7d#QX1QWsJp^>u^zDJ=mef479DO^S0}QvNk$>7ZGexIrlsRVvn5C_eU^@&JMV2$s z#XU>WGR2VtT)|RU!#!A5J%YU~1$wyKTJ(|lr`6MG`y6+*4Vxvh+@D*)xM2e0-gsYA z@KXFkiY~PPPe8E0>X|{6`k4y(uu{g|HZ^Ns8^E->?gNsu;zhw$FjzU1u(l^yr`ux2 z@Wsqj6NO8CovCb_5QVw)+5j0j&DH|hy@S#0I~lSZHj+k z6?$9I3YOffTWOn-`jbxTPHwT)u4=jC)Rw|EGc9lW42N3ah|Xs~q~?N!5?{iWqq<+2 zaazjlt=d>A9-I`ekWx;+a33Npa4&+>ajt*&|>W$~JNLd+bg7CTYlsC%x;S_7*%hmmv z*4U|^R53;xN%CsuTC5o28jQj}JoFf(M4M!TVjOpRQinRfCp}u)+RS#KuO?a|=>v9} zJ2S?!^XM_$$6iA8j0GVEfUqY*FGs;mAk$E}DIvy*cc;;X#6N7RwKQ%gS#(WV<1-cC zy`{7Zrtt9KberleX=u}8I#R(md(sSr;(a)JThtp9_P@QA zhFsO9XLtqU>vrYk`?jp-UZs2Gj*^{J#bUFZ>yOUg*8c#){wm>?I=anWp~_RfR-H2om` zFX?~HZ^@6(juk8%PBu`g!NO1`y@)U@fx?0_gOMf98eqmLF-WTrg&d?Ul1VT@fuVTm z6$w8O(TT|#HfFsJZiyp{u!qJ8Zx-cCoC0y+qW8}Dc8w3o(RZ7N@cH_fM1o-pKOeC2 zG@VSzn@fmej<#&Fi@YVlR^r46z?o$Ple^4t`UxrIKXMUnyBlB>H>G}eWgwl=8!2;UnqmZfh(F*aGs ziErDgteAnreVT~DVUTmEI5=7aaqc45=J^jy#W3Nvr7kba09jUUy81w>sC46zSOY28 zGb_s#&ItEQ;l>Ft+n)efjn33#ni~c`2EV#JOFB`;-UzPGI_3_!W1%cT3vhv=Y_$ml z2A3IAC^|9gP8$7Jrr7BkNRm_}ZgPtTx74u%EWz$bT5JJ{a^$1i>b=I_+?isL2D4L9N*de_is||D63ap!*lX`vV+))ucE8` zukLY_O!um8C_ay%Iy`43^^Kdw7mR+5;4ZYI`tH{Jd$)fzXg|+s%VC#ZX!`&06xdzA5J<+k(F%YSU5k=xV|g5mSj<-0Ljta%ysI%AB>MN#=O)W&E4fV)oxve&YUw zH2!)1W`0=n0D@ra+hu~Q`|Yj6BrfWG50KTyJtcky8Jd2D+zDkYv<&i+Gd=o*Nj5qJ ztr$($GdO1k!5gw%4^N4ZXK2W=z4msk@bpRDt8lFShrVguekivDm*~xmizT5>ESVvK zAYx%065U6@&&|Sk>i}G@UigHv=Ip3?DiP zn)SD>Hro-wmd%o0MSQ@quzdL};Tsm4H=?*Ok#F@4%hmfWz*t_Ua;C{xHCBZp{RbHh zQ9i~MRS)RlEG$Mw=BGw-rDb9hmV-B7b{GnUi6^J;AfBHX8cQP5t1MDAtwkxe3cZgx z6vG&XhCNp4;*J_tN@LLLtk#W8zSf0e*h#c)htg@5)lbn-)vKA1p52;-v&unH3a1Tp z%N0j?tWye2YoZ(z^p0&X(cnxQKrXp8SBod?1$`t+ZZfkJzDf;c079@y!M!M=-uGS~ zPgd>9VQRm6tGY2-s;}7e=wT++V18Ej`mO%}#S$!Th+gL&E&TdU=Du-i-UDT4bP20~ z{+xCzPYYAjv|8-X$%o#+g=CZzvgD%X7vLGI?zEx0G#qDuuF}S*I8<^iH-B^f&-T1( zWAeN5tC9kc8wirUiWO=Lo|CFLJE=RVD-`<39GJ9sX&TncnqCdz{mGV9lB?u{bV>K8bDr_bemhQB7 zak;F^fbQ4JW)o+zp+!mGxuEjt)?qB;MMiWnDS=6d%(gP)Zb*I_%?;6TCF+J+nP>$N zU8m40i*pjI%9#egpXqh|I(3vMpk-w8cD`P+wbN;$gPNaJf>$Qv&)FD?qFJi2VX>K) zEp^*Y$69J{K&@>ibHrN8sA-$@j2~8_kS}xBP3d-;T2YqM6c14^vSn1r=gDQXv-4H< zdzgZ~RBzJ2jc9o;IgkyYp{`CYb=zHd^m#H%OoZ!0%6`yRGIclwv0_#~RjthlFE!P` zzUvmZShYatMA00yImhZpL1fC)mv4fI)?lv6O6}+SzAbNI_8mD~T8lP{Cq4d&@P+Ai z@@4Nuz8~iqwDRA$`u=cr3V1d8D3i32?^4>sizz8YKt*^qKD zv11yPV3hD>>Z1mh1x650W#8cLb zQrWVtE3U|}9%S2WjK&iMu>^@cZ?8+n9mMZ+Oe9pO53H#ozO)Yv--+3bXb0?MU>c#+ z7XkXJ-Ex?xTi5*hs!%0{yM&;j+;^)tvj(NDdfrT<6w=sr5Qm^Ft;Cgv&VIjX5$%$S zwA*cT!4~U#45y-8W(yOeehsaty0Fan==aQ9g9P1n$$pSA|-)t!@QWAZx!ffIAq7X1V>rTKQs z2*N{gD70Ax>ieKtxs7#}&JkDLHm5o~lzuZver~h=9&-n1(6wB_h@BTyeHQoKbajn& z&zxh_yrCjmKS0Ol+f6xm=@+E6E1s}C5yTn=$L%FCc(pNwI$i@?%4Tc9E3pp7q__2T$ruORjUb{IgDCpN{k z;((8s0(E0%8|ICaUI@c$=VLJ(tE95%fiYOlW;Fquyu2+qeVLLg8d*G@e)JE^MZcMIcRKf@FTOqB}n$JNusSkDH#l^hnnmJHSwsG z!8$M?Ttc>}^CdgZiQL>V;(aEZ8QC7hLR>KYqBEW*Cxvu4o2*l=_LqiE+RX;{B zDY3ZPG|O@{&5c$Rr)uH~fL@M{zFK*Xd0+)#Mob2IwFcF$ke9QGW)j)Ocl~oxnwzJw zQ-PZSl(OW5xcO}-D&4C^D^?p5Y`s)_;YpG;KwEu^tjf4>9;F#zctQ%M|CS|=UyGLe=z7SAzkZUMQF0CKhU4eT}L|=UE}oYlX(9C zmb*Xm-=TX0NVZRe7twaSR?ir!Id%|$-4#P zr|Gicz?@%9II`lvn>SQnCg3rPXm||KgcC5MsnK#V+%>QzYd!Lr9JXf==$k6CB4`KG z#737WNvRtNql`E7%*RA$5EbMI$+HQBlvCX?b}=byH3ktJ#+re;y-2Eq-ERe7S)K>61wEb~sUjbxY-Fw0Gt80!h%=H{DCdP>B-EOj)y-|DZ@ z^_nV7V_;xRJt2Eem7;Ac4ok$R?nhbp(t;~^l(p!qiq=}Kd-UKH^_J_e0?d#?zd8Y| zFH2Hga`wQEaQdbe92jj1mtAjeHFIJex=m3Xw=b1YC9*eB7l$)VuNIn9t`)gOr>Ew3{Kuxbc)ZEt*xvSX*F6kzmlz$5Tn@CQ1kFAMP*c&1uiyl|Pak z@k3lSjjyc~yhe1}9=6$(B~~@tA~+^xX+*-T*f566TG%sekvfWM`bO>0A+ik{v+qqV ze8dQcutt=gJft!(JkThlvC=VsQY?-7wM!o}P$p)Y=-dr+zWY{1hefk1Nl}Py1@F3= zKM0VRy?TOEA_`j zWMY#i;|yVx(e@orb_d0x5b3Jo0kYT2Y;qG*bfN7T<#yg9$!zmAG;1X&B3-6&H)ZJ6 zK0BpLIbW#gHJ9Ovuc{U}$166YKpHoqxvg39*h;TJx2^J%a_vN8D=a~BOPvYwWtx=o z!3?_Ei}?^GT4&GbE`K+J9`$yVGiT$6dxAxWPS=9+!x&zNkoX%}}Sb;VB1fUD4yO{?`-;dxvOHkz7;(NwjEZW5z;lb!i5n7H+6JR_1(yS|DGWDF^I{(#+F zKh1UhZs|w$uo?-0mYM0=L1o-2*UDPutL(b*h;a(+?SW8Sxl*pjKuy#_1-1H<`yJP> z2;Yalm^|B2oQ*oamvwtq!m`tsJZXy@I}DgxzQSNo@8PtW0jF2f#X;C#4Vp*{(IZUF zBaet5B4+)&hQZx5hC}0RkSk0BuL`99W5-ilX_oW z7~f#LKf97H;wMO=I+y1^3=tc1N5>~?cTfyG#fZ9I$llPR_aD)-nPe|2V&lxp1eW$Q zt*|JGHE-8rkJoVQAn%E|lFW!f%j&izV>N+QMz{(NQ8a9i5H@gYg#m1{A{}Tm6%o~D z__FjZjt;SPy>qImh)1ou+9{MChKEDcR-+9WMs@~j>eT+obVMs^iDtFJm0Gk%u~>_9 zVpaLweuJ4up zJMR89vRf-ni3oq9pP3e>4xs*PDM)u+L_3c3FaB(Ck1zm>(=>f9{bp*bS`|8E-(z%V zBXW&90~jn-m6KsmtX5SLPh>lZLaKYQgs5%tYg-b=i>qZlU@Jj1WF%|v_OrS=#`xkc z2jO4LT6S}Ljcy&)_AO}EoWttrl0!U+8)Udb%VKCg(^38)6MiuV6`dQx1cV4ZoLoL1 z9(jS9_DF><;Ab@L&KSH1P8o!i%A0p%B8U-UMt9X=^OvM$lmHylU4K2Z@YeWLi{<5g9Y3Noq9OA5$a{-ma%<62(t}C#FwXM;0aUl*By|1-3?TOa3vQ(Ju zB=HuCGms?6Ar&56E24$h-zt*K19|;}RL*x}bFLQ=mcgvVk#G`_yc)%3)Zq-HIB74I z!I&my6Gk-7B;>Jo5&Z;oEK^XF>e zS~|xbbUj~nrdg=Z)CWjZxX7(ocGO)7S#ve#?SWrDq_n=)mikkD3DY!=uC1>sOWuOB zx!J8QeO$7sVI!NmxG#^nwwmOAV4kOJDh6tSVXdmfy*t$_AiTF% zTin!9#Ybg?QlyazDb;%#RW|m>xfN~MXj3{vgCfBd&n+RSjv7k-=lAdFJ?Z&@`LWF3 zj!Jg$b+=_~%;FQ)h%$8jw;|egt_zz#aeX_pn5Fi;ar2NMuJ{oP&huu7*!eF{!^pKfwOl8DA1pj*+6D`1&ctO*aI1Dx%(?~Z3iHyP$!hAw**>7zdEzXj<`Z^n3!)uUc z!HoyfkwZvx4;-L382b&8CNfMG1HgANwEHLklPJ;?(d=c=>_|?_+X_K|$ATjebN(o~ zwN~2gZ#Ig8vfQt%cnG4bHJ(5wvQMEwuroG`f&#-`?0t2bxfSIPA*))U**6p`Z7P`|k`*t|zt=!zkRQQYy zp!Tj@D`ON~s?LcNism&ys-aKN9)t3XUcc4->!`6Ms7P5F67sB2X`XtJ_O3gfx>O)E z+ZF1QB;u)5u_{)bEkivHXreC5c7+#eNswd$1;wPx-w1S-+4=tf-khg?@;!IlH4^L^ zW4!5VL8wpaw*$KXUhHlU{L$oU`o81fTH2wDYZGhAgY|_ z{VNi%D*eDHJQZ2J=MwUzcL4eS|;&t}NlH5nWnh7s7~2ZY145g^2h4AZxF z(sF{y89{)K;W4+(`vJuC3@5iqK^`zgUfM&_&9eMxB zHZ^Fi^?3z+)f%B+ZqTct-_|V3X@vG}uDZeOwBj`!u3t0g4I(Zq<~ES7tsJHSmKm#S zk|2Lr)xK%v9lzm z*wi0F^3qRHa;Id|sjK}(o|!WQRrWpK#eEj`qbroZPIz1=OQeicpe~6B@Q6QFN@m=8 zE}gmf?)pFZ&Be6eFxOn*$7a@OHY2qkxw5#ouB*4N(|Gcvt;SdOt#&EHNKNc3Y;KhR z`_)sSxq8uELvFz~?Ay%bHL_fzsIxCIAoNG+{{ZTzS9E6>{yF|x)%9y_XBEc;`w5{p z5I<~(J3?D`x|jw}TGVEAjo3pAzbyAbA*Kdf4FDoCeqz1AzVC3`4l!*Kcfb%s&C+7% z-xhY6Sm!%h-t46PA71&|VGtV*OcV3PX^?qxA*J?`wb|I4rjL*kS*4=!cFG5lm>){| zbW>qS?Hi^m5FGQv!lZ*tjV4qXeqj?WvYISbYu4N73tG)JBILMr3~TW^B-w>P6%in9 zji?M(%K$+54lLEyO&DUiJZ$C3IwF-4)~z*w81rg*W-sOurpbnYM^SiFw<8y0Nr^JG zH%ex1Sy$GUes-;moYYKf!3EXJ2*f}DM_Z}3YyFUlIew%xu%^&%qY)$u#3CwED-@U$ z)KHdDyl;TBYX}hf%U}IsKVzQ3W1Za3#thLm-voZ%)3~FC(t-#U6&SYikVU)KtBgz= zwRay-sPjeb(az`%5w@p&d1uS0*F1)9Mm0n_$mNfT^Yx7l?Si<45sWD0n}WDCONrA23pRa|Z89yPg|YtE9RF0N`<>I(KPS*VxX^<`b$=AqM+{bTxHK;P8M2j}%_ zv-wp)*UG^9UXw-;#=9?*DN~1b99X!8b%Ze$UD{}Qz z{{Uy!NitbBFhC*&s7h~KeysN2d|n)B`K$Ot0yGdJWU{4i24THaZdsAg3w|01WiV8| zu%?vQw>#YhhGnPS-eKgkV71PH4+7c;ZT3#gcT0^AM%;(7>{pSrzo@c8Z zq{fRNh{$C&Z_|T9)GP`E)X(Vsvy}?M49IzZ7~anv27LT>5{Hh+x{I1H4l z@O)fnP1hzi^FH`N^3l4U`Ujz zRzUZ>8A;>YG=yr#o(y88v!EFR6@@%w5VC?Od%~C!^ub(N>yIhRwv2?fAlJ=c#4$6W z>iPRAUXn>eJw`TeRa#{85p(Pdh!M)#PbqS(9Y+3lhQ&N}rmbAsMo~zxnz!7WEW5Mv zXL|C$WczFyrMib`qecoYeA)I&W|fN@O&HB|)#^=3_9DQGJ4OJRdT;bc^_R759!C7J z{95OWscw)e?601IvK<4eo;I)IgF!9B0g=d*%#-)KF8M!PwaxqWOM@SD;sbWW!!t02 zN!=Diot|kK_M9pBb`WZQFw6v|KG_ z7rPOVwRFZTYZG#aBk?wD*5nbRddpUge>EfRurtpPL_SJqlMqg)y;bN!(M{~vBsN<# zPc4$)kQySNm0j-N0t&QpV_fVgv|FH6WTzKu_Dz614O;B1Y{argDot11z`_cIP85#E z9$I41)s@GjBb35@Dk>_}wE_YgPLGCM{1`2>^HQ@LhOjP|ZDu0`wY3zDw^%W?JhWqG zCLIDWsqAH>K9w}4_th8qCXqbm`E;JOY`9$QZ5&0YVQGtIFVj=jZ2n>0P63pl;0#WQ zj>@o8It4fnG}~3sk&O(A7ky!I4ot9ow0ZW_H(4+P(baT~Yi`wc1un;|O0#V?(Gy@B z4A3cUZYwbO$B)r@{{ZuTU&85|nfj>5PX4E*$65U{I>~k5u5h&JI~@~#_2#u3g0fqA zk^pYWZJOh=;aJJDDQj)&h_jcg_W5Qii(%{bU-a9pZ+=YtWBj$|QUD@l6tP`HB10px zf5FxVSW6xs^l3Ix8pkTVi5g2C5oK;4H%FO~*`s*b&?dywhmp@o8wiN~UeD4)a9^gu zk>{Uj8ffC=_!&gsg#Q3BBNt|}P&LK&DFgZhof`!B4+(>xBOgYOF^9S(#vDk@2nB`h z_{QY+e}`xGa^U5JpG$y(Pk!kB&SFnFb6l4bmGIwdx7lx2HU||Nt9J@UeY_UI`-;?~As{)f7#edHI`;1{sk z%1o@PRj0%VEze2~c1=PXY|v2-pPZ^c3mH}D0D|=GYSt@K%2LY`x(OPGDyc9?$${ho zH}tuzJ3zgu&u2ELDeZ;~a?wmOx=M`1?reeKAUY~TJ0PYs+fD*&w8#RtbOYx`bZS~` zfX@~SbY*KQ9JXb_2^Edy4z#U`+?0K8v97+3wO-oju60w{WTViamzmESp?tKz8yn?33J)%wrY7LOVJSbj?K>>arX#?4WfK{GJq@K<6gv0;K+4te(!atuL17%{p= zdZ$H?hRwQUZopBQLiA;p!Nhk%^9|Ak$=^I?Vdc#bPktW5o%UI)H$#IcOoLiH*`rqf z03-Pfw0LjIk){hQMLgzv!jsH&p~hk9i7PpSihM)hl3?zazsLn4Owb~3k51z(X8i%> z=wQ-I#tRPOw5;nw!MU8R2KyxD0Ui-Uwua9cLNa8Ouy$T&qQoVE52bC1xB=}F!-Zyv zDE2G*@D|_~TPo%vT8EV}Ocpy9Jj|U^wx+KYezapD!VDP|M&AZzBf8lMV|_)Mzhn~& zFl5*fOffH-t-01FjcDuDE{DYKes!aO7(g8;#OOQW?6Sf@!)emOnNLUg%h>zkSi#wi zwnDJ?H3zWue9K{#AXg~15`k8+)UCa$IA^51%7cWwcj31HMhFZ$Qnj3Tpn}tgw2(%-E){2z^Q%sEkq_U%!+(aA( zof0DJ9K<75@du@^&(Lx(@}dhw(qs<#{z(lEUPfh&b#}EatNoJHxti{hRxu*m@ef}X zGb&_lC^E_1 zD0ZWFQ@6<6{vgGD7QxTuZ-yLa_@V`VRfUgw^!G$O+p7slz>(^aLrGMNiqYVLt-PvX z=@!>EL}@1gM6)MNRv6&=49PtQ0L?dH?L=B}Z#KYbjpj2634b*>&uI#O-RB8TvhNG{ z_c=H+xVIoyw}@Jj)U(Ua4*6fz63Zhn*^p=^6v|o1dKzzxHRXyjyS1akRp~>Q1yx;{d-7$qUj=z6EUlGZKjFhQM;y}p zqesL+BWETs35HfnH9>zU_Z^2=S0mFoCUzDRx>A8nOSYWa(3$n`jEInGMRXCV+BzMn zRammBiLBF*EpCVIU)E0Vyg5Vhv+`G#ZA#2UiG?GDva3yzp7d4i%|nBXWeym*9c8ww z3Y#c^@so%LIin1>N`>?d+3CdZqcu45%8tWjR$7E#A4{4827OQ5ZmnHF+ku` zL_?y)9QrQE60`b7h?1rakr}~1GH`tvB<1;@zTA7Ys#BvE%!Lt>cND>%5x z-U#7vEuN*bt1_1sR4fzX8CRjWgfcTa(!@p9?!k$xZq!tzsqGO)h6m5zl;#wiQY6oVp1jWRBAwZ>{DS)g~;&q&#|-idYdCCShLXrOF=61D9_(T}OC=QP&q z_i#F4;=9whC0?j_ITS+Vk}f3uflC^#k;uudRqog|%a4Ym5io7FbyuKyaNUDlm5hA# zNio@LAiG4Ws@fED9j{mYe}cI=N1%T%>B=I7=dL$-s%$Z@E8+YtEXG>Rchs25T!ED- znvI_+Mj@ls02j4`%hEeE=wOQW2VwrDew1`~<)7qt2673M zm|o19QCPZrM$!qJUj`*fHmX-&r+`3ff$HvvF+Q90Z+89U?NGDbCP05a#GrCJ#_ib_ zVEyxOi@!uHi0>B0tt`0hdbf<(U;!PXUag}_iaNWL?MBDN)tGhzfV6y895XP6kbx&H zpJ|9ld~*_G5eebUJd=bydJ5?ldPvQpNckowkTNJ_aRY2NS4LiK*BhJ z&SeELZeQCpmc?fMVVJ@h(>x!sO~?poG0qq!Qv3Dby!JEZgXofjlf!WM`XbJhQ3g<# zeyXynSh7(o6#(&Ag%X;zqb&~SwA8!i511I_2&z$Lq^77vrPk*Fik=~RS2jR4jVTbW zQ+Lf`45aj8BuS5k6ZE&4F}Q~Hf|hJpL^ZcF7Ijyit!c&)cb0?j6S4Pa}c=#$xMv0N9GSm>8!yW%DY5$ zu|vm->X~LltjgWY3Wy3F4^-G%>t3DZHbSdg>!MK%^PgLD1Wu0;Uf74akMl~?{Nk@_ z{y63enW-l^?KRF*oX-5aWuShkvQ}xl#aJX5_rZyqd< zv(_a^`J_}`PdBH1eyMjiY5r~g5b{SL0FR`c<Ik5`X0r9-k97a^StQ zb^Rhh$$EBs473OZ{_<5_Al0mR`D(p9H{z)|=0w@aV!T`x0xr;8$uF5a5G!ICA zPp|hO6cUgq8Q3Vx;`#av1hty@NO>v&rD*oPA%hzJ`#VDXgF+_}G|mbnx2Ce{klNcz zgJ3Qh(Ue%=rixNWs%?GV1746`^&F3mh z>fWNOe8vs|ihCVgKXuo{n0%V?9>C7D9ii zmh7~TqY(*OPq=6{2bIt_JGOO&UCEq>N^>VI;RRwHxf-b2UuB04oxv}^cQLQ>En19W zMnu0&)`y_D))qtcZypRsgvs!1qLou7D29sXq@+bs`&nBZt zz}eb<8)jQs>#_u3P6sGHwzzQRVJ;TaVMc44jF^zvqQKkaY-GlbK7N74VGd!I9$>B5 z#?Ve|e1?05?fUiZ5^Q9x@uHH6?4i3=1PJ*)Swh=DZIo zmiIY8{^FO+1rat_%nbG-S`9oz}7g zYMI?*Pd}^8$0HD1qwZw#Pk{-y7S_T9zQ>d0S`?17h;T0`ktV`zJtx(5eRj!f zHSE~$AqZ&LZkAvMj(3lBTWaALYj#`9j2O=CQj=9PV$g!Fpd9MfY(9;LV$|ebDF<4B zL+bkZ^Em{3tM3Kt3q^gY<{G}XnS(w1+78JUFui6?G98XqY6M44i$|1W^PBI_o=v2v z3=LOalMy0lDOPEarX}Cq1<{wkesaoWQihl=eSEgO2-x6M#N-a6lx1XVPSe> z%!e6_R*9OS?k*>8f&&;YG7SicN?zG2QB=x6auP9$KeCHw$LHkn*1wZrwAtMvnpTXibGh zd|6oh88$QJjcnP>{WW0RlI)g}_6Y4Oiwzr+v37v^C2CfQFz1v-&6E!2m}Mix+Tw2| za%^A(=$cI22y+!qW2*=(X;DfmSeIWzOc&3(rlPL}e!-aSF-khiI|+Ql9aa&xzo(WO zlH6d|Nwa1BSJa3oh!C~c#MW>#IWozPiRLw#27HFk&5dNM29`;j#md?PCse9O#``ZgN-($5uvuKB1E?JLp+qodpHR#P+6udx+u43w$y=K*^qL zK<(thk(w7?@1#fBjNCV3BW~H!p<|9D$%oymZV{&(Nh@JlY?m#Qb>1Z}rlFOu; zQ`V9=#~``7tQ1$1zigsvQZk6}Fe~(U?!!z^u38#{DUfdp)SSm5P86uVd;)l{tp2aJ zMALA@rVk*pYB@U@K8ip&uVJ9u;DOVog-2v6Kh(u?qg1H+;bActW(rEv!%dT!y~(ns zMXwuhSup(+A)(=nG@pqC%qzkQ;5Jy|pxAbJk^G<>7! zn7Rj^#!BudimR3LQ2zjC6!W1j@yJn><>pBrj?=t=}iq zfs0853pkZcZ{lrtQ6(jA4<29;%>ZFG>;Yn{m_R9lV-v365`^Z*w227PGxXcXhgkiK zp{5+B$Eks9(^La4Qrt6!yJ2A}}SoWo_d02K0>hS{UySAxf z%orS&#aXaNI&D??gj$m+7!f9VS|g6LMZ%UDA)P)kD-1vlI|uyZuBv!IIvIf>Gb2;gnNYJ z5C{R3%!D+{STcIXOg*WHmK)1VAjWq*GIG*MjBTkh3pU3=xVY+jv@9D%GF8Ot2;;E( zZs?@09+VkKg0G z$Q(yCJ(9GuiXo^zZ`k3njVAcLH1z^c=Zc&f?fD#m)!jO^OYnV+W~hkYl7+;8W7^r= zX_b;2m8|0l*u*o?U#GvU9r)|{t@)q%&&C6+a0I}7GXT-75s411wMiXp%EQlNp5dPp z)t22k>&_%BrN+pIpyc5k=B*Rq#@+0oyY^4h8)g#*%ko}&M0#61gW)V0lBoO*=Mt_d2{n(LvN4Pd0}qKpQ#*P_(P$=^R<~Ow#ucwWu6rH6AtW7vJMILn7+3myN$Sz z0b%+b-I%H1R>E-`F}c8p)|H>EuBHo7_QGmlOL)Gkfpg%>QUV&bAAi|Z;sY)u>eo3d ztd&IM%XPw;SH?A1q{PpGwc&Oaz$z1hQtc@Wg4vT_<(q?zoI;U`?CdDjNd7^ZkFI(R z2ov2r(^sSPio}Y|&vMn6KEYu7goS>4(fr@VXLm)89i2{b@x!KBPdw`L-Od-sI&rl? z(Noy&l;FmE^oF!*hH&st#~8%4%(hc}LZ`y6xFnm=_Rq67X#ThUt9Ktl{%ih3aAygL zW)#FfY+o}TDt75=B$(^Qc>0_sEHCak?(nShM*XgD(6?=s4-I)v2uDO&Ud5F(Wa44{ zDllPq@?h`Tm!f@Vg#$U7A#FITD}U0Z>J^;fGX)aJc^|fF6zCXC&tpxTx_2>=eBVDr zCg4M-VTQaXmtT{#bap`TOT^zVRv1IeX8!;U4BBf-(;PPbYeSHO)} z3gWcwC-BklIA)hF2<(y$TyB(~g2(!)*=N5gMw{V+6v|bxW+qVv4pNBqJ z23&e@ZkRSoQ>Nv#;2KNsnzxW~cEYEyWk*`7etTQ-{B>}jtya9XeXcCcESh4s)Jx0N zZQ0LkvaA3Vb*E$d2*9{aLk@dA2=1uMqWUcYwrHlzE7>Pf>*<iLYSE|sdycQRRMxu~Jr{Xiot@Etn$$&4GMja9uJ9%Xz(E^i;FKk-%0r?Zjb1s< zRbUMgLrQ5mOY@6M(|vxP{+4y$Z1P{?hvpYK)^bWo1YD3n`(S6Q>0tLxb5A3y=CKLq z%_$hxM1%qJ^3TtwX%#mt1rWp8Q2oqljd*Y0qc{e%PKh7iN69QFY+p%_*Dq8V-tHh0 zIUi#0(-h8dq{k$fT4@k4m@$heZ^pjVD0Hbhv6;MPfdKUdcY>Cbwa`VQWz+?%5^wl`e%U>gkz_B@^mO~olYpiov6=mjYzCk+ zXbjFvS9OCAcd+7X!fcU5^B_Mtfp<^#Bq_u5lQZZr8|DuugW0tJXB}YY(AFZ z>pNk?D;(4DXFB{;)|FH{h)O%G@YJPwA6bO9W63-0+Re7IJ7g+T zC@*#Q$9!FX8)GS15Q}C`5Q_VuDwYKat;GV2VpAl%Z=9tjU4;^zMvK?c5Gv|^tgHBA zktm*ihvVq#?#6@-)Y>OQyYcIh&(yAaV7PxMEUGD1&4ek6DDmv-BALhSwCiz_%WhY3 zM#1@7T;tU{K}qdmBYpe5@@&5pbII(sah*_}4UMC0Tf&^pkaZQ{U=D{-~ z1a3H(0nD%3b{P|`fDFj7ewDH@O&INk=Atn@fKZS)#N=qgXw80WDk@PA1j@u|O%d1! z#xoRt5~PIi)~pPCv{iYy5dHi9B0om0^EM8W#J8S_qvg%mGwCt&_iRYhweNyhkPS@Y zBWstLal?m8HyF&Z#$1U$^);kM-{={(b%@gPYXJj(ThhrI6Dd@+m*utgLJCfYBabF& zs!EW{gc@U}J}Sy#Qfo?_F~gx8y;+&E%COgkg^*nYCa4-jqkhM z0nZ~N=Vu_2cG$-yfS%&wQq`Fss6LdI zz`W?f#?~z*=CoL_2vI!Kgu6XWI?!6$62wyqNx?*T%JK$6HLwADs=Ad{8{<&b^$$qB zD~eOL1vypM1tDS57Lww=^3+xAnR~4vYv`!7Nd9c&XuLJSNvqP_VWqVj0&~L_T9RY6 zx6sX2FCE^Y9Tp)Sq%c` z$%pK$r^A#FXVZ0O?yUtvki!VlBp*{_2fUynIC-nxqDf%`d{!ZX=WExLwgp+~z>}wAGgB;4Oa>clo6gIB z=qm`tkmGf+$DE-*6|%!NO_;wYPW>@7S+(IK&Fe2PWl8pC-HU8dm$Bb4;DI)LYZd-B zrj|he05R>;F_N54swCLR;Wc;~N&-rmW!1xSsvse%fb{abFqH}-*u~tyCo{=K_0J^s zIVwzrPpbkdR%macDYF}{4!UfIuXvp$VES@IqS{JoEK!T$Ze{+plFeG<$R4~7xK3nd zS$TX3Bd2b$G94U;UHu_hOR#Gf+)x=RsmEuc!n^GbZ%D~gq4|wXnh~0gkEmHhUWgF4 z?(%V9wd>3J^$1oJWM>ao6S}cRL{dO5a&hUFsY*6YXnp(Iisqu4%U3r~H56-sT06>Z z5;4`iOX|mgD9(7}s$n4|vHGWzr1+7{7ox0jX}w3#Z!>wp5|3T8`Toghs`OZu_KO8b zBj15y`biq#uI7aYV6j&&$`ITRB0_7+9+UkQ{a5`5l&G&wR6L;&`pq!AC2xK2losc&Wnm|^#v$kr7x5)-qHtHj= zR%FJQS|L769ArowB3!t;V`DmUyoPZ3Yl|4ZoKL5+4#UFYJc!2yWyj@{A~rFt6N2il zFso&IH$yOxtUFiBZOB6a_}FGw-Jl_qn<8fNif-I#2 z@|)kqsaRF(BC8hZbAxZRa1QhbDNIvS5a zxYK^wEmXQy6neO29=4`8eQgLGHP7IN>7|GXLl$vNbCRl*dk0AAY(c}>8&<|oV}TKW zR5K?P@oR&$!q%;DPRZIGiJm=yqH4jyz}OgC-d5fo7q;0p7H+QqqAwn|mFMFJxp}tOiD0Mds9{R zj)7ZPC?h?EN*1rWEO|c?Cm%&*SZkWbtr3m_TxKdV)SXhEtGdpgHokR+)Cp65bJ*OO z%A7!r2CTO!_PMKV5S5#HB0{}D!+UkQiA;Y#i>gP2p}nI(ovXGsN&qfV)s2*2e757dP{rVFQ0)4|MIpHcv2s#UjE-zMXY1-I8&(We!u|3GJeVTo30w!+2{{TtP zL4mNF$|Feiq$2s_XcC{3iFmMk#qH&?9&jez@U{CMNl06K6v+o<6L}Bc3x4e;R`ky* z8Ug)AmxoH1vkl%&VO4p2wC?u{vi|_M^*vbatp3T0pX_D-0F0C-D1zzOu;&rR&Ic=G zQ|!P%xsNs%2kP|!zClB0;D{QHXyp5f?$y@P~PbqU)2|mW;Kl!-pmmr?t2zN`mbO%o2-WR)68j+ zS_A%;q!O9=;a4@U!`wbb4A3^N?IrAQwB&Vcn!Bte$50E8&NY4;XXO&eM!!E@{{TdL zleKsY@jvrtjzeKOG>8&6Ox!ap(wG=_qnQfgA_Rb^{*AUK2PFF(pskxU3N}@LFQ*A2 zaC8zO^ZR7dOL32u5*dUv&vyr66BOVA`&Z?$CleBPMxB!3GN%#1z*+EzONd;uk(l2S zUDi-`sZvD8AX#~XSc3$HK_{zBsVjy^tg(|**%Cy^aE+8&Y7#%o_iq6aC5?sGceic~$ckgAUZS-=?HJJ7> zL|>~p`=D&Z`s?Bhhk=p6ZEOjesZyF7m*m`kvLB({_0=3H_~rR8$$@uf`mU&cO52=t z!{D{MKv^M^7`0b0dy1j4ZX83%-VuwxDB}rk3QOoR;^xXS6qs`J#F&U|LXU&nszf2u z*aU9)m@+b8ok?R}I}1DvMub)rOop^bN}UwOp_#M~i}oKyB)4V^PIpfBTL3wkh4r9d zQoBCbDl=Ba*`d$PPbyeGcpKIlF_R&57-Lx}Xk3=#{qJ+0yq>J>Z-$oZmW0iioh$^! zYWon#@J{*I=%7=?8wio|3}!))rMdFPw<^lcvv&(p z_;e06+9KElrh+wsBGp$>){4sel7ZqxBQSx{z15QN*ERCnm(8AAXAPz+$WaoZos26n z1KdxURZeq*9It|!S^yG_uO}&|z0%%2iLL(tL0cWZ+!sE@cCwof&}EqvkHo=`S!Zu&>U~a;)UxN z-v#q9Ndk(0`(;DFr>%PBWe@DAJqRXB#z>F0oZU@T-ikrKN2G#T{4)IA{E*)e63rh zj>;~RFh2JgBZx`FqR!3Qw2;vbUQ^<(kvu8{scZ8-+v9>pqTAw)bYC0vyyuVhXhI`d zVur+oW%BhwNbJ$`ZM?@Fn9C6b2=#>U$y;)*j z4sC+u%(ZW~n^M8KZRxlD;|_GFN{o~ptE3BiB~ZqAeqf3>WX3~WoJf-w^#1@x=cufD zuFSNhHR{~~tevQ~r1W{xib4_N6a6(z#UTTmSR3NXROS>(r)!E;~eO$d=!sT0kq=yJUkYEqPg#nfOHp&QD?04bhM&q8feQm6B^A zc4ku?Op zHL{3f3dk(4c*hWJ;g*yt|tS5DfrC_OzI*4L8ZR(Xl zm)L_7ZZX0Q+TtmMenX*obCap{Rg%lEqKN>B4|UbiytQz1$UeR53qHjB!>gqrii%V= zY>I}Io|HqmY=JmV!qB5NibmMauwy>u2=6d}7-;Bz{r#Np9o!Z9Tlt^Ms?FewtL!B( zcd_=g*2mnEB1S8>VAh4-539=4gs22}#D$rBJcqU%$a5f>556Y68I@T)4Hie(5YR(4 z@=^vHT-7Y*(3!TRYm+nBUTG~HIcfPTxQ&6MCVT+JHMhu+eTL=|K>Y;%TXJ792QQ;& zX50%Yg0{ulz#M=g;$+Cx9K;=Yc)|)15`JWU3A=9Hxa>Ou+_=E>weJ|rbT$#GZL8#% z^HgA&?XZG1FIg@^nybqeV>PuJpthriH_wEjk20`<9ge7k zv5A;KD_VV1X3BP-N3T;A7S+kBm(n{>z+SaL91~#Of@gaZEoO#C-#DJlcn-r%Z7Z$; z2DF1Z(G_8|ciBKL*BEB9vSIHyc6Y~2!Li=GI_uDd8tnbK+;&meyDu9O7pMp4*)FK| z1(VMs58AQRS|4gi4CXn@o~jmE$=Vx9%tU!xenVxLv$UYqmrroOaJu-IH$@EH;mO)r zPQho3vLW0<1^CF@IV2UM&;0|>FEc9|ywJ-JI;zeRV#e13FCElexz2pMOH-!>ZE1=@ zEc6>D+Xj(6eFw1nAK<+0M24*!MT87n$zm7^FD5-sUW!$fB4n0iU8D!kljg<$0AED_ zko9+djQ+NMf;(_m=U?OR9Fdyh=W)q zLm4j}Srv;H7_wzW_|l;X8{|NV^r+^vjghK1x-nr}ZWEal;%DKDEDmO2C+cTbHYtGJ zXid2g@JcWV64YT*G4COLA%up7=CVWZklG!>WZ3Ms z8CPQW1JMfw)uoYw$F=OVjYak@8Pelnzge|SCD1qennph>6MLtq+7S|CZhvnc(UPki zrrL$>4pSDrkbEwfg;F3SnbdP;u#66JahtwwTmCnxQWcp{S~;(2u6i?@jLuHntTmj% zCPrH4b242^eF@J5>sY*MKCW}4RJT_F#u2R1*Z#nu z4RRS`jwWqC<3Z$nFs2d!S&1c%*Al)O(W`LXC&OdEVEwR4#nyc|oQ#MH(I-w0h^O?0 zbeN1^T%t*_8;-2wIwmsn+8C6LM=+UVIobA`rMG)OSymAn9RC2J*9R2G9eVdoh+t8( z1jNq8-x1BqyBIY_J{-&)F9nGVZ?~f+I29tvmH}$LB!@1c)g&2&(WPM*2aR{kNjr8^ zk&!Dp>Y7(tYFlN33Td#wXT+WKNwUWX5YDZGE=%K112Z!P-6Z7#*%o-5rR|I1hXvJA ztZKtf8_yYDNZl%FOk{Z14n?bLL~?qwO0~0C*2@;lIo_op2Ybc7AKRkKq|>f@-Z(=6 zQ=0Lj=o)>AdNHSUoov2OUt~We6M4BRd!CfE#FjKt80N+AW6IeJW1HW^ON$r~&COcq z7b*>soT%)$(Ojg-6vQzpzEqmx+1T(HeQU)iJoC@o*F@G*Y>4XCeK|HA#eOePgV+{| zUVox$m3>Fdu_B_F-y;>hmpu(_*ElzcgyN*Jx?U`R2e-t-&kAl{+{_+Pi;wlc^&_bI zlZbyBe=9kuO_Zz{S`I=HH|UIrxwDRq1&n;Kv$uIL8BjZnXpxTCx$Pg}2s*Zr5PUXs zu+{$1oSVbi~1(H+F-E+mQ(05ClA1$#XW3uU^jgwveR_GEXT_V z*0^k$PGl#1lqraLTCT>it9?GSur3%RK&%D#{ZVI|SC%=hY=kQo$SAtv+Kg&-isn>o zI)NToQi^oc5-;54vY%;rM^vO~)s{N;gbBGGldsiSOD=DLwncqAhY(6U{I#$NQkdMir0G@Y&EGBU+{~>AhB2kE=eA~}Orx9PiWXVE)MO!KTN2vs zrV>{Pu#!fcJkH<=;@MV+V%=s7HsdHzEm6o6)X@GpFYjncZY~=FMD@YeLOggT=PQVu zNiGl*qX>GQ(XF5_3KPgVtLM> zqq)n6X(_IW`hr`CVxHb?A(TS{{Rwzo9S89$--s7=t+X#aiA`pNcL70L0tXX#%8E8qvN+B(xR7%6De8DAD*FaftPnnnPOFv z<`D`sv@^k`VY?k$Ni9Zi1V#x@0i0VsAz+~;sc;HrAglv3y~%rL#75wF2FON+Z`&$= znl)~TAa$f3J<+diZ>}6l^EoKTZS_qsDbMpbt!*X+ zl07t>Q^4a4upvX3lKP@w&qoVojy`FqxXv114MrDwzx1>8XY>=KxFhlV@^7ETYs5*m z7y@EpzE0g(yjoT0PS-z6*Qv5G|;#}BfDO?rJ(?vSwxqcN~f0Dre?xHBWux-;gCt__uL9) zt00Dag{7NFtzTk7a6RPoh$Z&C$4uP~l1qxg)2;WtooCu zN@Af(2DXf-g<3~90Te0h{hQ#4@d~T4X-0et25|(vhV`tNb5D{kUbr5BB7JF#)p;g7 z&2YRkiiM?W^r)60@$|6@8f#ktB9QQA+Q9h4Ucs?!HjTL-GW%wLpJbBUerj{`ZLCH4 zlVhJB)ahl~m&W?~1lYK&goA)5DhX}4D>OpMr48&trjgJDV4r5_PGVurVpbQ*w(V#3 zhiDkjP`MMt0dP7KivirOQGIgWLMLOcH5F*1Xs&y$xi9nnWeNlA+v@_>YITO%eE}cB zEBks^w(DP5e=wzBr8dMv3oCJ4L7Q4QTx`(s) zvE<4t6`jlrJ11gXiu9vXODvZ?l!zFTn>A8Nb2cn%F|mTZD{R$bUs(^SF060gS^N?~ zC`&Z$X)JJ>KF5xbJDj;EXvAvp95D){hONSh2+NSw`3EBbl{^f{>q5_J`Xs#O>zRm4 z8WL#Ohqo!Tg%nz38QQQNFn;n{ibK>}CC?f>k*Id^A44+n4|64k(0Ki@-VL#cm5F9= z4&3f@QiCn=0mbdLWn#NimFoya!kIYiCb(c3+4@CD*gc_oVGV#bNSi$}Fvvh4Z#`U* zblJ9Wrm}$Y&A7R($}V*^u)e>!l}ljo)kT99J*ivL?$ssWu>N12VDRTVbEhiOb(Fbe z5>4o7i*Rp*q-jkK?qBHJ#E(Nz-Cv?SwT_eLK`<}rJp8_A+K}^JD|?WEYbS@ohl5d z`gKT=8VOU&+U9%;R{8QhjX0`i2IUP4w{#LSAnYc27j~R9#V@y968bghU>T&Kb^&pw zc?6M^BISZ&W-K`n7(*BdnHh*h97hX}G&a&c!j?STBHG0Jqibrw)6ini#NytG`DKLc zcAbn>$i;`ycFf!2!@3%pt*;Aj0y<9N@h2QrY@QLB*(_^$Kx_@k2f#6$4l31m-C+x5 z&26b$ZKmcHyNs+1ajoGtNJXcsRY>{PHGk8MRNSjiEvGH&+^|1VnLRb{xUs?2C>wBL zx;B(D9aU(w-y=@l9yxi7GA6>(S_srRSbp=X&v|D+FNj8(y(<1zxn(u}Z(X_pa&mCm2AkEYR!WtP<2~Q4>f< zCQ22qQnL`*rZo;@PH6=r7!%~o-KuY7bcgPTc;U_;l^>DZo~TKNQt;MLkyuu=xlKaw z!tCaN`#~{BgD6BtF~<**YYR=ME`ni(5u@gW>nCx#|zD#Ac1_PH=7<+x5*XQni$bN6A6>2RxiVt3 z6FoCveWVKLHwMx%y^4RXqC?u@jn5YfI-Cm2?4aU4v#JzPfr!N&c0#>NK~ez07EfY? zv8Erg7exlaoGy~FMhPZlASY2z<=ZEGtPm<&W!*+&Hn6v=E*xi%05L@n^q5YNp4sPTx$4|_DQ-QZx zz>AD}rf*o5*s-4Q`Q*?pJX!E!$=3+zeYoq7oLr;JpFB(!4G^-0HYWW#@YOTve(DE5 z8GFVSi8nEZBTJ7CG@TnrVHzyS(ZzUA5T&w@>A92Fwm6|r*ts%S!gQSpzkiwSPjH9u z?am5NkC7LjN0>i!!a>REU1T8!Z-WTM9^Awnhedde^>bXHs@=-8b*oQNP?ZuZ z_G~LkVo<()r9uKGB(~V%f)o;(&5J12$344M{4Nq80AA->pWA3B`Q%&D_UOBm7=3-fFe% zq+`ET19g>MwK{L1HMDWJHIb^?c2-60fJ$-aGXozi=&|zWKsBf+Ne4FIP@Q`;;O+R! z+kJk!evfyJjmDprAC-JxrJNYobpHUP#?cll$k=k#*#?_kBr&T{9TH^h2b_rVW@s1j zm^xxWI9Nu=v*U`8J7#C1WXYj5h)G?L;bcBMi3ed2Uc>F%v6(Sxf&iRON6bSHt9#GS z!vImqW4s=Ndg8)mioG&#kuWI`fZv@VtVd^(Ja>#Cx6O~3sLkVt>VVoIfmW^#`O8GE3dX;N2@rlAdPD(w&=>5aZi9T!K^OKJ$Vl{FpRN@0r?P!xM>bAV~ zjAIb>6~HO5(&Btq(YVjTUZAn&Nse4j&qqifE(xd;NkoaV6oGGQV>8Bah2}x<_Un#t zBU{cET_p)c5>MYVlP4d5K%8zg9Q}g9lymCY z#yRl$&@@?#QQ-`cqI0|l3qJC*ET;=%*K@R7nVG;#QDX^_U2;4Qbh63Y1u?={$$MH? z3b&qh^?~DJ)RP;I*~+z?9JRPFugI@fZN#PCY^en+B1;EY)ru8*?MwlK1N86)AO;Eq z5@TSck-E0daMGu@eRet8I>TCknFX|9OGvYfXp6I%@(na#vtaBks{spr3Tzn>NfaI& zeBzq-Oo=qe)#wwq!CBv8yd+?Cqi%*%#?6?pt;lYN1X}KoqyxlbBNY2Ril07l!0)?G z97-Q%}i8%PgnDoJaU&c(#~nXnimQ-J{}4b(sh1l%!54*(mUbv zb$MN{M4j@+>BpA>+Z6-a@bdABtTd8EgD9B^(d0vyjCn#@d=Gklvi_m>y~n~|li!S7 z^&)J2grfp!h(121RrrL0Zj4S=(B{U+V_5K>5_L<<4Uz=s?60DS+a$)wQ-=LMAag)Y zz#$xvZQ7(SPBFSZ0oVZ$*^I$H#yPKffQ2z#W?vTWx1^Ch>#@iV6)2=l2ITBoY{713 z)#lI;aw{7X!3L9^F>IWOUdT2KZ0MQK`&Q}0ym#EXJ{kQDd8}-%KY~YH3odTE3r4 zSA_Zv_VSC9gNeHh%KZBbs-NLm_5u45eZRGSS!+8~`JglJ{KQ1*dry@WMn9L7a>jPbNi9K7!=$r01Wgi#S#q(OW?halC z(w@%tAdM4sVIK{prp^fmR_Sw@ z_df~eBHnSiE=PL;!*%KNGAs*WE>ciNITiet{{W?cH$?}`A!nZZ_2O0i;^)Vi-nBM6gU^UkzXxiNBtHPw1P;X8gLg1kyZ>=RDO zD2xhr$Q!k;XCmD~-xpBv1r1IK4Q0UMqV)Z96jf z1?PlmjyQ_dos>dz+>f5I8dw;d+N?LI)%jOxuR3*a5qT37e|ecLe&LqMA%!-ySJ2&I z%&ea7uCueNFxCBr%3=)7l?wHIhgsIF1(({Slaj;)>ftlXHq~&_qwqpj4^H|L`l=VdEVYMT+QHTu>$&7I_f$2(p?onU7>(l))Hz}(|DqMNam zTpNjM+IwQ*&@rN)Fg;O>U6_&)&2+EJ+e|O9Y~8#+QT~Shv3`Tx90B=P`Af>0g0vl8 z98=v}VZdt+tktY}wUU}XFgAW#Fpm_|Fb!;aAZgBK?L(yP1b=qjTOfrvz#w$35aS;b z-l{0fsLSQ@j6hLHq@<=z#q(JBW#IvxjA@2MOwuc9L4`Mc2Pp!ZLITL-fSSq*V~aIR zynwonLmL(U01zX$nHjg(`HR@Y0szNc0q!G+mZLalXI@t0y@hGrEEmjMnAw1ZVj{3L zBH}Y9+~cqgF+A*IrXg#wNi#b)F-0g*#BL8%_&W&YV4`c~9A%m6+2WiT7-vu}J1Umv4?i2bC&EVdm3Tj+e&f7pn;R-kAsD z*av(dc;F`HDN~8qz>D})BI)^rQkgSUWc&0#t*f=Qfg?$@zSD)u)F`{@3SnH4MhG~z z8k1Lajc*f9HqyB59->HYs9CF{#E=7pbz)RxxoXP>q)=U>xH^#KYId-PwYvn1HNCQe z>q_3M8>dseFyErLjafF3=XJWos4G1j4Vi)lS)bafOCl3iu(aygLCdtGGT;E_8eD}3 zVH40lqu;H+qE;6d{#E`w^0Dk!@+BOsA2EZa$ygES<5;0-CMGvyL7tPPloNrp_=pzAF-BN^bNlobrMAIqJrlWdM4~{W3{AcH1nrIsN@uM zXJ8^*Hq@0}B&fe#5kaqFs8UscjTQ2>UlB4KqUD@6Zw%x>r2RIyc7IE;=(W@vbch7YyKs<_I^Vha z2oEeYEUDvaE@T2K<09oK5~QkrWs@5R1CI4WGV}WFbJNK>Yggda*+>r+9EArrTCq9J zfaY!6D(aO9Z}Z=c(D~xI-vQM1)P5BLO}QE4D=CMzt1Y zCRO>&$(mb&`LY250sfIBE|*ZdVfau~e5XbhSdzqzX4Os_UJ%9TX-7qgoNf=>mYIIQ zgby;8uvT-V3Y;oVs0V|Umxk$W;@Dd)d-3MazR1{4wvCc?Vm8U?MH=-D&_HnqGXqDn zN{Q<&zADEtXJY4UR%ZiP_4*geQkIxcif+@-l;vSTEHgE89=fi+3ZjWyAiA!@3@Ap) zF*JlSznr!a3chPDi#k+Dn~H;p3+%ldBt65HU$eVPO4)Q4iuNc*(D=as;+%{TW159SBBcJ%m1{vVw&J@0 zGejFsiEKAmj@RvPaMt5a=-(awv9+A3-0D7j<%BLzp=nCBh}mH)onDqKV-x87sWt8WH{LJ7P3?IOJ0aeHZ#g`vct< zM+<&A@J}5e&SDBeN|T~-gW)4%8(eashOlNoVQu&Y&`n9FcwLerFih}x(>n$1iKBio zOZp;7Epc;~C+NZUZqqkph1}$k8L~(x5$h)M9()l@f=_lpaY1tnJ$#&n&q3W zZi|v~&tmI5^=Rd@2zikxq){xxre-2`v%#kZnW_}ym;v%uqaO|+L5cypUg@-LOB%!@ zdz`r3+5{{RfGlm8s(wgGEmGi2Ip{-HWS)Z`XwGLbsqt(R683Nri67_oQHE>joZyOy zPdi|dR?`V9o9Q~gNW=QD9=5i^kW|*RX%oQ8fC%s=^(cBH!6&3Vnd%!SW;s#xD+om* zdW%fOWS(+zUuAGk_fbuv$Ht67fM7h%zQYVhLV~+&Sm!pcYnU_QXMhQuSo*_J{Nqlv zi4n89vbjauDUQYa>GMjlkrOGkm)+LXgJ2FEn&=Szqw6NRN3d*+R)Rkyj2zI=03B>p ztEwGKzL!a+7&A_{u_83PI{63>wDZz!$z8=Fnvc76QPceY0Dsd}uRx)*5Mwva_N76} zwRZ|HaoulO7t>0a^^>OCQr|7g>nD=;V1eliakfFqxUFf6(A)|*xtK?q?TA&$K}kCXRmf<`PSG^Q;>H)-Dp z&Mkq{&XUDk$+6JlZz~|~=BrNPv#m=R_RuS78?wt5Q}NQc7~Zd8#EWCb$_ZIx>@dW7 zBJF8WENO-{f~f?Jb%Pw*Ijd420O~qzNQ?k2G$KatNZ)3Ga0PalyL6ZGHqDUA(J7;C2*4-dJZdgQ?pBR`Btq z&KC99Q&hx0mhB7Ee01b z<$2DdRg)OaVPrtfO*w(7qvw$8DAh%G$-LTwz$_FSJe8gHTeg&P%b~YJ0WJBCy>|U2 zv}vw$=LVW&n(IR7cB!Z)%;1REVOQ9Q_0Tjzk+35IBO>tULBoo}HwNFILkiSnXrNI*{oJfY{q7yYnGPgG$cS&?BJgwVhIbD`hE9KnS za?&rOVzDuiX2Ow^D;V|^he^U-0KN6O3>)ETGi++4B=0vx*>BwR&0#6l;JNOtAMkK= zaO7FM>0B$<;LXC2k0T{ndSc`p^0twTVtv#;WnVvf(JX^Jq=Aq-1~sAH>C{}NNZ!18 z7()TyzhA#dJ8P!+Kk<+ATbI$;h8n;w?Q;f>D0NHYD$y(z zUyR8M27JthPKESITE6$$Ar5XC_vn+jbm2O_&Qam;wuqbQAo5F&8?);XG{UZ+`YtT$TkDbUxct^Tc%=54`F)ou+={I_cB+W0R- zHmcOvf(`0v$gj2Ft6recUcKtGKpvWT8FHjr=DtA%!yqaN+3hLWwGz@GW6~syQJ(6y z=hGb->26r(6p%7pQ`-*KN_O&Ei@ww8))Vh2TTqfty4QzJtIt?$iV(Ya3||Na;lLpmPtweY$6mqvCld-YSqPC2Y54kK zVMHQnjAtZAt+c@d20mYZw$sC z$uV$_TWF~4gN9CEAB1U8u^d`gxeVK^m4c$8T6mLd+gLUEst)+XwdwjBTYffkrLT9l z+ppF=!ba^F^BHuA0tv+1HSbpW#xvR%MMgPqG+Wa5ZBYtTh?PhAbhWQBP(FpqkzhQ8 z>c*J5w^h`?Eb>@K7zOgDW#_qM^!;_tv%!f03@n6of`Q-!7Uhcak!FPGy4KlGEQl(% zI&gv;G@J4O6IzNDUzenI?eXUM6@`8=VG@c&+(IOvq@%zT)36k2ElUNw6#=_Kshz%* z(!qB;O45=y2^wrSDMHSc^CGr6c;$o|Np5A&V->II7MmTTF%f>r|AHC|xsVak;uXFCDK5t6~> z8o6aYqTnsuB}ixnL9qV-LG$JF&!!L~+Piu2(9u4ltp5N+7poNRY#B{hi|ee5*p{C$ zImvP1GexmLI5Q#2F{^PzLb@1$4sZya~Yt z_!0$&vfI~TbfF<`PP6HXqR%v>rozizZ_8j%?9`I%x@ILP;b77H>&yWwD$x8ri}m?% zo8`~YzLA5GWQ{tY!C+cf&M%1Y?4_lG7&X=fg2OAPJ-}wNfY}pgvi8Fv5L>K3xxphf zY|9D}24#&Bz8wA_w5Xm%T|HfzE>a@)QgoacU=%Y6&03rNeYHhZTcpjBX~$s_Rt0q{ zd-j4w+BEt&^w?!h5wu-%57d@TwsOvR^ewAoDYgL6#-ht%*R?N2D9wtD-^-RCH%G1Q zy0&>1HCu9=xRP+OD|h{sy?~yI<0_S$^;qsv-JJJWHX5a$8-~=r;jT)D#dS`CI`vqR z*|gJl9eAS1U37k{dA`K$PGpAp!0j|SJVn?+mF;TkMEFeVN{!YfHu;jwTTZzfXIG1_ z@wSC9YEq8dTEpw#+8@@`el_C{!9H=_Gh_nVV}}^`wMsDKq$4g%E*wh){l0|9++UTX zMVcs_kh+GOk*G%L^9TZ-E=*=s;pDgK-|FFc0-TiyZp&Fb7XFb5i_rUg-KbuE<0m+i z~fKzz7GJ9Gn?7noafUWr;jDoKuz z*<}`t6;^F^f`2bQ5>Do>V&>g@r!Q7BeGD45B^PO7`A19HVh3N?DSl3%MHbfrmC~u6 zx#gDN2_s`yz}(=#dr|~Y(79uZmKTPdbHjOeYHkca8+$7f>#^p3cz_KPEhEaho)R;?MimNb~mQ^YS~w9ABcKt z!ma=ruCC-3a^TH)evI_r&gT+MCWLIfpEB2mLn_4i zC_YFMY3s=@vc@(+8rD29SHX{f)lV}`Z!x;avMyV`{=2K+C0`wP%;Rkh5b3(}5 zilQ#M$8++KFsteGWt^KgZKAn8vbzVNTl}5WDYLFRF>`qp3a&(!8NQD3q9uRQcULZR z0Q%$em0FJFMKGclI*X#HDt}~KqO;MhHJ@Knat--vvD$qR`Ds)d1CWsW>?+BYnk_6v0=VcX;NQ}jdh6R1BrKOi`Rft6shv{&sh*Y}c% z0IbDEQd*PbZz3S3jRa9l3~Ct=jxbA$z@{-r7>^@%IejZ)!CmrJ7`18ILMUT`WRCX@ zn$JO;4s00Zk1UUrF_dQMiv{#NB*$Qqy22bij*`%!yo_1vhklJ46R-su&_8Zv92GQ^ zP}=Vqc%(l!s~6uG_@HHqt+7G(QA{OD)XL1!)7Dcht9hO1__j+ShDfuj0;|pA7%T-9 zjchg%>3u>e&d?yKf(?`uWRms30EbznZX7iFL|5hB7v;Lz^!})XDTTRTjMjt4Z$T4^r7EF4Jh1rDWIw z#dkzOxgy7tNn655-u(EjVIrC5^>*ks`pUyVI|Q;vUGnWM&osY1^Av6wsVpcS+Oy`= z4Vv^;+HG`I*DH3Zu)1Dduh{L8w)ofe|s0F2sG&yYK zNY0b<8pohsa$@MmJ-f8c*F1JmW9(!QULK5n*Ro)p1|vYkv1(-%bevb+dm|+st=@iIb|fM>!!}cKCYP=3wT*>|1W&I$ z+~+tnaq#R7ax7F>2s}s0wU|VaSm>;WZyoaZf*{%!60B81BeQF-fano7F=9gqEU*b6 zTNF4u+0iyjITmv$5V$k|z*KD5!fS1rnxvl#rNWj_!EC0~k=&pYoU&dz(Yd=I`kH00 z)JHSwg3k}eDMJJ&2yd!M8~0XXju=pKhF@V>Wwn>9kL9q9o(5#i@a~2+$i(j}GU&El zFIxu80bry&bWNOUqKR5hw<0Ri_Pvsfmk3^p2E?Yss>QL7ZN**LL2mw|3NV*ytk&P_ z?G$X-8s4q?D9$BqG>aOLSm(ZP3flaYtIo^r&(@CN=#S4I%5Tpe z3Tlj)MaNRW^H?oBKwe^&Ib6Td^yJtgJdG1P0Q;&IWi&+AWI5RTwd`H^nHc(X_{b~_ z-3&2?;$%Tc`$0i1tlp7Ms%9gA-;CkYV11c+iaWO)3B1VsbH*OiBO+;)KQL2cHUbjC z8X;+e2n@uI3lA%z=EWvtEL|qo9|uImjuK;JSY~mKcw-j!>-N-ecCq?Tp({HY)U(GC zCbh64Yu5RTj5~o_t4tRLmuF8N6|5=(?XQ)rai4D;3tNE^_EXn7aEB(TDl8!>$JlnI z!5oZjTou?%bE^2EGHKpWMqJq`?SF3os&giTQc!l2Nw>yCUHA83m`9x z$3hkkU8YKO3n5QAm!A42kU%x2 zPTKedgH_i*DD#Wv%Y4OkVk~V(i_z4Du$@<1ixri_T{l(~Eu_y{UiFLAB`&0*oYd-0 zY3HhPmg2EB7VtM?uxdS+nDUGHL&(~7wIU^_-BrMpo-H28gf{z3SZJkMtSN|cwX0cU z7aXhSaRPJ}jL@GjW@lYg_dwQfsX9CM>-BnP75+;8S@Exf$jde%VWT-|xH^Q6Hwe;- z7$2)J8;)v)u3n8f4K!#_oTbi-fhh}eV;=G_-TGk7ktlbBqiLBxVD%Y?$7DWCiCPK4 zCKQ6?mPP4i!_QHaYccvG6nCzOiO)A|(@Aa>zBxGpypJ+D^|k?11kV*I%@=6$`wWi` zU}^ZmFq0^5=D}0BS&$zrd8DnrPYejeiJ~r8PmrCYF$-pe*o+6`9|c z{GBOXj<1~wC2n$H=2UMi5@Q#XnNEc~zI+(=K}0={xMOh#HNedq;8(aD_B1lS+Oz2h zCU%}FU1}A#5*tXnrJbvH&z>7}>jb9nNelQHM(<0hqceOxK$CN?b0*q;mTL^s=qO z7Tn#u-KxrMuHD$|Yhj}Mzs*>d{yiP8*9AGY<}6R2E$TEe&4JLTJyoLUT@`9|x)rsR zWr(!FNbpWv^eJtiD+_E`z})@PwKbwTxWW3x1pQXkY0^1=f;oL&xlBci)>65UzOQz$ z!e274o)+2ai>CS%31e|$#h$|D?CB1#PDS0aqHsnhbmT7^;GYFrAaVHF~lM?}$5n15o;T~8! z2r7A#Ah&PAIcKy5*Yv4HM}=)BgCcJD^I#nr6yz!43xynaczOQ%A%-*(0*yl523hUt zuTt17*K0oNw#xLC!7<{-Dmc)lRmgAa3CzK)3>V&$E|8W}9i2K#)m(9kq<)W!QVC`j zUgV&-pPZ{TWvxjy+?AT@1hMlgIq?IuXhr~IMSZGldgvM5(5H<;o|vCJa@1}3?I){b z0HR;m<0x*|u0oxVBTAVzZLA8b^DJMIq5U{~e+EBG9Re1K^T6u{#iwMVW~QXZAFYHp z-(<$R@qzvDD!ut)X^lAP5%FWWtxVs9?chK*v?b_`D^-?CK-1<-X|hAvHv;!wbgSj5 zV1vCL)fqY=%J@)TGf&mKEtMFY-DF?9i1LVy%hqn#<_b+=c+!&sJ|Pl1g>&69e=W)_(y-e7t4<7&ns!S{VivauI=9VSq)cn zGGR|^W*zzg;8D^NE0SBl#TS8=MKvV<0FX>XN_QUgx4t}Cj4S;U`bGMI`gZ-u{{WUB zi5%DByXio(2M58ZX3rC}*g?gLH#v&)^2kzzAju(H}|BU}(0L@w3T% zB4KpAozzVQW74$RAe?N)%#nanUWxg@80HM4=-uX{CB?EGooqf?9SnW9pd9Rr3PBtA z4QD1ZFsLCJe41kbyRHz=zdpK%ue39?hFgGtOqU$)LA)=|s3YU+9+OrZRjpdJrtXdn zFLPSTOEQ=Vq-!m!!*xL@stA(mwRI)zZFGEgGldMF^qts{2ZZVHMQ<{*={Yd=&x!FO zqr#hu6o`TC>j8_FTHUrhJ-C^KWM;cv3I_7vw~sKa#U+5mWe-*uM|eoQbMcq`JvlRM z{ripq$zja;xKps%0y?#?6p+M3?T%f~pnGn~iO^8&<1DXtXG=k@pv#Pcg|V4(LT(iW zrna5P_6wL|3dS9eAl3(;&9-xUla+dHM!Gaksd<4{^cITR%||=f{Gh$eRXsmKQIwY) zMQXQwB8^i$zN&%P#k!KU!1YymNp&Ywsa*WQ#U>MSS&-$jva-jr3ap%ny&2+D4Kbud#ey7Zi-vJI5DxRdK?3-z zH(I-6y=cJ`z->AKn1MH7Gdc#9De&wloev^V{?t8zR%>{O z@d-xvNl5vgEjP{gy_s!Y+pimref16P)w?C7wc7<;19KX_wtRdZ&cMqXZLJ<`7*oV- zi;SyM-%JU@RQBztfPjULsC7|ZADTti(%@oJtrchnt036*oos5j8EB>}OcD{Aw64i) z%(`Kq6>aq^-fsc8DzWw0#e2w~g!k5uZ zosoJtSXRRS07RX-ZN@|JDy9+0t4j?AsAlwv?f(GEo0(o4 ztjpdhx@#U#fmFNT7FhAj2GFcJ-qo+>G^$5(N{&}{Fdd*IN;|J(#dcwdP^G*2Gy3QH zrDlF-eqeqz^V?$R((@PR`wPM=+UGUF0x8U7BCujHjZ})`2!yB*i*Eyg;gR>ti?S9Q z{vp38kT)7to3v?Y^MYeO23!8mY(^+AOW`2OKAXJE)Ps0Tnha~gd{)@u4=u9BUVcp_ zAXNVVLz@~;=o*+bfZktUgA9(={EG~{DY(Weuucg-~{WR5SEU`W=CbSIz+W+zU6R@&RqWhPmcy@IwDM9 zfepY+gDQ3$y=tUM)3&vm%~z~9*XtyzDK>R$1_Uq>t87|PrL`{X3lz4!QAx)rlx)-) zCMzxMK8qhG$&4{J=*lTFTk)^7H?GAcC(SM6*FMpK=p&gKQ^^j;LuRA!Jo#2$jJJ6& zYI~~WU8AgHi`&UgNs$H-#tP-Be$N=T;20%_5RCCp8)ae8R&PaH7wMbg?@hSwdu{&P zw=1rFBkwhO-(H1aX`rIFY@lh=QfvhK9x6f?PO{Tga`#ZwO?q(H2_+uy=8nhUYgOxF z=T>lMTeH=**E93wZk74_Q7a+B=){5Qr*3LPTgse$Y1VjKfPF>MeQp`bzpH;cnw!-b z)dYPqc>yfVsi>BCaoDWb*^V4ewl& zkV@lN)r%BjjKcT}nUff>Wi^O!hMLFjLStZ4UcuuHP8laivT!C!8>H^dA@TTAhHJ9$ zmmge+16<(=pNG+`wHQM_Aj+EBvA-Qqbr~PO-%dfEO21_b6qc!Gf@2B^nZqQGo27;( zq5J2LF9HRfR&CkgpmN)5h6%Ap;jE!?-w%xCj+_e|w71lsW}ZBJW*J~rD3q-Sk39e$?L;-{7S_x;HHdK zvKv-}8Jn|$J)3*Qr_9QeBTi~wi;Sr=fo0=dV+WRpEI>HWy51+jS1|O2c$PpokfS(a zI9VnE5jV*`wzp%SQ7E+P1!?5lG;6-Vxq2U6aG69+AuOf2_| z*(4lB7-vK4_f`dQ)3{dRG4^kYG?@r+W=D^S&H1#?JNCab{SXz>6~f@{1?2V06}8kX2V5X%`*1d#zvs9kinQCTK1;QDqhDIOju zrqZjeX_z`>t1CS^8;+*W6TERnLsJuHun(Q-X=03(l%AmHsRZ4W2rKN`vj7ekHgqV4 zNkOEIl{(%uV1xgU@N?M%W;ZE8McZECR>8knNuIuF% zK8n08h7LvN6lg4p4GTl7X>@oK?>JD3z8(ed>?v*zdwVNH@0(D~p_qLWpnQK-0(1@e;a*eVUx;!IXi zEAxW{o~ITHWI55oSHV`z4|rNqB>Gh-kBa^b+w(S z$_-f6hC7>?`MY~13WqYa*=|#BGKR=>(VwdUEo#`UW^ODRn&)SrYz24>wEe?mSDg(* z$>%>P9yLnl{&%BHcMmaCzOiU&t^WW)xVlf(v|~|@VUFmh&TxI#ua93h%gpRIrgJ>t`(nN$YCOwdC~G$2RMoWwN-i#Btjetk!+9 zyRugf>#OW56%lRL4ldHnZRtun?B`m!xDr^xN^o?zQp*M4A`G3NCfI;{ai+sbtTb$# zMIL~evb#H1D=V)O(wNG>3KKTY~Q}Fs~p()Vi$&Vp=PSO^0 zRXm-vHkNzww!5LU%G|{OU%Iw!48}h#4Nb#gh+PuT@bE`ig%sSGY5bkHYOR11?PFOB zJmXWcdL{uCDRRaZk^@Ob0?URh;Z}21!`EF0(3ZOsq`T^EO=a1ag_lLC^~&!wr1TdZ zh1L-PC`X$C^j>u1D>0ggpG0#ds*;Mtse_$wgVj4`Q!P;>wi~T2D`LCgyH2nC?nR-w zCaYv;XF;u=+ve20D^CZ61?5G{ws;(sk@{v%yQMjjq{vOwh!Pe?0g^I_G|mdI)^9Po z4?Z-&OyGKPn!WdHq2zc7d=N;1Aa$L2-g-xA>2GrC`%^c}eP(Rj6+H-75-XyXZS{?xr;>e6T_GW%043h0HvB2)D0 zzg1y@LZ_yvvG8? z>4h6&+%^{2O5>cf04s4~KTAh!f~-Phs{$fqfd0*p7&&$@h8i0fdCg?j1&c-8vl3(i zT6VRVg&Y|9qlrVDJ50l)9qMN@V8obCKlJfMy*|h>2+W{S%1m7;3Ak7Tdo^!c_QjId zblcr`)AzN?KV9-+@{{O$hSaYt#A)?mg}8~e3srGct>%9mpQEoov#r#YCBx0mN@`D4 zHH=@Hir$>{ZPRv5ZF8ssyKL(F?h%hr^0g{~%5*KVt<)-fFKRVfyzKn=xy`mUg-_Vm zH6W+a{ohwEb!X$0B<9#)-i^_=QmbeU1Ythcn=HD_!2RXe>XW#KxGJIZ4dbBAgH=8L zyZ*O+fWFh;mj3|Hg*+)`!g2@(H|pO2=MSl!|VLu)s`<;TMnMnlL&zNh#-posR?~s+KIk=CoG~f*Pg-0Uf?3Uf7lmm(yKY+0Ve8dUl$z zZ;qg&$Sl0AScj{zpegmB3)PTgM&`ynpv;4n$y1V7p6MgY745ceyFjven)a^kl>Cm8 zXkf8xz9~knsV-LH(j4J$qLa>;Z`*rO_7Ys`vQ2;*%hOa!{R8NZX>f)4_{&htvd~Nv zsoYkb2rOY5#)gK`3nky2f(dFMn8yH+!82MxmLSpkS^Dkz(i5L}$NB8#MHa(qVYY56 zsbr-Y4#hA$Lg*$*D`_XVoIU%!ACwtWKG6nzaFTNQjTn(_qITzi)+O=rg&pxa-LvPTN`C-xyBSIbN9*X)o#{!oyc9JlYb#G z+Vgglv;=~oV=nAm*ulIh8wR(6R11xQg0EpAnHLSOHI@q|qZ%B!1?RgI4STdr=4-Z!mUEn`iV7RNdCqr_$qU9;Uq=X_Pxnr=oxqt`j^$T;{RC zpG6~>CejkXB&bv)k=kAFo;sa6sItLx9oDX74P(*@)g8)ggRtCqW#^ZcZkL zR3b)dhhjlWw<*OxEa&^%h3r5*$95vy+=tB2)-thcIXf4jKLnaqzTS5x<@)8uVv}h_ zX&7oV0a>llGD#Ua&VIIjvhQ6H2ak6d+L8sDCfX30k`06}A+VQ(d0Ho{$DXCLSp z@N)aje-*+$xuZcdF@k_FtiK~>JJ{z(OT#E|B}8KyLow4}>%(S-K@(l;}yPM&DA-(lPk?wCvtNlH=TJ|D24WwoYny? zhA`ko1W5I=3@l?Yu`Bo{Qt1Zc2$IX~$6kn&Fc!-wj4?`|SPRy0(m_&zuw(>ZK;~z; zujXqvyRW*?!%C~8ocriaih*#MPYXcQ9L+hs%_48Q>_tNJpu(_~v|E&-;P!u*tK z(X6jNXR{&(X>m{O&UdW&?H-8dI)9z$`1h|ghu;`U{g}R2N!xVr5EP%j4Wv7aFllwd<2wpfMS*{n&OS_46b{Yd*q(aDKObkM$?$r!9UX z{#A0q!gMy)H5v0aH`n@$GN*!NU&d1s)|Q~bOUQ$$*kOg|k0TgHxpH@G2Qz-yI_8T# z9tP-ygc^gEnXrn$7i5X?Jf4y;4uZgpfN0ZAOqMdFxBXcNK?~NRqYlo zaxYpStTd&n30YM~B!gT>R&ZHUKc*1vn~FvpN9k^3#%8m-5((E~^*G7XfbOvCx!*zi z*!#*0lvh}yIVlru8Z?|TM%3XiwhkGQqUOmdN=Dk?9&jCg`?^+P{2{5JloaT?_p#0k zpwG_BA|jAhWzf=DbQIPH>N3H|oW*rVWu;xOURk{jbKLgNdl(Q~V^ycq5Se#ZmIqB( zyq(Sb`jk~wM(9s*nwa6ReU!~xYb6?HD2#|qHb6dgt8X;B%bstvlzULMKh^5uD1K8z zsd;ac##VO9oo8FL*wcCgiJ$k?fXzQzXq|!i?|Qp*+iybhf8?$HnjdnL-n!(&5Wg;} z@ls0?TeucfQZ@oGQX4U!1d6w=^Fsz*=HX^2a{9E z&B%Vtq7x#$G$fhZBYat8gi`RaH@#~pyCGQ$6Z00ppwaL=<4!UvKwP913|lzcV9U%= zS;4kO)KW_6jANVFGE8=`aL+KYwq6zm*m~Duv@a$HisJd7IOKZJsIU&Cg8|;@V5KeG zYGCC+U=Gj~ImB$x*|SO+9#~SCE#}KWvV*T5HdCmP$yP#@f*_U(0VcavDmtJ95pcF( zU>KH_tp3=TW>Xh9$~a9G^^u^MYRHM$zeIq{wTVIlfy;LgO^Lb?G-V4EI>^@(paK!r z2>#&YoFndp3ub`Qw#Z+3ug#F#6I%w6(+=wlxg?z83EN4-KF>O`FtsS51~oKBX*SXx z$qeZtW$n}?r`X-r9h1;D-(Wpgwk3mdT{nGyyFERvD!Bap=T|au7GpHYj2>hBLP0JB z8j<#6HE75VERqWkX?!~XF`N|Or zb8#DL+2%*6;yYg1ullN{{GTMky0w`t05{obY!$tTFJXj21aSvujzT9iaPyuMQDq~b z?P_BA{T%&~@7|f<56J%j%1R7fUhKZ0`I?u_tFg+d8#fFjxif{#kq}e^VK%V<_X)aV zcyN3VrQxC3cri>j4U?yIQe!4#GaYct!Q4JnIR5}hWLBR7X-@!e)@mrz0OT{O$copd z2$sQuw3dr5IS7%N5m|AHD$4+(MTJe2HkKjTYV&Tb`g4;HY|W5-3uLZ^l@--PZHk+j z+0!qJfQ|0$a7JM%ESgn6-`l~pAyJSjQyP|NR%=;sbgP1qv8y*`UQjWMohX&OHEG->-qWj;_Da()!c5VFAxVCABIGX%a2Q98q1IxkC!l<4PHqwH-? z9Njf{an-YH?H>rqUN(9j{QajK^c8MS-5ME&eJ&1SHO_aWE33sc-&Exk^A1h=A zW@sXiFm_uGkA$sn{Fh)V9cW(ENqTW)@{k3!Vj4uL!QBy_(BPGTZE`xTcpVpJif?k- z5~h$#OEpY{NGO;h!C=>*Kbqm`ku0$MSu+8iqTvk+g24EgC_jdRbb5743Rc{VVW>T% zGP=O9v&Tfg5lUW(e0fVmDB)#K2PYF(4_JZN?}FfwHcGGUk{enPM_BV0iE z7|anVf@K#{lN0H`WI$$l!^0U8qj{o4tmA3bI}I4j5J%Y4Bj`9M(<|~bn7@%7TOnj- z9wxzVmN|e_0yxIgICB_*24a2mX{F$gnV6$r3)E&d+;jR+$$vs3KNzO|hqEoQC*kns z)$DMe1IMcAwrKr_?5icx;<1?Tf=`zHl+50paL1OmIxmw1vHTqOvvi#uj>!iz1hm5f zKE1fg2*_r&YR!ZQ*hMES*E*AyOeoX11xrK}G$m>QOdJ~03f#czuA0z`f_9F(wWwgr z8xZ(IBNJRqJr;4HDm3}7D<-I{igSNJ)RD0@3Rb%#QQF*{(Np`jG9!A5 zEBi$ga&fm#iZX_s(9B%(aanwiB8i}BK3uKnB`N7kY$N{YnvQpss7F}s4k{Xww%mWG z?p)Ez^&}6hx*h?|qFD`7lB^hl#JLY0I$CZr+QlZYI)Z=!>Q=zh;%e$n>-}W?dE5ED z`Q`ZQ%0`@P+=4Mn`B=zI7+8dY--fdbC+EgroA8;$Hx8X3UZ_%_&z*!U_C?R_WOc=_uZtxonMoqVc1g%C7c%!miRJ!g$(dXgrecRblGEP zfpUzldL+Gp&t$!`X&hvYKEDj+BlVq$8H_Nax->Y~hqe*fCgZd$=gUUKu!&uE3D>eE zZ3L`#=*@Rjmat@o&7TOYuM4TlvrC3* zI?611^+jl1d?{s%1ubSY4;c9sU5ipng@!l=i6Sy&M%^Sqi4diHOcbpfUmlI)<;6I>vZ@MNQ~Te9%Y5<+_+SPjOBR<7t#n?*2t<|4l6#O zC!4S$o3;^c7uh<+12`d1F{8LkQJbp=P?02?lFK&JvTU0VsU5A`D#){~U!R{%yQYRmD1g^F#JKn@}kFmb0Y! zADu#?t)~TbFsX-3^C4KM&>o%QxpQw!Q}*v@UleaE^YNtN#z(Z{TroHyVZ7{dY^m=l z7GlL@)?{Q~PmQ0sWypxVO176rc7N-S=xdKG{ziUn@Ydpjl_uCqOKejXPwJbce5quu z(nyS$GgD%NP`Dd~d5-9Q=c-94I%UPtH*DBANAvC4vEQ$6n)06)&5$%?Y+S*Fpvv+t z!owiX$}t`K@YFMIvoJO6%kMpfH(t4(@6sw{79F8?Mq?MHnSM5zCL(4uzL*<)C8SZ2 zM81#Y+0bM`H=l;A3!40Nd~~|WyE!_)xEnC2K1^+qSd&b9rJvCzSOL!TTQt>%ArL1_ z8LmVfCl?vV_`FrPI9mP?aDY&jLoZio+EeSWQ>#Q06^abslgAwk^E^SOnv&ZEqrS)9 zG%<5yqOI-Ozs+(az$Geb)Z8P%#f(f1cJAurME?MOYq2V1=E1AQ3m+$|0n2gXN}d?R zX%OV?j3RFA@-G+NBP2^0=X)>IkfX|mNr{OCQgb*#e>>)7V_B6cy0T_p(#oLgsBSvv zJLJ)88?vqon%c0eTf=+MSZxWA@X$kg!3c}4CRfO{lv6$h;V9d3e&LOKXnREmEdU&< zJ3Ec~*}l5X>aptXL+=mI4t}fX{$H(Yb^2z%e=rE-tB;?N_vx3k>%sng>%MpB`=PU< z`9t#|#eJAFqRP2}7&8E#DTtox*vzl2npEkuU9Iy64H$M=yipAtzx&hnkF`2Sh(8{G zIe0S?gp^>oqLY?74TK3i)a-})I&NZJ!~5iYkbY$LAGu+a-5+N-PG(0&Tpp~aXIED1A^p=?k^12Y zlNVldpl)R@37j0NI&kl7u`5e{{Tep-5~9vwAf`O< z`0p!1H^|sRRwcv^1t^g4lVu*(C~lnwbrt~+c}a1l&jME{*94ldEX}y=a}*_RLH_`> z2--QuG!mGOYgdXXmU6wq7%;enAP!^SploTSWQXyh zObqo(hc|3|cSV~QZt=NE(FB%>kmYH@N0FsW*d&}-69#5$Eh7ni@cEJbHUmlM5hoTF zB^!hQ^wD2UN484CMpD7i(KqeszNFw`=3sB8l@7}Q>L z+fwm-x!bHZw;O2Xhh!3n*cU?#L}-l~4aC=z3}WMEZkH5U&p;l_NOtd$kCp?hc~35B3H`FF%#9Rg(c-BsdHuNUX}A4m2L0}PZC}A5J8{?M#kNj+01)& zRajknc23>_QdD6%%EM;1wiFSPpv?LtG9(Uc)<{UO9!>fmhLz2+1=SZ+4;jLYnKlva znm{l>Cup9otI5WXY!E6yZOw6nYnr@A$=Q!Jq!B2r;vz8(g)(%*CZDW}k+JAK)nGW@ z*5Ql~#~x!9XX>$(Jh>_@CkIU>8G|eaG?QQXpd6~37QiEGr_>8o8kN4>bs^nZSEOkk zWvrEXYnzelD&`zx`rE7)emgv`EIYf?oqbnfaQB`DHFE+DN-d>IaR}OmkSZ>Cjm8#? zmJl%d6@$4N(5}jpioY<(g9_{{w@+h!)P93@-{lYEClv7tz7j|8ZB+|s!mrs$ERHZY z9N8A8H3wVP%PeN865~P7WVo42*BW+EW0UqIQW5dTc|Bgu5A0q0y@eyqdv)ih-LRSm zLL?hW@I}RLkr&w5w1bnUY&b=Bm{IVA(b1>J)#*W#^v074MBKDT+DAv9YAig;&J*EI zlN%X>WJr%MIAqO8#hKFbeY={hXSXZrErVe}ecBQ^@Bt#Rgm7IUI#0Mqn$M>ob5150 z&KOGthO3&^1d^*pLJ1O7uS1RzN|XSWgeg+Hu0yA#rh^Atj4~mcC;;IWJa(+u2`H%WpnWjw)^dIZJ>u~=h`NmrFo*Tr&H^|Qw`~`ooQN$@dU724#W)%3XQqcm+(sJ zoqUxrsXdkXarmpm zJSMeZt*0Kf(iUdV6iQN4Byq+B?~Ayu{SdSi*N#Ht`8O{;(;t4#a$v_N8+c+fwTLUH zZjxeSnHvCxdq+!_H*SKFWX9k2jA9kj>{;2xkEo1)rR5Bs)pjTvJo9b@WvrO+RJfC5 z!Nl76QMgMlxKVMFg2}*$&rYq%k(jCTv_dHwZu6|Cj9m9Ukzvogk6ktnF0RD98wzwk zVt5VFNtQ>ZgbRLv#29gjV5CXVs_h^qM`3vuP)u9#OhsQC3a#5%*8@<2S80fA@T%I| zcu|_OG!lNOn3+FaF-TP+*D8Eu(Xw!lj_H00V8~w3{##d-6<7StgbBLy7&!O32A$Oc zNYZ=E>)(RhZ(>Lzqa7L!%HJqv8{_JZV{z5(upyNT;hFa{3Y`)rM0OHuC2Suy0#Rif z5>K18Y1|(i{9rn)Wqgo*tfTRAvKkh>e+0MENFe?YCIe`Mb;~J*2t@PC4CirH2C2}63 zDy9r_DVztwUkg##P%oplZk;kDUm%({@YxWRPqD=(8PwaYigwm{e{--c2OIJbVhAI38$6P*cgN^o=ud)Udgqmbu2fp=-J! z0jYbT_Sev>vJmA#j`-BkQ7vt9y+2gBo{JmH6YDd98aEFvVgYSfu!@+;H!FsQbR z13ylUOp}d@;EN|9-)qm25UI_fK{H5gJgkeo85&?Mm#E&XbC=rD2FN|#GXxkWm zdt>RA8*GX3qU?|C`7yM_+2&tIm#25y{E0BG^@*^EVj!82WLJ)hDZVD5GEBzMOcAYr zU^4@n2J9iKSb4GY+vZ-z@P>h>{MQ0ZPd7}$Kfg)vI}fojD+nWuF*3c>z#)B*%7a#+ zOq1o2v}{g?J$SR(Zx(U;6}_V$bi&*OQ=;tdhu_L3wRr-m#R88@?hc7V@#S z#UrR0SQ{^!i<)Y(`VSeIyH#zY=|0BKW^)S`(*aHa51{ySFsEaSk0xY+SP<7XZPn1! zEf5-J$s=bb*VC9JZly@VaU%M%ykca{-^hS>Bk?FUya<)2(YHWBiZ4}+Anb- zxJ8`~J3{uI@+FTb2*zQ(g$7)&W9r$fEz-4(%fe&!{k&m~9%B=4xN6-e$xwdPlz`-6 zHiC{*&`jzi<>=Rh{exgIz{6RHVfUK}J5brvw0#$OF;l)5Cw841eJ~1S=!1&JYfoT? zB-~tOU^I@uH0fs$Y9y0nh+7u5hizMv?E7uPUFYqMId68UfM8A@US`b_2}hGB-RrDO z2^!nfr!_-|Gte0`f!?xmCL5Qk#VqQn)XF+~QVnke4RD^V8E0O(`E7(0kn_0!nWHhq zO@q-Z*%0P^8(!iei2Eqy;Olu@chz~%J0a;6_UUn;d#-j+Ng3TNXlR3`?I5!#hG2{A z+XOU%@C8pSCN=f?Jek1szpXOh!qZ~3sjCe44Nbnm-5=1hnN{Nz8S$Kj8X&e{K2wTo z38>NS1VNE8Wm_BHnl3v&b0vBDUq1cs>PJ&CRfeGN-;WCoWWMu_5>SLOPnQU_hD)g{Z;6IyK!8YkZ;Mx_Z3(k+Vc0%i}Lsx^ARkB(SBbP_1(&j@VvF z=`aMX=Mx-FN>!PpS#~VufnT{SDnhn8%&{KM{Y8nuJntExpiHEk(GDa6Uwq6`Lh{@V zWr+id$L_5$2A*TV`v=&2vrRZ z0ymq9GZy%2#1G~+4yalL!b3T#G|Il;TPE$aD;sAn{Y>+}KGrqIAJ(ExnLiaGz7J`r zO$}(bE@V2swIX#lMiMQ;T=Tm^!xyn-Lj+SDG6$T~q)Bqg=7Tc{frAuCr6lO^dCb7E z^cU#=0PIhEv~g$UPvcb-q-Bm$d}DAWLy|6REYGXr#HY4&hNXCHfd}s*c4R5KW6#wN zAvu9&NQ=J>(B?=7EKvv(io^_7U&6QfEJ0~YQ{_aSGsgAUM-|v&44hUb*aw9AJKg^2 zC=8aYaog{Q7Uss1DrE4+a~--~dUWF}(M9~rR4L4h8<-Oe>Ww2t-)BkKemjlS)o|K2 z>FZI3&w4Lnm6$Y?<;6qg)tzfszj1zFhTU7c%8Xk^oso7+&kSU)4rCRw39zOX5K|QE zFe%b%s=}mF2&kY6w}=bP8uEJ+uzU@>VoQiWFSX~E&D(m^WeSjH4x>@An#BZ644!%w zqm$9#IX8l@^4cFc(B_>sKPVu=&xJBvX~87ygi>i(g3R~Sjv%he!-1)%ux#FVrjhj# z97=3|hV7S*o#X8C9wR2pn;s;Ctb@YNAUyYvv0#PZtC?_{#tug&(u zrPjcHY_ky%TB3zN0zuGX*d_{kTx?j`w9&e)nMuVld2#7a>gK|%ZnQ!Ut*R|TsU6h+ z05Vcl9VJCk(!#L5rqbKB*a~{(TqA;+tRxhA=pMIIhfQ%*sE6Yr_~-Idp^D@UuB)`l z%BgwD&555ly zY^8v4h))1|Ar{^YfJ`;kwbgp0t_MY7O;#ZlhE=20lBDZhF*e7Bne z7|oFqn!xJQ7Dl`z&Gs~1JNhEeO@c3OsfUa*%K~mB|5qmm?|JV;A788z#nx z``Q&}GKIrLf%9&5sObpMi2~1g0FowVzq(9DavHN!kN2=6S$3a_QLtOlp0P9Y+xLK3 zb%!~x=eDw~4R*uhYxRq7T8*}8f{mf!4n19OcUzKUF+<#wtN^m%OPMs4Zo&o8C=Zy{ z4^{1ulbb%hwmL1yoz9kzD0A{hBGr7-7z$07wxJ5ps^yGD+u~CBDvj>Bx2>MvbEdhz zmapQohF#i2SmUlWpUfV{fuxZ{$APinPs|e$Vg6{$oQVGD=|9rH)^7eg#s2`6{{WXf zd#V%}>_YVTH1I6u=+y_~V6bP{38`O8GT~twrAHYP?4(556t+;bZ!R80JW56;BRSCK zX&*aA`1@voGEG#>cru|+-=lVn$LcO5_8?A54feU2M1#DC3qnua%Kp9th*Y<(kx zP93IMFOG@BMiJ)6f@gHLpAbk3_i#DJ8{Gq@36Cdqkt3PBfHO{_1_$iiGYO-$g|Kdx zfmC*J^?QilbW7i&I|NmRc)A*A-f>Mo^gi2i!F92EBIQzmS);%jjYAqsY@N`c&nviQ zoC~CdjU>>WizU z(IYJE6dAT6+p%dPE@Dh6W})*d~IwH{nxt&qVnhdT(k3><>4w)@&0 z#rk>rA$I(454bmbod5>(Dimazb-Q9n-d zXgJzPNZlJ;%{nB*&Tqt*rO45omNh*yb!9FNGraTPeAABU=9L*`uQJ>g`$f}Ww!PU_ z3u~Q{Xo*;HCx?Zx>NyOmCzi3>sSBN||h_vkEeo{T36!>z2_itE`P@S{t0tlic-q=>}>HUrpvSNBf^3`(t-k0BwRtg)9JN|^f1#5rZhPb*EDEz{izm1{{c#!Td>O_SXJkWy_YRPA73S!8 zk1Yo#)#2wltMo(k^Yp3j^W*Z@@&5pw%Z)1v8z}giBWT$h8D5E)QW-&{WGignzi{00 z+Y29bdSlTOv4-JyXz}K*j8GHrY4vkWw;6_^A zfQ(DwWN?}kjIlC4i6DTP(I*eoS6qprvD8B$CM5u1R-R~4aj>RNgE(Hvp6!6bI#J+u z1NFCK+fL2a{g&X+TG-ru*vujRHGKqe_Ug`MN4H_9Ql%yW}pmufyOuFpwlLR zS=m=r<14!C8d{=qZC->LzC#pGOr`lGMNEc3r-QT~9jXYUl0@J{e5g1#gwyDzElV~9 z`Crs>w?S)bTdt#PCPjFtroHi@p7l#kt%N;=7i7tEL}jB~Ewe_Q*`h4TI6)+}D;(cm z5z_!?B6Q6$Lli5cDWsx6_eOZ@5c0zY8h@FX-M(PSTVSegJZ(smP#w9FIcB|C609S( z#r$s1x$M0MB0-SWHd6U00T}=^mRg&=lNbQLXbiOEk*X5&5;dPrZ(Ow_q}y>#lW3`^ z8v2~0YDy23>w+;xB?_KNS&lvr?*YZ{sEm1)xnpAqwV=JVP;i5p=B^0@{#uxkeN z)>U#u!a^o`0YMu@H_fUn!MJbgAJk9NPUyGzWAfMWf1iCC!LlChVdeM~mXEYelk{Cv zp~_?)7hIpDiJu-sSi7`t#Y=EC04tQ3X6Eww*CNZG%;NU~( z@nn+~D=20`&(R+l3~N{v6*?r*Q)a`HpnhcZijr_{ndE`PDTHbMK`WqPZQCR2j$RgU z2u<29S-^$nMQ(k!?ng^4?TX{G-OihKJXbMVk%(egn*2#8_l+i4aJQj5WRlh(-^)k+ z3z@(~wN22V5CuTAoViVe2VW%8fsItip_F5sCa*A%jho4C9iwAhNu*F>kkCMEZR5sV zu+p??k-28X=`fVaSwY3&6i)giy~F0T51aM^Hb!0}JMVJW6C%^21lhcQvIp(^A}|hM zA9y0#yk}!vTZBj5qrNQ&%q<(F70OEHgRz!a?zDIyyFtdm3wPqg_N4HJj#}bRQR#c0A5wH9hF$Q zTvUPU2$U-dn&+zGwC7G~s=5BZULu^m1j`DxMgAuMg+O}0U~NtoqDp&jMs(KcT&K;8 zaZE2C9~i=87U`=j17lB@>yi|y0MaK4+wP6H;Uf(sV@%i{ zce%nZC0K!t0X7xY6zZ9+oA+`VFyFpLN9-W6*m~mkoCtmT={1?7_tOO91oS85_j6i*{e$y0WGH^w1`?;ME-C&FH+VN2(?%PLC5gx?=Ami@e~&0fTlU$`NSJf(NT76ch6GG7K zcmy-P8Ss)=*u|`6rgr}TVMQLl>vfw`FPT?$y;R@ApKyY|a>pJLc-_EXiJXEHrs&ekB(;PZ$a z-0+Ng0yreJHdiiktM*-OgR8PqkGLn3s=v5;qfzH-6Pv4CQnr9? z#1zRRquWl4)oXoqVnKo4m)MECCWNS{#up+1{wxrlA4oon>oQSuLPwtyBkiVE4Ng*w z>mhuvohSN{`b*kY?;ZY7{!nv-O*-4d6G`2eh4d-dzrJ1IB_HXO7)do*R|nx^Hfm_22(&^Wm1yLShN zoZkwslG_d`TLz3*?D?KnT?R&B}`bJXn)Emn`*k5 zX0ioaR4Z!A)l#6~`2DqBD<-%Pc&xZdgo;F*h}dSQCKN^o>Y`@gjm&S-snGM3%xxg~ zqO*ceL{E)ugiw~fVk7B`_MC`H@;oW<7pEn zXkBIe!dZa3W{1IqWVefQ@kHii;3W4iOUhl$qN*H}$YPraY6lIV{;Y^Fy4V&=C5rz5 zZy2+0mwPiU5(4|I(9a7=S1aU0#MN5is2Fij!#$Pz4*G?T&gJb_s(==ArhwT4DT?-~ zqN*-brt13Imvdec!#p0H%w;^Cpu`j=CD%U5w@Fw$O~-7C$50DBEUvKRx3I>-DaKfU z3@xfYS(T2vVpJEFzev3f%Y@nrob zvAL{#IgsOh0KdA3LEW!7Fig!8kCXKMf(NU-Ha$|;d@*cT%c@IzRzCnf~n zsGK-Ov0ggDrS91V}xDj6W{qKR%|+6P0RwuPT&>WE5k zIi?b!n1p@|q~)ZFMRg{ZflGs5sL{m?QU`=~C77fk;{6UV;zCWZudf`|JTCo^%%S+e;N(6q^0;9u3A#1>H{Hx*G12bm({R-Qw%LMp z$IfPr+au3^P8~izQ1F8)9KTPGDWK1oiGAn-9Q?TzY6(4pNn$nFUK*G~#|Uhy+a4Qr zKA=4BuxvJ0n-0aZ-z_%F?TNVDBo@DMABO##%LU%6YoNM^i{_|i!=0{XlvH1`X+1U- z8)dz!6tn?osB4t2X698MZKEmVt>^p+l(_AB8D_3YY6PDXP3dLzgTi+i*B;Yz%!!iK zUGId_5?m;S*q4V)9`uMnsB*#{kw}R!G?7(194AEni2l*{^A{CaztNz#K3X;4abeM9rMFwx9_0tk)ugn;H3Q~CdfO? z;=ey!(~ASo{f0tKL-skbQRi*MqJEhzG1kj%iH19*#UqmGXtH48tjrxPDg~oY(e&~A zB#vNT$c#A~gh@0jc14ExdT&VyoJ$g*($I(z**4QFM6Jc2b z5!NT`h6v2~BmLY$EQqifZV9zMfq39LK{(MhPS-PG384fJ*(yGXk@rNA_G2gLjqyF( zrNYZH1Gj{eoY>!NhZ{06;2&sQghCY0Hbq~HD`||pJEW9Zw(YrFa;E1MMj-jYV}d<` zDL~=&u)&ByjtJ6$-V`iN?lEqAdVPgr*_P|Oy}bG6p>o~O_euA5apVh^x?y1ACz5NU zT7!G5R7H(jhR3h115j;j(d=$!i9&L?9qWc+Igf5L|RayEptd9R{_GncQZ|UPr4U@75SBx?>D`n)Bry6wQc`z_cV1g$(I4W&1F_;RK>hiXST(K;{g6ot zuof1JYKOns>x~*7u6^(IOEOz!6E2H(2!!*GP z`Jg&E!wE#2H;wg=N%8R6{?#ZK%owe^CZ8Fn;7mk8BTuC(#QNC{3Zep<5~WXA8xsg% zoHfPXG6_vmg%k0O#A?RSo5L*`a)u&M<`A;Pg=esxsVyI8t_eYT=eK;xkoHPe#yp7L zlZyk%-ud>Zkg+6O^sQrKWNbcI1Jb}UY-qmdEw4GG2Fbz>-8f?x&kcQs?GTHK-u(Uo zmx$qx5j1O&D^T6c0b@-uz5!S}NQ@|LQGv24TguHLa2>vi-YwS)j0dPIdQZX9dj(q( zCy0!U*zN?OV4kNPL+(m6p)6v%KAkesZT8q^hS0V3OHNVzr9n`Yd`6`US9L-__UJPi zHxrvwKCWuDt{%w^TXH%pRVd_$)h!!+l!G0m62bPlF#3d4eAtr55uU1VQBjdgPZ~N) zcSwGkeyVnD^}xT4zm%Nmvq9NmPAuyvyk;Mp;}9L}m@+kLG$789zGi0;bK~Wo+GOXp zGol%itXNoa6;%+%k0i#o^2wVO&=Yp7D4x zGUa(%<=c$z`%l8uh))X1v^zM~9E`);sfA)&2!ZU{Fpu{1;`^bTQc_Svt5wlz`o_LV zi&Z0pPS$x7JSS?Kn#|a{I_HHihm#bFtz@3zW0xFnz+%HJ34#pX&b za(9kr1%pA2b;bpE?8-)47750L!KQH_Cbuv6)x0d%YN?#|^VU7u!C^euFBc1~)o!oY zb%CgU9+v}YFoEqg;Y>b^+M??kk7-5rHGw)*0@cTMgd5NbnIGS+`PgP zz(AFuhh7syO_|i6s~@agz{kVijvtd$ddZK9fG-mq05d^}kJ~1Wn*`>u7{om-VlmAG zHhqlEQiuKcY?$O=OMh2`fBPq?X_3U`E_!0P~WGEZ6Be5o>(Zx#*!q6P3?L})kn~y7~+To ztfgI3D`7eHJw~~&&@*4HBew=xXMr%fw3!fN=w^fpK{;c@9E5mljLNfClJ*sgRMwm1 z5pf2ZS$yokQug>m(-Hkh+a@Uil1h9lPQN~%CqRhItwg-}IeKwz&@~eUl&GF<$82qT zFe|tu{etl$e1s6A&v6kOvqH_Mly5N+1l&e$g&x- ztfu#?G5XZ7pKTX6qF^Q9K<4EFEFo2z3NIZH=PJA)Ce2QR1Y*I7D6MGh!H%ie?h5$cnk z7VsNB12a4fY|Q1slk9;_ki?nGs6SdiSi5H0{N(&w{J7_h+efgdLt)V*ZgI#6neM#z z3-FhoN1xpN-wmYL0mPvY4@MSv>EnJ;c$o?uvoy* zf|!Lebr9Mzx!Og$Buw;7)8U~qrta7tLidDsN7+Bzhd}$yWTmed$=%{Is;E6PVoZ?` zkgeE#sxFCB1IgZYU=33-jz%K6+u1XF_ zq=@@m>wKp>8jweUzjH`i_tqGBPVRq5W3@Ly~ShARgzbFhm+t1uUt|X zl$kacZj)YFN!1D+5l>V6P&bXErPymQ8pnmefJ=RJVYXwDX)><8@Jrd;c8ahA!BPzU zA7~a4XT!-0pwJ5$HYsLwZ2l{rVp$rYtNCsC<@u+|&%VVZ<46_|N=uZf4H4Pf*R#<3d( z?%Ng}YC2(y_tPNF#5*uz!w9`lGS!A| zI&sqg^@jR0nAgtxDZa}{t^u$a*c>@1x(siN43vRmBx9mP{W`aplj__IzGCo82G=K8 zRG<=hWu$zmBd{tiW=j(!W3SS)Hpd4(W{iTyam!DUvVoDJd(?MKiaSKTg(idCJmf4} ze%5YqL80R9kw17-(g71JXLa9dy|mi58(W>!?AArE6!Iernqf-IVRUA~nMa;rQk{&1 z5%ELTu6o|DY9t>#Y6+A(Dyb#Q6oq%sT%3c+;zhoL9#D3Xb_zR?6Hd4&*r}tOy0@Tw zs;F8)N)+!4spP^OsI~-xhEz;^F|!48WLNdXkZMBwtXWAY!zG_oeuw_meu2Awb^c6# zOmey9mI&}z#cRfFal>+%vP>NJ!Sgb3yq~TI+aySl7-q~&LerxeyD-%atTa!FdlO)? z+OzaN(r^e2C5VjVl#hspK|+`^2uprI&FdE#s62U+J>DBxyf+G!3LNG|9g<}bTAk?m zvgF2^A`=FUv(%++oLN_7pC!qLKVuwtrv{%2$0BX3XI@t9i%P(Av7a2@oS;gLUb1gf zkiiNx?ea@V7D_t{>} zD7yn>h%$9U*2At`XtEM>zQX0eM_E5AUZTjJ!X!)$5 zB+^^me2C!q;-yd}OV31+&~ckEY`A&v6Dn@_QNG)T{31&+uLy=nL&Zq8{rPRkaZO_} zkOh`txbj9Glr&9?AaQAsz>4=&t$|sktsMEMP0e$>zqDHSQCo(_Xfcs3L)ojhrs4jm zx2}(E@q#E)C9t0~x+J3UFbl6Hz%jn#@GigZD ztQ{*XO!e8`o$XrH5{2i#Q%PXDdRR7ah9Hg#-zyzc9#iN=0Lo6n6!JjB5sCtU2vbV+8D* zCRD&4ZyzFjUE4l_KpT4b2(zQYVb-eBKmlmcg0XwH4pEBEf#KNj-Y_9#UpBM7jCRSl zTkMMj`$)Mu1L@otoLxS%>73FC3Byfln@32M?#)8xZQZPx0Gg6(8rf#9Oiiil1$Y%Y zOl2Ul$nFWd;PWJ%AG9=i6EGB4$e(2bCWh;_#;fWe;0bLFV#2RYnG8?iWh7;?K80et z$&3(0a;bz{}9#&=4?_gYZ zqziRv@hUmnN69eB)r@B<8yo}FWGiuYKhdYat^pSsYMr-L~}au++(yqiSS zbXr&!;JWuojhP`X>Koc;ZkD0C+<5JgK>16XHxF3FfrcV{=0)w_EWhki&P>5Xgpd!E z?BdDCMCpP}N?&=se?z}^dtJkXKNkN0F?nQD2zey+0r=jb+aSSmg{}@6PKrY%xuk|h z*k^7U=8QHrU)jcsD`;~NM;MPHSWSju#GUS;`AmhnJjrl3L)OSYA)9s$n%uA~gvHZ^ zl$k;6YFOJc7Tm_16S1*aK`&%NEI5dAL<4N!jJfdF3;iB!f13a=xx#I-V7>08vg*oJ3U@KC1%Q8Wr99aK~*~iPj7=J=eTP=w6`Ct zMKSfdEpJaHtgNF~A?y0?B9@u@gTZwOFV=5Yc9R_nKof)n>(}=99@%Z$wvDMo9t*qK z(8d_owqWx)>dRR8ScoepZm81*LnDUfw|GmeOpMo+<0_Tq$}?Q;5TyC`xI4^7guws- z{Y7^Ok0%&g36G59&~g*2v@Bg&*U%pkM(?oVb`GOUqgR-SD5 zGoT_CTH_sWM67eoWzW1zUR8=|tzeGh8dipS`pHYe%g`dytTb6&7jZX}{NSc^neiuk zfYsJA$;4zpjgJ2SwHb_IE@?Q?3TKQtI%Rn#2{s#>^?UUDxjK9E%kg9Kqnib>VJqz+ z36zEafgvp_nfVT8@g+!ufXD+;;N)XYnF4l^wq31x)~(<5R| z%VcO|pqkbaRxu076u=lceNc!_97I%+zOamlFw`d7HL>}5%0Rv3*G;UuM+!-eJ6`1` zk5!~enG5OHGtD_ul*GckQ+mA12&Oej+EXzDydG!|vSBhx4fIVLK4)>Bz9^tw#&r?s{Tt*_NU^w`9CRK>@Mmg34xpd!2#Q>{Pv0rWPESA83^zi1;RT?SMvO zK7ml-Nj@oPv0$VM4L2uRTQ9?h933t-jDF0upPKHQMegEjae5qX^6QzDMh;oGfU@`oIN~Q=4)LiN=NabgB<7)RdPURW;puG~_b^p_zbfe8A&a zlVP(d9aqJh(hbRe(H zTR%*DqoIF2{{R>N04I6sCQZHz+A?fi`@j&$T+#yLLt@bveh^p%;W&2OWMP4w7=b=S z3~c4V+p=zhBK#SVh|-1YnGS3+8nO0X)ycZqRXxC@j0fzEyG$edg|h(gM)s%!w&Rw( zv_zjcev**(eWz2V2@@qtS7F=hhcxE*dQ6fluQYQsSY0@u7BlX15 zH*7J-%1eW*Q%n{YGww6f89HmP?g}f8_?iUdZQYYxbW&Ku%WcI9l>kRMxJD~-#1 zHMFo;#;&kX~zSIgXWse?WGOs%wQ5lO_;b&A2+PfVUOQO0yJo zmdw{=w4*Ud6Q0Jk86-e%+*52qHaG20=>GtITijFl(fMP_V!m=D102Eaosf-XH0&a~ z7t->TWiaE1z{~5%mWUaAaz3rPBJ7NOIe2n*?q0)5z2Hf1^lU~bqqpuB#jpgw2>~EH zzM(|NvMe2rFt)MVwrkIBxf_AaBM?E&d;Spot1vg3r+W)a2>k`MLxfrxG0|*#-2T|Z z<_L03WW!SnVk0JAHn3ZFEvn+)!(`hG_UXoUf#_3gT9`}dOar~GOd}cM3d4_@7zJiy z-GM!xuKK6CwW!6Yt5)?u8qgwYFnx6r+9vEp7-~Z_SS){u5S(Rw{>tXiBSbZ5noU|{ zi$hIhw$2_>vk#$&Zed(tKfP!KS<(Yw!|1qkd^FdTr+9h{p4_Of8=${Ly9b}YKR8uw zm%(cih-;0iN!9S>G;KE{Xn3>a!S`nG7k%uDtCVQcF-4Of3K6`1Do|uyZ39KjCF5G7 zywCxMMr04ZT*XMH_OgftA|Ac$=h`j%PS|I)pKU9hmw#^lmpXoZ$y?|P1`*GexiYF# zz#JQTw}9MnAw<8i=!=~qRshO+ROuP1H9b#F^S>{1?ITVto(~PSZFwY_k-9Z8Y+xg; z=90QEUe{Qod9sXFZic2vylta4j&M#Z=-r#4Hbqotk}hNn8WV|6i2#R2+sFtB7HVNGyT&1=t%h}E$j9hWcP}6wG9U*kQ=}WnIMhwaE zBkYU_gJ>C$FqSjJ>qIAwEwN3KCVL`7HLeD-JO1`Q5M@U`A9R4u+S8a8jp8x}*4WvN z^KQYU1sc>fv3Y?l>_Wu$fU>0sBqXK=Q)kQHpkbpDr74?X*2`fw-Pe4}>{IQy#b`L4 zW$`vUDA96Y#3?XI6FOZ6*LGs9g0@C<^)hQ@$!^lN4n14X%yr#cSjIy@T(1$}BCTf- zCf}zO6*v*7x}T40R3~Aj)%a=8etXxL1KK7lw4%B^(VWr#5t9*@czAI3d|-H~(I@T{ zIiAq4=I_=xLK4`0P;GzF-tSf6jsvYH%O)9pfQ$i?^@ZKL83&{q0pL0D^qt%;AGf`| z%rx_+5%UEc&e4;~N7@*~opM9&)W@WgeC1Idj*LsmNsRJFPM#pzF;Zh(8Es2$%73w~ zhbMlw?f(GOEi|2by?(@q`>r~NHBu^sm!XwoQCMWsVMT3@hqvqbClX)aYP6G#Z{(zk z%agf&x#arS;4}74Ca5*dN!}0=^N3^_k>oFGUb|_IQB|ZFARUIda&WbiM8oPc0HG9n z8oqp0_HMs|v%sGUc)7Cn4L8vL0IGkj-En2|-{H6C*A3^Ydq|bc_Dz!lB*~F-8M<7Z@$_mfFX(ag+U(Vm z5cvaU<(T=6Fr~_e6!6 zZMtD+>`x?N9iqqeem3tsbH{T1bzwAA)wxF1ojz*bP+3T$7^BPGfe>3^$@WXpHJ^YE zWnMe8!P=!WugTF45r(0SH*;ODbP=(N_fP3J>9<_ee4Sl%zc)K($~D4~=j~kdh#jJO ztI$Q+<`ICjFUPLz>XqTFPTMN5u&e4?uB7>5h377EUs#VGUgF>jY68ip(qm&k6-{!w ztS*@sSJ1T;S}S9ueGv!e<0@yBOY*!Z#HeAa0$~M{3tN&%y^|tm6zA@O_V`RL!1^ z#Dr5iAm_7QQ6d-;N7?gr3XS2I8uWXnd;$c3&5#>rGRn!;19rg2F_OVv5}UX*6QWFy zDs3(v)=^XS#2QmQ(S_gRo{!^L>j9Mst3RVimD0i+S4bn#p8YR^j7q^FeHu zqR6Ht;6WdUy@8BubOy(&-pyQv=i9(e!xB-^>AIe?O^sQcrBYVw%K2U`dQnnnxO4Z% zdRQ(E!^t*fXgNvY>4uPO>=f`?(rS=82@Erl^L%8<6|iNS3N$l5P3&NSaAK%{Fv zbKn|{HBx@fUYHV35bWS4ZpV9GvVL5aM_NOrxe*9Dtm%0gQx4hec%vA8(KAr8Q&;6e z!Gb6m^P|c46L58wk=9+I`gQtYb$)$*ZvHHDypcXNL=mbh7TAIaLXWgI(o64Pur?Zg z^=TyZ_{HQ;V|@n+n|BSAODGw66B40GhBt1Vt`Q?mqos*-*0Ld5ok1uvt%%;nuQ7nE6(GzO()zUAgqo}v zVd$R>-tt<<2gi03;lw41oT;p?mK%JniFGx6KtR&6!bE6d>CTHqpy?Gte`Bn$5qS{H z;r{?YB|LoDc6U{sI6rd?Da$pTW}dqQfMPI~vDG{wY5FuOmNySU8BQkZ(DO+_n=1e_ zJu4V-7{18PR83o<9dvuMo^EGeul*GLC+o{htye}tnY36ILAIo7zLw|u!4l!Z1X+Nj z8deljpKZ#D*01DPiUTfF+9_Jri09hog5;iWNM2{(pd-A>*1$$4dM3$Z*tIgqyO{dX zI)B}j{{VB_w0^yW#mV<}8-DeC zr@qCFGk(0Y!Y+;8kd2cHuB>HEjy15d@nPD23PUMTV&M-dQ^B5`qGnkyRM|lsSaMyT z6nv=pZyZB^y5@^$+#U=A8geHXZWCCbb|6iT9TTzTF$=6Bo^tTtRyKW zSlPwbFl_3e%MEbW&0!!QXn@XEQY=R~wCuM(wGnLAvdW+(plO^{H0x>wHLBL|+X$hR zWn&j_x_8Z8m|UfwbHt-+=yKrYBVm|=83w@u$Er5Gv+~dByTeo*r6>Z_2CrKLF2(sS zGMCV$O4YkuU3#@oBk?DLp{p$sGR*I=;>eaRUSj6$o=Fs@Oi@VM?#K!Csdwg_g0lTX^EGIdl0L2zf~SK_4>FXIyeZ<7G0%DRm1mMR{tgmWtxKH0+d z`J?6m`$zBEGhDg5g-Rk5baFY#fP8{GT#irq`ZEWD^7cghT9)M zL7K!{YOq5r0&$ZjAs_^r#|GXFjv=+V6LE_Z%(d+%RW@|?l^tH{i6DoY$HO?!u(&|) z`b^n&Lq*4ESvo9-YZBA2?B%)OrKmwx7 zJrQ=T#cF`bf&;eb z$_q@hijtSiOV?ssZTBx~_3i0oUcIM&B$kiPwxdOBDa!In=Z`^BOzDY`Nkv3K*4?=jres&lK$Sidt#`bjMkW<_8`-*V^hflk^dA1u zYa|FB?3e&^H;_sR7b0BmCWaX=>2bA~8c)`h*>I!=*h^5qO8Jlzk)RzNhmGRREO`t* zMa2@9H!1o}s*=cd&AYx1U~6#r^G8gKF|9z-=?&TMhRmFFswwxdsuFO5*jYbOjrp#Y zT_7aUsB*SLjqG0~$0`%n2y)CEk_UF;%+@W5bvQWLC9Mhfb*SLY=pFtFQEeZncJms^ zsJ%+sUm{zVNX0!A9KkcP4@`!oL8@12Ayz1z2<6o%h=3qBs>Z~-BJfdH8rIUm(%i^` z^lp%r3*i;2oYXyozVlI0V@R|CVUPzA@tIY%>?0Er=T6v1cBCW40}nu5@?@~Z!Fp8S z{uhrpMNwc)WHV`L+J53i)m4q@envNNITXv%y@D z+_ujkOtp&wvm6I`ZSyDKC|eTzb`Oqf7Gwc3i%#}+=XG{>QCqgZMs-!N>tONvWt#4Q zMAe)dkP>-{s4nXezQsm_Gust|D81Iw%OWwpv+9xw6P^%hK5OT$Hm}jot&EAxN*Z^Uuq)je( zN=HzBqkfk5UDd&<2)XhF6^)>TIHvT5+Zm8X6Sm0P771W10pk|JQu_d(V(093p%ffj?d)&2o1LU? z$e(Lk&JRoz*Y|>xU)f@y2iDYDzkQE2)qJixkEPh5z{cAvI1ALsibYf%L_)3wT_(jA zE}NxVb8M`$(Afo+&9ShC*wEyo-qV-R+SKx<4J z(f7C8*4_!}AOfxWRy++;*fG#xI1(F25X;X?4&U>SHV1;td*4&79lLw3Qdazr%=*b zK9qI-{^;KH=*Nr0rZ1aP>*tz8^?`B=r_Wn7wO!AZFFxHZ6pfNxJXN^`lH#pL6PW|X zK|d=Rl8Z*rT+dul98FNf7qU!K(dFx%Up}e`JL_}pduRZe*WD#r2%xyp>{8~^If;{w z26YQtWGWs^{{Ul25-|RT5ru>ZZ(7^&QbD!#AL~!)muuLZRrx*n56!r1lM-?0BQ}TM z5?;ADL6C7|q!%xPa*7O2WhKqS!zb^GqCef*@0V3(_~`8bmGrZu>^U zYwV`hkdkdo}`>Nfu6*a~vl3^XOZ__X*v8LEZNLQ`J z(~M5_h;lHw&H5_U#6_A%1q#fadVslDZBR-;O`Zv)z1X0nR;7`pKt zWbDCosm@_3iNvkfy^OhWCFsCWHrH$<1g> zSYXGRfGiXY$mDrxLR^jE#B17qNV!=ufbf;)(LG<=m*tOn-05!@wtGw9gRsc?g z{YU*#@AiH`{EhsD=NXYAm)I=CI1&XATOh=<9E_=E()X1Wx9o?_hp1^#CuM7rFK(z% zrIs5cu9baKBX5^D^03Ck2n@<+f-}PeXJF{BKp10r7gGDmM){fR6{FTTVoNYA?Krz; z)10*a#=Z$;!m4b?B(2GOa^$pXOmEMO_c3jzP6=iHqtD0O1IPQp8c-B)J+L>41dJDHIE<{4EDhraU!co-e z(;^yH*))>xk|s|dy=}2qeU_EJ8GXQw%v2!BzUTce{cimNaN`fkugNZ0Rcb0C11^9i z@CmCDQSzPk$OK~OW7Z?l^EE=>@gb#0>ocwdoWvZ3CJ{WtX@G1|InuoKp9NTp1}1G5 zQ{lcz(83SHTY#TkHlB22-h@Kh23QE^V2v*+-n7;|@HldDS`*_)G(cvY2D$3SKw;C1 zfgy=Fiv}OGB57kDSAOpzw6!lBvfM6f_hE<2P0q%1x7d6goLZgg339O1$&Pk_L7&2N z7hG#Q1XjV2<4RXR-B{HD_0x`LXd`rtzGNX@paNf)9Rki+&o4YsuvEz00MaKNwQE{| zs(cj7Da0~iq~O*Z0KyvzCQB^5E9qNyHUUeu7Syf#75Q(Tb%oYIX3e>aE385DV2au0 z5>u81w(r(5bSau7`-v=v2xH7kEak_qfG=S*X6RzrcR7{284EpL#CgAoP^RkJGtK$= zK6Vq~%@gq%0W{_6%xAgU0L#Q`?AA;+O5QFotgH(|uKMGlI}XV+*1acQl8B{y-xWP` z6&KlVMjUme|J2jBqq}7#>Oo z@mzJ6^633X{Ym``=>GuE{{Y2*&K^fz#kMw*!3CNCdI)LrU~AKsa(#-~uq2Iev0XDN zHktBbPKmNaQPZTxm#aw)>gK>6kOaOSjxr8pSzmfOX+5G?mga>dOa*{}V^^@AAP1NO zFP+x(L@lli;_EDmWY|V57RE-#p-?q1eDnmoc(DvXn!H$u?Q9;clN7=_d8yf=5VKY+ zOFgdbv{;DUc59)VCdiecgmM_drnZ?K4nU{DUgE$+8yBh;YW!ufRtSDr(gSKWM{U$0 z)GLjuy6ONGIJ$hE#oc>f(u9$0u3AIOwm`#C9cWp~9RA$|b(R#7it=(rfIPHhyCcfQ zply83vOToCBEM#AV7rfRr76T?2Zcz~3dTj7mbgvr1Z`ZvQwim)1%+RHmna6ttpcXW zS|=}eqS>Cnt11>f9wZ5KV3gBjGCkWJPUlr{pEj#ZX5PCQ(0s9NgyYH|qf9X-bDJDg z@^B|?*s~&gvfMq(%nL#bn!Wn#t@;nBtn(d3f9-USxXaaZ78ocBJkJ`Ew(E@cthRhT zc6gt3t(P5LyLzQ$G%CwI&9&Dtb454KJnI=G22CaxhO&&u4A>(MQ*f|ZB#58YogRry z^SaXfWMfkI3l6}tz&de=!_O}5)h}cS(q{&i!x?RgU`A~!ZiMb1(Qnmn)4T8UkMW1| zADOqnCK?+UoFL+JR(@_9suz$;&K$%(9Eke$K$1j$pLgS^b{pAYYnuDZ&)$vOc1>zM&HlcPzjTu11Tl6G1bp)HU*^q+J@?(ySm* zAv6mN{Fz}T%{NtRf9_5dGr0llArDlow-%>sZ2|(qUqph%2m@x)F;iPLTX&>d>PI=T zROFZxjIpKJwebz9OilL3LGDtis3XE={*Z zYR?hld8;^t+`%NWoll3{i} zKcK2kYoqDP8kWWc)={E8CmnXqRyZ6)a@HN-n@n2$3DeW4o^eB}T#*^K2oh&_fh|jo zQy2DNIiPlNWq3QopLhmQ5rucdAZMk&KtEGE=cYI-@qhA*n#jkSxnRJunz8{uIT5w% z!0-|pN=DL5G&qK%Z`cxdnzg`}9Dxi6=pmrk+@i0Y6Do8+4Mz@OX{fNy11W;R?BjYk z)VYJT`Mww5K{c#P+CN4KgwM zYVhZ2Wzm@V{Vv|9>Lo~MDoS;Hq6&4b1+{I}DBfV?nik{&BTZJ$)G=7~YS+>EtEBw9 zjk~EQ(I=H>KG(BQWi7dk6xOoVkaoe7Xl`3m4;4@^-x!<^WrmZG*u}b2t1Tp+UanON zBF8EN$hmzzYHon-K4A(_S#5#Jhn|$h^qoVto>@7tV)2g1{S&e?;@{2X$gOEpjblGJ zCDj^6LXu<+g=J7_?W3Dk85z^WOeHL_#Ug9?%nu#5=jzVW=nH4L8V&S~pl@$AG4%sC zJPUFS$~1jqlVIBuYE4B{tndkX$FcK;jGFizLc@7X@XHrA(OkJhQd9*uPU8;9MwJ?7 z)sl^i2Vid?DUS9@t}dJE{B`HDTNNEwN}Eobm)jZA z3QN`QrhIx^^oRA2vn*aO{F(fO=a?pEda%qOWQj8dhGG;q>I z7=@H=+a?c}t4v}O;qHsDkoj>zZI`Yj@}Qcc5lmx0Rx)g6h_>qyZJB$y3IQRhhZGYefM$2tGC&YyP;P@9Xm@#FJZsp#N?Zq>>i3Bo z8pXEUTdaGwRq32QsYL-Hb13cT#u%9uEOPa;O2fJ{jGAiRClM9{wG2gFzh^J}+z}Uz zAlqF1U5x<9GK!-i37s0i9uE@{kELMmjaSnndFh%;b&ADn&_lA#_#{E&T9;YPt%R&B zHPge?*@Ci5t?njy%R8Y}4d>~*TEEuXI9;z#wXVE$cXwO7vCJI%WY;bA#4?g3sLR)o zsl*oM6xg;;UKvZsDK%%&)0ab=Gp;jDccs&;zeBIs+|&7WM{by?$BBXF*G`Mb)Tegc zHo_+)X|qyi?LBKCR{Pp^5QDY}AsezQI`HW(y}UYC^gu|?hPg9jRm-vma>-J>%~Q}x zgYR^c72Vpf6tei*y4=;ymj3`h^tBzGu1X7cHr}<@BJ(v*&U7sYT;Qu)8Ln169Al*M?(BKXgfcx%7x`4n8oBUt)S3zdh|?4^3e2L)__rEgA0krAE<2^> zb!W1FNq<(mZL5ZVCI0{@x#$B7Qi0fiNI~5iLEgx{?}2gs`ICkXki>33f1c zohQ!2#b!1K&>3eakqI=C^`B3@+}2lK-pRk8e3W>=vO?7K6>wNOnE6=x%q}5cqG@ds zKwpsr$746CMhlqn1hwx-YwqvT>yRZe`OnWQ;9$>Xh5)oU(llVo>gG_bWnDmF&2N5( zEvXS~SfEC}sIZP1C1UA+>u#*-PQ-h$uR<=J27_8!J#j$teWu;URozkZOyFwqZ2%|f zdsQu70U_%wD_$;iiWn}Bqwu2W>P~{?if*!{TB`z&75>@VCQVeujX9Ximzye(*I?dV zR@2XX?L(HTCAw~3f(@EMH8uBvi|1yqk=|(v*ji3*8ySW-u=Z$U1!yVUU#(xPJ;_4Rf>3KC)rVjKQO7o-Tmp;J(=s1RaMqMOSCg4T2wx z5up>*pJB~Sn}ilIAsa&c)mP^!7cFf5OwkD{{V$+bodQjJ80-&xR^{z zM^3S;=gRG)Q;;PK$-xqO*hbfIFxx3d)wf#8Y-Kvxj;E5t$QLRQ;w6(1m(h1YIu)m-R01tRPVG*a)x)TTPhtixeSM(;MP4wCjqvoK3kQQM{;4tktEQ z7O#+CQQKpko1mbxG*=|AF2uA)WWB+(%fwM{0nqYkEfo6P)o29c$DciL_n$HN0Az@f z<+%h}WFICF(b7bAU+jg%E0lR;FpgQTPC3>RC3zH{eomd{*q+4I2!@ezn}&yE3Se0i zVX!5y;qMN;P1@bh)D6nLA7*p~R(e-PK;eMB0zQ_lD^1>|_joPH>;C|r z&mD8D7~KOib8-*nijsE{1Fb^sTqmu5caquxoI|lwFHQB;=~oY*-tEnt&W%^|vkGOP z!CCEupI6DYLQ5SYNe&uDxZ04yjYf#U7T z1Ot+E8bfN9zDii|1mVNL+}L{pHG}tF-B=6N9n)lKnil7JH0E3Lz^`0(G>BBI()M3n+da`E-xb5wzm5s%OKILRepNDu!jn2pOK=b zE7OazYMI1O?kMAawwzgsLGZ7f)A3a64bwx^r%(+Y4)b2=yLW$WCpUqG6t53 zg5pg_GWe=|SX>z5DwMe@TpQr28qO}OGQAqkG4Uy`h`wq3c8rsmtk2c2Q>#o~lis@O z(e|}ir6}yJr9ot?yFW@xgaBDUroU@#6bz-ZMU)OSp(BJmR@17RAPfLdH^nm z>OREo{)o-@w9bmu=2opdAtW@GZ3Wm9<}F=STj|y3d9*!(eq@~J& z!oD^+BH4$@(?bZDl39gCy56^Rb&~LA$#LxEW6XA}I^=@+P?5ZDW&xp+KIsC%bX8!$ z3{xdzVLa7MHT_#J-)Lra`hiK_py%w@sn}IFfUD7S;<$j6G&hHVTIvx68s4P9z`tBx zh}LaL!|+Xoje^akO>u*6^*l`&%#3aH@*i zF$wm+%Tkh_=788UxGO4#GzC*-wBR#&Ct9`%*e75~IH#GYCIJX$QW@{*x9la0%b%E> zfoE9;lVDefk!GioHoFvz!#W`854H3JDUFJ73~6E$^D|9(AWRASSuNg+{RHYem6y91 z=UBPFk3debPg8Js)l40)8DNuaYSp!b5QlbE9!%1|gBTYaKkd%^dk!)tr}5 zDJ?srI!~+ItOZ7kDEI9`b^|iuf>qzm8tfs; zt?A3mv&9fFlVsH`CDfeK+;6|qFWEloF~q-*{{WlZj=JlP3KGxwyCua)alHQ4;))1_ zz_Q$QjwHY6QnDk6&w1uJx*>(vzZhC~Yu-RUrWg$M=~*j;cAgr1SR%yLWY1qC5_Tpv zFEZ$gL5UxEY$T5nFL6=BHt&t$uf7b|rBucWj%n_jfXp1?o>&xPyXWuOT$bOo=b%bh z(cpqM3AvC(*Jig^E^YQ*&RX<&b8^_WuV33bA*MLN5oUv1b~113+TB?&sU}2OPBkgS zR4ooPdbaS?>ia!+N9SKOaTc&AQ)+uCBPgK~as#ZTt5}{ng6f+CZBi1;ir}mwb45mc zqm4^&CRL^FhpHssj`gCIn+Rxz9q?D)wW+r03n_KVa1};uoaFDoRdXK+B=kDfBs2|k zj3Uo;8VPAlQO;zTt8WLOA&qVowTC4paPnHKorN}4_CM=C>%@Ls{DrxH^ZpdIr_JZ2P**utmPe0|K^V zs&#T37KLjhBD?pEG@xs{nr<1i5VuxpTD9p11f^&Gkg03B{{V|=I-;H8WZ4y=F!QGi z9~{Fad2FJzY6WpHQ77wu(YrcR8UvW<)urosE7lz)Yu~reg&4M!vkvnlfNw*tuc#>w z8iN;7eHyx3Q)fnZpX(p#F|I}Yiu{J70Tq#JTD{d6;ARnmh-w>~Qs>O#229FPJrQi0_>6;U%`U?F z1)jpTEW?a1w*~0R-J)cg@nPD0WdYZsn7>zS3vy*!CJl(QSzDRb7UJA$=QGKQHA}@h zjt5`J$~1`ND!#9rTDDG}?G7Y_!o(HK*kRompw_izs`JjqcE~x6dU=I<<>%Km=Ag;O z?$tWx#TLkzDjONf;!&$Uno+xl=CV~uai!QOW6GY4wo{v7s>fhSFMx*iA=V(J>va5; zBw2}i3m0LmioHYtHL-e9>qcZljwU@}m{NCV{>A>LEq;0aNOO!`3y#iKJ3U#ECtDGn3=D?9x8w#W~vFfd3xS1-sa`)Fr-5oyb(|7opR%u!GcQy(}r|iy7 zuCIE3qv<)f1$|*u2iGZ=!jh}0t4y@X%VxHDvAic-uJCm?K~fxsB@tdu=@EvRGQGGx zhM+RUYLu@WB5&z@%gEuTTJdUo;C1#l8idAv zxC8nz-m~kE+0W2-em(q|{9>zRtTswYOZ1Uo{*v4ZaN$s3o-YxGo%WN8U0??#axU^#{)*kkXi zi=4(yj#ggyuOa=*p&gZVVZhL>;vHGc_E`Sfowx^)*mb zQ#tLMlbInP6HQ^XRv7ul)G1Qm;Os3rSyyV)3lc=abM!=s2yT{g8XPW(uWIABT|c<( zlTm^tG>Z~egzGW;o$Th0eYTO}gK}oB@o@F%5i8A5X zgp~L~u%k=dn_GQnDkjvj&g{L60Pj`(bZFkqL;S ztZD`D*N#S({GK$%rG4?p4rhm_NkXfyNo=Yfkdvr%Q;aGRqV3wizdlNzd3>J%b?w2TKC&sg4JhB zG+ey;G{j#$xOdVljo6SW<5FCUMx1J|s@~e<>#CZ3w-LE6BT{BG{?s>1E0p;@A%1$T zRr6b`n)H%nwCl=7#2O1p3+V;E4iFUA%4#983ds-b-wLS}PPCZO>vxta<8tGx#^lfQN?5%66E(}+4Yq?JtN?A;!4H9 z8f~~;DLAp1W!V`Jg~lcnxLJJ{*If_O*7sUiSHboLo1}qGW~kXxt6CP+18AjKb-st; z6UH*q0ar}e>1*tcwUunb<9XTq{u6;zIgL)Q;%ebm2pbUs$SF@x$=257*Q3wqf9!vM zh~@tP#=pl}q+F?32y@aX3{7!bHD{O5cWMkw)~TN^nj0Wi5Hg<=Uz>GqkONL^nQPwy z3C$S0S~8Wj?>!oF~viEFhKrOQoePZmDQYnRW0lA~mnm3lz~rg)1-< ztMF?Boq@WeM~vU7=H1Md-cu79bf#Y-dFs#T3g(J$kFLY6hS>I3JXqj`6xg|AQr3!0 z8`)lkv#uI}I?F3n{N^fPd`pasYuACbRPv&%WEIYbNv#VDJAG5i5$0RMVIpMxTQKbf zBwp8&X)FPi7?>hRxUkc4{)P1ARm>lr)Rv)S*di%uGX>0NX2YS921o(|$lIxbR=2e5 z3VKtLk;pE{Zwm4hwL0vVXm>|NbVbw9FMTq4dpA&1$n?PPc%R2mGThHqmrlNTzEzQ| z`pH_aw_2c5q%S%dW&YSALJ9`}g{t}Yo_R9A48LGFQ*C-H;v$=e#A4_czF`wRDiy-x z+?HUTY^f_@qg2|LBM>AYn8ByS63A16?ZHR0tr5d{xWgg8)?sWyO9i)CcF%nNuKtl) zJZ<=W`CCMhfvz{+7|laTkgNj4w?rKad?OoyN|ltTnl@rV@MuMaAVF(iq4D`yBxt- z`S6x}gcFjkWsM|}#kd-^9U(DYjA03yO=w_*2E)|h^^Man0=gc?ePg?_cX3{_>6Z00 z+cl~X)d#>w_O{2`dbqqRJ1(q5V06@m>uB?$ZG;>hPEVA&RsfH=Ka#d%&8T2@BBs3* zva2<2{^Zo1Nrw&Bww-S8W8X=UF5#S-KGVNB%|ac&3Z=UKN@GZ{PcO$Xwvj6H zssP$4+b+r;Be;cI7OEoepdlT}W>`}irXaFnRuc4U6hgtZW7jP#=T(WYV`BuS{YKN` zzE|!hwMUm?vNk2Arb}OJknSqGl+!hJ ziX%6*{E0-?N@=w7@|OLxYuYtk6rtz;IpHGI0#FpD$^9anw+-u|iS;@``^ z$L>ldCtj9=OHIha*fz|7yh{d9lN1b^UYvXpK?>*Q4V|YVW>t)DgCKtB@#aEh9UbZ* zFvVgB&{uHyljE4t=H1~i_CZOC=jEhY3yMZ^YXKg1sDxTpg~HA;acB2=A{cula$C6g zVIWHSOP18{i4R_%yz$Ws^YzQvRum_dFJBs24qFz>K|Ai$bK7oMj!e~+l&+$WYZ*ej zYNCvT43YQ6izcb)Awuv~S8s7Y;4Wd=*wwcrH-<_Q(iN3KJF3j#rxj&k!3IhRWcPq-;b~4eeA6u~J(S(Kk*S=aYmYqbSh}|F4 ztL2v8Q1>4m=VkRTbaWR(zTa~#>vGAe?F1TngQ!F=TQJuPda-IB=bJ^14jL26oquDF z9m(CQ9*6Ekn)=OOLFP_xEyTQXomVEoeW_ZZ&b1&?7E1F|cnp;HN<@CgqdJPvg+OxP z5jCkCT?>FWB?3^gC+eGdii&v9A1eaA3gGG}Ih9n_ci^GfV3X;7vHisT4p`-1%}>VO zYCeKbMpklcSOZB!J2ggD@%xA^5GNq`d7+|xqge?a?>H_myCK7ZW<0qOx9*1&v^@YA z;SusE$92=2S0?A8322&2r0s9m!c1z!Z6%gTN!6)s!Va7bg=k(dBth>z-@XiTV%XGi zZP7On(}6kw2163CRGmE3^|@>tp@2KA!-uV2f}3q@x|!`;nTXsmw+(WYIc(`C5aS=0 zT*g7%Y*Pv+L7_@e*(T&svAKO-!?Ec^S}xq3NwX;+^4LMV^+ZQZu49yL8%XjN~~m5td-N4C1x%l2H8MJ(QCe!A+4 z_0r%yjY0Xy6Qm+)0TwL^AeNWcOd^bcO zj~;Vc!JX!lOhnW`P-`#1EzmDvtP5p5lCO!g5q}T468G&a z?9odz*ei;2jHR`Pdbbc>08yK*q;6h$tgEY48ECI9&{eU{Lk_D}hbcLdtFx*b3h=)< zo}ZN0SlJcBb+x2BO&b--Z5H(tG>TMwXQ)_Jm4p)=Zem%YJ=~n{(`!49c%5~zRij>* zXz*hEyIKnqS56}}>(60xcw8zG-45h`+paWZkb*)7g=J(h7G z#*O}Kh%=%+vpOfUyH4BcUZ$r=+GMRtnG&Go8s5dQIgaa7=t>&N*-6;8-9LhTAvMf}FIiC-7EWNBldvuDrY^OME<$h+5is7vJJrG_)(8sH zK3R|v&u$rspt;C}=JC6{fXfO2-)4(qh%eaiNS+xd-*iCqaVeu2lPrbRW~CyeC#bhB zACNl{E<{3)8Z7t08gXx5))2{QE2fIQwA(CpP10V@wl}$2)^tO*txg7H9qd{ork}x$ z{mQ8#=B1edEAPu6TPNTive)aH$}}Q;=%kX)(=BiF-hu|n$cYGPI+?(3WDf1&idkJ&;tAIDITOLf>vdlKGDa^Lb8`t7<~eFPV`pG z89QOi1}$}Pqszfy%=<3hjS#|M>gz_WT!R*swe9k?SLRh_E}`eaRRXyiBSJP1ZFm*O z>4P8U=RtF*HdCs%qpfXCx~Q@nLJ81-)U0Z$QLHl)qJ8M}xhm|_*AOYowN=8yS(KXL zY@(pLue0-nUdXDbmX!(C`h8Bza@WamYb#n+cq#g=Ot5*8y%~a1MMiLdS!)FATZrlo zTh8rFVKkB=!zh)kUiNin{F0ms$X+I_y+1<)>wBwh4ieXP_cRF8-ce$5i!4U0D6|U|NPclVyv}-0?-x(JVz)5Tx!Zz^+DO>U*uSNfPpa zF4^RZ(!N>ur=s!)Y}*`-5i9e8)#IKoLg&)~!C2!HBO8S}dp*}tFW8GPn$+GP*?sTr zCl88O4TVcKJ{zIx>u&X%3>;nYvw&crrlAmJC}8a#Ks6H^Pdddn(4A5HS=v_jGk!FF zQ1ceOB_jAZ-eV$4n7VW{N2Qojz1JEEASZtVLXKxv^G6D-2)v|DN z;D&tT`TD@Q?`(gcy8f|4#(adov|fyj#%nitJz_EIFF zy9vRDv zf?d^TRL`i^D037Qgw?O7j?|i80v{HO&#}4qomDrg0CS(`0+N3|QiGL*LUMgj-U#|- zSNo8q`LxTO?pNy(!4wTR3+3A##Uxx)V=1zg<2`XdRcJ(YFPb4K^tFWR zSHnEqQnMtk+&T@pQJhb*n<{frE3=j_qCDCSk6NhrrxM`mXMn0r%+WwdU|yb8gfe&+ zIc96Ka+s@4hu5w;%&Qks^dETGJ*Cpbm>;?d59UWcj9}aV8)5cM@ykFAD&7HJSkJt0 z-!iv-QFNuHkYu^*>#YusYpnQ1Q;WI6qQKA+vT^lX(2GHbo^0B~s=A8f zqj~suFTB^=;Rh(R1&6HLW)|@wN4JU#BpH2!veomk2rANtwGkd)WCK4B#$FO-3LY*T@=-^+ z%!m+n@)9AC4#uL#6gD$=i)RzG-j$yjG>ss=b0&|{u+12pk4P>AHdred*pdc=GdNYP zO&!|^*)&6Wm_TY|OgS6_7lSNT1X-GVbx)ag6S0i-GxLbX>+08gBvQi0vJOaTg5~PV zFJ$%dgUR#_Qp%#xuQds_>9=d!P~CdA(nV^QuGwh{_(QY@em&qEImWH6mgE8QR91HS zG4(|!^bsbSWT%>2>bsUH)vE2jspvY+z_9QeN=5Dpk_GUwYgk5Y)0Fpcf-7Skt`6ww zj&&(-WnMgPam`-yDRaH0L^&2u*e%Nx|7;3*(6RZ%Ws5|lw`If;OFcUo)7Tg@vISC z_a?e3;#)%{Sz-lLvJ#$(GBtEPi`o4*)$YvwIQoV8{m=Gv1+#)SY&We2xM(?a{k?E=zWW&}Y92j<#th-!gH%@ZO%&1XnWr-bsLDt?L>5F% zi_cp})WfP23tL)fk8F)qeWIi;!&zcRWEY>&#~B?3-Y$}aocaF%R`JE=`40%?iUjz) z-C|~ppDockOasyY7xOswPPPZmle2Fwu(|IJ#C)zea*ThDWO4~ZD%SjDJ8GD_-%=XV z4X4de6M%D7jmJefthhQgK~jfNSDfcq^Vd4_-8UV%HP{Mpck@2B32CWrJ07b>V2(J4 z=px5Mt8;kHdaj!hkIW4bw-k}A2PUk+B*joaK$!554YqOt4Y4ed+E-3R&|1j-g8fYG z@-)xohvI)YK?G^23~yr^BDzeVvEE2PbCVJ)9_sy+?uo#L=MQN7WNgtUNRrSsXp<)> z9!s2DdDU2Bykys)p;6F9%iFWx7Z_R7Bp8R9J6tV<=Rs4Cy_zdf)-lpse60$axtD&@A)2eoB0l%4UL1+1zkOs>xs9+xF`L95cruovdt11$j znd};V*5~H=W(nCb^{jk8(dS;Ksw&%sD!s6ECU!^Ys+TC+$`7TwqMO~@R_`<17V4av z$d5PL#@iXG<2&19)0T%sa{f@&y3>YkD}$JA8g}nztFlhH^Fhod)y+kaEtPF7wUVPl zhdST)uqUe)t= zs`7DFk+>y|C1sEyi3!hgw+8TAFvGSK%ZH*>AN%>&rn>~%1r!RB=!(*}xvVa5KXTF6 zjUi@>WMWLT>Mn+$LELoTJoF~+f9OiVU77W&z9V-p@ocZoE$G~H%{1BK2>K4J6?IN; zbgo#~U}iKV)#J7e!O&d|MDwbBMH6*Yb(H;X^D>%@drMxuU`fZ-Cw83KVCJt7(8fb6 zD=ZDMPT8NKf3v@!6;iL~SKtK*P$(&Ex_mDoTXPD2Z0n=79Mip6t&X7(vhIRo=atzC zQnZm|;?64-WaL?T^c9FgBssFfNc6anIgs)eCjHb?YR!lTR*hP6)g%iTsYIiMjBiVl zmhBnC(J1oqmC2;?9}MpGrlr^ogO4m1IpAGuP9Fem86AczAIkRox-F@3+|%#eTdvDV zt;MhA7~a$Fa3Mjpw=RLm(uQ59W&xUBfSBVJ4F2bY+wr19W}v|%3AuFAGh_jO|e+wGswZcD1V3YY6YpX?D= zeS=e4@2BYQwyI$Vuo7Azf;n`r^DUC!BW-y20DwDGk0ycrPv_cu+Iy(M*LGTYXy1Ih zkkXJs1F->$?F<6LyX=HM8r}@>MY3ij&M-H~J|R1K z5*Y1tuTkC2m$|z`_Q&-QZzTR^{xov5=@28tnYPlb;e5!&L&OBUTWPNzq$PSxnci zmiv352i@|tX`?M*tSXq~{vSUAXD~sOij%wsfTGz#Qn4h&YbG1_DlTOxF zMYm4z1vopDxvn`bd}i~{LN=#cOLSe4dZUSjsX(2CyqjzvtUKe3%5?t#=#NL%G&_qO zghI>c+R|N>BEYL}75OP$Va)Z9QCADP?GkF@>B&7{g$Z)B>C7s2RwPT6YL2=|iK*Pr zuUP7hAZvHURjXf6s5<%b=d$@Gq65;L@xvkHR8_roM;k-~G^KdEZ#UH?<`HfCdkZj5 zcO3phK?D+k4#VESl@iJXDamp?c@fG6e$X5nmzk2UtjIYmvlMCS9>sruy$y77<9X^& z7*4I!%qB-@OU|_QrR1}_QQI|PUD=90(}nN8Vw#21HDf~>J&Sx+ye5@f@;5eC^bKk2 zJz1)Cnh{B?lL}s|CRCc5wQZKbW2SD~I9ET{bYf+ZM|yKqTMD>ySJt@Cv03}t?3^|@ zEF?q1B^Oa5@S$p3n#(fk{R#b%{Y3kM{{Sw3G`OrIX%cmJg=WLg0VRfzDxqTp&?;nM zbM`=&0gybgJE3frKSbQV)3loTe5S7y*f(hZ0H(~!ELH()hTss^AdJJyL?T3K4+N;e z%+d2-F**z*VHrc|mmpCq0<6+jA>x(uON~*e^E#DGptTKb09!`O{@En&z}z6TCBj86Ty0) zeXGm7^cAjzT`}0)VZsg-hv&6D2D@ojOT8zhYOZct;v8GU<){jJ3-8eM#Zxc-goACi ze@(8MKALvw3ewASHEo1QXFH;{>3VkQOC&uOaJjaU!-{nT&)NK=rsu2%+$j;?xU;?j za`$A_VJtf7M#i(b`O-pjLbtXf0-6g|*sRyCW|*11+%N&X9Qn8(x| z3DOE(P0$VnG@jW-O<=RA+cb`p*7~aa(|q+^wL;YtjSCU}scP~V zk|)`#fg|rIwP7_YDP|K9oJ|1~Nq2Y#MoqQT8SsoZPwF4+x9GK9#y^-pn7nPE>4o*? zeF5xh{1*0+=3@ar=c*&zqlIv4TvRT-w?=Z~@^vV2*Caw zdr6sJt6wKNlJ9oyE1}YX57?FQ(vEusD?77Lm*f%gzIO#pxTce*k-yVtiq^Q~WW9oJy2~O<(6rsP0dS*SCpR?G3}weJZ3tps?G_fnk>Y#f zvZy;9`aQkUZ%@|^{Oi;-$eV_<$x71=J0^N>LvnTMm21|+?rwHsdR%ouqZ#badD()Y zzxnU360y4gq^ffL0nD`JM?}_1d&>j~X@(aF^`fU3^?NyNRg(>zXKn24#LhS6!M-(9 z4kK0D{9T_s)oFHO6l!$VMHW5mTF`d2BpF{hD{(RB_hEJZjDG%g(jPtkC4N*=dFeM3 z6Cy@357irP+&x#+Z^g|e2w@U46f?^6vc`9#@%C?%H9;|;fU*AS`#eevZ(M;dPEw|Q z;8HQh%ak5|Qwi#XG=Q_S1L%%+cD+J(MkX2QhMR7H7IyMjjg9F&UuqKPt|8@kas@jN zi?GuDj7k{E>GRDp+-y<0iH^~}tl^HA4U1UYBOt%7MtGx5;o+m~Ds57-Ik@VKHtU2< zkzH5T1&EO2SGry3HMNUTb5g#_Xpc@;e1%=|=QWk8c~@?@Lu^u_V-1oSPRR5;t@X3v zF;J|Puvf=feu<2*dM49gs0;M9I^`{EQwTk#O3tP(`gxC`tDb#h-E;_*g=?+v3ig!m zPF=B@_8;XQt*>exzF(kV+R*jdJnwH+wzN6|!{{ZhZCL3w&Ti_P_LEFX4-RDt{8OFz z{;#kro@k^lE_R`sM4n5P(|v(9ndPcwLCJy<@I(n$EAJ3=Oarpfw(iv4b6OxzSba39m!qA0n^dXy>jiaTy9t>&tX4r zXX~zsoTtn@ytg{&qE(-+~(RivIu)e=Vp1_W-pQ{UfXU19nNej>QZcdrU#(!xEKWZ-0}Ll)c&~!}ctK5xx2> zh<@cKZ`z^~-J?c&Hfr>yp+0Rf1RRqZW{C3>B%x>(G+|r^VrM@!5CF&qiStcU8+9{U%>H5c5`arECUa-4oUs;i!K-%2-l zN%9(J)nP{z3t3MZ(KVR2q?25N2-6#!Ci*@zT_dvR+&uIu1 zvI#|2=xZ<<b#Fjn_F5|0Kwh~hWuFD|lwQ(*w9fn(aU1`u(eKL;6 zsGmPrZzy$S4mveApE>nia*kB2Wx8~eg~e2Ioi@997ORd~Ncv$TiPHG_g;pZt>iemq zjw&A-p0rlQ%amQBJ!7uUx9ArAg~?`G?d`Umx@PKXXRu%c*d^T$e8ne;{x3D)a-6VO zRRH4QwN{pu_t_(I znSM)+FgIgioseZ}>U)izv=jBf@>-UYv6+*c#yM)#X4#e^eJdGm%6E2`eL zofO7)m?dn~jYCn>CpJ96NSp%F6?FDLO*dNoy0+^*dD&vV>8h#bxO33HY@=NM+=AIdSo5zrunDsS{Sax=f)K3)=(eJ`W*45Z z>EzxzT*Pk4XtQpRwfafFHI`b=2}_0G&9wJ>JKs9qEc4sQE+343tg|5_3bBch9>GtV zxq;PZtfYo|PT@nZODHkaw(9*`e6g^%^O+uB=c-49tF``W6vLP@o)Y@Nq&E$>!;BTM zv{K$XcJ-5KA#u9JWV*{+5Lh?jqk-j7l4i%&c={twleN~i@B-Y{SVE9mB1Cq_1KmaX zk=&io&%yrymj3{pJbYcYxiI7rKqG__W#g)qkrr0*OWMzBGB!bBdM33&gizp!@+U#k zUN&(jZpU0aq7i|*PRJ4T?l-7!$K!y^ri`@C6C>}TO4dpy`JRp)>te*8wMoYUVWh*F zCCnLUK^oMW^}&xiF74YOk%h0_=k$y+XemaiPHz^)o6u$$ESBlL7q048S$jy*W%)o8 zP;=)}6?0l8CoFcQ4MKAd6xH>;D3siI zuv-EuJ&c;mR>cBQfKYb<#phf909n7J)-98MUG8<#nL8a+!zFoXDlZv>qE*q$mdibV zCCxR4x6an5?(y(cjpl^sYV!Nr&=3rc)NZSKk&|$)fVb)GiFRYkma$3>Y-B1q{w>;T zithDY?8b*&Of65%)lV$)))Tqjp+eU2Tn4QvRFjVd4%UO)CKB2ZOa?Ct0Aaa0me8m8 zJz$)&z=fQ#6In{J&fW=CNQ}qJMW)ucZEMQTimhebqh?_toj}`?a3%KuHjFCSlWUrA zc56zCyDNmjMyg=_FVCwsY20jeEih|(ubVmVlxiBnnEDV0HOyI5B3i7!qusPX38^ z-|5k=5d5_K*yf<3;W9~yphmL0NRnHR==LZ(kXRhRgJ{D>y}G_69En|eH)9_%x*%-B z(puu`gDMY`=0%C_1R?lR9BsOZeYOk|8g=0)fZ2W*Txo=uz(7Xqqwe&FxDFaxPUZ3v z2em01%mExeIGA*XK4TS&TLCZ zjumEJ(L3D1vCg}C5yk%ixUU;-&0bu*={Y(+wbkaHPnS1ZgF~G(@Eb($Cot9aA&O6; zD+=+lXUlF0T=X|TVEK@jYA*KVUarar*ppKXv5VXiMwcpkO`R$eI!gQf zeHvX6l{mR0pE=^Ovnv{P6#~75q6>xfOMo^@ZcBQ8a7x=4-MQM0e0fQyvn@#66r6m| zZnC%6rmPy{Ws9=aDQEQA+b2r};18h~imFT)B=FFd+%!f9WesIh+8|*N_O*VLsCm}U zNvS%vp{%-W5PpcJb}0j^Ic~e-s=Jg_He{`6Es5TuY-3>rvq}=QG+Y!7;@{WI5Cm3y z(3z;hh)WeWU51b7_Vwc+WE2?MyQ5rRWwQ-fASL#&nBKYR^9&t(`T_bG*zMf2`BV9e zNvr{eOW6@anUo6>F*eFw8c)crlK^_sttc4+`UaYdl!#Kd;gJj%CgeZjlOkn=#o^@7 zZV#_5gU>^WCr-IKwGsG6a)L?UB;cWCwumJ^L?2`9jXjAc38qW}4~|~rD}K0JwvqZ7 z(ArPP^qQO_I~_)Eb$@ug-P_!5#=P#kpLt^&mA=x_mkP^- z@?G~svADOb&5=4;jq1=@D_(V-R^FV!%`^pDOY+@l7Z|DyHnCuJA-fE9T);3q#vS&w zHp?HQZwni;uQ!zIZ6Q8k!s0iHx0Y*{fqzbo9&>70Xp7IavrCjk_nKJqa(0Fg&S~d$ zrML%8zC1w}8Sh3xe75;_p_4^ES0o{{4p(_5gsA0`^Js>K>Zd*L6}YI$9@pS%4t=Yf z>Yiy$flpVaSB*9$xel(Z6-KxqT?5YaQ(x%!QF2%3E^ro_%LNlTiJmT@3_GI`g#C-ZQIs1+0)2_VF$#kXzbA<(>OwbW43X&6Kh%soIB|Ac`fsZk09$ z#SMYSUTCYV+9B)IT}`$oz57?yh*@DSKDO(h*WSG;$NvD8Ka#xuyETe<^9n+pJuQZT zkOxZ!F!p8CL^qa+8Vq5_b`;D`UYPPuq4PBRCN|%-4mS=Ya?>%^J7(}qK%xC4ln)NX z-=kzHxIrbmTI*N3=0br7%+HAxFTx7Or3N(c{Q!U6BErY?Z5B z{i(+7=we%Xb6ZWlWGpijUE0DR!1R~K@yXY+Hr))zDa&i)wpWXTvRH*l|%O+JR|?y@3hev=XhQ4$z{NF3>{FE*n_co>yHOKJcZj)CVFIQ`XT~7aon! z#nG!~m#QxP5ndYCv3kJ{jbK+&T2_u2tQeUGhV|?&lq+FjF1gf)PgD*x)dgMD=oO^6 z!h_~yMyXp=OvOGQVPz{j^_Ql(r}FlK)nuH45K;dC7=vq-I%K!20D{3*8~xJYxf&)5 za_OIaxYm+GPiIp)Pg=`X(B%|trk-whzbh)9$^}5#n`p+pkjUBLA@rb1O&)=&_-?8P z%rr1J<&7j(4`UIHYfDsU;&U((!?a`BSBz!bDOd<`&V6c*J*!X$hcE*}I__6E0xA*+yqp-@hUd;u6uf zD3T-v-fkd?n&%u(hQYaH%u-@xl#R5o+~G?2aS%oFk-JyX=V#}p*w?KKUe#c_EPG0Q zy2{68zK6l;t|T>{V?99FM#+k0L3yCmHRk8B7gDln{hbRymclWkSQ1f9yHd82OyS~c zPFPN@ns_FpqJkLeFna$n~)WdcF*`Qs8yY56|6risGJctmk)>PFW$i%(7V@Ze07h`Q5b zrkyEyKW_uHJ3$((!|GeSLTc-^%aCO|I=l)l-PI27T1*kleBIA<^)t%oX>N;;2++um zl*$CNAgCf+{I9Ghmxd03>Ev!Lq*uhNZ2RjONpZ<-oF#^*0|@ch)y!`eN=~RmbMbAz z0Bzd@Pm$370H(jHeR%uAKa`)HT+X^eVi8kNqst|tJX5)$ZJ$0S_{XM0jyqw0mGO(y&|G?gpgAoi)^_%boIOSi*eb<8LsTrJt4Hx zYZ7ZAvFCPA_EESn-i27Hu&nU_oupDucvxm5%wA(wXN;)bPd>7HNd9cjEnD*EI3!kd zILjV`6=RccFs6gK;5eerLK^Qb(XG19+v$8PYlTNvR>cfpv`NM)dycQPmhy^1T!hDA zOAPwkQL>Pmc~(l+<$4z4O%0rzStL2ggkWt5$}y>YNgCqG*w<0GXkKN7Bkik>)WI8i zw`^2a=(N;327O}PyK>G|yGw}5@7_EoNlcFe~D3F>@ z=-Q6l^I2fyUss!M8rzpUQi;^ORq4xqVxjWSD{{qiQganj4AJoOl*(bfzP4faVHE}} zm+bZ+Jx6ojv`;E2QB$*9-C?cicc_VNn>gTveVfawkZkfO3}v%;EMX{-q)B_*zob7< zzfC&gR|9@&@P8(3fvtKOMAuTF4{j$`FU5xi*~A5PCLQKf3nT(5kG zj$|bd*CckP*uuAIh-=+~F`1XTOGwZUYzc%W3&Td}6SV2`WU+_i#@QLJI;n4GY6mMp z_;7TUgR5rUo(tGh$*nLoZo!9aOH$Q4d0Q?n1?_uWv0GuEC4{GMi^ElAc)$x#D2#4` z#@d2NoT}|MU>IZ#0>^Y>z->U}*_AZeG*OmnKBVOHinR@4%JtO(yr@-O8=-5XM=N>c zC67K>u?t_My0E`UTa8f5Se9)P#_SDuOtziXsoN-8u<(rzYphkJfm7{ma)(q}f#s$` z>3XKNx#(7l5+!BP^?=i<_8O0l5zxpr0jghF0ov)RuxwF~6JPWZSsc)MqqD0km71wn zv602uLPkoi-XI1$qm|gXrG0j!p_LV>YFl|Z3WUAd9|31wV%#-lo6(iUK|+~@pwp7N zo7R0awazUWn2#3V`0E?h-amIo^u?0iz_dQsUDqT+jXibg2i z=7vErHZJI6XZe8+OO#%XvsQu<0_0K#=y4@#Y8xYX5)$>wRGP%1HIHzp zk_$=|Agq}gvgIhv!1V2NSvO0-i%)gD{4vV~?8^vyfSy)sG?S^VM7L?2=Nr~kBE_u~ z2!ZGCuqS9ytsk({R$SF(jdE1qp4UQk3rSF<`P-S3r1RNF+T`qV%(XtNIC9~07AH(v zdH#jk7S*j(+@O546zGfJY$!3Elzk$-b5r?x%~=+{s!a>`4RjXGsIL_^WE%G5^o)8= z*)!!=XxO#t9J0x;TdNy;YO&;dAq8`!>V(OTD?ejlpo+EQrC~a^x4=w1+`(W@~G+ljxJf0i6yKn+}AjK0)6 z$U|inldeQ@n>LC+t#q#6r0A0%m??3}>iT}6r?o|i9DkDw3EMIl;!*ZsOLPr+orw@g z_nE+_gJ4!-AG3BpFVAgWX&NL}2ct)UJaKJ0nd3#+J6np_xv(8`RvOe*A5#eCLE{Kc zJ*mjHyO!rGX&JIQ46TS|z_q8QTy?UbxO@>uaEu4BB~53vP{;;I9Jdoj^3H z9g@ODXVuobEgDBSbbAV-;#Hb$Y9QBz)hi5k!IATHD@kqXE_H9+tV+gj&eig93nr{( zoQ~sOTvY1eO8S5S%xMtox<0+9gtMyeg*s$Ax~}_7BK@PMkTfS25N6CphWw}sBed}Y zEbk4s6tVNWx|3@S;7rYwWSY_3GpyJxI`Ml|uAR--rBGspF;Ua+8Bf>H#s|_cFnsH(vke*coHCV_Nwnw zAdiktQQL&GOM8keGK(LoL6}FvP0q!Zc(E! zXnKWy(|7uS-M)*(>d z)@MS}8_`KOPZ*bEZluU}3S+ z!{|2VS=S#xinLs8R4Y@-2kMsJ1>K41vkzGyw72ZJ$6&p9Az3N%P+neYM0Ui-ag8+P zt_fy3&787ywdYLobuzckI%_KXj%vGOp1{Eh$n2T0@`gl%zt|VqUXJM=hHtx~SaBe) zZa2*xm0v)$Ynf0iaisbCqG($#Wk~KGz*Vkqf{8ecRdQe+yh-?tHoV&{84IZE_c_v( z0^T(@tf|_cl$|J!m8ror6hhf1->bq}HFIeyF3P&;1IExKBaS0BlFH70lFB(#-an<)T>cxGesvna*jB`*4 z3?K-wAYf?8ggJ0rmp)@*O(U7kukYX3?%bvMBk_}i=-GjpLI#fpV3P;o>(wM%77B92 zIzb&Teh@MpFodyZ7<4XZ(q24yxDAUo0calj%O(sE(233)th71H$4&~4Mq>QILyYVA z=#3HyiI;|D)1{OiQd}>FP9_@-yY`hP6bWCV!y%qIjod^-bm-V0&(~yR%@Iry8i$eNsLyt8joYh&~MfU zzICF6&~&9Jugw_LelMae4sI=5va#1wV_G&npVx-$yT_q=AqsTE$&uPyD(hCYv6dH2 z%0a%-m7S671$66;NSf_W(=)5nL6=wy&qtSXR*H>5plq*iNHu=DtG!uBDPE1!{EQEm~KN zdm|k7!z-lvrUZwLIWofQ?&zZZWvYWx(MgGH0)n>7lbyi*PlHBMTHTTZC# zn#0b}$rn_7$qUgt=<)XvmkoWF2$H7(y-WeDLSInsji1gjC*oe zcDHKl*3m41Dn_?0Qy8n|S6PS-R~<10`4R9v9O!tw65-Qr#kBX~Bn8g06n3pit{vqo z*eq15j+Ojup;hyxS=F^zHz2g5m9xA!;e=~p7qqT4aNEd>QgnOz7II<9pQqwmbJ)_G#8<@z?X zej>k`o36iI6D>nG<(Odq05S2w2)+@~Uph<#GGvJhxft7;;jKZ+Ng8v=j1sZ+b`BYJ{eZj^mzo7RNh|d#^#+_EVFiET|1P;$x=pi*^{i8Dy-H z*}e6`DK3F*T7bejAT_B^&j>XkBTDcDR*(MxN1I--Fjrj*6ACAok{77fJ`>|ni?)Ye zb8Q9*s2I2Kgo#~uJ>}Cx?R&z@g<~Wm-RrD0z?jI z&{RTeeTL;i8o27#3F^ePH&#oQ4&@0g0g$bo6@25|$r+N4R!^%cIu@;32oXDCoUOZy zsa?{+A?_?l^={TUihEJ%?CG&O1dQ7(dNuQP&dS^9dl|mL4kZ@zL}XS%WSMN$OpfYH zPjM@Yen#XumB`b`Fj**$kc$~E+B9;VY2~V)jj1{(o0L|?o;hC@n*-8hlycbt^8|Yz zM7znOp|gJGE=Y`yWIUlo41fh;#1jHTYNUKkhM6FH2JI47;xOZeLW~!}OIJ#Moqmk= z(C#1nn*8_XcqjDaG}bCpP81gy46V4C4>4jh9_$PR`vS2ENq}^A;JPNV>0cy$V+=}b zf?7t6(V)&Fh?o$j@0uOB5F)-vouqkf19a{A{YPvGVg(mEy5Ivul6wXm3|uV9gXE2h z(lCb1cFh-_=!@@1Ca$(ztZz?ky%=8ZVZzO@p|>*R#kFxt!%<@h+QpC-%wHUyj+&OY zAVes%n;q+CP3KDK$SS6vHP?rjBGxvcn@XGAuaxKtey}6WobS)0x-!Y#$+g>YU2W5*&l3b6?rSK zPemy$@iZe=2`*2|x>(0Dp3evV?n>rEH9>9jMr$qtMz%GUSnK{Zs|YMgY%TIUG_5u+G2r1Kn6oauli%N_9rU5Y zUzC5Dw8Ys%_N&5Rz@sE3h+Cj+&UlEj&*+cjnBYmpnnGZIWaH%cG4)_hZ@9}E7c+3Z z*Mw=FkbqQML@8i}Z;Df9O&qoB;?Rbc2G{~@2G;jOxNeoZXgW=m zekFq^rv-D}N2VN>sr>OKz=KZP7h=|g%G$E0`Og{~myjXSw#$`mS1>QTSB!;UE$)}K zyFA6Uwf&_5fk>kLnk!{>W=%|*^$-ILy5_#>o@lyt*tDe^H>ju4#rit8sm;w=xOt}N zIaW^GRyh-T=~kQ`Ma%&8Ca=8twb<(9w>tDI<#_)9@q#gZLqKub;w~XE($-5^%BV+} zmZ6$fdqE(41({u-T)xtgX0Te6e0wXJ1{AC*Iw1&RcQjTkRh3*F?!YB5V8powv}ZMe zMb|S7YSqJ8=nIwyMG;tiDcu2@ZEk|ihS;O-BxYJw-wh`1%G$oXQw>KBZVioAUOc-J z!%VJj&VO39*NoaV6D7=$~PJlYWR*oLTv? z`J>OU_G@ZGiLi5=pgTtE#xZRXBED!}*(PqmgZ6#m@dDtylM+O2e^2uteZy2wC*>o- z+Vd-~BEgTsiq5?kcKlZTi8!8#rImM*$Y#hy&9BXgA7(5}K*naU&mZZKLLY;fF-M^0 zGXfHf?}J&owOV|Tq)AErEE18 z$b}mw(CE&qUZV=JSYT6kAFY+^&3(>vCpOpgwKoE#OuLG6#Z`GFM8e^ld39rTm)sNhROTvrRR+k@I;?pevGo1I- z!7er*yiQ%*K^d@s+n-OckeS>k=d~34-CT)$5iRzHE-V!dnm>)NKS;| z(AzBQUG85}F-USNdS|Viq9ZeqM8?mU5zkp@N6o)AR0P>;@aCDF_Z5oV-OjspFcJ3b&+Hz`ox+$wa;!F&m>`bv^ykkG4E(RoXRmVTIU@ zY6FhDovF?JTNb^oTWlc}GqsP)cZJu_dM;%UDz`GFQFnP*+EiO-O>srMUFMcuB;n}> zx^mL3yth*1)`wZhryPCfR|}?!9MN35*|^s8UaHlG`_81Sx}QfjkURykq<9POnbs|K z1tq|b3l6Z=+@N7RF*+llu?HD(G<_=$qsNCO^Xk@BouM?qsq6nk9``5&t)tM(Rqgk)LUI{o3Pf!DZ_uS zmu!mptd*XjVN$i%9&@z`%sld$6L-u$hYv4dV8Gb6h>2o^7-^07qq>$V4-upqq4RWV zs!oa#Y}93>wWmeXVP#s*8u}zBvZOHqk%+cz)QA>hDbT;7zpq_ry~&@C{{WcvR_HSF z1ttMVKTn7TJ65Q2aob{iy3uC8KAlOZZys*#UCb2RpVfh_g%Lg`MX2&lSQ;+1%^tIF}0eTfjIdG-6u zD;-s;oJ^4K)RZMneO0J-xr{3Jb@#p0eW;(={{WqrF}AuyDKSblk$k&bG3u zDY07;n?FKzDcLWvoQ-rX!qakjA#HNZ+gd#k$!%^-9-X3)uB=XDCFXk6B(;vId&W8Xhue+xz9ybb$aEm z)`81>5COhKB>B$C(U{lTfkd828Biwo>C-bQQ^2wIpLG7#c2_#`r{#y_#eJ3oXGPVA zvB9}x6HG1Awu9VOADCiF6fJtQ-h4_wI_IUtY*|L3+8fh0RW7poG zAmsk@ab;rIP57T6qC&>r9ej0|g=CowynS-t0^AwpYJF^7@GXxceueQzN%SlpkNo3C zrtJ3MqDP$}bafk8s;b9q1kx{$3R^qL>{OQmu8Ws-tb6d!&orG>w3!OJks&gH8eN>`ot}1(#V?;GTbQHC3unTft zJoin5u&Z~eoy`}XuS-BpnbG%@&Gy~0vej3`=<3G0xeCx77ffk%S;EVRVyv|}`LUOn zAG6b|Ly|DZHFu7Ptg8M-uX!$qrMZHrVS|i0;Mh#*3kkr2Y(ml`#5UOFadhNDd_~W8 zH63qOgAHg;F5s|26r$@I7^JHCxwRaeT&CbFT($u7H5ZEk$Mj#be_8vtpExV>lZEI# zdS_2o)=v@g*LP*R zJn|I?wlFuBj6g=RS*uK8hEETOjv8M`x!yp`bI|p}B*kJh?)9D0jbI;ap#bCy? zP)}kMe>v88`4+n8KZn;4)vs>UqIxNY&PVS{H&eGk7lPP}A&E7ityxjf1Y*Hl_9)*x z*=`_&Wv&YQPTJ~L4N&oEq6M{Z=K8~#Ie8X~=V}fq?F|dE3XTb|h?jL`x&B6eH@l&3 zgkGwV`Il5X)FvdUvhM(N#dvT&%-OSBD?jL1#sJkZe@2bIvj{1#65yXdL_4qbH@SJw z^S|;3j;jTE6GHEA36ALtB1<@6p`K<11ZWfSLD8kfgg-+5J7s7rJAALDjFxyu*k_h{ zei1KZy(0%0A~xN1xtkB6#YBrkpddjk;H5|6P@qy4 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