[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