Mercurial > hg > orthanc-wsi
changeset 602:f72b20cbd84e
integration annotations->mainline
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Mon, 07 Sep 2026 20:58:51 +0200 |
| parents | 164565ec35c3 (current diff) a30dd5ee45c6 (diff) |
| children | 5b6af8d1f3a3 |
| files | |
| diffstat | 88 files changed, 18406 insertions(+), 986 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/ApplicationToolbox.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Applications/ApplicationToolbox.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -91,7 +91,7 @@ static void PrintProgress(BagOfTasksProcessor::Handle* handle, - bool* done) + const bool* done) { unsigned int previous = 0;
--- a/Applications/CMakeLists.txt Mon Sep 07 15:12:02 2026 +0200 +++ b/Applications/CMakeLists.txt Mon Sep 07 20:58:51 2026 +0200 @@ -129,7 +129,7 @@ ${ORTHANC_WSI_DIR}/Framework/ColorSpaces.cpp ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp ${ORTHANC_WSI_DIR}/Framework/DicomizerParameters.cpp - ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp + ${ORTHANC_WSI_DIR}/Framework/FrameworkEnumerations.cpp ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp ${ORTHANC_WSI_DIR}/Framework/ImagedVolumeParameters.cpp ${ORTHANC_WSI_DIR}/Framework/Inputs/CytomineImage.cpp
--- a/Framework/Algorithms/TranscodeTileCommand.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Algorithms/TranscodeTileCommand.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -86,7 +86,7 @@ } else { - printf("ICI\n"); + // TODO } } }
--- a/Framework/BackgroundColor.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/BackgroundColor.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -25,6 +25,8 @@ #include "ImageToolbox.h" +#include <iostream> + #include <OrthancException.h> #include <boost/lexical_cast.hpp> @@ -106,6 +108,21 @@ } + std::string BackgroundColor::ToHexadecimalString() const + { + if (present_) + { + char tmp[32]; + sprintf(tmp, "#%02x%02x%02x", red_, green_, blue_); + return tmp; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); // Should have called "IsPresent()" + } + } + + std::string BackgroundColor::ToHexadecimalString(uint8_t defaultRed, uint8_t defaultGreen, uint8_t defaultBlue) const
--- a/Framework/BackgroundColor.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/BackgroundColor.h Mon Sep 07 20:58:51 2026 +0200 @@ -74,6 +74,8 @@ std::string Format() const; + std::string ToHexadecimalString() const; // Will throw if absent + std::string ToHexadecimalString(uint8_t defaultRed, uint8_t defaultGreen, uint8_t defaultBlue) const;
--- a/Framework/Enumerations.cpp Mon Sep 07 15:12:02 2026 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,224 +0,0 @@ -/** - * 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-2026 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. - **/ - - -#include "PrecompiledHeadersWSI.h" -#include "Enumerations.h" - -#include "Jpeg2000Reader.h" - -#include <Logging.h> -#include <OrthancException.h> -#include <SystemToolbox.h> -#include <Toolbox.h> - -#include <string.h> -#include <boost/algorithm/string/predicate.hpp> - -#define HEADER(s) (const void*) (s), sizeof(s)-1 - -namespace OrthancWSI -{ - const char* EnumerationToString(ImageCompression compression) - { - switch (compression) - { - case ImageCompression_Unknown: - return "Unknown"; - - case ImageCompression_None: - return "Raw image"; - - case ImageCompression_Png: - return "PNG"; - - case ImageCompression_Jpeg: - return "JPEG"; - - case ImageCompression_Jpeg2000: - return "JPEG2000"; - - case ImageCompression_Tiff: - return "TIFF"; - - case ImageCompression_Dicom: - return "DICOM"; - - case ImageCompression_JpegLS: - return "JPEG-LS"; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - } - - - static bool MatchHeader(const void* actual, - size_t actualSize, - const void* expected, - size_t expectedSize) - { - if (actualSize < expectedSize) - { - return false; - } - else - { - return memcmp(actual, expected, expectedSize) == 0; - } - } - - - ImageCompression DetectFormatFromFile(const std::string& path) - { - std::string lower; - Orthanc::Toolbox::ToLowerCase(lower, path); - - std::string header; - Orthanc::SystemToolbox::ReadHeader(header, path, 256); - - ImageCompression tmp = DetectFormatFromMemory(header.c_str(), header.size()); - if (tmp != ImageCompression_Unknown) - { - if (tmp == ImageCompression_Jpeg && - boost::algorithm::ends_with(lower, ".mrxs")) - { - /** - * Special case of MIRAX / 3DHISTECH images, that contain a JPEG - * thumbnail, and that are thus confused with JPEG. - * https://bitbucket.org/sjodogne/orthanc/issues/163/ - **/ - - LOG(WARNING) << "The file extension \".mrxs\" indicates a MIRAX / 3DHISTECH image, " - << "skipping auto-detection of the file format"; - return ImageCompression_Unknown; - } - else if (tmp == ImageCompression_Tiff && - boost::algorithm::ends_with(lower, ".ndpi")) - { - LOG(WARNING) << "The file extension \".ndpi\" indicates a Hamamatsu image, " - << "use the flag \"--force-openslide 1\" if you do not have enough RAM to store the entire image"; - return ImageCompression_Tiff; - } - else if (tmp == ImageCompression_Tiff && - boost::algorithm::ends_with(lower, ".scn")) - { - LOG(WARNING) << "The file extension \".scn\" indicates a Leica image, " - << "use the flag \"--reencode 1\" or \"--force-openslide 1\" if you encounter problems"; - return ImageCompression_Tiff; - } - else - { - return tmp; - } - } - - // Cannot detect the format using the header, fallback to the use - // of the filename extension - - if (boost::algorithm::ends_with(lower, ".jpeg") || - boost::algorithm::ends_with(lower, ".jpg")) - { - return ImageCompression_Jpeg; - } - - if (boost::algorithm::ends_with(lower, ".png")) - { - return ImageCompression_Png; - } - - if (boost::algorithm::ends_with(lower, ".tiff") || - boost::algorithm::ends_with(lower, ".tif")) - { - return ImageCompression_Tiff; - } - - if (boost::algorithm::ends_with(lower, ".jp2") || - boost::algorithm::ends_with(lower, ".j2k")) - { - return ImageCompression_Jpeg2000; - } - - if (boost::algorithm::ends_with(lower, ".dcm")) - { - return ImageCompression_Dicom; - } - - return ImageCompression_Unknown; - } - - - ImageCompression DetectFormatFromMemory(const void* buffer, - size_t size) - { - if (MatchHeader(buffer, size, HEADER("\377\330\377"))) - { - return ImageCompression_Jpeg; - } - - if (MatchHeader(buffer, size, HEADER("\xff\x4f\xff\x51")) || - MatchHeader(buffer, size, HEADER("\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a"))) - { - return ImageCompression_Jpeg2000; - } - - if (MatchHeader(buffer, size, HEADER("\211PNG\r\n\032\n"))) - { - return ImageCompression_Png; - } - - if (MatchHeader(buffer, size, HEADER("\115\115\000\052")) || - MatchHeader(buffer, size, HEADER("\111\111\052\000")) || - MatchHeader(buffer, size, HEADER("\115\115\000\053\000\010\000\000")) || - MatchHeader(buffer, size, HEADER("\111\111\053\000\010\000\000\000"))) - { - return ImageCompression_Tiff; - } - - if (size >= 128 + 4 && - MatchHeader(reinterpret_cast<const uint8_t*>(buffer) + 128, size - 128, HEADER("DICM"))) - { - bool ok = true; - for (size_t i = 0; ok && i < 128; i++) - { - if (reinterpret_cast<const uint8_t*>(buffer)[i] != 0) - { - ok = false; - } - } - - if (ok) - { - return ImageCompression_Dicom; - } - } - - Jpeg2000Format jpeg2000 = Jpeg2000Reader::DetectFormatFromMemory(buffer, size); - if (jpeg2000 == Jpeg2000Format_JP2 || - jpeg2000 == Jpeg2000Format_J2K) - { - return ImageCompression_Jpeg2000; - } - - return ImageCompression_Unknown; - } -}
--- a/Framework/Enumerations.h Mon Sep 07 15:12:02 2026 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include <Enumerations.h> - -#include <stdint.h> -#include <string> - -namespace OrthancWSI -{ - static const char* const VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD = "1.2.840.10008.5.1.4.1.1.77.1.6"; - - // WARNING - Don't change the enum values below, as this would break - // serialization of "DicomPyramidInstance" - enum ImageCompression - { - ImageCompression_Unknown = 1, - ImageCompression_None = 2, - ImageCompression_Dicom = 3, - ImageCompression_Png = 4, - ImageCompression_Jpeg = 5, - ImageCompression_Jpeg2000 = 6, - ImageCompression_Tiff = 7, - ImageCompression_UseOrthancPreview = 8, - ImageCompression_JpegLS = 9 - }; - - enum OpticalPath - { - OpticalPath_None, - OpticalPath_Brightfield - }; - - const char* EnumerationToString(ImageCompression compression); - - ImageCompression DetectFormatFromFile(const std::string& path); - - ImageCompression DetectFormatFromMemory(const void* buffer, - size_t size); - - inline unsigned int CeilingDivision(unsigned int a, - unsigned int b) - { - if (a % b == 0) - { - return a / b; - } - else - { - return a / b + 1; - } - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/FrameworkEnumerations.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,224 @@ +/** + * 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-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeadersWSI.h" +#include "FrameworkEnumerations.h" + +#include "Jpeg2000Reader.h" + +#include <Logging.h> +#include <OrthancException.h> +#include <SystemToolbox.h> +#include <Toolbox.h> + +#include <string.h> +#include <boost/algorithm/string/predicate.hpp> + +#define HEADER(s) reinterpret_cast<const void*>(s), sizeof(s) - 1 + +namespace OrthancWSI +{ + const char* EnumerationToString(ImageCompression compression) + { + switch (compression) + { + case ImageCompression_Unknown: + return "Unknown"; + + case ImageCompression_None: + return "Raw image"; + + case ImageCompression_Png: + return "PNG"; + + case ImageCompression_Jpeg: + return "JPEG"; + + case ImageCompression_Jpeg2000: + return "JPEG2000"; + + case ImageCompression_Tiff: + return "TIFF"; + + case ImageCompression_Dicom: + return "DICOM"; + + case ImageCompression_JpegLS: + return "JPEG-LS"; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + static bool MatchHeader(const void* actual, + size_t actualSize, + const void* expected, + size_t expectedSize) + { + if (actualSize < expectedSize) + { + return false; + } + else + { + return memcmp(actual, expected, expectedSize) == 0; + } + } + + + ImageCompression DetectFormatFromFile(const std::string& path) + { + std::string lower; + Orthanc::Toolbox::ToLowerCase(lower, path); + + std::string header; + Orthanc::SystemToolbox::ReadHeader(header, path, 256); + + ImageCompression tmp = DetectFormatFromMemory(header.c_str(), header.size()); + if (tmp != ImageCompression_Unknown) + { + if (tmp == ImageCompression_Jpeg && + boost::algorithm::ends_with(lower, ".mrxs")) + { + /** + * Special case of MIRAX / 3DHISTECH images, that contain a JPEG + * thumbnail, and that are thus confused with JPEG. + * https://bitbucket.org/sjodogne/orthanc/issues/163/ + **/ + + LOG(WARNING) << "The file extension \".mrxs\" indicates a MIRAX / 3DHISTECH image, " + << "skipping auto-detection of the file format"; + return ImageCompression_Unknown; + } + else if (tmp == ImageCompression_Tiff && + boost::algorithm::ends_with(lower, ".ndpi")) + { + LOG(WARNING) << "The file extension \".ndpi\" indicates a Hamamatsu image, " + << "use the flag \"--force-openslide 1\" if you do not have enough RAM to store the entire image"; + return ImageCompression_Tiff; + } + else if (tmp == ImageCompression_Tiff && + boost::algorithm::ends_with(lower, ".scn")) + { + LOG(WARNING) << "The file extension \".scn\" indicates a Leica image, " + << "use the flag \"--reencode 1\" or \"--force-openslide 1\" if you encounter problems"; + return ImageCompression_Tiff; + } + else + { + return tmp; + } + } + + // Cannot detect the format using the header, fallback to the use + // of the filename extension + + if (boost::algorithm::ends_with(lower, ".jpeg") || + boost::algorithm::ends_with(lower, ".jpg")) + { + return ImageCompression_Jpeg; + } + + if (boost::algorithm::ends_with(lower, ".png")) + { + return ImageCompression_Png; + } + + if (boost::algorithm::ends_with(lower, ".tiff") || + boost::algorithm::ends_with(lower, ".tif")) + { + return ImageCompression_Tiff; + } + + if (boost::algorithm::ends_with(lower, ".jp2") || + boost::algorithm::ends_with(lower, ".j2k")) + { + return ImageCompression_Jpeg2000; + } + + if (boost::algorithm::ends_with(lower, ".dcm")) + { + return ImageCompression_Dicom; + } + + return ImageCompression_Unknown; + } + + + ImageCompression DetectFormatFromMemory(const void* buffer, + size_t size) + { + if (MatchHeader(buffer, size, HEADER("\377\330\377"))) + { + return ImageCompression_Jpeg; + } + + if (MatchHeader(buffer, size, HEADER("\xff\x4f\xff\x51")) || + MatchHeader(buffer, size, HEADER("\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a"))) + { + return ImageCompression_Jpeg2000; + } + + if (MatchHeader(buffer, size, HEADER("\211PNG\r\n\032\n"))) + { + return ImageCompression_Png; + } + + if (MatchHeader(buffer, size, HEADER("\115\115\000\052")) || + MatchHeader(buffer, size, HEADER("\111\111\052\000")) || + MatchHeader(buffer, size, HEADER("\115\115\000\053\000\010\000\000")) || + MatchHeader(buffer, size, HEADER("\111\111\053\000\010\000\000\000"))) + { + return ImageCompression_Tiff; + } + + if (size >= 128 + 4 && + MatchHeader(reinterpret_cast<const uint8_t*>(buffer) + 128, size - 128, HEADER("DICM"))) + { + bool ok = true; + for (size_t i = 0; ok && i < 128; i++) + { + if (reinterpret_cast<const uint8_t*>(buffer)[i] != 0) + { + ok = false; + } + } + + if (ok) + { + return ImageCompression_Dicom; + } + } + + Jpeg2000Format jpeg2000 = Jpeg2000Reader::DetectFormatFromMemory(buffer, size); + if (jpeg2000 == Jpeg2000Format_JP2 || + jpeg2000 == Jpeg2000Format_J2K) + { + return ImageCompression_Jpeg2000; + } + + return ImageCompression_Unknown; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/FrameworkEnumerations.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,95 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <Enumerations.h> + +#include <stdint.h> +#include <string> + +namespace OrthancWSI +{ + static const char* const VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD = "1.2.840.10008.5.1.4.1.1.77.1.6"; + + // WARNING - Don't change the enum values below, as this would break + // serialization of "DicomPyramidInstance" + enum ImageCompression + { + ImageCompression_Unknown = 1, + ImageCompression_None = 2, + ImageCompression_Dicom = 3, + ImageCompression_Png = 4, + ImageCompression_Jpeg = 5, + ImageCompression_Jpeg2000 = 6, + ImageCompression_Tiff = 7, + ImageCompression_UseOrthancPreview = 8, + ImageCompression_JpegLS = 9 + }; + + enum OpticalPath + { + OpticalPath_None, + OpticalPath_Brightfield + }; + + + // Used by the viewer + enum AuthenticationSource + { + AuthenticationSource_None, + AuthenticationSource_RegisteredUsers, + AuthenticationSource_Plugin, + AuthenticationSource_HttpHeader + }; + + + // Used by the viewer + enum ProjectRole + { + ProjectRole_Instructor, + ProjectRole_Learner, + ProjectRole_Guest + }; + + + const char* EnumerationToString(ImageCompression compression); + + ImageCompression DetectFormatFromFile(const std::string& path); + + ImageCompression DetectFormatFromMemory(const void* buffer, + size_t size); + + inline unsigned int CeilingDivision(unsigned int a, + unsigned int b) + { + if (a % b == 0) + { + return a / b; + } + else + { + return a / b + 1; + } + } +}
--- a/Framework/Inputs/DecodedPyramidCache.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/DecodedPyramidCache.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -161,7 +161,15 @@ while (!cache_.IsEmpty()) { CachedPyramid* pyramid = NULL; - cache_.RemoveOldest(pyramid); + + try + { + cache_.RemoveOldest(pyramid); + } + catch (Orthanc::OrthancException&) + { + // Should never happen, don't throw exceptions in destructor + } if (pyramid != NULL) {
--- a/Framework/Inputs/DicomPyramid.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/DicomPyramid.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -39,8 +39,8 @@ { struct DicomPyramid::Comparator { - bool operator() (DicomPyramidInstance* const& a, - DicomPyramidInstance* const& b) const + bool operator() (const DicomPyramidInstance* const& a, + const DicomPyramidInstance* const& b) const { return a->GetTotalWidth() > b->GetTotalWidth(); }
--- a/Framework/Inputs/DicomPyramid.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/DicomPyramid.h Mon Sep 07 20:58:51 2026 +0200 @@ -54,7 +54,7 @@ const std::string& seriesId, bool useCache); - virtual ~DicomPyramid() + virtual ~DicomPyramid() ORTHANC_OVERRIDE { Clear(); }
--- a/Framework/Inputs/DicomPyramidInstance.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/DicomPyramidInstance.h Mon Sep 07 20:58:51 2026 +0200 @@ -25,7 +25,7 @@ #include "../../Resources/Orthanc/Stone/IOrthancConnection.h" #include "../BackgroundColor.h" -#include "../Enumerations.h" +#include "../FrameworkEnumerations.h" #include <boost/noncopyable.hpp> #include <vector>
--- a/Framework/Inputs/ITiledPyramid.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/ITiledPyramid.h Mon Sep 07 20:58:51 2026 +0200 @@ -24,7 +24,7 @@ #pragma once #include "../BackgroundColor.h" -#include "../Enumerations.h" +#include "../FrameworkEnumerations.h" #include <Compatibility.h> #include <Images/ImageAccessor.h>
--- a/Framework/Inputs/OnTheFlyPyramid.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/OnTheFlyPyramid.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -84,7 +84,7 @@ Orthanc::ImageProcessing::Convert(*baseLevel_, *protection); } - Orthanc::ImageAccessor* current = baseLevel_.get(); + const Orthanc::ImageAccessor* current = baseLevel_.get(); while (current->GetWidth() > tileWidth_ || current->GetHeight() > tileHeight_) {
--- a/Framework/Inputs/OnTheFlyPyramid.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/OnTheFlyPyramid.h Mon Sep 07 20:58:51 2026 +0200 @@ -53,7 +53,7 @@ unsigned int tileHeight, bool smooth); - virtual ~OnTheFlyPyramid(); + virtual ~OnTheFlyPyramid() ORTHANC_OVERRIDE; const Orthanc::ImageAccessor& GetLevel(unsigned int level) const;
--- a/Framework/Inputs/OpenSlideLibrary.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/OpenSlideLibrary.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -39,14 +39,14 @@ OpenSlideLibrary::OpenSlideLibrary(const std::string& path) : library_(path) { - close_ = (FunctionClose) library_.GetFunction("openslide_close"); - getLevelCount_ = (FunctionGetLevelCount) library_.GetFunction("openslide_get_level_count"); - getLevelDimensions_ = (FunctionGetLevelDimensions) library_.GetFunction("openslide_get_level_dimensions"); - getLevelDownsample_ = (FunctionGetLevelDownsample) library_.GetFunction("openslide_get_level_downsample"); - open_ = (FunctionOpen) library_.GetFunction("openslide_open"); - readRegion_ = (FunctionReadRegion) library_.GetFunction("openslide_read_region"); - getPropertyNames_ = (FunctionGetPropertyNames) library_.GetFunction("openslide_get_property_names"); - getPropertyValue_ = (FunctionGetPropertyValue) library_.GetFunction("openslide_get_property_value"); + close_ = reinterpret_cast<FunctionClose>(library_.GetFunction("openslide_close")); + getLevelCount_ = reinterpret_cast<FunctionGetLevelCount>(library_.GetFunction("openslide_get_level_count")); + getLevelDimensions_ = reinterpret_cast<FunctionGetLevelDimensions>(library_.GetFunction("openslide_get_level_dimensions")); + getLevelDownsample_ = reinterpret_cast<FunctionGetLevelDownsample>(library_.GetFunction("openslide_get_level_downsample")); + open_ = reinterpret_cast<FunctionOpen>(library_.GetFunction("openslide_open")); + readRegion_ = reinterpret_cast<FunctionReadRegion>(library_.GetFunction("openslide_read_region")); + getPropertyNames_ = reinterpret_cast<FunctionGetPropertyNames>(library_.GetFunction("openslide_get_property_names")); + getPropertyValue_ = reinterpret_cast<FunctionGetPropertyValue>(library_.GetFunction("openslide_get_property_value")); }
--- a/Framework/Inputs/TiledPyramidStatistics.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Inputs/TiledPyramidStatistics.h Mon Sep 07 20:58:51 2026 +0200 @@ -40,7 +40,7 @@ public: explicit TiledPyramidStatistics(ITiledPyramid& source); // Takes ownership - virtual ~TiledPyramidStatistics(); + virtual ~TiledPyramidStatistics() ORTHANC_OVERRIDE; virtual unsigned int GetLevelCount() const ORTHANC_OVERRIDE {
--- a/Framework/Jpeg2000Reader.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Jpeg2000Reader.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -243,7 +243,7 @@ #endif public: - OpenJpegInput(OpenJpegDecoder& decoder, + OpenJpegInput(const OpenJpegDecoder& decoder, const void* buffer, size_t size) : buffer_(reinterpret_cast<const uint8_t*>(buffer)),
--- a/Framework/Jpeg2000Writer.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Jpeg2000Writer.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -248,7 +248,7 @@ #endif public: - explicit OpenJpegOutput(OpenJpegEncoder& encoder) : + explicit OpenJpegOutput(const OpenJpegEncoder& encoder) : cio_(NULL) { #if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
--- a/Framework/Outputs/DicomPyramidWriter.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Outputs/DicomPyramidWriter.h Mon Sep 07 20:58:51 2026 +0200 @@ -79,7 +79,7 @@ const ImagedVolumeParameters& volume, Orthanc::PhotometricInterpretation photometric); - virtual ~DicomPyramidWriter(); + virtual ~DicomPyramidWriter() ORTHANC_OVERRIDE; virtual void Flush() ORTHANC_OVERRIDE; };
--- a/Framework/Outputs/HierarchicalTiffWriter.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Outputs/HierarchicalTiffWriter.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -81,8 +81,8 @@ struct HierarchicalTiffWriter::Comparator { - inline bool operator() (PendingTile* const& a, - PendingTile* const& b) + inline bool operator() (const PendingTile* const& a, + const PendingTile* const& b) { if (a->GetLevel() < b->GetLevel()) {
--- a/Framework/Outputs/HierarchicalTiffWriter.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Outputs/HierarchicalTiffWriter.h Mon Sep 07 20:58:51 2026 +0200 @@ -86,7 +86,7 @@ unsigned int tileHeight, Orthanc::PhotometricInterpretation photometric); - virtual ~HierarchicalTiffWriter(); + virtual ~HierarchicalTiffWriter() ORTHANC_OVERRIDE; virtual void Flush() ORTHANC_OVERRIDE; };
--- a/Framework/Outputs/IPyramidWriter.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Outputs/IPyramidWriter.h Mon Sep 07 20:58:51 2026 +0200 @@ -23,7 +23,8 @@ #pragma once -#include "../Enumerations.h" +#include "../FrameworkEnumerations.h" + #include <Images/ImageAccessor.h> #include <boost/noncopyable.hpp>
--- a/Framework/Outputs/InMemoryTiledImage.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Outputs/InMemoryTiledImage.h Mon Sep 07 20:58:51 2026 +0200 @@ -58,7 +58,7 @@ Orthanc::PhotometricInterpretation photometric, BackgroundColor backgroundColor); - virtual ~InMemoryTiledImage(); + virtual ~InMemoryTiledImage() ORTHANC_OVERRIDE; virtual unsigned int GetLevelCount() const ORTHANC_OVERRIDE {
--- a/Framework/Outputs/MultiframeDicomWriter.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/Outputs/MultiframeDicomWriter.h Mon Sep 07 20:58:51 2026 +0200 @@ -23,7 +23,7 @@ #pragma once -#include "../Enumerations.h" +#include "../FrameworkEnumerations.h" #include <Compatibility.h> // For std::unique_ptr #include <ChunkedBuffer.h>
--- a/Framework/TiffReader.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Framework/TiffReader.h Mon Sep 07 20:58:51 2026 +0200 @@ -23,7 +23,7 @@ #pragma once -#include "Enumerations.h" +#include "FrameworkEnumerations.h" #include <tiff.h> #include <tiffio.h>
--- a/NEWS Mon Sep 07 15:12:02 2026 +0200 +++ b/NEWS Mon Sep 07 20:58:51 2026 +0200 @@ -1,6 +1,29 @@ Pending changes in the mainline =============================== +=> Recommended SDK version: 1.12.9 <= +=> Minimum SDK version: 1.7.0 <= + +* Support for annotations +* Persistence of annotations in the Orthanc database (SDK 1.12.8 is required) + +Configuration +------------- + +* New configuration option "EnableAnnotations" to enable annotations +* New configuration option "AuthenticationSource" to enable per-user annotations: + - "None" uses the administrative Orthanc user (default) + - "RegisteredUsers" relies on the HTTP basic authentication that is built in Orthanc + - "HttpHeader" takes the user out of the HTTP header specified in "AuthenticationHttpHeader" option + - "Plugin" can be used if the "orthanc-education" plugin is installed +* New configuration option "Instructors" to distinguish between learners + and instructors, only if the authentication source is "HttpHeader" +* New configuration option "EnableAnnotationsSharing" to enable sharing of annotations +* New configuration option "EnableLearnerToLearnerSharing" to enable sharing between learners + +Maintenance +----------- + * OrthancWSIDicomizer: - Fill the DICOM tag "Objective Lens Power Attribute" (0048,0112) with the scanner magnification, either when using OpenSlide or
--- a/Resources/CMake/JavaScriptLibraries.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/CMake/JavaScriptLibraries.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -31,6 +31,23 @@ "${BASE_URL}/bootstrap-5.3.3.zip" "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3") +DownloadPackage( + "6e819ef7fcd49bb13cf809fd8c5fb20b" + "${BASE_URL}/bootstrap-icons-1.13.1.zip" + "${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1") + +DownloadPackage( + "8242afdc5bd44105d9dc9e6535315484" + "${BASE_URL}/dicom-web/vuejs-2.6.10.tar.gz" + "${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10") + +# axios v0.31.0 is the last release before Axios removed the committed dist/ artifacts +# https://github.com/axios/axios/releases?page=2#release-v0.31.0 +DownloadPackage( + "e1fe2cd9270b513874aea946977f2d47" + "${BASE_URL}/axios-0.31.0.tar.gz" + "${CMAKE_CURRENT_BINARY_DIR}/axios-0.31.0") + # curl -L https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js | gzip > /tmp/popper-2.11.8.min.js.gz DownloadCompressedFile( @@ -38,22 +55,65 @@ "${BASE_URL}/WSI/popper-2.11.8.min.js.gz" "${CMAKE_CURRENT_BINARY_DIR}/popper.min.js") +DownloadFile( + "aa2c54fdbbbeb1bb056793ec480928eb" + "${BASE_URL}/WSI/modern-screenshot-4.7.0.js") + set(JAVASCRIPT_LIBS_DIR ${CMAKE_CURRENT_BINARY_DIR}/javascript-libs) file(MAKE_DIRECTORY ${JAVASCRIPT_LIBS_DIR}) file(COPY + ${CMAKE_CURRENT_BINARY_DIR}/axios-0.31.0/dist/axios.min.js + ${CMAKE_CURRENT_BINARY_DIR}/axios-0.31.0/dist/axios.min.js.map ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3/dist/js/bootstrap.min.js ${CMAKE_CURRENT_BINARY_DIR}/openlayers-10.6.1-package/dist/ol.js ${CMAKE_CURRENT_BINARY_DIR}/popper.min.js + ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js + ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/modern-screenshot-4.7.0.js DESTINATION ${JAVASCRIPT_LIBS_DIR}/js ) +file(RENAME + ${JAVASCRIPT_LIBS_DIR}/js/modern-screenshot-4.7.0.js + ${JAVASCRIPT_LIBS_DIR}/js/modern-screenshot.js + ) + file(COPY ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3/dist/css/bootstrap.min.css ${CMAKE_CURRENT_BINARY_DIR}/openlayers-10.6.1-package/ol.css DESTINATION ${JAVASCRIPT_LIBS_DIR}/css ) + +file(COPY + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrow-clockwise.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrow-up-right.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrows-fullscreen.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrows-move.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrows.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/brightness-high.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/camera.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/chevron-compact-left.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/chevron-compact-right.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/circle.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/cloud-download.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/eye-slash.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/eye.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/file-earmark-plus.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/geo-alt.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/hand-index.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/pen.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/pentagon.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/share.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/square.svg + ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/trash.svg + + ${CMAKE_CURRENT_LIST_DIR}/../Icons/freehand-area-svgrepo-com.svg + ${CMAKE_CURRENT_LIST_DIR}/../Icons/freehand-svgrepo-com.svg + + DESTINATION + ${JAVASCRIPT_LIBS_DIR}/svg + )
--- a/Resources/CMake/Version.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/CMake/Version.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -21,16 +21,19 @@ set(ORTHANC_WSI_VERSION "mainline") +set(ORTHANC_WSI_ANNOTATIONS_VERSION "1") # Must be an integer + if (ORTHANC_WSI_VERSION STREQUAL "mainline") set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") else() - set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.9") + set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.13.0") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") endif() add_definitions( -DORTHANC_WSI_VERSION="${ORTHANC_WSI_VERSION}" + -DORTHANC_WSI_ANNOTATIONS_VERSION=${ORTHANC_WSI_ANNOTATIONS_VERSION} ) set(ORTHANC_FRAMEWORK_VERSION "${ORTHANC_FRAMEWORK_DEFAULT_VERSION}" CACHE STRING "Version of the Orthanc framework")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Icons/README.txt Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,2 @@ +These free SVG icons come from: +https://www.svgrepo.com/vectors/freehand/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Icons/freehand-area-svgrepo-com.svg Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M21.8 15.66c0 3.479-1.17 6.14-4.553 6.14-3.743 0-5.414-2.337-8.7-2.337-.844 0-1.535.154-2.22.154A4.14 4.14 0 0 1 2 15.504c0-2.329 2.956-2.756 3.19-7.521C5.373 4.254 8.23 2.2 11.565 2.2 17.521 2.2 21.8 8.757 21.8 15.66zm-1 0c0-6.017-3.711-12.46-9.235-12.46-2.37 0-5.2 1.265-5.376 4.832a8.817 8.817 0 0 1-2.272 5.716C3.314 14.493 3 14.91 3 15.504c0 3.077 3.293 3.113 3.326 3.113a8.45 8.45 0 0 0 .89-.066 12.225 12.225 0 0 1 1.33-.088 10.29 10.29 0 0 1 4.465 1.2 9.621 9.621 0 0 0 4.235 1.137c.879 0 3.554 0 3.554-5.14z"/><path opacity=".25" d="M17.246 20.8a9.621 9.621 0 0 1-4.235-1.137 10.29 10.29 0 0 0-4.464-1.2 12.225 12.225 0 0 0-1.331.088 8.45 8.45 0 0 1-.89.066C6.293 18.617 3 18.58 3 15.504c0-.594.314-1.01.917-1.756a8.817 8.817 0 0 0 2.272-5.716C6.364 4.465 9.194 3.2 11.565 3.2c5.524 0 9.235 6.443 9.235 12.46 0 5.14-2.675 5.14-3.554 5.14z"/><path fill="none" d="M0 0h24v24H0z"/></svg> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Icons/freehand-svgrepo-com.svg Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23 14.25A3.88 3.88 0 0 0 19.25 10C16.314 10 15 12.763 15 15.5a6.493 6.493 0 0 0 .95 3.516 7.005 7.005 0 0 1-4.905-1.566A3.255 3.255 0 0 1 10 15a9.084 9.084 0 0 1 1.555-3.894A8.31 8.31 0 0 0 13 7.5 2.276 2.276 0 0 0 10.5 5c-.919 0-1.795 1.072-2.81 2.314C6.714 8.511 5.498 10 4.5 10 3.684 10 2 9.51 2 8c0-1.848 2.703-4.028 4.002-5.076l.266-.215-.632-.775-.262.212C3.845 3.379 1 5.675 1 8c0 2.07 2.047 3 3.5 3 1.473 0 2.797-1.622 3.965-3.053C9.174 7.08 10.055 6 10.5 6c1.038 0 1.5.463 1.5 1.5a7.868 7.868 0 0 1-1.313 3.11A9.681 9.681 0 0 0 9 15a4.275 4.275 0 0 0 1.357 3.176A8.438 8.438 0 0 0 16.5 20c.072 0 .144-.001.215-.003a11.08 11.08 0 0 0 6.326 2.871l.167-.986a11.16 11.16 0 0 1-5.178-2.024A5.937 5.937 0 0 0 23 14.25zm-7 1.25c0-2.24 1.005-4.5 3.25-4.5.951 0 2.75.68 2.75 3.25a5.033 5.033 0 0 1-4.857 4.722A5.396 5.396 0 0 1 16 15.5z"/><path fill="none" d="M0 0h24v24H0z"/></svg> \ No newline at end of file
--- a/Resources/Orthanc/CMake/AutoGeneratedCode.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/CMake/AutoGeneratedCode.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -76,5 +76,5 @@ list(APPEND AUTOGENERATED_SOURCES "${TARGET_BASE}.cpp" - ) + ) endmacro()
--- a/Resources/Orthanc/CMake/Compiler.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/CMake/Compiler.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -43,7 +43,7 @@ include(CheckLibraryExists) if ((CMAKE_CROSSCOMPILING AND NOT - "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR + "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") # Cross-compilation necessarily implies standalone and static build SET(STATIC_BUILD ON) @@ -70,7 +70,7 @@ 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 + # -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) @@ -85,12 +85,12 @@ foreach(flag_var CMAKE_C_FLAGS_DEBUG CMAKE_CXX_FLAGS_DEBUG - CMAKE_C_FLAGS_RELEASE + CMAKE_C_FLAGS_RELEASE CMAKE_CXX_FLAGS_RELEASE - CMAKE_C_FLAGS_MINSIZEREL - CMAKE_CXX_FLAGS_MINSIZEREL - CMAKE_C_FLAGS_RELWITHDEBINFO - CMAKE_CXX_FLAGS_RELWITHDEBINFO) + 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) @@ -109,7 +109,7 @@ 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 @@ -188,7 +188,7 @@ # for LFS (Large File Support). # https://ohse.de/uwe/articles/lfs.html add_definitions( - -D_LARGEFILE64_SOURCE=1 + -D_LARGEFILE64_SOURCE=1 -D_FILE_OFFSET_BITS=64 ) endif() @@ -254,14 +254,14 @@ add_definitions( -D_XOPEN_SOURCE=1 ) - + # Linking with iconv breaks the Universal builds on modern compilers # 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()
--- a/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -177,6 +177,10 @@ set(ORTHANC_FRAMEWORK_MD5 "66b5a2ee60706c4a502896083b9e1a01") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.10") set(ORTHANC_FRAMEWORK_MD5 "d5e1ba442104c89a24013cb859a9d6bf") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.11") + set(ORTHANC_FRAMEWORK_MD5 "389b273b64b513ba8fc3233f34201cc1") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.13.0") + set(ORTHANC_FRAMEWORK_MD5 "cc95d3e509612b541e10e91c831fbfe3") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc @@ -231,6 +235,16 @@ # for BlockingSharedMessageQueue.WaitEmpty() set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) set(ORTHANC_FRAMEWORK_MD5 "c037cd2ddbe1b65b431692855483161b") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "56eb61c86f93") + # OE2 1.11.0 (framework post-1.12.10) + # for HttpClient that returns the answer body in case of HTTP error + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "665f8aa70d7c5091bc20da37cf664910") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "004b351797fe") + # PixelsMasker 0.1.2 (framework post-1.12.11) + # for ScopedThreadNameSetter + set(ORTHANC_FRAMEWORK_PRE_RELEASE ON) + set(ORTHANC_FRAMEWORK_MD5 "f078ca997217b831ab3f6741f08a8c07") endif() endif() endif() @@ -250,7 +264,7 @@ 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() @@ -260,8 +274,8 @@ 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 + find_program(ORTHANC_FRAMEWORK_7ZIP 7z + PATHS "$ENV{ProgramFiles}/7-Zip" "$ENV{ProgramW6432}/7-Zip" ) @@ -289,7 +303,7 @@ 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() @@ -314,14 +328,14 @@ 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}) @@ -384,14 +398,14 @@ message("Downloading: ${ORTHANC_FRAMEWORK_URL}") file(DOWNLOAD - "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}" + "${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() endif() @@ -427,7 +441,7 @@ 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") @@ -440,7 +454,7 @@ RESULT_VARIABLE Failure OUTPUT_QUIET ) - + if (Failure) message(FATAL_ERROR "Error while running the uncompression tool") endif() @@ -462,7 +476,7 @@ RESULT_VARIABLE Failure ) endif() - + if (Failure) message(FATAL_ERROR "Error while running the uncompression tool") endif() @@ -528,10 +542,10 @@ 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) - + if(CMAKE_VERSION VERSION_GREATER "3.11") find_package(Python REQUIRED COMPONENTS Interpreter) set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) @@ -539,7 +553,7 @@ include(FindPythonInterp) find_package(PythonInterp REQUIRED) endif() - + include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake) include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake) include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake) @@ -600,19 +614,19 @@ 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()
--- a/Resources/Orthanc/CMake/DownloadPackage.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/CMake/DownloadPackage.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -59,8 +59,8 @@ ## if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows") - find_program(ZIP_EXECUTABLE 7z - PATHS + find_program(ZIP_EXECUTABLE 7z + PATHS "$ENV{ProgramFiles}/7-Zip" "$ENV{ProgramW6432}/7-Zip" ) @@ -92,7 +92,7 @@ set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}") if (NOT EXISTS "${TMP_PATH}") - message("Downloading ${Url}") + message("Downloading ${Url} since the file was not found in ${TMP_PATH}") # This fixes issue 6: "I think cmake shouldn't download the # packages which are not in the system, it should stop and let @@ -124,7 +124,7 @@ file(REMOVE ${TMP_PATH}) message(FATAL_ERROR "Cannot download file: ${Url}") endif() - + else() message("Using local copy of ${Url}") @@ -143,7 +143,7 @@ macro(DownloadPackage MD5 Url TargetDirectory) if (NOT IS_DIRECTORY "${TargetDirectory}") DownloadFile("${MD5}" "${Url}") - + GetUrlExtension(TMP_EXTENSION "${Url}") #message(${TMP_EXTENSION}) message("Uncompressing ${TMP_FILENAME}") @@ -152,7 +152,7 @@ # 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 + if (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz") OR ("${TMP_EXTENSION}" STREQUAL "xz")) execute_process( @@ -221,7 +221,7 @@ message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}") endif() endif() - + if (Failure) message(FATAL_ERROR "Error while running the uncompression tool") endif() @@ -237,7 +237,7 @@ macro(DownloadCompressedFile MD5 Url TargetFile) if (NOT EXISTS "${TargetFile}") DownloadFile("${MD5}" "${Url}") - + GetUrlExtension(TMP_EXTENSION "${Url}") #message(${TMP_EXTENSION}) message("Uncompressing ${TMP_FILENAME}") @@ -275,7 +275,7 @@ message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}") endif() endif() - + if (Failure) message(FATAL_ERROR "Error while running the uncompression tool") endif()
--- a/Resources/Orthanc/CMake/EmbedResources.py Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/CMake/EmbedResources.py Mon Sep 07 20:58:51 2026 +0200 @@ -90,7 +90,7 @@ 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 = {} @@ -165,7 +165,7 @@ for ns in NAMESPACE.split('.'): header.write('namespace %s {\n' % ns) - + header.write(""" enum FileResourceId @@ -177,7 +177,7 @@ if resources[name]['Type'] == 'File': if isFirst: isFirst = False - else: + else: header.write(',\n') header.write(' %s' % name) @@ -193,7 +193,7 @@ if resources[name]['Type'] == 'Directory': if isFirst: isFirst = False - else: + else: header.write(',\n') header.write(' %s' % name) @@ -235,8 +235,8 @@ # 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 + 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
--- a/Resources/Orthanc/LinuxStandardBaseToolchain.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/LinuxStandardBaseToolchain.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -62,7 +62,7 @@ # which compilers to use for C and C++ SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc) -if (${CMAKE_VERSION} VERSION_LESS "3.6.0") +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++) @@ -72,7 +72,7 @@ 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 +# 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)
--- a/Resources/Orthanc/MinGW-W64-Toolchain32.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/MinGW-W64-Toolchain32.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -32,7 +32,7 @@ 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 +# 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)
--- a/Resources/Orthanc/MinGW-W64-Toolchain64.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/MinGW-W64-Toolchain64.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -32,7 +32,7 @@ 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 +# 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)
--- a/Resources/Orthanc/MinGWToolchain.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/MinGWToolchain.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -32,7 +32,7 @@ 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 +# 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)
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -10,7 +10,7 @@ * modify it under the terms of the 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 @@ -28,7 +28,7 @@ #include <boost/move/unique_ptr.hpp> #include <boost/thread.hpp> #include <boost/algorithm/string/join.hpp> - +#include <limits> #include <json/reader.h> #include <json/version.h> @@ -61,6 +61,13 @@ #endif +#ifdef _MSC_VER +# define ORTHANC_SCANF sscanf_s +#else +# define ORTHANC_SCANF sscanf +#endif + + namespace OrthancPlugins { static OrthancPluginContext* globalContext_ = NULL; @@ -117,7 +124,7 @@ } -#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 +#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1 void LogMessage(OrthancPluginLogLevel level, const char* file, uint32_t line, @@ -125,7 +132,7 @@ { if (HasGlobalContext()) { -#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 +#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1 const char* pluginName = (pluginName_.empty() ? NULL : pluginName_.c_str()); OrthancPluginLogMessage(GetGlobalContext(), message.c_str(), pluginName, file, line, OrthancPluginLogCategory_Generic, level); #else @@ -184,7 +191,7 @@ // Prevent using garbage information buffer_.data = NULL; buffer_.size = 0; - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } @@ -209,7 +216,7 @@ } else { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } @@ -227,7 +234,7 @@ { Clear(); } - catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) // NOLINT(bugprone-empty-catch) { // Don't throw exceptions in destructors } @@ -264,7 +271,7 @@ } else { - if (size > 0) + if (buffer != NULL && size > 0) { memcpy(buffer_.data, buffer, size); } @@ -365,7 +372,7 @@ } -#if (HAS_ORTHANC_PLUGIN_PEERS == 1) || (HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1) || (HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1) +#if (HAS_ORTHANC_PLUGINS_PEERS == 1) || (HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1) || (HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1) static void DecodeHttpHeaders(HttpHeaders& target, const MemoryBuffer& source) { @@ -411,7 +418,7 @@ { headersKeys_.push_back(it->first.c_str()); headersValues_.push_back(it->second.c_str()); - } + } } const char* const* GetKeys() @@ -439,7 +446,7 @@ PluginHttpHeaders headers(httpHeaders); return CheckHttp(OrthancPluginRestApiGet2( - GetGlobalContext(), &buffer_, uri.c_str(), + GetGlobalContext(), &buffer_, uri.c_str(), headers.GetSize(), headers.GetKeys(), headers.GetValues(), applyPlugins)); @@ -451,7 +458,7 @@ bool applyPlugins) { Clear(); - + // Cast for compatibility with Orthanc SDK <= 1.5.6 const char* b = reinterpret_cast<const char*>(body); @@ -465,7 +472,7 @@ } } -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 bool MemoryBuffer::RestApiPost(const std::string& uri, const void* body, @@ -478,7 +485,7 @@ PluginHttpHeaders headers(httpHeaders); - return CheckHttp(OrthancPluginCallRestApi(GetGlobalContext(), + return CheckHttp(OrthancPluginCallRestApi(GetGlobalContext(), &buffer_, *answerHeaders, &httpStatus, @@ -534,10 +541,10 @@ #else Json::CharReaderBuilder builder; builder.settings_["collectComments"] = collectComments; - + const std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); assert(reader.get() != NULL); - + JSONCPP_STRING err; if (reader->parse(reinterpret_cast<const char*>(buffer), reinterpret_cast<const char*>(buffer) + size, &target, &err)) @@ -558,7 +565,7 @@ { return ReadJson(target, source.empty() ? NULL : source.c_str(), source.size()); } - + bool ReadJson(Json::Value& target, const void* buffer, @@ -566,14 +573,14 @@ { 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, @@ -595,7 +602,7 @@ target = Json::writeString(builder, source); #endif } - + void WriteStyledJson(std::string& target, const Json::Value& source) @@ -675,7 +682,7 @@ { Clear(); } - catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) // NOLINT(bugprone-empty-catch) { // Don't throw exceptions in destructors } @@ -839,7 +846,7 @@ } else { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); + ORTHANC_PLUGINS_THROW_ERROR_CODE(error); } } @@ -862,7 +869,7 @@ ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); } } - + OrthancConfiguration::OrthancConfiguration() { @@ -977,7 +984,15 @@ return true; case Json::uintValue: - target = configuration_[key].asUInt(); + if (configuration_[key].asUInt() > static_cast<unsigned int>(std::numeric_limits<int>::max())) + { + ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) + + "\" is too large to fit in an integer"); + + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + + target = static_cast<int>(configuration_[key].asUInt()); return true; default: @@ -1338,7 +1353,7 @@ { Clear(); } - catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) + catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) // NOLINT(bugprone-empty-catch) { // Don't throw exceptions in destructors } @@ -1473,7 +1488,7 @@ } -#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 +#if HAS_ORTHANC_PLUGINS_FIND_MATCHER == 1 FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) : matcher_(NULL), worklist_(worklist) @@ -1542,13 +1557,27 @@ } } -#endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */ +#endif /* HAS_ORTHANC_PLUGINS_FIND_MATCHER == 1 */ + + static void CheckAnswerSizeIsLessThan4GB(const std::string& answer) + { + if (answer.size() > static_cast<size_t>(std::numeric_limits<uint32_t>::max())) + { +#if HAS_ORTHANC_EXCEPTION == 1 + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Cannot send HTTP response larger than 4GB"); +#else + ORTHANC_PLUGINS_LOG_ERROR("Cannot send HTTP response larger than 4GB"); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); +#endif + } + } void AnswerJson(const Json::Value& value, OrthancPluginRestOutput* output) { std::string bodyString; - WriteStyledJson(bodyString, value); + WriteStyledJson(bodyString, value); + CheckAnswerSizeIsLessThan4GB(bodyString); OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json"); } @@ -1556,6 +1585,7 @@ const char* mimeType, OrthancPluginRestOutput* output) { + CheckAnswerSizeIsLessThan4GB(answer); OrthancPluginAnswerBuffer(GetGlobalContext(), output, answer.c_str(), answer.size(), mimeType); } @@ -1564,6 +1594,26 @@ OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError); } + void AnswerHttpError(uint16_t httpError, + OrthancPluginRestOutput* output, + const std::string& answer, + const char* mimeType) + { + CheckAnswerSizeIsLessThan4GB(answer); + + OrthancPluginSetHttpHeader(GetGlobalContext(), + output, + "content-type", + mimeType); + + OrthancPluginSendHttpStatus(GetGlobalContext(), + output, + httpError, + answer.c_str(), + static_cast<uint32_t>(answer.size())); + } + + void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods) { OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods); @@ -1691,7 +1741,7 @@ } } -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 bool RestApiPost(Json::Value& result, const std::string& uri, const Json::Value& body, @@ -1786,7 +1836,7 @@ } else { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); + ORTHANC_PLUGINS_THROW_ERROR_CODE(error); } } @@ -1815,12 +1865,6 @@ 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 && @@ -1908,7 +1952,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_PEERS == 1 +#if HAS_ORTHANC_PLUGINS_PEERS == 1 size_t OrthancPeers::GetPeerIndex(const std::string& name) const { size_t index; @@ -1932,7 +1976,7 @@ if (peers_ == NULL) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin); } uint32_t count = OrthancPluginGetPeersCount(GetGlobalContext(), peers_); @@ -1943,7 +1987,7 @@ if (name == NULL) { OrthancPluginFreePeers(GetGlobalContext(), peers_); - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin); } index_[name] = i; @@ -1981,14 +2025,14 @@ { if (index >= index_.size()) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } else { const char* s = OrthancPluginGetPeerName(GetGlobalContext(), peers_, static_cast<uint32_t>(index)); if (s == NULL) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin); } else { @@ -2002,14 +2046,14 @@ { if (index >= index_.size()) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } else { const char* s = OrthancPluginGetPeerUrl(GetGlobalContext(), peers_, static_cast<uint32_t>(index)); if (s == NULL) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin); } else { @@ -2031,7 +2075,7 @@ { if (index >= index_.size()) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } else { @@ -2064,7 +2108,7 @@ { if (index >= index_.size()) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } OrthancPlugins::MemoryBuffer answer; @@ -2152,7 +2196,7 @@ size_t index, const std::string& uri, const std::string& body, - const HttpHeaders& headers, + const HttpHeaders& headers, unsigned int timeout) const { MemoryBuffer buffer; @@ -2174,7 +2218,7 @@ size_t index, const std::string& uri, const std::string& body, - const HttpHeaders& headers, + const HttpHeaders& headers, unsigned int timeout) const { MemoryBuffer buffer; @@ -2262,7 +2306,7 @@ { if (index >= index_.size()) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } if (body.size() > 0xffffffffu) @@ -2285,7 +2329,7 @@ { target.Swap(answer); DecodeHttpHeaders(answerHeaders, answerHeadersBuffer); - + return (status == 200); } else @@ -2302,7 +2346,7 @@ { if (index >= index_.size()) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } if (body.size() > 0xffffffffu) @@ -2348,7 +2392,7 @@ { if (index >= index_.size()) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } OrthancPlugins::MemoryBuffer answer; @@ -2389,7 +2433,7 @@ ** JOBS ******************************************************************/ -#if HAS_ORTHANC_PLUGIN_JOB == 1 +#if HAS_ORTHANC_PLUGINS_JOB == 1 void OrthancJob::CallbackFinalize(void* job) { if (job != NULL) @@ -2428,7 +2472,7 @@ { memcpy(target->data, source.c_str(), source.size()); } - + return OrthancPluginErrorCode_Success; } } @@ -2471,7 +2515,7 @@ { assert(job != NULL); OrthancJob& that = *reinterpret_cast<OrthancJob*>(job); - + if (that.hasSerialized_) { if (CopyStringToMemoryBuffer(target, that.serialized_) == OrthancPluginErrorCode_Success) @@ -2587,7 +2631,7 @@ if (content.type() != Json::objectValue) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat); } else { @@ -2607,7 +2651,7 @@ { if (serialized.type() != Json::objectValue) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat); } else { @@ -2622,7 +2666,7 @@ if (progress < 0 || progress > 1) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange); } progress_ = progress; @@ -2642,7 +2686,7 @@ { if (job == NULL) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_NullPointer); } OrthancPluginJob* orthanc = @@ -2657,7 +2701,7 @@ if (orthanc == NULL) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin); } else { @@ -2671,7 +2715,7 @@ { if (job == NULL) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_NullPointer); } OrthancPluginJob* orthanc = Create(job); @@ -2682,7 +2726,7 @@ { ORTHANC_PLUGINS_LOG_ERROR("Plugin cannot submit job"); OrthancPluginFreeJob(GetGlobalContext(), orthanc); - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin); } else { @@ -2710,7 +2754,7 @@ !status.isMember("State") || status["State"].type() != Json::stringValue) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InexistentItem); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_InexistentItem); } const std::string state = status["State"].asString(); @@ -2739,30 +2783,30 @@ if (!status.isMember("ErrorCode") || status["ErrorCode"].type() != Json::intValue) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_InternalError); } else { if (!status.isMember("ErrorDescription") || status["ErrorDescription"].type() != Json::stringValue) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt()); + ORTHANC_PLUGINS_THROW_ERROR_CODE(status["ErrorCode"].asInt()); } else { - #if HAS_ORTHANC_EXCEPTION == 1 +#if HAS_ORTHANC_EXCEPTION == 1 throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(status["ErrorCode"].asInt()), status["ErrorDescription"].asString()); - #else +#else ORTHANC_PLUGINS_LOG_ERROR("Exception while executing the job: " + status["ErrorDescription"].asString()); - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt()); - #endif + ORTHANC_PLUGINS_THROW_ERROR_CODE(status["ErrorCode"].asInt()); +#endif } } } else { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError); + ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_InternalError); } } } @@ -2777,7 +2821,7 @@ static const char* KEY_PRIORITY = "Priority"; boost::movelib::unique_ptr<OrthancJob> protection(job); - + if (body.type() != Json::objectValue) { #if HAS_ORTHANC_EXCEPTION == 1 @@ -2790,7 +2834,7 @@ } bool synchronous = true; - + if (body.isMember(KEY_SYNCHRONOUS)) { if (body[KEY_SYNCHRONOUS].type() != Json::booleanValue) @@ -2849,7 +2893,7 @@ priority = !body[KEY_PRIORITY].asInt(); } } - + Json::Value result; if (synchronous) @@ -2879,13 +2923,13 @@ ** METRICS ******************************************************************/ -#if HAS_ORTHANC_PLUGIN_METRICS == 1 +#if HAS_ORTHANC_PLUGINS_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(); @@ -2902,7 +2946,7 @@ ** HTTP CLIENT ******************************************************************/ -#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1 class HttpClient::RequestBodyWrapper : public boost::noncopyable { private: @@ -2921,18 +2965,18 @@ body_(body), done_(false) { - } + } static uint8_t IsDone(void* body) { return GetObject(body).done_; } - + static const void* GetChunkData(void* body) { return GetObject(body).chunk_.c_str(); } - + static uint32_t GetChunkSize(void* body) { return static_cast<uint32_t>(GetObject(body).chunk_.size()); @@ -2941,7 +2985,7 @@ static OrthancPluginErrorCode Next(void* body) { RequestBodyWrapper& that = GetObject(body); - + if (that.done_) { return OrthancPluginErrorCode_BadSequenceOfCalls; @@ -2962,11 +3006,11 @@ return OrthancPluginErrorCode_Plugin; } } - } + } }; -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1 static OrthancPluginErrorCode AnswerAddHeaderCallback(void* answer, const char* key, const char* value) @@ -2990,7 +3034,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1 static OrthancPluginErrorCode AnswerAddChunkCallback(void* answer, const void* data, uint32_t size) @@ -3034,7 +3078,7 @@ } } - + void HttpClient::SetCredentials(const std::string& username, const std::string& password) { @@ -3042,7 +3086,7 @@ password_ = password; } - + void HttpClient::ClearCredentials() { username_.clear(); @@ -3059,7 +3103,7 @@ certificateKeyPassword_ = keyPassword; } - + void HttpClient::ClearCertificate() { certificateFile_.clear(); @@ -3074,21 +3118,21 @@ 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(); @@ -3254,7 +3298,7 @@ }; -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1 class MemoryAnswer : public HttpClient::IAnswer { private: @@ -3288,7 +3332,7 @@ } -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1 void HttpClient::ExecuteWithStream(uint16_t& httpStatus, IAnswer& answer, IRequestBody& body) const @@ -3317,7 +3361,7 @@ } RequestBodyWrapper request(body); - + OrthancPluginErrorCode error = OrthancPluginChunkedHttpClient( GetGlobalContext(), &answer, @@ -3344,10 +3388,10 @@ if (error != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); - } - } -#endif + ORTHANC_PLUGINS_THROW_ERROR_CODE(error); + } + } +#endif void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus, HttpHeaders& answerHeaders, @@ -3386,7 +3430,7 @@ if (error != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error); + ORTHANC_PLUGINS_THROW_ERROR_CODE(error); } DecodeHttpHeaders(answerHeaders, answerHeadersBuffer); @@ -3396,7 +3440,7 @@ void HttpClient::Execute(IAnswer& answer) { -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1 if (allowChunkedTransfers_) { if (chunkedBody_ != NULL) @@ -3412,7 +3456,7 @@ 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) @@ -3421,10 +3465,10 @@ std::string answerBody; Execute(answerHeaders, answerBody); - for (HttpHeaders::const_iterator it = answerHeaders.begin(); + for (HttpHeaders::const_iterator it = answerHeaders.begin(); it != answerHeaders.end(); ++it) { - answer.AddHeader(it->first, it->second); + answer.AddHeader(it->first, it->second); } if (!answerBody.empty()) @@ -3437,7 +3481,7 @@ void HttpClient::Execute(HttpHeaders& answerHeaders /* out */, std::string& answerBody /* out */) { -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1 if (allowChunkedTransfers_) { MemoryAnswer answer; @@ -3447,7 +3491,7 @@ 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) @@ -3455,7 +3499,7 @@ if (chunkedBody_ != NULL) { ChunkedBuffer buffer; - + std::string chunk; while (chunkedBody_->ReadNextChunk(chunk)) { @@ -3479,7 +3523,7 @@ { std::string body; Execute(answerHeaders, body); - + if (!ReadJson(answerBody, body)) { ORTHANC_PLUGINS_LOG_ERROR("Cannot convert HTTP answer body to JSON"); @@ -3495,7 +3539,7 @@ Execute(answerHeaders, body); } -#endif /* HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 */ +#endif /* HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1 */ @@ -3512,7 +3556,7 @@ const OrthancPluginHttpRequest* request) { } - + IChunkedRequestReader *NullChunkedRestCallback(const char* url, const OrthancPluginHttpRequest* request) { @@ -3520,7 +3564,7 @@ } -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER == 1 OrthancPluginErrorCode ChunkedRequestReaderAddChunk( OrthancPluginServerChunkedRequestReader* reader, @@ -3551,7 +3595,7 @@ } } - + OrthancPluginErrorCode ChunkedRequestReaderExecute( OrthancPluginServerChunkedRequestReader* reader, OrthancPluginRestOutput* output) @@ -3580,7 +3624,7 @@ } } - + void ChunkedRequestReaderFinalize( OrthancPluginServerChunkedRequestReader* reader) { @@ -3591,7 +3635,7 @@ } #else - + OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output, const char* url, const OrthancPluginHttpRequest* request, @@ -3615,7 +3659,7 @@ { allowed += ","; } - + allowed += "POST"; } @@ -3625,7 +3669,7 @@ { allowed += ","; } - + allowed += "DELETE"; } @@ -3635,10 +3679,10 @@ { allowed += ","; } - + allowed += "PUT"; } - + switch (request->method) { case OrthancPluginHttpMethod_Get: @@ -3715,7 +3759,7 @@ } catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) { -#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1 +#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS == 1 if (HasGlobalContext() && e.HasDetails()) { @@ -3742,7 +3786,7 @@ } -#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 +#if HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP == 1 OrthancPluginErrorCode IStorageCommitmentScpHandler::Lookup( OrthancPluginStorageCommitmentFailureReason* target, void* rawHandler, @@ -3751,7 +3795,7 @@ { assert(target != NULL && rawHandler != NULL); - + try { IStorageCommitmentScpHandler& handler = *reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler); @@ -3770,7 +3814,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 +#if HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP == 1 void IStorageCommitmentScpHandler::Destructor(void* rawHandler) { assert(rawHandler != NULL); @@ -3779,7 +3823,7 @@ #endif -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) DicomInstance::DicomInstance(const OrthancPluginDicomInstance* instance) : toFree_(false), instance_(instance) @@ -3820,7 +3864,7 @@ #endif } - + std::string DicomInstance::GetRemoteAet() const { const char* s = OrthancPluginGetInstanceRemoteAet(GetGlobalContext(), instance_); @@ -3841,7 +3885,7 @@ s.Assign(OrthancPluginGetInstanceJson(GetGlobalContext(), instance_)); s.ToJson(target); } - + void DicomInstance::GetSimplifiedJson(Json::Value& target) const { @@ -3863,7 +3907,7 @@ } #endif - + #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) bool DicomInstance::HasPixelData() const { @@ -3880,7 +3924,7 @@ #endif -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) void DicomInstance::GetRawFrame(std::string& target, unsigned int frameIndex) const { @@ -3894,13 +3938,13 @@ } else { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } #endif -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) OrthancImage* DicomInstance::GetDecodedFrame(unsigned int frameIndex) const { OrthancPluginImage* image = OrthancPluginGetInstanceDecodedFrame( @@ -3915,7 +3959,7 @@ return new OrthancImage(image); } } -#endif +#endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) @@ -3931,11 +3975,11 @@ } else { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } #endif - + #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) DicomInstance* DicomInstance::Transcode(const void* buffer, @@ -3980,7 +4024,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 static std::vector<std::string> WebDavConvertPath(uint32_t pathSize, const char* const* pathItems) { @@ -3994,9 +4038,9 @@ return result; } #endif - - -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + + +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 static OrthancPluginErrorCode WebDavIsExistingFolder(uint8_t* isExisting, uint32_t pathSize, const char* const* pathItems, @@ -4020,8 +4064,8 @@ } #endif - -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 static OrthancPluginErrorCode WebDavListFolder(uint8_t* isExisting, OrthancPluginWebDavCollection* collection, OrthancPluginWebDavAddFile addFile, @@ -4031,12 +4075,12 @@ void* payload) { IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload); - + try { std::list<IWebDavCollection::FileInfo> files; std::list<IWebDavCollection::FolderInfo> subfolders; - + if (!that.ListFolder(files, subfolders, WebDavConvertPath(pathSize, pathItems))) { *isExisting = 0; @@ -4044,33 +4088,33 @@ else { *isExisting = 1; - + for (std::list<IWebDavCollection::FileInfo>::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<IWebDavCollection::FolderInfo>::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) @@ -4082,10 +4126,10 @@ return OrthancPluginErrorCode_Plugin; } } -#endif - - -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 +#endif + + +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 static OrthancPluginErrorCode WebDavRetrieveFile(OrthancPluginWebDavCollection* collection, OrthancPluginWebDavRetrieveFile retrieveFile, uint32_t pathSize, @@ -4097,7 +4141,7 @@ try { std::string content, mime, dateTime; - + if (that.GetFile(content, mime, dateTime, WebDavConvertPath(pathSize, pathItems))) { return retrieveFile(collection, content.empty() ? NULL : content.c_str(), @@ -4117,11 +4161,11 @@ { return OrthancPluginErrorCode_InternalError; } - } + } #endif -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 static OrthancPluginErrorCode WebDavStoreFileCallback(uint8_t* isReadOnly, /* out */ uint32_t pathSize, const char* const* pathItems, @@ -4137,7 +4181,7 @@ { ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); } - + *isReadOnly = (that.StoreFile(WebDavConvertPath(pathSize, pathItems), data, static_cast<size_t>(size)) ? 1 : 0); return OrthancPluginErrorCode_Success; @@ -4153,8 +4197,8 @@ } #endif - -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 static OrthancPluginErrorCode WebDavCreateFolderCallback(uint8_t* isReadOnly, /* out */ uint32_t pathSize, const char* const* pathItems, @@ -4177,9 +4221,9 @@ } } #endif - - -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + + +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 static OrthancPluginErrorCode WebDavDeleteItemCallback(uint8_t* isReadOnly, /* out */ uint32_t pathSize, const char* const* pathItems, @@ -4203,8 +4247,8 @@ } #endif - -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 + +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 void IWebDavCollection::Register(const std::string& uri, IWebDavCollection& collection) { @@ -4214,7 +4258,7 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } #endif @@ -4226,7 +4270,7 @@ for (uint32_t i = 0; i < request->getCount; ++i) { result[request->getKeys[i]] = request->getValues[i]; - } + } } void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request) @@ -4236,7 +4280,7 @@ for (uint32_t i = 0; i < request->headersCount; ++i) { result[request->headersKeys[i]] = request->headersValues[i]; - } + } } void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request) @@ -4304,7 +4348,7 @@ } -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 RestApiClient::RestApiClient() : method_(OrthancPluginHttpMethod_Get), path_("/"), @@ -4338,7 +4382,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 void RestApiClient::AddRequestHeader(const std::string& key, const std::string& value) { @@ -4354,7 +4398,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 void RestApiClient::SetRequestHeader(const std::string& key, const std::string& value) { @@ -4363,7 +4407,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 bool RestApiClient::Execute() { if (requestBody_.size() > 0xffffffffu) @@ -4405,7 +4449,7 @@ } else { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } } @@ -4430,7 +4474,7 @@ mimeType = h->second.c_str(); } } - + AnswerString(answerBody_, mimeType, output); } else @@ -4446,7 +4490,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 uint16_t RestApiClient::GetHttpStatus() const { if (httpStatus_ == 0) @@ -4461,7 +4505,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 bool RestApiClient::LookupAnswerHeader(std::string& value, const std::string& key) const { @@ -4486,7 +4530,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 const std::string& RestApiClient::GetAnswerBody() const { if (httpStatus_ == 0) @@ -4501,7 +4545,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator *iterator) : iterator_(iterator) { @@ -4513,7 +4557,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 KeyValueStore::Iterator::~Iterator() { OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_); @@ -4521,7 +4565,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 bool KeyValueStore::Iterator::Next() { uint8_t done; @@ -4529,7 +4573,7 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } else { @@ -4539,7 +4583,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 std::string KeyValueStore::Iterator::GetKey() const { const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_); @@ -4555,7 +4599,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 void KeyValueStore::Iterator::GetValue(std::string& value) const { OrthancPlugins::MemoryBuffer valueBuffer; @@ -4563,7 +4607,7 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } else { @@ -4573,7 +4617,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 void KeyValueStore::Store(const std::string& key, const void* value, size_t valueSize) @@ -4587,13 +4631,13 @@ key.c_str(), value, static_cast<uint32_t>(valueSize)); if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 bool KeyValueStore::GetValue(std::string& value, const std::string& key) { @@ -4604,7 +4648,7 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } else if (found) { @@ -4619,7 +4663,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 void KeyValueStore::DeleteKey(const std::string& key) { OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(), @@ -4627,13 +4671,13 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 KeyValueStore::Iterator* KeyValueStore::CreateIterator() { return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str())); @@ -4641,7 +4685,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_QUEUES == 1 +#if HAS_ORTHANC_PLUGINS_QUEUES == 1 void Queue::Enqueue(const void* value, size_t valueSize) { @@ -4655,13 +4699,13 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } } #endif -#if HAS_ORTHANC_PLUGIN_QUEUES == 1 +#if HAS_ORTHANC_PLUGINS_QUEUES == 1 bool Queue::DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin) { @@ -4676,7 +4720,7 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } else if (found) { @@ -4691,7 +4735,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_QUEUES == 1 +#if HAS_ORTHANC_PLUGINS_QUEUES == 1 uint64_t Queue::GetSize() { uint64_t size = 0; @@ -4699,7 +4743,7 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } else { @@ -4709,7 +4753,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 bool Queue::ReserveInternal(std::string& value, uint64_t& valueId, OrthancPluginQueueOrigin origin, uint32_t releaseTimeout) { uint8_t found = false; @@ -4720,7 +4764,7 @@ if (code != OrthancPluginErrorCode_Success) { - ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + ORTHANC_PLUGINS_THROW_ERROR_CODE(code); } else if (found) { @@ -4735,7 +4779,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 bool Queue::ReserveBack(std::string& value, uint64_t& valueId, uint32_t releaseTimeout) { return ReserveInternal(value, valueId, OrthancPluginQueueOrigin_Back, releaseTimeout); @@ -4743,7 +4787,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 bool Queue::ReserveFront(std::string& value, uint64_t& valueId, uint32_t releaseTimeout) { return ReserveInternal(value, valueId, OrthancPluginQueueOrigin_Front, releaseTimeout); @@ -4751,7 +4795,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 void Queue::Acknowledge(uint64_t valueId) { OrthancPluginAcknowledgeQueueValue(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), valueId);
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Mon Sep 07 20:58:51 2026 +0200 @@ -10,7 +10,7 @@ * modify it under the terms of the 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 @@ -27,7 +27,6 @@ #include <orthanc/OrthancCPlugin.h> #include <boost/noncopyable.hpp> -#include <boost/lexical_cast.hpp> #include <boost/thread/mutex.hpp> #include <boost/date_time/posix_time/posix_time.hpp> #include <json/value.h> @@ -41,7 +40,7 @@ /** * 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 * **/ @@ -67,98 +66,98 @@ #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 +# define HAS_ORTHANC_PLUGINS_FIND_MATCHER 1 #else -# define HAS_ORTHANC_PLUGIN_FIND_MATCHER 0 +# define HAS_ORTHANC_PLUGINS_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 +# define HAS_ORTHANC_PLUGINS_PEERS 1 +# define HAS_ORTHANC_PLUGINS_JOB 1 #else -# define HAS_ORTHANC_PLUGIN_PEERS 0 -# define HAS_ORTHANC_PLUGIN_JOB 0 +# define HAS_ORTHANC_PLUGINS_PEERS 0 +# define HAS_ORTHANC_PLUGINS_JOB 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0) -# define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS 1 +# define HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS 1 #else -# define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS 0 +# define HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 4) -# define HAS_ORTHANC_PLUGIN_METRICS 1 +# define HAS_ORTHANC_PLUGINS_METRICS 1 #else -# define HAS_ORTHANC_PLUGIN_METRICS 0 +# define HAS_ORTHANC_PLUGINS_METRICS 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 1, 0) -# define HAS_ORTHANC_PLUGIN_HTTP_CLIENT 1 +# define HAS_ORTHANC_PLUGINS_HTTP_CLIENT 1 #else -# define HAS_ORTHANC_PLUGIN_HTTP_CLIENT 0 +# define HAS_ORTHANC_PLUGINS_HTTP_CLIENT 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7) -# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT 1 +# define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT 1 #else -# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT 0 +# define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7) -# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER 1 +# define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER 1 #else -# define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER 0 +# define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 0) -# define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP 1 +# define HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP 1 #else -# define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP 0 +# define HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2) -# define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API 1 +# define HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API 1 #else -# define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API 0 +# define HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 10, 1) -# define HAS_ORTHANC_PLUGIN_WEBDAV 1 +# define HAS_ORTHANC_PLUGINS_WEBDAV 1 #else -# define HAS_ORTHANC_PLUGIN_WEBDAV 0 +# define HAS_ORTHANC_PLUGINS_WEBDAV 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) -# define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 1 +# define HAS_ORTHANC_PLUGINS_LOG_MESSAGE 1 #else -# define HAS_ORTHANC_PLUGIN_LOG_MESSAGE 0 +# define HAS_ORTHANC_PLUGINS_LOG_MESSAGE 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) -# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 1 -# define HAS_ORTHANC_PLUGIN_QUEUES 1 +# define HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES 1 +# define HAS_ORTHANC_PLUGINS_QUEUES 1 #else -# define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES 0 -# define HAS_ORTHANC_PLUGIN_QUEUES 0 +# define HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES 0 +# define HAS_ORTHANC_PLUGINS_QUEUES 0 #endif #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 10) -# define HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE 1 +# define HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE 1 #else -# define HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE 0 +# define HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE 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 +# define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(f) [[deprecated]] f #elif defined(__GNUC__) || defined(__clang__) -# define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) f __attribute__((deprecated)) +# define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(f) f __attribute__((deprecated)) #elif defined(_MSC_VER) -# define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) __declspec(deprecated) f +# define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(f) __declspec(deprecated) f #else -# define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED +# define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED #endif @@ -172,7 +171,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 +#if HAS_ORTHANC_PLUGINS_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) @@ -204,7 +203,7 @@ OrthancPluginContext* GetGlobalContext(); - + class OrthancImage; @@ -297,7 +296,7 @@ const Json::Value& body, bool applyPlugins); -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 bool RestApiPost(const std::string& uri, const Json::Value& body, const HttpHeaders& httpHeaders, @@ -394,9 +393,9 @@ void ToString(std::string& target) const; void ToJson(Json::Value& target) const; - + void ToJsonWithoutComments(Json::Value& target) const; -}; + }; class OrthancConfiguration : public boost::noncopyable @@ -408,7 +407,7 @@ std::string GetPath(const std::string& key) const; void LoadConfiguration(); - + public: OrthancConfiguration(); // loads the full Orthanc configuration @@ -428,7 +427,7 @@ bool LookupStringValue(std::string& target, const std::string& key) const; - + bool LookupIntegerValue(int& target, const std::string& key) const; @@ -511,7 +510,7 @@ unsigned int GetHeight() const; unsigned int GetPitch() const; - + void* GetBuffer() const; const OrthancPluginImage* GetObject() const @@ -528,14 +527,14 @@ void AnswerJpegImage(OrthancPluginRestOutput* output, uint8_t quality) const; - + void* GetWriteableBuffer(); OrthancPluginImage* Release(); }; -#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 +#if HAS_ORTHANC_PLUGINS_FIND_MATCHER == 1 class FindMatcher : public boost::noncopyable { private: @@ -574,13 +573,13 @@ 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); + const std::string& source); bool ReadJsonWithoutComments(Json::Value& target, const void* buffer, @@ -622,7 +621,7 @@ size_t bodySize, bool applyPlugins); -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 bool RestApiPost(Json::Value& result, const std::string& uri, const Json::Value& body, @@ -690,36 +689,41 @@ void AnswerHttpError(uint16_t httpError, OrthancPluginRestOutput* output); + void AnswerHttpError(uint16_t httpError, + OrthancPluginRestOutput* output, + const std::string& answer, + const char* mimeType); + 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 +#if HAS_ORTHANC_PLUGINS_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 +#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1 // Use macro ORTHANC_PLUGINS_LOG_ERROR() instead - ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogError(const std::string& message)); + ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(void LogError(const std::string& message)); #else void LogError(const std::string& message); #endif -#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 +#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1 // Use macro ORTHANC_PLUGINS_LOG_WARNING() instead - ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogWarning(const std::string& message)); + ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(void LogWarning(const std::string& message)); #else void LogWarning(const std::string& message); #endif -#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1 +#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1 // Use macro ORTHANC_PLUGINS_LOG_INFO() instead - ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogInfo(const std::string& message)); + ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(void LogInfo(const std::string& message)); #else void LogInfo(const std::string& message); #endif @@ -727,7 +731,7 @@ void ReportMinimalOrthancVersion(unsigned int major, unsigned int minor, unsigned int revision); - + bool CheckMinimalOrthancVersion(unsigned int major, unsigned int minor, unsigned int revision); @@ -751,7 +755,7 @@ } catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e) { -#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1 +#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS == 1 if (HasGlobalContext() && e.HasDetails()) { @@ -776,7 +780,7 @@ } } - + template <RestCallback Callback> void RegisterRestCallback(const std::string& uri, bool isThreadSafe) @@ -794,7 +798,7 @@ } -#if HAS_ORTHANC_PLUGIN_PEERS == 1 +#if HAS_ORTHANC_PLUGINS_PEERS == 1 class OrthancPeers : public boost::noncopyable { private: @@ -902,7 +906,7 @@ const std::string& body, const HttpHeaders& headers, unsigned int timeout) const; - + bool DoPost(Json::Value& target, HttpHeaders& answerHeaders, size_t index, @@ -939,7 +943,7 @@ -#if HAS_ORTHANC_PLUGIN_JOB == 1 +#if HAS_ORTHANC_PLUGINS_JOB == 1 class OrthancJob : public boost::noncopyable { private: @@ -985,10 +989,10 @@ void UpdateSerialized(const Json::Value& serialized); void UpdateProgress(float progress); - + public: explicit OrthancJob(const std::string& jobType); - + virtual ~OrthancJob() { } @@ -996,7 +1000,7 @@ virtual OrthancPluginJobStepStatus Step() = 0; virtual void Stop(OrthancPluginJobStopReason reason) = 0; - + virtual void Reset() = 0; static OrthancPluginJob* Create(OrthancJob* job /* takes ownership */); @@ -1018,14 +1022,27 @@ #endif -#if HAS_ORTHANC_PLUGIN_METRICS == 1 +#if HAS_ORTHANC_PLUGINS_METRICS == 1 inline void SetMetricsValue(const char* name, float value) { OrthancPluginSetMetricsValue(GetGlobalContext(), name, value, OrthancPluginMetricsType_Default); } +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1) + inline void SetMetricsValue(const char* name, + int64_t value) + { + OrthancPluginSetMetricsIntegerValue(GetGlobalContext(), name, + value, OrthancPluginMetricsType_Default); + } +#endif + + +#if HAS_ORTHANC_PLUGINS_METRICS == 1 class MetricsTimer : public boost::noncopyable { private: @@ -1040,7 +1057,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1 class HttpClient : public boost::noncopyable { public: @@ -1088,7 +1105,7 @@ IRequestBody* chunkedBody_; bool allowChunkedTransfers_; -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1 void ExecuteWithStream(uint16_t& httpStatus, // out IAnswer& answer, // out IRequestBody& body) const; @@ -1098,7 +1115,7 @@ HttpHeaders& answerHeaders, // out std::string& answerBody, // out const std::string& body) const; - + public: HttpClient(); @@ -1213,16 +1230,16 @@ 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 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER == 1 template <ChunkedRestCallback Callback> static OrthancPluginErrorCode ChunkedProtect(OrthancPluginServerChunkedRequestReader** reader, - const char* url, - const OrthancPluginHttpRequest* request) + const char* url, + const OrthancPluginHttpRequest* request) { try { @@ -1269,7 +1286,7 @@ void ChunkedRequestReaderFinalize( OrthancPluginServerChunkedRequestReader* reader); -#else +#else OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output, const char* url, @@ -1310,7 +1327,7 @@ public: static void Apply(const std::string& uri) { -#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1 +#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER == 1 OrthancPluginRegisterChunkedRestCallback( GetGlobalContext(), uri.c_str(), GetHandler == Internals::NullRestCallback ? NULL : Internals::Protect<GetHandler>, @@ -1322,25 +1339,25 @@ Internals::ChunkedRequestReaderFinalize); #else OrthancPluginRegisterRestCallbackNoLock( - GetGlobalContext(), uri.c_str(), + GetGlobalContext(), uri.c_str(), Internals::ChunkedRestCompatibility<GetHandler, PostHandler, DeleteHandler, PutHandler>); #endif } }; - + -#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1 +#if HAS_ORTHANC_PLUGINS_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, @@ -1356,14 +1373,14 @@ private: bool toFree_; -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) +#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) +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) explicit DicomInstance(const OrthancPluginDicomInstance* instance); #else explicit DicomInstance(OrthancPluginDicomInstance* instance); @@ -1443,15 +1460,15 @@ }; // helper method to convert Http headers from the plugin SDK to a std::map -void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request); + void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request); // helper method to re-serialize the get arguments from the SDK into a string -void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request); + void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request); // helper method to convert Get arguments from the plugin SDK to a std::map -void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request); + void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request); -#if HAS_ORTHANC_PLUGIN_WEBDAV == 1 +#if HAS_ORTHANC_PLUGINS_WEBDAV == 1 class IWebDavCollection : public boost::noncopyable { public: @@ -1498,7 +1515,7 @@ return dateTime_; } }; - + class FolderInfo { private: @@ -1523,7 +1540,7 @@ return dateTime_; } }; - + virtual ~IWebDavCollection() { } @@ -1533,7 +1550,7 @@ virtual bool ListFolder(std::list<FileInfo>& files, std::list<FolderInfo>& subfolders, const std::vector<std::string>& path) = 0; - + virtual bool GetFile(std::string& content /* out */, std::string& mime /* out */, std::string& dateTime /* out */, @@ -1562,7 +1579,7 @@ const std::string& javascript); -#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1 +#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1 class RestApiClient : public boost::noncopyable { private: @@ -1580,7 +1597,7 @@ public: RestApiClient(); - + // used to forward a call from the plugin to the core RestApiClient(const char* url, const OrthancPluginHttpRequest* request); @@ -1662,7 +1679,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1 +#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1 class KeyValueStore : public boost::noncopyable { public: @@ -1717,7 +1734,7 @@ #endif -#if HAS_ORTHANC_PLUGIN_QUEUES == 1 +#if HAS_ORTHANC_PLUGINS_QUEUES == 1 class Queue : public boost::noncopyable { private: @@ -1725,7 +1742,7 @@ bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin); -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 bool ReserveInternal(std::string& value, uint64_t& valueId, OrthancPluginQueueOrigin origin, uint32_t releaseTimeout); #endif @@ -1748,7 +1765,7 @@ Enqueue(value.empty() ? NULL : value.c_str(), value.size()); } -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 // Use ReserveBack() instead ORTHANC_PLUGIN_DEPRECATED #endif @@ -1757,7 +1774,7 @@ return DequeueInternal(value, OrthancPluginQueueOrigin_Back); } -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 // Use ReserveFront() instead ORTHANC_PLUGIN_DEPRECATED #endif @@ -1768,15 +1785,15 @@ uint64_t GetSize(); -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 bool ReserveBack(std::string& value, uint64_t& valueId, uint32_t releaseTimeout); #endif -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 bool ReserveFront(std::string& value, uint64_t& valueId, uint32_t releaseTimeout); #endif -#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1 +#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1 void Acknowledge(uint64_t valueId); #endif };
--- a/Resources/Orthanc/Plugins/OrthancPluginException.h Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginException.h Mon Sep 07 20:58:51 2026 +0200 @@ -10,7 +10,7 @@ * modify it under the terms of the 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 @@ -41,13 +41,45 @@ #endif -#define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code) \ +#if HAS_ORTHANC_EXCEPTION == 1 && defined(__ORTHANC_FILE__) // the OrthancException class accepts a "details" argument -> add the file and line number + +# include <boost/lexical_cast.hpp> + +# define ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE_HELPER(line) #line +# define ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE(line) ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE_HELPER(line) +# define ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO_HELPER(errorCode, errorCodeStr) \ + throw ::Orthanc::OrthancException( \ + errorCode, "Plugin error code " + errorCodeStr + " triggered from " __ORTHANC_FILE__ ":" \ + ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE(__LINE__)) + +# define ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO(errorCode) \ + throw ::Orthanc::OrthancException( \ + errorCode, #errorCode " triggered from " __ORTHANC_FILE__ ":" \ + ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE(__LINE__)) + +# define ORTHANC_PLUGINS_THROW_ERROR_CODE(code) \ + ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO_HELPER( \ + static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code), \ + boost::lexical_cast<std::string>(code)) + +# define ORTHANC_PLUGINS_THROW_EXCEPTION(code) \ + ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO_HELPER( \ + ORTHANC_PLUGINS_GET_ERROR_CODE(code), \ + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_GET_ERROR_CODE(code))) + +#else // the PluginException does not accept a "details" argument + +# define ORTHANC_PLUGINS_THROW_ERROR_CODE(code) \ throw ORTHANC_PLUGINS_EXCEPTION_CLASS(static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code)); +# define ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO(errorCode) \ + ORTHANC_PLUGINS_THROW_ERROR_CODE(errorCode) -#define ORTHANC_PLUGINS_THROW_EXCEPTION(code) \ +# define ORTHANC_PLUGINS_THROW_EXCEPTION(code) \ throw ORTHANC_PLUGINS_EXCEPTION_CLASS(ORTHANC_PLUGINS_GET_ERROR_CODE(code)); - + +#endif + #define ORTHANC_PLUGINS_CHECK_ERROR(code) \ if (code != ORTHANC_PLUGINS_GET_ERROR_CODE(Success)) \
--- a/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake Mon Sep 07 20:58:51 2026 +0200 @@ -9,7 +9,7 @@ # modify it under the terms of the 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Orthanc/Sdk-1.12.9/orthanc/OrthancCPlugin.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,10711 @@ +/** + * \mainpage + * + * This C/C++ SDK allows external developers to create plugins that + * can be loaded into Orthanc to extend its functionality. Each + * Orthanc plugin must expose 4 public functions with the following + * signatures: + * + * -# <tt>int32_t OrthancPluginInitialize(const OrthancPluginContext* context)</tt>: + * This function is invoked by Orthanc when it loads the plugin on startup. + * The plugin must: + * - Check its compatibility with the Orthanc version using + * ::OrthancPluginCheckVersion(). + * - Store the context pointer so that it can use the plugin + * services of Orthanc. + * - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback(). + * - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback(). + * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). + * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea3(). + * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4(). + * - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback(). + * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). + * - 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(). + * - Possibly register a callback to authenticate HTTP requests using OrthancPluginRegisterHttpAuthentication(). + * - Possibly register a callback to store audit logs using OrthancPluginRegisterAuditLogHandler(). + * -# <tt>void OrthancPluginFinalize()</tt>: + * This function is invoked by Orthanc during its shutdown. The plugin + * must free all its memory. + * -# <tt>const char* OrthancPluginGetName()</tt>: + * The plugin must return a short string to identify itself. + * -# <tt>const char* OrthancPluginGetVersion()</tt>: + * The plugin must return a string containing its version number. + * + * The name and the version of a plugin is only used to prevent it + * from being loaded twice. Note that, in C++, it is mandatory to + * declare these functions within an <tt>extern "C"</tt> section. + * + * To ensure multi-threading safety, the various REST callbacks are + * guaranteed to be executed in mutual exclusion since Orthanc + * 0.8.5. If this feature is undesired (notably when developing + * high-performance plugins handling simultaneous requests), use + * ::OrthancPluginRegisterRestCallbackNoLock(). + **/ + + + +/** + * @defgroup Images Images and compression + * @brief Functions to deal with images and compressed buffers. + * + * @defgroup REST REST + * @brief Functions to answer REST requests in a callback. + * + * @defgroup Callbacks Callbacks + * @brief Functions to register and manage callbacks by the plugins. + * + * @defgroup DicomCallbacks DicomCallbacks + * @brief Functions to register and manage DICOM callbacks (worklists, C-FIND, C-MOVE, storage commitment). + * + * @defgroup Orthanc Orthanc + * @brief Functions to access the content of the Orthanc server. + * + * @defgroup DicomInstance DicomInstance + * @brief Functions to access DICOM images that are managed by the Orthanc core. + **/ + + + +/** + * @defgroup Toolbox Toolbox + * @brief Generic functions to help with the creation of plugins. + **/ + + + +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + + +#pragma once + + +#include <stdio.h> +#include <string.h> + +#ifdef _WIN32 +# define ORTHANC_PLUGINS_API __declspec(dllexport) +#elif __GNUC__ >= 4 +# define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default"))) +#else +# define ORTHANC_PLUGINS_API +#endif + +#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 +#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 9 + + +#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 + + +#ifndef ORTHANC_PLUGIN_DEPRECATED +# if defined(_MSC_VER) +# define ORTHANC_PLUGIN_DEPRECATED __declspec(deprecated) +# elif __GNUC__ >= 4 +# define ORTHANC_PLUGIN_DEPRECATED __attribute__ ((deprecated)) +# elif defined(__clang__) +# define ORTHANC_PLUGIN_DEPRECATED __attribute__ ((deprecated)) +# else +# pragma message("WARNING: You need to implement ORTHANC_PLUGINS_DEPRECATED for this compiler") +# define ORTHANC_PLUGIN_DEPRECATED +# endif +#endif + + +#ifndef ORTHANC_PLUGIN_SINCE_SDK +/** + * This macro is used by the code model generator that produces the + * "OrthancPluginCodeModel.json" file. The code model is notably used + * to generate the Python and Java wrappers. Primitives that are not + * tagged with this macro were introduced before Orthanc 1.0.0. + **/ +# if defined(__clang__) +# define ORTHANC_PLUGIN_SINCE_SDK(version) __attribute__ ((annotate("ORTHANC_PLUGIN_SINCE_SDK " version))) +# else +# define ORTHANC_PLUGIN_SINCE_SDK(version) +# endif +#endif + + + +/******************************************************************** + ** Inclusion of standard libraries. + ********************************************************************/ + +/** + * For Microsoft Visual Studio, a compatibility "stdint.h" can be + * downloaded at the following URL: + * https://orthanc.uclouvain.be/hg/orthanc/raw-file/default/OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h + **/ +#include <stdint.h> + +#include <stdlib.h> + + + +/******************************************************************** + ** Definition of the Orthanc Plugin API. + ********************************************************************/ + +/** @{ */ + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * The various error codes that can be returned by the Orthanc core. + **/ + typedef enum + { + OrthancPluginErrorCode_InternalError = -1 /*!< Internal error */, + OrthancPluginErrorCode_Success = 0 /*!< Success */, + OrthancPluginErrorCode_Plugin = 1 /*!< Error encountered within the plugin engine */, + OrthancPluginErrorCode_NotImplemented = 2 /*!< Not implemented yet */, + OrthancPluginErrorCode_ParameterOutOfRange = 3 /*!< Parameter out of range */, + OrthancPluginErrorCode_NotEnoughMemory = 4 /*!< The server hosting Orthanc is running out of memory */, + OrthancPluginErrorCode_BadParameterType = 5 /*!< Bad type for a parameter */, + OrthancPluginErrorCode_BadSequenceOfCalls = 6 /*!< Bad sequence of calls */, + OrthancPluginErrorCode_InexistentItem = 7 /*!< Accessing an inexistent item */, + OrthancPluginErrorCode_BadRequest = 8 /*!< Bad request */, + OrthancPluginErrorCode_NetworkProtocol = 9 /*!< Error in the network protocol */, + OrthancPluginErrorCode_SystemCommand = 10 /*!< Error while calling a system command */, + OrthancPluginErrorCode_Database = 11 /*!< Error with the database engine */, + OrthancPluginErrorCode_UriSyntax = 12 /*!< Badly formatted URI */, + OrthancPluginErrorCode_InexistentFile = 13 /*!< Inexistent file */, + OrthancPluginErrorCode_CannotWriteFile = 14 /*!< Cannot write to file */, + OrthancPluginErrorCode_BadFileFormat = 15 /*!< Bad file format */, + OrthancPluginErrorCode_Timeout = 16 /*!< Timeout */, + OrthancPluginErrorCode_UnknownResource = 17 /*!< Unknown resource */, + OrthancPluginErrorCode_IncompatibleDatabaseVersion = 18 /*!< Incompatible version of the database */, + OrthancPluginErrorCode_FullStorage = 19 /*!< The file storage is full */, + OrthancPluginErrorCode_CorruptedFile = 20 /*!< Corrupted file (e.g. inconsistent MD5 hash) */, + OrthancPluginErrorCode_InexistentTag = 21 /*!< Inexistent tag */, + OrthancPluginErrorCode_ReadOnly = 22 /*!< Cannot modify a read-only data structure */, + OrthancPluginErrorCode_IncompatibleImageFormat = 23 /*!< Incompatible format of the images */, + OrthancPluginErrorCode_IncompatibleImageSize = 24 /*!< Incompatible size of the images */, + OrthancPluginErrorCode_SharedLibrary = 25 /*!< Error while using a shared library (plugin) */, + OrthancPluginErrorCode_UnknownPluginService = 26 /*!< Plugin invoking an unknown service */, + OrthancPluginErrorCode_UnknownDicomTag = 27 /*!< Unknown DICOM tag */, + OrthancPluginErrorCode_BadJson = 28 /*!< Cannot parse a JSON document */, + OrthancPluginErrorCode_Unauthorized = 29 /*!< Bad credentials were provided to an HTTP request */, + OrthancPluginErrorCode_BadFont = 30 /*!< Badly formatted font file */, + OrthancPluginErrorCode_DatabasePlugin = 31 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */, + OrthancPluginErrorCode_StorageAreaPlugin = 32 /*!< Error in the plugin implementing a custom storage area */, + OrthancPluginErrorCode_EmptyRequest = 33 /*!< The request is empty */, + OrthancPluginErrorCode_NotAcceptable = 34 /*!< Cannot send a response which is acceptable according to the Accept HTTP header */, + OrthancPluginErrorCode_NullPointer = 35 /*!< Cannot handle a NULL pointer */, + OrthancPluginErrorCode_DatabaseUnavailable = 36 /*!< The database is currently not available (probably a transient situation) */, + OrthancPluginErrorCode_CanceledJob = 37 /*!< This job was canceled */, + OrthancPluginErrorCode_BadGeometry = 38 /*!< Geometry error encountered in Stone */, + OrthancPluginErrorCode_SslInitialization = 39 /*!< Cannot initialize SSL encryption, check out your certificates */, + OrthancPluginErrorCode_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_ForbiddenAccess = 45 /*!< Access to a resource is forbidden */, + OrthancPluginErrorCode_DuplicateResource = 46 /*!< Duplicate resource */, + OrthancPluginErrorCode_IncompatibleConfigurations = 47 /*!< Your configuration file contains configuration that are mutually incompatible */, + 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: Bind 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_DicomGetUnavailable = 2045 /*!< DicomUserConnection: The C-GET command is not supported by the remote 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; + + + /* -------------------------------------------------- + New in version 1.12.9 + -------------------------------------------------- */ + + /** + * @brief If a HTTP authentication callback is registered, the + * content of the custom payload generated by the callback. + **/ + const void* authenticationPayload; + + /** + * @brief The size of the custom authentication payload (0 if no + * authentication callback is registered). + **/ + uint32_t authenticationPayloadSize; + + } OrthancPluginHttpRequest; + + + typedef enum + { + /* 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 */ + _OrthancPluginService_SetMetricsIntegerValue = 43, /* New in Orthanc 1.12.1 */ + _OrthancPluginService_SetCurrentThreadName = 44, /* New in Orthanc 1.12.2 */ + _OrthancPluginService_LogMessage = 45, /* New in Orthanc 1.12.4 */ + _OrthancPluginService_AdoptDicomInstance = 46, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetAttachmentCustomData = 47, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_SetAttachmentCustomData = 48, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_StoreKeyValue = 49, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DeleteKeyValue = 50, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetKeyValue = 51, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_CreateKeysValuesIterator = 52, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_FreeKeysValuesIterator = 53, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorNext = 54, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetKey = 55, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_KeysValuesIteratorGetValue = 56, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_EnqueueValue = 57, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_DequeueValue = 58, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_GetQueueSize = 59, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_SetStableStatus = 60, /* New in Orthanc 1.12.9 */ + _OrthancPluginService_EmitAuditLog = 61, /* New in Orthanc 1.12.9 */ + + /* Registration of callbacks */ + _OrthancPluginService_RegisterRestCallback = 1000, + _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, /* New in Orthanc 1.6.0 */ + _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014, /* New in Orthanc 1.6.1 */ + _OrthancPluginService_RegisterTranscoderCallback = 1015, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_RegisterStorageArea2 = 1016, /* New in Orthanc 1.9.0 */ + _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017, /* New in Orthanc 1.10.0 */ + _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.10.0 */ + _OrthancPluginService_RegisterWebDavCollection = 1019, /* New in Orthanc 1.10.1 */ + _OrthancPluginService_RegisterStorageArea3 = 1020, /* New in Orthanc 1.12.8 */ + _OrthancPluginService_RegisterHttpAuthentication = 1021, /* New in Orthanc 1.12.9 */ + _OrthancPluginService_RegisterAuditLogHandler = 1022, /* New in Orthanc 1.12.9 */ + + /* Sending answers to REST calls */ + _OrthancPluginService_AnswerBuffer = 2000, + _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, + _OrthancPluginService_StartStreamAnswer = 2014, + _OrthancPluginService_SendStreamChunk = 2015, + + /* 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 */ + _OrthancPluginService_LoadDicomInstance = 4020, /* New in Orthanc 1.12.1 */ + + /* Services for plugins implementing a database back-end */ + _OrthancPluginService_RegisterDatabaseBackend = 5000, /* New in Orthanc 0.8.6 */ + _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 */ + _OrthancPluginService_RegisterDatabaseBackendV4 = 5007, /* New in Orthanc 1.12.0 */ + + /* 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, /* Deprecated since SDK 1.11.3 */ + _OrthancPluginService_FreeJob = 9001, + _OrthancPluginService_SubmitJob = 9002, + _OrthancPluginService_RegisterJobsUnserializer = 9003, + _OrthancPluginService_CreateJob2 = 9004, /* New in SDK 1.11.3 */ + + _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 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 7, + + /** + * @brief Graylevel, unsigned 32bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * four bytes. + **/ + OrthancPluginPixelFormat_Grayscale32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 8, + + /** + * @brief Graylevel, floating-point 32bpp image. + * + * The image is graylevel. Each pixel is floating-point and stored + * in four bytes. + **/ + OrthancPluginPixelFormat_Float32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 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 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 10, + + /** + * @brief Graylevel, unsigned 64bpp image. + * + * The image is graylevel. Each pixel is unsigned and stored in + * eight bytes. + **/ + OrthancPluginPixelFormat_Grayscale64 ORTHANC_PLUGIN_SINCE_SDK("1.4.0") = 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 ORTHANC_PLUGIN_SINCE_SDK("1.9.2") = 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. + * Note: This enumeration is not used to store changes in the database! + * @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 ORTHANC_PLUGIN_SINCE_SDK("1.4.2") = 14, /*!< The list of Orthanc peers has changed */ + OrthancPluginChangeType_UpdatedModalities ORTHANC_PLUGIN_SINCE_SDK("1.4.2") = 15, /*!< The list of DICOM modalities has changed */ + OrthancPluginChangeType_JobSubmitted ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 16, /*!< New Job submitted */ + OrthancPluginChangeType_JobSuccess ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 17, /*!< A Job has completed successfully */ + OrthancPluginChangeType_JobFailure ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 18, /*!< A Job has failed */ + + _OrthancPluginChangeType_INTERNAL = 0x7fffffff + } OrthancPluginChangeType; + + + /** + * 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_None ORTHANC_PLUGIN_SINCE_SDK("1.12.8") = 4, /*!< No compression (new in Orthanc 1.12.8) */ + + _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, /*!< Default formatting */ + 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 ORTHANC_PLUGIN_SINCE_SDK("1.9.1") = (1 << 6), /*!< Stop processing after pixel data (new in 1.9.1) */ + OrthancPluginDicomToJsonFlags_SkipGroupLengths ORTHANC_PLUGIN_SINCE_SDK("1.9.1") = (1 << 7), /*!< Skip tags whose element is zero (new in 1.9.1) */ + + _OrthancPluginDicomToJsonFlags_INTERNAL = 0x7fffffff + } OrthancPluginDicomToJsonFlags; + + + /** + * Flags for the creation of a DICOM file. + * @ingroup Toolbox + * @see OrthancPluginCreateDicom() + **/ + typedef enum + { + OrthancPluginCreateDicomFlags_None ORTHANC_PLUGIN_SINCE_SDK("1.2.0") = 0, /*!< Default mode */ + OrthancPluginCreateDicomFlags_DecodeDataUriScheme = (1 << 0), /*!< Decode fields encoded using data URI scheme */ + OrthancPluginCreateDicomFlags_GenerateIdentifiers = (1 << 1), /*!< Automatically generate DICOM identifiers */ + + _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 ORTHANC_PLUGIN_SINCE_SDK("1.8.0") = 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 ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + { + OrthancPluginJobStepStatus_Success = 1, /*!< The job has successfully executed all its steps */ + OrthancPluginJobStepStatus_Failure = 2, /*!< The job has failed while executing this step */ + 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 ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + { + OrthancPluginJobStopReason_Success = 1, /*!< The job has succeeded */ + OrthancPluginJobStopReason_Paused = 2, /*!< The job was paused, and will be resumed later */ + 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 ORTHANC_PLUGIN_SINCE_SDK("1.5.4") + { + 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 ORTHANC_PLUGIN_SINCE_SDK("1.5.4") + { + 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 ORTHANC_PLUGIN_SINCE_SDK("1.6.0") + { + /** + * Success: The DICOM instance is properly stored in the SCP + **/ + OrthancPluginStorageCommitmentFailureReason_Success = 0, + + /** + * 0110H: A general failure in processing the operation was encountered + **/ + OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1, + + /** + * 0112H: One or more of the elements in the Referenced SOP + * Instance Sequence was not available + **/ + OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2, + + /** + * 0213H: The SCP does not currently have enough resources to + * store the requested SOP Instance(s) + **/ + OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3, + + /** + * 0122H: Storage Commitment has been requested for a SOP Instance + * with a SOP Class that is not supported by the SCP + **/ + OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4, + + /** + * 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_ClassInstanceConflict = 5, + + /** + * 0131H: The Transaction UID of the Storage Commitment Request is + * already in use + **/ + OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6 + } OrthancPluginStorageCommitmentFailureReason; + + + /** + * The action to be taken after ReceivedInstanceCallback is triggered + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.10.0") + { + OrthancPluginReceivedInstanceAction_KeepAsIs = 1, /*!< Keep the instance as is */ + OrthancPluginReceivedInstanceAction_Modify = 2, /*!< Modify the instance */ + OrthancPluginReceivedInstanceAction_Discard = 3, /*!< Discard the instance */ + + _OrthancPluginReceivedInstanceAction_INTERNAL = 0x7fffffff + } OrthancPluginReceivedInstanceAction; + + + /** + * Mode specifying how to load a DICOM instance. + * @see OrthancPluginLoadDicomInstance() + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.1") + { + /** + * Load the whole DICOM file, including pixel data + **/ + OrthancPluginLoadDicomInstanceMode_WholeDicom = 1, + + /** + * Load the whole DICOM file until pixel data, which speeds up the + * loading + **/ + OrthancPluginLoadDicomInstanceMode_UntilPixelData = 2, + + /** + * Load the whole DICOM file until pixel data, and replace pixel + * data by an empty tag whose VR (value representation) is the + * same as those of the original DICOM file + **/ + OrthancPluginLoadDicomInstanceMode_EmptyPixelData = 3, + + _OrthancPluginLoadDicomInstanceMode_INTERNAL = 0x7fffffff + } OrthancPluginLoadDicomInstanceMode; + + + /** + * The log levels supported by Orthanc. + * + * These values must match those of enumeration "LogLevel" in the + * Orthanc Core. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.4") + { + OrthancPluginLogLevel_Error = 0, /*!< Error log level */ + OrthancPluginLogLevel_Warning = 1, /*!< Warning log level */ + OrthancPluginLogLevel_Info = 2, /*!< Info log level */ + OrthancPluginLogLevel_Trace = 3, /*!< Trace log level */ + + _OrthancPluginLogLevel_INTERNAL = 0x7fffffff + } OrthancPluginLogLevel; + + + /** + * The log categories supported by Orthanc. + * + * These values must match those of enumeration "LogCategory" in the + * Orthanc Core. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.4") + { + OrthancPluginLogCategory_Generic = (1 << 0), /*!< Generic (default) category */ + OrthancPluginLogCategory_Plugins = (1 << 1), /*!< Plugin engine related logs (shall not be used by plugins) */ + OrthancPluginLogCategory_Http = (1 << 2), /*!< HTTP related logs */ + OrthancPluginLogCategory_Sqlite = (1 << 3), /*!< SQLite related logs (shall not be used by plugins) */ + OrthancPluginLogCategory_Dicom = (1 << 4), /*!< DICOM related logs */ + OrthancPluginLogCategory_Jobs = (1 << 5), /*!< jobs related logs */ + OrthancPluginLogCategory_Lua = (1 << 6), /*!< Lua related logs (shall not be used by plugins) */ + + _OrthancPluginLogCategory_INTERNAL = 0x7fffffff + } OrthancPluginLogCategory; + + + /** + * The store status related to the adoption of a DICOM instance. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + { + OrthancPluginStoreStatus_Success = 0, /*!< The file has been stored/adopted */ + OrthancPluginStoreStatus_AlreadyStored = 1, /*!< The file has already been stored/adopted (only if OverwriteInstances is set to false)*/ + OrthancPluginStoreStatus_Failure = 2, /*!< The file could not be stored/adopted */ + OrthancPluginStoreStatus_FilteredOut = 3, /*!< The file has been filtered out by a Lua script or a plugin */ + OrthancPluginStoreStatus_StorageFull = 4, /*!< The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)*/ + + _OrthancPluginStoreStatus_INTERNAL = 0x7fffffff + } OrthancPluginStoreStatus; + + + /** + * The supported modes to remove an element from a queue. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + { + OrthancPluginQueueOrigin_Front = 0, /*!< Dequeue from the front of the queue */ + OrthancPluginQueueOrigin_Back = 1, /*!< Dequeue from the back of the queue */ + + _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff + } OrthancPluginQueueOrigin; + + + /** + * The "Stable" status of a resource. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + { + OrthancPluginStableStatus_Stable = 0, /*!< The resource is stable */ + OrthancPluginStableStatus_Unstable = 1, /*!< The resource is unstable */ + + _OrthancPluginStableStatus_INTERNAL = 0x7fffffff + } OrthancPluginStableStatus; + + + /** + * Status associated with the authentication of a HTTP request. + **/ + typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + { + OrthancPluginHttpAuthenticationStatus_Granted = 0, /*!< The authentication has been granted */ + OrthancPluginHttpAuthenticationStatus_Unauthorized = 1, /*!< The authentication has failed (401 HTTP status) */ + OrthancPluginHttpAuthenticationStatus_Forbidden = 2, /*!< The authorization has failed (403 HTTP status) */ + OrthancPluginHttpAuthenticationStatus_Redirect = 3, /*!< Redirect to another path (307 HTTP status, e.g., for login) */ + + _OrthancPluginHttpAuthenticationStatus_INTERNAL = 0x7fffffff + } OrthancPluginHttpAuthenticationStatus; + + + /** + * @brief A 32-bit memory buffer allocated by the core system of Orthanc. + * + * A memory buffer allocated by the core system of Orthanc. When the + * 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(). + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.9.0") 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 ORTHANC_PLUGIN_SINCE_SDK("1.1.0") + _OrthancPluginFindQuery_t OrthancPluginFindQuery; + + + + /** + * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists. + * @ingroup DicomCallbacks + **/ + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.1.0") + _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 ORTHANC_PLUGIN_SINCE_SDK("1.2.0") + _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 ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + _OrthancPluginPeers_t OrthancPluginPeers; + + + + /** + * @brief Opaque structure to a job to be executed by Orthanc. + * @ingroup Toolbox + **/ + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + _OrthancPluginJob_t OrthancPluginJob; + + + + /** + * @brief Opaque structure that represents a node in a JSON or XML + * document used in DICOMweb. + * @ingroup Toolbox + **/ + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.5.4") + _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode; + + + + /** + * @brief Signature of a callback function that answers to a REST request. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginRestCallback) ( + OrthancPluginRestOutput* output, + const char* uri, + const OrthancPluginHttpRequest* request); + + + + /** + * @brief Signature of a callback function that is triggered when Orthanc stores a new DICOM instance. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginOnStoredInstanceCallback) ( + const OrthancPluginDicomInstance* instance, + const char* instanceId); + + + + /** + * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginOnChangeCallback) ( + OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId); + + + + /** + * @brief Signature of a callback function to decode a DICOM instance as an image. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginDecodeImageCallback) ( + OrthancPluginImage** target, + const void* dicom, + const uint32_t size, + uint32_t frameIndex); + + + + /** + * @brief Signature of a function to free dynamic memory. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginFree) (void* buffer); + + + + /** + * @brief Signature of a function to set the content of a node + * encoding a binary DICOM tag, into a JSON or XML document + * generated for DICOMweb. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginDicomWebSetBinaryNode) ( + OrthancPluginDicomWebNode* node, + OrthancPluginDicomWebBinaryMode mode, + const char* bulkDataUri); + + + + /** + * @brief Callback for writing to the storage area. + * + * Signature of a callback function that is triggered when Orthanc writes a file to the storage area. + * + * @param uuid The UUID of the file. + * @param content The content of the file. + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate) ( + const char* uuid, + const void* content, + int64_t size, + OrthancPluginContentType type); + + + + /** + * @brief Callback for reading from the storage area. + * + * Signature of a callback function that is triggered when Orthanc reads a file from the storage area. + * + * @param content The content of the file (output). + * @param size The size of the file (output). + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @return 0 if success, other value if error. + * @ingroup Callbacks + * + * @warning The "content" buffer *must* have been allocated using + * the "malloc()" function of your C standard library (i.e. nor + * "new[]", neither a pointer to a buffer). The "free()" function of + * your C standard library will automatically be invoked on the + * "content" pointer. + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRead) ( + void** content, + int64_t* size, + const char* uuid, + OrthancPluginContentType type); + + + + /** + * @brief Callback for 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 for writing to the storage area. + * + * Signature of a callback function that is triggered when Orthanc writes a file to the storage area. + * + * @param customData Custom, plugin-specific data associated with the attachment (out). + * It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer(). The core of Orthanc will free it. + * If the plugin does not generate custom data, leave `customData` unchanged; it will default to an empty value. + * @param uuid The UUID of the file. + * @param content The content of the file (might be compressed data). + * @param size The size of the file. + * @param type The content type corresponding to this file. + * @param compressionType The compression algorithm that was used to encode `content` + * (the absence of compression is indicated using `OrthancPluginCompressionType_None`). + * @param dicomInstance The DICOM instance being stored. Equals `NULL` if not storing a DICOM instance. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate2) ( + OrthancPluginMemoryBuffer* customData, + const char* uuid, + const void* content, + uint64_t size, + OrthancPluginContentType type, + OrthancPluginCompressionType compressionType, + const OrthancPluginDicomInstance* dicomInstance); + + + + /** + * @brief Callback for reading a range of a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * reads a portion of a file from the storage area. Orthanc + * indicates the start position and the length of the range. + * + * @param target Memory buffer where to store the content of the range. + * The memory buffer is allocated and freed by Orthanc. The length of the range + * of interest corresponds to the size of this buffer. + * @param uuid The UUID of the file of interest. + * @param type The content type corresponding to this file. + * @param rangeStart Start position of the requested range in the file. + * @param customData The custom data of the file of interest. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) ( + OrthancPluginMemoryBuffer64* target, + const char* uuid, + OrthancPluginContentType type, + uint64_t rangeStart, + const void* customData, + uint32_t customDataSize); + + + + /** + * @brief Callback for removing a file from the storage area. + * + * Signature of a callback function that is triggered when Orthanc + * deletes a file from the storage area. + * + * @param uuid The UUID of the file to be removed. + * @param type The content type corresponding to this file. + * @param customData The custom data of the file to be removed. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) ( + const char* uuid, + OrthancPluginContentType type, + const void* customData, + uint32_t customDataSize); + + + /** + * @brief Callback to handle the C-Find SCP requests for worklists. + * + * Signature of a callback function that is triggered when Orthanc + * 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. + * + * Note that if you are using HTTP basic authentication, you can + * extract the username from the "Authorization" HTTP header. The + * value of that header contains username:pwd encoded in base64. + * + * @param method The HTTP method used by the request. + * @param uri The URI of interest. + * @param ip The IP address of the HTTP client. + * @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 + * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3. + **/ + typedef const char* (*OrthancPluginJobGetContent) (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 target The target memory buffer where to store the JSON string. + * This buffer must be allocated using OrthancPluginCreateMemoryBuffer() + * and will be freed by the Orthanc core. + * @param job The job of interest. + * @return 0 if success, other value if error. + * @ingroup Toolbox + **/ + typedef OrthancPluginErrorCode (*OrthancPluginJobGetContent2) (OrthancPluginMemoryBuffer* target, + 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 + * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3. + **/ + typedef const char* (*OrthancPluginJobGetSerialized) (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 target The target memory buffer where to store the JSON string. + * This buffer must be allocated using OrthancPluginCreateMemoryBuffer() + * and will be freed by the Orthanc core. + * @param job The job of interest. + * @return 1 if the serialization has succeeded, 0 if serialization is + * not implemented for this type of job, or -1 in case of error. + **/ + typedef int32_t (*OrthancPluginJobGetSerialized2) (OrthancPluginMemoryBuffer* target, + 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() or + * OrthancPluginSetMetricsIntegerValue(). + * 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_SINCE_SDK("1.4.0") + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginCheckVersionAdvanced( + OrthancPluginContext* context, + int32_t expectedMajor, + int32_t expectedMinor, + int32_t expectedRevision) + { + int32_t major, minor, revision; + + if (sizeof(int) != sizeof(int32_t) || /* Ensure binary compatibility with Orthanc SDK <= 1.12.1 */ + 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(OrthancPluginJobStopReason) || + sizeof(int32_t) != sizeof(OrthancPluginConstraintType) || + sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || + sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || + sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || + sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction) || + sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) || + sizeof(int32_t) != sizeof(OrthancPluginLogLevel) || + sizeof(int32_t) != sizeof(OrthancPluginLogCategory) || + sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) || + sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) || + sizeof(int32_t) != sizeof(OrthancPluginStableStatus) || + sizeof(int32_t) != sizeof(OrthancPluginHttpAuthenticationStatus)) + { + /* Mismatch in the size of the enumerations */ + return 0; + } + + /* 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 int32_t 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_SINCE_SDK("1.9.0") + 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 int32_t 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 New plugins should use OrthancPluginRegisterStorageArea3() + **/ + ORTHANC_PLUGIN_DEPRECATED 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_DEPRECATED 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. + * + * @deprecated This function should not be used anymore because the + * result of the call to "OrthancPluginGetName()" depends on the + * system. Use "OrthancPluginSetRootUri2()" instead. + **/ + ORTHANC_PLUGIN_DEPRECATED 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 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 plugin Identifier of your plugin (it must match "OrthancPluginGetName()"). + * @param uri The root URI for this plugin. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri2( + OrthancPluginContext* context, + const char* plugin, + const char* uri) + { + _OrthancPluginSetPluginProperty params; + params.plugin = plugin; + 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. + * + * @deprecated This function should not be used anymore because the + * result of the call to "OrthancPluginGetName()" depends on the + * system. Use "OrthancPluginSetDescription2()" instead. + **/ + ORTHANC_PLUGIN_DEPRECATED 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 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 plugin Identifier of your plugin (it must match "OrthancPluginGetName()"). + * @param description The description. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription2( + OrthancPluginContext* context, + const char* plugin, + const char* description) + { + _OrthancPluginSetPluginProperty params; + params.plugin = plugin; + 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. + * + * @deprecated This function should not be used anymore because the + * result of the call to "OrthancPluginGetName()" depends on the + * system. Use "OrthancPluginExtendOrthancExplorer2()" instead. + **/ + ORTHANC_PLUGIN_DEPRECATED 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); + } + + + /** + * @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 plugin Identifier of your plugin (it must match "OrthancPluginGetName()"). + * @param javascript The custom JavaScript code. + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer2( + OrthancPluginContext* context, + const char* plugin, + const char* javascript) + { + _OrthancPluginSetPluginProperty params; + params.plugin = plugin; + 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 <tt>NULL</tt> if no password protection). + * @param password The password (can be <tt>NULL</tt> if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpGet( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* url, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶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 <tt>NULL</tt> if no password protection). + * @param password The password (can be <tt>NULL</tt> if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpPost( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* url, + const void* body, + uint32_t bodySize, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶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 <tt>NULL</tt> if no password protection). + * @param password The password (can be <tt>NULL</tt> if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpPut( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* url, + const void* body, + uint32_t bodySize, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶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 <tt>NULL</tt> if no password protection). + * @param password The password (can be <tt>NULL</tt> if no password protection). + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginHttpDelete( + OrthancPluginContext* context, + const char* url, + const char* username, + const char* password) + { + _OrthancPluginCallHttpClient params; + memset(¶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. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. + **/ + ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaCreate( + OrthancPluginContext* context, + 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. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. + **/ + ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRead( + OrthancPluginContext* context, + 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. + * @warning This function will result in a "not implemented" error on versions of the + * Orthanc core above 1.12.6. + **/ + ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStorageAreaRemove( + OrthancPluginContext* context, + 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 ("<tt>n</tt>"). + * @return 0 if success, other value if error. + * @see OrthancPluginRegisterPrivateDictionaryTag() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDictionaryTag( + OrthancPluginContext* context, + uint16_t group, + uint16_t element, + OrthancPluginValueRepresentation vr, + const char* name, + uint32_t minMultiplicity, + uint32_t maxMultiplicity) + { + _OrthancPluginRegisterDictionaryTag params; + params.group = group; + params.element = element; + params.vr = vr; + params.name = name; + params.minMultiplicity = minMultiplicity; + params.maxMultiplicity = maxMultiplicity; + + return context->InvokeService(context, _OrthancPluginService_RegisterDictionaryTag, ¶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 ("<tt>n</tt>"). + * @param privateCreator The private creator of this private tag. + * @return 0 if success, other value if error. + * @see OrthancPluginRegisterDictionaryTag() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.2.0") + 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 <tt>NULL</tt> if no header). + * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header). + * @param afterPlugins If 0, the built-in API of Orthanc is used. + * If 1, the API is tainted by the plugins. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginRestApiGet(), OrthancPluginRestApiGetAfterPlugins() + * @ingroup Orthanc + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRestApiGet2( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* uri, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + int32_t afterPlugins) + { + _OrthancPluginRestApiGet2 params; + params.target = target; + params.uri = uri; + params.headersCount = headersCount; + params.headersKeys = headersKeys; + params.headersValues = headersValues; + params.afterPlugins = afterPlugins; + + return context->InvokeService(context, _OrthancPluginService_RestApiGet2, ¶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_SINCE_SDK("1.0.0") + 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_SINCE_SDK("1.1.0") + ORTHANC_PLUGIN_DEPRECATED 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 <tt>NULL</tt> if no header). + * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header). + * @param username The username (can be <tt>NULL</tt> if no password protection). + * @param password The password (can be <tt>NULL</tt> if no password protection). + * @param body The HTTP body for a POST or PUT request. + * @param bodySize The size of the body. + * @param timeout Timeout in seconds (0 for default timeout). + * @param certificateFile Path to the client certificate for HTTPS, in PEM format + * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS). + * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format + * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS). + * @param certificateKeyPassword Password to unlock the key of the client certificate + * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS). + * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginCallPeerApi() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.1.0") + 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_SINCE_SDK("1.2.0") + 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_SINCE_SDK("1.2.0") + 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_SINCE_SDK("1.2.0") + 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_SINCE_SDK("1.3.0") + 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_SINCE_SDK("1.4.2") + 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_SINCE_SDK("1.4.2") + 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_SINCE_SDK("1.4.2") + 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_SINCE_SDK("1.4.2") + 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_SINCE_SDK("1.4.2") + 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_SINCE_SDK("1.4.2") + 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 <tt>NULL</tt> if no header). + * @param additionalHeadersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header). + * @param body The HTTP body for a POST or PUT request. + * @param bodySize The size of the body. + * @param timeout Timeout in seconds (0 for default timeout). + * @return 0 if success, or the error code if failure. + * @see OrthancPluginHttpClient() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + 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 + * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.4.2") + ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob( + OrthancPluginContext *context, + void *job, + 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** target; + void *job; + OrthancPluginJobFinalize finalize; + const char *type; + OrthancPluginJobGetProgress getProgress; + OrthancPluginJobGetContent2 getContent; + OrthancPluginJobGetSerialized2 getSerialized; + OrthancPluginJobStep step; + OrthancPluginJobStop stop; + OrthancPluginJobReset reset; + } _OrthancPluginCreateJob2; + + /** + * @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_SINCE_SDK("1.11.3") + ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob2( + OrthancPluginContext *context, + void *job, + OrthancPluginJobFinalize finalize, + const char *type, + OrthancPluginJobGetProgress getProgress, + OrthancPluginJobGetContent2 getContent, + OrthancPluginJobGetSerialized2 getSerialized, + OrthancPluginJobStep step, + OrthancPluginJobStop stop, + OrthancPluginJobReset reset) + { + OrthancPluginJob* target = NULL; + + _OrthancPluginCreateJob2 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_CreateJob2, ¶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_SINCE_SDK("1.4.2") + 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; + int32_t 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_SINCE_SDK("1.4.2") + ORTHANC_PLUGIN_INLINE char *OrthancPluginSubmitJob( + OrthancPluginContext *context, + OrthancPluginJob *job, + int32_t 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_SINCE_SDK("1.4.2") + 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_SINCE_SDK("1.5.0") + 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_SINCE_SDK("1.5.0") + 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 floating-point metrics. + * + * This function sets the value of a floating-point metrics to + * monitor the behavior of the plugin through tools such as + * Prometheus. The values of all the metrics are stored within the + * Orthanc context. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param name The name of the metrics to be set. + * @param value The value of the metrics. + * @param type The type of the metrics. This parameter is only taken into consideration + * the first time this metrics is set. + * @ingroup Toolbox + * @see OrthancPluginSetMetricsIntegerValue() + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.4") + 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() or + * OrthancPluginSetMetricsIntegerValue(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback function to handle the refresh. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.4") + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRefreshMetricsCallback( + OrthancPluginContext* context, + OrthancPluginRefreshMetricsCallback callback) + { + _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_SINCE_SDK("1.5.4") + ORTHANC_PLUGIN_DEPRECATED 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_SINCE_SDK("1.5.4") + ORTHANC_PLUGIN_DEPRECATED 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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 <tt>NULL</tt> if no header). + * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header). + * @param request The user payload containing the request body, and acting as an iterator. + * It will be provided to the callbacks for the request. + * @param requestIsDone Callback function to tell whether the request body is entirely read. + * @param requestChunkData Callback function to get the content of the current data chunk of the request body. + * @param requestChunkSize Callback function to get the size of the current data chunk of the request body. + * @param requestNext Callback function to advance to the next data chunk of the request body. + * @param username The username (can be <tt>NULL</tt> if no password protection). + * @param password The password (can be <tt>NULL</tt> if no password protection). + * @param timeout Timeout in seconds (0 for default timeout). + * @param certificateFile Path to the client certificate for HTTPS, in PEM format + * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS). + * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format + * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS). + * @param certificateKeyPassword Password to unlock the key of the client certificate + * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS). + * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginHttpClient() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.5.7") + 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 ORTHANC_PLUGIN_SINCE_SDK("1.5.7") + _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 uri The URI that is accessed. + * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used. + * @return 0 if success, or the error code if failure. + **/ + typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) ( + OrthancPluginServerChunkedRequestReader** reader, + const char* uri, + 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_SINCE_SDK("1.5.7") + 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_SINCE_SDK("1.5.7") + 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_SINCE_SDK("1.6.0") + 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_SINCE_SDK("1.6.1") + 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_SINCE_SDK("1.10.0") + 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,<br/> + * `OrthancPluginReceivedInstanceAction_Modify` to store the modified DICOM contained in `modifiedDicomBuffer`,<br/> + * `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_SINCE_SDK("1.10.0") + 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_SINCE_SDK("1.6.1") + 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_SINCE_SDK("1.6.1") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.7.0") + 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_SINCE_SDK("1.8.1") + 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_SINCE_SDK("1.9.0") + 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 + * @deprecated New plugins should use OrthancPluginRegisterStorageArea3() + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.9.0") + 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_SINCE_SDK("1.9.0") + 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 <tt>NULL</tt> if no header). + * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header). + * @param 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_SINCE_SDK("1.9.2") + 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 ORTHANC_PLUGIN_SINCE_SDK("1.10.1") + _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_SINCE_SDK("1.10.1") + 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_SINCE_SDK("1.11.1") + 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; + } + } + + + typedef struct + { + OrthancPluginStorageCreate2 create; + OrthancPluginStorageReadRange2 readRange; + OrthancPluginStorageRemove2 remove; + } _OrthancPluginRegisterStorageArea3; + + /** + * @brief Register a custom storage area, with support for custom data. + * + * This function registers a custom storage area, to replace the + * built-in way Orthanc stores its files on the filesystem. This + * function must be called during the initialization of the plugin, + * i.e. inside the OrthancPluginInitialize() public function. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param create The callback function to store a file on the custom storage area. + * @param readRange The callback function to read some range of a file from the custom storage area. + * @param remove The callback function to remove a file from the custom storage area. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3( + OrthancPluginContext* context, + OrthancPluginStorageCreate2 create, + OrthancPluginStorageReadRange2 readRange, + OrthancPluginStorageRemove2 remove) + { + _OrthancPluginRegisterStorageArea3 params; + params.create = create; + params.readRange = readRange; + params.remove = remove; + context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, ¶ms); + } + + /** + * @brief Signature of a callback function that is triggered when + * the Orthanc core requests an operation from the database plugin. + * Both request and response are encoded as protobuf buffers. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginCallDatabaseBackendV4) ( + OrthancPluginMemoryBuffer64* response, + void* backend, + const void* request, + uint64_t requestSize); + + /** + * @brief Signature of a callback function that is triggered when + * the database plugin must be finalized. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginFinalizeDatabaseBackendV4) (void* backend); + + typedef struct + { + void* backend; + uint32_t maxDatabaseRetries; + OrthancPluginCallDatabaseBackendV4 operations; + OrthancPluginFinalizeDatabaseBackendV4 finalize; + } _OrthancPluginRegisterDatabaseBackendV4; + + /** + * @brief Register a custom database back-end. + * + * This function was added in Orthanc SDK 1.12.0. It uses Google + * Protocol Buffers for the communications between the Orthanc core + * and database plugins. Check out "OrthancDatabasePlugin.proto" for + * the definition of the protobuf messages. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param backend Pointer to the custom database backend. + * @param maxDatabaseRetries Maximum number of retries if transaction doesn't succeed. + * If no retry is successful, OrthancPluginErrorCode_DatabaseCannotSerialize is generated. + * @param operations Access to the operations of the custom database backend. + * @param finalize Callback to deallocate the custom database backend. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.0") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV4( + OrthancPluginContext* context, + void* backend, + uint32_t maxDatabaseRetries, + OrthancPluginCallDatabaseBackendV4 operations, + OrthancPluginFinalizeDatabaseBackendV4 finalize) + { + _OrthancPluginRegisterDatabaseBackendV4 params; + params.backend = backend; + params.maxDatabaseRetries = maxDatabaseRetries; + params.operations = operations; + params.finalize = finalize; + + return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, ¶ms); + } + + + typedef struct + { + OrthancPluginDicomInstance** target; + const char* instanceId; + OrthancPluginLoadDicomInstanceMode mode; + } _OrthancPluginLoadDicomInstance; + + /** + * @brief Load a DICOM instance from the Orthanc server. + * + * This function loads a DICOM instance from the content of the + * Orthanc database. The function returns a new pointer to a data + * structure that is managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The Orthanc identifier of the DICOM instance of interest. + * @param mode Flag specifying how to deal with pixel data. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.1") + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginLoadDicomInstance( + OrthancPluginContext* context, + const char* instanceId, + OrthancPluginLoadDicomInstanceMode mode) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginLoadDicomInstance params; + params.target = ⌖ + params.instanceId = instanceId; + params.mode = mode; + + if (context->InvokeService(context, _OrthancPluginService_LoadDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + const char* name; + int64_t value; + OrthancPluginMetricsType type; + } _OrthancPluginSetMetricsIntegerValue; + + /** + * @brief Set the value of an integer metrics. + * + * This function sets the value of an integer metrics to monitor the + * behavior of the plugin through tools such as Prometheus. The + * values of all the metrics are stored within the Orthanc context. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param name The name of the metrics to be set. + * @param value The value of the metrics. + * @param type The type of the metrics. This parameter is only taken into consideration + * the first time this metrics is set. + * @ingroup Toolbox + * @see OrthancPluginSetMetricsValue() + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.1") + ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsIntegerValue( + OrthancPluginContext* context, + const char* name, + int64_t value, + OrthancPluginMetricsType type) + { + _OrthancPluginSetMetricsIntegerValue params; + params.name = name; + params.value = value; + params.type = type; + context->InvokeService(context, _OrthancPluginService_SetMetricsIntegerValue, ¶ms); + } + + + /** + * @brief Set the name of the current thread. + * + * This function gives a name to the thread that is calling this + * function. This name is used in the Orthanc logs. This function + * must only be called from threads that the plugin has created + * itself. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param threadName The name of the current thread. A thread name cannot be longer than 16 characters. + * @return 0 if success, other value if error. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.2") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetCurrentThreadName( + OrthancPluginContext* context, + const char* threadName) + { + return context->InvokeService(context, _OrthancPluginService_SetCurrentThreadName, threadName); + } + + + typedef struct + { + /* Note: This structure is also defined in Logging.h and it must be binary compatible */ + const char* message; + const char* plugin; + const char* file; + uint32_t line; + OrthancPluginLogCategory category; + OrthancPluginLogLevel level; + } _OrthancPluginLogMessage; + + + /** + * @brief Log a message. + * + * Log a message using the Orthanc logging system. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param message The message to be logged. + * @param plugin The plugin name. + * @param file The filename in the plugin code. + * @param line The file line in the plugin code. + * @param category The category. + * @param level The level of the message. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.4") + ORTHANC_PLUGIN_INLINE void OrthancPluginLogMessage( + OrthancPluginContext* context, + const char* message, + const char* plugin, + const char* file, + uint32_t line, + OrthancPluginLogCategory category, + OrthancPluginLogLevel level) + { + _OrthancPluginLogMessage m; + m.message = message; + m.plugin = plugin; + m.file = file; + m.line = line; + m.category = category; + m.level = level; + context->InvokeService(context, _OrthancPluginService_LogMessage, &m); + } + + + typedef struct + { + OrthancPluginRestOutput* output; + const char* contentType; + } _OrthancPluginStartStreamAnswer; + + /** + * @brief Start an HTTP stream answer. + * + * Initiates an HTTP stream 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 contentType The MIME type of the items in the stream answer. + * @return 0 if success, or the error code if failure. + * @see OrthancPluginSendStreamChunk() + * @ingroup REST + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.6") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartStreamAnswer( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* contentType) + { + _OrthancPluginStartStreamAnswer params; + params.output = output; + params.contentType = contentType; + return context->InvokeService(context, _OrthancPluginService_StartStreamAnswer, ¶ms); + } + + + /** + * @brief Send a chunk as a part of an HTTP stream answer. + * + * This function sends a chunk as part of an HTTP stream + * answer that was initiated by OrthancPluginStartStreamAnswer(). + * + * @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 OrthancPluginStartStreamAnswer() + * @ingroup REST + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.6") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendStreamChunk( + 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_SendStreamChunk, ¶ms); + } + + + typedef struct + { + OrthancPluginMemoryBuffer* instanceId; + OrthancPluginMemoryBuffer* attachmentUuid; + OrthancPluginStoreStatus* storeStatus; + const void* dicom; + uint64_t dicomSize; + const void* customData; + uint32_t customDataSize; + } _OrthancPluginAdoptDicomInstance; + + /** + * @brief Adopt a DICOM instance read from the filesystem. + * + * This function requests Orthanc to create a DICOM resource at the + * "Instance" level in its database, using the content of a DICOM + * instance read from the filesystem. The newly created DICOM + * resource is associated with an attachment whose content type is + * "OrthancPluginContentType_Dicom". The attachment is associated + * with the provided custom data. + * + * This function should only be used in combination with a custom + * storage area featuring support for custom data (i.e., installed + * using OrthancPluginRegisterStorageArea3()). The custom storage + * area is responsible for *not* duplicating the DICOM file into the + * storage area of Orthanc, hence the name "Adopt". The support for + * custom data is necessary for the custom storage area to + * distinguish between adopted and non-adopted DICOM instances. + * + * Check out the "AdoptDicomInstance" plugin in the source + * distribution of Orthanc for a working sample: + * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Plugins/Samples/AdoptDicomInstance/ + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The target memory buffer that will be filled by + * the Orthanc core with the public identifier of the newly created + * instance. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param attachmentUuid The target memory buffer that will be + * filled by the Orthanc core with the UUID of the newly created + * attachment corresponding to the adopted DICOM instance. It must + * be freed with OrthancPluginFreeMemoryBuffer(). + * @param storeStatus Variable that will be filled by the Orthanc core + * with the status of store operation. + * @param dicom Pointer to the DICOM instance read from the filesystem. + * @param dicomSize Size of the DICOM instance. + * @param customData The custom data to associated with the attachment. + * @param customDataSize The size of the custom data. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginAdoptDicomInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* instanceId, /* out */ + OrthancPluginMemoryBuffer* attachmentUuid, /* out */ + OrthancPluginStoreStatus* storeStatus, /* out */ + const void* dicom, + uint64_t dicomSize, + const void* customData, + uint32_t customDataSize) + { + _OrthancPluginAdoptDicomInstance params; + params.instanceId = instanceId; + params.attachmentUuid = attachmentUuid; + params.storeStatus = storeStatus; + params.dicom = dicom; + params.dicomSize = dicomSize; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_AdoptDicomInstance, ¶ms); + } + + + typedef struct + { + OrthancPluginMemoryBuffer* customData; + const char* attachmentUuid; + } _OrthancPluginGetAttachmentCustomData; + + /** + * @brief Retrieve the custom data associated with an attachment in the Orthanc database. + * + * If no custom data is associated with the attachment of interest, + * the target memory buffer is filled with the NULL value and a zero size. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param customData Memory buffer where to store the retrieved value. It must be freed + * by the plugin by calling OrthancPluginFreeMemoryBuffer(). + * @param attachmentUuid The UUID of the attachment of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetAttachmentCustomData( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* customData, /* out */ + const char* attachmentUuid /* in */) + { + _OrthancPluginGetAttachmentCustomData params; + params.customData = customData; + params.attachmentUuid = attachmentUuid; + + return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* attachmentUuid; + const void* customData; + uint32_t customDataSize; + } _OrthancPluginSetAttachmentCustomData; + + /** + * @brief Update the custom data associated with an attachment in the Orthanc database. + * + * This function is notably used in the "orthanc-advanced-storage" + * when the plugin moves an attachment. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param attachmentUuid The UUID of the attachment of interest. + * @param customData The value to store. + * @param customDataSize The size of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetAttachmentCustomData( + OrthancPluginContext* context, + const char* attachmentUuid, /* in */ + const void* customData, /* in */ + uint32_t customDataSize /* in */) + { + _OrthancPluginSetAttachmentCustomData params; + params.attachmentUuid = attachmentUuid; + params.customData = customData; + params.customDataSize = customDataSize; + + return context->InvokeService(context, _OrthancPluginService_SetAttachmentCustomData, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + const void* value; + uint32_t valueSize; + } _OrthancPluginStoreKeyValue; + + /** + * @brief Store a key-value pair in the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @param value The value to store. + * @param valueSize The length of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginStoreKeyValue params; + params.storeId = storeId; + params.key = key; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, ¶ms); + } + + + typedef struct + { + const char* storeId; + const char* key; + } _OrthancPluginDeleteKeyValue; + + /** + * @brief Delete a key-value pair from the Orthanc database. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue( + OrthancPluginContext* context, + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginDeleteKeyValue params; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, ¶ms); + } + + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* storeId; + const char* key; + } _OrthancPluginGetKeyValue; + + /** + * @brief Get the value associated with a key in the Orthanc key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. the key exists in the key-value store. + * @param target Memory buffer where to store the retrieved value. It must be freed + * by the plugin by calling OrthancPluginFreeMemoryBuffer(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @param key The key of the value to retrieve from the store (note: storeId + key must be unique). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* storeId, /* in */ + const char* key /* in */) + { + _OrthancPluginGetKeyValue params; + params.found = found; + params.target = target; + params.storeId = storeId; + params.key = key; + + return context->InvokeService(context, _OrthancPluginService_GetKeyValue, ¶ms); + } + + + /** + * @brief Opaque structure that represents an iterator over the keys and values of + * a key-value store. + * @ingroup Callbacks + **/ + typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator; + + + typedef struct + { + OrthancPluginKeysValuesIterator** target; + const char* storeId; + } _OrthancPluginCreateKeysValuesIterator; + + + /** + * @brief Create an iterator over the key-value pairs of a key-value store in the Orthanc database. + * + * The iterator loops over the keys according to the lexicographical order. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param storeId A unique identifier identifying both the plugin and the key-value store. + * @return The newly allocated iterator, or NULL in the case of an error. + * The iterator must be freed by calling OrthancPluginFreeKeysValuesIterator(). + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginKeysValuesIterator* OrthancPluginCreateKeysValuesIterator( + OrthancPluginContext* context, + const char* storeId) + { + OrthancPluginKeysValuesIterator* target = NULL; + + _OrthancPluginCreateKeysValuesIterator params; + params.target = ⌖ + params.storeId = storeId; + + if (context->InvokeService(context, _OrthancPluginService_CreateKeysValuesIterator, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + typedef struct + { + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginFreeKeysValuesIterator; + + /** + * @brief Free an iterator over a key-value store. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeKeysValuesIterator( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + _OrthancPluginFreeKeysValuesIterator params; + params.iterator = iterator; + + context->InvokeService(context, _OrthancPluginService_FreeKeysValuesIterator, ¶ms); + } + + + typedef struct + { + uint8_t* done; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorNext; + + /** + * @brief Read the next element of an iterator over a key-value store. + * + * The iterator loops over the keys according to the lexicographical order. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param done Pointer to a Boolean that is set to "true" iff. the iterator has reached the end of the store. + * @param iterator The iterator of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorNext( + OrthancPluginContext* context, + uint8_t* done, /* out */ + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorNext params; + params.done = done; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorNext, ¶ms); + } + + + typedef struct + { + const char** target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetKey; + + /** + * @brief Get the current key of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param iterator The iterator of interest. + * @return The current key, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetKey( + OrthancPluginContext* context, + OrthancPluginKeysValuesIterator* iterator) + { + const char* target = NULL; + + _OrthancPluginKeysValuesIteratorGetKey params; + params.target = ⌖ + params.iterator = iterator; + + if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetKey, ¶ms) == OrthancPluginErrorCode_Success) + { + return target; + } + else + { + return NULL; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + OrthancPluginKeysValuesIterator* iterator; + } _OrthancPluginKeysValuesIteratorGetValue; + + /** + * @brief Get the current value of an iterator over a key-value store. + * + * Before using this function, the function OrthancPluginKeysValuesIteratorNext() + * must have been called at least once. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Memory buffer where to store the value that has been retrieved from the key-value store. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param iterator The iterator of interest. + * @return The current value, or NULL in the case of an error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorGetValue( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target /* out */, + OrthancPluginKeysValuesIterator* iterator /* in */) + { + _OrthancPluginKeysValuesIteratorGetValue params; + params.target = target; + params.iterator = iterator; + + return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetValue, ¶ms); + } + + + typedef struct + { + const char* queueId; + const void* value; + uint32_t valueSize; + } _OrthancPluginEnqueueValue; + + /** + * @brief Append a value to the back of a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param value The value to store. + * @param valueSize The size of the value to store. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue( + OrthancPluginContext* context, + const char* queueId, /* in */ + const void* value, /* in */ + uint32_t valueSize /* in */) + { + _OrthancPluginEnqueueValue params; + params.queueId = queueId; + params.value = value; + params.valueSize = valueSize; + + return context->InvokeService(context, _OrthancPluginService_EnqueueValue, ¶ms); + } + + + typedef struct + { + uint8_t* found; + OrthancPluginMemoryBuffer* target; + const char* queueId; + OrthancPluginQueueOrigin origin; + } _OrthancPluginDequeueValue; + + /** + * @brief Dequeue a value from a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param found Pointer to a Boolean that is set to "true" iff. a value has been dequeued. + * @param target Memory buffer where to store the value that has been retrieved from the queue. + * It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param origin The position from where the value is dequeued (back for LIFO, front for FIFO). + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue( + OrthancPluginContext* context, + uint8_t* found, /* out */ + OrthancPluginMemoryBuffer* target, /* out */ + const char* queueId, /* in */ + OrthancPluginQueueOrigin origin /* in */) + { + _OrthancPluginDequeueValue params; + params.found = found; + params.target = target; + params.queueId = queueId; + params.origin = origin; + + return context->InvokeService(context, _OrthancPluginService_DequeueValue, ¶ms); + } + + + typedef struct + { + const char* queueId; + uint64_t* size; + } _OrthancPluginGetQueueSize; + + /** + * @brief Get the number of elements in a queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param queueId A unique identifier identifying both the plugin and the queue. + * @param size The number of elements in the queue. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.8") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetQueueSize( + OrthancPluginContext* context, + const char* queueId, /* in */ + uint64_t* size /* out */) + { + _OrthancPluginGetQueueSize params; + params.queueId = queueId; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_GetQueueSize, ¶ms); + } + + + typedef struct + { + const char* resourceId; + OrthancPluginStableStatus stableStatus; + uint8_t* statusHasChanged; + } _OrthancPluginSetStableStatus; + + /** + * @brief Change the "Stable" status of a resource. + * + * Forcing a resource to "Stable" if it is currently "Unstable" will + * change its "Stable" status AND trigger a new "Stable" change, + * which will also trigger listener callbacks. + * + * Forcing a resource to "Stable" if it is already "Stable" has no + * effect (no-op). + * + * Forcing a resource to "Unstable" will change its "Stable" status + * to "Unstable" AND reset its stabilization period, no matter its + * initial state. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param statusHasChanged Whether the status has changed (1) or not (0) during the execution of this command. + * @param resourceId The Orthanc identifier of the DICOM resource of interest. + * @param stableStatus The new stable status of the resource of interest. + * @return 0 if success, other value if error. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetStableStatus( + OrthancPluginContext* context, + uint8_t* statusHasChanged, /* out */ + const char* resourceId, /* in */ + OrthancPluginStableStatus stableStatus /* in */) + { + _OrthancPluginSetStableStatus params; + params.resourceId = resourceId; + params.stableStatus= stableStatus; + params.statusHasChanged = statusHasChanged; + + return context->InvokeService(context, _OrthancPluginService_SetStableStatus, ¶ms); + } + + + /** + * @brief Callback to authenticate a HTTP request. + * + * Signature of a callback function that authenticates every incoming HTTP request. + * + * @param status The output status of the authentication. + * @param customPayload If status is `OrthancPluginHttpAuthenticationStatus_Granted`, + * a custom payload that will be provided to the HTTP handler callback. + * @param redirection If status is `OrthancPluginHttpAuthenticationStatus_Redirect`, + * a buffer filled with the path where to redirect the user (typically, a login page). + * The path is relative to the root of the Web server of Orthanc. + * @param uri The URI of interest (without the possible GET arguments). + * @param ip The IP address of the HTTP client. + * @param headersCount The number of HTTP headers. + * @param headersKeys The keys of the HTTP headers (always converted to low-case). + * @param headersValues The values of the HTTP headers. + * @param getCount For a GET request, the number of GET parameters. + * @param getKeys For a GET request, the keys of the GET parameters. + * @param getValues For a GET request, the values of the GET parameters. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginHttpAuthentication) ( + OrthancPluginHttpAuthenticationStatus* status, /* out */ + OrthancPluginMemoryBuffer* customPayload, /* out */ + OrthancPluginMemoryBuffer* redirection, /* out */ + const char* uri, + const char* ip, + uint32_t headersCount, + const char* const* headersKeys, + const char* const* headersValues, + uint32_t getCount, + const char* const* getKeys, + const char* const* getValues); + + + typedef struct + { + OrthancPluginHttpAuthentication callback; + } _OrthancPluginHttpAuthentication; + + /** + * @brief Register a callback to handle HTTP authentication (and + * possibly HTTP authorization). + * + * This function installs a callback that is executed for each + * incoming HTTP request to handle HTTP authentication. At most one + * plugin can register such a callback. This gives the opportunity + * to the plugin to validate access tokens (such as a JWT), possibly + * redirecting the user to a login page. The authentication callback + * can generate a custom payload that will be provided to the + * subsequent REST handling callback (cf. `authenticationPayload` in + * `OrthancPluginHttpRequest`). + * + * If one plugin installs a HTTP authentication callback, the + * built-in HTTP authentication of Orthanc is disabled. This means + * that the "RegisteredUsers" and "AuthenticationEnabled" + * configuration options of Orthanc are totally ignored. In + * addition, tokens generated by + * OrthancPluginGenerateRestApiAuthorizationToken() become + * ineffective. + * + * This HTTP authentication callback can notably be used if some + * resource in the REST API must be available for public access, if + * the "RemoteAccessAllowed" configuration option is set to "true" + * (which necessitates bypassing the built-in HTTP authentication of + * Orthanc). + * + * In addition, the callback can handle HTTP authorization + * simultaneously with HTTP authentication, by reporting the + * "OrthancPluginHttpAuthenticationStatus_Forbidden" status. This + * corresponds to the behavior of callbacks installed using + * OrthancPluginRegisterIncomingHttpRequestFilter2(), but the latter + * callbacks do not provide access to the authentication payload. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The HTTP authentication callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterHttpAuthentication( + OrthancPluginContext* context, + OrthancPluginHttpAuthentication callback) + { + _OrthancPluginHttpAuthentication params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterHttpAuthentication, ¶ms); + } + + + typedef struct + { + const char* sourcePlugin; + const char* userId; + OrthancPluginResourceType resourceType; + const char* resourceId; + const char* action; + const void* logData; + uint32_t logDataSize; + } _OrthancPluginEmitAuditLog; + + + /** + * @brief Generate an audit log to signal security-related events. + * + * Generate an audit log that will be broadcasted to all the plugins + * that have registered a callback handler using + * OrthancPluginRegisterAuditLogHandler(). If no plugin has + * registered such a callback, the audit log is ignored. + * + * A typical handler would record the audit log in a database and/or + * relay the audit log to a message broker. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param sourcePlugin The name of the source plugin, to properly interpret the + * content of "action" and "logData". + * @param userId A string that uniquely identifies the user or + * entity that is executing the action on the resource. + * @param resourceType The type of the resource this audit log relates to. + * @param resourceId The resource this audit log relates to. + * @param action The action that was performed on the resource. + * @param logData A pointer to custom log data. + * @param logDataSize The size of the custom log data. + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE void OrthancPluginEmitAuditLog( + OrthancPluginContext* context, + const char* sourcePlugin, + const char* userId, + OrthancPluginResourceType resourceType, + const char* resourceId, + const char* action, + const void* logData, + uint32_t logDataSize) + { + _OrthancPluginEmitAuditLog m; + m.sourcePlugin = sourcePlugin; + m.userId = userId; + m.resourceType = resourceType; + m.resourceId = resourceId; + m.action = action; + m.logData = logData; + m.logDataSize = logDataSize; + context->InvokeService(context, _OrthancPluginService_EmitAuditLog, &m); + } + + + /** + * @brief Callback to handle an audit log. + * + * Signature of a callback function that handles an audit log + * emitted by a source plugin. + * + * @param sourcePlugin The name of the source plugin. This information can + * be used to properly interpret the content of the "action" and + * "logData" arguments. + * @param userId A string uniquely identifying the user or entity that is executing the action on the resource. + * @param resourceType The type of the resource this log relates to. + * @param resourceId The resource this log relates to. + * @param action The action that is performed on the resource. + * @param logData A pointer to custom log data. + * @param logDataSize The size of the custom log data. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginAuditLogHandler) ( + const char* sourcePlugin, + const char* userId, + OrthancPluginResourceType resourceType, + const char* resourceId, + const char* action, + const void* logData, + uint32_t logDataSize); + + typedef struct + { + OrthancPluginAuditLogHandler handler; + } _OrthancPluginAuditLogHandler; + + /** + * @brief Register a callback to handle audit logs. + * + * This function installs a callback to listen to each audit log + * that is generated by some other plugin. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param handler The audit log handler. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_SINCE_SDK("1.12.9") + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterAuditLogHandler( + OrthancPluginContext* context, + OrthancPluginAuditLogHandler handler) + { + _OrthancPluginAuditLogHandler params; + params.handler = handler; + + return context->InvokeService(context, _OrthancPluginService_RegisterAuditLogHandler, ¶ms); + } + + +#ifdef __cplusplus +} +#endif + + +/** @} */
--- a/Resources/SyncOrthancFolder.py Mon Sep 07 15:12:02 2026 +0200 +++ b/Resources/SyncOrthancFolder.py Mon Sep 07 20:58:51 2026 +0200 @@ -11,7 +11,10 @@ import urllib.request TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc') -PLUGIN_SDK_VERSION = '1.7.0' +PLUGIN_SDK_VERSIONS = [ + [ '1.7.0', 'Plugins/Include' ], + [ '1.12.9', 'OrthancServer/Plugins/Include' ], +] REPOSITORY = 'https://orthanc.uclouvain.be/hg/%s/raw-file' FILES = [ @@ -81,13 +84,14 @@ f[1], os.path.join(f[2], os.path.basename(f[1])) ]) -for f in SDK: - commands.append([ - 'orthanc', - 'Orthanc-%s' % PLUGIN_SDK_VERSION, - 'Plugins/Include/%s' % f, - 'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f) - ]) +for (version, folder) in PLUGIN_SDK_VERSIONS: + for f in SDK: + commands.append([ + 'orthanc', + 'Orthanc-%s' % version, + '%s/%s' % (folder, f), + 'Sdk-%s/%s' % (version, f) + ]) pool = multiprocessing.Pool(10) # simultaneous downloads
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsRestApi.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,595 @@ +/** + * 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-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "AnnotationsRestApi.h" + +#include "../ViewerConfiguration.h" +#include "../ViewerToolbox.h" +#include "CachedAnnotationsWorkspace.h" +#include "CachedUserFeatures.h" +#include "IAuthenticatedUser.h" + +#include <SerializationToolbox.h> +#include <Toolbox.h> + +#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + + +static const char* const KEY_FEATURES = "features"; +static const char* const KEY_LAYER_ID = "layer-id"; + + +namespace OrthancWSI +{ + class AnnotationsCommandContext : public boost::noncopyable + { + private: + std::unique_ptr<IAuthenticatedUser> user_; + Json::Value body_; + std::unique_ptr<CachedAnnotationsWorkspace> workspace_; + ProjectRole role_; + + public: + explicit AnnotationsCommandContext(const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + user_.reset(IAuthenticatedUser::FromHttpRequest(request)); + + if (!Orthanc::Toolbox::ReadJson(body_, request->body, request->bodySize) || + !body_.isObject()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + const std::string projectId = Orthanc::SerializationToolbox::ReadString(body_, "project", "" /* default project */); + const std::string levelString = Orthanc::SerializationToolbox::ReadString(body_, "level"); + const std::string resourceId = Orthanc::SerializationToolbox::ReadString(body_, "resource"); + unsigned int frameNumber = Orthanc::SerializationToolbox::ReadUnsignedInteger(body_, "frame", 0 /* default frame */); + + role_ = user_->GetRoleInProject(projectId); + + if (role_ != ProjectRole_Instructor && + role_ != ProjectRole_Learner) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "User \"" + user_->Format() + + "\" is not instructor or learner of project \"" + projectId + "\""); + } + + Orthanc::ResourceType level = Orthanc::StringToResourceType(levelString.c_str()); + AnnotationsWorkspaceId workspaceId(projectId, level, resourceId, frameNumber); + + workspace_.reset(new CachedAnnotationsWorkspace(workspaceId)); + } + + const IAuthenticatedUser& GetUser() const + { + assert(user_.get() != NULL); + return *user_; + } + + ProjectRole GetRole() const + { + return role_; + } + + AnnotationsWorkspace& GetWorkspace() + { + assert(workspace_.get() != NULL); + return workspace_->GetContent(); + } + + std::string GetBodyString(const char* field) const + { + return Orthanc::SerializationToolbox::ReadString(body_, field); + } + + const Json::Value& GetBodyField(const char* field) const + { + if (!body_.isMember(field)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + else + { + return body_[field]; + } + } + + AnnotationsWorkspace::UserReader* CreateUserReader() const + { + return new AnnotationsWorkspace::UserReader(workspace_->GetContent(), GetUser().GetAnnotatingId(), role_); + } + + AnnotationsWorkspace::UserWriter* CreateUserWriter() + { + return new AnnotationsWorkspace::UserWriter(workspace_->GetContent(), GetUser().GetAnnotatingId(), role_); + } + }; + + + static bool ProtectPostRequest(OrthancPluginRestOutput* output, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + return false; + } + else + { + return true; + } + } + + + void GetWorkspaceInfo(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + static const char* const KEY_IS_LEARNER = "is_learner"; + static const char* const KEY_IS_INSTRUCTOR = "is_instructor"; + + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + Json::Value answer; + + answer["name"] = context.GetWorkspace().GetProjectName(); + answer["description"] = context.GetWorkspace().GetProjectDescription(); + answer["project"] = context.GetWorkspace().GetId().GetProjectId(); + answer["enabled"] = ViewerConfiguration::GetInstance().AreAnnotationsEnabled(); + answer["sharing"] = (ViewerConfiguration::GetInstance().AreAnnotationsEnabled() && + ViewerConfiguration::GetInstance().IsAnnotationsSharingEnabled()); + answer["learner_to_learner_sharing"] = ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled(); + answer["user"] = context.GetUser().Format(); + + std::string role; + switch (context.GetRole()) + { + case ProjectRole_Learner: + answer[KEY_IS_LEARNER] = true; + answer[KEY_IS_INSTRUCTOR] = false; + break; + + case ProjectRole_Instructor: + answer[KEY_IS_LEARNER] = false; + answer[KEY_IS_INSTRUCTOR] = true; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + answer["role"] = role; + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) + answer["persistent"] = true; +#else + answer["persistent"] = false; +#endif + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void ListUserLayers(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + Json::Value answer; + + { + std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader()); + reader->ListLayers(answer); + } + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void CreateUserLayer(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + Json::Value answer; + + { + std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter()); + writer->CreateUserLayer(answer); + } + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void SaveUserLayer(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + UserLayer updated(context.GetBodyField("layer")); + + { + std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter()); + writer->UpdateUserLayer(updated); + } + + ViewerToolbox::AnswerEmpty(output); + } + } + + + void DeleteUserLayer(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + const std::string layerId = context.GetBodyString(KEY_LAYER_ID); + + { + std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter()); + writer->DeleteUserLayer(layerId); + } + + ViewerToolbox::AnswerEmpty(output); + } + } + + + void LoadUserFeatures(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + } + else + { + AnnotationsCommandContext context(request); + + Json::Value answer; + + { + CachedUserFeatures cached(context.GetWorkspace().GetId(), context.GetUser().GetAnnotatingId()); + cached.GetFeatures().GetContent(answer[KEY_FEATURES]); + } + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void SaveUserFeatures(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + } + else + { + AnnotationsCommandContext context(request); + + { + CachedUserFeatures cached(context.GetWorkspace().GetId(), context.GetUser().GetAnnotatingId()); + cached.GetFeatures().SetContent(context.GetBodyField(KEY_FEATURES)); + } + + ViewerToolbox::AnswerEmpty(output); + } + } + + + void SearchActiveUsers(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + } + else + { + AnnotationsCommandContext context(request); + const UserId self = context.GetUser().GetAnnotatingId(); + + const std::string query = context.GetBodyString("query"); + + std::set<UserId> users; + + { + std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader()); + reader->SearchActiveUsers(users, query); + } + + Json::Value answer = Json::arrayValue; + + for (std::set<UserId>::const_iterator it = users.begin(); it != users.end(); ++it) + { + assert(it->GetType() == UserId::Type_Root || + it->GetType() == UserId::Type_Standard); + + if (answer.size() >= 20) + { + break; // Don't load too many users + } + else if (!self.Equals(*it)) // Don't add self + { + Json::Value item; + it->Serialize(item); + answer.append(item); + } + } + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void ListUsersSharingLayersWithMe(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + } + else + { + AnnotationsCommandContext context(request); + + std::set<UserId> users; + + { + std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader()); + reader->ListUsersSharingLayersWithMe(users); + } + + Json::Value answer = Json::arrayValue; + + for (std::set<UserId>::const_iterator it = users.begin(); it != users.end(); ++it) + { + Json::Value item; + it->Serialize(item); + answer.append(item); + } + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void ListLayersSharedWithMe(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + } + else + { + AnnotationsCommandContext context(request); + + const UserId author(context.GetBodyField("author")); + Json::Value answer; + + { + std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader()); + reader->ListLayersSharedWithMe(answer, author); + } + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void ImportLayer(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + } + else + { + AnnotationsCommandContext context(request); + + const UserId author(context.GetBodyField("author")); + const std::string layerId = context.GetBodyString("layer"); + + { + std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter()); + writer->ImportLayer(author, layerId); + } + + ViewerToolbox::AnswerEmpty(output); + } + } + + + void RemoveImportedLayer(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); + } + else + { + AnnotationsCommandContext context(request); + + const std::string layerId = context.GetBodyString("layer"); + + { + std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter()); + writer->RemoveImportedLayer(layerId); + } + + ViewerToolbox::AnswerEmpty(output); + } + } + + + void SaveImportedLayer(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + ImportedLayer updated(context.GetBodyField("layer")); + + { + std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter()); + writer->UpdateImportedLayer(updated); + } + + ViewerToolbox::AnswerEmpty(output); + } + } + + + void CreateStandardUser(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + UserId user(UserId::Type_Standard, context.GetBodyString("name")); + + Json::Value answer; + user.Serialize(answer); + + ViewerToolbox::AnswerJson(output, answer); + } + } + + + void LoadImportedFeatures(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (ProtectPostRequest(output, request)) + { + AnnotationsCommandContext context(request); + + std::set<UserId> authors; + std::set<std::string> layerIds; + + { + std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader()); + reader->ListImportedLayers(authors, layerIds); + } + + Json::Value importedFeatures = Json::arrayValue; + + // Loop over the imported authors + for (std::set<UserId>::const_iterator it = authors.begin(); it != authors.end(); ++it) + { + Json::Value authorFeatures; + + { + CachedUserFeatures author(context.GetWorkspace().GetId(), *it); + author.GetFeatures().GetContent(authorFeatures); + } + + assert(authorFeatures.isArray()); + + for (Json::Value::ArrayIndex i = 0; i < authorFeatures.size(); i++) + { + std::string layerId = Orthanc::SerializationToolbox::ReadString(authorFeatures[i], KEY_LAYER_ID); + if (layerIds.find(layerId) != layerIds.end()) + { + importedFeatures.append(authorFeatures[i]); + } + } + } + + Json::Value answer; + answer[KEY_FEATURES] = importedFeatures; + ViewerToolbox::AnswerJson(output, answer); + } + } +} + + +void RegisterAnnotationsRestApi() +{ + OrthancPlugins::RegisterRestCallback<OrthancWSI::GetWorkspaceInfo>("/wsi/api/workspace-info", true); + + if (OrthancWSI::ViewerConfiguration::GetInstance().AreAnnotationsEnabled()) + { + OrthancPlugins::RegisterRestCallback<OrthancWSI::CreateUserLayer>("/wsi/api/create-user-layer", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::DeleteUserLayer>("/wsi/api/delete-user-layer", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::ListUserLayers>("/wsi/api/list-user-layers", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::SaveUserLayer>("/wsi/api/save-user-layer", true); + + OrthancPlugins::RegisterRestCallback<OrthancWSI::LoadUserFeatures>("/wsi/api/load-user-features", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::SaveUserFeatures>("/wsi/api/save-user-features", true); + + if (OrthancWSI::ViewerConfiguration::GetInstance().IsAnnotationsSharingEnabled()) + { + OrthancPlugins::RegisterRestCallback<OrthancWSI::CreateStandardUser>("/wsi/api/create-standard-user", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::SearchActiveUsers>("/wsi/api/search-active-users", true); + + OrthancPlugins::RegisterRestCallback<OrthancWSI::ImportLayer>("/wsi/api/import-layer", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::ListLayersSharedWithMe>("/wsi/api/list-shared-layers", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::ListUsersSharingLayersWithMe>("/wsi/api/list-sharing-users", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::RemoveImportedLayer>("/wsi/api/remove-imported-layer", true); + OrthancPlugins::RegisterRestCallback<OrthancWSI::SaveImportedLayer>("/wsi/api/save-imported-layer", true); + + OrthancPlugins::RegisterRestCallback<OrthancWSI::LoadImportedFeatures>("/wsi/api/load-imported-features", true); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsRestApi.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,26 @@ +/** + * 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-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +void RegisterAnnotationsRestApi();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,649 @@ +/** + * 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-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "AnnotationsWorkspace.h" + +#include "../ViewerConfiguration.h" +#include "../ViewerToolbox.h" + +#include <OrthancException.h> + +#include <boost/regex.hpp> + + +static const char* const KEY_ACTIVE_INSTRUCTORS = "active-instructors"; +static const char* const KEY_ACTIVE_LEARNERS = "active-learners"; + + +namespace OrthancWSI +{ + class AnnotationsWorkspace::PersistentInfo : public ISerializable + { + private: + // An active user is always of type "standard" + std::set<UserId> activeInstructors_; + std::set<UserId> activeLearners_; + + + static void ParseActiveUsers(std::set<UserId>& target, + const Json::Value& serialized) + { + if (!serialized.isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + target.clear(); + + for (Json::Value::ArrayIndex i = 0; i < serialized.size(); i++) + { + target.insert(UserId(serialized[i])); + } + } + + + static void SerializeActiveUsers(Json::Value& serialized, + const std::set<UserId>& source) + { + serialized = Json::arrayValue; + + for (std::set<UserId>::const_iterator it = source.begin(); it != source.end(); ++it) + { + Json::Value user; + it->Serialize(user); + serialized.append(user); + } + } + + void SanityCheck() const + { +#if !defined(NDEBUG) + for (std::set<UserId>::const_iterator it = activeInstructors_.begin(); it != activeInstructors_.end(); ++it) + { + assert(activeLearners_.find(*it) == activeLearners_.end()); + } + + for (std::set<UserId>::const_iterator it = activeLearners_.begin(); it != activeLearners_.end(); ++it) + { + assert(activeInstructors_.find(*it) == activeInstructors_.end()); + } +#endif + } + + public: + PersistentInfo() + { + } + + + explicit PersistentInfo(const Json::Value& serialized) + { + if (!serialized.isObject() || + !serialized.isMember(KEY_ACTIVE_INSTRUCTORS) || + !serialized.isMember(KEY_ACTIVE_LEARNERS)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + ParseActiveUsers(activeInstructors_, serialized[KEY_ACTIVE_INSTRUCTORS]); + ParseActiveUsers(activeLearners_, serialized[KEY_ACTIVE_LEARNERS]); + + SanityCheck(); + } + + + // Return "true" iff. the user was not already tagged as active or + // if the user has changed their role in the project (from learner + // to instructor, or from instructor to learner) + bool AddActiveUser(const UserId& user, + ProjectRole role) + { + SanityCheck(); + + switch (role) + { + case ProjectRole_Instructor: + if (activeInstructors_.find(user) == activeInstructors_.end()) + { + activeLearners_.erase(user); // Accomodate with change in the role + activeInstructors_.insert(user); + SanityCheck(); + return true; + } + else + { + return false; + } + + case ProjectRole_Learner: + if (activeLearners_.find(user) == activeLearners_.end()) + { + activeInstructors_.erase(user); // Accomodate with change in the role + activeLearners_.insert(user); + SanityCheck(); + return true; + } + else + { + return false; + } + + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + bool LookupUserRole(ProjectRole& role, + const UserId& user) const + { + if (activeInstructors_.find(user) != activeInstructors_.end()) + { + assert(activeLearners_.find(user) == activeLearners_.end()); + role = ProjectRole_Instructor; + return true; + } + else if (activeLearners_.find(user) != activeLearners_.end()) + { + assert(activeInstructors_.find(user) == activeInstructors_.end()); + role = ProjectRole_Learner; + return true; + } + else + { + return false; + } + } + + + virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE + { + serialized = Json::objectValue; + SerializeActiveUsers(serialized[KEY_ACTIVE_INSTRUCTORS], activeInstructors_); + SerializeActiveUsers(serialized[KEY_ACTIVE_LEARNERS], activeLearners_); + } + + + class ActiveUsersIterator : public boost::noncopyable + { + private: + std::set<UserId>::const_iterator instructorsIterator_; + std::set<UserId>::const_iterator instructorsEnd_; + std::set<UserId>::const_iterator learnersIterator_; + std::set<UserId>::const_iterator learnersEnd_; + + public: + explicit ActiveUsersIterator(const PersistentInfo& that) : + instructorsIterator_(that.activeInstructors_.begin()), + instructorsEnd_(that.activeInstructors_.end()), + learnersIterator_(that.activeLearners_.begin()), + learnersEnd_(that.activeLearners_.end()) + { + that.SanityCheck(); + } + + bool IsDone() const + { + return (instructorsIterator_ == instructorsEnd_ && + learnersIterator_ == learnersEnd_); + } + + const UserId& GetUser() const + { + if (instructorsIterator_ != instructorsEnd_) + { + return *instructorsIterator_; + } + else if (learnersIterator_ != learnersEnd_) + { + return *learnersIterator_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + ProjectRole GetRole() const + { + if (instructorsIterator_ != instructorsEnd_) + { + return ProjectRole_Instructor; + } + else if (learnersIterator_ != learnersEnd_) + { + return ProjectRole_Learner; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void Next() + { + if (instructorsIterator_ != instructorsEnd_) + { + ++instructorsIterator_; + } + else if (learnersIterator_ != learnersEnd_) + { + ++learnersIterator_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + }; + }; + + + + void AnnotationsWorkspace::Load(const UserId& user) + { + const std::string key = id_.GetSettingsKey(user); + + Json::Value layers; + if (ViewerToolbox::LookupKeyValueStore(layers, key)) + { + std::unique_ptr<UserAnnotationsSettings> item(new UserAnnotationsSettings(layers)); + + if (content_.find(user) == content_.end()) // Should never be false + { + content_[user] = item.release(); + } + } + } + + + bool AnnotationsWorkspace::IsSharedWith(const UserLayer& layer, + const UserId& layerAuthorId, + const UserId& viewerId, + ProjectRole viewerRole) const + { + assert(persistentInfo_.get() != NULL); + + ProjectRole authorRole; + if (persistentInfo_->LookupUserRole(authorRole, layerAuthorId)) + { + return layer.IsSharedWith(authorRole, viewerId, viewerRole); + } + else + { + return false; + } + } + + + AnnotationsWorkspace::AnnotationsWorkspace(const AnnotationsWorkspaceId& id) : + id_(id), + projectInformation_(id.GetProjectId()) + { + const std::string key = id.GetInfoKey(); + + Json::Value info; + + if (ViewerToolbox::LookupKeyValueStore(info, key)) + { + persistentInfo_.reset(new PersistentInfo(info)); + + PersistentInfo::ActiveUsersIterator iterator(*persistentInfo_); + + while (!iterator.IsDone()) + { + Load(iterator.GetUser()); + iterator.Next(); + } + } + else + { + persistentInfo_.reset(new PersistentInfo); + persistentInfo_->Serialize(info); + ViewerToolbox::SetKeyValueStore(key, info); + } + } + + + AnnotationsWorkspace::~AnnotationsWorkspace() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + AnnotationsWorkspace::UserReader::UserReader(AnnotationsWorkspace& that, + const UserId& userId, + ProjectRole userRole) : + lock_(that.mutex_), + that_(that), + userId_(userId), + userRole_(userRole) + { + Content::const_iterator found = that.content_.find(userId); + + if (found == that.content_.end()) + { + userSettings_ = NULL; + } + else + { + assert(found->second != NULL); + userSettings_ = found->second; + } + } + + + void AnnotationsWorkspace::UserReader::ListLayers(Json::Value& serialized) const + { + serialized = Json::objectValue; + + if (IsValid()) + { + userSettings_->Serialize(serialized); + } + else + { + UserAnnotationsSettings empty; + empty.Serialize(serialized); + } + } + + + void AnnotationsWorkspace::UserReader::ListUsersSharingLayersWithMe(std::set<UserId>& target) const + { + target.clear(); + + // Loop over all the users (i.e., all the possible authors) in this workspace + for (Content::const_iterator it = that_.content_.begin(); it != that_.content_.end(); ++it) + { + assert(it->second != NULL); + + if (!userId_.Equals(it->first)) // Don't add self + { + LayersCollection::Iterator iterator(it->second->GetUserLayers()); + + // Loop over all the layers of this user in this workspace + while (!iterator.IsDone()) + { + const UserLayer& layer = dynamic_cast<const UserLayer&>(iterator.GetLayer()); + + if (that_.IsSharedWith(layer, it->first, userId_, userRole_)) + { + target.insert(it->first); + break; + } + else + { + iterator.Next(); + } + } + } + } + } + + + void AnnotationsWorkspace::UserReader::ListLayersSharedWithMe(Json::Value& target, + const UserId& author) const + { + target = Json::arrayValue; + + if (IsValid()) + { + Content::const_iterator found = that_.content_.find(author); + + if (found != that_.content_.end()) + { + assert(found->second != NULL); + LayersCollection::Iterator iterator(found->second->GetUserLayers()); + + while (!iterator.IsDone()) + { + const UserLayer& layer = dynamic_cast<const UserLayer&>(iterator.GetLayer()); + + if (that_.IsSharedWith(layer, author, userId_, userRole_)) + { + Json::Value item; + layer.Serialize(item); + target.append(item); + } + + iterator.Next(); + } + } + } + } + + + void AnnotationsWorkspace::UserReader::SearchActiveUsers(std::set<UserId>& target, + const std::string& query) const + { + target.clear(); + + const boost::regex re(query); + + PersistentInfo::ActiveUsersIterator iterator(*that_.persistentInfo_); + + while (!iterator.IsDone()) + { + if (boost::regex_search(iterator.GetUser().GetName(), re)) + { + bool add = false; + + switch (userRole_) + { + case ProjectRole_Instructor: + add = true; + break; + + case ProjectRole_Learner: + switch (iterator.GetRole()) // Consider the role of the other user + { + case ProjectRole_Instructor: + // Learners can always share with instructors + add = true; + break; + + case ProjectRole_Learner: + add = ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled(); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + break; + + case ProjectRole_Guest: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + if (add) + { + target.insert(iterator.GetUser()); + } + } + + iterator.Next(); + } + } + + + void AnnotationsWorkspace::UserReader::ListImportedLayers(std::set<UserId>& authors, + std::set<std::string>& layerIds) const + { + authors.clear(); + layerIds.clear(); + + if (IsValid()) + { + LayersCollection::Iterator iterator(userSettings_->GetImportedLayers()); + + while (!iterator.IsDone()) + { + const ImportedLayer& layer = dynamic_cast<const ImportedLayer&>(iterator.GetLayer()); + + Content::const_iterator found = that_.content_.find(layer.GetAuthor()); + + if (found != that_.content_.end()) + { + assert(found->second != NULL); + + // Check that the layer has not been deleted in the meantime by its author, + // and that the layer is still shared with this user + if (found->second->GetUserLayers().HasLayer(layer.GetId())) + { + const UserLayer& authorLayer = found->second->GetUserLayer(layer.GetId()); + + if (that_.IsSharedWith(authorLayer, layer.GetAuthor(), userId_, userRole_)) + { + authors.insert(layer.GetAuthor()); + layerIds.insert(layer.GetId()); + } + } + } + + iterator.Next(); + } + } + } + + + void AnnotationsWorkspace::UserWriter::Commit() + { + ISerializable::SetKeyValueStore(that_.id_.GetSettingsKey(userId_), *userSettings_); + } + + + AnnotationsWorkspace::UserWriter::UserWriter(AnnotationsWorkspace& that, + const UserId& userId, + ProjectRole userRole) : + lock_(that.mutex_), + that_(that), + userId_(userId), + userRole_(userRole) + { + if (that.persistentInfo_->AddActiveUser(userId_, userRole)) + { + // Only update the key-value store if this is the first time we + // meet this user or if their role has changed in the project + ISerializable::SetKeyValueStore(that.id_.GetInfoKey(), *that.persistentInfo_); + } + + Content::iterator found = that.content_.find(userId_); + + if (found == that.content_.end()) + { + std::unique_ptr<UserAnnotationsSettings> layers(new UserAnnotationsSettings); + userSettings_ = layers.get(); + that.content_[userId_] = layers.release(); + Commit(); + } + else + { + assert(found->second != NULL); + userSettings_ = found->second; + } + } + + + void AnnotationsWorkspace::UserWriter::CreateUserLayer(Json::Value& answer) + { + assert(userSettings_ != NULL); + + const std::string layerId = userSettings_->CreateUserLayer(); + Commit(); + + const UserLayer& layer = userSettings_->GetUserLayer(layerId); + layer.Serialize(answer); + } + + + void AnnotationsWorkspace::UserWriter::UpdateUserLayer(const UserLayer& updated) + { + assert(userSettings_ != NULL); + + UserLayer& layer = userSettings_->GetUserLayer(updated.GetId()); + layer.Assign(updated); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::DeleteUserLayer(const std::string& layerId) + { + assert(userSettings_ != NULL); + userSettings_->GetUserLayers().DeleteLayer(layerId); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::ImportLayer(const UserId& author, + const std::string& layerId) + { + assert(userSettings_ != NULL); + + Content::const_iterator found = that_.content_.find(author); + if (found == that_.content_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + assert(found->second != NULL); + const UserAnnotationsSettings& authorData = *found->second; + + const UserLayer& layer = authorData.GetUserLayer(layerId); + if (!that_.IsSharedWith(layer, author, userId_, userRole_)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + } + + userSettings_->ImportLayer(author, layer); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::RemoveImportedLayer(const std::string& layerId) + { + assert(userSettings_ != NULL); + userSettings_->GetImportedLayers().DeleteLayer(layerId); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::UpdateImportedLayer(const ImportedLayer& updated) + { + assert(userSettings_ != NULL); + + ImportedLayer& layer = userSettings_->GetImportedLayer(updated.GetId()); + layer.Assign(updated); + Commit(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,141 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "AnnotationsWorkspaceId.h" +#include "ProjectInformation.h" +#include "UserAnnotationsSettings.h" + +#include <IDynamicObject.h> +#include <MultiThreading/ReaderWriterLock.h> + + +namespace OrthancWSI +{ + class AnnotationsWorkspace : public Orthanc::IDynamicObject + { + private: + class PersistentInfo; + + typedef std::map<UserId, UserAnnotationsSettings*> Content; + + Orthanc::ReaderWriterLock mutex_; + AnnotationsWorkspaceId id_; + std::unique_ptr<PersistentInfo> persistentInfo_; + Content content_; + ProjectInformation projectInformation_; + + void Load(const UserId& user); + + bool IsSharedWith(const UserLayer& layer, + const UserId& layerAuthorId, + const UserId& viewerId, + ProjectRole viewerRole) const; + + public: + explicit AnnotationsWorkspace(const AnnotationsWorkspaceId& id); + + virtual ~AnnotationsWorkspace() ORTHANC_OVERRIDE; + + const AnnotationsWorkspaceId& GetId() const + { + return id_; + } + + std::string GetProjectName() + { + return projectInformation_.GetName(); + } + + std::string GetProjectDescription() + { + return projectInformation_.GetDescription(); + } + + + class UserReader : public boost::noncopyable + { + private: + Orthanc::ReaderWriterLock::ReadLock lock_; + AnnotationsWorkspace& that_; + UserId userId_; + const UserAnnotationsSettings* userSettings_; + ProjectRole userRole_; + + public: + UserReader(AnnotationsWorkspace& that, + const UserId& userId, + ProjectRole userRole); + + bool IsValid() const + { + return userSettings_ != NULL; + } + + void ListLayers(Json::Value& serialized) const; + + void ListUsersSharingLayersWithMe(std::set<UserId>& target) const; + + void ListLayersSharedWithMe(Json::Value& target, + const UserId& author) const; + + void ListImportedLayers(std::set<UserId>& authors, + std::set<std::string>& layerIds) const; + + void SearchActiveUsers(std::set<UserId>& target, + const std::string& query) const; + }; + + + class UserWriter : public boost::noncopyable + { + private: + Orthanc::ReaderWriterLock::WriteLock lock_; + AnnotationsWorkspace& that_; + UserId userId_; + UserAnnotationsSettings* userSettings_; + ProjectRole userRole_; + + void Commit(); + + public: + UserWriter(AnnotationsWorkspace& that, + const UserId& userId, + ProjectRole userRole); + + void CreateUserLayer(Json::Value& answer); + + void UpdateUserLayer(const UserLayer& updated); + + void DeleteUserLayer(const std::string& layerId); + + void ImportLayer(const UserId& author, + const std::string& layerId); + + void RemoveImportedLayer(const std::string& layerId); + + void UpdateImportedLayer(const ImportedLayer& updated); + }; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspaceId.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,112 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "AnnotationsWorkspaceId.h" + +#include <OrthancException.h> +#include <Toolbox.h> + +#include <boost/lexical_cast.hpp> + + +namespace OrthancWSI +{ + std::string AnnotationsWorkspaceId::GetKeyPrefix() const + { + switch (level_) + { + case Orthanc::ResourceType_Series: + return projectId_ + "|series|" + resourceId_; + + case Orthanc::ResourceType_Instance: + return projectId_ + "|instance|" + boost::lexical_cast<std::string>(frameNumber_) + "|" + resourceId_; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + AnnotationsWorkspaceId::AnnotationsWorkspaceId(const std::string& projectId, + Orthanc::ResourceType level, + const std::string& resourceId, + unsigned int frameNumber) : + projectId_(projectId), + level_(level), + resourceId_(resourceId), + frameNumber_(frameNumber) + { + if (level_ != Orthanc::ResourceType_Series && + level_ != Orthanc::ResourceType_Instance) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + // The pipe symbol is disallowed as it is used to build the key, cf. GetKey() + if (!Orthanc::Toolbox::IsAsciiString(projectId_) || + projectId_.find('|') != std::string::npos) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Project name containing non-ASCII characters or the pipe symbol: " + projectId_); + } + + if (!Orthanc::Toolbox::IsAsciiString(resourceId_) || + resourceId_.find('|') != std::string::npos) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Resource ID containing non-ASCII characters or the pipe symbol: " + resourceId_); + } + } + + + unsigned int AnnotationsWorkspaceId::GetFrameNumber() const + { + if (level_ == Orthanc::ResourceType_Instance) + { + return frameNumber_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + std::string AnnotationsWorkspaceId::GetInfoKey() const + { + return GetKeyPrefix() + "|info"; + } + + + std::string AnnotationsWorkspaceId::GetSettingsKey(const UserId& user) const + { + return GetKeyPrefix() + "|settings|" + user.GetKey(); + } + + + std::string AnnotationsWorkspaceId::GetFeaturesKey(const UserId& user) const + { + return GetKeyPrefix() + "|features|" + user.GetKey(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspaceId.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,72 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "UserId.h" + +#include <Enumerations.h> + + +namespace OrthancWSI +{ + class AnnotationsWorkspaceId + { + private: + std::string projectId_; + Orthanc::ResourceType level_; + std::string resourceId_; + unsigned int frameNumber_; + + std::string GetKeyPrefix() const; + + public: + AnnotationsWorkspaceId(const std::string& projectId, + Orthanc::ResourceType level, + const std::string& resourceId, + unsigned int frameNumber); + + const std::string& GetProjectId() const + { + return projectId_; + } + + Orthanc::ResourceType GetLevel() const + { + return level_; + } + + const std::string& GetResourceId() const + { + return resourceId_; + } + + unsigned int GetFrameNumber() const; + + std::string GetInfoKey() const; + + std::string GetSettingsKey(const UserId& user) const; + + std::string GetFeaturesKey(const UserId& user) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/CachedAnnotationsWorkspace.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,63 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "CachedAnnotationsWorkspace.h" + +#include "../ViewerConfiguration.h" + + +namespace OrthancWSI +{ + Orthanc::SharedObjectCache& CachedAnnotationsWorkspace::GetCache() + { + static boost::mutex mutex; + static std::unique_ptr<Orthanc::SharedObjectCache> cache; + + { + boost::mutex::scoped_lock lock(mutex); + + if (cache.get() == NULL) + { + const unsigned int cacheSize = ViewerConfiguration::GetInstance().GetAnnotationsCacheSize(); + cache.reset(new Orthanc::SharedObjectCache(cacheSize)); + } + + return *cache; + } + } + + + CachedAnnotationsWorkspace::CachedAnnotationsWorkspace(const AnnotationsWorkspaceId& id) + { + const std::string key = id.GetInfoKey(); + + cached_ = GetCache().GetCachedValue(key); + + if (cached_.get() == NULL) + { + cached_.reset(new AnnotationsWorkspace(id)); + GetCache().Store(key, cached_, 1); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/CachedAnnotationsWorkspace.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,50 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "AnnotationsWorkspace.h" + +#include <Cache/SharedObjectCache.h> + +#include <boost/shared_ptr.hpp> + + +namespace OrthancWSI +{ + class CachedAnnotationsWorkspace : public boost::noncopyable + { + private: + static Orthanc::SharedObjectCache& GetCache(); + + boost::shared_ptr<Orthanc::IDynamicObject> cached_; + + public: + explicit CachedAnnotationsWorkspace(const AnnotationsWorkspaceId& id); + + AnnotationsWorkspace& GetContent() const + { + return dynamic_cast<AnnotationsWorkspace&>(*cached_); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/CachedUserFeatures.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,64 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "CachedUserFeatures.h" + +#include "../ViewerConfiguration.h" + + +namespace OrthancWSI +{ + Orthanc::SharedObjectCache& CachedUserFeatures::GetCache() + { + static boost::mutex mutex; + static std::unique_ptr<Orthanc::SharedObjectCache> cache; + + { + boost::mutex::scoped_lock lock(mutex); + + if (cache.get() == NULL) + { + const unsigned int cacheSize = ViewerConfiguration::GetInstance().GetFeaturesCacheSize(); + cache.reset(new Orthanc::SharedObjectCache(cacheSize)); + } + + return *cache; + } + } + + + CachedUserFeatures::CachedUserFeatures(const AnnotationsWorkspaceId& id, + const UserId& user) + { + const std::string key = id.GetFeaturesKey(user); + + cached_ = GetCache().GetCachedValue(key); + + if (cached_.get() == NULL) + { + cached_.reset(new UserFeatures(key)); + GetCache().Store(key, cached_, 1); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/CachedUserFeatures.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,52 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "AnnotationsWorkspaceId.h" +#include "UserFeatures.h" + +#include <Cache/SharedObjectCache.h> + +#include <boost/shared_ptr.hpp> + + +namespace OrthancWSI +{ + class CachedUserFeatures : public boost::noncopyable + { + private: + static Orthanc::SharedObjectCache& GetCache(); + + boost::shared_ptr<Orthanc::IDynamicObject> cached_; + + public: + CachedUserFeatures(const AnnotationsWorkspaceId& id, + const UserId& user); + + UserFeatures& GetFeatures() const + { + return dynamic_cast<UserFeatures&>(*cached_); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/IAuthenticatedUser.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,343 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "IAuthenticatedUser.h" + +#include "../ViewerConfiguration.h" + +#include <Compatibility.h> +#include <OrthancException.h> +#include <SerializationToolbox.h> +#include <Toolbox.h> + +#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#include <cassert> +#include <json/reader.h> + + +namespace OrthancWSI +{ + namespace + { + // If Orthanc runs without an authentication plugin + class RootUser : public IAuthenticatedUser + { + public: + virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE + { + return UserId(UserId::Type_Root); + } + + virtual std::string Format() const ORTHANC_OVERRIDE + { + return "(root)"; + } + + virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE + { + return ProjectRole_Instructor; + } + }; + + + class GuestUser : public IAuthenticatedUser + { + public: + virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE + { + // Anonymous users cannot save annotations + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + } + + virtual std::string Format() const ORTHANC_OVERRIDE + { + return "(guest)"; + } + + virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE + { + return ProjectRole_Guest; + } + }; + + + class EducationPluginUser : public IAuthenticatedUser + { + public: + enum EducationRole + { + EducationRole_Administrator, + EducationRole_Standard, + EducationRole_Guest + }; + + private: + std::string id_; + EducationRole role_; + std::set<std::string> instructorOfProjects_; + std::set<std::string> learnerOfProjects_; + + public: + explicit EducationPluginUser(const Json::Value& authentication) + { + assert(Orthanc::SerializationToolbox::ReadString(authentication, "source") == "orthanc-education"); + + id_ = Orthanc::SerializationToolbox::ReadString(authentication, "id"); + + const std::string role = Orthanc::SerializationToolbox::ReadString(authentication, "role"); + + if (role == "admin") + { + role_ = EducationRole_Administrator; + } + else if (role == "standard") + { + role_ = EducationRole_Standard; + } + else if (role == "guest") + { + role_ = EducationRole_Guest; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + Orthanc::SerializationToolbox::ReadSetOfStrings(instructorOfProjects_, authentication, "instructor_of"); + Orthanc::SerializationToolbox::ReadSetOfStrings(learnerOfProjects_, authentication, "learner_of"); + } + + virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE + { + switch (role_) + { + case EducationRole_Administrator: + return UserId(UserId::Type_Root); + + case EducationRole_Standard: + return UserId(UserId::Type_Standard, id_); + + case EducationRole_Guest: + // Anonymous users cannot save annotations + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + virtual std::string Format() const ORTHANC_OVERRIDE + { + return id_; + } + + virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE + { + switch (role_) + { + case EducationRole_Administrator: + return ProjectRole_Instructor; + + case EducationRole_Standard: + if (instructorOfProjects_.find(projectId) != instructorOfProjects_.end()) + { + return ProjectRole_Instructor; + } + else if (learnerOfProjects_.find(projectId) != learnerOfProjects_.end()) + { + return ProjectRole_Learner; + } + else + { + return ProjectRole_Guest; + } + + case EducationRole_Guest: + return ProjectRole_Guest; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + }; + + + class GenericStandardUser : public IAuthenticatedUser + { + private: + ProjectRole role_; + std::string username_; + + public: + GenericStandardUser(ProjectRole role, + const std::string& username) : + role_(role), + username_(username) + { + } + + virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE + { + return UserId(UserId::Type_Standard, username_); + } + + virtual std::string Format() const ORTHANC_OVERRIDE + { + return username_; + } + + virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE + { + return role_; + } + }; + } + + + static IAuthenticatedUser* FromRegisteredUsers(const OrthancPluginHttpRequest* request) + { + for (uint32_t i = 0; i < request->headersCount; i++) + { + if (std::string(request->headersKeys[i]) == "authorization") + { + const std::string value(request->headersValues[i]); + + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, value, ' '); + + if (tokens.size() == 2 && + tokens[0] == "Basic") + { + std::string decoded; + Orthanc::Toolbox::DecodeBase64(decoded, tokens[1]); + + Orthanc::Toolbox::TokenizeString(tokens, decoded, ':'); + if (!tokens.empty() && + !tokens[0].empty()) + { + return new GenericStandardUser(ProjectRole_Instructor, tokens[0]); + } + } + } + } + + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, + "Forbidden access, HTTP basic authentication is missing"); + } + + + static IAuthenticatedUser* FromHttpHeader(const OrthancPluginHttpRequest* request) + { + const std::string& header = ViewerConfiguration::GetInstance().GetAuthenticationHttpHeader(); + + for (uint32_t i = 0; i < request->headersCount; i++) + { + if (std::string(request->headersKeys[i]) == header) + { + const std::string user(request->headersValues[i]); + + if (user.empty()) + { + return new GuestUser; + } + else if (ViewerConfiguration::GetInstance().IsInstructor(user)) + { + return new GenericStandardUser(ProjectRole_Instructor, user); + } + else + { + return new GenericStandardUser(ProjectRole_Learner, user); + } + } + } + + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, + "Forbidden access, as HTTP header \"" + header + "\" is not set by your proxy"); + } + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9) + static IAuthenticatedUser* FromPlugin(const OrthancPluginHttpRequest* request) + { + if (request->authenticationPayloadSize == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "No authentication plugin is properly installed"); + } + else + { + const char* payload = reinterpret_cast<const char*>(request->authenticationPayload); + + // We use "Json::Reader" as "Orthanc::ReadJson()" would write an + // error log if the authentication payload is not a JSON string + Json::Reader reader; + + Json::Value authentication; + if (reader.parse(payload, payload + request->authenticationPayloadSize, authentication, false)) + { + const std::string source = Orthanc::SerializationToolbox::ReadString(authentication, "source", "(none)"); + + if (source == "orthanc-education") + { + return new EducationPluginUser(authentication); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Unknown authentication plugin: " + source); + } + } + + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Unknown authentication plugin"); + } + } +#endif + + + + IAuthenticatedUser* IAuthenticatedUser::FromHttpRequest(const OrthancPluginHttpRequest* request) + { + switch (ViewerConfiguration::GetInstance().GetAuthenticationSource()) + { + case AuthenticationSource_None: + // No authentication is available, use the root user of Orthanc + return new RootUser; + + case AuthenticationSource_RegisteredUsers: + return FromRegisteredUsers(request); + + case AuthenticationSource_HttpHeader: + return FromHttpHeader(request); + + case AuthenticationSource_Plugin: +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9) + return FromPlugin(request); +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); +#endif + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/IAuthenticatedUser.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,51 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../Framework/FrameworkEnumerations.h" +#include "UserId.h" + +#include <orthanc/OrthancCPlugin.h> + +#include <boost/noncopyable.hpp> + + +namespace OrthancWSI +{ + class IAuthenticatedUser : public boost::noncopyable + { + public: + virtual ~IAuthenticatedUser() + { + } + + virtual UserId GetAnnotatingId() const = 0; + + virtual std::string Format() const = 0; + + virtual ProjectRole GetRoleInProject(const std::string& projectId) const = 0; + + static IAuthenticatedUser* FromHttpRequest(const OrthancPluginHttpRequest* request); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/ILayer.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,36 @@ +/** + * 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-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "ISerializable.h" + + +namespace OrthancWSI +{ + class ILayer : public ISerializable + { + public: + virtual std::string GetId() const = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/ISerializable.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,50 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "ISerializable.h" + +#include "../ViewerToolbox.h" + +#include <Toolbox.h> + + +namespace OrthancWSI +{ + void ISerializable::Serialize(std::string& serialized, + const ISerializable& obj) + { + Json::Value value; + obj.Serialize(value); + Orthanc::Toolbox::WriteFastJson(serialized, value); + } + + + void ISerializable::SetKeyValueStore(const std::string& key, + const ISerializable& obj) + { + std::string s; + ISerializable::Serialize(s, obj); + ViewerToolbox::SetKeyValueStore(key, s); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/ISerializable.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,48 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <boost/noncopyable.hpp> +#include <json/value.h> +#include <string> + + +namespace OrthancWSI +{ + class ISerializable : public boost::noncopyable + { + public: + virtual ~ISerializable() + { + } + + virtual void Serialize(Json::Value& serialized) const = 0; + + static void Serialize(std::string& serialized, + const ISerializable& obj); + + static void SetKeyValueStore(const std::string& key, + const ISerializable& obj); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/ImportedLayer.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,95 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "ImportedLayer.h" + +#include "UserLayer.h" + +#include <OrthancException.h> +#include <SerializationToolbox.h> + + +static const char* const KEY_VISIBLE = "visible"; +static const char* const KEY_AUTHOR = "author"; +static const char* const KEY_COLOR = "color"; +static const char* const KEY_ID = "id"; +static const char* const KEY_NAME = "name"; + + +namespace OrthancWSI +{ + ImportedLayer::ImportedLayer(const UserId& author, + const UserLayer& layer) : + isVisible_(true), + color_(layer.GetColor()), + author_(author), + id_(layer.GetId()), + name_(layer.GetName()) + { + } + + + ImportedLayer::ImportedLayer(const Json::Value& serialized) + { + if (!serialized.isObject() || + !serialized.isMember(KEY_AUTHOR)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + isVisible_ = Orthanc::SerializationToolbox::ReadBoolean(serialized, KEY_VISIBLE); + author_ = UserId(serialized[KEY_AUTHOR]); + id_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_ID); + name_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_NAME); + color_ = BackgroundColor::FromHexadecimalString(Orthanc::SerializationToolbox::ReadString(serialized, KEY_COLOR)); + } + + + void ImportedLayer::Assign(const ImportedLayer& other) + { + if (other.GetId() != id_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + isVisible_ = other.isVisible_; + color_ = other.color_; + author_ = other.author_; + name_ = other.name_; + } + } + + + void ImportedLayer::Serialize(Json::Value& serialized) const + { + serialized = Json::objectValue; + serialized[KEY_VISIBLE] = isVisible_; + serialized[KEY_COLOR] = color_.ToHexadecimalString(); + serialized[KEY_ID] = id_; + serialized[KEY_NAME] = name_; + + author_.Serialize(serialized[KEY_AUTHOR]); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/ImportedLayer.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../Framework/BackgroundColor.h" +#include "ILayer.h" +#include "UserId.h" + +#include <Compatibility.h> + + +namespace OrthancWSI +{ + class UserLayer; + + class ImportedLayer : public ILayer + { + private: + bool isVisible_; + BackgroundColor color_; + UserId author_; + std::string id_; + std::string name_; + + public: + ImportedLayer(const UserId& author, + const UserLayer& layer); + + explicit ImportedLayer(const Json::Value& serialized); + + void Assign(const ImportedLayer& other); + + virtual std::string GetId() const ORTHANC_OVERRIDE + { + return id_; + } + + bool IsVisible() const + { + return isVisible_; + } + + const BackgroundColor& GetColor() const + { + return color_; + } + + const UserId& GetAuthor() const + { + return author_; + } + + const std::string& GetName() const + { + return name_; + } + + virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/LayersCollection.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,170 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "LayersCollection.h" + +#include <OrthancException.h> + +#include <cassert> + + +namespace OrthancWSI +{ + LayersCollection::~LayersCollection() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + } + + + size_t LayersCollection::GetSize() const + { + assert(content_.size() == index_.size()); + return content_.size(); + } + + + void LayersCollection::AddLayer(ILayer* layer) + { + std::unique_ptr<ILayer> protection(layer); + + if (layer == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + const std::string id = protection->GetId(); + + if (index_.find(id) != index_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Duplicate layer ID"); + } + else + { + content_.push_back(protection.release()); + + Content::iterator it = content_.end(); + --it; // Points to the element we just inserted + index_[id] = it; + } + } + + + bool LayersCollection::HasLayer(const std::string& id) const + { + return (index_.find(id) != index_.end()); + } + + + ILayer& LayersCollection::GetLayer(const std::string& id) const + { + Index::const_iterator found = index_.find(id); + + if (found == index_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + else + { + assert(*(found->second) != NULL); + return **(found->second); + } + } + + + void LayersCollection::DeleteLayer(const std::string& id) + { + Index::iterator found = index_.find(id); + + if (found == index_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + else + { + assert(*(found->second) != NULL); + delete *(found->second); + content_.erase(found->second); + index_.erase(found); + } + } + + + void LayersCollection::Serialize(Json::Value& serialized) const + { + serialized = Json::arrayValue; + + for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(*it != NULL); + + Json::Value item; + (*it)->Serialize(item); + serialized.append(item); + } + } + + + LayersCollection::Iterator::Iterator(const LayersCollection& that) : + it_(that.content_.begin()), + end_(that.content_.end()) + { + } + + + bool LayersCollection::Iterator::IsDone() const + { + return it_ == end_; + } + + + const ILayer& LayersCollection::Iterator::GetLayer() const + { + if (IsDone()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + assert(*it_ != NULL); + return **it_; + } + } + + + void LayersCollection::Iterator::Next() + { + if (IsDone()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + ++it_; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/LayersCollection.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,76 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "ILayer.h" + +#include <Compatibility.h> + +#include <list> +#include <map> + + +namespace OrthancWSI +{ + class LayersCollection : public ISerializable + { + private: + typedef std::list<ILayer*> Content; + typedef std::map<std::string, Content::iterator> Index; + + Content content_; + Index index_; + + public: + virtual ~LayersCollection() ORTHANC_OVERRIDE; + + size_t GetSize() const; + + void AddLayer(ILayer* layer /* takes ownership */); + + bool HasLayer(const std::string& id) const; + + ILayer& GetLayer(const std::string& id) const; + + void DeleteLayer(const std::string& id); + + virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE; + + class Iterator : public boost::noncopyable + { + private: + Content::const_iterator it_; + Content::const_iterator end_; + + public: + explicit Iterator(const LayersCollection& that); + + bool IsDone() const; + + const ILayer& GetLayer() const; + + void Next(); + }; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/ProjectInformation.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,91 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "ProjectInformation.h" + +#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#include <SerializationToolbox.h> + + +static boost::posix_time::ptime GetNow() +{ + return boost::posix_time::second_clock::universal_time(); +} + + +namespace OrthancWSI +{ + void ProjectInformation::Load() + { + Json::Value info; + + if (OrthancPlugins::RestApiGet(info, "/education/api-plugins/project?id=" + projectId_, true) && + info.isObject()) + { + // The "orthanc-education" plugin is available + name_ = Orthanc::SerializationToolbox::ReadString(info, "name", ""); + description_ = Orthanc::SerializationToolbox::ReadString(info, "description", ""); + } + else + { + name_.clear(); + description_.clear(); + } + + lastUpdate_ = GetNow(); + } + + + void ProjectInformation::Refresh() + { + if (GetNow() - lastUpdate_ >= boost::posix_time::seconds(10)) + { + Load(); + } + } + + + ProjectInformation::ProjectInformation(const std::string& projectId) : + projectId_(projectId) + { + Load(); + } + + + std::string ProjectInformation::GetName() + { + boost::mutex::scoped_lock lock(mutex_); + Refresh(); + return name_; + } + + + std::string ProjectInformation::GetDescription() + { + boost::mutex::scoped_lock lock(mutex_); + Refresh(); + return description_; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/ProjectInformation.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <boost/date_time/posix_time/posix_time.hpp> +#include <boost/thread/mutex.hpp> +#include <string> + + +namespace OrthancWSI +{ + class ProjectInformation : public boost::noncopyable + { + private: + boost::mutex mutex_; + std::string projectId_; + boost::posix_time::ptime lastUpdate_; + std::string name_; + std::string description_; + + void Load(); + + // The mutex must be locked + void Refresh(); + + public: + explicit ProjectInformation(const std::string& projectId); + + std::string GetName(); + + std::string GetDescription(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserAnnotationsSettings.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,147 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "UserAnnotationsSettings.h" + +#include <Logging.h> +#include <OrthancException.h> + +#include <boost/lexical_cast.hpp> + + +static const char* const KEY_USER_LAYERS = "user-layers"; +static const char* const KEY_IMPORTED_LAYERS = "imported-layers"; + + +namespace OrthancWSI +{ + UserAnnotationsSettings::UserAnnotationsSettings(const Json::Value& serialized) + { + if (!serialized.isObject() || + !serialized.isMember(KEY_USER_LAYERS) || + !serialized.isMember(KEY_IMPORTED_LAYERS) || + !serialized[KEY_USER_LAYERS].isArray() || + !serialized[KEY_IMPORTED_LAYERS].isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + const Json::Value& a = serialized[KEY_USER_LAYERS]; + for (Json::Value::ArrayIndex i = 0; i < a.size(); i++) + { + userLayers_.AddLayer(new UserLayer(a[i])); + } + + const Json::Value& b = serialized[KEY_IMPORTED_LAYERS]; + for (Json::Value::ArrayIndex i = 0; i < b.size(); i++) + { + importedLayers_.AddLayer(new ImportedLayer(b[i])); + } + } + } + + + std::string UserAnnotationsSettings::AddUserLayer(UserLayer* layer) + { + std::unique_ptr<UserLayer> protection(layer); + + if (layer == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + const std::string id = protection->GetId(); + + userLayers_.AddLayer(protection.release()); + + return id; + } + + + UserLayer& UserAnnotationsSettings::GetUserLayer(const std::string& layerId) const + { + return dynamic_cast<UserLayer&>(userLayers_.GetLayer(layerId)); + } + + + ImportedLayer& UserAnnotationsSettings::GetImportedLayer(const std::string& layerId) const + { + return dynamic_cast<ImportedLayer&>(importedLayers_.GetLayer(layerId)); + } + + + std::string UserAnnotationsSettings::CreateUserLayer() + { + static const uint8_t PALETTE[] = { + 0xe6, 0x39, 0x46, // red: #e63946 + 0x2a, 0x9d, 0x8f, + 0xe9, 0xc4, 0x6a, + 0x26, 0x46, 0x53, + 0xf4, 0xa2, 0x61 + }; + + static const size_t PALETTE_SIZE = sizeof(PALETTE) / (3 * sizeof(uint8_t)); + + size_t item = userLayers_.GetSize() % PALETTE_SIZE; + + BackgroundColor color(PALETTE[3 * item], + PALETTE[3 * item + 1], + PALETTE[3 * item + 2]); + + std::string name; + if (userLayers_.GetSize() == 0) + { + name = "Default"; + } + else + { + name = "Layer " + boost::lexical_cast<std::string>(userLayers_.GetSize() + 1); + } + + return AddUserLayer(new UserLayer(color, name)); + } + + + void UserAnnotationsSettings::Serialize(Json::Value& serialized) const + { + serialized = Json::objectValue; + userLayers_.Serialize(serialized[KEY_USER_LAYERS]); + importedLayers_.Serialize(serialized[KEY_IMPORTED_LAYERS]); + } + + + void UserAnnotationsSettings::ImportLayer(const UserId& author, + const UserLayer& layer) + { + if (importedLayers_.HasLayer(layer.GetId())) + { + LOG(INFO) << "Cannot re-import already imported layer: " << layer.GetId(); + } + else + { + importedLayers_.AddLayer(new ImportedLayer(author, layer)); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserAnnotationsSettings.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,79 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "LayersCollection.h" +#include "UserLayer.h" +#include "ImportedLayer.h" + + +namespace OrthancWSI +{ + class UserAnnotationsSettings : public ISerializable + { + private: + LayersCollection userLayers_; + LayersCollection importedLayers_; + + public: + UserAnnotationsSettings() + { + } + + explicit UserAnnotationsSettings(const Json::Value& serialized); + + std::string AddUserLayer(UserLayer* layer); + + UserLayer& GetUserLayer(const std::string& layerId) const; + + ImportedLayer& GetImportedLayer(const std::string& layerId) const; + + LayersCollection& GetUserLayers() + { + return userLayers_; + } + + const LayersCollection& GetUserLayers() const + { + return userLayers_; + } + + LayersCollection& GetImportedLayers() + { + return importedLayers_; + } + + const LayersCollection& GetImportedLayers() const + { + return importedLayers_; + } + + std::string CreateUserLayer(); + + virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE; + + void ImportLayer(const UserId& author, + const UserLayer& layer); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserFeatures.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,147 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "UserFeatures.h" + +#include "../ViewerToolbox.h" + +#include <Compression/GzipCompressor.h> +#include <OrthancException.h> +#include <SerializationToolbox.h> +#include <Toolbox.h> + +#include <boost/lexical_cast.hpp> + + +static const char* const KEY_FEATURES = "features"; +static const char* const KEY_LAYER_ID = "layer-id"; +static const char* const KEY_TYPE = "type"; +static const char* const KEY_VERSION = "version"; + + +namespace OrthancWSI +{ + void UserFeatures::Load() + { + content_ = Json::arrayValue; + + std::string compressed; + if (ViewerToolbox::LookupKeyValueStore(compressed, key_)) + { + std::string uncompressed; + Orthanc::GzipCompressor compressor; + Orthanc::IBufferCompressor::Uncompress(uncompressed, compressor, compressed); + + Json::Value unserialized; + + if (!Orthanc::Toolbox::ReadJson(unserialized, uncompressed) || + !unserialized.isObject() || + !unserialized.isMember(KEY_FEATURES) || + !unserialized[KEY_FEATURES].isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + const unsigned int version = Orthanc::SerializationToolbox::ReadUnsignedInteger(unserialized, KEY_VERSION); + + if (version == ORTHANC_WSI_ANNOTATIONS_VERSION) + { + content_ = unserialized[KEY_FEATURES]; + } + else + { + switch (version) + { + // Implement version conversion here + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Cannot load annotations from version: " + + boost::lexical_cast<std::string>(version)); + } + } + } + } + + + void UserFeatures::Save() const + { + assert(content_.isArray()); + + Json::Value unserialized; + unserialized[KEY_VERSION] = static_cast<unsigned int>(ORTHANC_WSI_ANNOTATIONS_VERSION); + unserialized[KEY_FEATURES] = content_; + + std::string serialized; + Orthanc::Toolbox::WriteFastJson(serialized, unserialized); + + std::string compressed; + Orthanc::GzipCompressor compressor; + Orthanc::IBufferCompressor::Compress(compressed, compressor, serialized); + + ViewerToolbox::SetKeyValueStore(key_, compressed); + } + + + UserFeatures::UserFeatures(const std::string& key) : + key_(key) + { + Load(); + } + + + void UserFeatures::GetContent(Json::Value& target) + { + Orthanc::ReaderWriterLock::ReadLock lock(mutex_); + + assert(content_.isArray()); + target = content_; + } + + + void UserFeatures::SetContent(const Json::Value& content) + { + if (!content.isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + for (Json::Value::ArrayIndex i = 0; i < content.size(); i++) + { + if (!content[i].isObject() || + !content[i].isMember(KEY_LAYER_ID) || + !content[i].isMember(KEY_TYPE) || + !content[i][KEY_LAYER_ID].isString() || + !content[i][KEY_TYPE].isString()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + } + + { + Orthanc::ReaderWriterLock::WriteLock lock(mutex_); + content_ = content; + Save(); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserFeatures.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <IDynamicObject.h> +#include <MultiThreading/ReaderWriterLock.h> + +#include <json/value.h> +#include <string> + + +namespace OrthancWSI +{ + class UserFeatures : public Orthanc::IDynamicObject + { + private: + Orthanc::ReaderWriterLock mutex_; + std::string key_; + Json::Value content_; + + void Load(); + + void Save() const; + + public: + explicit UserFeatures(const std::string& key); + + void GetContent(Json::Value& target); + + void SetContent(const Json::Value& content); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserId.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,145 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "UserId.h" + +#include <OrthancException.h> +#include <SerializationToolbox.h> +#include <Toolbox.h> + + +static const char* const KEY_TYPE = "type"; +static const char* const KEY_NAME = "name"; + + +namespace OrthancWSI +{ + void UserId::Setup(Type type, + const std::string& name) + { + type_ = type; + name_ = name; + + switch (type_) + { + case Type_Invalid: + case Type_Root: + if (!name.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + break; + + case Type_Standard: + if (name.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + UserId::UserId(const Json::Value& serialized) + { + Setup(static_cast<Type>(Orthanc::SerializationToolbox::ReadInteger(serialized, KEY_TYPE)), + Orthanc::SerializationToolbox::ReadString(serialized, KEY_NAME)); + } + + + bool UserId::Equals(const UserId& other) const + { + if (type_ != other.type_) + { + return false; + } + else if (type_ == Type_Standard) + { + return name_ == other.name_; + } + else + { + return true; + } + } + + + bool UserId::operator<(const UserId& other) const + { + if (type_ < other.type_) + { + return true; + } + else if (type_ > other.type_) + { + return false; + } + else + { + return name_ < other.name_; + } + } + + + std::string UserId::GetKey() const + { + switch (type_) + { + case Type_Root: + return "root"; + + case Type_Standard: + { + // The pipe character "|" is not part of Base64, so we can safely use it to separate components + std::string s; + Orthanc::Toolbox::EncodeBase64(s, name_); + return "user_" + s; + } + + case Type_Invalid: + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + void UserId::Serialize(Json::Value& target) const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + target = Json::objectValue; + target[KEY_TYPE] = static_cast<int>(type_); + target[KEY_NAME] = name_; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserId.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,86 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <json/value.h> +#include <string> + + +namespace OrthancWSI +{ + class UserId + { + public: + enum Type + { + Type_Root, + Type_Standard, + Type_Invalid + }; + + private: + Type type_; + std::string name_; + + void Setup(Type type, + const std::string& name); + + public: + explicit UserId() + { + Setup(Type_Invalid, ""); + } + + explicit UserId(Type type) + { + Setup(type, ""); + } + + UserId(Type type, + const std::string& name) + { + Setup(type, name); + } + + explicit UserId(const Json::Value& serialized); + + Type GetType() const + { + return type_; + } + + const std::string& GetName() const + { + return name_; + } + + bool Equals(const UserId& other) const; + + bool operator<(const UserId& other) const; + + std::string GetKey() const; + + void Serialize(Json::Value& target) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserLayer.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,199 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "UserLayer.h" + +#include "../ViewerConfiguration.h" + +#include <OrthancException.h> +#include <SerializationToolbox.h> +#include <Toolbox.h> + +#include <cassert> + + +static const char* const KEY_COLOR = "color"; +static const char* const KEY_ID = "id"; +static const char* const KEY_NAME = "name"; +static const char* const KEY_PUBLIC = "public"; +static const char* const KEY_SHARED_WITH = "shared_with"; +static const char* const KEY_VISIBLE = "visible"; + + +namespace OrthancWSI +{ + UserLayer::UserLayer(const BackgroundColor& color, + const std::string& name) : + isVisible_(true), + color_(color), + id_(Orthanc::Toolbox::GenerateUuid()), + name_(name), + isPublic_(false) + { + } + + + UserLayer::UserLayer(const Json::Value& serialized) + { + if (!serialized.isObject() || + !serialized.isMember(KEY_SHARED_WITH) || + !serialized[KEY_SHARED_WITH].isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + isVisible_ = Orthanc::SerializationToolbox::ReadBoolean(serialized, KEY_VISIBLE); + color_ = BackgroundColor::FromHexadecimalString(Orthanc::SerializationToolbox::ReadString(serialized, KEY_COLOR)); + id_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_ID); + name_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_NAME); + isPublic_ = Orthanc::SerializationToolbox::ReadBoolean(serialized, KEY_PUBLIC); + + const Json::Value& v = serialized[KEY_SHARED_WITH]; + for (Json::Value::ArrayIndex i = 0; i < v.size(); i++) + { + sharedWith_.insert(UserId(v[i])); + } + } + + + bool UserLayer::IsSharedWith(ProjectRole authorRole, + const UserId& viewerId, + ProjectRole viewerRole) const + { + /** + + Instructors can see: + + - All layers tagged "publicly shared with instructors" + (i.e. public), created by anyone (instructors or learners). + + - Any layer explicitly shared with them, by anyone. + + Learners can see: + + - All layers tagged public that were created by instructors + (this is true "class-wide public" for instructor content). + + - Any instructor layer explicitly shared with them. + + - Any learner layer explicitly shared with them by name, only + if learner-to-learner sharing is enabled (cf. configuration + option "EnableLearnerToLearnerSharing"). + + Note 1: Learner layers tagged "public" are visible only to + instructors, never to other learners, regardless of the + learner-to-learner sharing configuration. This is a deliberate + asymmetry: for a learner, "public" means "submitted/visible to + instructors," not "visible to the class." This prevents one + learner's work from becoming broadcast to the whole cohort, + while still allowing small, named-group collaboration (e.g., + project teams) through explicit sharing. + + Note 2: Learner-to-learner sharing (configuration option) + governs only the explicit-share-list channel between + learners. It has no effect on instructor visibility and no + effect on the behavior of the "public" tag (public learner + layers are never learner-visible whether this option is "true" + or "false"). + + **/ + + if (viewerId.GetType() != UserId::Type_Standard) + { + return false; + } + + const bool explicitlyShared = sharedWith_.find(viewerId) != sharedWith_.end(); + + switch (authorRole) + { + case ProjectRole_Instructor: + // Instructor layers: "public" truly means public to everyone, + // and explicit sharing is unconditional + return isPublic_ || explicitlyShared; + + case ProjectRole_Learner: + switch (viewerRole) + { + case ProjectRole_Instructor: + // Instructors see public learner layers, and anything shared with them + return isPublic_ || explicitlyShared; + + case ProjectRole_Learner: + // Learner viewing another learner's layer: "public" never applies, + // explicit sharing is gated by the configuration switch. + return explicitlyShared && ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled(); + + case ProjectRole_Guest: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + case ProjectRole_Guest: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + void UserLayer::Assign(const UserLayer& other) + { + if (other.GetId() != id_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + isVisible_ = other.isVisible_; + color_ = other.color_; + name_ = other.name_; + sharedWith_ = other.sharedWith_; + isPublic_ = other.isPublic_; + } + } + + + void UserLayer::Serialize(Json::Value& serialized) const + { + Json::Value sharedWith = Json::arrayValue; + for (std::set<UserId>::const_iterator it = sharedWith_.begin(); it != sharedWith_.end(); ++it) + { + Json::Value item; + it->Serialize(item); + sharedWith.append(item); + } + + serialized = Json::objectValue; + serialized[KEY_VISIBLE] = isVisible_; + serialized[KEY_COLOR] = color_.ToHexadecimalString(); + serialized[KEY_ID] = id_; + serialized[KEY_NAME] = name_; + serialized[KEY_PUBLIC] = isPublic_; + serialized[KEY_SHARED_WITH] = sharedWith; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserLayer.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../Framework/BackgroundColor.h" +#include "../../Framework/FrameworkEnumerations.h" +#include "ILayer.h" +#include "UserId.h" + +#include <Compatibility.h> + +#include <set> + + +namespace OrthancWSI +{ + class UserLayer : public ILayer + { + private: + bool isVisible_; + BackgroundColor color_; + std::string id_; + std::string name_; + std::set<UserId> sharedWith_; + bool isPublic_; + + public: + UserLayer(const BackgroundColor& color, + const std::string& name); + + explicit UserLayer(const Json::Value& serialized); + + virtual std::string GetId() const ORTHANC_OVERRIDE + { + return id_; + } + + bool IsVisible() const + { + return isVisible_; + } + + const BackgroundColor& GetColor() const + { + return color_; + } + + const std::string& GetName() const + { + return name_; + } + + bool IsSharedWith(ProjectRole authorRole, + const UserId& viewerId, + ProjectRole viewerRole) const; + + void Assign(const UserLayer& other); + + virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE; + }; +}
--- a/ViewerPlugin/CMakeLists.txt Mon Sep 07 15:12:02 2026 +0200 +++ b/ViewerPlugin/CMakeLists.txt Mon Sep 07 20:58:51 2026 +0200 @@ -37,7 +37,7 @@ # Advanced parameters to fine-tune linking against system libraries SET(USE_SYSTEM_OPENJPEG ON CACHE BOOL "Use the system version of OpenJpeg") SET(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK") -set(ORTHANC_SDK_VERSION "1.7.0" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"framework\" or \"1.7.0\")") +SET(ORTHANC_SDK_VERSION "1.12.9" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"framework\", \"1.7.0\", or \"1.12.9\")") ##################################################################### @@ -99,6 +99,8 @@ if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK) if (ORTHANC_SDK_VERSION STREQUAL "1.7.0") include_directories(${CMAKE_SOURCE_DIR}/../Resources/Orthanc/Sdk-1.7.0) + elseif (ORTHANC_SDK_VERSION STREQUAL "1.12.9") + include_directories(${CMAKE_SOURCE_DIR}/../Resources/Orthanc/Sdk-1.12.9) elseif (ORTHANC_SDK_VERSION STREQUAL "framework") include_directories(${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Include/) else() @@ -183,17 +185,33 @@ ##################################################################### set(ORTHANC_WSI_SOURCES + Annotations/AnnotationsRestApi.cpp + Annotations/AnnotationsWorkspace.cpp + Annotations/AnnotationsWorkspaceId.cpp + Annotations/CachedAnnotationsWorkspace.cpp + Annotations/CachedUserFeatures.cpp + Annotations/IAuthenticatedUser.cpp + Annotations/ISerializable.cpp + Annotations/ImportedLayer.cpp + Annotations/LayersCollection.cpp + Annotations/ProjectInformation.cpp + Annotations/UserAnnotationsSettings.cpp + Annotations/UserFeatures.cpp + Annotations/UserId.cpp + Annotations/UserLayer.cpp DicomPyramidCache.cpp IIIF.cpp OrthancPluginConnection.cpp OrthancPyramidFrameFetcher.cpp Plugin.cpp RawTile.cpp + ViewerConfiguration.cpp + ViewerToolbox.cpp ${ORTHANC_WSI_DIR}/Framework/BackgroundColor.cpp ${ORTHANC_WSI_DIR}/Framework/ColorSpaces.cpp ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp - ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp + ${ORTHANC_WSI_DIR}/Framework/FrameworkEnumerations.cpp ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedPyramidCache.cpp ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedTiledPyramid.cpp
--- a/ViewerPlugin/DicomPyramidCache.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/ViewerPlugin/DicomPyramidCache.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -139,7 +139,15 @@ while (!cache_.IsEmpty()) { DicomPyramid* pyramid = NULL; - std::string seriesId = cache_.RemoveOldest(pyramid); + + try + { + /* std::string seriesId = */ cache_.RemoveOldest(pyramid); + } + catch (Orthanc::OrthancException&) + { + // Should never happen, don't throw exceptions in destructor + } if (pyramid != NULL) { @@ -154,7 +162,7 @@ { if (singleton_.get() == NULL) { - singleton_.reset(new DicomPyramidCache(new OrthancWSI::OrthancPluginConnection, maxSize, useMetadataCache)); + singleton_.reset(new DicomPyramidCache(new OrthancPluginConnection, maxSize, useMetadataCache)); } else {
--- a/ViewerPlugin/OrthancPyramidFrameFetcher.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/ViewerPlugin/OrthancPyramidFrameFetcher.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -91,11 +91,11 @@ } - DecodedTiledPyramid* OrthancPyramidFrameFetcher::Fetch(const std::string &instanceId, + DecodedTiledPyramid* OrthancPyramidFrameFetcher::Fetch(const std::string& instanceId, unsigned frameNumber) { OrthancPlugins::MemoryBuffer buffer; - buffer.GetDicomInstance(instanceId.c_str()); + buffer.GetDicomInstance(instanceId); OrthancPlugins::DicomInstance dicom(buffer.GetData(), buffer.GetSize()); @@ -129,7 +129,7 @@ if (paddingX_ >= 2) { - paddedWidth = OrthancWSI::CeilingDivision(frame->GetWidth(), paddingX_) * paddingX_; + paddedWidth = CeilingDivision(frame->GetWidth(), paddingX_) * paddingX_; } else { @@ -138,7 +138,7 @@ if (paddingY_ >= 2) { - paddedHeight = OrthancWSI::CeilingDivision(frame->GetHeight(), paddingY_) * paddingY_; + paddedHeight = CeilingDivision(frame->GetHeight(), paddingY_) * paddingY_; } else {
--- a/ViewerPlugin/Plugin.cpp Mon Sep 07 15:12:02 2026 +0200 +++ b/ViewerPlugin/Plugin.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -23,21 +23,18 @@ #include "../Framework/PrecompiledHeadersWSI.h" -#include "OrthancPyramidFrameFetcher.h" +#include "../Framework/ColorSpaces.h" +#include "../Framework/ImageToolbox.h" +#include "Annotations/AnnotationsRestApi.h" #include "DicomPyramidCache.h" #include "IIIF.h" +#include "OrthancPluginConnection.h" +#include "OrthancPyramidFrameFetcher.h" #include "RawTile.h" -#include "../Framework/ColorSpaces.h" -#include "../Framework/Inputs/DecodedTiledPyramid.h" -#include "../Framework/Inputs/OnTheFlyPyramid.h" -#include "../Framework/Inputs/DecodedPyramidCache.h" -#include "../Framework/ImageToolbox.h" +#include "ViewerConfiguration.h" +#include "ViewerToolbox.h" -#include <Compatibility.h> // For std::unique_ptr -#include <Images/Image.h> -#include <Images/ImageProcessing.h> #include <Logging.h> -#include <OrthancException.h> #include <SystemToolbox.h> #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" @@ -45,9 +42,6 @@ #include <EmbeddedResources.h> #include <cassert> -#include <Images/PngReader.h> - -#include "OrthancPluginConnection.h" #define ORTHANC_PLUGIN_NAME "wsi" @@ -140,8 +134,7 @@ } } - std::string s = answer.toStyledString(); - OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json"); + OrthancWSI::ViewerToolbox::AnswerJson(output, answer); } @@ -168,8 +161,7 @@ DescribePyramid(answer, accessor.GetPyramid()); } - std::string s = answer.toStyledString(); - OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json"); + OrthancWSI::ViewerToolbox::AnswerJson(output, answer); } @@ -462,6 +454,7 @@ #endif + extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) @@ -496,6 +489,14 @@ Orthanc::Logging::Initialize(context); #endif +#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) + LOG(WARNING) << "The whole-slide imaging viewer was compiled against an old " + << "version of the Orthanc SDK, annotations will not be persistent"; +#elif !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9) + LOG(WARNING) << "The whole-slide imaging viewer was compiled against an old " + << "version of the Orthanc SDK, per-user annotations are not supported"; +#endif + try { /** @@ -514,7 +515,7 @@ an a* or b* of -128.0, 0x8080 corresponds to an a* or b* of 0.0 and 0xFFFF corresponds to an a* or b* of 127.0 - **/ + **/ OrthancWSI::LABColor lab; if (!OrthancWSI::LABColor::DecodeDicomRecommendedAbsentPixelCIELab(lab, "65535\\0\\0") || @@ -539,122 +540,114 @@ return -1; } - // Limit the number of PNG transcoders to the number of available - // hardware threads (e.g. number of CPUs or cores or - // hyperthreading units) - unsigned int threads = Orthanc::SystemToolbox::GetHardwareConcurrency(); - OrthancWSI::RawTile::InitializeTranscoderSemaphore(threads); - - LOG(WARNING) << "The whole-slide imaging plugin will use at most " << threads << " threads to transcode the tiles"; - - OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Provides a Web viewer of whole-slide microscopic images within Orthanc."); - - OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */, - true /* Use the metadata cache - Should be "false" only during development */); - + try { - std::unique_ptr<OrthancWSI::OrthancPyramidFrameFetcher> fetcher( - new OrthancWSI::OrthancPyramidFrameFetcher(new OrthancWSI::OrthancPluginConnection(), false /* smooth - TODO PARAMETER */)); - fetcher->SetPaddingX(64); // TODO PARAMETER - fetcher->SetPaddingY(64); // TODO PARAMETER - fetcher->SetDefaultBackgroundColor(255, 255, 255); // TODO PARAMETER + // Limit the number of PNG transcoders to the number of available + // hardware threads (e.g. number of CPUs or cores or + // hyperthreading units) + unsigned int threads = Orthanc::SystemToolbox::GetHardwareConcurrency(); + OrthancWSI::RawTile::InitializeTranscoderSemaphore(threads); + + LOG(WARNING) << "The whole-slide imaging plugin will use at most " << threads << " threads to transcode the tiles"; + + OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Provides a Web viewer of whole-slide microscopic images within Orthanc."); + + OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */, + true /* Use the metadata cache - Should be "false" only during development */); - OrthancWSI::DecodedPyramidCache::InitializeInstance(fetcher.release(), - 10 /* TODO - PARAMETER */, - 256 * 1024 * 1024 /* TODO - PARAMETER */); - } + { + std::unique_ptr<OrthancWSI::OrthancPyramidFrameFetcher> fetcher( + new OrthancWSI::OrthancPyramidFrameFetcher(new OrthancWSI::OrthancPluginConnection(), false /* smooth - TODO PARAMETER */)); + fetcher->SetPaddingX(64); // TODO PARAMETER + fetcher->SetPaddingY(64); // TODO PARAMETER + fetcher->SetDefaultBackgroundColor(255, 255, 255); // TODO PARAMETER - OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); + OrthancWSI::DecodedPyramidCache::InitializeInstance(fetcher.release(), + 10 /* TODO - PARAMETER */, + 256 * 1024 * 1024 /* TODO - PARAMETER */); + } - OrthancPlugins::RegisterRestCallback<ServeJavaScriptLibraries>("/wsi/libs/(.*)", true); + OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); + + OrthancPlugins::RegisterRestCallback<ServeJavaScriptLibraries>("/wsi/libs/(.*)", true); #if ORTHANC_STANDALONE == 1 - OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.html)", true); - OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.js)", true); + OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.html)", true); + OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.js)", true); #else - OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.html)", true); - OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.js)", true); + OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.html)", true); + OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.js)", true); #endif - OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true); - OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true); - OrthancPlugins::RegisterRestCallback<ServeFramePyramid>("/wsi/frames-pyramids/([0-9a-f-]+)/([0-9-]+)", true); - OrthancPlugins::RegisterRestCallback<ServeFrameTile>("/wsi/frames-tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true); + OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true); + OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true); + OrthancPlugins::RegisterRestCallback<ServeFramePyramid>("/wsi/frames-pyramids/([0-9a-f-]+)/([0-9-]+)", true); + OrthancPlugins::RegisterRestCallback<ServeFrameTile>("/wsi/frames-tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true); - OrthancPlugins::OrthancConfiguration mainConfiguration; + const bool enableIIIF = OrthancWSI::ViewerConfiguration::GetInstance().IsIIIFEnabled(); + bool serveMirador = false; + bool serveOpenSeadragon = false; - OrthancPlugins::OrthancConfiguration wsiConfiguration; - mainConfiguration.GetSection(wsiConfiguration, "WholeSlideImaging"); + if (enableIIIF) + { + std::string iiifPublicUrl; + InitializeIIIF(iiifPublicUrl); + + serveMirador = OrthancWSI::ViewerConfiguration::GetInstance().IsServeMirador(); + serveOpenSeadragon = OrthancWSI::ViewerConfiguration::GetInstance().IsServeOpenSeadragon(); - const bool enableIIIF = wsiConfiguration.GetBooleanValue("EnableIIIF", true); - bool serveMirador = false; - bool serveOpenSeadragon = false; - std::string iiifPublicUrl; - - if (enableIIIF) - { - if (!wsiConfiguration.LookupStringValue(iiifPublicUrl, "OrthancPublicURL")) - { - unsigned int port = mainConfiguration.GetUnsignedIntegerValue("HttpPort", 8042); - iiifPublicUrl = "http://localhost:" + boost::lexical_cast<std::string>(port) + "/"; - } - - if (iiifPublicUrl.empty() || - iiifPublicUrl[iiifPublicUrl.size() - 1] != '/') - { - iiifPublicUrl += "/"; + bool value; + if (OrthancWSI::ViewerConfiguration::GetInstance().LookupForcePowersOfTwoScaleFactors(value)) + { + SetIIIFForcePowersOfTwoScaleFactors(value); + } + else + { + /** + * By default, compatibility mode is disabled. However, if + * Mirador or OSD are enabled, compatibility mode is + * automatically enabled to enhance user experience, at least + * until issue 2379 of OSD is solved: + * https://github.com/openseadragon/openseadragon/issues/2379 + **/ + SetIIIFForcePowersOfTwoScaleFactors(serveMirador || serveOpenSeadragon); + } } - iiifPublicUrl += "wsi/iiif/"; - - InitializeIIIF(iiifPublicUrl); + LOG(WARNING) << "Support of IIIF is " << (enableIIIF ? "enabled" : "disabled") << " in the whole-slide imaging plugin"; - serveMirador = wsiConfiguration.GetBooleanValue("ServeMirador", false); - serveOpenSeadragon = wsiConfiguration.GetBooleanValue("ServeOpenSeadragon", false); - - bool value; - if (wsiConfiguration.LookupBooleanValue(value, "ForcePowersOfTwoScaleFactors")) + if (serveMirador) { - SetIIIFForcePowersOfTwoScaleFactors(value); + OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(mirador.html)", true); } - else + + if (serveOpenSeadragon) { - /** - * By default, compatibility mode is disabled. However, if - * Mirador or OSD are enabled, compatibility mode is - * automatically enabled to enhance user experience, at least - * until issue 2379 of OSD is solved: - * https://github.com/openseadragon/openseadragon/issues/2379 - **/ - SetIIIFForcePowersOfTwoScaleFactors(serveMirador || serveOpenSeadragon); + OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(openseadragon.html)", true); } - } + + { + // Extend the default Orthanc Explorer with custom JavaScript for WSI - LOG(WARNING) << "Support of IIIF is " << (enableIIIF ? "enabled" : "disabled") << " in the whole-slide imaging plugin"; + std::string explorer; + Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER); - if (serveMirador) - { - OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(mirador.html)", true); + std::map<std::string, std::string> dictionary; + dictionary["ENABLE_IIIF"] = (enableIIIF ? "true" : "false"); + dictionary["SERVE_MIRADOR"] = (serveMirador ? "true" : "false"); + dictionary["SERVE_OPEN_SEADRAGON"] = (serveOpenSeadragon ? "true" : "false"); + explorer = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary); + + OrthancPlugins::ExtendOrthancExplorer(ORTHANC_PLUGIN_NAME, explorer); + } + + // New in WSI 4.0 + RegisterAnnotationsRestApi(); } - - if (serveOpenSeadragon) - { - OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(openseadragon.html)", true); - } - + catch (Orthanc::OrthancException& e) { - // Extend the default Orthanc Explorer with custom JavaScript for WSI - - std::string explorer; - Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER); - - std::map<std::string, std::string> dictionary; - dictionary["ENABLE_IIIF"] = (enableIIIF ? "true" : "false"); - dictionary["SERVE_MIRADOR"] = (serveMirador ? "true" : "false"); - dictionary["SERVE_OPEN_SEADRAGON"] = (serveOpenSeadragon ? "true" : "false"); - explorer = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary); - - OrthancPlugins::ExtendOrthancExplorer(ORTHANC_PLUGIN_NAME, explorer); + LOG(ERROR) << "Exception while starting whole-slide imaging viewer: " << e.What(); + return -1; } return 0;
--- a/ViewerPlugin/RawTile.h Mon Sep 07 15:12:02 2026 +0200 +++ b/ViewerPlugin/RawTile.h Mon Sep 07 20:58:51 2026 +0200 @@ -23,7 +23,7 @@ #pragma once -#include "../Framework/Enumerations.h" +#include "../Framework/FrameworkEnumerations.h" #include "../Framework/Inputs/ITiledPyramid.h" #include <orthanc/OrthancCPlugin.h>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/ViewerConfiguration.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,239 @@ +/** + * 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-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../Framework/PrecompiledHeadersWSI.h" +#include "ViewerConfiguration.h" + +#include <SerializationToolbox.h> +#include <Toolbox.h> + + +static const char* const KEY_AUTHENTICATION_SOURCE = "AuthenticationSource"; +static const char* const KEY_AUTHENTICATION_HTTP_HEADER = "AuthenticationHttpHeader"; +static const char* const KEY_AUTHENTICATION_ENABLED = "AuthenticationEnabled"; + + +namespace OrthancWSI +{ + ViewerConfiguration::ViewerConfiguration() + { + mainConfiguration_.GetSection(wsiConfiguration_, "WholeSlideImaging"); + + std::string value; + if (wsiConfiguration_.LookupStringValue(value, KEY_AUTHENTICATION_SOURCE)) + { + if (value == "None") + { + authenticationSource_ = AuthenticationSource_None; + } + else if (value == "HttpHeader") + { + authenticationSource_ = AuthenticationSource_HttpHeader; + + if (!wsiConfiguration_.LookupStringValue(authenticationHttpHeader_, KEY_AUTHENTICATION_HTTP_HEADER) || + authenticationHttpHeader_.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Configuration option \"" + + std::string(KEY_AUTHENTICATION_HTTP_HEADER) + "\" must be defined and non-empty"); + } + + Orthanc::Toolbox::ToLowerCase(authenticationHttpHeader_); + + if (!wsiConfiguration_.LookupSetOfStrings(instructors_, "Instructors", false)) + { + instructors_.clear(); + } + } + else if (value == "RegisteredUsers") + { + if (mainConfiguration_.GetBooleanValue(KEY_AUTHENTICATION_ENABLED, false)) + { + authenticationSource_ = AuthenticationSource_RegisteredUsers; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Configuration option \"" + + std::string(KEY_AUTHENTICATION_ENABLED) + "\" must be set to \"true\" " + + "to use the registered users as the authentication source"); + } + } + else if (value == "Plugin") + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9) + authenticationSource_ = AuthenticationSource_Plugin; +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Your Orthanc SDK is too old to support " + "authentication from plugins, check configuration option \"" + + std::string(KEY_AUTHENTICATION_SOURCE) + "\""); +#endif + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Unknown source of authentication: " + value); + } + } + else + { + authenticationSource_ = AuthenticationSource_None; + } + } + + + const ViewerConfiguration& ViewerConfiguration::GetInstance() + { + static ViewerConfiguration configuration; + return configuration; + } + + + bool ViewerConfiguration::IsIIIFEnabled() const + { + return wsiConfiguration_.GetBooleanValue("EnableIIIF", true); + } + + + std::string ViewerConfiguration::GetOrthancPublicUrl() const + { + std::string base; + + if (wsiConfiguration_.LookupStringValue(base, "OrthancPublicURL")) + { + return base; + } + else + { + unsigned int port = mainConfiguration_.GetUnsignedIntegerValue("HttpPort", 8042); + return "http://localhost:" + boost::lexical_cast<std::string>(port); + } + } + + + std::string ViewerConfiguration::GetIIIFPublicUrl() const + { + if (IsIIIFEnabled()) + { + return Orthanc::Toolbox::JoinUri(GetOrthancPublicUrl(), "wsi/iiif/"); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + bool ViewerConfiguration::IsServeMirador() const + { + if (IsIIIFEnabled()) + { + return wsiConfiguration_.GetBooleanValue("ServeMirador", false); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + bool ViewerConfiguration::IsServeOpenSeadragon() const + { + if (IsIIIFEnabled()) + { + return wsiConfiguration_.GetBooleanValue("ServeOpenSeadragon", false); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + bool ViewerConfiguration::LookupForcePowersOfTwoScaleFactors(bool& value) const + { + if (IsIIIFEnabled()) + { + return wsiConfiguration_.LookupBooleanValue(value, "ForcePowersOfTwoScaleFactors"); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + bool ViewerConfiguration::AreAnnotationsEnabled() const + { + return wsiConfiguration_.GetBooleanValue("EnableAnnotations", true); + } + + + const std::string& ViewerConfiguration::GetAuthenticationHttpHeader() const + { + if (authenticationSource_ != AuthenticationSource_HttpHeader) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + assert(!authenticationHttpHeader_.empty()); + return authenticationHttpHeader_; + } + } + + + unsigned int ViewerConfiguration::GetAnnotationsCacheSize() const + { + return 100; // TODO - Configuration option? + } + + + unsigned int ViewerConfiguration::GetFeaturesCacheSize() const + { + return 100; // TODO - Configuration option? + } + + + bool ViewerConfiguration::IsAnnotationsSharingEnabled() const + { + return wsiConfiguration_.GetBooleanValue("EnableAnnotationsSharing", false); + } + + + bool ViewerConfiguration::IsInstructor(const std::string& username) const + { + if (username.empty()) + { + return false; + } + else + { + return instructors_.find(username) != instructors_.end(); + } + } + + + bool ViewerConfiguration::IsLearnerToLearnerSharingEnabled() const + { + return wsiConfiguration_.GetBooleanValue("EnableLearnerToLearnerSharing", false); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/ViewerConfiguration.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../Framework/FrameworkEnumerations.h" + +#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + + +namespace OrthancWSI +{ + class ViewerConfiguration + { + private: + OrthancPlugins::OrthancConfiguration mainConfiguration_; + OrthancPlugins::OrthancConfiguration wsiConfiguration_; + AuthenticationSource authenticationSource_; + std::string authenticationHttpHeader_; + std::set<std::string> instructors_; + + ViewerConfiguration(); + + public: + static const ViewerConfiguration& GetInstance(); + + bool IsIIIFEnabled() const; + + std::string GetOrthancPublicUrl() const; + + std::string GetIIIFPublicUrl() const; + + bool IsServeMirador() const; + + bool IsServeOpenSeadragon() const; + + bool LookupForcePowersOfTwoScaleFactors(bool& value) const; + + bool AreAnnotationsEnabled() const; + + AuthenticationSource GetAuthenticationSource() const + { + return authenticationSource_; + } + + const std::string& GetAuthenticationHttpHeader() const; + + unsigned int GetAnnotationsCacheSize() const; + + unsigned int GetFeaturesCacheSize() const; + + bool IsAnnotationsSharingEnabled() const; + + bool IsInstructor(const std::string& username) const; + + bool IsLearnerToLearnerSharingEnabled() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/ViewerToolbox.cpp Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,112 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../Framework/PrecompiledHeadersWSI.h" +#include "ViewerToolbox.h" + +#include <Logging.h> +#include <Toolbox.h> + +#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + + +static const char* const KEY_VALUE_STORE = "wsi"; + + +namespace OrthancWSI +{ + namespace ViewerToolbox + { + void AnswerJson(OrthancPluginRestOutput* output, + const Json::Value& value) + { + std::string s; + Orthanc::Toolbox::WriteFastJson(s, value); + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json"); + } + + + void AnswerEmpty(OrthancPluginRestOutput* output) + { + Json::Value answer = Json::objectValue; + AnswerJson(output, answer); + } + + + void SetKeyValueStore(const std::string& key, + const std::string& value) + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) + OrthancPlugins::KeyValueStore store(KEY_VALUE_STORE); + store.Store(key, value); +#else + LOG(WARNING) << "Your Orthanc SDK is too old to save annotations"; +#endif + } + + + void SetKeyValueStore(const std::string& key, + const Json::Value& value) + { + std::string s; + Orthanc::Toolbox::WriteFastJson(s, value); + SetKeyValueStore(key, s); + } + + + bool LookupKeyValueStore(std::string& value, + const std::string& key) + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8) + OrthancPlugins::KeyValueStore store(KEY_VALUE_STORE); + return store.GetValue(value, key); +#else + LOG(WARNING) << "Your Orthanc SDK is too old to load annotations"; + return false; +#endif + } + + + bool LookupKeyValueStore(Json::Value& value, + const std::string& key) + { + std::string s; + if (LookupKeyValueStore(s, key)) + { + if (Orthanc::Toolbox::ReadJson(value, s)) + { + return true; + } + else + { + LOG(WARNING) << "Discarding incorrect JSON in the key-value store: " << key; + return false; + } + } + else + { + return false; + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/ViewerToolbox.h Mon Sep 07 20:58:51 2026 +0200 @@ -0,0 +1,51 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <json/value.h> +#include <orthanc/OrthancCPlugin.h> + + +namespace OrthancWSI +{ + namespace ViewerToolbox + { + void AnswerJson(OrthancPluginRestOutput* output, + const Json::Value& value); + + void AnswerEmpty(OrthancPluginRestOutput* output); + + void SetKeyValueStore(const std::string& key, + const std::string& value); + + void SetKeyValueStore(const std::string& key, + const Json::Value& value); + + bool LookupKeyValueStore(std::string& value, + const std::string& key); + + bool LookupKeyValueStore(Json::Value& value, + const std::string& key); + } +}
--- a/ViewerPlugin/WebApplication/viewer.html Mon Sep 07 15:12:02 2026 +0200 +++ b/ViewerPlugin/WebApplication/viewer.html Mon Sep 07 20:58:51 2026 +0200 @@ -3,7 +3,7 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Orthanc for Whole-Slide Imaging</title> <link rel="stylesheet" href="../libs/css/bootstrap.min.css" type="text/css"> @@ -14,6 +14,7 @@ body { margin: 0; height: 100%; + overflow: hidden; } #map { @@ -23,41 +24,682 @@ width: 100%; } - .ol-rotate { - top: 4em; - left: .5em; - right: initial; + #toolbar-left { /* the top/left coordinates are adjusted in JavaScript */ + position: absolute; + z-index: 20; + } + + #toolbar-left .ol-rotate { + right: auto; + } + + #toolbar-top { /* the top/left coordinates are adjusted in JavaScript */ + padding-left: 1em; + position: absolute; + z-index: 20; + } + + .icon-btn { /* the width and height can be adjusted in JavaScript */ + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px; + } + + .icon-btn:disabled { + opacity: 100%; + background-color: white; + } + + .btn-outline-secondary { + background-color: white; /* make the toolbar buttons opaque */ + } + + .offcanvas { + background-color: #E5E5E5; + } + + th { + background-color: transparent !important; + } + + td { + background-color: transparent !important; + } + + #right-panel { + --bs-offcanvas-width: 300px; + --bs-offcanvas-border-width: 0; + /*transition: 0.1s ease-in-out;*/ /* this gives a fast animation */ + /*transition: none;*/ /* this removes the animation */ } + + #right-panel-toggle { + position: fixed; + top: 0; + bottom: 0; + z-index: 20; + border-radius: 4px 0 0 4px; + border-right: none; + opacity: 0.75; + padding: 0px; + } + + .ol-attribution { + right: 20px !important; /* keep the description out of the vertical toggle button */ + } + + /* Bootstrap Icons */ + .bi { + display: inline-block; + width: 1em; + height: 1em; + background-color: currentColor; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + } + + /* The used icons must be added to ../Resources/CMake/JavaScriptLibraries.cmake */ + .bi-arrow-clockwise { -webkit-mask-image: url("../libs/svg/arrow-clockwise.svg"); mask-image: url("../libs/svg/arrow-clockwise.svg"); } + .bi-arrow-up-right { -webkit-mask-image: url("../libs/svg/arrow-up-right.svg"); mask-image: url("../libs/svg/arrow-up-right.svg"); } + .bi-arrows { -webkit-mask-image: url("../libs/svg/arrows.svg"); mask-image: url("../libs/svg/arrows.svg"); } + .bi-arrows-fullscreen { -webkit-mask-image: url("../libs/svg/arrows-fullscreen.svg"); mask-image: url("../libs/svg/arrows-fullscreen.svg"); } + .bi-arrows-move { -webkit-mask-image: url("../libs/svg/arrows-move.svg"); mask-image: url("../libs/svg/arrows-move.svg"); } + .bi-brightness-high { -webkit-mask-image: url("../libs/svg/brightness-high.svg"); mask-image: url("../libs/svg/brightness-high.svg"); } + .bi-camera { -webkit-mask-image: url("../libs/svg/camera.svg"); mask-image: url("../libs/svg/camera.svg"); } + .bi-chevron-compact-left { -webkit-mask-image: url("../libs/svg/chevron-compact-left.svg"); mask-image: url("../libs/svg/chevron-compact-left.svg"); } + .bi-chevron-compact-right { -webkit-mask-image: url("../libs/svg/chevron-compact-right.svg"); mask-image: url("../libs/svg/chevron-compact-right.svg"); } + .bi-circle { -webkit-mask-image: url("../libs/svg/circle.svg"); mask-image: url("../libs/svg/circle.svg"); } + .bi-cloud-download { -webkit-mask-image: url("../libs/svg/cloud-download.svg"); mask-image: url("../libs/svg/cloud-download.svg"); } + .bi-eye { -webkit-mask-image: url("../libs/svg/eye.svg"); mask-image: url("../libs/svg/eye.svg"); } + .bi-eye-slash { -webkit-mask-image: url("../libs/svg/eye-slash.svg"); mask-image: url("../libs/svg/eye-slash.svg"); } + .bi-file-earmark-plus { -webkit-mask-image: url("../libs/svg/file-earmark-plus.svg"); mask-image: url("../libs/svg/file-earmark-plus.svg"); } + .bi-freehand-closed { -webkit-mask-image: url("../libs/svg/freehand-area-svgrepo-com.svg"); mask-image: url("../libs/svg/freehand-area-svgrepo-com.svg"); } + .bi-freehand-line { -webkit-mask-image: url("../libs/svg/freehand-svgrepo-com.svg"); mask-image: url("../libs/svg/freehand-svgrepo-com.svg"); } + .bi-geo-alt { -webkit-mask-image: url("../libs/svg/geo-alt.svg"); mask-image: url("../libs/svg/geo-alt.svg"); } + .bi-hand-index { -webkit-mask-image: url("../libs/svg/hand-index.svg"); mask-image: url("../libs/svg/hand-index.svg"); } + .bi-pen { -webkit-mask-image: url("../libs/svg/pen.svg"); mask-image: url("../libs/svg/pen.svg"); } + .bi-pentagon { -webkit-mask-image: url("../libs/svg/pentagon.svg"); mask-image: url("../libs/svg/pentagon.svg"); } + .bi-share { -webkit-mask-image: url("../libs/svg/share.svg"); mask-image: url("../libs/svg/share.svg"); } + .bi-square { -webkit-mask-image: url("../libs/svg/square.svg"); mask-image: url("../libs/svg/square.svg"); } + .bi-trash { -webkit-mask-image: url("../libs/svg/trash.svg"); mask-image: url("../libs/svg/trash.svg"); } + + + .ol-scale-magnification { + color: #000; + font-size: 12px; + text-align: center; + margin-top: 2px; + } + </style> </head> <body> - <div id="map"> - </div> + <div id="app"> + <div id="map" :style="{ background: mapBackground }"> + <div id="toolbar-top" style="display:none" v-show="toolbarsVisible"> + <div class="btn-group" role="group" v-if="workspaceInfo.enabled"> + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'select' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Select annotation" + v-on:click="ToggleSelectTool()"> + <i class="bi bi-hand-index"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'line' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw line" + v-on:click="ToggleDrawTool('line')"> + <i class="bi bi-arrows"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'point' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw point" + v-on:click="ToggleDrawTool('point')"> + <i class="bi bi-geo-alt"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'circle' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw circle" + v-on:click="ToggleDrawTool('circle')"> + <i class="bi bi-circle"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'rectangle' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw rectangle" + v-on:click="ToggleDrawTool('rectangle')"> + <i class="bi bi-square"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'closed-polygon' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw closed polygon" + v-on:click="ToggleDrawTool('closed-polygon')"> + <i class="bi bi-pentagon"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'freehand-line' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw freehand line" + v-on:click="ToggleDrawTool('freehand-line')"> + <i class="bi bi-freehand-line"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'freehand' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw freehand closed polygon" + v-on:click="ToggleDrawTool('freehand')"> + <i class="bi bi-freehand-closed"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'arrow' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw arrow" + v-on:click="ToggleDrawTool('arrow')"> + <i class="bi bi-arrow-up-right"></i> + </button> + </div> - <div style="display:none"> - <div id="popover-content"> - <div class="container"> - <div class="row mb-2"> - <div class="btn-group btn-group-sm" role="group"> - <button type="button" class="btn btn-outline-dark" id="rotation-reset">Reset</button> - <button type="button" class="btn btn-outline-dark" id="rotation-plus90">+90°</button> - <button type="button" class="btn btn-outline-dark" id="rotation-minus90">-90°</button> + <div class="btn-group" role="group" v-if="workspaceInfo.enabled"> + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'move' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Move annotation" + v-on:click="ToggleDrawTool('move')"> + <i class="bi bi-arrows-move"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + :class="{ active: activeDrawTool === 'modify' }" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Modify points" + v-on:click="ToggleDrawTool('modify')"> + <i class="bi bi-pen"></i> + </button> + + <button type="button" class="btn btn-outline-secondary icon-btn" + data-bs-toggle="tooltip" data-bs-placement="bottom" title="Delete selected annotation" + v-bind:disabled="selectedFeature === null || selectedFeature.get('layer-id') !== activeUserLayerId" + v-on:click="DeleteSelectedAnnotation()"> + <i class="bi bi-trash"></i> + </button> + </div> + + <div class="alert alert-danger alert-dismissible d-inline-block py-0" role="alert" + v-if="workspaceInfo.enabled && !workspaceInfo.persistent"> + Your annotations will not be saved + <button type="button" class="btn-close top-50 translate-middle-y" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + </div> + + <div id="toolbar-left" style="display:none" v-show="toolbarsVisible"> + <div id="toolbar-left-content" style="position:absolute; display:flex; flex-direction:column; align-items:center;"> + <div class="btn-group-vertical" role="group" style="padding-top: 1em"> + <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons" + data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 1x" + v-on:click="SetMagnification(map, referenceMagnification, 1)"> + <small>1x</small> + </button> + + <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons" + data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 4x" + v-on:click="SetMagnification(map, referenceMagnification, 4)"> + <small>4x</small> + </button> + + <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons" + data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 10x" + v-on:click="SetMagnification(map, referenceMagnification, 10)"> + <small>10x</small> + </button> + + <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons" + data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 20x" + v-on:click="SetMagnification(map, referenceMagnification, 20)"> + <small>20x</small> + </button> + + <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons" + data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 40x" + v-on:click="SetMagnification(map, referenceMagnification, 40)"> + <small>40x</small> + </button> + + <button type="button" class="btn btn-outline-secondary d-inline-flex" id="button-adjustments"> + <i class="bi bi-brightness-high"></i> + </button> + + <button type="button" class="btn btn-outline-secondary d-inline-flex" + data-bs-toggle="tooltip" data-bs-placement="right" title="Take a screenshot" + v-on:click="TakeScreenshot()"> + <i class="bi bi-camera"></i> + </button> + </div> + <div id="toolbar-spinner" v-show="showSpinner" style="padding-top:1em;"> + <div class="spinner-border spinner-border-sm text-secondary" role="status"> + <span class="visually-hidden">Saving...</span> + </div> </div> </div> - <div class="row"> - <input type="range" class="form-range" min="-180" max="180" id="rotation-slider"> + </div> + </div> + + <div class="offcanvas offcanvas-end" data-bs-backdrop="false" data-bs-scroll="true" + tabindex="-1" id="right-panel" v-show="workspaceInfo.enabled"> + <div class="offcanvas-header"> + <h5 class="offcanvas-title">Annotations</h5> + <button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button> + </div> + <div class="offcanvas-body p-2"> + <h5 v-if="workspaceInfo.name !== ''">{{ workspaceInfo.name }}</h5> + + <p v-if="workspaceInfo.description !== ''"> + <small>{{ workspaceInfo.description }}</small> + </p> + + <p v-if="workspaceInfo.user !== ''"> + <small>Logged as:</small> + <span class="badge bg-secondary">{{ workspaceInfo.user }}</span> + </p> + + <div class="d-flex align-items-center mb-1"> + <h5>User layers</h5> + <button type="button" class="btn btn-primary btn-sm ms-auto" + v-on:click="CreateUserLayer()"> + <i class="bi bi-file-earmark-plus"></i> + Create + </button> + </div> + <table class="table table-sm table-borderless mb-0 align-middle"> + <thead class="small text-muted"> + <tr> + <th class="p-0 text-center fw-normal" title="Editing layer">E</th> + <th class="p-0 text-center fw-normal" title="Visible">V</th> + <th class="p-0 text-center fw-normal" title="Color">C</th> + <th class="px-1 py-0 fw-normal">Name</th> + <th class="p-0 text-center fw-normal" title="Share layer with other users" + v-if="workspaceInfo.sharing">S</th> + <th class="p-0"></th> + </tr> + </thead> + <tbody> + <tr v-for="layer in userLayers"> + <td class="text-center align-middle p-0"> + <input type="radio" class="form-check-input m-0" + v-bind:checked="activeUserLayerId === layer.id" + v-on:change="activeUserLayerId = layer.id"> + </td> + <td class="text-center align-middle p-0"> + <button type="button" class="btn btn-link p-1" style="color:inherit"> + <i class="bi" + :class="layer.visible ? 'bi-eye' : 'bi-eye-slash'" + v-on:click="layer.visible = !layer.visible; drawLayer.changed(); SaveUserLayer(layer)" + ></i> + </button> + </td> + <td class="text-center align-middle p-0"> + <input type="color" style="width:22px;height:22px;padding:1px;border:none;cursor:pointer;border-radius:3px" + v-model:value="layer.color" + v-on:input="drawLayer.changed()" + v-on:change="SaveUserLayer(layer)"> + </td> + <td class="align-middle px-1 py-0"> + <input type="text" class="form-control form-control-sm py-0" + v-model:value="layer.name" + v-on:change="SaveUserLayer(layer)"> + </td> + <td class="text-center align-middle p-0" v-if="workspaceInfo.sharing"> + <button type="button" class="btn btn-link p-1" + v-on:click="ShowShareUserLayerModal(layer)"> + <i class="bi bi-share"></i> + </button> + </td> + <td class="text-center align-middle p-0"> + <button type="button" class="btn btn-link p-0" + :class="userLayers.length <= 1 ? 'text-muted' : 'text-danger'" + v-bind:disabled="userLayers.length <= 1" + v-on:click="DeleteUserLayer(layer.id)"> + <i class="bi bi-trash"></i> + </button> + </td> + </tr> + </tbody> + </table> + + <hr class="my-4"> + + <div class="d-flex align-items-center mb-1" v-if="workspaceInfo.sharing"> + <h5>Imported layers</h5> + <button type="button" class="btn btn-primary btn-sm ms-auto" + v-on:click="ShowImportLayerModal()"> + <i class="bi bi-cloud-download"></i> + Import + </button> + </div> + + <table class="table table-sm table-borderless mb-3 align-middle" v-if="workspaceInfo.sharing"> + <thead class="small text-muted"> + <tr> + <th class="p-0 text-center fw-normal" title="Visible">V</th> + <th class="p-0 text-center fw-normal" title="Color">C</th> + <th class="px-1 py-0 fw-normal" style="width:100%">Name</th> + <th class="p-0"></th> + </tr> + </thead> + <tbody> + <tr v-for="layer in importedLayers"> + <td class="text-center align-middle p-0"> + <button type="button" class="btn btn-link p-1" style="color:inherit"> + <i class="bi" + :class="layer.visible ? 'bi-eye' : 'bi-eye-slash'" + v-on:click="layer.visible = !layer.visible; drawImportedLayer.changed(); SaveImportedLayer(layer)" + ></i> + </button> + </td> + <td class="text-center align-middle p-0"> + <input type="color" style="width:22px;height:22px;padding:1px;border:none;cursor:pointer;border-radius:3px" + v-model:value="layer.color" + v-on:input="drawImportedLayer.changed()" + v-on:change="SaveImportedLayer(layer)"> + </td> + <td class="align-middle px-1 py-0"> + <span class="small d-block">{{layer.name}}</span> + <span class="badge bg-secondary">{{ layer.author.name }}</span> + </td> + <td class="text-center align-middle p-0"> + <button type="button" class="btn btn-link p-0" + v-on:click="DeleteImportedLayer(layer.id)"> + <i class="bi bi-trash"></i> + </button> + </td> + </tr> + </tbody> + </table> + + <button class="btn btn-sm btn-primary w-100 mb-2" v-on:click="ReloadImportedFeatures()" + v-if="workspaceInfo.sharing && importedLayers.length > 0"> + <i class="bi bi-arrow-clockwise me-2 align-middle"></i> Reload imported content + </button> + + <hr class="my-4" v-if="workspaceInfo.sharing"> + + <div v-show="selectedFeature !== null" class="mb-3"> + <h5>Current selection</h5> + <button class="btn btn-sm btn-primary w-100 mb-2" v-on:click="FocusAnnotation()"> + <i class="bi bi-arrows-fullscreen me-2 align-middle"></i> Focus on annotation + </button> + <div> + <template v-for="prop in annotationProperties"> + <div v-if="prop.type === 'readonly'" class="d-flex align-items-baseline border-bottom py-1 gap-2"> + <span class="text-muted small flex-shrink-0" style="width:5em">{{prop.label}}</span> + <span class="text-break small">{{prop.value}}</span> + </div> + <div v-else-if="prop.type === 'readonly-textarea'" class="d-flex align-items-start border-bottom py-1 gap-2"> + <label class="text-muted small flex-shrink-0 mt-1 mb-0" style="width:5em">{{prop.label}}</label> + <textarea class="form-control form-control-sm small flex-grow-1" + rows="3" + readonly + style="background-color: transparent;" + v-model="prop.value"></textarea> + </div> + <div v-else-if="prop.type === 'editable'" class="d-flex align-items-center border-bottom py-1 gap-2"> + <label class="text-muted small flex-shrink-0 mb-0" style="width:5em">{{prop.label}}</label> + <input type="text" class="form-control form-control-sm small flex-grow-1" + v-model="prop.value" + v-on:change="UpdateAnnotationProperty(prop)"> + </div> + <div v-else-if="prop.type === 'editable-textarea'" class="d-flex align-items-start border-bottom py-1 gap-2"> + <label class="text-muted small flex-shrink-0 mt-1 mb-0" style="width:5em">{{prop.label}}</label> + <textarea class="form-control form-control-sm small flex-grow-1" + rows="3" + v-model="prop.value" + v-on:change="UpdateAnnotationProperty(prop)"></textarea> + </div> + <div v-else-if="prop.type === 'dropdown'" class="d-flex align-items-center border-bottom py-1 gap-2"> + <label class="text-muted small flex-shrink-0 mb-0" style="width:5em">{{prop.label}}</label> + <select class="form-select form-select-sm small flex-grow-1" + v-model="prop.value" + v-on:change="UpdateAnnotationProperty(prop)"> + <option v-for="opt in prop.options" :value="opt.value">{{opt.label}}</option> + </select> + </div> + </template> + </div> + </div> + </div> + </div> + + <button id="right-panel-toggle" type="button" class="btn btn-secondary" + style="display:none" v-show="workspaceInfo.enabled && toolbarsVisible" + data-bs-toggle="offcanvas" data-bs-target="#right-panel"> + <i class="bi" :class="panelOpen ? 'bi-chevron-compact-right' : 'bi-chevron-compact-left'"></i> + </button> + + + <!-- Confirmation modal for annotation deletion --> + <div class="modal fade" id="modal-delete-annotation" tabindex="-1"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <div class="modal-body"> + Delete this annotation? + </div> + <div class="modal-footer py-1"> + <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-sm btn-danger" v-on:click="ConfirmDeleteAnnotation()">Delete</button> + </div> + </div> + </div> + </div> + + + <!-- Confirmation modal for user layer deletion --> + <div class="modal fade" id="modal-delete-user-layer" tabindex="-1"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <div class="modal-body"> + Delete this layer and all its annotations? + </div> + <div class="modal-footer py-1"> + <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-sm btn-danger" v-on:click="UserLayerDeleteConfirmed()">Delete</button> + </div> + </div> + </div> + </div> + + + <!-- Confirmation modal for imported layer deletion --> + <div class="modal fade" id="modal-delete-imported-layer" tabindex="-1"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <div class="modal-body"> + Remove this imported layer? + </div> + <div class="modal-footer py-1"> + <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-sm btn-danger" v-on:click="ImportedLayerDeleteConfirmed()">Delete</button> + </div> + </div> + </div> + </div> + + + <!-- Modal to configure the sharing of a user layer --> + <div class="modal fade" id="modal-share-user-layer" tabindex="-1"> + <div class="modal-dialog modal-xl"> + <div class="modal-content"> + <div class="modal-header py-2"> + <h5 class="modal-title fs-6">Sharing parameters of layer "{{shareLayerTarget.name}}"</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + + <div class="form-check mb-3"> + <input class="form-check-input" type="checkbox" id="share-layer-public" + v-model="shareLayerPublic"> + <label class="form-check-label" for="share-layer-public"> + <span v-if="workspaceInfo.is_instructor">Make this layer public (accessible to any instructor or learner)</span> + <span v-if="workspaceInfo.is_learner">Make this layer visible to instructors</span> + </label> + </div> + + <label class="form-label small mb-1"> + <span v-if="shareLayerCanAddLearner">Shared with specific learners or instructors:</span> + <span v-if="!shareLayerCanAddLearner">Shared with specific instructors (learners in this list are ignored):</span> + </label> + <div class="mb-3 border rounded p-2" style="min-height:2.5em; max-height:8em; overflow-y:auto"> + <span v-if="shareLayerUsers.length === 0" class="text-muted small">No users added</span> + <span v-for="(user, index) in shareLayerUsers" :key="user.name" + class="badge bg-secondary me-1 mb-1" + style="cursor:pointer" + v-on:click="shareLayerUsers.splice(index, 1); shareLayerSearchQuery = ''; shareLayerSearchResults = [];" + :title="'Remove ' + user.name"> + {{ user.name }} × + </span> + </div> + + <label class="form-label small mb-1"> + <span v-if="shareLayerCanAddLearner">Add learner or instructor:</span> + <span v-if="!shareLayerCanAddLearner">Add instructor:</span> + </label> + <div class="input-group input-group-sm mb-1"> + <input type="text" class="form-control" placeholder="Type to search or enter a user ID..." + v-model="shareLayerSearchQuery" + v-on:input="ShareLayerSearchUsers()" + v-on:keydown.enter="ShareLayerAddStandardUser(shareLayerSearchQuery)"> + <button class="btn btn-outline-secondary" type="button" + v-on:click="ShareLayerAddStandardUser(shareLayerSearchQuery)">Add</button> + </div> + <div v-if="shareLayerSearchResults.length > 0" + class="list-group list-group-flush mt-1 border rounded" + style="max-height:8em; overflow-y:auto"> + <button type="button" + class="list-group-item list-group-item-action py-1 small" + v-for="user in shareLayerSearchResults" + :key="user.name" + v-on:click="ShareLayerAddUser(user)"> + {{ user.name }} + </button> + </div> + + </div> + <div class="modal-footer py-1"> + <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-sm btn-primary" v-on:click="ShareLayerSave()">Save</button> + </div> + </div> + </div> + </div> + + + <!-- Modal to import a layer from another user --> + <div class="modal fade" id="modal-import-layer" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header py-2"> + <h5 class="modal-title fs-6">Import shared layer</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <div class="mb-2"> + <label class="form-label small mb-1">User</label> + <input type="text" class="form-control form-control-sm" + placeholder="Type to search a user..." + v-model="importUserSearchQuery" + v-on:input="ImportUserSearchChanged()"> + <div v-if="importUserSearchResults.length > 0" + class="list-group list-group-flush mt-1 border rounded" + style="max-height:8em; overflow-y:auto"> + <button type="button" + class="list-group-item list-group-item-action py-1 small" + v-for="user in importUserSearchResults" + :key="user.name" + v-on:click="ImportUserSelect(user)"> + {{ user.name }} + </button> + </div> + <div v-if="importSelectedUser" class="mt-1 small text-muted"> + Selected: <strong>{{ importSelectedUser.name }}</strong> + </div> + </div> + <div v-show="importSelectedUser !== ''"> + <label class="form-label small mb-1">Layer</label> + <select class="form-select form-select-sm" v-model="importSelectedLayer"> + <option value="">Select a layer...</option> + <option v-for="layer in importAvailableLayers" :value="layer.id">{{layer.name}}</option> + </select> + </div> + </div> + <div class="modal-footer py-1"> + <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-sm btn-primary" + :disabled="!importSelectedLayer" + v-on:click="ImportLayerConfirmed()">Import</button> + </div> + </div> + </div> + </div> + + + <div style="display:none"> + <div id="popover-rotate"> + <div class="container"> + <div class="row mb-2"> + <div class="btn-group btn-group-sm" role="group"> + <button type="button" class="btn btn-outline-dark" v-on:click="ResetRotation()">Reset</button> + <button type="button" class="btn btn-outline-dark" v-on:click="RotateBy(90)">+90°</button> + <button type="button" class="btn btn-outline-dark" v-on:click="RotateBy(-90)">-90°</button> + </div> + </div> + <div class="row"> + <input type="range" class="form-range" min="-180" max="180" + v-model.number="rotationDeg" + v-on:input="SetMapRotation()" v-on:change="SetMapRotation()"> + </div> + </div> + </div> + </div> + + <div style="display:none"> + <div id="popover-adjustments"> + <div class="container"> + <div class="mb-1"> + <label for="range-brightness" class="form-label">Brightness: <b class="ps-2">{{brightness}}</b></label> + <input id="range-brightness" type="range" class="form-range" min="-100" max="100" step="1" + v-model.number="brightness" v-on:input="map.render()"> + </div> + <div class="mb-1"> + <label for="range-contrast" class="form-label">Contrast: <b class="ps-2">{{contrast}}</b></label> + <input id="range-contrast" type="range" class="form-range" min="-100" max="100" step="1" + v-model.number="contrast" v-on:input="map.render()"> + </div> + <div class="mb-1"> + <label for="range-saturation" class="form-label">Saturation: <b class="ps-2">{{saturation}}</b></label> + <input id="range-saturation" type="range" class="form-range" min="-100" max="100" step="1" + v-model.number="saturation" v-on:input="map.render()"> + </div> + <div class="d-grid"> + <button class="btn btn-primary" type="button" v-on:click="ResetAdjustments()"> + Reset + </button> + </div> </div> </div> </div> </div> - <!-- This is the version of jQuery that is used by Orthanc Explorer --> - <script src="../../app/libs/jquery.min.js"></script> - <script src="../libs/js/popper.min.js"></script> + <script src="../libs/js/axios.min.js"></script> + <script src="../libs/js/vue.min.js"></script> + <script src="../libs/js/popper.min.js"></script> <!-- Popper is needed by Bootstrap (for popovers) --> <script src="../libs/js/bootstrap.min.js"></script> <script src="../libs/js/ol.js"></script> + <script src="../libs/js/modern-screenshot.js"></script> + <script src="viewer.js"></script> </body> </html>
--- a/ViewerPlugin/WebApplication/viewer.js Mon Sep 07 15:12:02 2026 +0200 +++ b/ViewerPlugin/WebApplication/viewer.js Mon Sep 07 20:58:51 2026 +0200 @@ -21,191 +21,1651 @@ **/ +var app = new Vue({ + el: '#app', + + data() { + return { + projectId: '', + level: '', + resourceId: '', + frameNumber: 0, + brightness: 0, // In the range between [-100,100] + contrast: 0, // In the range between [-100,100] + saturation: 0, // In the range between [-100,100] + // hue: 0, // Degrees, in the range between [-180,180] + + // Main state for annotations + workspaceInfo: {}, + imageDescription: '', + userLayers: [], + importedLayers: [], + activeUserLayerId: null, + + // UI state + toolbarsVisible: false, + panelOpen: false, + mapBackground: '', + rotationDeg: 0, + activeDrawTool: null, + showMagnificationButtons: false, + + // Bootstrap modals + modalDeleteUserLayer: null, + modalDeleteImportedLayer: null, + modalDeleteAnnotation: null, + pendingDelete: null, + + // Loading/saving using the backend + isPendingChange: false, + isSaving: false, + showSpinner: false, + + // Annotation selection panel + annotationProperties: [], + selectedFeature: null, + + // OpenLayers objects + map: null, + drawSource: null, + drawLayer: null, // Used in HTML + drawImportedSource: null, + drawImportedLayer: null, + drawLine: null, + drawPoint: null, + drawCircle: null, + drawRectangle: null, + drawClosedPolygon: null, + drawFreehand: null, + drawFreehandLine: null, + drawArrow: null, + moveFeature: null, + modifyFeature: null, + modifyFeatureCollection: null, + selectAnnotation: null, + + /** + * Magnification at full-resolution image pixels, convention + * commonly used for pathology WSI: 40x scan = 0.25 µm/pixel + **/ + referenceMagnification: 40, // TODO - Could be read from pyramid + + // Share layer modal + modalShareUserLayer: null, + shareLayerTarget: {}, + shareLayerPublic: false, + shareLayerUsers: [], + shareLayerSearchQuery: '', + shareLayerSearchResults: [], + + // Import layer modal + modalImportLayer: null, + importAvailableUsers: [], + importUserSearchQuery: '', + importUserSearchResults: [], + importSelectedUser: '', + importSelectedLayer: '', + importAvailableLayers: [] + }; + }, + + computed: { + shareLayerCanAddLearner: function() { + return (this.workspaceInfo.is_instructor === true || + (this.workspaceInfo.is_learner === true && + this.workspaceInfo.learner_to_learner_sharing === true)); + } + }, + + watch: { + activeUserLayerId: function() { + if (this.activeDrawTool === 'modify') { + this.RefreshModifyFeatureCollection(); + } + } + }, + + mounted: function() { + this.InitializePanelAnimation(); + + this.modalDeleteUserLayer = new bootstrap.Modal(document.getElementById('modal-delete-user-layer')); + this.modalDeleteImportedLayer = new bootstrap.Modal(document.getElementById('modal-delete-imported-layer')); + this.modalDeleteAnnotation = new bootstrap.Modal(document.getElementById('modal-delete-annotation')); + this.modalShareUserLayer = new bootstrap.Modal(document.getElementById('modal-share-user-layer')); + this.modalImportLayer = new bootstrap.Modal(document.getElementById('modal-import-layer')); + + document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) { + new bootstrap.Tooltip(el, { trigger: 'hover' }); + }); + + // document.getElementById('right-panel-toggle').click(); // Open the side menu at startup + + const params = new URLSearchParams(document.location.search); + + if (params.has('project')) { + this.projectId = params.get('project'); + } + + if (params.has('description')) { + this.imageDescription = params.get('description'); + } + + if (params.has('series')) { + this.level = 'series'; + this.resourceId = params.get('series'); + } else if (params.has('instance')) { + this.level = 'instance'; + this.resourceId = params.get('instance'); + + if (params.has('frame')) { + this.frameNumber = params.get('frame'); + } + } else { + alert('Error - No series ID and no instance ID specified!'); + return; + } + + this.LoadPyramid(); + }, + + methods: { + + // ----------------------------------------------------------------------- + // Persistence of layers and annotations + // ----------------------------------------------------------------------- + + CreatePostPayload: function(args) { + args['project'] = this.projectId; + args['level'] = this.level; + args['resource'] = this.resourceId; + args['frame'] = this.frameNumber; + return JSON.stringify(args); + }, + + LoadLayers: function(activeLayerId) { + var that = this; + axios.post('../api/list-user-layers', + this.CreatePostPayload({})) + .then(function(response) { + that.userLayers = response.data['user-layers']; + that.importedLayers = response.data['imported-layers']; + + if (that.userLayers.length == 0) { + that.CreateUserLayer(); + } else if (activeLayerId !== undefined) { + that.activeUserLayerId = activeLayerId; + } else { + that.activeUserLayerId = that.userLayers[0].id; + } + + that.LoadUserFeatures(); + that.ReloadImportedFeatures(); + }) + .catch(function() { + console.error('Cannot load the saved annotations'); + }); + }, + + CreateUserLayer: function() { + var that = this; + axios.post('../api/create-user-layer', + this.CreatePostPayload({})) + .then(function(response) { + that.LoadLayers(response.data.id); + }) + .catch(function() { + console.error('Cannot create a new user layer'); + }); + }, + + SaveUserLayer: function(layer) { + var that = this; + axios.post('../api/save-user-layer', + this.CreatePostPayload({ + 'layer': layer + })) + .catch(function() { + console.error('Cannot save user layer'); + }); + }, + + LoadUserFeatures: function() { + console.assert(this.drawSource !== null); // InitializeAnnotations() must have been invoked + console.assert(this.workspaceInfo.enabled !== undefined); // LoadLayers() must have been invoked + + if (!this.workspaceInfo.enabled) { + return; + } + + this.showSpinner = true; + + var that = this; + axios.post('../api/load-user-features', + this.CreatePostPayload({})) + .then(function(response) { + that.drawSource.clear(); + + // We check that the original layer is still available (could have been some write error) + var availableLayerIds = []; + for (let i = 0; i < that.userLayers.length; i++) { + availableLayerIds.push(that.userLayers[i].id); + } + + for (let i = 0; i < response.data.features.length; i++) { + var layerId = response.data.features[i]['layer-id']; + + if (availableLayerIds.includes(layerId)) { + UnserializeFeatureOntoMap(that.drawSource, response.data.features[i]); + } + } + + // Now that the user features are loaded, we can install the save callback + that.drawSource.on('addfeature', function (e) { + that.SaveUserFeatures(); + }); + that.drawSource.on('removefeature', function (e) { + that.SaveUserFeatures(); + }); + }) + .catch(function() { + console.error('Cannot load user features'); + }) + .finally(function() { + that.showSpinner = false; + }); + }, + + SaveUserFeatures: function() + { + var that = this; + + function Execute() + { + var features = []; + + that.drawSource.getFeatures().forEach(function (feature, index) { + var item = SerializeFeature(feature); + + if (item !== null) { + item['layer-id'] = feature.get('layer-id'); + + var label = feature.get('label'); + if (label !== undefined) { + item['label'] = label; + } + + var date = feature.get('creation-datetime'); + if (date !== undefined) { + item['creation-datetime'] = date.getTime(); + } + + features.push(item); + } + }); + + that.isPendingChange = false; + that.isSaving = true; + that.showSpinner = true; + window.addEventListener('beforeunload', BeforeUnloadHandler); + + axios.post('../api/save-user-features', + that.CreatePostPayload({ + 'features': features + })) + .then(function() { + // Success + }) + .catch(function() { + console.error('Cannot save the annotations'); + }) + .finally(function() { + console.assert(that.isSaving === true); + + if (that.isPendingChange) { + Execute(); + } else { + that.isSaving = false; + that.showSpinner = false; + window.removeEventListener('beforeunload', BeforeUnloadHandler); + } + }); + } + + this.isPendingChange = true; + + if (!this.isSaving) { + Execute(); + } + }, + + DeleteUserLayer: function(id) { + this.pendingDelete = id; + this.modalDeleteUserLayer.show(); + }, + + UserLayerDeleteConfirmed: function() { + var layerId = this.pendingDelete; // The ID of the layer to be removed + + this.modalDeleteUserLayer.hide(); + var that = this; + axios.post('../api/delete-user-layer', + this.CreatePostPayload({ + 'layer-id': layerId + }) + ) + .then(function(response) { + that.LoadLayers(); + + // Remove the features that were part of this layer + that.drawSource.getFeatures().forEach(function(feature) { + if (feature.get('layer-id') === layerId) { + that.drawSource.removeFeature(feature); + } + }); + }) + .catch(function(error) { + console.error('Cannot delete the layer'); + }); + }, + + TakeScreenshot: function() { + modernScreenshot.domToBlob(document.body, { + filter: function (element) { + if (element.classList === undefined) { + return true; + } else { + return (element.id !== 'toolbar-top' && + element.id !== 'toolbar-left' && + !element.classList.contains('tooltip') && + !element.classList.contains('ol-control')); // "+", "-", and "rotate" buttons + } + } + }) + .then(function (blob) { + navigator.clipboard.write([ + new ClipboardItem({ + 'image/png': blob + }) + ]) + .then(function () { + alert('Screenshot copied to clipboard!'); + }) + .catch(function (error) { + alert('Could not copy screenshot\n\n(' + error + ')'); + }); + }); + }, + + LoadAnnotationsInfo: function() { + var that = this; + axios.post('../api/workspace-info', + this.CreatePostPayload({})) + .then(function(response) { + that.workspaceInfo = response.data; + + if (that.workspaceInfo.enabled) { + that.LoadLayers(); + } + }) + .catch(function(error) { + }); + }, + + // ----------------------------------------------------------------------- + // Annotation selection panel + // ----------------------------------------------------------------------- + + AddReadOnlyProperty: function(label, value) { + this.annotationProperties.push({ + type: 'readonly', + label: label, + value: value + }); + }, + + AddReadOnlyTextAreaProperty: function(label, value) { + this.annotationProperties.push({ + type: 'readonly-textarea', + label: label, + value: value + }); + }, + + AddEditableProperty: function(label, value, featureProp) { + this.annotationProperties.push({ + type: 'editable', + label: label, + value: value, + featureProp: featureProp + }); + }, + + AddTextAreaProperty: function(label, value, featureProp) { + this.annotationProperties.push({ + type: 'editable-textarea', + label: label, + value: value, + featureProp: featureProp + }); + }, + + AddDropdownProperty: function(label, options, selectedValue, featureProp) { + this.annotationProperties.push({ + type: 'dropdown', + label: label, + value: selectedValue, + options: options, + featureProp: featureProp + }); + }, + + ClearAnnotationSelection: function(clearOlSelection) { + if (clearOlSelection === undefined) { + clearOlSelection = true; + } + + if (clearOlSelection && this.selectAnnotation !== null) { + this.selectAnnotation.getFeatures().clear(); + } + + this.selectedFeature = null; + this.annotationProperties = []; + }, + + UpdateAnnotationProperty: function(prop) { + if (this.selectedFeature && + prop.featureProp && + this.drawSource !== null && + this.drawSource.hasFeature(this.selectedFeature)) { + this.selectedFeature.set(prop.featureProp, prop.value); + this.SaveUserFeatures(); + } + }, + + FocusAnnotation: function() { + if (this.selectedFeature) { + bootstrap.Offcanvas.getOrCreateInstance(document.getElementById('right-panel')).hide(); + this.map.getView().fit(this.selectedFeature.getGeometry().getExtent(), { padding: [40, 40, 40, 40], duration: 300 }); + } + }, + + // ----------------------------------------------------------------------- + // Rotation controls (driven from the popover) + // ----------------------------------------------------------------------- + + ResetRotation: function() { + this.rotationDeg = 0; + this.SetMapRotation(); + }, + + RotateBy: function(deg) { + this.rotationDeg = parseInt(this.rotationDeg) + deg; + + while (this.rotationDeg > 180) { + this.rotationDeg -= 360; + } + + while (this.rotationDeg < -180) { + this.rotationDeg += 360; + } + + this.SetMapRotation(); + }, + + SetMapRotation: function() { + this.map.getView().setRotation(this.rotationDeg / 180 * Math.PI); + }, + + // ----------------------------------------------------------------------- + // Draw tool activation + // ----------------------------------------------------------------------- + + DeactivateAll: function() { + this.map.removeInteraction(this.drawLine); + this.map.removeInteraction(this.drawPoint); + this.map.removeInteraction(this.drawCircle); + this.map.removeInteraction(this.drawRectangle); + this.map.removeInteraction(this.drawClosedPolygon); + this.map.removeInteraction(this.drawFreehand); + this.map.removeInteraction(this.drawFreehandLine); + this.map.removeInteraction(this.drawArrow); + this.map.removeInteraction(this.moveFeature); + this.map.removeInteraction(this.modifyFeature); + this.map.removeInteraction(this.selectAnnotation); + + this.activeDrawTool = null; + this.map.getViewport().style.cursor = ''; + this.ClearAnnotationSelection(true); + }, + + ToggleSelectTool: function() { + var wasActive = this.activeDrawTool === 'select'; + this.DeactivateAll(); + if (!wasActive) { + this.map.addInteraction(this.selectAnnotation); + this.activeDrawTool = 'select'; + this.map.getViewport().style.cursor = 'pointer'; + } + }, + + ToggleDrawTool: function(toolName) { + var interactions = { + 'line': this.drawLine, + 'point': this.drawPoint, + 'circle': this.drawCircle, + 'rectangle': this.drawRectangle, + 'closed-polygon': this.drawClosedPolygon, + 'freehand': this.drawFreehand, + 'freehand-line': this.drawFreehandLine, + 'arrow': this.drawArrow, + 'move': this.moveFeature, + 'modify': this.modifyFeature + }; + + var cursors = { + 'freehand': 'crosshair', + 'freehand-line': 'crosshair' + }; + + // Draw tools that keep selectAnnotation active to highlight the newly drawn feature + var drawTools = ['line', 'point', 'circle', 'rectangle', 'closed-polygon', 'freehand', 'freehand-line']; + + var wasActive = this.activeDrawTool === toolName; + this.DeactivateAll(); + if (!wasActive) { + if (toolName === 'modify') { + this.RefreshModifyFeatureCollection(); + } + this.map.addInteraction(interactions[toolName]); + if (drawTools.indexOf(toolName) !== -1) { + this.map.addInteraction(this.selectAnnotation); // kept active to show blue highlight + } + this.activeDrawTool = toolName; + var cursor = cursors[toolName]; + if (cursor) { + this.map.getViewport().style.cursor = cursor; + } + } + }, + + RefreshModifyFeatureCollection: function() { + if (this.modifyFeatureCollection !== null & + this.drawSource !== null) { + var activeLayerId = this.activeUserLayerId; + this.modifyFeatureCollection.clear(); + this.drawSource.getFeatures().forEach(function(feature) { + if (feature.get('layer-id') === activeLayerId) { + app.modifyFeatureCollection.push(feature); + } + }); + } + }, + + DeleteSelectedAnnotation: function() { + var selected = this.selectAnnotation.getFeatures(); + if (selected.getLength() > 0 && + this.drawSource !== null && + this.drawSource.hasFeature(selected.item(0))) { + this.modalDeleteAnnotation.show(); + } + }, + + ConfirmDeleteAnnotation: function() { + var selected = this.selectAnnotation.getFeatures(); + + var that = this; + selected.forEach(function(feature) { + that.drawSource.removeFeature(feature); + }); + + selected.clear(); + this.ClearAnnotationSelection(false); + this.modalDeleteAnnotation.hide(); + }, + + // ----------------------------------------------------------------------- + // Panel animation + // ----------------------------------------------------------------------- + + InitializePanelAnimation: function() { + var that = this; + var panel = document.getElementById('right-panel'); + var toggle = document.getElementById('right-panel-toggle'); + var isResizing = false; + + // Ensure the initial chevron direction matches the real offcanvas state. + that.panelOpen = panel.classList.contains('show'); + + function ResizingLoop() { + // If the offcanvas is hidden (e.g. before workspace load), its left edge can be 0, + // which would incorrectly move the toggle off-screen. Clamp to [0, panelWidth]. + var panelRect = panel.getBoundingClientRect(); + var panelWidth = Math.max(0, panelRect.width || 0); + var right = window.innerWidth - panelRect.left; + + if (!isFinite(right)) { + right = 0; + } + + right = Math.max(0, Math.min(right, panelWidth)); + toggle.style.right = right + 'px'; + + if (isResizing) { + requestAnimationFrame(ResizingLoop); + } + } + + function StartResizing() { + isResizing = true; + requestAnimationFrame(ResizingLoop); + } + + function StopResizing() { + isResizing = false; + ResizingLoop(); + } + + ResizingLoop(); + + panel.addEventListener('hide.bs.offcanvas', function() { + that.panelOpen = false; + StartResizing(); + }); + panel.addEventListener('hidden.bs.offcanvas', StopResizing); + + panel.addEventListener('show.bs.offcanvas', function() { + that.panelOpen = true; + StartResizing(); + }); + panel.addEventListener('shown.bs.offcanvas', StopResizing); + + // Keep the toggle aligned if viewport size changes. + window.addEventListener('resize', StopResizing); + }, + + // ----------------------------------------------------------------------- + // Pyramid loading and map initialization + // ----------------------------------------------------------------------- + + LoadPyramid: function() { + var that = this; + + if (this.level == 'series') + { + axios.get('../pyramids/' + this.resourceId) + .then(function(response) { + that.InitializePyramid(response.data, '../tiles/' + that.resourceId + '/'); + }) + .catch(function(error) { + alert('Error - Cannot get the pyramid structure of series: ' + that.resourceId); + }); + } + else if (this.level == 'instance') + { + axios.get('../frames-pyramids/' + this.resourceId + '/' + this.frameNumber) + .then(function(response) { + that.InitializePyramid(response.data, '../frames-tiles/' + that.resourceId + '/' + that.frameNumber + '/'); + }) + .catch(function(error) { + alert('Error - Cannot get the pyramid structure of frame ' + that.frameNumber + ' of instance: ' + that.resourceId); + }); + } + }, + + InitializePyramid: function(pyramid, tilesBaseUrl) { + this.mapBackground = pyramid['BackgroundColor']; // New in WSI 2.1 + + var width = pyramid['TotalWidth']; + var height = pyramid['TotalHeight']; + var countLevels = pyramid['Resolutions'].length; + + var metersPerUnit = null; + var imagedVolumeWidth = pyramid['ImagedVolumeWidth']; // In millimeters + var imagedVolumeHeight = pyramid['ImagedVolumeHeight']; + if (imagedVolumeWidth !== undefined && + imagedVolumeHeight !== undefined) { + var metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(width)); + var metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(height)); + if (IsNear(metersPerUnitX / metersPerUnitY, 1)) { + metersPerUnit = metersPerUnitX; + } else { + // Backward compatibility with OrthancWSIDicomizer <= 3.2, where X/Y were swapped + metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(height)); + metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(width)); + if (IsNear(metersPerUnitX / metersPerUnitY, 1)) { + metersPerUnit = metersPerUnitX; + } else { + console.error('Anisotropic pixel spacing (may result from an inconsistency ' + + 'in the imaged volume size), not showing the scale'); + } + } + } + + if (metersPerUnit) { + this.showMagnificationButtons = true; + } + + // Maps always need a projection, but Zoomify layers are not geo-referenced, and + // are only measured in pixels. So, we create a fake projection that the map + // can use to properly display the layer. + var proj = new ol.proj.Projection({ + code: 'pixel', + units: 'pixel', + metersPerUnit: metersPerUnit, + extent: [0, 0, width, height] + }); + + var extent = [0, -height, width, 0]; + + var rotateControl = new ol.control.Rotate({ + target: 'toolbar-left', + autoHide: false, // Show the button even if rotation is 0 + resetNorth: function() { // Disable the default action + } + }); + + new bootstrap.Popover(rotateControl.element, { + placement: 'right', + container: 'body', + html: true, + content: document.getElementById('popover-rotate') + }); + + new bootstrap.Popover(document.getElementById('button-adjustments'), { + placement: 'right', + container: 'body', + html: true, + content: document.getElementById('popover-adjustments') + }); + + // Disable the rotation of the map, and inertia while panning + // http://stackoverflow.com/a/25682186 + var interactions = ol.interaction.defaults.defaults({ + //pinchRotate : false, + dragPan: false // disable kinetics + //shiftDragZoom: false // disable zoom box + }).extend([ + new ol.interaction.DragPan(), + new ol.interaction.DragRotate({ + //condition: ol.events.condition.shiftKeyOnly // Rotate only when Shift key is pressed + }) + ]); + + var controls = ol.control.defaults.defaults({ + attribution: false, + rotate: false // remove the default rotate + }).extend([ + rotateControl, + + /*new ol.control.ScaleLine({ + minWidth: 100 + })*/ + new MicroscopeScaleLine({ + minWidth: 100, + referenceMagnification: this.referenceMagnification + }) + + ]); + + if (this.imageDescription !== null) { + controls.extend([ + new ol.control.Attribution({ + attributions: this.imageDescription, + collapsible: false + }) + ]); + } + + var tileLayer = new ol.layer.Tile({ + extent: extent, + source: new ol.source.TileImage({ + projection: proj, + tileUrlFunction: function(tileCoord, pixelRatio, projection) { + return (tilesBaseUrl + (countLevels - 1 - tileCoord[0]) + '/' + tileCoord[1] + '/' + tileCoord[2]); + }, + tileGrid: new ol.tilegrid.TileGrid({ + extent: extent, + resolutions: pyramid['Resolutions'].reverse(), + tileSizes: pyramid['TilesSizes'].reverse() + }) + }), + wrapX: false, + projection: proj + }); + + var that = this; + tileLayer.on('prerender', (event) => { + const context = event.context; + + if (context) { + context.save(); + var brightness = Math.pow(4, that.brightness / 100.0); // Ranges between 0.25 and 4 + var contrast = Math.pow(4, that.contrast / 100.0); // Ranges between 0.25 and 4 + var saturation = Math.pow(4, that.saturation / 100.0); // Ranges between 0.25 and 4 + context.filter = + 'brightness(' + brightness.toFixed(4) + ') ' + + 'contrast(' + contrast.toFixed(4) + ')' + + 'saturate(' + saturation.toFixed(4) + ')'; + // 'hue-rotate(' + that.hue + 'deg)'; + } + }); + + tileLayer.on('postrender', (event) => { + const context = event.context; + + if (context) { + context.restore(); + } + }); + + this.map = new ol.Map({ + target: 'map', + layers: [ tileLayer ], + view: new ol.View({ + projection: proj, + center: [width / 2, -height / 2], + zoom: 0, + minResolution: 0.1 // "1" means "do not interpelate over pixels" + }), + interactions: interactions, + controls: controls + }); + + // Prevent toolbar pointer events from reaching OL interactions (e.g. Select) + [ 'toolbar-left', 'toolbar-top' ].forEach(function(id) { + var el = document.getElementById(id); + [ 'pointerdown', 'pointerup', 'pointermove', 'click' ].forEach(function(type) { + el.addEventListener(type, function(e) { + e.stopPropagation(); + }); + }); + }); + + // Re-append toolbars inside the map viewport so they inherit OL's scaling + var viewport = this.map.getViewport(); + viewport.appendChild(document.getElementById('toolbar-left')); + viewport.appendChild(document.getElementById('toolbar-top')); + + this.map.once('postrender', function() { + // Match Bootstrap button size to OL button size + /*var olBtnSize = document.querySelector('.ol-zoom button').offsetWidth + 'px'; + document.querySelectorAll('.icon-btn').forEach(function(el) { + el.style.width = olBtnSize; + el.style.height = olBtnSize; + });*/ + + // Move the top toolbar directly right to the zoom control, regardless of scaling + var zoomEl = document.querySelector('.ol-zoom'); + document.getElementById('toolbar-top').style.left = (zoomEl.offsetLeft + zoomEl.offsetWidth) + 'px'; + document.getElementById('toolbar-top').style.top = zoomEl.offsetTop + 'px'; + + // Move the left toolbar directly below the zoom control, regardless of scaling + document.getElementById('toolbar-left').style.left = zoomEl.offsetLeft + 'px'; + document.getElementById('toolbar-left').style.top = (zoomEl.offsetTop + zoomEl.offsetHeight) + 'px'; + + // Move the vertical buttons below the rotate control, regardless of scaling + var rotateEl = document.querySelector('.ol-rotate'); + document.getElementById('toolbar-left-content').style.top = (rotateEl.offsetTop + rotateEl.offsetHeight) + 'px'; + }); + + this.map.getView().fit(extent, this.map.getSize()); + + this.toolbarsVisible = true; + this.InitializeAnnotations(); + }, + + ResetAdjustments: function() { + this.brightness = 0; + this.contrast = 0; + this.saturation = 0; + this.map.render(); + }, + + // ----------------------------------------------------------------------- + // Drawing annotations + // ----------------------------------------------------------------------- + + InitializeAnnotations: function() { + function GetLayerById(id) { + for (var i = 0; i < app.userLayers.length; i++) { + if (app.userLayers[i].id == id) { + return app.userLayers[i]; + } + } + return null; + } + + function GetImportedLayerById(id) { + for (var i = 0; i < app.importedLayers.length; i++) { + if (app.importedLayers[i].id == id) { + return app.importedLayers[i]; + } + } + return null; + } + + function GetLayerOfFeature(feature) { + var layerId = feature.get('layer-id'); + console.assert(layerId !== null); + var layer = GetLayerById(layerId); + console.assert(layer !== null); + return layer; + } + + function IsFeatureVisible(feature) { + var layerId = feature.get('layer-id'); + var userLayer = GetLayerById(layerId); + if (userLayer !== null) { + return userLayer.visible; + } + + var importedLayer = GetImportedLayerById(layerId); + return importedLayer !== null && importedLayer.visible; + } + + // Single vector source holding all features from all layers + this.drawSource = new ol.source.Vector(); + + this.drawLayer = new ol.layer.Vector({ + source: this.drawSource, + style: function(feature, resolution) { + var layer = GetLayerOfFeature(feature); + if (layer.visible) { + return CreateFeatureStyle(feature, resolution, layer.color); + } else { + return null; + } + } + }); + + this.map.addLayer(this.drawLayer); + + // Shared annotations: a separate read-only source for all imported layers + this.drawImportedSource = new ol.source.Vector(); + this.drawImportedLayer = new ol.layer.Vector({ + source: this.drawImportedSource, + style: function(feature, resolution) { + var entry = GetImportedLayerById(feature.get('layer-id')); + if (!entry || !entry.visible) { + return null; + } + + return CreateFeatureStyle(feature, resolution, entry.color); + } + }); + this.map.addLayer(this.drawImportedLayer); + + // Draw interactions (inactive until toggled) + this.drawLine = new ol.interaction.Draw({ + source: this.drawSource, + type: 'LineString', + maxPoints: 2 + }); + this.drawPoint = new ol.interaction.Draw({ source: this.drawSource, type: 'Point' }); + this.drawCircle = new ol.interaction.Draw({ source: this.drawSource, type: 'Circle' }); + this.drawRectangle = new ol.interaction.Draw({ + source: this.drawSource, + type: 'Circle', + geometryFunction: ol.interaction.Draw.createBox() + }); + this.drawClosedPolygon = new ol.interaction.Draw({ source: this.drawSource, type: 'Polygon' }); + this.drawFreehand = new ol.interaction.Draw({ source: this.drawSource, type: 'Polygon', freehand: true }); + this.drawFreehandLine = new ol.interaction.Draw({ source: this.drawSource, type: 'LineString', freehand: true }); + this.drawArrow = new ol.interaction.Draw({ + source: this.drawSource, + type: 'LineString', + maxPoints: 2 + }); + + this.moveFeature = new ol.interaction.Translate({ + source: this.drawSource, + filter: function(feature) { + return feature.get('layer-id') === app.activeUserLayerId; + } + }); + this.modifyFeatureCollection = new ol.Collection(); + this.modifyFeature = new ol.interaction.Modify({ features: this.modifyFeatureCollection }); + + // Select interaction (inactive until toggled). + // The condition restricts user-click selection to the dedicated select tool only, + // preventing spurious selection events when starting a draw near an existing feature. + this.selectAnnotation = new ol.interaction.Select({ + layers: [ this.drawLayer, this.drawImportedLayer ], + filter: function(feature) { + return IsFeatureVisible(feature); + }, + condition: function(e) { + return ol.events.condition.singleClick(e) && app.activeDrawTool === 'select'; + }, + hitTolerance: 5, /* pixels around the feature that count as a hit */ + style: function(feature, resolution) { + return CreateFeatureStyle(feature, resolution, '#0000ff'); /* selected annotations are in blue */ + } + }); + + + var that = this; + + function preventDoubleClickZoom() { + that.map.getInteractions().forEach(function(interaction) { + if (interaction instanceof ol.interaction.DoubleClickZoom) { + interaction.setActive(false); + setTimeout(function() { interaction.setActive(true); }, 50); + } + }); + } + + function onDrawEnd(e, callPreventDoubleClickZoom) { + e.feature.set('layer-id', app.activeUserLayerId); + e.feature.set('creation-datetime', new Date()); + if (callPreventDoubleClickZoom) { + preventDoubleClickZoom(); + } + that.selectAnnotation.getFeatures().clear(); + that.selectAnnotation.getFeatures().push(e.feature); + that.selectAnnotation.dispatchEvent({ type: 'select', selected: [e.feature], deselected: [] }); + } + + this.drawLine.on('drawend', function(e) { onDrawEnd(e, true); }); + this.drawPoint.on('drawend', function(e) { onDrawEnd(e, true); }); + this.drawCircle.on('drawend', function(e) { onDrawEnd(e, true); }); + this.drawRectangle.on('drawend', function(e) { onDrawEnd(e, true); }); + this.drawClosedPolygon.on('drawend', function(e) { onDrawEnd(e, true); }); + this.drawFreehand.on('drawend', function(e) { onDrawEnd(e, false); }); + this.drawFreehandLine.on('drawend', function(e) { onDrawEnd(e, false); }); + this.drawArrow.on('drawend', function(e) { + e.feature.set('type', 'arrow'); + onDrawEnd(e, false); + }); + + this.moveFeature.on('translateend', function(e) { that.SaveUserFeatures(); }); + this.modifyFeature.on('modifyend', function(e) { that.SaveUserFeatures(); }); + + this.selectAnnotation.on('select', function(e) { + if (e.selected.length === 1) { + that.annotationProperties = []; + + var feature = e.selected[0]; + that.selectedFeature = feature; + + var date = feature.get('creation-datetime'); + if (date === undefined) { + that.AddReadOnlyProperty('Date', ''); + } else { + that.AddReadOnlyProperty('Date', date.toLocaleString()); + } + + var geometry = feature.getGeometry(); + + if (geometry.getType() === 'LineString') { + // Line, freehand + that.AddReadOnlyProperty('Length', FormatLength(geometry.getLength(), that.map.getView().getProjection())); + } else if (geometry.getType() === 'Circle') { + // geometry.getArea() is not available on circles + var radius = geometry.getRadius(); + var area = Math.PI * radius * radius; + that.AddReadOnlyProperty('Area', FormatArea(area, that.map.getView().getProjection())); + } else if (geometry.getType() === 'Polygon') { + // Rectangle, closed polygon, freehand polygon + that.AddReadOnlyProperty('Area', FormatArea(geometry.getArea(), that.map.getView().getProjection())); + } + + if (that.drawSource !== null && + that.drawSource.hasFeature(feature)) { + that.AddTextAreaProperty('Label', feature.get('label') || '', 'label'); + } else { + that.AddReadOnlyTextAreaProperty('Label', feature.get('label') || ''); + } + + /* + // TODO + that.AddDropdownProperty('Category', [ + { value: 'tumor', label: 'Tumor' }, + { value: 'stroma', label: 'Stroma' }, + { value: 'necrosis', label: 'Necrosis' } + ], feature.get('category') || '', 'category'); + */ + + bootstrap.Offcanvas.getOrCreateInstance(document.getElementById('right-panel')).show(); + } else { + that.ClearAnnotationSelection(false); + } + }); + + this.LoadAnnotationsInfo(); + }, + + // ----------------------------------------------------------------------- + // Imported layers + // ----------------------------------------------------------------------- + + ShowShareUserLayerModal: function(layer) { + this.shareLayerTarget = layer; + this.shareLayerPublic = layer.public; + this.shareLayerUsers = layer.shared_with; + this.shareLayerSearchQuery = ''; + this.shareLayerSearchResults = []; + this.modalShareUserLayer.show(); + }, + + ShareLayerIsUserSelected: function(user) { + for (var i = 0; i < this.shareLayerUsers.length; i++) { + if (this.shareLayerUsers[i].type == user.type && + this.shareLayerUsers[i].name == user.name) { + return true; + } + } + + return false; + }, + + ShareLayerAddUser: function(user) { + if (!this.ShareLayerIsUserSelected(user)) { + this.shareLayerUsers.push(user); + } + + this.shareLayerSearchQuery = ''; + this.shareLayerSearchResults = []; + }, + + ShareLayerAddStandardUser: function(name) { + var that = this; + axios.post('../api/create-standard-user', + this.CreatePostPayload({ 'name': name })) + .then(function(response) { + that.ShareLayerAddUser(response.data); + }) + .catch(function() { + console.error('Cannot create standard user'); + }); + + this.shareLayerSearchQuery = ''; + this.shareLayerSearchResults = []; + }, + + ShareLayerSearchUsers: function() { + var query = this.shareLayerSearchQuery.trim(); + if (!query) { + this.shareLayerSearchResults = []; + } else { + var that = this; + axios.post('../api/search-active-users', + this.CreatePostPayload({ 'query': query })) + .then(function(response) { + that.shareLayerSearchResults = []; + for (var i = 0; i < response.data.length; i++) { + if (!that.ShareLayerIsUserSelected(response.data[i])) { + that.shareLayerSearchResults.push(response.data[i]); + } + } + }) + .catch(function() { + that.shareLayerSearchResults = []; + }); + } + }, + + ShareLayerSave: function() { + this.shareLayerTarget.public = this.shareLayerPublic; + this.shareLayerTarget.shared_with = this.shareLayerUsers; + this.SaveUserLayer(this.shareLayerTarget); + this.modalShareUserLayer.hide(); + }, + + + + ShowImportLayerModal: function() { + this.importUserSearchQuery = ''; + this.importUserSearchResults = []; + this.importSelectedUser = ''; + this.importSelectedLayer = ''; + this.importAvailableUsers = []; + this.importAvailableLayers = []; + this.modalImportLayer.show(); + + var that = this; + axios.post('../api/list-sharing-users', + this.CreatePostPayload({})) + .then(function(response) { + that.importAvailableUsers = response.data; + }) + .catch(function() { + console.error('Cannot load users sharing layers'); + }); + }, + + ImportUserSearchChanged: function() { + this.importSelectedUser = ''; + this.importSelectedLayer = ''; + this.importAvailableLayers = []; + this.importUserSearchResults = []; + + var query = this.importUserSearchQuery.trim().toLowerCase(); + + if (query !== '') { + for (var i = 0; i < this.importAvailableUsers.length; i++) { + if (this.importAvailableUsers[i].name.toLowerCase().indexOf(query) !== -1) { + this.importUserSearchResults.push(this.importAvailableUsers[i]); + } + } + } + }, + + ImportUserSelect: function(userId) { + this.importUserSearchQuery = ''; + this.importUserSearchResults = []; + this.importSelectedUser = userId; + this.ImportUserChanged(); + }, + + ImportUserChanged: function() { + this.importSelectedLayer = ''; + this.importAvailableLayers = []; + + var that = this; + axios.post('../api/list-shared-layers', + this.CreatePostPayload({ 'author': this.importSelectedUser })) + .then(function(response) { + that.importAvailableLayers = response.data; + }) + .catch(function() { + console.error('Cannot load imported layers'); + }); + }, + + ImportLayerConfirmed: function() { + this.modalImportLayer.hide(); + var userId = this.importSelectedUser; + var layerId = this.importSelectedLayer; + + var that = this; + axios.post('../api/import-layer', + this.CreatePostPayload({ + 'author': userId, + 'layer': layerId + })) + .then(function(response) { + that.LoadLayers(); + }) + .catch(function() { + console.error('Cannot import layer'); + }); + }, + + + DeleteImportedLayer: function(layerId) { + this.pendingDelete = layerId; + this.modalDeleteImportedLayer.show(); + }, + + ImportedLayerDeleteConfirmed: function() { + this.modalDeleteImportedLayer.hide(); + var layerId = this.pendingDelete; + + var that = this; + axios.post('../api/remove-imported-layer', + this.CreatePostPayload({ + 'layer': layerId + })) + .then(function(response) { + that.LoadLayers(); + }) + .catch(function() { + console.error('Cannot remove imported layer'); + }); + }, + + SaveImportedLayer: function(layer) { + var that = this; + axios.post('../api/save-imported-layer', + this.CreatePostPayload({ + 'layer': layer + })) + .catch(function() { + console.error('Cannot save imported layer'); + }); + }, + + + ReloadImportedFeatures: function() { + // Reset current selection when refreshing imported content. + this.ClearAnnotationSelection(true); + + console.assert(this.drawImportedSource !== null); // InitializeAnnotations() must have been invoked + console.assert(this.workspaceInfo.enabled !== undefined); // LoadLayers() must have been invoked + + if (!this.workspaceInfo.sharing) { + return; + } + + var that = this; + axios.post('../api/load-imported-features', + this.CreatePostPayload({})) + .then(function(response) { + that.drawImportedSource.clear(); + + for (let i = 0; i < response.data.features.length; i++) { + UnserializeFeatureOntoMap(that.drawImportedSource, response.data.features[i]); + } + }) + .catch(function() { + console.error('Cannot load imported features'); + }); + } + } +}); + + function IsNear(a, b) { return Math.abs(a - b) <= 0.01; } -function InitializePyramid(pyramid, tilesBaseUrl) +function FormatUnit(value, units) { - $('#map').css('background', pyramid['BackgroundColor']); // New in WSI 2.1 - - var width = pyramid['TotalWidth']; - var height = pyramid['TotalHeight']; - var countLevels = pyramid['Resolutions'].length; + // Order by unit size ascending (factor descending) + for (var i = 0; i < units.length - 1; i++) { + var nextScaled = value * units[i + 1].factor; - var metersPerUnit = null; - var imagedVolumeWidth = pyramid['ImagedVolumeWidth']; // In millimeters - var imagedVolumeHeight = pyramid['ImagedVolumeHeight']; - if (imagedVolumeWidth !== undefined && - imagedVolumeHeight !== undefined) { - var metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(width)); - var metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(height)); - if (IsNear(metersPerUnitX / metersPerUnitY, 1)) { - metersPerUnit = metersPerUnitX; - } else { - // Backward compatibility with OrthancWSIDicomizer <= 3.2, where X/Y were swapped - metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(height)); - metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(width)); - if (IsNear(metersPerUnitX / metersPerUnitY, 1)) { - metersPerUnit = metersPerUnitX; - } else { - console.error('Anisotropic pixel spacing (may result from an inconsistency ' + - 'in the imaged volume size), not showing the scale'); - } + // Stop when the next larger unit would produce a value below 1 + if (Math.abs(nextScaled) < 1) { + var scaled = value * units[i].factor; + return scaled.toFixed(2) + ' ' + units[i].label; } } - // Maps always need a projection, but Zoomify layers are not geo-referenced, and - // are only measured in pixels. So, we create a fake projection that the map - // can use to properly display the layer. - var proj = new ol.proj.Projection({ - code: 'pixel', - units: 'pixel', - metersPerUnit: metersPerUnit, - extent: [0, 0, width, height] - }); + // Use the largest unit available + var largest = units[units.length - 1]; + var scaled = value * largest.factor; + return scaled.toFixed(2) + ' ' + largest.label; +} - var extent = [0, -height, width, 0]; + +function FormatLength(lengthPx, projection) +{ + var metersPerUnit = projection.getMetersPerUnit(); + if (metersPerUnit) { + var meters = lengthPx * metersPerUnit; - var rotateControl = new ol.control.Rotate({ - autoHide: false, // Show the button even if rotation is 0 - resetNorth: function() { // Disable the default action - } - }); + return FormatUnit(meters, [ + { label: 'μm', factor: 1e6 }, + { label: 'mm', factor: 1e3 }, + { label: 'cm', factor: 1e2 }, + { label: 'm', factor: 1 }, + { label: 'km', factor: 1e-3 } + ]); - new bootstrap.Popover(rotateControl.element, { - placement: 'right', - container: 'body', - html: true, - content: $('#popover-content') - }); + } else { + return lengthPx.toFixed(0) + ' px'; + } +} + - // Disable the rotation of the map, and inertia while panning - // http://stackoverflow.com/a/25682186 - var interactions = ol.interaction.defaults.defaults({ - //pinchRotate : false, - dragPan: false // disable kinetics - //shiftDragZoom: false // disable zoom box - }).extend([ - new ol.interaction.DragPan(), - new ol.interaction.DragRotate({ - //condition: ol.events.condition.shiftKeyOnly // Rotate only when Shift key is pressed - }) - ]); +function FormatArea(areaPx, projection) +{ + var metersPerUnit = projection.getMetersPerUnit(); + if (metersPerUnit) { + var sqMeters = areaPx * metersPerUnit * metersPerUnit; + + return FormatUnit(sqMeters, [ + { label: 'μm²', factor: 1e12 }, + { label: 'mm²', factor: 1e6 }, + { label: 'cm²', factor: 1e4 }, + { label: 'm²', factor: 1 }, + { label: 'km²', factor: 1e-6 } + ]); - var controls = ol.control.defaults.defaults({ - attribution: false - }).extend([ - rotateControl, - new ol.control.ScaleLine({ - minWidth: 100 - }) - ]); + } else { + return areaPx.toFixed(0) + ' px²'; + } +} + - const params = new URLSearchParams(document.location.search); - if (params.has('description')) { - controls.extend([ - new ol.control.Attribution({ - attributions: params.get('description'), - collapsible: false - }) - ]); +function CreateLayerStyle(color) +{ + function HexToRGBA(hex, alpha) + { + var r = parseInt(hex.slice(1, 3), 16); + var g = parseInt(hex.slice(3, 5), 16); + var b = parseInt(hex.slice(5, 7), 16); + return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'; } - - var layer = new ol.layer.Tile({ - extent: extent, - source: new ol.source.TileImage({ - projection: proj, - tileUrlFunction: function(tileCoord, pixelRatio, projection) { - return (tilesBaseUrl + (countLevels - 1 - tileCoord[0]) + '/' + tileCoord[1] + '/' + tileCoord[2]); - }, - tileGrid: new ol.tilegrid.TileGrid({ - extent: extent, - resolutions: pyramid['Resolutions'].reverse(), - tileSizes: pyramid['TilesSizes'].reverse() - }) - }), - wrapX: false, - projection: proj - }); - - - var map = new ol.Map({ - target: 'map', - layers: [ layer ], - view: new ol.View({ - projection: proj, - center: [width / 2, -height / 2], - zoom: 0, - minResolution: 0.1 // "1" means "do not interpelate over pixels" - }), - interactions: interactions, - controls: controls - }); - - map.getView().fit(extent, map.getSize()); - - - $('#rotation-slider').on('input change', function() { - map.getView().setRotation(this.value / 180 * Math.PI); - }); - - $('#rotation-reset').click(function() { - $('#rotation-slider').val(0).change(); - }); - - $('#rotation-minus90').click(function() { - var angle = parseInt($('#rotation-slider').val()) - 90; - if (angle < -180) { - angle += 360; - } - $('#rotation-slider').val(angle).change(); - }); - - $('#rotation-plus90').click(function() { - var angle = parseInt($('#rotation-slider').val()) + 90; - if (angle > 180) { - angle -= 360; - } - $('#rotation-slider').val(angle).change(); + return new ol.style.Style({ + stroke: new ol.style.Stroke({ color: color, width: 2 }), + fill: new ol.style.Fill({ color: HexToRGBA(color, 0.2) }), + // The "image" style is used by point annotations + image: new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ color: color }) + }) }); } -$(document).ready(function() { - const params = new URLSearchParams(document.location.search); +function CreateArrowStyle(feature, resolution, color) +{ + const coordinates = feature.getGeometry().getCoordinates(); + + if (coordinates.length == 2) { + const start = coordinates[0]; + const end = coordinates[1]; + + const angle = Math.atan2( + end[1] - start[1], + end[0] - start[0] + ); + + const headLength = 10 * resolution; + const headAngle = Math.PI / 6; + + const p1 = [ + end[0] - headLength * Math.cos(angle - headAngle), + end[1] - headLength * Math.sin(angle - headAngle) + ]; + + const p2 = [ + end[0] - headLength * Math.cos(angle + headAngle), + end[1] - headLength * Math.sin(angle + headAngle) + ]; + + return [ + // shaft + new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: color, + width: 2 + }) + }), - if (params.has('series')) { - var seriesId = params.get('series'); - $.ajax({ - url : '../pyramids/' + seriesId, - error: function() { - alert('Error - Cannot get the pyramid structure of series: ' + seriesId); - }, - success : function(pyramid) { - InitializePyramid(pyramid, '../tiles/' + seriesId + '/'); - } - }); - } else if (params.has('instance')) { - var frameNumber = 0; - if (params.has('frame')) { - frameNumber = params.get('frame'); + // arrow head + new ol.style.Style({ + geometry: new ol.geom.LineString([ + p1, end, p2 + ]), + stroke: new ol.style.Stroke({ + color: color, + width: 2 + }) + }) + ]; + } +} + + +function IsArrowFeature(feature) +{ + return feature.get('type') === 'arrow'; +} + + +function CreateFeatureStyle(feature, resolution, color) +{ + if (IsArrowFeature(feature)) { + return CreateArrowStyle(feature, resolution, color); + } else { + return CreateLayerStyle(color); + } +} + + +function SerializeFeature(feature) +{ + var type = feature.getGeometry().getType(); + + if (type === 'LineString') { + var s; + if (IsArrowFeature(feature)) { + s = 'arrow'; + } else { + s = 'polyline'; } - var instanceId = params.get('instance'); - $.ajax({ - url : '../frames-pyramids/' + instanceId + '/' + frameNumber, - error: function() { - alert('Error - Cannot get the pyramid structure of frame ' + frameNumber + ' of instance: ' + instanceId); - }, - success : function(pyramid) { - InitializePyramid(pyramid, '../frames-tiles/' + instanceId + '/' + frameNumber + '/'); + return { + 'type' : s, + 'coordinates' : feature.getGeometry().getCoordinates() + }; + } else if (type === 'Point') { + return { + 'type' : 'point', + 'coordinates' : feature.getGeometry().getCoordinates() + }; + } else if (type === 'Circle') { + return { + 'type' : 'circle', + 'center' : feature.getGeometry().getCenter(), + 'radius' : feature.getGeometry().getRadius() + }; + } else if (type === 'Polygon') { + return { + 'type' : 'polygon', + 'coordinates' : feature.getGeometry().getCoordinates() + }; + } else { + console.error('Not implemented: ' + type); + return null; + } +} + + +function UnserializeGeometry(json) +{ + if (json.type === 'polyline') { + return new ol.geom.LineString(json['coordinates']); + } else if (json.type === 'arrow') { + var feature = new ol.geom.LineString(json['coordinates']); + feature.set('type', 'arrow'); + return feature; + } else if (json.type === 'point') { + return new ol.geom.Point(json['coordinates']); + } else if (json.type === 'circle') { + return new ol.geom.Circle(json['center'], json['radius']); + } else if (json.type === 'polygon') { + return new ol.geom.Polygon(json['coordinates']); + } else { + console.error('Not implemented: ' + json.type); + return null; + } +} + + +function UnserializeFeatureOntoMap(mapSource, serialized) +{ + var geometry = UnserializeGeometry(serialized); + + if (geometry !== null) { + var feature = new ol.Feature(geometry); + + var layerId = serialized['layer-id']; + console.assert(layerId !== undefined); + feature.set('layer-id', layerId); + + var type = serialized['type']; + if (type !== undefined) { + feature.set('type', type); + } + + var label = serialized['label']; + if (label !== undefined) { + feature.set('label', label); + } + + var date = serialized['creation-datetime']; // This is a numerical timestamp + if (date !== undefined) { + feature.set('creation-datetime', new Date(date)); + } + + mapSource.addFeature(feature); + } +} + + +function BeforeUnloadHandler(event) +{ + event.preventDefault(); + + // Included for legacy support, e.g. Chrome/Edge < 119 + event.returnValue = true; +}; + + + + +function SetMagnification(map, referenceMagnification, magnification) +{ + var view = map.getView(); + var projection = view.getProjection(); + + if (projection.getMetersPerUnit()) { // Ensure that "metersPerUnit" is not null + var resolution = referenceMagnification / magnification; + + view.animate({ + resolution: view.getConstrainedResolution(resolution), + duration: 250 + }); + } +} + + +function GetMagnification(map, referenceMagnification) +{ + var view = map.getView(); + var projection = view.getProjection(); + + if (projection.getMetersPerUnit() !== undefined) { // Ensure that "metersPerUnit" is not null + var resolution = view.getResolution(); + + return referenceMagnification / resolution; + } +} + + +/** + * A ScaleLine control that also displays the equivalent microscope + * objective magnification (4x, 10x, 40x,...) for the current zoom level. + */ +class MicroscopeScaleLine extends ol.control.ScaleLine { + constructor(options = {}) { + super(options); + + this.referenceMagnification_ = options.referenceMagnification; + console.assert(this.referenceMagnification_ !== undefined); + + this.magnificationElement_ = document.createElement('div'); + this.magnificationElement_.className = 'ol-scale-magnification'; + this.element.appendChild(this.magnificationElement_); + } + + updateElement_() { + super.updateElement_(); + + var map = this.getMap(); + if (map && this.magnificationElement_) { + var magnification = GetMagnification(map, this.referenceMagnification_); + if (magnification) { + this.magnificationElement_.innerText = magnification.toFixed(2) + 'x'; } - }); - } else { - alert('Error - No series ID and no instance ID specified!'); + } } -}); +}
