Mercurial > hg > orthanc-wsi
changeset 517:eefdef0a313e annotations
integration mainline->annotations
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Fri, 21 Aug 2026 17:21:41 +0200 |
| parents | e26bf161c039 (current diff) 205c879ba2cc (diff) |
| children | 5ce456812a51 |
| files | NEWS ViewerPlugin/CMakeLists.txt ViewerPlugin/Plugin.cpp |
| diffstat | 39 files changed, 734 insertions(+), 295 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.github/workflows/mirror.yml Fri Aug 21 17:21:41 2026 +0200 @@ -0,0 +1,53 @@ +name: Mirror Mercurial to GitHub + +env: + HG_SOURCE_URL: https://orthanc.uclouvain.be/hg/orthanc-wsi/ + GITHUB_TARGET: jodogne/mirror-orthanc-wsi + FAST_EXPORT_VERSION: fast-export-260405 + +on: + schedule: + - cron: '0 2 * * *' + workflow_dispatch: {} + +concurrency: + group: hg-mirror + cancel-in-progress: false + +permissions: + contents: write # GITHUB_TOKEN is read-only by default; this grants push access + +jobs: + mirror: + if: ${{ github.repository == 'jodogne/mirror-orthanc-wsi' }} + runs-on: ubuntu-slim + steps: + - name: Install Mercurial and fast-export + run: | + sudo apt-get update + DEBIAN_FRONTEND=noninteractive sudo apt-get install -y mercurial git wget + cd /tmp/ + wget https://orthanc.uclouvain.be/downloads/third-party-downloads/${FAST_EXPORT_VERSION}.tar.gz + tar xvf ${FAST_EXPORT_VERSION}.tar.gz + + - name: Full clone of Mercurial source + run: | + hg clone "${HG_SOURCE_URL}" hg-repo + + - name: Full conversion (fresh git repo every run) + run: | + git init --bare git-repo + cd git-repo + /tmp/${FAST_EXPORT_VERSION}/hg-fast-export.sh -r ../hg-repo --hg-hash --hgtags + + - name: Push (never force, so it can't rewrite or delete history) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd git-repo + git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_TARGET}" + git push origin 'refs/heads/*:refs/heads/*' + git push origin 'refs/tags/*:refs/tags/*' + if git rev-parse --verify refs/notes/hg >/dev/null 2>&1; then + git push origin refs/notes/hg:refs/notes/hg + fi
--- a/Applications/ApplicationToolbox.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Applications/ApplicationToolbox.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -41,13 +41,13 @@ #include <cassert> -static const char* OPTION_CA_CERTIFICATES = "ca-certificates"; -static const char* OPTION_PASSWORD = "password"; -static const char* OPTION_PROXY = "proxy"; -static const char* OPTION_TIMEOUT = "timeout"; -static const char* OPTION_URL = "orthanc"; -static const char* OPTION_USERNAME = "username"; -static const char* OPTION_VERIFY_PEERS = "verify-peers"; +static const char* const OPTION_CA_CERTIFICATES = "ca-certificates"; +static const char* const OPTION_PASSWORD = "password"; +static const char* const OPTION_PROXY = "proxy"; +static const char* const OPTION_TIMEOUT = "timeout"; +static const char* const OPTION_URL = "orthanc"; +static const char* const OPTION_USERNAME = "username"; +static const char* const OPTION_VERIFY_PEERS = "verify-peers";
--- a/Applications/CMakeLists.txt Fri Aug 21 15:28:23 2026 +0200 +++ b/Applications/CMakeLists.txt Fri Aug 21 17:21:41 2026 +0200 @@ -125,6 +125,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/BackgroundColor.cpp ${ORTHANC_WSI_DIR}/Framework/ColorSpaces.cpp ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp ${ORTHANC_WSI_DIR}/Framework/DicomizerParameters.cpp
--- a/Applications/DicomToTiff.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Applications/DicomToTiff.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -35,14 +35,14 @@ #include "ApplicationToolbox.h" -static const char* OPTION_COLOR = "color"; -static const char* OPTION_HELP = "help"; -static const char* OPTION_INPUT = "input"; -static const char* OPTION_JPEG_QUALITY = "jpeg-quality"; -static const char* OPTION_OUTPUT = "output"; -static const char* OPTION_REENCODE = "reencode"; -static const char* OPTION_VERBOSE = "verbose"; -static const char* OPTION_VERSION = "version"; +static const char* const OPTION_COLOR = "color"; +static const char* const OPTION_HELP = "help"; +static const char* const OPTION_INPUT = "input"; +static const char* const OPTION_JPEG_QUALITY = "jpeg-quality"; +static const char* const OPTION_OUTPUT = "output"; +static const char* const OPTION_REENCODE = "reencode"; +static const char* const OPTION_VERBOSE = "verbose"; +static const char* const OPTION_VERSION = "version"; static bool ParseParameters(int& exitStatus, boost::program_options::variables_map& options,
--- a/Applications/Dicomizer.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Applications/Dicomizer.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -56,50 +56,53 @@ #include <dcmtk/dcmdata/dcvrat.h> -static const char* OPTION_COLOR = "color"; -static const char* OPTION_COMPRESSION = "compression"; -static const char* OPTION_DATASET = "dataset"; -static const char* OPTION_FOLDER = "folder"; -static const char* OPTION_FOLDER_PATTERN = "folder-pattern"; -static const char* OPTION_HELP = "help"; -static const char* OPTION_ICC_PROFILE = "icc-profile"; -static const char* OPTION_IMAGED_DEPTH = "imaged-depth"; -static const char* OPTION_IMAGED_HEIGHT = "imaged-height"; -static const char* OPTION_IMAGED_WIDTH = "imaged-width"; -static const char* OPTION_INPUT = "input"; -static const char* OPTION_JPEG_QUALITY = "jpeg-quality"; -static const char* OPTION_LEVELS = "levels"; -static const char* OPTION_LOWER_LEVELS = "lower-levels"; -static const char* OPTION_MAX_SIZE = "max-size"; -static const char* OPTION_OFFSET_X = "offset-x"; -static const char* OPTION_OFFSET_Y = "offset-y"; -static const char* OPTION_OPENSLIDE = "openslide"; -static const char* OPTION_OPTICAL_PATH = "optical-path"; -static const char* OPTION_PYRAMID = "pyramid"; -static const char* OPTION_REENCODE = "reencode"; -static const char* OPTION_REPAINT = "repaint"; -static const char* OPTION_SAFETY = "safety"; -static const char* OPTION_SAMPLE_DATASET = "sample-dataset"; -static const char* OPTION_SMOOTH = "smooth"; -static const char* OPTION_THREADS = "threads"; -static const char* OPTION_TILE_HEIGHT = "tile-height"; -static const char* OPTION_TILE_WIDTH = "tile-width"; -static const char* OPTION_VERBOSE = "verbose"; -static const char* OPTION_VERSION = "version"; +static const char* const OPTION_COLOR = "color"; +static const char* const OPTION_COMPRESSION = "compression"; +static const char* const OPTION_DATASET = "dataset"; +static const char* const OPTION_FOLDER = "folder"; +static const char* const OPTION_FOLDER_PATTERN = "folder-pattern"; +static const char* const OPTION_HELP = "help"; +static const char* const OPTION_ICC_PROFILE = "icc-profile"; +static const char* const OPTION_IMAGED_DEPTH = "imaged-depth"; +static const char* const OPTION_IMAGED_HEIGHT = "imaged-height"; +static const char* const OPTION_IMAGED_WIDTH = "imaged-width"; +static const char* const OPTION_INPUT = "input"; +static const char* const OPTION_JPEG_QUALITY = "jpeg-quality"; +static const char* const OPTION_LEVELS = "levels"; +static const char* const OPTION_LOWER_LEVELS = "lower-levels"; +static const char* const OPTION_MAX_SIZE = "max-size"; +static const char* const OPTION_OFFSET_X = "offset-x"; +static const char* const OPTION_OFFSET_Y = "offset-y"; +static const char* const OPTION_OPENSLIDE = "openslide"; +static const char* const OPTION_OPTICAL_PATH = "optical-path"; +static const char* const OPTION_PYRAMID = "pyramid"; +static const char* const OPTION_REENCODE = "reencode"; +static const char* const OPTION_REPAINT = "repaint"; +static const char* const OPTION_SAFETY = "safety"; +static const char* const OPTION_SAMPLE_DATASET = "sample-dataset"; +static const char* const OPTION_SMOOTH = "smooth"; +static const char* const OPTION_THREADS = "threads"; +static const char* const OPTION_TILE_HEIGHT = "tile-height"; +static const char* const OPTION_TILE_WIDTH = "tile-width"; +static const char* const OPTION_VERBOSE = "verbose"; +static const char* const OPTION_VERSION = "version"; // New in release 1.1 -static const char* OPTION_CYTOMINE_URL = "cytomine-url"; -static const char* OPTION_CYTOMINE_IMAGE_INSTANCE_ID = "cytomine-image"; -static const char* OPTION_CYTOMINE_PUBLIC_KEY = "cytomine-public-key"; -static const char* OPTION_CYTOMINE_PRIVATE_KEY = "cytomine-private-key"; -static const char* OPTION_CYTOMINE_COMPRESSION = "cytomine-compression"; +static const char* const OPTION_CYTOMINE_URL = "cytomine-url"; +static const char* const OPTION_CYTOMINE_IMAGE_INSTANCE_ID = "cytomine-image"; +static const char* const OPTION_CYTOMINE_PUBLIC_KEY = "cytomine-public-key"; +static const char* const OPTION_CYTOMINE_PRIVATE_KEY = "cytomine-private-key"; +static const char* const OPTION_CYTOMINE_COMPRESSION = "cytomine-compression"; // New in release 2.1 -static const char* OPTION_FORCE_OPENSLIDE = "force-openslide"; -static const char* OPTION_PADDING = "padding"; +static const char* const OPTION_FORCE_OPENSLIDE = "force-openslide"; +static const char* const OPTION_PADDING = "padding"; // New in release 3.3 -static const char* OPTION_ENCODING = "encoding"; +static const char* const OPTION_ENCODING = "encoding"; + +// New in release 4.0 +static const char* const OPTION_MAGNIFICATION = "magnification"; #if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 0) @@ -178,7 +181,7 @@ if (lowerLevelsCount != levelsCount) { LOG(WARNING) << "Constructing the " << lowerLevelsCount << " lower levels of the pyramid"; - OrthancWSI::TruncatedPyramidWriter truncated(target, lowerLevelsCount, source.GetPhotometricInterpretation()); + OrthancWSI::TruncatedPyramidWriter truncated(target, lowerLevelsCount, source.GetPhotometricInterpretation(), source.GetBackgroundColor()); OrthancWSI::ReconstructPyramidCommand::PrepareBagOfTasks (tasks, truncated, source, lowerLevelsCount + 1, 0, parameters); OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount()); @@ -216,14 +219,23 @@ LOG(WARNING) << "Pixel format: " << Orthanc::EnumerationToString(source.GetPixelFormat()); LOG(WARNING) << "Source photometric interpretation: " << Orthanc::EnumerationToString(source.GetPhotometricInterpretation()); LOG(WARNING) << "Source compression: " << EnumerationToString(sourceCompression); + LOG(WARNING) << "Source background color: " << source.GetBackgroundColor().Format(); LOG(WARNING) << "Smoothing is " << (parameters.IsSmoothEnabled() ? "enabled" : "disabled"); if (parameters.IsRepaintBackground()) { - LOG(WARNING) << "Repainting the background with color: (" - << static_cast<int>(parameters.GetBackgroundColorRed()) << "," - << static_cast<int>(parameters.GetBackgroundColorGreen()) << "," - << static_cast<int>(parameters.GetBackgroundColorBlue()) << ")"; + if (parameters.GetBackgroundColor().IsPresent()) + { + LOG(WARNING) << "Repainting the background with color: (" + << static_cast<int>(parameters.GetBackgroundColor().GetRed()) << "," + << static_cast<int>(parameters.GetBackgroundColor().GetGreen()) << "," + << static_cast<int>(parameters.GetBackgroundColor().GetBlue()) << ")"; + } + else + { + // TODO Background color + LOG(WARNING) << "Repainting the background with default color (white)"; + } } else { @@ -233,7 +245,8 @@ Orthanc::PhotometricInterpretation targetPhotometric; bool transcoding; - if (parameters.IsForceReencode() || + if (parameters.IsRepaintBackground() || + parameters.IsForceReencode() || parameters.IsReconstructPyramid() || sourceCompression != parameters.GetTargetCompression()) { @@ -673,15 +686,31 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } + if (!opticalPath->tagExists(DCM_ObjectiveLensPower) && + volume.HasObjectiveLensPower()) + { + OrthancWSI::DicomToolbox::SetStringTag(*opticalPath, DCM_ObjectiveLensPower, + boost::lexical_cast<std::string>(volume.GetObjectiveLensPower())); + } + 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::RGBColor rgb; + + if (parameters.GetBackgroundColor().IsPresent()) + { + rgb = OrthancWSI::RGBColor(parameters.GetBackgroundColor()); + } + else + { + // TODO Background color + rgb = OrthancWSI::RGBColor(255, 255, 255); + } + OrthancWSI::sRGBColor srgb(rgb); OrthancWSI::XYZColor xyz(srgb); OrthancWSI::LABColor lab(xyz); @@ -801,6 +830,8 @@ "X offset the specimen, wrt. slide coordinates origin (in mm)") (OPTION_OFFSET_Y, boost::program_options::value<float>()->default_value(40), "Y offset the specimen, wrt. slide coordinates origin (in mm)") + (OPTION_MAGNIFICATION, boost::program_options::value<float>(), + "Nominal power of the objective lens (typically 40)") ; boost::program_options::options_description restOptions @@ -1182,6 +1213,12 @@ parameters.SetEncoding(Orthanc::StringToEncoding(s.c_str())); } + // New in WSI 4.0 + if (options.count(OPTION_MAGNIFICATION)) + { + volume.SetObjectiveLensPower(options[OPTION_MAGNIFICATION].as<float>()); + } + return true; } @@ -1291,10 +1328,7 @@ { if (parameters.GetPadding() > 1) { - plainImage->SetPadding(parameters.GetPadding(), - parameters.GetBackgroundColorRed(), - parameters.GetBackgroundColorGreen(), - parameters.GetBackgroundColorBlue()); + plainImage->SetPadding(parameters.GetPadding(), parameters.GetBackgroundColor()); } return plainImage.release(); @@ -1309,9 +1343,17 @@ new OrthancWSI::OpenSlidePyramid(path, parameters.GetTargetTileWidth(512), parameters.GetTargetTileHeight(512))); - openslide->SetBackgroundColor(parameters.GetBackgroundColorRed(), - parameters.GetBackgroundColorGreen(), - parameters.GetBackgroundColorBlue()); + if (parameters.GetBackgroundColor().IsPresent()) + { + openslide->SetBackgroundColor(parameters.GetBackgroundColor().GetRed(), + parameters.GetBackgroundColor().GetGreen(), + parameters.GetBackgroundColor().GetBlue()); + } + else + { + // TODO Background color + openslide->SetBackgroundColor(255, 255, 255); + } float volumeWidth, volumeHeight; if (openslide->LookupImagedVolumeSize(volumeWidth, volumeHeight)) @@ -1329,6 +1371,17 @@ } } + float power; + if (!volume.HasObjectiveLensPower() && + openslide->LookupObjectiveLensPower(power)) + { + volume.SetObjectiveLensPower(power); + + char buf[32]; + sprintf(buf, "%.02f", power); + LOG(WARNING) << "Objective power of the scanner (reference magnification): " << buf << "x"; + } + return openslide.release(); } catch (Orthanc::OrthancException&)
--- a/Framework/Algorithms/PyramidReader.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Algorithms/PyramidReader.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -70,11 +70,7 @@ decoded_->GetRegion(a, 0, bottom, that_.sourceTileWidth_, that_.sourceTileHeight_ - bottom); - ImageToolbox::Set(a, - that_.parameters_.GetBackgroundColorRed(), - that_.parameters_.GetBackgroundColorGreen(), - that_.parameters_.GetBackgroundColorBlue()); - + that_.parameters_.FillBackgroundColor(a); } if ((tileX_ + 1) * that_.sourceTileWidth_ > that_.levelWidth_) @@ -87,10 +83,7 @@ decoded_->GetRegion(a, right, 0, that_.sourceTileWidth_ - right, that_.sourceTileHeight_); - ImageToolbox::Set(a, - that_.parameters_.GetBackgroundColorRed(), - that_.parameters_.GetBackgroundColorGreen(), - that_.parameters_.GetBackgroundColorBlue()); + that_.parameters_.FillBackgroundColor(a); } } @@ -158,7 +151,8 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } - decoded_.reset(ImageToolbox::DecodeTile(rawTile_, rawTileCompression_)); + decoded_.reset(that_.DecodeRawTile(rawTile_, rawTileCompression_)); + if (decoded_.get() == NULL) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); @@ -177,15 +171,26 @@ }; + Orthanc::ImageAccessor* PyramidReader::DecodeRawTile(const std::string& tile, + ImageCompression compression) const + { + if (compression == ImageCompression_None) + { + return ImageToolbox::DecodeRawTile(tile, source_.GetPixelFormat(), sourceTileWidth_, sourceTileHeight_); + } + else + { + return ImageToolbox::DecodeTile(tile, compression); + } + } + + Orthanc::ImageAccessor& PyramidReader::GetOutsideTile() { if (outside_.get() == NULL) { outside_.reset(ImageToolbox::Allocate(source_.GetPixelFormat(), targetTileWidth_, targetTileHeight_)); - ImageToolbox::Set(*outside_, - parameters_.GetBackgroundColorRed(), - parameters_.GetBackgroundColorGreen(), - parameters_.GetBackgroundColorBlue()); + parameters_.FillBackgroundColor(*outside_); } return *outside_; @@ -209,7 +214,7 @@ { if (parameters_.IsSafetyCheck()) { - std::unique_ptr<Orthanc::ImageAccessor> decoded(ImageToolbox::DecodeTile(tile, compression)); + std::unique_ptr<Orthanc::ImageAccessor> decoded(DecodeRawTile(tile, compression)); CheckTileSize(*decoded); } }
--- a/Framework/Algorithms/PyramidReader.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Algorithms/PyramidReader.h Fri Aug 21 17:21:41 2026 +0200 @@ -57,6 +57,9 @@ std::unique_ptr<Orthanc::ImageAccessor> outside_; + Orthanc::ImageAccessor* DecodeRawTile(const std::string& tile, + ImageCompression compression) const; + Orthanc::ImageAccessor& GetOutsideTile(); void CheckTileSize(const Orthanc::ImageAccessor& tile) const;
--- a/Framework/Algorithms/ReconstructPyramidCommand.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Algorithms/ReconstructPyramidCommand.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -83,11 +83,8 @@ std::unique_ptr<Orthanc::ImageAccessor> mosaic(ImageToolbox::Allocate(source_.GetPixelFormat(), 2 * target_.GetTileWidth(), 2 * target_.GetTileHeight())); - ImageToolbox::Set(*mosaic, - source_.GetParameters().GetBackgroundColorRed(), - source_.GetParameters().GetBackgroundColorGreen(), - source_.GetParameters().GetBackgroundColorBlue()); + source_.GetParameters().FillBackgroundColor(*mosaic); isEmpty = true; {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/BackgroundColor.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -0,0 +1,180 @@ +/** + * 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 "BackgroundColor.h" + +#include "ImageToolbox.h" + +#include <OrthancException.h> + +#include <boost/lexical_cast.hpp> + + +namespace OrthancWSI +{ + void BackgroundColor::Clear() + { + present_ = false; + red_ = 0; + green_ = 0; + blue_ = 0; + } + + + void BackgroundColor::SetValue(uint8_t red, + uint8_t green, + uint8_t blue) + { + present_ = true; + red_ = red; + green_ = green; + blue_ = blue; + } + + + uint8_t BackgroundColor::GetRed() const + { + if (present_) + { + return red_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + uint8_t BackgroundColor::GetGreen() const + { + if (present_) + { + return green_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + uint8_t BackgroundColor::GetBlue() const + { + if (present_) + { + return blue_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + std::string BackgroundColor::Format() const + { + if (present_) + { + return ("(" + boost::lexical_cast<std::string>(static_cast<unsigned int>(red_)) + "," + + boost::lexical_cast<std::string>(static_cast<unsigned int>(green_)) + "," + + boost::lexical_cast<std::string>(static_cast<unsigned int>(blue_)) + ")"); + } + else + { + return "None"; + } + } + + + std::string BackgroundColor::ToHexadecimalString(uint8_t defaultRed, + uint8_t defaultGreen, + uint8_t defaultBlue) const + { + char tmp[32]; + + if (present_) + { + sprintf(tmp, "#%02x%02x%02x", red_, green_, blue_); + } + else + { + sprintf(tmp, "#%02x%02x%02x", defaultRed, defaultGreen, defaultBlue); + } + + return tmp; + } + + + static unsigned int GetHexDigit(char c) + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + else if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + else if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + BackgroundColor BackgroundColor::FromHexadecimalString(const std::string& color) + { + if (color.size() != 7 || + color[0] != '#') + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + else + { + unsigned int r = GetHexDigit(color[1]) * 16 + GetHexDigit(color[2]); + unsigned int g = GetHexDigit(color[3]) * 16 + GetHexDigit(color[4]); + unsigned int b = GetHexDigit(color[5]) * 16 + GetHexDigit(color[6]); + return BackgroundColor(r, g, b); + } + } + + + void BackgroundColor::Fill(Orthanc::ImageAccessor& region, + uint8_t defaultRed, + uint8_t defaultGreen, + uint8_t defaultBlue) const + { + if (present_) + { + ImageToolbox::Set(region, red_, green_, blue_); + } + else + { + ImageToolbox::Set(region, defaultRed, defaultGreen, defaultBlue); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/BackgroundColor.h Fri Aug 21 17:21:41 2026 +0200 @@ -0,0 +1,88 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-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 <stdint.h> +#include <string> + + +namespace Orthanc +{ + class ImageAccessor; +} + +namespace OrthancWSI +{ + class BackgroundColor + { + private: + bool present_; + uint8_t red_; + uint8_t green_; + uint8_t blue_; + + public: + BackgroundColor() + { + Clear(); + } + + BackgroundColor(uint8_t red, + uint8_t green, + uint8_t blue) + { + SetValue(red, green, blue); + } + + void Clear(); + + void SetValue(uint8_t red, + uint8_t green, + uint8_t blue); + + uint8_t IsPresent() const + { + return present_; + } + + uint8_t GetRed() const; + + uint8_t GetGreen() const; + + uint8_t GetBlue() const; + + std::string Format() const; + + std::string ToHexadecimalString(uint8_t defaultRed, + uint8_t defaultGreen, + uint8_t defaultBlue) const; + + static BackgroundColor FromHexadecimalString(const std::string& color); + + void Fill(Orthanc::ImageAccessor& region, + uint8_t defaultRed, + uint8_t defaultGreen, + uint8_t defaultBlue) const; + }; +}
--- a/Framework/ColorSpaces.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/ColorSpaces.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -30,6 +30,14 @@ namespace OrthancWSI { + RGBColor::RGBColor(const BackgroundColor& color) : + r_(color.GetRed()), + g_(color.GetGreen()), + b_(color.GetBlue()) + { + } + + RGBColor::RGBColor(const sRGBColor& srgb) { if (srgb.GetR() < 0)
--- a/Framework/ColorSpaces.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/ColorSpaces.h Fri Aug 21 17:21:41 2026 +0200 @@ -23,7 +23,8 @@ #pragma once -#include <stdint.h> +#include "BackgroundColor.h" + #include <string> @@ -42,6 +43,13 @@ uint8_t b_; public: + RGBColor() : + r_(0), + g_(0), + b_(0) + { + } + RGBColor(uint8_t r, uint8_t g, uint8_t b) : @@ -51,6 +59,8 @@ { } + explicit RGBColor(const BackgroundColor& color); + explicit RGBColor(const sRGBColor& srgb); uint8_t GetR() const
--- a/Framework/DicomizerParameters.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/DicomizerParameters.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -79,24 +79,10 @@ padding_(1), encoding_(Orthanc::Encoding_Latin1) { - backgroundColor_[0] = 255; - backgroundColor_[1] = 255; - backgroundColor_[2] = 255; threadsCount_ = ChooseNumberOfThreads(); } - void DicomizerParameters::SetBackgroundColor(uint8_t red, - uint8_t green, - uint8_t blue) - { - repaintBackground_ = true; - backgroundColor_[0] = red; - backgroundColor_[1] = green; - backgroundColor_[2] = blue; - } - - void DicomizerParameters::SetTargetTileSize(unsigned int width, unsigned int height) { @@ -376,4 +362,11 @@ padding_ = padding; } } + + + void DicomizerParameters::FillBackgroundColor(Orthanc::ImageAccessor& region) const + { + // TODO Background color + backgroundColor_.Fill(region, 255, 255, 255); + } }
--- a/Framework/DicomizerParameters.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/DicomizerParameters.h Fri Aug 21 17:21:41 2026 +0200 @@ -23,10 +23,12 @@ #pragma once +#include "BackgroundColor.h" +#include "DicomToolbox.h" #include "Inputs/ITiledPyramid.h" #include "Outputs/IPyramidWriter.h" #include "Targets/IFileTarget.h" -#include "DicomToolbox.h" + #include <WebServiceParameters.h> #include <stdint.h> @@ -38,7 +40,7 @@ private: bool safetyCheck_; bool repaintBackground_; - uint8_t backgroundColor_[3]; + BackgroundColor backgroundColor_; ImageCompression targetCompression_; bool hasTargetTileSize_; unsigned int targetTileWidth_; @@ -100,21 +102,14 @@ void SetBackgroundColor(uint8_t red, uint8_t green, - uint8_t blue); - - uint8_t GetBackgroundColorRed() const + uint8_t blue) { - return backgroundColor_[0]; + backgroundColor_.SetValue(red, green, blue); } - uint8_t GetBackgroundColorGreen() const + const BackgroundColor& GetBackgroundColor() const { - return backgroundColor_[1]; - } - - uint8_t GetBackgroundColorBlue() const - { - return backgroundColor_[2]; + return backgroundColor_; } void SetTargetCompression(ImageCompression compression) @@ -313,5 +308,7 @@ { encoding_ = encoding; } + + void FillBackgroundColor(Orthanc::ImageAccessor& region) const; }; }
--- a/Framework/ImageToolbox.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/ImageToolbox.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -121,6 +121,11 @@ Orthanc::ImageProcessing::Set(image, r, g, b, 0 /* alpha is ignored */); break; + case Orthanc::PixelFormat_BGRA32: + case Orthanc::PixelFormat_RGBA32: + Orthanc::ImageProcessing::Set(image, r, g, b, 255); + break; + default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); }
--- a/Framework/ImagedVolumeParameters.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/ImagedVolumeParameters.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -37,7 +37,10 @@ // Typical parameters for a specimen, in millimeters depth_(1), offsetX_(20), - offsetY_(40) + offsetY_(40), + + hasObjectiveLensPower_(false), + objectiveLensPower_(0) { } @@ -133,4 +136,31 @@ physicalX = offsetX_ - GetHeight() * static_cast<float>(imageY) / static_cast<float>(totalHeight); physicalY = offsetY_ - GetWidth() * static_cast<float>(imageX) / static_cast<float>(totalWidth); } + + + float ImagedVolumeParameters::GetObjectiveLensPower() const + { + if (hasObjectiveLensPower_) + { + return objectiveLensPower_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void ImagedVolumeParameters::SetObjectiveLensPower(float power) + { + if (power <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + objectiveLensPower_ = power; + hasObjectiveLensPower_ = true; + } + } }
--- a/Framework/ImagedVolumeParameters.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/ImagedVolumeParameters.h Fri Aug 21 17:21:41 2026 +0200 @@ -38,6 +38,10 @@ float offsetX_; float offsetY_; + // New in WSI 4.0 + bool hasObjectiveLensPower_; + float objectiveLensPower_; // The magnification level reported by the scanner + public: ImagedVolumeParameters(); @@ -94,5 +98,14 @@ unsigned int imageY, unsigned int totalWidth, unsigned int totalHeight) const; + + bool HasObjectiveLensPower() const + { + return hasObjectiveLensPower_; + } + + float GetObjectiveLensPower() const; + + void SetObjectiveLensPower(float power); }; }
--- a/Framework/Inputs/DecodedTiledPyramid.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/DecodedTiledPyramid.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -33,32 +33,6 @@ namespace OrthancWSI { - DecodedTiledPyramid::DecodedTiledPyramid() - { - SetBackgroundColor(255, 255, 255); - } - - - void DecodedTiledPyramid::SetBackgroundColor(uint8_t red, - uint8_t green, - uint8_t blue) - { - backgroundColor_[0] = red; - backgroundColor_[1] = green; - backgroundColor_[2] = blue; - } - - - void DecodedTiledPyramid::GetBackgroundColor(uint8_t& red, - uint8_t& green, - uint8_t& blue) const - { - red = backgroundColor_[0]; - green = backgroundColor_[1]; - blue = backgroundColor_[2]; - } - - Orthanc::ImageAccessor* DecodedTiledPyramid::DecodeTile(bool& isEmpty, unsigned int level, unsigned int tileX, @@ -74,7 +48,7 @@ y >= GetLevelHeight(level)) // (*) { isEmpty = true; - ImageToolbox::Set(*tile, backgroundColor_[0], backgroundColor_[1], backgroundColor_[2]); + backgroundColor_.Fill(*tile, 255, 255, 255); // TODO Background color return tile.release(); } @@ -116,7 +90,7 @@ ReadRegion(*cropped, isEmpty, level, x, y); // Create a white tile, and fill it with the cropped content - ImageToolbox::Set(*tile, backgroundColor_[0], backgroundColor_[1], backgroundColor_[2]); + backgroundColor_.Fill(*tile, 255, 255, 255); // TODO Background color ImageToolbox::Embed(*tile, *cropped, 0, 0); }
--- a/Framework/Inputs/DecodedTiledPyramid.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/DecodedTiledPyramid.h Fri Aug 21 17:21:41 2026 +0200 @@ -23,6 +23,7 @@ #pragma once +#include "../BackgroundColor.h" #include "ITiledPyramid.h" @@ -36,7 +37,7 @@ class DecodedTiledPyramid : public ITiledPyramid { private: - uint8_t backgroundColor_[3]; + BackgroundColor backgroundColor_; protected: // Subclasses can assume that the requested region is fully inside @@ -49,15 +50,17 @@ unsigned int y) = 0; public: - DecodedTiledPyramid(); - void SetBackgroundColor(uint8_t red, uint8_t green, - uint8_t blue); + uint8_t blue) + { + backgroundColor_.SetValue(red, green, blue); + } - void GetBackgroundColor(uint8_t& red, - uint8_t& green, - uint8_t& blue) const; + virtual BackgroundColor GetBackgroundColor() const ORTHANC_OVERRIDE + { + return backgroundColor_; + } virtual Orthanc::ImageAccessor* DecodeTile(bool& isEmpty, unsigned int level,
--- a/Framework/Inputs/DicomPyramid.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/DicomPyramid.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -109,13 +109,7 @@ tokens[2] == "VOLUME" || tokens[2] == "THUMBNAIL" /* "may be the apex (lowest resolution) layer of a Multi-Resolution Pyramid" */) { - if (instance->HasBackgroundColor()) - { - backgroundRed_ = instance->GetBackgroundRed(); - backgroundGreen_ = instance->GetBackgroundGreen(); - backgroundBlue_ = instance->GetBackgroundBlue(); - } - + backgroundColor_ = instance->GetBackgroundColor(); instances_.push_back(instance.release()); } } @@ -170,10 +164,7 @@ const std::string& seriesId, bool useCache) : orthanc_(orthanc), - seriesId_(seriesId), - backgroundRed_(255), - backgroundGreen_(255), - backgroundBlue_(255) + seriesId_(seriesId) { RegisterInstances(seriesId, useCache);
--- a/Framework/Inputs/DicomPyramid.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/DicomPyramid.h Fri Aug 21 17:21:41 2026 +0200 @@ -38,9 +38,7 @@ std::string seriesId_; std::vector<DicomPyramidInstance*> instances_; std::vector<DicomPyramidLevel*> levels_; - uint8_t backgroundRed_; - uint8_t backgroundGreen_; - uint8_t backgroundBlue_; + BackgroundColor backgroundColor_; void Clear(); @@ -89,19 +87,9 @@ virtual Orthanc::PhotometricInterpretation GetPhotometricInterpretation() const ORTHANC_OVERRIDE; - uint8_t GetBackgroundRed() const - { - return backgroundRed_; - } - - uint8_t GetBackgroundGreen() const + virtual BackgroundColor GetBackgroundColor() const ORTHANC_OVERRIDE { - return backgroundGreen_; - } - - uint8_t GetBackgroundBlue() const - { - return backgroundBlue_; + return backgroundColor_; } bool LookupImagedVolumeSize(double& width,
--- a/Framework/Inputs/DicomPyramidInstance.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/DicomPyramidInstance.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -290,10 +290,7 @@ XYZColor xyz(lab); sRGBColor srgb(xyz); RGBColor rgb(srgb); - hasBackgroundColor_ = true; - backgroundRed_ = rgb.GetR(); - backgroundGreen_ = rgb.GetG(); - backgroundBlue_ = rgb.GetB(); + backgroundColor_.SetValue(rgb.GetR(), rgb.GetG(), rgb.GetB()); } } @@ -310,10 +307,6 @@ instanceId_(instanceId), hasCompression_(false), compression_(ImageCompression_None), // Dummy initialization for serialization - hasBackgroundColor_(false), - backgroundRed_(0), - backgroundGreen_(0), - backgroundBlue_(0), hasImagedVolumeSize_(false), imagedVolumeWidth_(0), imagedVolumeHeight_(0), @@ -408,12 +401,12 @@ content[IMAGE_TYPE] = imageType_; content[VERSION] = SERIALIZED_VERSION; - if (hasBackgroundColor_) + if (backgroundColor_.IsPresent()) { Json::Value color = Json::arrayValue; - color.append(backgroundRed_); - color.append(backgroundGreen_); - color.append(backgroundBlue_); + color.append(backgroundColor_.GetRed()); + color.append(backgroundColor_.GetGreen()); + color.append(backgroundColor_.GetBlue()); content[BACKGROUND_COLOR] = color; } @@ -484,7 +477,7 @@ frames_[i].second = f[i][1].asInt(); } - hasBackgroundColor_ = false; + backgroundColor_.Clear(); if (content.isMember(BACKGROUND_COLOR)) { const Json::Value& color = content[BACKGROUND_COLOR]; @@ -494,10 +487,7 @@ color[1].isUInt() && color[2].isUInt()) { - hasBackgroundColor_ = true; - backgroundRed_ = color[0].asUInt(); - backgroundGreen_ = color[1].asUInt(); - backgroundBlue_ = color[2].asUInt(); + backgroundColor_.SetValue(color[0].asUInt(), color[1].asUInt(), color[2].asUInt()); } } @@ -520,45 +510,6 @@ } - 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); - } - } - - double DicomPyramidInstance::GetImagedVolumeWidth() const { if (hasImagedVolumeSize_)
--- a/Framework/Inputs/DicomPyramidInstance.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/DicomPyramidInstance.h Fri Aug 21 17:21:41 2026 +0200 @@ -23,8 +23,9 @@ #pragma once +#include "../../Resources/Orthanc/Stone/IOrthancConnection.h" +#include "../BackgroundColor.h" #include "../Enumerations.h" -#include "../../Resources/Orthanc/Stone/IOrthancConnection.h" #include <boost/noncopyable.hpp> #include <vector> @@ -47,10 +48,7 @@ std::vector<FrameLocation> frames_; Orthanc::PhotometricInterpretation photometric_; std::string imageType_; - bool hasBackgroundColor_; - uint8_t backgroundRed_; - uint8_t backgroundGreen_; - uint8_t backgroundBlue_; + BackgroundColor backgroundColor_; bool hasImagedVolumeSize_; double imagedVolumeWidth_; double imagedVolumeHeight_; @@ -120,17 +118,11 @@ void Serialize(std::string& result) const; - bool HasBackgroundColor() const + const BackgroundColor& GetBackgroundColor() const { - return hasBackgroundColor_; + return backgroundColor_; } - uint8_t GetBackgroundRed() const; - - uint8_t GetBackgroundGreen() const; - - uint8_t GetBackgroundBlue() const; - bool HasImagedVolumeSize() const { return hasImagedVolumeSize_;
--- a/Framework/Inputs/HierarchicalTiff.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/HierarchicalTiff.h Fri Aug 21 17:21:41 2026 +0200 @@ -106,5 +106,10 @@ bool LookupImagedVolumeSize(double& width, double& height) const; + + virtual BackgroundColor GetBackgroundColor() const ORTHANC_OVERRIDE + { + return BackgroundColor(); // No background color + } }; }
--- a/Framework/Inputs/ITiledPyramid.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/ITiledPyramid.h Fri Aug 21 17:21:41 2026 +0200 @@ -23,6 +23,7 @@ #pragma once +#include "../BackgroundColor.h" #include "../Enumerations.h" #include <Compatibility.h> @@ -69,5 +70,7 @@ virtual Orthanc::PixelFormat GetPixelFormat() const = 0; virtual Orthanc::PhotometricInterpretation GetPhotometricInterpretation() const = 0; + + virtual BackgroundColor GetBackgroundColor() const = 0; }; }
--- a/Framework/Inputs/OpenSlideLibrary.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/OpenSlideLibrary.h Fri Aug 21 17:21:41 2026 +0200 @@ -29,6 +29,26 @@ #include <map> #include <vector> + +/** + * List of the properties supported by OpenSlide, taken from + * "openslide.h". As orthanc-wsi loads the OpenSlide shared library at + * runtime, the file "openslide.h" is not available at compile time. + **/ + +#define OPENSLIDE_PROPERTY_NAME_COMMENT "openslide.comment" +#define OPENSLIDE_PROPERTY_NAME_VENDOR "openslide.vendor" +#define OPENSLIDE_PROPERTY_NAME_QUICKHASH1 "openslide.quickhash-1" +#define OPENSLIDE_PROPERTY_NAME_BACKGROUND_COLOR "openslide.background-color" // Since 3.2.3 +#define OPENSLIDE_PROPERTY_NAME_OBJECTIVE_POWER "openslide.objective-power" // Since 3.3.0 +#define OPENSLIDE_PROPERTY_NAME_MPP_X "openslide.mpp-x" // Since 3.3.0 +#define OPENSLIDE_PROPERTY_NAME_MPP_Y "openslide.mpp-y" // Since 3.3.0 +#define OPENSLIDE_PROPERTY_NAME_BOUNDS_X "openslide.bounds-x" // Since 3.4.0 +#define OPENSLIDE_PROPERTY_NAME_BOUNDS_Y "openslide.bounds-y" // Since 3.4.0 +#define OPENSLIDE_PROPERTY_NAME_BOUNDS_WIDTH "openslide.bounds-width" // Since 3.4.0 +#define OPENSLIDE_PROPERTY_NAME_BOUNDS_HEIGHT "openslide.bounds-height" // Since 3.4.0 + + namespace OrthancWSI { class OpenSlideLibrary : public boost::noncopyable
--- a/Framework/Inputs/OpenSlidePyramid.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/OpenSlidePyramid.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -93,7 +93,19 @@ // Implements alpha blending: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending uint8_t backgroundRed, backgroundGreen, backgroundBlue; - GetBackgroundColor(backgroundRed, backgroundGreen, backgroundBlue); + if (GetBackgroundColor().IsPresent()) + { + backgroundRed = GetBackgroundColor().GetRed(); + backgroundGreen = GetBackgroundColor().GetGreen(); + backgroundBlue = GetBackgroundColor().GetBlue(); + } + else + { + // TODO Background color + backgroundRed = 255; + backgroundGreen = 255; + backgroundBlue = 255; + } for (unsigned int yy = 0; yy < height; yy++) { @@ -144,9 +156,9 @@ double mppx; double mppy; - if (image_.LookupProperty(s, "openslide.mpp-x") && + if (image_.LookupProperty(s, OPENSLIDE_PROPERTY_NAME_MPP_X) && Orthanc::SerializationToolbox::ParseDouble(mppx, s) && - image_.LookupProperty(s, "openslide.mpp-y") && + image_.LookupProperty(s, OPENSLIDE_PROPERTY_NAME_MPP_Y) && Orthanc::SerializationToolbox::ParseDouble(mppy, s)) { width = mppx / 1000.0 * static_cast<double>(image_.GetLevelWidth(0)); @@ -171,4 +183,13 @@ return countPixels * Orthanc::GetBytesPerPixel(Orthanc::PixelFormat_RGBA32); } + + + bool OpenSlidePyramid::LookupObjectiveLensPower(float& power) const + { + std::string s; + + return (image_.LookupProperty(s, OPENSLIDE_PROPERTY_NAME_OBJECTIVE_POWER) && + Orthanc::SerializationToolbox::ParseFloat(power, s)); + } }
--- a/Framework/Inputs/OpenSlidePyramid.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/OpenSlidePyramid.h Fri Aug 21 17:21:41 2026 +0200 @@ -86,5 +86,7 @@ float& height) const; size_t GetMemoryUsage() const ORTHANC_OVERRIDE; + + bool LookupObjectiveLensPower(float& power) const; }; }
--- a/Framework/Inputs/SingleLevelDecodedPyramid.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/SingleLevelDecodedPyramid.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -47,7 +47,8 @@ } else { - Orthanc::ImageProcessing::Set(target, backgroundRed_, backgroundGreen_, backgroundBlue_, 255); + // TODO Background color + backgroundColor_.Fill(target, 255, 255, 255); if (x < image_.GetWidth() && y < image_.GetHeight()) @@ -69,10 +70,7 @@ unsigned int tileHeight) : tileWidth_(tileWidth), tileHeight_(tileHeight), - padding_(0), - backgroundRed_(255), - backgroundGreen_(255), - backgroundBlue_(255) + padding_(0) { } @@ -130,13 +128,9 @@ void SingleLevelDecodedPyramid::SetPadding(unsigned int padding, - uint8_t backgroundRed, - uint8_t backgroundGreen, - uint8_t backgroundBlue) + const BackgroundColor& backgroundColor) { padding_ = padding; - backgroundRed_ = backgroundRed; - backgroundGreen_ = backgroundGreen; - backgroundBlue_ = backgroundBlue; + backgroundColor_ = backgroundColor; } }
--- a/Framework/Inputs/SingleLevelDecodedPyramid.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/SingleLevelDecodedPyramid.h Fri Aug 21 17:21:41 2026 +0200 @@ -23,6 +23,7 @@ #pragma once +#include "../BackgroundColor.h" #include "DecodedTiledPyramid.h" namespace OrthancWSI @@ -34,9 +35,7 @@ unsigned int tileWidth_; unsigned int tileHeight_; unsigned int padding_; - uint8_t backgroundRed_; - uint8_t backgroundGreen_; - uint8_t backgroundBlue_; + BackgroundColor backgroundColor_; protected: void SetImage(const Orthanc::ImageAccessor& image) @@ -81,9 +80,7 @@ virtual Orthanc::PhotometricInterpretation GetPhotometricInterpretation() const ORTHANC_OVERRIDE; void SetPadding(unsigned int padding, - uint8_t backgroundRed, - uint8_t backgroundGreen, - uint8_t backgroundBlue); + const BackgroundColor& backgroundColor); size_t GetMemoryUsage() const ORTHANC_OVERRIDE {
--- a/Framework/Inputs/TiledPyramidStatistics.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Inputs/TiledPyramidStatistics.h Fri Aug 21 17:21:41 2026 +0200 @@ -87,5 +87,10 @@ { return source_.GetPhotometricInterpretation(); } + + virtual BackgroundColor GetBackgroundColor() const ORTHANC_OVERRIDE + { + return source_.GetBackgroundColor(); + } }; }
--- a/Framework/Outputs/InMemoryTiledImage.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Outputs/InMemoryTiledImage.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -48,13 +48,15 @@ unsigned int countTilesY, unsigned int tileWidth, unsigned int tileHeight, - Orthanc::PhotometricInterpretation photometric) : + Orthanc::PhotometricInterpretation photometric, + BackgroundColor backgroundColor) : format_(format), countTilesX_(countTilesX), countTilesY_(countTilesY), tileWidth_(tileWidth), tileHeight_(tileHeight), - photometric_(photometric) + photometric_(photometric), + backgroundColor_(backgroundColor) { } @@ -147,7 +149,17 @@ unsigned int tileX, unsigned int tileY) { - std::unique_ptr<Orthanc::ImageAccessor> decoded(ImageToolbox::DecodeTile(raw, compression)); + std::unique_ptr<Orthanc::ImageAccessor> decoded; + + if (compression == ImageCompression_None) + { + decoded.reset(ImageToolbox::DecodeRawTile(raw, format_, tileWidth_, tileHeight_)); + } + else + { + decoded.reset(ImageToolbox::DecodeTile(raw, compression)); + } + EncodeTile(*decoded, level, tileX, tileY); }
--- a/Framework/Outputs/InMemoryTiledImage.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Outputs/InMemoryTiledImage.h Fri Aug 21 17:21:41 2026 +0200 @@ -47,6 +47,7 @@ unsigned int tileHeight_; Tiles tiles_; Orthanc::PhotometricInterpretation photometric_; + BackgroundColor backgroundColor_; public: InMemoryTiledImage(Orthanc::PixelFormat format, @@ -54,7 +55,8 @@ unsigned int countTilesY, unsigned int tileWidth, unsigned int tileHeight, - Orthanc::PhotometricInterpretation photometric); + Orthanc::PhotometricInterpretation photometric, + BackgroundColor backgroundColor); virtual ~InMemoryTiledImage(); @@ -124,5 +126,10 @@ { return photometric_; } + + virtual BackgroundColor GetBackgroundColor() const ORTHANC_OVERRIDE + { + return backgroundColor_; + } }; }
--- a/Framework/Outputs/TruncatedPyramidWriter.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Outputs/TruncatedPyramidWriter.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -30,14 +30,16 @@ { TruncatedPyramidWriter::TruncatedPyramidWriter(IPyramidWriter& lower, unsigned int upperLevelIndex, - Orthanc::PhotometricInterpretation photometric) : + Orthanc::PhotometricInterpretation photometric, + const BackgroundColor& backgroundColor) : lowerLevels_(lower), upperLevel_(lower.GetPixelFormat(), lower.GetCountTilesX(upperLevelIndex), lower.GetCountTilesY(upperLevelIndex), lower.GetTileWidth(), lower.GetTileHeight(), - photometric), + photometric, + backgroundColor), upperLevelIndex_(upperLevelIndex) { if (upperLevelIndex > lower.GetLevelCount())
--- a/Framework/Outputs/TruncatedPyramidWriter.h Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/Outputs/TruncatedPyramidWriter.h Fri Aug 21 17:21:41 2026 +0200 @@ -37,7 +37,8 @@ public: TruncatedPyramidWriter(IPyramidWriter& lower, unsigned int upperLevelIndex, - Orthanc::PhotometricInterpretation photometric); + Orthanc::PhotometricInterpretation photometric, + const BackgroundColor& backgroundColor); virtual unsigned int GetLevelCount() const ORTHANC_OVERRIDE {
--- a/Framework/TiffReader.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/Framework/TiffReader.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -26,6 +26,9 @@ #include <Logging.h> #include <OrthancException.h> +#include <boost/lexical_cast.hpp> + + namespace OrthancWSI { TiffReader::TiffReader(const std::string& path) @@ -49,6 +52,44 @@ } + static std::string DescribeTiffPlanarConfig(unsigned int value) + { + switch (value) + { + case PLANARCONFIG_CONTIG: + return "CONTIG"; + + case PLANARCONFIG_SEPARATE: + return "SEPARATE"; + + default: + return boost::lexical_cast<std::string>(value); + } + } + + + static std::string DescribeTiffPhotometry(unsigned int value) + { + switch (value) + { + case PHOTOMETRIC_RGB: + return "RGB"; + + case PHOTOMETRIC_PALETTE: + return "PALETTE"; + + case PHOTOMETRIC_YCBCR: + return "YCBCR"; + + case PHOTOMETRIC_CIELAB: + return "CIELAB"; + + default: + return boost::lexical_cast<std::string>(value); + } + } + + bool TiffReader::GetCurrentDirectoryInformation(ImageCompression& compression, Orthanc::PixelFormat& pixelFormat, Orthanc::PhotometricInterpretation& photometric) @@ -103,7 +144,7 @@ break; default: - LOG(ERROR) << "Unknown photometric interpretation in TIFF: " << photometricTiff; + LOG(WARNING) << "Unsupported photometric interpretation in TIFF: " << DescribeTiffPhotometry(photometricTiff); return false; } } @@ -124,6 +165,10 @@ } else { + LOG(WARNING) << "Unsupported TIFF layout: " << EnumerationToString(compression) << " with " + << channels << " channels, " << bpp << " bpp, " + << DescribeTiffPlanarConfig(planar) << " planar configuration, and " + << DescribeTiffPhotometry(photometricTiff) << " photometry"; return false; }
--- a/NEWS Fri Aug 21 15:28:23 2026 +0200 +++ b/NEWS Fri Aug 21 17:21:41 2026 +0200 @@ -12,8 +12,12 @@ ----------- * OrthancWSIDicomizer: - - Now adding a FrameOfReferenceUID DICOM tag. This is required, amongst others, - by the measurement tools in the OHIF microscopy viewer. + - Fill the DICOM tag "Objective Lens Power Attribute" (0048,0112) + with the scanner magnification, either when using OpenSlide or + when the "--magnification" command-line option is provided + - Now adding a FrameOfReferenceUID DICOM tag. This is required, amongst others, + by the measurement tools in the OHIF microscopy viewer + - Support DICOM-ization of tiled TIFF with no compression * Viewer plugin: - Added CMake option "ORTHANC_SDK_VERSION"
--- a/ViewerPlugin/CMakeLists.txt Fri Aug 21 15:28:23 2026 +0200 +++ b/ViewerPlugin/CMakeLists.txt Fri Aug 21 17:21:41 2026 +0200 @@ -197,6 +197,7 @@ RawTile.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
--- a/ViewerPlugin/Plugin.cpp Fri Aug 21 15:28:23 2026 +0200 +++ b/ViewerPlugin/Plugin.cpp Fri Aug 21 17:21:41 2026 +0200 @@ -95,6 +95,9 @@ result["TilesSizes"] = tilesSizes; result["TotalHeight"] = totalHeight; result["TotalWidth"] = totalWidth; + + // New in WSI 2.1 (Default background is white) + result["BackgroundColor"] = pyramid.GetBackgroundColor().ToHexadecimalString(255, 255, 255); } @@ -113,15 +116,6 @@ OrthancWSI::DicomPyramidCache::Locker locker(seriesId); DescribePyramid(answer, locker.GetPyramid()); - { - // New in WSI 2.1 - char tmp[16]; - sprintf(tmp, "#%02x%02x%02x", locker.GetPyramid().GetBackgroundRed(), - locker.GetPyramid().GetBackgroundGreen(), - locker.GetPyramid().GetBackgroundBlue()); - answer["BackgroundColor"] = tmp; - } - // New in WSI 3.1 double imagedVolumeWidth, imagedVolumeHeight; if (locker.GetPyramid().LookupImagedVolumeSize(imagedVolumeWidth, imagedVolumeHeight)) @@ -156,15 +150,6 @@ { OrthancWSI::DecodedPyramidCache::Accessor accessor(OrthancWSI::DecodedPyramidCache::GetInstance(), instanceId, frameNumber); DescribePyramid(answer, accessor.GetPyramid()); - - { - uint8_t red, green, blue; - accessor.GetPyramid().GetBackgroundColor(red, green, blue); - - char tmp[16]; - sprintf(tmp, "#%02x%02x%02x", red, green, blue); - answer["BackgroundColor"] = tmp; - } } OrthancWSI::ViewerToolbox::AnswerJson(output, answer);
