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);