[med-svn] [Git][med-team/orthanc-wsi][upstream] New upstream version 3.0+dfsg

Sebastien Jodogne (@jodogne-guest) gitlab at salsa.debian.org
Fri Dec 20 17:55:01 GMT 2024



Sebastien Jodogne pushed to branch upstream at Debian Med / orthanc-wsi


Commits:
aa95bf05 by jodogne-guest at 2024-12-20T18:42:41+01:00
New upstream version 3.0+dfsg
- - - - -


29 changed files:

- + .clang-format
- .hg_archival.txt
- Applications/CMakeLists.txt
- CITATION.cff
- Framework/ImageToolbox.cpp
- Framework/ImageToolbox.h
- Framework/Inputs/CytomineImage.h
- + Framework/Inputs/DecodedPyramidCache.cpp
- + Framework/Inputs/DecodedPyramidCache.h
- Framework/Inputs/DecodedTiledPyramid.h
- + Framework/Inputs/OnTheFlyPyramid.cpp
- + Framework/Inputs/OnTheFlyPyramid.h
- Framework/Inputs/OpenSlidePyramid.cpp
- Framework/Inputs/OpenSlidePyramid.h
- Framework/Inputs/SingleLevelDecodedPyramid.h
- NEWS
- Resources/CMake/Version.cmake
- Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
- Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
- Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
- Resources/SyncOrthancFolder.py
- ViewerPlugin/CMakeLists.txt
- ViewerPlugin/IIIF.cpp
- ViewerPlugin/OrthancExplorer.js
- + ViewerPlugin/OrthancPyramidFrameFetcher.cpp
- + ViewerPlugin/OrthancPyramidFrameFetcher.h
- ViewerPlugin/Plugin.cpp
- ViewerPlugin/RawTile.cpp
- ViewerPlugin/viewer.js


Changes:

=====================================
.clang-format
=====================================
@@ -0,0 +1,57 @@
+---
+Language: Cpp
+BasedOnStyle: LLVM
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignOperands: true
+AlignTrailingComments: false
+AlwaysBreakTemplateDeclarations: Yes
+BraceWrapping: 
+  AfterCaseLabel: true
+  AfterClass: true
+  AfterControlStatement: true
+  AfterEnum: true
+  AfterFunction: true
+  AfterNamespace: true
+  AfterStruct: true
+  AfterUnion: true
+  AfterExternBlock: true
+  BeforeCatch: true
+  BeforeElse: true
+  BeforeLambdaBody: true
+  BeforeWhile: true
+  IndentBraces: false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBraces: Custom
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializers: AfterColon
+BreakConstructorInitializersBeforeComma: false
+ColumnLimit: 200
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ContinuationIndentWidth: 2
+IncludeCategories: 
+  - Regex: '^<.*'
+    Priority: 1
+  - Regex: '^".*'
+    Priority: 2
+  - Regex: '.*'
+    Priority: 3
+IncludeIsMainRegex: '([-_](test|unittest))?$'
+IndentCaseLabels: true
+InsertNewlineAtEOF: true
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 2
+NamespaceIndentation: All
+SpaceAfterCStyleCast: true
+SpaceAfterTemplateKeyword: false
+SpaceBeforeRangeBasedForLoopColon: false
+SpaceInEmptyParentheses: false
+SpacesInAngles: false
+SpacesInConditionalStatement: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+TabWidth: 2
+...


=====================================
.hg_archival.txt
=====================================
@@ -1,6 +1,6 @@
 repo: 4a7a53257c7df5a97aea39377b8c9a6e815c9763
-node: db391f32b91ce9ce78b4d991e2b64ac7dffd4a99
-branch: OrthancWSI-2.1
+node: 27b03f0b4550f283b3d2c166451a481128ff131d
+branch: OrthancWSI-3.0
 latesttag: null
-latesttagdistance: 315
-changessincelatesttag: 324
+latesttagdistance: 343
+changessincelatesttag: 352


=====================================
Applications/CMakeLists.txt
=====================================
@@ -114,11 +114,13 @@ set(ORTHANC_WSI_SOURCES
   ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp
   ${ORTHANC_WSI_DIR}/Framework/ImagedVolumeParameters.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/CytomineImage.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedPyramidCache.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedTiledPyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidInstance.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidLevel.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/HierarchicalTiff.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/OnTheFlyPyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/OpenSlideLibrary.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/OpenSlidePyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/PlainTiff.cpp


=====================================
CITATION.cff
=====================================
@@ -37,5 +37,5 @@ authors:
 doi: "10.5220/0006155100810087"
 license: "AGPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc-wsi/"
-version: "2.0"
-date-released: 2023-10-07
+version: "3.0"
+date-released: 2024-12-20


=====================================
Framework/ImageToolbox.cpp
=====================================
@@ -329,5 +329,24 @@ namespace OrthancWSI
       }
 #endif
     }
+
+
+    ImageCompression Convert(Orthanc::MimeType type)
+    {
+      switch (type)
+      {
+        case Orthanc::MimeType_Png:
+          return ImageCompression_Png;
+
+        case Orthanc::MimeType_Jpeg:
+          return ImageCompression_Jpeg;
+
+        case Orthanc::MimeType_Jpeg2000:
+          return ImageCompression_Jpeg2000;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
   }
 }


=====================================
Framework/ImageToolbox.h
=====================================
@@ -72,5 +72,7 @@ namespace OrthancWSI
     void CheckConstantTileSize(const ITiledPyramid& source);
 
     void ConvertJpegYCbCrToRgb(Orthanc::ImageAccessor& image /* inplace */);
+
+    ImageCompression Convert(Orthanc::MimeType type);
   }
 }


=====================================
Framework/Inputs/CytomineImage.h
=====================================
@@ -91,5 +91,10 @@ namespace OrthancWSI
     }
 
     void SetImageCompression(ImageCompression compression);
+
+    virtual size_t GetMemoryUsage() const ORTHANC_OVERRIDE
+    {
+      return 0;  // Image is stored on the remote Cytomine server
+    }
   };
 }


=====================================
Framework/Inputs/DecodedPyramidCache.cpp
=====================================
@@ -0,0 +1,257 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersWSI.h"
+#include "DecodedPyramidCache.h"
+
+
+static std::unique_ptr<OrthancWSI::DecodedPyramidCache>  singleton_;
+
+namespace OrthancWSI
+{
+  class DecodedPyramidCache::CachedPyramid : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr<DecodedTiledPyramid>  pyramid_;
+    size_t                                memory_;
+
+  public:
+    explicit CachedPyramid(DecodedTiledPyramid* pyramid) :
+      pyramid_(pyramid)
+    {
+      if (pyramid == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+      }
+      else
+      {
+        memory_ = pyramid->GetMemoryUsage();
+      }
+    }
+
+    DecodedTiledPyramid& GetPyramid() const
+    {
+      assert(pyramid_ != NULL);
+      return *pyramid_;
+    }
+
+    size_t GetMemoryUsage() const
+    {
+      return memory_;
+    }
+  };
+
+
+  bool DecodedPyramidCache::SanityCheck()
+  {
+    return (cache_.GetSize() < maxCount_);
+  }
+
+
+  void DecodedPyramidCache::MakeRoom(size_t memory)
+  {
+    // Mutex must be locked
+
+    while (cache_.GetSize() >= maxCount_ ||
+           (!cache_.IsEmpty() &&
+            maxMemory_ != 0 &&
+            memoryUsage_ + memory > maxMemory_))
+    {
+      CachedPyramid* oldest = NULL;
+      cache_.RemoveOldest(oldest);
+
+      if (oldest == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      else
+      {
+        memoryUsage_ -= oldest->GetMemoryUsage();
+        delete oldest;
+      }
+    }
+
+    assert(SanityCheck());
+  }
+
+
+  DecodedPyramidCache::CachedPyramid * DecodedPyramidCache::Store(FrameIdentifier identifier,
+                                                                  DecodedTiledPyramid *pyramid)
+  {
+    // Mutex must be locked
+
+    std::unique_ptr<CachedPyramid> payload(new CachedPyramid(pyramid));
+    CachedPyramid* result;
+
+    if (cache_.Contains(identifier, result))
+    {
+      // This element has already been cached by another thread
+      if (result == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      // Tag the series as the most recently used
+      cache_.MakeMostRecent(identifier);
+
+      return result;
+    }
+    else
+    {
+      result = payload.get();
+
+      MakeRoom(payload->GetMemoryUsage());
+
+      memoryUsage_ += payload->GetMemoryUsage();
+
+      // Add a new element to the cache and make it the most
+      // recently used entry
+      cache_.Add(identifier, payload.release());
+
+      assert(SanityCheck());
+      return result;
+    }
+  }
+
+
+  DecodedPyramidCache::DecodedPyramidCache(IPyramidFetcher *fetcher,
+                                               size_t maxCount,
+                                               size_t maxMemory):
+    fetcher_(fetcher),
+    maxCount_(maxCount),
+    maxMemory_(maxMemory),  // 256 MB
+    memoryUsage_(0)
+  {
+    if (fetcher == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    if (maxCount == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(SanityCheck());
+  }
+
+
+  DecodedPyramidCache::~DecodedPyramidCache()
+  {
+    while (!cache_.IsEmpty())
+    {
+      CachedPyramid* pyramid = NULL;
+      cache_.RemoveOldest(pyramid);
+
+      if (pyramid != NULL)
+      {
+        delete pyramid;
+      }
+    }
+  }
+
+
+  void DecodedPyramidCache::InitializeInstance(IPyramidFetcher *fetcher,
+                                                 size_t maxSize,
+                                                 size_t maxMemory)
+  {
+    if (singleton_.get() == NULL)
+    {
+      singleton_.reset(new DecodedPyramidCache(fetcher, maxSize, maxMemory));
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void DecodedPyramidCache::FinalizeInstance()
+  {
+    if (singleton_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      singleton_.reset(NULL);
+    }
+  }
+
+
+  DecodedPyramidCache & DecodedPyramidCache::GetInstance()
+  {
+    if (singleton_.get() == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *singleton_;
+    }
+  }
+
+
+  DecodedPyramidCache::Accessor::Accessor(DecodedPyramidCache& that,
+                                            const std::string &instanceId,
+                                            unsigned int frameNumber):
+    lock_(that.mutex_),
+    identifier_(instanceId, frameNumber),
+    pyramid_(NULL)
+  {
+    if (that.cache_.Contains(identifier_, pyramid_))
+    {
+      if (pyramid_ == NULL)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      // Tag the series as the most recently used
+      that.cache_.MakeMostRecent(identifier_);
+    }
+    else
+    {
+      // Unlock the mutex as creating the pyramid is a time-consuming operation
+      lock_.unlock();
+
+      std::unique_ptr<DecodedTiledPyramid> payload(that.fetcher_->Fetch(instanceId, frameNumber));
+
+      // Re-lock, as we now modify the cache
+      lock_.lock();
+      pyramid_ = that.Store(identifier_, payload.release());
+    }
+  }
+
+
+  DecodedTiledPyramid & DecodedPyramidCache::Accessor::GetPyramid() const
+  {
+    if (IsValid())
+    {
+      return pyramid_->GetPyramid();
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}


=====================================
Framework/Inputs/DecodedPyramidCache.h
=====================================
@@ -0,0 +1,116 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DecodedTiledPyramid.h"
+
+#include <Cache/LeastRecentlyUsedIndex.h>
+
+#include <boost/thread/mutex.hpp>
+
+
+namespace OrthancWSI
+{
+  class DecodedPyramidCache : public boost::noncopyable
+  {
+  public:
+    class IPyramidFetcher : public boost::noncopyable
+    {
+    public:
+      virtual ~IPyramidFetcher()
+      {
+      }
+
+      virtual DecodedTiledPyramid* Fetch(const std::string& instanceId,
+                                         unsigned int frameNumber) = 0;
+    };
+
+  private:
+    class CachedPyramid;
+
+    typedef std::pair<std::string, unsigned int> FrameIdentifier;  // Associates an instance ID with a frame number
+
+    typedef Orthanc::LeastRecentlyUsedIndex<FrameIdentifier, CachedPyramid*>  Cache;
+
+    std::unique_ptr<IPyramidFetcher> fetcher_;
+
+    boost::mutex  mutex_;
+    size_t        maxCount_;
+    size_t        maxMemory_;
+    size_t        memoryUsage_;
+    Cache         cache_;
+
+    bool SanityCheck();
+
+    void MakeRoom(size_t memory);
+
+    CachedPyramid* Store(FrameIdentifier identifier,
+                         DecodedTiledPyramid* pyramid);
+
+    DecodedPyramidCache(IPyramidFetcher* fetcher /* takes ownership */,
+                          size_t maxCount,
+                          size_t maxMemory);
+
+  public:
+    ~DecodedPyramidCache();
+
+    static void InitializeInstance(IPyramidFetcher* fetcher,
+                                   size_t maxSize,
+                                   size_t maxMemory);
+
+    static void FinalizeInstance();
+
+    static DecodedPyramidCache& GetInstance();
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock lock_;
+      FrameIdentifier           identifier_;
+      CachedPyramid*            pyramid_;
+
+    public:
+      Accessor(DecodedPyramidCache& that,
+               const std::string& instanceId,
+               unsigned int frameNumber);
+
+      bool IsValid() const
+      {
+        return pyramid_ != NULL;
+      }
+
+      const std::string& GetInstanceId() const
+      {
+        return identifier_.first;
+      }
+
+      unsigned int GetFrameNumber() const
+      {
+        return identifier_.second;
+      }
+
+      DecodedTiledPyramid& GetPyramid() const;
+    };
+  };
+}


=====================================
Framework/Inputs/DecodedTiledPyramid.h
=====================================
@@ -72,5 +72,7 @@ namespace OrthancWSI
     {
       return false;   // No access to the raw tiles
     }
+
+    virtual size_t GetMemoryUsage() const = 0;
   };
 }


=====================================
Framework/Inputs/OnTheFlyPyramid.cpp
=====================================
@@ -0,0 +1,151 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersWSI.h"
+#include "OnTheFlyPyramid.h"
+
+#include <OrthancException.h>
+
+#include <cassert>
+#include <Images/Image.h>
+#include <Images/ImageProcessing.h>
+
+
+namespace OrthancWSI
+{
+  void OnTheFlyPyramid::ReadRegion(Orthanc::ImageAccessor &target,
+                                   bool &isEmpty,
+                                   unsigned level,
+                                   unsigned x,
+                                   unsigned y)
+  {
+    isEmpty = false;
+
+    const Orthanc::ImageAccessor& source = GetLevel(level);
+
+    if (target.GetWidth() > tileWidth_ ||
+      target.GetHeight() > tileHeight_ ||
+      x + target.GetWidth() > source.GetWidth() ||
+      y + target.GetHeight() > source.GetHeight())
+    {
+      // This should be handled by the base class
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      Orthanc::ImageAccessor from;
+      source.GetRegion(from, x, y, target.GetWidth(), target.GetHeight());
+      Orthanc::ImageProcessing::Copy(target, from);
+    }
+  }
+
+
+  OnTheFlyPyramid::OnTheFlyPyramid(Orthanc::ImageAccessor *baseLevel,
+                                   unsigned int tileWidth,
+                                   unsigned int tileHeight,
+                                   bool smooth) :
+    tileWidth_(tileWidth),
+    tileHeight_(tileHeight)
+  {
+    if (baseLevel == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    std::unique_ptr<Orthanc::ImageAccessor> protection(baseLevel);
+
+    if (protection->GetFormat() == Orthanc::PixelFormat_RGB24)
+    {
+      baseLevel_.reset(protection.release());
+    }
+    else
+    {
+      baseLevel_.reset(new Orthanc::Image(Orthanc::PixelFormat_RGB24, protection->GetWidth(), protection->GetHeight(), false));
+      Orthanc::ImageProcessing::Convert(*baseLevel_, *protection);
+    }
+
+    Orthanc::ImageAccessor* current = baseLevel_.get();
+    while (current->GetWidth() > tileWidth_ ||
+           current->GetHeight() > tileHeight_)
+    {
+      std::unique_ptr<Orthanc::ImageAccessor> next;
+
+      if (smooth)
+      {
+        std::unique_ptr<Orthanc::ImageAccessor> smoothed(Orthanc::Image::Clone(*current));
+        Orthanc::ImageProcessing::SmoothGaussian5x5(*smoothed, false);
+        next.reset(Orthanc::ImageProcessing::Halve(*smoothed, false));
+      }
+      else
+      {
+        next.reset(Orthanc::ImageProcessing::Halve(*current, false));
+      }
+
+      higherLevels_.push_back(next.release());
+      current = higherLevels_.back();
+    }
+  }
+
+
+  OnTheFlyPyramid::~OnTheFlyPyramid()
+  {
+    for (size_t i = 0; i < higherLevels_.size(); i++)
+    {
+      assert(higherLevels_[i] != NULL);
+      delete higherLevels_[i];
+    }
+  }
+
+
+  const Orthanc::ImageAccessor & OnTheFlyPyramid::GetLevel(unsigned int level) const
+  {
+    if (level >= GetLevelCount())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else if (level == 0)
+    {
+      assert(baseLevel_.get() != NULL);
+      return *baseLevel_;
+    }
+    else
+    {
+      assert(higherLevels_[level - 1] != NULL);
+      return *higherLevels_[level - 1];
+    }
+  }
+
+
+  size_t OnTheFlyPyramid::GetMemoryUsage() const
+  {
+    size_t memory = baseLevel_->GetSize();
+
+    for (size_t i= 0; i < higherLevels_.size(); i++)
+    {
+      assert(higherLevels_[i] != NULL);
+      memory += higherLevels_[i]->GetSize();
+    }
+
+    return memory;
+  }
+}


=====================================
Framework/Inputs/OnTheFlyPyramid.h
=====================================
@@ -0,0 +1,97 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DecodedTiledPyramid.h"
+
+#include <Compatibility.h>
+
+#include <vector>
+
+
+namespace OrthancWSI
+{
+  class OnTheFlyPyramid : public DecodedTiledPyramid
+  {
+  private:
+    std::unique_ptr<Orthanc::ImageAccessor>  baseLevel_;
+    std::vector<Orthanc::ImageAccessor*>     higherLevels_;
+    unsigned int                             tileWidth_;
+    unsigned int                             tileHeight_;
+
+  protected:
+    void ReadRegion(Orthanc::ImageAccessor &target,
+                    bool &isEmpty,
+                    unsigned level,
+                    unsigned x,
+                    unsigned y) ORTHANC_OVERRIDE;
+
+  public:
+    OnTheFlyPyramid(Orthanc::ImageAccessor* baseLevel /* takes ownership */,
+                    unsigned int tileWidth,
+                    unsigned int tileHeight,
+                    bool smooth);
+
+    virtual ~OnTheFlyPyramid();
+
+    const Orthanc::ImageAccessor& GetLevel(unsigned int level) const;
+
+    unsigned GetLevelCount() const ORTHANC_OVERRIDE
+    {
+      return higherLevels_.size() + 1 /* base level */;
+    }
+
+    unsigned GetLevelWidth(unsigned int level) const ORTHANC_OVERRIDE
+    {
+      return GetLevel(level).GetWidth();
+    }
+
+    unsigned GetLevelHeight(unsigned int level) const ORTHANC_OVERRIDE
+    {
+      return GetLevel(level).GetHeight();
+    }
+
+    unsigned GetTileWidth(unsigned int level) const ORTHANC_OVERRIDE
+    {
+      return tileWidth_;
+    }
+
+    unsigned GetTileHeight(unsigned level) const ORTHANC_OVERRIDE
+    {
+      return tileHeight_;
+    }
+
+    Orthanc::PixelFormat GetPixelFormat() const ORTHANC_OVERRIDE
+    {
+      return baseLevel_->GetFormat();
+    }
+
+    Orthanc::PhotometricInterpretation GetPhotometricInterpretation() const ORTHANC_OVERRIDE
+    {
+      return Orthanc::PhotometricInterpretation_RGB;
+    }
+
+    size_t GetMemoryUsage() const ORTHANC_OVERRIDE;
+  };
+}


=====================================
Framework/Inputs/OpenSlidePyramid.cpp
=====================================
@@ -160,4 +160,17 @@ namespace OrthancWSI
       return false;
     }
   }
+
+
+  size_t OpenSlidePyramid::GetMemoryUsage() const
+  {
+    size_t countPixels = 0;
+
+    for (unsigned int i = 0; i < image_.GetLevelCount(); i++)
+    {
+      countPixels += image_.GetLevelWidth(i) * image_.GetLevelHeight(i);
+    }
+
+    return countPixels * Orthanc::GetBytesPerPixel(Orthanc::PixelFormat_RGBA32);
+  }
 }


=====================================
Framework/Inputs/OpenSlidePyramid.h
=====================================
@@ -84,5 +84,7 @@ namespace OrthancWSI
 
     bool LookupImagedVolumeSize(float& width,
                                 float& height) const;
+
+    size_t GetMemoryUsage() const ORTHANC_OVERRIDE;
   };
 }


=====================================
Framework/Inputs/SingleLevelDecodedPyramid.h
=====================================
@@ -84,5 +84,10 @@ namespace OrthancWSI
                     uint8_t backgroundRed,
                     uint8_t backgroundGreen,
                     uint8_t backgroundBlue);
+
+    size_t GetMemoryUsage() const ORTHANC_OVERRIDE
+    {
+      return image_.GetSize();
+    }
   };
 }


=====================================
NEWS
=====================================
@@ -2,6 +2,14 @@ Pending changes in the mainline
 ===============================
 
 
+Version 3.0 (2024-12-20)
+========================
+
+=> Minimum SDK version: 1.7.0 <=
+
+* On-the-fly creation of pyramids from frames of DICOM instances
+
+
 Version 2.1 (2024-10-18)
 ========================
 


=====================================
Resources/CMake/Version.cmake
=====================================
@@ -19,13 +19,13 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-set(ORTHANC_WSI_VERSION "2.1")
+set(ORTHANC_WSI_VERSION "3.0")
 
 if (ORTHANC_WSI_VERSION STREQUAL "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.4")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.5")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
 


=====================================
Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
=====================================
@@ -165,6 +165,8 @@ if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
         set(ORTHANC_FRAMEWORK_MD5 "975f5bf2142c22cb1777b4f6a0a614c5")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.4")
         set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.5")
+        set(ORTHANC_FRAMEWORK_MD5 "5bb69f092981fdcfc11dec0a0f9a7db3")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc


=====================================
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
=====================================
@@ -334,9 +334,9 @@ namespace OrthancPlugins
     std::vector<const char*> headersValues_;
 
   public:
-    explicit PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders)
+    explicit PluginHttpHeaders(const HttpHeaders& httpHeaders)
     {
-      for (std::map<std::string, std::string>::const_iterator
+      for (HttpHeaders::const_iterator
              it = httpHeaders.begin(); it != httpHeaders.end(); ++it)
       {
         headersKeys_.push_back(it->first.c_str());
@@ -361,7 +361,7 @@ namespace OrthancPlugins
   };
 
   bool MemoryBuffer::RestApiGet(const std::string& uri,
-                                const std::map<std::string, std::string>& httpHeaders,
+                                const HttpHeaders& httpHeaders,
                                 bool applyPlugins)
   {
     Clear();
@@ -400,7 +400,7 @@ namespace OrthancPlugins
   bool MemoryBuffer::RestApiPost(const std::string& uri,
                                  const void* body,
                                  size_t bodySize,
-                                 const std::map<std::string, std::string>& httpHeaders,
+                                 const HttpHeaders& httpHeaders,
                                  bool applyPlugins)
   {
     MemoryBuffer answerHeaders;
@@ -422,7 +422,7 @@ namespace OrthancPlugins
 
   bool MemoryBuffer::RestApiPost(const std::string& uri,
                                  const Json::Value& body,
-                                 const std::map<std::string, std::string>& httpHeaders,
+                                 const HttpHeaders& httpHeaders,
                                  bool applyPlugins)
   {
     std::string s;
@@ -1490,7 +1490,7 @@ namespace OrthancPlugins
 
   bool RestApiGetString(std::string& result,
                         const std::string& uri,
-                        const std::map<std::string, std::string>& httpHeaders,
+                        const HttpHeaders& httpHeaders,
                         bool applyPlugins)
   {
     MemoryBuffer answer;
@@ -1508,7 +1508,7 @@ namespace OrthancPlugins
 
   bool RestApiGet(Json::Value& result,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& httpHeaders,
+                  const HttpHeaders& httpHeaders,
                   bool applyPlugins)
   {
     MemoryBuffer answer;
@@ -1598,7 +1598,7 @@ namespace OrthancPlugins
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
-                   const std::map<std::string, std::string>& httpHeaders,
+                   const HttpHeaders& httpHeaders,
                    bool applyPlugins)
   {
     MemoryBuffer answer;
@@ -1963,7 +1963,7 @@ namespace OrthancPlugins
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            size_t index,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -1994,7 +1994,7 @@ namespace OrthancPlugins
   bool OrthancPeers::DoGet(MemoryBuffer& target,
                            const std::string& name,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2005,7 +2005,7 @@ namespace OrthancPlugins
   bool OrthancPeers::DoGet(Json::Value& target,
                            size_t index,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2024,7 +2024,7 @@ namespace OrthancPlugins
   bool OrthancPeers::DoGet(Json::Value& target,
                            const std::string& name,
                            const std::string& uri,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2044,7 +2044,7 @@ namespace OrthancPlugins
                             const std::string& name,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2056,7 +2056,7 @@ namespace OrthancPlugins
                             size_t index,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2076,7 +2076,7 @@ namespace OrthancPlugins
                             const std::string& name,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     MemoryBuffer buffer;
 
@@ -2096,7 +2096,7 @@ namespace OrthancPlugins
                             size_t index,
                             const std::string& uri,
                             const std::string& body,
-                            const std::map<std::string, std::string>& headers) const
+                            const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -2133,7 +2133,7 @@ namespace OrthancPlugins
   bool OrthancPeers::DoPut(size_t index,
                            const std::string& uri,
                            const std::string& body,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -2169,7 +2169,7 @@ namespace OrthancPlugins
   bool OrthancPeers::DoPut(const std::string& name,
                            const std::string& uri,
                            const std::string& body,
-                           const std::map<std::string, std::string>& headers) const
+                           const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2179,7 +2179,7 @@ namespace OrthancPlugins
 
   bool OrthancPeers::DoDelete(size_t index,
                               const std::string& uri,
-                              const std::map<std::string, std::string>& headers) const
+                              const HttpHeaders& headers) const
   {
     if (index >= index_.size())
     {
@@ -2208,7 +2208,7 @@ namespace OrthancPlugins
 
   bool OrthancPeers::DoDelete(const std::string& name,
                               const std::string& uri,
-                              const std::map<std::string, std::string>& headers) const
+                              const HttpHeaders& headers) const
   {
     size_t index;
     return (LookupName(index, name) &&
@@ -2923,12 +2923,12 @@ namespace OrthancPlugins
       std::vector<const char*>  headersValues_;
 
     public:
-      HeadersWrapper(const HttpClient::HttpHeaders& headers)
+      HeadersWrapper(const HttpHeaders& headers)
       {
         headersKeys_.reserve(headers.size());
         headersValues_.reserve(headers.size());
 
-        for (HttpClient::HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
+        for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
         {
           headersKeys_.push_back(it->first.c_str());
           headersValues_.push_back(it->second.c_str());
@@ -3076,11 +3076,11 @@ namespace OrthancPlugins
     class MemoryAnswer : public HttpClient::IAnswer
     {
     private:
-      HttpClient::HttpHeaders  headers_;
-      ChunkedBuffer            body_;
+      HttpHeaders    headers_;
+      ChunkedBuffer  body_;
 
     public:
-      const HttpClient::HttpHeaders& GetHeaders() const
+      const HttpHeaders& GetHeaders() const
       {
         return headers_;
       }
@@ -3168,6 +3168,35 @@ namespace OrthancPlugins
 #endif    
 
 
+  static void DecodeHttpHeaders(HttpHeaders& target,
+                                const MemoryBuffer& source)
+  {
+    Json::Value v;
+    source.ToJson(v);
+
+    if (v.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    Json::Value::Members members = v.getMemberNames();
+    target.clear();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& h = v[members[i]];
+      if (h.type() != Json::stringValue)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+      }
+      else
+      {
+        target[members[i]] = h.asString();
+      }
+    }
+  }
+
+
   void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus,
                                         HttpHeaders& answerHeaders,
                                         std::string& answerBody,
@@ -3208,30 +3237,7 @@ namespace OrthancPlugins
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
     }
 
-    Json::Value v;
-    answerHeadersBuffer.ToJson(v);
-
-    if (v.type() != Json::objectValue)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-
-    Json::Value::Members members = v.getMemberNames();
-    answerHeaders.clear();
-
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const Json::Value& h = v[members[i]];
-      if (h.type() != Json::stringValue)
-      {
-        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-      }
-      else
-      {
-        answerHeaders[members[i]] = h.asString();
-      }
-    }
-
+    DecodeHttpHeaders(answerHeaders, answerHeadersBuffer);
     answerBodyBuffer.ToString(answerBody);
   }
 
@@ -4061,7 +4067,7 @@ namespace OrthancPlugins
   }
 #endif
 
-  void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request)
+  void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request)
   {
     result.clear();
 
@@ -4114,4 +4120,135 @@ namespace OrthancPlugins
     SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_OrthancExplorer, javascript);
 #endif
   }
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  RestApiClient::RestApiClient() :
+    method_(OrthancPluginHttpMethod_Get),
+    path_("/"),
+    afterPlugins_(false),
+    httpStatus_(0)
+  {
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  void RestApiClient::AddRequestHeader(const std::string& key,
+                                       const std::string& value)
+  {
+    if (requestHeaders_.find(key) == requestHeaders_.end())
+    {
+      requestHeaders_[key] = value;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  bool RestApiClient::Execute()
+  {
+    if (requestBody_.size() > 0xffffffffu)
+    {
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    PluginHttpHeaders converted(requestHeaders_);
+
+    MemoryBuffer body;
+    MemoryBuffer headers;
+
+    OrthancPluginErrorCode code = OrthancPluginCallRestApi(GetGlobalContext(), *body, *headers, &httpStatus_, method_, path_.c_str(),
+                                                           requestHeaders_.size(), converted.GetKeys(), converted.GetValues(),
+                                                           requestBody_.c_str(), requestBody_.size(), afterPlugins_ ? 1 : 0);
+
+    answerHeaders_.clear();
+    answerBody_.clear();
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      if (httpStatus_ == 0)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+      }
+
+      DecodeHttpHeaders(answerHeaders_, headers);
+      body.ToString(answerBody_);
+      return true;
+    }
+    else
+    {
+      if (code == OrthancPluginErrorCode_UnknownResource ||
+          code == OrthancPluginErrorCode_InexistentItem)
+      {
+        httpStatus_ = 404;
+        return false;
+      }
+      else
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      }
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  uint16_t RestApiClient::GetHttpStatus() const
+  {
+    if (httpStatus_ == 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+    else
+    {
+      return httpStatus_;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  bool RestApiClient::LookupAnswerHeader(std::string& value,
+                                         const std::string& key) const
+  {
+    if (httpStatus_ == 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+    else
+    {
+      HttpHeaders::const_iterator found = answerHeaders_.find(key);
+      if (found == answerHeaders_.end())
+      {
+        return false;
+      }
+      else
+      {
+        value = found->second;
+        return true;
+      }
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  const std::string& RestApiClient::GetAnswerBody() const
+  {
+    if (httpStatus_ == 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+    else
+    {
+      return answerBody_;
+    }
+  }
+#endif
 }


=====================================
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
=====================================
@@ -170,6 +170,8 @@
 
 namespace OrthancPlugins
 {
+  typedef std::map<std::string, std::string>  HttpHeaders;
+
   typedef void (*RestCallback) (OrthancPluginRestOutput* output,
                                 const char* url,
                                 const OrthancPluginHttpRequest* request);
@@ -257,7 +259,7 @@ namespace OrthancPlugins
                     bool applyPlugins);
 
     bool RestApiGet(const std::string& uri,
-                    const std::map<std::string, std::string>& httpHeaders,
+                    const HttpHeaders& httpHeaders,
                     bool applyPlugins);
 
     bool RestApiPost(const std::string& uri,
@@ -277,13 +279,13 @@ namespace OrthancPlugins
 #if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
     bool RestApiPost(const std::string& uri,
                      const Json::Value& body,
-                     const std::map<std::string, std::string>& httpHeaders,
+                     const HttpHeaders& httpHeaders,
                      bool applyPlugins);
 
     bool RestApiPost(const std::string& uri,
                      const void* body,
                      size_t bodySize,
-                     const std::map<std::string, std::string>& httpHeaders,
+                     const HttpHeaders& httpHeaders,
                      bool applyPlugins);
 #endif
 
@@ -581,7 +583,7 @@ namespace OrthancPlugins
 
   bool RestApiGet(Json::Value& result,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& httpHeaders,
+                  const HttpHeaders& httpHeaders,
                   bool applyPlugins);
 
   bool RestApiGetString(std::string& result,
@@ -590,7 +592,7 @@ namespace OrthancPlugins
 
   bool RestApiGetString(std::string& result,
                         const std::string& uri,
-                        const std::map<std::string, std::string>& httpHeaders,
+                        const HttpHeaders& httpHeaders,
                         bool applyPlugins);
 
   bool RestApiPost(std::string& result,
@@ -609,7 +611,7 @@ namespace OrthancPlugins
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
-                   const std::map<std::string, std::string>& httpHeaders,
+                   const HttpHeaders& httpHeaders,
                    bool applyPlugins);
 #endif
 
@@ -829,64 +831,64 @@ namespace OrthancPlugins
     bool DoGet(MemoryBuffer& target,
                size_t index,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoGet(MemoryBuffer& target,
                const std::string& name,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoGet(Json::Value& target,
                size_t index,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoGet(Json::Value& target,
                const std::string& name,
                const std::string& uri,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 size_t index,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPost(MemoryBuffer& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPost(Json::Value& target,
                 size_t index,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
                 const std::string& body,
-                const std::map<std::string, std::string>& headers) const;
+                const HttpHeaders& headers) const;
 
     bool DoPut(size_t index,
                const std::string& uri,
                const std::string& body,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoPut(const std::string& name,
                const std::string& uri,
                const std::string& body,
-               const std::map<std::string, std::string>& headers) const;
+               const HttpHeaders& headers) const;
 
     bool DoDelete(size_t index,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& headers) const;
+                  const HttpHeaders& headers) const;
 
     bool DoDelete(const std::string& name,
                   const std::string& uri,
-                  const std::map<std::string, std::string>& headers) const;
+                  const HttpHeaders& headers) const;
   };
 #endif
 
@@ -996,8 +998,6 @@ namespace OrthancPlugins
   class HttpClient : public boost::noncopyable
   {
   public:
-    typedef std::map<std::string, std::string>  HttpHeaders;
-
     class IRequestBody : public boost::noncopyable
     {
     public:
@@ -1397,7 +1397,7 @@ namespace OrthancPlugins
   };
 
 // helper method to convert Http headers from the plugin SDK to a std::map
-void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request);
+void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request);
 
 #if HAS_ORTHANC_PLUGIN_WEBDAV == 1
   class IWebDavCollection : public boost::noncopyable
@@ -1508,4 +1508,88 @@ void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPlu
 
   void ExtendOrthancExplorer(const std::string& pluginIdentifier,
                              const std::string& javascript);
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+  class RestApiClient : public boost::noncopyable
+  {
+  private:
+    // Request
+    OrthancPluginHttpMethod  method_;
+    std::string              path_;
+    HttpHeaders              requestHeaders_;
+    std::string              requestBody_;
+    bool                     afterPlugins_;
+
+    // Answer
+    uint16_t                 httpStatus_;
+    HttpHeaders              answerHeaders_;
+    std::string              answerBody_;
+
+  public:
+    RestApiClient();
+
+    void SetMethod(OrthancPluginHttpMethod method)
+    {
+      method_ = method;
+    }
+
+    OrthancPluginHttpMethod GetMethod() const
+    {
+      return method_;
+    }
+
+    void SetPath(const std::string& path)
+    {
+      path_ = path;
+    }
+
+    const std::string& GetPath() const
+    {
+      return path_;
+    }
+
+    void AddRequestHeader(const std::string& key,
+                          const std::string& value);
+
+    const HttpHeaders& GetRequestHeaders() const
+    {
+      return requestHeaders_;
+    }
+
+    void SetRequestBody(const std::string& body)
+    {
+      requestBody_ = body;
+    }
+
+    void SwapRequestBody(std::string& body)
+    {
+      requestBody_.swap(body);
+    }
+
+    void SetAfterPlugins(bool afterPlugins)
+    {
+      afterPlugins_ = afterPlugins;
+    }
+
+    bool IsAfterPlugins() const
+    {
+      return afterPlugins_;
+    }
+
+    const std::string& GetRequestBody() const
+    {
+      return requestBody_;
+    }
+
+    bool Execute();
+
+    uint16_t GetHttpStatus() const;
+
+    bool LookupAnswerHeader(std::string& value,
+                            const std::string& key) const;
+
+    const std::string& GetAnswerBody() const;
+  };
+#endif
 }


=====================================
Resources/SyncOrthancFolder.py
=====================================
@@ -11,7 +11,7 @@ import stat
 import urllib.request
 
 TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc')
-PLUGIN_SDK_VERSION = '1.0.0'
+PLUGIN_SDK_VERSION = '1.7.0'
 REPOSITORY = 'https://orthanc.uclouvain.be/hg/%s/raw-file'
 
 FILES = [


=====================================
ViewerPlugin/CMakeLists.txt
=====================================
@@ -55,7 +55,7 @@ include(${CMAKE_SOURCE_DIR}/../Resources/Orthanc/CMake/DownloadOrthancFramework.
 if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
   if (ORTHANC_FRAMEWORK_USE_SHARED)
     include(FindBoost)
-    find_package(Boost COMPONENTS system)
+    find_package(Boost COMPONENTS system thread)
     
     if (NOT Boost_FOUND)
       message(FATAL_ERROR "Unable to locate Boost on this system")
@@ -93,7 +93,7 @@ include(${ORTHANC_WSI_DIR}/Resources/CMake/OpenJpegConfiguration.cmake)
 #####################################################################
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
-  include_directories(${CMAKE_SOURCE_DIR}/../Resources/Orthanc/Sdk-1.0.0)
+  include_directories(${CMAKE_SOURCE_DIR}/../Resources/Orthanc/Sdk-1.7.0)
 else ()
   CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H)
   if (NOT HAVE_ORTHANC_H)
@@ -188,6 +188,7 @@ set(ORTHANC_WSI_SOURCES
   DicomPyramidCache.cpp
   IIIF.cpp
   OrthancPluginConnection.cpp
+  OrthancPyramidFrameFetcher.cpp
   Plugin.cpp
   RawTile.cpp
 
@@ -195,9 +196,12 @@ set(ORTHANC_WSI_SOURCES
   ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp
   ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp
   ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedPyramidCache.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedTiledPyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidInstance.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DicomPyramidLevel.cpp
+  ${ORTHANC_WSI_DIR}/Framework/Inputs/OnTheFlyPyramid.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/PyramidWithRawTiles.cpp
   ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Reader.cpp
   ${ORTHANC_WSI_DIR}/Framework/Jpeg2000Writer.cpp


=====================================
ViewerPlugin/IIIF.cpp
=====================================
@@ -35,6 +35,8 @@
 
 #include <boost/math/special_functions/round.hpp>
 
+#include "../Framework/Inputs/DecodedPyramidCache.h"
+
 
 static const char* const ROWS = "0028,0010";
 static const char* const COLUMNS = "0028,0011";
@@ -44,17 +46,10 @@ static std::string  iiifPublicUrl_;
 static bool         iiifForcePowersOfTwoScaleFactors_ = false;
 
 
-void ServeIIIFTiledImageInfo(OrthancPluginRestOutput* output,
-                             const char* url,
-                             const OrthancPluginHttpRequest* request)
+static void GeneratePyramidInfo(Json::Value& result,
+                                const OrthancWSI::ITiledPyramid& pyramid,
+                                const std::string& logName)
 {
-  const std::string seriesId(request->groups[0]);
-
-  LOG(INFO) << "IIIF: Image API call to whole-slide pyramid of series " << seriesId;
-
-  OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
-  const OrthancWSI::ITiledPyramid& pyramid = locker.GetPyramid();
-
   if (pyramid.GetLevelCount() == 0)
   {
     throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
@@ -113,13 +108,13 @@ void ServeIIIFTiledImageInfo(OrthancPluginRestOutput* output,
       }
       else
       {
-        LOG(WARNING) << "IIIF - Dropping level " << i << " of series " << seriesId
+        LOG(WARNING) << "IIIF - Dropping level " << i << " of " << logName
                      << ", as it doesn't follow the powers-of-two pattern";
       }
     }
     else
     {
-      LOG(WARNING) << "IIIF - Dropping level " << i << " of series " << seriesId
+      LOG(WARNING) << "IIIF - Dropping level " << i << " of " << logName
                    << ", as the full width/height ("
                    << pyramid.GetLevelWidth(0) << "x" << pyramid.GetLevelHeight(0)
                    << ") of the image is not an integer multiple of the level width/height ("
@@ -144,173 +139,218 @@ void ServeIIIFTiledImageInfo(OrthancPluginRestOutput* output,
   tiles["height"] = pyramid.GetTileHeight(0);
   tiles["scaleFactors"] = scaleFactors;
 
-  Json::Value result;
+  result = Json::objectValue;
   result["@context"] = "http://iiif.io/api/image/3/context.json";
   result["profile"] = "level0";
   result["protocol"] = "http://iiif.io/api/image";
   result["type"] = "ImageService3";
 
-  result["id"] = iiifPublicUrl_ + "tiles/" + seriesId;
   result["width"] = pyramid.GetLevelWidth(0);
   result["height"] = pyramid.GetLevelHeight(0);
   result["sizes"] = reversedSizes;
   result["tiles"].append(tiles);
-
-  std::string s = result.toStyledString();
-  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Json));
-}
-
-
-static unsigned int GetPhysicalTileWidth(const OrthancWSI::ITiledPyramid& pyramid,
-                                         unsigned int level)
-{
-  return static_cast<unsigned int>(boost::math::iround(
-                                     static_cast<float>(pyramid.GetTileWidth(level)) *
-                                     static_cast<float>(pyramid.GetLevelWidth(0)) /
-                                     static_cast<float>(pyramid.GetLevelWidth(level))));
 }
 
 
-static unsigned int GetPhysicalTileHeight(const OrthancWSI::ITiledPyramid& pyramid,
-                                          unsigned int level)
-{
-  return static_cast<unsigned int>(boost::math::iround(
-                                     static_cast<float>(pyramid.GetTileHeight(level)) *
-                                     static_cast<float>(pyramid.GetLevelHeight(0)) /
-                                     static_cast<float>(pyramid.GetLevelHeight(level))));
-}
-
-
-void ServeIIIFTiledImageTile(OrthancPluginRestOutput* output,
-                             const char* url,
-                             const OrthancPluginHttpRequest* request)
+void ServeIIIFSeriesPyramidInfo(OrthancPluginRestOutput* output,
+                                const char* url,
+                                const OrthancPluginHttpRequest* request)
 {
   const std::string seriesId(request->groups[0]);
-  const std::string region(request->groups[1]);
-  const std::string size(request->groups[2]);
-  const std::string rotation(request->groups[3]);
-  const std::string quality(request->groups[4]);
-  const std::string format(request->groups[5]);
 
-  LOG(INFO) << "IIIF: Image API call to tile of series " << seriesId << ": "
-            << "region=" << region << "; size=" << size << "; rotation="
-            << rotation << "; quality=" << quality << "; format=" << format;
+  LOG(INFO) << "IIIF: Image API call to whole-slide pyramid of series " << seriesId;
 
-  if (rotation != "0")
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported rotation: " + rotation);
-  }
+  Json::Value result;
 
-  if (quality != "default")
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported quality: " + quality);
+    OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
+    GeneratePyramidInfo(result, locker.GetPyramid(), "series " + seriesId);
   }
 
-  if (format != "jpg")
-  {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported format: " + format);
-  }
+  result["id"] = iiifPublicUrl_ + "tiles/" + seriesId;
 
-  if (region == "full")
-  {
-    OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
+  std::string s = result.toStyledString();
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Json));
+}
 
-    OrthancWSI::ITiledPyramid& pyramid = locker.GetPyramid();
-    const unsigned int level = pyramid.GetLevelCount() - 1;
 
-    Orthanc::Image full(Orthanc::PixelFormat_RGB24, pyramid.GetLevelWidth(level), pyramid.GetLevelHeight(level), false);
-    Orthanc::ImageProcessing::Set(full, 255, 255, 255, 0);
+namespace
+{
+  class RegionParameters
+  {
+  private:
+    bool        isFull_;
+    uint32_t    x_;
+    uint32_t    y_;
+    uint32_t    regionWidth_;
+    uint32_t    regionHeight_;
+    uint32_t    cropWidth_;
+    uint32_t    cropHeight_;
+
+    void CheckNotFull() const
+    {
+      if (isFull_)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+    }
 
-    const unsigned int nx = OrthancWSI::CeilingDivision(pyramid.GetLevelWidth(level), pyramid.GetTileWidth(level));
-    const unsigned int ny = OrthancWSI::CeilingDivision(pyramid.GetLevelHeight(level), pyramid.GetTileHeight(level));
-    for (unsigned int ty = 0; ty < ny; ty++)
+  public:
+    RegionParameters(const std::string& region,
+                     const std::string& size,
+                     const std::string& rotation,
+                     const std::string& quality,
+                     const std::string& format) :
+      isFull_(true),
+      x_(0),
+      y_(0),
+      regionWidth_(0),
+      regionHeight_(0),
+      cropWidth_(0),
+      cropHeight_(0)
     {
-      const unsigned int y = ty * pyramid.GetTileHeight(level);
-      const unsigned int height = std::min(pyramid.GetTileHeight(level), full.GetHeight() - y);
+      if (rotation != "0")
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported rotation: " + rotation);
+      }
+
+      if (quality != "default")
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported quality: " + quality);
+      }
+
+      if (format != "jpg")
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Unsupported format: " + format);
+      }
 
-      for (unsigned int tx = 0; tx < nx; tx++)
+      if (region == "full")
+      {
+        isFull_ = true;
+      }
+      else
       {
-        const unsigned int x = tx * pyramid.GetTileWidth(level);
+        isFull_ = false;
 
-        bool isEmpty;  // Unused
-        std::unique_ptr<Orthanc::ImageAccessor> tile(pyramid.DecodeTile(isEmpty, level, tx, ty));
+        std::vector<std::string> tokens;
+        Orthanc::Toolbox::TokenizeString(tokens, region, ',');
 
-        const unsigned int width = std::min(pyramid.GetTileWidth(level), full.GetWidth() - x);
+        if (tokens.size() != 4 ||
+            !Orthanc::SerializationToolbox::ParseUnsignedInteger32(x_, tokens[0]) ||
+            !Orthanc::SerializationToolbox::ParseUnsignedInteger32(y_, tokens[1]) ||
+            !Orthanc::SerializationToolbox::ParseUnsignedInteger32(regionWidth_, tokens[2]) ||
+            !Orthanc::SerializationToolbox::ParseUnsignedInteger32(regionHeight_, tokens[3]))
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Invalid (x,y,width,height) region, found: " + region);
+        }
 
-        Orthanc::ImageAccessor source, target;
-        tile->GetRegion(source, 0, 0, width, height);
-        full.GetRegion(target, x, y, width, height);
+        Orthanc::Toolbox::TokenizeString(tokens, size, ',');
 
-        Orthanc::ImageProcessing::Copy(target, source);
+        bool ok = false;
+        if (tokens.size() == 2 &&
+            Orthanc::SerializationToolbox::ParseUnsignedInteger32(cropWidth_, tokens[0]))
+        {
+          if (tokens[1].empty())
+          {
+            cropHeight_ = cropWidth_;
+            ok = true;
+          }
+          else if (Orthanc::SerializationToolbox::ParseUnsignedInteger32(cropHeight_, tokens[1]))
+          {
+            ok = true;
+          }
+        }
+
+        if (!ok)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Invalid (width,height) crop, found: " + size);
+        }
       }
     }
 
-    std::string encoded;
-    OrthancWSI::RawTile::Encode(encoded, full, Orthanc::MimeType_Jpeg);
-
-    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(),
-                              encoded.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg));
-  }
-  else
-  {
-    std::vector<std::string> tokens;
-    Orthanc::Toolbox::TokenizeString(tokens, region, ',');
+    bool IsFull() const
+    {
+      return isFull_;
+    }
 
-    uint32_t regionX, regionY, regionWidth, regionHeight;
+    uint32_t GetX() const
+    {
+      CheckNotFull();
+      return x_;
+    }
 
-    if (tokens.size() != 4 ||
-        !Orthanc::SerializationToolbox::ParseUnsignedInteger32(regionX, tokens[0]) ||
-        !Orthanc::SerializationToolbox::ParseUnsignedInteger32(regionY, tokens[1]) ||
-        !Orthanc::SerializationToolbox::ParseUnsignedInteger32(regionWidth, tokens[2]) ||
-        !Orthanc::SerializationToolbox::ParseUnsignedInteger32(regionHeight, tokens[3]))
+    uint32_t GetY() const
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Invalid (x,y,width,height) region, found: " + region);
+      CheckNotFull();
+      return y_;
     }
 
-    uint32_t cropWidth, cropHeight;
+    uint32_t GetRegionWidth() const
+    {
+      CheckNotFull();
+      return regionWidth_;
+    }
 
-    Orthanc::Toolbox::TokenizeString(tokens, size, ',');
+    uint32_t GetRegionHeight() const
+    {
+      CheckNotFull();
+      return regionHeight_;
+    }
 
-    bool ok = false;
-    if (tokens.size() == 2 &&
-        Orthanc::SerializationToolbox::ParseUnsignedInteger32(cropWidth, tokens[0]))
+    uint32_t GetCropWidth() const
     {
-      if (tokens[1].empty())
-      {
-        cropHeight = cropWidth;
-        ok = true;
-      }
-      else if (Orthanc::SerializationToolbox::ParseUnsignedInteger32(cropHeight, tokens[1]))
-      {
-        ok = true;
-      }
+      CheckNotFull();
+      return cropWidth_;
     }
 
-    if (!ok)
+    uint32_t GetCropHeight() const
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "IIIF - Invalid (width,height) crop, found: " + size);
+      CheckNotFull();
+      return cropHeight_;
     }
+  };
+
 
-    std::unique_ptr<OrthancWSI::RawTile> rawTile;
-    std::unique_ptr<Orthanc::ImageAccessor> toCrop;
+  class RegionRenderer : public boost::noncopyable
+  {
+  private:
+    RegionParameters                         parameters_;
+    std::unique_ptr<OrthancWSI::RawTile>     rawTile_;
+    std::unique_ptr<Orthanc::ImageAccessor>  toCrop_;
 
+    static unsigned int GetPhysicalTileWidth(const OrthancWSI::ITiledPyramid& pyramid,
+                                             unsigned int level)
     {
-      OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
+      return static_cast<unsigned int>(boost::math::iround(static_cast<float>(pyramid.GetTileWidth(level)) *
+                                                           static_cast<float>(pyramid.GetLevelWidth(0)) /
+                                                           static_cast<float>(pyramid.GetLevelWidth(level))));
+    }
 
-      OrthancWSI::ITiledPyramid& pyramid = locker.GetPyramid();
+    static unsigned int GetPhysicalTileHeight(const OrthancWSI::ITiledPyramid& pyramid,
+                                              unsigned int level)
+    {
+      return static_cast<unsigned int>(boost::math::iround(static_cast<float>(pyramid.GetTileHeight(level)) *
+                                                           static_cast<float>(pyramid.GetLevelHeight(0)) /
+                                                           static_cast<float>(pyramid.GetLevelHeight(level))));
+    }
 
+  public:
+    RegionRenderer(const RegionParameters& parameters,
+                   OrthancWSI::ITiledPyramid& pyramid) :
+      parameters_(parameters)
+    {
       unsigned int level;
       for (level = 0; level < pyramid.GetLevelCount(); level++)
       {
         const unsigned int physicalTileWidth = GetPhysicalTileWidth(pyramid, level);
         const unsigned int physicalTileHeight = GetPhysicalTileHeight(pyramid, level);
 
-        if (regionX % physicalTileWidth == 0 &&
-            regionY % physicalTileHeight == 0 &&
-            static_cast<unsigned int>(regionWidth) <= physicalTileWidth &&
-            static_cast<unsigned int>(regionHeight) <= physicalTileHeight &&
-            static_cast<unsigned int>(regionX + regionWidth) <= pyramid.GetLevelWidth(0) &&
-            static_cast<unsigned int>(regionY + regionHeight) <= pyramid.GetLevelHeight(0))
+        if (parameters.GetX() % physicalTileWidth == 0 &&
+            parameters.GetY() % physicalTileHeight == 0 &&
+            parameters.GetRegionWidth() <= physicalTileWidth &&
+            parameters.GetRegionHeight() <= physicalTileHeight &&
+            parameters.GetX() + parameters.GetRegionWidth() <= pyramid.GetLevelWidth(0) &&
+            parameters.GetY() + parameters.GetRegionHeight() <= pyramid.GetLevelHeight(0))
         {
           break;
         }
@@ -320,87 +360,187 @@ void ServeIIIFTiledImageTile(OrthancPluginRestOutput* output,
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "IIIF - Cannot locate the level of interest");
       }
-      else if (static_cast<unsigned int>(cropWidth) > pyramid.GetTileWidth(level))
+      else if (parameters.GetCropWidth() > pyramid.GetTileWidth(level))
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "IIIF - Request for a cropping that is too large for the tile size");
       }
       else
       {
-        rawTile.reset(new OrthancWSI::RawTile(locker.GetPyramid(), level,
-                                              regionX / GetPhysicalTileWidth(pyramid, level),
-                                              regionY / GetPhysicalTileHeight(pyramid, level)));
+        const unsigned int tileX = parameters.GetX() / GetPhysicalTileWidth(pyramid, level);
+        const unsigned int tileY = parameters.GetY() / GetPhysicalTileHeight(pyramid, level);
+        rawTile_.reset(new OrthancWSI::RawTile(pyramid, level, tileX, tileY));
 
-        assert(rawTile->GetTileWidth() == pyramid.GetTileWidth(level));
-        assert(rawTile->GetTileHeight() == pyramid.GetTileHeight(level));
-
-        if (!rawTile->IsEmpty() &&
-            (static_cast<unsigned int>(cropWidth) < pyramid.GetTileWidth(level) ||
-             static_cast<unsigned int>(cropHeight) < pyramid.GetTileHeight(level)))
+        if (rawTile_->IsEmpty())
+        {
+          bool isEmpty;
+          toCrop_.reset(pyramid.DecodeTile(isEmpty, level, tileX, tileY));
+          if (isEmpty)
+          {
+            toCrop_.reset(NULL);
+          }
+          else
+          {
+            rawTile_.reset(NULL);
+          }
+        }
+        else
         {
-          toCrop.reset(rawTile->Decode());
-          rawTile.reset(NULL);
+          assert(rawTile_->GetTileWidth() == pyramid.GetTileWidth(level));
+          assert(rawTile_->GetTileHeight() == pyramid.GetTileHeight(level));
+
+          if (!rawTile_->IsEmpty() &&
+              (parameters.GetCropWidth() < pyramid.GetTileWidth(level) ||
+               parameters.GetCropHeight() < pyramid.GetTileHeight(level)))
+          {
+            toCrop_.reset(rawTile_->Decode());
+            rawTile_.reset(NULL);
+          }
         }
       }
     }
 
-    if (rawTile.get() != NULL)
+    void Answer(OrthancPluginRestOutput* output)
     {
-      assert(toCrop.get() == NULL);
-
-      if (rawTile->IsEmpty())
+      if (rawTile_.get() != NULL)
       {
-        if (static_cast<unsigned int>(cropWidth) < rawTile->GetTileWidth() ||
-            static_cast<unsigned int>(cropHeight) < rawTile->GetTileHeight())
+        assert(toCrop_.get() == NULL);
+
+        if (rawTile_->IsEmpty())
         {
-          OrthancWSI::RawTile::AnswerBackgroundTile(output, static_cast<unsigned int>(cropWidth), static_cast<unsigned int>(cropHeight));
+          if (parameters_.GetCropWidth() < rawTile_->GetTileWidth() ||
+              parameters_.GetCropHeight() < rawTile_->GetTileHeight())
+          {
+            OrthancWSI::RawTile::AnswerBackgroundTile(output, parameters_.GetCropWidth(), parameters_.GetCropHeight());
+          }
+          else
+          {
+            OrthancWSI::RawTile::AnswerBackgroundTile(output, rawTile_->GetTileWidth(), rawTile_->GetTileHeight());
+          }
         }
         else
         {
-          OrthancWSI::RawTile::AnswerBackgroundTile(output, rawTile->GetTileWidth(), rawTile->GetTileHeight());
+          // Level 0 Compliance of IIIF expects JPEG files
+          rawTile_->Answer(output, Orthanc::MimeType_Jpeg);
+        }
+      }
+      else if (toCrop_.get() != NULL)
+      {
+        assert(rawTile_.get() == NULL);
+
+        if (parameters_.GetCropWidth() > toCrop_->GetWidth() ||
+            parameters_.GetCropHeight() > toCrop_->GetHeight())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "IIIF - Asking to crop outside of the tile size");
         }
+
+        Orthanc::ImageAccessor cropped;
+        toCrop_->GetRegion(cropped, 0, 0, parameters_.GetCropWidth(), parameters_.GetCropHeight());
+
+        std::string encoded;
+        OrthancWSI::RawTile::Encode(encoded, cropped, Orthanc::MimeType_Jpeg);
+
+        OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(),
+                                  encoded.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg));
       }
       else
       {
-        // Level 0 Compliance of IIIF expects JPEG files
-        rawTile->Answer(output, Orthanc::MimeType_Jpeg);
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
     }
-    else if (toCrop.get() != NULL)
+  };
+}
+
+
+static Orthanc::ImageAccessor* RenderFullImage(OrthancWSI::ITiledPyramid& pyramid)
+{
+  const unsigned int level = pyramid.GetLevelCount() - 1;
+
+  std::unique_ptr<Orthanc::ImageAccessor> full(new Orthanc::Image(Orthanc::PixelFormat_RGB24, pyramid.GetLevelWidth(level), pyramid.GetLevelHeight(level), false));
+  Orthanc::ImageProcessing::Set(*full, 255, 255, 255, 0);
+
+  const unsigned int nx = OrthancWSI::CeilingDivision(pyramid.GetLevelWidth(level), pyramid.GetTileWidth(level));
+  const unsigned int ny = OrthancWSI::CeilingDivision(pyramid.GetLevelHeight(level), pyramid.GetTileHeight(level));
+  for (unsigned int ty = 0; ty < ny; ty++)
+  {
+    const unsigned int y = ty * pyramid.GetTileHeight(level);
+    const unsigned int height = std::min(pyramid.GetTileHeight(level), full->GetHeight() - y);
+
+    for (unsigned int tx = 0; tx < nx; tx++)
     {
-      assert(rawTile.get() == NULL);
+      const unsigned int x = tx * pyramid.GetTileWidth(level);
 
-      if (static_cast<unsigned int>(cropWidth) > toCrop->GetWidth() ||
-          static_cast<unsigned int>(cropHeight) > toCrop->GetHeight())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "IIIF - Asking to crop outside of the tile size");
-      }
+      bool isEmpty;  // Unused
+      std::unique_ptr<Orthanc::ImageAccessor> tile(pyramid.DecodeTile(isEmpty, level, tx, ty));
 
-      Orthanc::ImageAccessor cropped;
-      toCrop->GetRegion(cropped, 0, 0, cropWidth, cropHeight);
+      const unsigned int width = std::min(pyramid.GetTileWidth(level), full->GetWidth() - x);
 
-      std::string encoded;
-      OrthancWSI::RawTile::Encode(encoded, cropped, Orthanc::MimeType_Jpeg);
+      Orthanc::ImageAccessor source, target;
+      tile->GetRegion(source, 0, 0, width, height);
+      full->GetRegion(target, x, y, width, height);
 
-      OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(),
-                                encoded.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg));
+      Orthanc::ImageProcessing::Copy(target, source);
     }
-    else
+  }
+
+  return full.release();
+}
+
+
+void ServeIIIFTiledImageTile(OrthancPluginRestOutput* output,
+                             const char* url,
+                             const OrthancPluginHttpRequest* request)
+{
+  const std::string seriesId(request->groups[0]);
+  const std::string region(request->groups[1]);
+  const std::string size(request->groups[2]);
+  const std::string rotation(request->groups[3]);
+  const std::string quality(request->groups[4]);
+  const std::string format(request->groups[5]);
+
+  LOG(INFO) << "IIIF: Image API call to tile of series " << seriesId << ": "
+            << "region=" << region << "; size=" << size << "; rotation="
+            << rotation << "; quality=" << quality << "; format=" << format;
+
+  const RegionParameters parameters(region, size, rotation, quality, format);
+
+  if (parameters.IsFull())
+  {
+    std::unique_ptr<Orthanc::ImageAccessor> image;
+
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
+      image.reset(RenderFullImage(locker.GetPyramid()));
     }
+
+    std::string encoded;
+    OrthancWSI::RawTile::Encode(encoded, *image, Orthanc::MimeType_Jpeg);
+
+    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(),
+                              encoded.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg));
+  }
+  else
+  {
+    std::unique_ptr<RegionRenderer> renderer;
+
+    {
+      OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
+      renderer.reset(new RegionRenderer(parameters, locker.GetPyramid()));
+    }
+
+    renderer->Answer(output);
   }
 }
 
 
 static void AddCanvas(Json::Value& manifest,
-                      const std::string& seriesId,
+                      const std::string& resourceBase,
                       const std::string& imageService,
                       unsigned int page,
                       unsigned int width,
                       unsigned int height,
                       const std::string& description)
 {
-  const std::string base = iiifPublicUrl_ + seriesId;
+  const std::string base = iiifPublicUrl_ + resourceBase;
 
   Json::Value service;
   service["id"] = iiifPublicUrl_ + imageService;
@@ -439,6 +579,37 @@ static void AddCanvas(Json::Value& manifest,
 }
 
 
+static std::string GetTagValue(const Json::Value& tags,
+                               const std::string& key)
+{
+  if (tags.isMember(key))
+  {
+    return tags[key].asString();
+  }
+  else
+  {
+    return "";
+  }
+}
+
+
+static void FillManifest(Json::Value& manifest,
+                         const std::string& base,
+                         const Json::Value& study,
+                         const Json::Value& series)
+{
+  static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+
+  manifest["@context"] = "http://iiif.io/api/presentation/3/context.json";
+  manifest["id"] = base + "/manifest.json";
+  manifest["type"] = "Manifest";
+  manifest["label"]["en"].append(GetTagValue(study[MAIN_DICOM_TAGS], "StudyDate") + " - " +
+                                 GetTagValue(series[MAIN_DICOM_TAGS], "Modality") + " - " +
+                                 GetTagValue(study[MAIN_DICOM_TAGS], "StudyDescription") + " - " +
+                                 GetTagValue(series[MAIN_DICOM_TAGS], "SeriesDescription"));
+}
+
+
 void ServeIIIFManifest(OrthancPluginRestOutput* output,
                        const char* url,
                        const OrthancPluginHttpRequest* request)
@@ -479,16 +650,8 @@ void ServeIIIFManifest(OrthancPluginRestOutput* output,
 
   const std::string sopClassUid = Orthanc::Toolbox::StripSpaces(oneInstance[SOP_CLASS_UID].asString());
 
-  const std::string base = iiifPublicUrl_ + "series/" + seriesId;
-
   Json::Value manifest;
-  manifest["@context"] = "http://iiif.io/api/presentation/3/context.json";
-  manifest["id"] = base + "/manifest.json";
-  manifest["type"] = "Manifest";
-  manifest["label"]["en"].append(study["MainDicomTags"]["StudyDate"].asString() + " - " +
-                                 series["MainDicomTags"]["Modality"].asString() + " - " +
-                                 study["MainDicomTags"]["StudyDescription"].asString() + " - " +
-                                 series["MainDicomTags"]["SeriesDescription"].asString());
+  FillManifest(manifest, iiifPublicUrl_ + "series/" + seriesId, study, series);
 
   if (sopClassUid == OrthancWSI::VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD)
   {
@@ -641,15 +804,153 @@ void ServeIIIFFrameImage(OrthancPluginRestOutput* output,
 }
 
 
+void ServeIIIFFramePyramidManifest(OrthancPluginRestOutput* output,
+                                   const char* url,
+                                   const OrthancPluginHttpRequest* request)
+{
+  const std::string instanceId(request->groups[0]);
+
+  unsigned int frameNumber;
+  if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(frameNumber, request->groups[1]))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+  }
+
+  LOG(INFO) << "IIIF: Presentation API call to frame " << frameNumber << " of instance " << instanceId;
+
+  Json::Value instance, study, series;
+  if (!OrthancPlugins::RestApiGet(instance, "/instances/" + instanceId, false) ||
+      !OrthancPlugins::RestApiGet(series, "/instances/" + instanceId + "/series", false) ||
+      !OrthancPlugins::RestApiGet(study, "/instances/" + instanceId + "/study", false))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+  }
+
+  if (instance.type() != Json::objectValue ||
+      series.type() != Json::objectValue ||
+      study.type() != Json::objectValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+
+  const std::string resourceBase = "frames-pyramids/" + instanceId + "/" + boost::lexical_cast<std::string>(frameNumber);
+
+  Json::Value manifest;
+  FillManifest(manifest, iiifPublicUrl_ + resourceBase, study, series);
+
+  /**
+   * This is based on IIIF cookbook: "Support Deep Viewing with Basic
+   * Use of a IIIF Image Service."
+   * https://iiif.io/api/cookbook/recipe/0005-image-service/
+   **/
+  unsigned int width, height;
+
+  {
+    OrthancWSI::DecodedPyramidCache::Accessor accessor(OrthancWSI::DecodedPyramidCache::GetInstance(), instanceId, frameNumber);
+    width = accessor.GetPyramid().GetLevelWidth(0);
+    height = accessor.GetPyramid().GetLevelHeight(0);
+  }
+
+  AddCanvas(manifest, resourceBase, resourceBase, 1, width, height, "");
+
+  std::string s = manifest.toStyledString();
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Json));
+}
+
+
+void ServeIIIFFramePyramidInfo(OrthancPluginRestOutput* output,
+                               const char* url,
+                               const OrthancPluginHttpRequest* request)
+{
+  const std::string instanceId(request->groups[0]);
+  unsigned int frameNumber;
+  if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(frameNumber, request->groups[1]))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+  }
+
+  LOG(INFO) << "IIIF: Image API call to whole-slide pyramid of frame " << frameNumber << " of instance " << instanceId;
+
+  Json::Value result;
+
+  {
+    OrthancWSI::DecodedPyramidCache::Accessor accessor(OrthancWSI::DecodedPyramidCache::GetInstance(), instanceId, frameNumber);
+    GeneratePyramidInfo(result, accessor.GetPyramid(), "instance " + instanceId + " (frame " + boost::lexical_cast<std::string>(frameNumber) + ")");
+  }
+
+  result["id"] = iiifPublicUrl_ + "frames-pyramids/" + instanceId + "/" + boost::lexical_cast<std::string>(frameNumber);
+
+  std::string s = result.toStyledString();
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Json));
+}
+
+
+void ServeIIIFFramePyramidTile(OrthancPluginRestOutput* output,
+                               const char* url,
+                               const OrthancPluginHttpRequest* request)
+{
+  const std::string instanceId(request->groups[0]);
+  const std::string region(request->groups[2]);
+  const std::string size(request->groups[3]);
+  const std::string rotation(request->groups[4]);
+  const std::string quality(request->groups[5]);
+  const std::string format(request->groups[6]);
+
+  unsigned int frameNumber;
+  if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(frameNumber, request->groups[1]))
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+  }
+
+  LOG(INFO) << "IIIF: Image API call to tile of frame " << frameNumber << " in instance " << instanceId << ": "
+            << "region=" << region << "; size=" << size << "; rotation="
+            << rotation << "; quality=" << quality << "; format=" << format;
+
+  const RegionParameters parameters(region, size, rotation, quality, format);
+
+  if (parameters.IsFull())
+  {
+    std::unique_ptr<Orthanc::ImageAccessor> image;
+
+    {
+      OrthancWSI::DecodedPyramidCache::Accessor accessor(OrthancWSI::DecodedPyramidCache::GetInstance(), instanceId, frameNumber);
+      image.reset(RenderFullImage(accessor.GetPyramid()));
+    }
+
+    std::string encoded;
+    OrthancWSI::RawTile::Encode(encoded, *image, Orthanc::MimeType_Jpeg);
+
+    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(),
+                              encoded.size(), Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg));
+  }
+  else
+  {
+    std::unique_ptr<RegionRenderer> renderer;
+
+    {
+      OrthancWSI::DecodedPyramidCache::Accessor accessor(OrthancWSI::DecodedPyramidCache::GetInstance(), instanceId, frameNumber);
+      renderer.reset(new RegionRenderer(parameters, accessor.GetPyramid()));
+    }
+
+    renderer->Answer(output);
+  }
+}
+
+
 void InitializeIIIF(const std::string& iiifPublicUrl)
 {
   iiifPublicUrl_ = iiifPublicUrl;
 
-  OrthancPlugins::RegisterRestCallback<ServeIIIFTiledImageInfo>("/wsi/iiif/tiles/([0-9a-f-]+)/info.json", true);
+  OrthancPlugins::RegisterRestCallback<ServeIIIFSeriesPyramidInfo>("/wsi/iiif/tiles/([0-9a-f-]+)/info.json", true);
   OrthancPlugins::RegisterRestCallback<ServeIIIFTiledImageTile>("/wsi/iiif/tiles/([0-9a-f-]+)/([0-9a-z,:]+)/([0-9a-z,!:]+)/([0-9,!]+)/([a-z]+)\\.([a-z]+)", true);
   OrthancPlugins::RegisterRestCallback<ServeIIIFManifest>("/wsi/iiif/series/([0-9a-f-]+)/manifest.json", true);
   OrthancPlugins::RegisterRestCallback<ServeIIIFFrameInfo>("/wsi/iiif/frames/([0-9a-f-]+)/([0-9]+)/info.json", true);
   OrthancPlugins::RegisterRestCallback<ServeIIIFFrameImage>("/wsi/iiif/frames/([0-9a-f-]+)/([0-9]+)/full/max/0/default.jpg", true);
+
+  // New in WSI 3.0
+  OrthancPlugins::RegisterRestCallback<ServeIIIFFramePyramidManifest>("/wsi/iiif/frames-pyramids/([0-9a-f-]+)/([0-9]+)/manifest.json", true);
+  OrthancPlugins::RegisterRestCallback<ServeIIIFFramePyramidInfo>("/wsi/iiif/frames-pyramids/([0-9a-f-]+)/([0-9]+)/info.json", true);
+  OrthancPlugins::RegisterRestCallback<ServeIIIFFramePyramidTile>("/wsi/iiif/frames-pyramids/([0-9a-f-]+)/([0-9]+)/([0-9a-z,:]+)/([0-9a-z,!:]+)/([0-9,!]+)/([a-z]+)\\.([a-z]+)", true);
 }
 
 void SetIIIFForcePowersOfTwoScaleFactors(bool force)


=====================================
ViewerPlugin/OrthancExplorer.js
=====================================
@@ -24,24 +24,22 @@
 $('#series').live('pagebeforeshow', function() {
   var seriesId = $.mobile.pageData.uuid;
 
-  $('#mirador-button').remove();
-  $('#openseadragon-button').remove();
-  $('#wsi-button').remove();
+  $('#wsi-series-button').remove();
+  $('#wsi-series-iiif-button').remove();
+  $('#wsi-series-mirador-button').remove();
+  $('#wsi-series-openseadragon-button').remove();
 
-  $('#series-iiif-button').remove();
-  $('#series-access').listview("refresh");
+  $('#series-access').listview('refresh');
 
   // Test whether this is a whole-slide image by check the SOP Class
   // UID of one instance of the series
   GetResource('/series/' + seriesId, function(series) {
     GetResource('/instances/' + series['Instances'][0] + '/tags?simplify', function(instance) {
-      console.log(instance['SOPClassUID']);
-
       if (instance['SOPClassUID'] == '1.2.840.10008.5.1.4.1.1.77.1.6') {
 
         // This is a whole-slide image, register the button
         var b = $('<a>')
-          .attr('id', 'wsi-button')
+          .attr('id', 'wsi-series-button')
           .attr('data-role', 'button')
           .attr('href', '#')
           .attr('data-icon', 'search')
@@ -56,9 +54,10 @@ $('#series').live('pagebeforeshow', function() {
           }
         });
 
-        if (${SERVE_OPEN_SEADRAGON}) {
+        if (${ENABLE_IIIF} &&
+            ${SERVE_OPEN_SEADRAGON}) {
           var b = $('<a>')
-              .attr('id', 'openseadragon-button')
+              .attr('id', 'wsi-series-openseadragon-button')
               .attr('data-role', 'button')
               .attr('href', '#')
               .attr('data-icon', 'search')
@@ -82,11 +81,11 @@ $('#series').live('pagebeforeshow', function() {
             .text('Copy link to IIIF manifest');
 
         var li = $('<li>')
-            .attr('id', 'series-iiif-button')
+            .attr('id', 'wsi-series-iiif-button')
             .attr('data-icon', 'gear')
             .append(b);
 
-        $('#series-access').append(li).listview("refresh");
+        $('#series-access').append(li).listview('refresh');
 
         b.click(function(e) {
           if ($.mobile.pageData) {
@@ -98,9 +97,10 @@ $('#series').live('pagebeforeshow', function() {
         });
       }
 
-      if (${SERVE_MIRADOR}) {
+      if (${ENABLE_IIIF} &&
+          ${SERVE_MIRADOR}) {
         var b = $('<a>')
-            .attr('id', 'mirador-button')
+            .attr('id', 'wsi-series-mirador-button')
             .attr('data-role', 'button')
             .attr('href', '#')
             .attr('data-icon', 'search')
@@ -118,3 +118,90 @@ $('#series').live('pagebeforeshow', function() {
     });
   });
 });
+
+
+$('#instance').live('pagebeforeshow', function() {
+  var instanceId = $.mobile.pageData.uuid;
+
+  $('#wsi-instance-button').remove();
+  $('#wsi-instance-iiif-button').remove();
+  $('#wsi-instance-mirador-button').remove();
+  $('#wsi-instance-openseadragon-button').remove();
+
+  var b = $('<a>')
+    .attr('id', 'wsi-instance-button')
+    .attr('data-role', 'button')
+    .attr('href', '#')
+    .attr('data-icon', 'search')
+    .attr('data-theme', 'e')
+    .text('Deep zoom viewer')
+    .button();
+
+  b.insertAfter($('#instance-info'));
+  b.click(function() {
+    if ($.mobile.pageData) {
+      window.open('../wsi/app/viewer.html?instance=' + instanceId);
+    }
+  });
+
+  if (${ENABLE_IIIF} &&
+      ${SERVE_OPEN_SEADRAGON}) {
+    var b = $('<a>')
+        .attr('id', 'wsi-instance-openseadragon-button')
+        .attr('data-role', 'button')
+        .attr('href', '#')
+        .attr('data-icon', 'search')
+        .attr('data-theme', 'e')
+        .text('Test IIIF in OpenSeadragon')
+        .button();
+
+    b.insertAfter($('#instance-info'));
+    b.click(function () {
+      if ($.mobile.pageData) {
+        window.open('../wsi/app/openseadragon.html?image=../iiif/frames-pyramids/' + instanceId + '/0/info.json');
+      }
+    });
+  }
+
+  if (${ENABLE_IIIF}) {
+    var b = $('<a>')
+        .attr('data-role', 'button')
+        .attr('href', '#')
+        .text('Copy link to IIIF manifest');
+
+    var li = $('<li>')
+        .attr('id', 'wsi-instance-iiif-button')
+        .attr('data-icon', 'gear')
+        .append(b);
+
+    $('#instance-access').append(li).listview('refresh');
+
+    b.click(function(e) {
+      if ($.mobile.pageData) {
+        e.preventDefault();
+        var url = new URL('../wsi/iiif/frames-pyramids/' + instanceId + '/0/manifest.json', window.location.href);
+        navigator.clipboard.writeText(url.href);
+        $(e.target).closest('li').buttonMarkup({ icon: 'check' });
+      }
+    });
+  }
+
+  if (${ENABLE_IIIF} &&
+      ${SERVE_MIRADOR}) {
+    var b = $('<a>')
+        .attr('id', 'wsi-instance-mirador-button')
+        .attr('data-role', 'button')
+        .attr('href', '#')
+        .attr('data-icon', 'search')
+        .attr('data-theme', 'e')
+        .text('Test IIIF in Mirador')
+        .button();
+
+    b.insertAfter($('#instance-info'));
+    b.click(function() {
+      if ($.mobile.pageData) {
+        window.open('../wsi/app/mirador.html?iiif-content=../iiif/frames-pyramids/' + instanceId + '/0/manifest.json');
+      }
+    });
+  }
+});


=====================================
ViewerPlugin/OrthancPyramidFrameFetcher.cpp
=====================================
@@ -0,0 +1,231 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../Framework/PrecompiledHeadersWSI.h"
+#include "OrthancPyramidFrameFetcher.h"
+
+#include "../Framework/Inputs/OnTheFlyPyramid.h"
+
+#include <Images/Image.h>
+#include <Images/ImageProcessing.h>
+
+#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+
+namespace OrthancWSI
+{
+  void OrthancPyramidFrameFetcher::RenderGrayscale(Orthanc::ImageAccessor& target,
+                                                   const Orthanc::ImageAccessor& source)
+  {
+    Orthanc::Image converted(Orthanc::PixelFormat_Float32, source.GetWidth(), source.GetHeight(), false);
+    Orthanc::ImageProcessing::Convert(converted, source);
+
+    float minValue, maxValue;
+    Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, converted);
+
+    assert(minValue <= maxValue);
+    if (std::abs(maxValue - minValue) < 0.0001)
+    {
+      Orthanc::ImageProcessing::Set(target, 0);
+    }
+    else
+    {
+      const float scaling = 255.0f / (maxValue - minValue);
+      const float offset = -minValue;
+
+      Orthanc::Image rescaled(Orthanc::PixelFormat_Grayscale8, source.GetWidth(), source.GetHeight(), false);
+      Orthanc::ImageProcessing::ShiftScale(rescaled, converted, static_cast<float>(offset), static_cast<float>(scaling), false);
+      Orthanc::ImageProcessing::Convert(target, rescaled);
+    }
+  }
+
+
+  OrthancPyramidFrameFetcher::OrthancPyramidFrameFetcher(OrthancStone::IOrthancConnection* orthanc,
+                                                         bool smooth) :
+    orthanc_(orthanc),
+    smooth_(smooth),
+    tileWidth_(512),
+    tileHeight_(512),
+    paddingX_(0),
+    paddingY_(0),
+    defaultBackgroundRed_(0),
+    defaultBackgroundGreen_(0),
+    defaultBackgroundBlue_(0)
+  {
+    if (orthanc == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+  }
+
+
+  void OrthancPyramidFrameFetcher::SetTileWidth(unsigned int tileWidth)
+  {
+    if (tileWidth <= 2)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      tileWidth_ = tileWidth;
+    }
+  }
+
+
+  void OrthancPyramidFrameFetcher::SetTileHeight(unsigned int tileHeight)
+  {
+    if (tileHeight <= 2)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      tileHeight_ = tileHeight;
+    }
+  }
+
+
+  void OrthancPyramidFrameFetcher::SetDefaultBackgroundColor(uint8_t red,
+                                                             uint8_t green,
+                                                             uint8_t blue)
+  {
+    defaultBackgroundRed_ = red;
+    defaultBackgroundGreen_ = green;
+    defaultBackgroundBlue_ = blue;
+  }
+
+
+  DecodedTiledPyramid* OrthancPyramidFrameFetcher::Fetch(const std::string &instanceId,
+                                                         unsigned frameNumber)
+  {
+    OrthancPlugins::MemoryBuffer buffer;
+    buffer.GetDicomInstance(instanceId.c_str());
+
+    OrthancPlugins::DicomInstance dicom(buffer.GetData(), buffer.GetSize());
+
+    uint8_t backgroundRed = defaultBackgroundRed_;
+    uint8_t backgroundGreen = defaultBackgroundGreen_;
+    uint8_t backgroundBlue = defaultBackgroundBlue_;
+
+    Json::Value tags;
+    dicom.GetSimplifiedJson(tags);
+
+    static const char* const PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
+
+    if (tags.isMember(PHOTOMETRIC_INTERPRETATION) &&
+        tags[PHOTOMETRIC_INTERPRETATION].type() == Json::stringValue)
+    {
+      std::string p = tags[PHOTOMETRIC_INTERPRETATION].asString();
+      if (p == "MONOCHROME1")
+      {
+        backgroundRed = 255;
+        backgroundGreen = 255;
+        backgroundBlue = 255;
+      }
+      else if (p == "MONOCHROME2")
+      {
+        backgroundRed = 0;
+        backgroundGreen = 0;
+        backgroundBlue = 0;
+      }
+    }
+
+    std::unique_ptr<OrthancPlugins::OrthancImage> frame(dicom.GetDecodedFrame(frameNumber));
+
+    Orthanc::PixelFormat format;
+    switch (frame->GetPixelFormat())
+    {
+    case OrthancPluginPixelFormat_RGB24:
+      format = Orthanc::PixelFormat_RGB24;
+      break;
+
+    case OrthancPluginPixelFormat_Grayscale8:
+      format = Orthanc::PixelFormat_Grayscale8;
+      break;
+
+    case OrthancPluginPixelFormat_Grayscale16:
+      format = Orthanc::PixelFormat_Grayscale16;
+      break;
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    Orthanc::ImageAccessor source;
+    source.AssignReadOnly(format, frame->GetWidth(), frame->GetHeight(), frame->GetPitch(), frame->GetBuffer());
+
+    unsigned int paddedWidth, paddedHeight;
+
+    if (paddingX_ >= 2)
+    {
+      paddedWidth = OrthancWSI::CeilingDivision(source.GetWidth(), paddingX_) * paddingX_;
+    }
+    else
+    {
+      paddedWidth = source.GetWidth();
+    }
+
+    if (paddingY_ >= 2)
+    {
+      paddedHeight = OrthancWSI::CeilingDivision(source.GetHeight(), paddingY_) * paddingY_;
+    }
+    else
+    {
+      paddedHeight = source.GetHeight();
+    }
+
+    std::unique_ptr<Orthanc::ImageAccessor> rendered(new Orthanc::Image(Orthanc::PixelFormat_RGB24, paddedWidth, paddedHeight, false));
+
+    if (paddedWidth != source.GetWidth() ||
+        paddedHeight != source.GetHeight())
+    {
+      Orthanc::ImageProcessing::Set(*rendered, backgroundRed, backgroundGreen, backgroundBlue, 255 /* alpha */);
+    }
+
+    Orthanc::ImageAccessor region;
+    rendered->GetRegion(region, 0, 0, source.GetWidth(), source.GetHeight());
+
+    switch (format)
+    {
+    case Orthanc::PixelFormat_RGB24:
+      Orthanc::ImageProcessing::Copy(region, source);
+      break;
+
+    case Orthanc::PixelFormat_Grayscale8:
+      Orthanc::ImageProcessing::Convert(region, source);
+      break;
+
+    case Orthanc::PixelFormat_Grayscale16:
+      RenderGrayscale(region, source);
+      break;
+
+    default:
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    std::unique_ptr<DecodedTiledPyramid> result(new OnTheFlyPyramid(rendered.release(), tileWidth_, tileHeight_, smooth_));
+    result->SetBackgroundColor(backgroundRed, backgroundGreen, backgroundBlue);
+
+    return result.release();
+  }
+}


=====================================
ViewerPlugin/OrthancPyramidFrameFetcher.h
=====================================
@@ -0,0 +1,95 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Framework/Inputs/DecodedPyramidCache.h"
+#include "../Resources/Orthanc/Stone/IOrthancConnection.h"
+
+
+namespace OrthancWSI
+{
+  class OrthancPyramidFrameFetcher : public DecodedPyramidCache::IPyramidFetcher
+  {
+  private:
+    std::unique_ptr<OrthancStone::IOrthancConnection>  orthanc_;
+    bool                                               smooth_;
+    unsigned int                                       tileWidth_;
+    unsigned int                                       tileHeight_;
+    unsigned int                                       paddingX_;
+    unsigned int                                       paddingY_;
+    uint8_t                                            defaultBackgroundRed_;
+    uint8_t                                            defaultBackgroundGreen_;
+    uint8_t                                            defaultBackgroundBlue_;
+
+    static void RenderGrayscale(Orthanc::ImageAccessor& target,
+                                const Orthanc::ImageAccessor& source);
+
+  public:
+    OrthancPyramidFrameFetcher(OrthancStone::IOrthancConnection* orthanc,
+                               bool smooth);
+
+    unsigned int GetTileWidth() const
+    {
+      return tileWidth_;
+    }
+
+    void SetTileWidth(unsigned int tileWidth);
+
+    unsigned int GetTileHeight() const
+    {
+      return tileHeight_;
+    }
+
+    void SetTileHeight(unsigned int tileHeight);
+
+    unsigned int GetPaddingX() const
+    {
+      return paddingX_;
+    }
+
+    // "0" or "1" implies no padding
+    void SetPaddingX(unsigned int paddingX)
+    {
+      paddingX_ = paddingX;
+    }
+
+    unsigned int GetPaddingY() const
+    {
+      return paddingY_;
+    }
+
+    // "0" or "1" implies no padding
+    void SetPaddingY(unsigned int paddingY)
+    {
+      paddingY_ = paddingY;
+    }
+
+    void SetDefaultBackgroundColor(uint8_t red,
+                                   uint8_t green,
+                                   uint8_t blue);
+
+    DecodedTiledPyramid* Fetch(const std::string &instanceId,
+                               unsigned frameNumber) ORTHANC_OVERRIDE;
+  };
+}


=====================================
ViewerPlugin/Plugin.cpp
=====================================
@@ -23,9 +23,14 @@
 
 #include "../Framework/PrecompiledHeadersWSI.h"
 
+#include "OrthancPyramidFrameFetcher.h"
 #include "DicomPyramidCache.h"
 #include "IIIF.h"
 #include "RawTile.h"
+#include "../Framework/Inputs/DecodedTiledPyramid.h"
+#include "../Framework/Inputs/OnTheFlyPyramid.h"
+#include "../Framework/Inputs/DecodedPyramidCache.h"
+#include "../Framework/ImageToolbox.h"
 
 #include <Compatibility.h>  // For std::unique_ptr
 #include <Images/Image.h>
@@ -39,6 +44,10 @@
 #include <EmbeddedResources.h>
 
 #include <cassert>
+#include <Images/PngReader.h>
+
+#include "OrthancPluginConnection.h"
+
 
 #define ORTHANC_PLUGIN_NAME "wsi"
 
@@ -52,35 +61,25 @@ static bool DisplayPerformanceWarning()
 }
 
 
-void ServePyramid(OrthancPluginRestOutput* output,
-                  const char* url,
-                  const OrthancPluginHttpRequest* request)
+static void DescribePyramid(Json::Value& result,
+                            const OrthancWSI::ITiledPyramid& pyramid)
 {
-  std::string seriesId(request->groups[0]);
-
-  char tmp[1024];
-  sprintf(tmp, "Accessing whole-slide pyramid of series %s", seriesId.c_str());
-  OrthancPluginLogInfo(OrthancPlugins::GetGlobalContext(), tmp);
-  
-
-  OrthancWSI::DicomPyramidCache::Locker locker(seriesId);
-
-  unsigned int totalWidth = locker.GetPyramid().GetLevelWidth(0);
-  unsigned int totalHeight = locker.GetPyramid().GetLevelHeight(0);
+  unsigned int totalWidth = pyramid.GetLevelWidth(0);
+  unsigned int totalHeight = pyramid.GetLevelHeight(0);
 
   Json::Value sizes = Json::arrayValue;
   Json::Value resolutions = Json::arrayValue;
   Json::Value tilesCount = Json::arrayValue;
   Json::Value tilesSizes = Json::arrayValue;
-  for (unsigned int i = 0; i < locker.GetPyramid().GetLevelCount(); i++)
+  for (unsigned int i = 0; i < pyramid.GetLevelCount(); i++)
   {
-    const unsigned int levelWidth = locker.GetPyramid().GetLevelWidth(i);
-    const unsigned int levelHeight = locker.GetPyramid().GetLevelHeight(i);
-    const unsigned int tileWidth = locker.GetPyramid().GetTileWidth(i);
-    const unsigned int tileHeight = locker.GetPyramid().GetTileHeight(i);
-    
+    const unsigned int levelWidth = pyramid.GetLevelWidth(i);
+    const unsigned int levelHeight = pyramid.GetLevelHeight(i);
+    const unsigned int tileWidth = pyramid.GetTileWidth(i);
+    const unsigned int tileHeight = pyramid.GetTileHeight(i);
+
     resolutions.append(static_cast<float>(totalWidth) / static_cast<float>(levelWidth));
-    
+
     Json::Value s = Json::arrayValue;
     s.append(levelWidth);
     s.append(levelHeight);
@@ -97,28 +96,136 @@ void ServePyramid(OrthancPluginRestOutput* output,
     tilesSizes.append(s);
   }
 
-  Json::Value result;
-  result["ID"] = seriesId;
   result["Resolutions"] = resolutions;
   result["Sizes"] = sizes;
   result["TilesCount"] = tilesCount;
   result["TilesSizes"] = tilesSizes;
   result["TotalHeight"] = totalHeight;
   result["TotalWidth"] = totalWidth;
+}
+
+
+void ServePyramid(OrthancPluginRestOutput* output,
+                  const char* url,
+                  const OrthancPluginHttpRequest* request)
+{
+  std::string seriesId(request->groups[0]);
+
+  LOG(INFO) << "Accessing whole-slide pyramid of series " << seriesId;
+
+  Json::Value answer;
+  answer["ID"] = seriesId;
+
+  {
+    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;
+    }
+  }
+
+  std::string s = answer.toStyledString();
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
+}
+
+
+void ServeFramePyramid(OrthancPluginRestOutput* output,
+                       const char* url,
+                       const OrthancPluginHttpRequest* request)
+{
+  std::string instanceId(request->groups[0]);
+  int frameNumber = boost::lexical_cast<int>(request->groups[1]);
+
+  LOG(INFO) << "Accessing pyramid of frame " << frameNumber << " in instance " << instanceId;
 
+  if (frameNumber < 0)
   {
-    // New in WSI 2.1
-    sprintf(tmp, "#%02x%02x%02x", locker.GetPyramid().GetBackgroundRed(),
-            locker.GetPyramid().GetBackgroundGreen(),
-            locker.GetPyramid().GetBackgroundBlue());
-    result["BackgroundColor"] = tmp;
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+
+  Json::Value answer;
+  answer["ID"] = instanceId;
+  answer["FrameNumber"] = frameNumber;
+
+  {
+    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;
+    }
   }
 
-  std::string s = result.toStyledString();
+  std::string s = answer.toStyledString();
   OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
 }
 
 
+static bool LookupAcceptHeader(Orthanc::MimeType& target,
+                               const OrthancPluginHttpRequest* request)
+{
+  // Lookup whether a "Accept" HTTP header is present, to overwrite
+  // the default MIME type
+  for (uint32_t i = 0; i < request->headersCount; i++)
+  {
+    std::string key(request->headersKeys[i]);
+    Orthanc::Toolbox::ToLowerCase(key);
+
+    if (key == "accept")
+    {
+      std::vector<std::string> tokens;
+      Orthanc::Toolbox::TokenizeString(tokens, request->headersValues[i], ',');
+
+      bool compatible = false;
+
+      for (size_t j = 0; j < tokens.size(); j++)
+      {
+        std::string s = Orthanc::Toolbox::StripSpaces(tokens[j]);
+
+        if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Png))
+        {
+          target = Orthanc::MimeType_Png;
+          return true;
+        }
+        else if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg))
+        {
+          target = Orthanc::MimeType_Jpeg;
+          return true;
+        }
+        else if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg2000))
+        {
+          target = Orthanc::MimeType_Jpeg2000;
+          return true;
+        }
+        else if (s == "*/*" ||
+                 s == "image/*")
+        {
+          compatible = true;
+        }
+      }
+
+      if (!compatible)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotAcceptable);
+      }
+    }
+  }
+
+  return false;
+}
+
+
 void ServeTile(OrthancPluginRestOutput* output,
                const char* url,
                const OrthancPluginHttpRequest* request)
@@ -128,10 +235,8 @@ void ServeTile(OrthancPluginRestOutput* output,
   int tileY = boost::lexical_cast<int>(request->groups[3]);
   int tileX = boost::lexical_cast<int>(request->groups[2]);
 
-  char tmp[1024];
-  sprintf(tmp, "Accessing tile in series %s: (%d,%d) at level %d", seriesId.c_str(), tileX, tileY, level);
-  OrthancPluginLogInfo(OrthancPlugins::GetGlobalContext(), tmp);
-  
+  LOG(INFO) << "Accessing tile in series " << seriesId << ": (" << tileX << "," << tileY << ") at level " << level;
+
   if (level < 0 ||
       tileX < 0 ||
       tileY < 0)
@@ -176,69 +281,71 @@ void ServeTile(OrthancPluginRestOutput* output,
     mime = Orthanc::MimeType_Png;
   }
 
-  // Lookup whether a "Accept" HTTP header is present, to overwrite
-  // the default MIME type
-  for (uint32_t i = 0; i < request->headersCount; i++)
+  Orthanc::MimeType accept;
+  if (LookupAcceptHeader(accept, request))
   {
-    std::string key(request->headersKeys[i]);
-    Orthanc::Toolbox::ToLowerCase(key);
+    mime = accept;
+  }
 
-    if (key == "accept")
-    {
-      std::vector<std::string> tokens;
-      Orthanc::Toolbox::TokenizeString(tokens, request->headersValues[i], ',');
+  rawTile->Answer(output, mime);
+}
 
-      bool found = false;
 
-      for (size_t j = 0; j < tokens.size(); j++)
-      {
-        std::string s = Orthanc::Toolbox::StripSpaces(tokens[j]);
+void ServeFrameTile(OrthancPluginRestOutput* output,
+                    const char* url,
+                    const OrthancPluginHttpRequest* request)
+{
+  std::string instanceId(request->groups[0]);
+  int frameNumber = boost::lexical_cast<int>(request->groups[1]);
+  int level = boost::lexical_cast<int>(request->groups[2]);
+  int tileY = boost::lexical_cast<int>(request->groups[4]);
+  int tileX = boost::lexical_cast<int>(request->groups[3]);
 
-        if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Png))
-        {
-          mime = Orthanc::MimeType_Png;
-          found = true;
-        }
-        else if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg))
-        {
-          mime = Orthanc::MimeType_Jpeg;
-          found = true;
-        }
-        else if (s == Orthanc::EnumerationToString(Orthanc::MimeType_Jpeg2000))
-        {
-          mime = Orthanc::MimeType_Jpeg2000;
-          found = true;
-        }
-        else if (s == "*/*" ||
-                 s == "image/*")
-        {
-          found = true;
-        }
-      }
+  LOG(INFO) << "Accessing on-the-fly tile in frame " << frameNumber << " of instance " << instanceId <<": (" << tileX << "," << tileY << ") at level " << level;
 
-      if (!found)
-      {
-        OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 406 /* Not acceptable */);
-        return;
-      }
+  if (frameNumber < 0 ||
+      level < 0 ||
+      tileX < 0 ||
+      tileY < 0)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+
+  std::unique_ptr<Orthanc::ImageAccessor> tile;
+
+  {
+    OrthancWSI::DecodedPyramidCache::Accessor accessor(OrthancWSI::DecodedPyramidCache::GetInstance(), instanceId, frameNumber);
+    if (!accessor.IsValid())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
     }
+
+    bool isEmpty;  // Ignored
+    tile.reset(accessor.GetPyramid().DecodeTile(isEmpty, level, tileX, tileY));
   }
 
-  rawTile->Answer(output, mime);
+  Orthanc::MimeType mime;
+  if (!LookupAcceptHeader(mime, request))
+  {
+    mime = Orthanc::MimeType_Png;  // By default, use lossless compression
+  }
+
+  std::string encoded;
+  OrthancWSI::ImageToolbox::EncodeTile(encoded, *tile, OrthancWSI::ImageToolbox::Convert(mime), 90 /* only used for JPEG */);
+
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, encoded.c_str(),
+                            encoded.size(), Orthanc::EnumerationToString(mime));
 }
 
 
-OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, 
+OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
                                         OrthancPluginResourceType resourceType, 
                                         const char *resourceId)
 {
   if (resourceType == OrthancPluginResourceType_Series &&
       changeType == OrthancPluginChangeType_NewChildInstance)
-  { 
-    char tmp[1024];
-    sprintf(tmp, "New instance has been added to series %s, invalidating it", resourceId);
-    OrthancPluginLogInfo(OrthancPlugins::GetGlobalContext(), tmp);
-
+  {
+    LOG(INFO) << "New instance has been added to series " << resourceId << ", invalidating it";
     OrthancWSI::DicomPyramidCache::GetInstance().Invalidate(resourceId);
   }
 
@@ -302,7 +409,7 @@ extern "C"
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
   {
-    OrthancPlugins::SetGlobalContext(context);
+    OrthancPlugins::SetGlobalContext(context, ORTHANC_PLUGIN_NAME);
     assert(DisplayPerformanceWarning());
 
     /* Check the version of the Orthanc core */
@@ -324,7 +431,9 @@ extern "C"
       return -1;
     }
 
-#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2)
+#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4)
+    Orthanc::Logging::InitializePluginContext(context, ORTHANC_PLUGIN_NAME);
+#elif ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2)
     Orthanc::Logging::InitializePluginContext(context);
 #else
     Orthanc::Logging::Initialize(context);
@@ -336,14 +445,24 @@ extern "C"
     unsigned int threads = Orthanc::SystemToolbox::GetHardwareConcurrency();
     OrthancWSI::RawTile::InitializeTranscoderSemaphore(threads);
 
-    char info[1024];
-    sprintf(info, "The whole-slide imaging plugin will use at most %u threads to transcode the tiles", threads);
-    OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), info);
+    LOG(WARNING) << "The whole-slide imaging plugin will use at most " << threads << " threads to transcode the tiles";
 
     OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Provides a Web viewer of whole-slide microscopic images within Orthanc.");
 
     OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */);
 
+    {
+      std::unique_ptr<OrthancWSI::OrthancPyramidFrameFetcher> fetcher(
+        new OrthancWSI::OrthancPyramidFrameFetcher(new OrthancWSI::OrthancPluginConnection(), false /* smooth - TODO PARAMETER */));
+      fetcher->SetPaddingX(64);  // TODO PARAMETER
+      fetcher->SetPaddingY(64);  // TODO PARAMETER
+      fetcher->SetDefaultBackgroundColor(255, 255, 255);  // TODO PARAMETER
+
+      OrthancWSI::DecodedPyramidCache::InitializeInstance(fetcher.release(),
+                                                          10 /* TODO - PARAMETER */,
+                                                          256 * 1024 * 1024 /* TODO - PARAMETER */);
+    }
+
     OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback);
 
     OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(ol.css)", true);
@@ -352,6 +471,8 @@ extern "C"
     OrthancPlugins::RegisterRestCallback<ServeFile>("/wsi/app/(viewer.js)", true);
     OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true);
     OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
+    OrthancPlugins::RegisterRestCallback<ServeFramePyramid>("/wsi/frames-pyramids/([0-9a-f-]+)/([0-9-]+)", true);
+    OrthancPlugins::RegisterRestCallback<ServeFrameTile>("/wsi/frames-tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
 
     OrthancPlugins::OrthancConfiguration mainConfiguration;
 
@@ -435,6 +556,7 @@ extern "C"
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
+    OrthancWSI::DecodedPyramidCache::FinalizeInstance();
     OrthancWSI::DicomPyramidCache::FinalizeInstance();
     OrthancWSI::RawTile::FinalizeTranscoderSemaphore();
   }


=====================================
ViewerPlugin/RawTile.cpp
=====================================
@@ -40,25 +40,6 @@ static std::unique_ptr<Orthanc::Semaphore>  transcoderSemaphore_;
 
 namespace OrthancWSI
 {
-  static ImageCompression Convert(Orthanc::MimeType type)
-  {
-    switch (type)
-    {
-      case Orthanc::MimeType_Png:
-        return ImageCompression_Png;
-
-      case Orthanc::MimeType_Jpeg:
-        return ImageCompression_Jpeg;
-
-      case Orthanc::MimeType_Jpeg2000:
-        return ImageCompression_Jpeg2000;
-
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
   Orthanc::ImageAccessor* RawTile::DecodeInternal()
   {
     switch (compression_)
@@ -106,7 +87,7 @@ namespace OrthancWSI
                                const Orthanc::ImageAccessor& decoded,
                                Orthanc::MimeType encoding)
   {
-    ImageToolbox::EncodeTile(encoded, decoded, Convert(encoding), 90 /* only used for JPEG */);
+    ImageToolbox::EncodeTile(encoded, decoded, ImageToolbox::Convert(encoding), 90 /* only used for JPEG */);
   }
 
 


=====================================
ViewerPlugin/viewer.js
=====================================
@@ -48,81 +48,105 @@ function GetUrlParameter(sParam)
 };
 
 
+function InitializePyramid(pyramid, tilesBaseUrl)
+{
+  $('#map').css('background', pyramid['BackgroundColor']);  // New in WSI 2.1
+
+  var width = pyramid['TotalWidth'];
+  var height = pyramid['TotalHeight'];
+  var countLevels = pyramid['Resolutions'].length;
+
+  // Maps always need a projection, but Zoomify layers are not geo-referenced, and
+  // are only measured in pixels.  So, we create a fake projection that the map
+  // can use to properly display the layer.
+  var proj = new ol.proj.Projection({
+    code: 'pixel',
+    units: 'pixels',
+    extent: [0, 0, width, height]
+  });
+
+  var extent = [0, -height, width, 0];
+
+  // Disable the rotation of the map, and inertia while panning
+  // http://stackoverflow.com/a/25682186
+  var interactions = ol.interaction.defaults({
+    altShiftDragRotate : false,
+    pinchRotate : false,
+    dragPan: false
+  }).extend([
+    new ol.interaction.DragPan({kinetic: false})
+  ]);
+
+  var layer = new ol.layer.Tile({
+    extent: extent,
+    source: new ol.source.TileImage({
+      projection: proj,
+      tileUrlFunction: function(tileCoord, pixelRatio, projection) {
+        return (tilesBaseUrl + (countLevels - 1 - tileCoord[0]) + '/' + tileCoord[1] + '/' + (-tileCoord[2] - 1));
+      },
+      tileGrid: new ol.tilegrid.TileGrid({
+        extent: extent,
+        resolutions: pyramid['Resolutions'].reverse(),
+        tileSizes: pyramid['TilesSizes'].reverse()
+      })
+    }),
+    wrapX: false,
+    projection: proj
+  });
+
+
+  var map = new ol.Map({
+    target: 'map',
+    layers: [ layer ],
+    view: new ol.View({
+      projection: proj,
+      center: [width / 2, -height / 2],
+      zoom: 0,
+      minResolution: 1   // Do not interpelate over pixels
+    }),
+    interactions: interactions
+  });
+
+  map.getView().fit(extent, map.getSize());
+}
+
 
 $(document).ready(function() {
   var seriesId = GetUrlParameter('series');
-  if (seriesId.length == 0)
-  {
-    alert('Error - No series ID specified!');
-  }
-  else
+  var instanceId = GetUrlParameter('instance');
+
+  if (seriesId.length != 0)
   {
     $.ajax({
       url : '../pyramids/' + seriesId,
       error: function() {
         alert('Error - Cannot get the pyramid structure of series: ' + seriesId);
       },
-      success : function(series) {
-        $('#map').css('background', series['BackgroundColor']);  // New in WSI 2.1
-
-        var width = series['TotalWidth'];
-        var height = series['TotalHeight'];
-        var countLevels = series['Resolutions'].length;
-
-        // Maps always need a projection, but Zoomify layers are not geo-referenced, and
-        // are only measured in pixels.  So, we create a fake projection that the map
-        // can use to properly display the layer.
-        var proj = new ol.proj.Projection({
-          code: 'pixel',
-          units: 'pixels',
-          extent: [0, 0, width, height]
-        });
-
-        var extent = [0, -height, width, 0];
-        
-        // Disable the rotation of the map, and inertia while panning
-        // http://stackoverflow.com/a/25682186
-        var interactions = ol.interaction.defaults({
-          altShiftDragRotate : false, 
-          pinchRotate : false,
-          dragPan: false
-        }).extend([
-          new ol.interaction.DragPan({kinetic: false})
-        ]);
-
-        var layer = new ol.layer.Tile({
-          extent: extent,
-          source: new ol.source.TileImage({
-            projection: proj,
-            tileUrlFunction: function(tileCoord, pixelRatio, projection) {
-              return ('../tiles/' + seriesId + '/' + 
-                      (countLevels - 1 - tileCoord[0]) + '/' + tileCoord[1] + '/' + (-tileCoord[2] - 1));
-            },
-            tileGrid: new ol.tilegrid.TileGrid({
-              extent: extent,
-              resolutions: series['Resolutions'].reverse(),
-              tileSizes: series['TilesSizes'].reverse()
-            })
-          }),
-          wrapX: false,
-          projection: proj
-        });
-
-
-        var map = new ol.Map({
-          target: 'map',
-          layers: [ layer ],
-          view: new ol.View({
-            projection: proj,
-            center: [width / 2, -height / 2],
-            zoom: 0,
-            minResolution: 1   // Do not interpelate over pixels
-          }),
-          interactions: interactions
-        });
-
-        map.getView().fit(extent, map.getSize());
+      success : function(pyramid) {
+        InitializePyramid(pyramid, '../tiles/' + seriesId + '/');
+      }
+    });
+  }
+  else if (instanceId.length != 0)
+  {
+    var frameNumber = GetUrlParameter('frame');
+    if (frameNumber.length == 0)
+    {
+      frameNumber = 0;
+    }
+
+    $.ajax({
+      url : '../frames-pyramids/' + instanceId + '/' + frameNumber,
+      error: function() {
+        alert('Error - Cannot get the pyramid structure of frame ' + frameNumber + ' of instance: ' + instanceId);
+      },
+      success : function(pyramid) {
+        InitializePyramid(pyramid, '../frames-tiles/' + instanceId + '/' + frameNumber + '/');
       }
     });
   }
+  else
+  {
+    alert('Error - No series ID and no instance ID specified!');
+  }
 });



View it on GitLab: https://salsa.debian.org/med-team/orthanc-wsi/-/commit/aa95bf05f1bbe552c2635e2d9c20724ae2ec8bef

-- 
View it on GitLab: https://salsa.debian.org/med-team/orthanc-wsi/-/commit/aa95bf05f1bbe552c2635e2d9c20724ae2ec8bef
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20241220/5777a91c/attachment-0001.htm>


More information about the debian-med-commit mailing list