# HG changeset patch # User Sebastien Jodogne # Date 1729249735 -7200 # Node ID c42083d50ddf167664f51537b8d787ca8b8de9af # Parent ae2d769215d2eacd0642a524f50b42730b8267a0 Added support for DICOM tag "Recommended Absent Pixel CIELab" (0048,0015) diff -r ae2d769215d2 -r c42083d50ddf Applications/CMakeLists.txt --- a/Applications/CMakeLists.txt Fri Oct 18 08:48:53 2024 +0200 +++ b/Applications/CMakeLists.txt Fri Oct 18 13:08:55 2024 +0200 @@ -107,6 +107,7 @@ ${ORTHANC_WSI_DIR}/Framework/Algorithms/PyramidReader.cpp ${ORTHANC_WSI_DIR}/Framework/Algorithms/ReconstructPyramidCommand.cpp ${ORTHANC_WSI_DIR}/Framework/Algorithms/TranscodeTileCommand.cpp + ${ORTHANC_WSI_DIR}/Framework/ColorSpaces.cpp ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp ${ORTHANC_WSI_DIR}/Framework/DicomizerParameters.cpp ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp diff -r ae2d769215d2 -r c42083d50ddf Applications/Dicomizer.cpp --- a/Applications/Dicomizer.cpp Fri Oct 18 08:48:53 2024 +0200 +++ b/Applications/Dicomizer.cpp Fri Oct 18 13:08:55 2024 +0200 @@ -23,6 +23,7 @@ #include "../Framework/Algorithms/ReconstructPyramidCommand.h" #include "../Framework/Algorithms/TranscodeTileCommand.h" +#include "../Framework/ColorSpaces.h" #include "../Framework/DicomToolbox.h" #include "../Framework/DicomizerParameters.h" #include "../Framework/ImageToolbox.h" @@ -54,6 +55,8 @@ #include #include +#include + static const char* OPTION_COLOR = "color"; static const char* OPTION_COMPRESSION = "compression"; @@ -563,6 +566,26 @@ } SetupDimension(dataset, opticalPathId, source, volume); + + + // New in release 2.1 + if (!dataset.tagExists(DCM_RecommendedAbsentPixelCIELabValue)) + { + OrthancWSI::RGBColor rgb(parameters.GetBackgroundColorRed(), + parameters.GetBackgroundColorGreen(), + parameters.GetBackgroundColorBlue()); + OrthancWSI::sRGBColor srgb(rgb); + OrthancWSI::XYZColor xyz(srgb); + OrthancWSI::LABColor lab(xyz); + + uint16_t encoded[3]; + lab.EncodeDicomRecommendedAbsentPixelCIELab(encoded); + + if (!dataset.putAndInsertUint16Array(DCM_RecommendedAbsentPixelCIELabValue, encoded, 3).good()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } } @@ -1174,6 +1197,8 @@ } +#include "../Framework/ColorSpaces.h" + int main(int argc, char* argv[]) { OrthancWSI::ApplicationToolbox::GlobalInitialize(); diff -r ae2d769215d2 -r c42083d50ddf Framework/ColorSpaces.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/ColorSpaces.cpp Fri Oct 18 13:08:55 2024 +0200 @@ -0,0 +1,274 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ColorSpaces.h" + +#include +#include + +#include + + +namespace OrthancWSI +{ + RGBColor::RGBColor(const sRGBColor& srgb) + { + if (srgb.GetR() < 0) + { + r_ = 0; + } + else if (srgb.GetR() >= 1) + { + r_ = 255; + } + else + { + r_ = boost::math::iround(srgb.GetR() * 255.0f); + } + + if (srgb.GetG() < 0) + { + g_ = 0; + } + else if (srgb.GetG() >= 1) + { + g_ = 255; + } + else + { + g_ = boost::math::iround(srgb.GetG() * 255.0f); + } + + if (srgb.GetB() < 0) + { + b_ = 0; + } + else if (srgb.GetB() >= 1) + { + b_ = 255; + } + else + { + b_ = boost::math::iround(srgb.GetB() * 255.0f); + } + } + + + sRGBColor::sRGBColor(const RGBColor& rgb) + { + r_ = static_cast(rgb.GetR()) / 255.0f; + g_ = static_cast(rgb.GetG()) / 255.0f; + b_ = static_cast(rgb.GetB()) / 255.0f; + } + + + static float ApplyGammaXYZ(float value) + { + // https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz + if (value <= 0.0031308f) + { + return value * 12.92f; + } + else + { + return 1.055f * powf(value, 1.0f / 2.4f) - 0.055f; + } + } + + + sRGBColor::sRGBColor(const XYZColor& xyz) + { + // https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB + // https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz + const float sr = 3.2404542f * xyz.GetX() - 1.5371385f * xyz.GetY() - 0.4985314f * xyz.GetZ(); + const float sg = -0.9692660f * xyz.GetX() + 1.8760108f * xyz.GetY() + 0.0415560f * xyz.GetZ(); + const float sb = 0.0556434f * xyz.GetX() - 0.2040259f * xyz.GetY() + 1.0572252f * xyz.GetZ(); + + r_ = ApplyGammaXYZ(sr); + g_ = ApplyGammaXYZ(sg); + b_ = ApplyGammaXYZ(sb); + } + + + static float LinearizeXYZ(float value) + { + // https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz + if (value <= 0.04045f) + { + return value / 12.92f; + } + else + { + return powf((value + 0.055f) / 1.055f, 2.4f); + } + } + + + XYZColor::XYZColor(const sRGBColor& srgb) + { + // https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ + // https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz + const float linearizedR = LinearizeXYZ(srgb.GetR()); + const float linearizedG = LinearizeXYZ(srgb.GetG()); + const float linearizedB = LinearizeXYZ(srgb.GetB()); + + x_ = 0.4124564f * linearizedR + 0.3575761f * linearizedG + 0.1804375f * linearizedB; + y_ = 0.2126729f * linearizedR + 0.7151522f * linearizedG + 0.0721750f * linearizedB; + z_ = 0.0193339f * linearizedR + 0.1191920f * linearizedG + 0.9503041f * linearizedB; + } + + + static const float LAB_DELTA = 6.0f / 29.0f; + + static float LABf_inv(float t) + { + if (t > LAB_DELTA) + { + return powf(t, 3.0f); + } + else + { + return 3.0f * LAB_DELTA * LAB_DELTA * (t - 4.0f / 29.0f); + } + } + + + // Those correspond to Standard Illuminant D65 + // https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIEXYZ_to_CIELAB + static const float X_N = 95.0489f; + static const float Y_N = 100.0f; + static const float Z_N = 108.8840f; + + XYZColor::XYZColor(const LABColor& lab) + { + // https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIELAB_to_CIEXYZ + const float shared = (lab.GetL() + 16.0f) / 116.0f; + + x_ = X_N * LABf_inv(shared + lab.GetA() / 500.0f) / 100.0f; + y_ = Y_N * LABf_inv(shared) / 100.0f; + z_ = Z_N * LABf_inv(shared - lab.GetB() / 200.0f) / 100.0f; + } + + + static float LABf(float t) + { + if (t > powf(LAB_DELTA, 3.0f)) + { + return powf(t, 1.0f / 3.0f); + } + else + { + return t / 3.0f * powf(LAB_DELTA, -2.0f) + 4.0f / 29.0f; + } + } + + + LABColor::LABColor(const XYZColor& xyz) + { + // https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIEXYZ_to_CIELAB + + const float fx = LABf(xyz.GetX() * 100.0f / X_N); + const float fy = LABf(xyz.GetY() * 100.0f / Y_N); + const float fz = LABf(xyz.GetZ() * 100.0f / Z_N); + + l_ = 116.0f * fy - 16.0f; + a_ = 500.0f * (fx - fy); + b_ = 200.0f * (fy - fz); + } + + + static uint16_t EncodeUint16(float value, + float minValue, + float maxValue) + { + if (value <= minValue) + { + return 0; + } + else if (value >= maxValue) + { + return 0xffff; + } + else + { + float lambda = (value - minValue) / (maxValue - minValue); + assert(lambda >= 0 && lambda <= 1); + return static_cast(boost::math::iround(lambda * static_cast(0xffff))); + } + } + + + void LABColor::EncodeDicomRecommendedAbsentPixelCIELab(uint16_t target[3]) const + { + /** + * "An L value linearly scaled to 16 bits, such that 0x0000 + * corresponds to an L of 0.0, and 0xFFFF corresponds to an L of + * 100.0." + **/ + target[0] = EncodeUint16(GetL(), 0.0f, 100.0f); + + /** + * "An a* then a b* value, each linearly scaled to 16 bits and + * offset to an unsigned range, such that 0x0000 corresponds to 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" + **/ + target[1] = EncodeUint16(GetA(), -128.0f, 127.0f); + target[2] = EncodeUint16(GetB(), -128.0f, 127.0f); + } + + + LABColor LABColor::DecodeDicomRecommendedAbsentPixelCIELab(uint16_t l, + uint16_t a, + uint16_t b) + { + return LABColor(static_cast(l) / static_cast(0xffff) * 100.0f, + -128.0f + static_cast(a) / static_cast(0xffff) * 255.0f, + -128.0f + static_cast(b) / static_cast(0xffff) * 255.0f); + } + + + bool LABColor::DecodeDicomRecommendedAbsentPixelCIELab(LABColor& target, + const std::string& tag) + { + std::vector channels; + Orthanc::Toolbox::TokenizeString(channels, tag, '\\'); + + unsigned int l, a, b; + if (channels.size() == 3 && + Orthanc::SerializationToolbox::ParseUnsignedInteger32(l, channels[0]) && + Orthanc::SerializationToolbox::ParseUnsignedInteger32(a, channels[1]) && + Orthanc::SerializationToolbox::ParseUnsignedInteger32(b, channels[2]) && + l <= 0xffffu && + a <= 0xffffu && + b <= 0xffffu) + { + target = LABColor::DecodeDicomRecommendedAbsentPixelCIELab(l, a, b); + return true; + } + else + { + return false; + } + } +} diff -r ae2d769215d2 -r c42083d50ddf Framework/ColorSpaces.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/ColorSpaces.h Fri Oct 18 13:08:55 2024 +0200 @@ -0,0 +1,192 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU 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 . + **/ + + +#pragma once + +#include +#include + + +namespace OrthancWSI +{ + class sRGBColor; + class XYZColor; + class LABColor; + + + class RGBColor + { + private: + uint8_t r_; + uint8_t g_; + uint8_t b_; + + public: + RGBColor(uint8_t r, + uint8_t g, + uint8_t b) : + r_(r), + g_(g), + b_(b) + { + } + + RGBColor(const sRGBColor& srgb); + + uint8_t GetR() const + { + return r_; + } + + uint8_t GetG() const + { + return g_; + } + + uint8_t GetB() const + { + return b_; + } + }; + + + // Those correspond to Standard Illuminant D65 + // https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB + class sRGBColor + { + private: + float r_; + float g_; + float b_; + + public: + sRGBColor(float r, + float g, + float b) : + r_(r), + g_(g), + b_(b) + { + } + + sRGBColor(const RGBColor& rgb); + + sRGBColor(const XYZColor& xyz); + + float GetR() const + { + return r_; + } + + float GetG() const + { + return g_; + } + + float GetB() const + { + return b_; + } + }; + + + class XYZColor + { + private: + float x_; + float y_; + float z_; + + public: + XYZColor(const sRGBColor& srgb); + + XYZColor(const LABColor& lab); + + float GetX() const + { + return x_; + } + + float GetY() const + { + return y_; + } + + float GetZ() const + { + return z_; + } + }; + + + class LABColor + { + private: + float l_; + float a_; + float b_; + + public: + LABColor() : + l_(0), + a_(0), + b_(0) + { + } + + LABColor(float l, + float a, + float b) : + l_(l), + a_(a), + b_(b) + { + } + + LABColor(const XYZColor& xyz); + + float GetL() const + { + return l_; + } + + float GetA() const + { + return a_; + } + + float GetB() const + { + return b_; + } + + void EncodeDicomRecommendedAbsentPixelCIELab(uint16_t target[3]) const; + + static LABColor DecodeDicomRecommendedAbsentPixelCIELab(uint16_t l, + uint16_t a, + uint16_t b); + + static bool DecodeDicomRecommendedAbsentPixelCIELab(LABColor& target, + const std::string& tag); + }; +} diff -r ae2d769215d2 -r c42083d50ddf Framework/Inputs/DicomPyramid.cpp --- a/Framework/Inputs/DicomPyramid.cpp Fri Oct 18 08:48:53 2024 +0200 +++ b/Framework/Inputs/DicomPyramid.cpp Fri Oct 18 13:08:55 2024 +0200 @@ -103,8 +103,15 @@ (tokens[1] != "THUMBNAIL" && tokens[1] != "OVERVIEW")) { + if (instance->HasBackgroundColor()) + { + backgroundRed_ = instance->GetBackgroundRed(); + backgroundGreen_ = instance->GetBackgroundGreen(); + backgroundBlue_ = instance->GetBackgroundBlue(); + } + instances_.push_back(instance.release()); - } + } } catch (Orthanc::OrthancException&) { @@ -157,7 +164,10 @@ const std::string& seriesId, bool useCache) : orthanc_(orthanc), - seriesId_(seriesId) + seriesId_(seriesId), + backgroundRed_(255), + backgroundGreen_(255), + backgroundBlue_(255) { RegisterInstances(seriesId, useCache); diff -r ae2d769215d2 -r c42083d50ddf Framework/Inputs/DicomPyramid.h --- a/Framework/Inputs/DicomPyramid.h Fri Oct 18 08:48:53 2024 +0200 +++ b/Framework/Inputs/DicomPyramid.h Fri Oct 18 13:08:55 2024 +0200 @@ -38,6 +38,9 @@ std::string seriesId_; std::vector instances_; std::vector levels_; + uint8_t backgroundRed_; + uint8_t backgroundGreen_; + uint8_t backgroundBlue_; void Clear(); @@ -85,5 +88,20 @@ virtual Orthanc::PixelFormat GetPixelFormat() const ORTHANC_OVERRIDE; virtual Orthanc::PhotometricInterpretation GetPhotometricInterpretation() const ORTHANC_OVERRIDE; + + uint8_t GetBackgroundRed() const + { + return backgroundRed_; + } + + uint8_t GetBackgroundGreen() const + { + return backgroundGreen_; + } + + uint8_t GetBackgroundBlue() const + { + return backgroundBlue_; + } }; } diff -r ae2d769215d2 -r c42083d50ddf Framework/Inputs/DicomPyramidInstance.cpp --- a/Framework/Inputs/DicomPyramidInstance.cpp Fri Oct 18 08:48:53 2024 +0200 +++ b/Framework/Inputs/DicomPyramidInstance.cpp Fri Oct 18 13:08:55 2024 +0200 @@ -24,6 +24,7 @@ #include "../PrecompiledHeadersWSI.h" #include "DicomPyramidInstance.h" +#include "../ColorSpaces.h" #include "../DicomToolbox.h" #include "../../Resources/Orthanc/Stone/DicomDatasetReader.h" #include "../../Resources/Orthanc/Stone/FullOrthancDataset.h" @@ -51,6 +52,7 @@ static const Orthanc::DicomTag DICOM_TAG_TOTAL_PIXEL_MATRIX_COLUMNS(0x0048, 0x0006); static const Orthanc::DicomTag DICOM_TAG_TOTAL_PIXEL_MATRIX_ROWS(0x0048, 0x0007); static const Orthanc::DicomTag DICOM_TAG_IMAGE_TYPE(0x0008, 0x0008); + static const Orthanc::DicomTag DICOM_TAG_RECOMMENDED_ABSENT_PIXEL_CIELAB(0x0048, 0x0015); static ImageCompression DetectImageCompression(OrthancStone::IOrthancConnection& orthanc, const std::string& instanceId) @@ -263,6 +265,23 @@ frames_[i].second = i / w; } } + + // New in WSI 2.1 + std::string background; + if (dataset.GetStringValue(background, Orthanc::DicomPath(DICOM_TAG_RECOMMENDED_ABSENT_PIXEL_CIELAB))) + { + LABColor lab; + if (LABColor::DecodeDicomRecommendedAbsentPixelCIELab(lab, background)) + { + XYZColor xyz(lab); + sRGBColor srgb(xyz); + RGBColor rgb(srgb); + hasBackgroundColor_ = true; + backgroundRed_ = rgb.GetR(); + backgroundGreen_ = rgb.GetG(); + backgroundBlue_ = rgb.GetB(); + } + } } @@ -271,7 +290,11 @@ bool useCache) : instanceId_(instanceId), hasCompression_(false), - compression_(ImageCompression_None) // Dummy initialization for serialization + compression_(ImageCompression_None), // Dummy initialization for serialization + hasBackgroundColor_(false), + backgroundRed_(0), + backgroundGreen_(0), + backgroundBlue_(0) { if (useCache) { @@ -327,6 +350,7 @@ static const char* const TOTAL_HEIGHT = "TotalHeight"; static const char* const PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation"; static const char* const IMAGE_TYPE = "ImageType"; + static const char* const BACKGROUND_COLOR = "BackgroundColor"; void DicomPyramidInstance::Serialize(std::string& result) const @@ -355,6 +379,15 @@ content[PHOTOMETRIC_INTERPRETATION] = Orthanc::EnumerationToString(photometric_); content[IMAGE_TYPE] = imageType_; + if (hasBackgroundColor_) + { + Json::Value color = Json::arrayValue; + color.append(backgroundRed_); + color.append(backgroundGreen_); + color.append(backgroundBlue_); + content[BACKGROUND_COLOR] = color; + } + #if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 0) Orthanc::Toolbox::WriteFastJson(result, content); #else @@ -407,5 +440,61 @@ frames_[i].first = f[i][0].asInt(); frames_[i].second = f[i][1].asInt(); } + + hasBackgroundColor_ = false; + if (content.isMember(BACKGROUND_COLOR)) + { + const Json::Value& color = content[BACKGROUND_COLOR]; + if (color.type() == Json::arrayValue && + color.size() == 3u && + color[0].isUInt() && + color[1].isUInt() && + color[2].isUInt()) + { + hasBackgroundColor_ = true; + backgroundRed_ = color[0].asUInt(); + backgroundGreen_ = color[1].asUInt(); + backgroundBlue_ = color[2].asUInt(); + } + } + } + + + uint8_t DicomPyramidInstance::GetBackgroundRed() const + { + if (hasBackgroundColor_) + { + return backgroundRed_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + uint8_t DicomPyramidInstance::GetBackgroundGreen() const + { + if (hasBackgroundColor_) + { + return backgroundGreen_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + uint8_t DicomPyramidInstance::GetBackgroundBlue() const + { + if (hasBackgroundColor_) + { + return backgroundBlue_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } } } diff -r ae2d769215d2 -r c42083d50ddf Framework/Inputs/DicomPyramidInstance.h --- a/Framework/Inputs/DicomPyramidInstance.h Fri Oct 18 08:48:53 2024 +0200 +++ b/Framework/Inputs/DicomPyramidInstance.h Fri Oct 18 13:08:55 2024 +0200 @@ -47,6 +47,10 @@ std::vector frames_; Orthanc::PhotometricInterpretation photometric_; std::string imageType_; + bool hasBackgroundColor_; + uint8_t backgroundRed_; + uint8_t backgroundGreen_; + uint8_t backgroundBlue_; void Load(OrthancStone::IOrthancConnection& orthanc, const std::string& instanceId); @@ -110,5 +114,16 @@ unsigned int GetFrameLocationY(size_t frame) const; void Serialize(std::string& result) const; + + bool HasBackgroundColor() const + { + return hasBackgroundColor_; + } + + uint8_t GetBackgroundRed() const; + + uint8_t GetBackgroundGreen() const; + + uint8_t GetBackgroundBlue() const; }; } diff -r ae2d769215d2 -r c42083d50ddf NEWS --- a/NEWS Fri Oct 18 08:48:53 2024 +0200 +++ b/NEWS Fri Oct 18 13:08:55 2024 +0200 @@ -5,6 +5,7 @@ * OrthancWSIDicomizer supports plain TIFF, besides hierarchical TIFF * New option: "--force-openslide" to force the use of OpenSlide on TIFF-like files * New option: "--padding" to control deep zoom of plain PNG/JPEG/TIFF images over IIIF +* Added support for DICOM tag "Recommended Absent Pixel CIELab" (0048,0015) * Force version of Mirador to 3.3.0 * In the IIIF manifest, reverse the order of the "sizes" field, which seems to fix compatibility with Mirador v4.0.0-alpha diff -r ae2d769215d2 -r c42083d50ddf ViewerPlugin/CMakeLists.txt --- a/ViewerPlugin/CMakeLists.txt Fri Oct 18 08:48:53 2024 +0200 +++ b/ViewerPlugin/CMakeLists.txt Fri Oct 18 13:08:55 2024 +0200 @@ -191,6 +191,7 @@ Plugin.cpp RawTile.cpp + ${ORTHANC_WSI_DIR}/Framework/ColorSpaces.cpp ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp diff -r ae2d769215d2 -r c42083d50ddf ViewerPlugin/Plugin.cpp --- a/ViewerPlugin/Plugin.cpp Fri Oct 18 08:48:53 2024 +0200 +++ b/ViewerPlugin/Plugin.cpp Fri Oct 18 13:08:55 2024 +0200 @@ -106,6 +106,15 @@ result["TotalHeight"] = totalHeight; result["TotalWidth"] = totalWidth; + { + // New in WSI 2.1 + char tmp[16]; + sprintf(tmp, "#%02x%02x%02x", locker.GetPyramid().GetBackgroundRed(), + locker.GetPyramid().GetBackgroundGreen(), + locker.GetPyramid().GetBackgroundBlue()); + result["BackgroundColor"] = tmp; + } + std::string s = result.toStyledString(); OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json"); } diff -r ae2d769215d2 -r c42083d50ddf ViewerPlugin/viewer.js --- a/ViewerPlugin/viewer.js Fri Oct 18 08:48:53 2024 +0200 +++ b/ViewerPlugin/viewer.js Fri Oct 18 13:08:55 2024 +0200 @@ -63,6 +63,8 @@ alert('Error - Cannot get the pyramid structure of series: ' + seriesId); }, success : function(series) { + $('#map').css('background', series['BackgroundColor']); // New in WSI 2.1 + var width = series['TotalWidth']; var height = series['TotalHeight']; var countLevels = series['Resolutions'].length;