Mercurial > hg > orthanc-stone
changeset 2173:4596ad1b2aa4 dicom-sr
integration mainline->dicom-sr
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 22 Oct 2024 15:56:08 +0200 |
parents | e65fe2e50fde (current diff) 239fb2c893c1 (diff) |
children | |
files | Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp |
diffstat | 18 files changed, 619 insertions(+), 269 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/StoneWebViewer/NEWS Fri Sep 27 22:34:17 2024 +0200 +++ b/Applications/StoneWebViewer/NEWS Tue Oct 22 15:56:08 2024 +0200 @@ -1,6 +1,12 @@ Pending changes in the mainline =============================== +* Automatically stretch to whole range for images without preset +* Improved support of the (0028,9132) tag for Philips multiframe images +* Remember the previous layout when re-opening the viewer. +* Added a Print button in the PDF viewer toolbar. +* Added a Download button in the PDF viewer toolbar. + Version 2.6 (2024-08-31) ========================
--- a/Applications/StoneWebViewer/Plugin/Plugin.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/Applications/StoneWebViewer/Plugin/Plugin.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -61,7 +61,7 @@ } std::string version = info["Version"].asString(); - if (version != "mainline") + if (version.find("mainline") != 0) { std::vector<std::string> tokens; Orthanc::Toolbox::TokenizeString(tokens, version, '.');
--- a/Applications/StoneWebViewer/WebApplication/app.js Fri Sep 27 22:34:17 2024 +0200 +++ b/Applications/StoneWebViewer/WebApplication/app.js Tue Oct 22 15:56:08 2024 +0200 @@ -960,7 +960,7 @@ this.layoutCountX = 1; this.layoutCountY = 2; } - + localStorage.setItem('layout', layout); this.FitContent(); }, @@ -1369,7 +1369,11 @@ mounted: function() { // Warning: In this function, the "stone" global object is not initialized yet! - this.SetViewportLayout('1x1'); + if (localStorage.layout) { + this.SetViewportLayout(localStorage.layout); + } else { + this.SetViewportLayout('1x1'); + } if (localStorage.settingNotDiagnostic) { this.settingNotDiagnostic = (localStorage.settingNotDiagnostic == '1');
--- a/Applications/StoneWebViewer/WebApplication/index.html Fri Sep 27 22:34:17 2024 +0200 +++ b/Applications/StoneWebViewer/WebApplication/index.html Tue Oct 22 15:56:08 2024 +0200 @@ -898,6 +898,14 @@ <div class="wv-overlay"> <div class="wv-overlay-bottomleft wvPrintExclude"> + <button class="btn btn-primary" @click="Download()" + data-toggle="tooltip" data-title="Download"> + <i class="fa fa-download"></i> + </button> + <button class="btn btn-primary" @click="Print()" + data-toggle="tooltip" data-title="Print"> + <i class="fa fa-print"></i> + </button> <button class="btn btn-primary" @click="FitWidth()" data-toggle="tooltip" data-title="Fit page width"> <i class="fas fa-text-width"></i>
--- a/Applications/StoneWebViewer/WebApplication/pdf-viewer.js Fri Sep 27 22:34:17 2024 +0200 +++ b/Applications/StoneWebViewer/WebApplication/pdf-viewer.js Tue Oct 22 15:56:08 2024 +0200 @@ -92,6 +92,46 @@ }); }, methods: { + Download: function() { + if (this.pdfDoc !== null) { + const blob = new Blob([this.pdf], { type: 'application/pdf'}); + const blobUrl = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = blobUrl; + a.download = "report.pdf"; + + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Revoke the object URL to free up memory + URL.revokeObjectURL(blobUrl); + } + }, + Print: function() { + if (this.pdfDoc !== null) { + if (0) { // works on Chrome but with a popup that is blocked by default ! + const blob = new Blob([this.pdf], { type: 'application/pdf'}); + const blobUrl = URL.createObjectURL(blob); + + let w = window.open(blobUrl, '_blank'); + w.print(); + } else { + // Let's open a new window with the pdf + // First we need to convert the pdf from a byte array to a binary string and then to b64 + let binaryStringPdf = ''; + for (let i = 0; i < this.pdf.length; i++) { + binaryStringPdf += String.fromCharCode(this.pdf[i]); + } + + const htmlContent = '<html><body style="margin: 0;"><embed width="100%" height="100%" src="data:application/pdf;base64,' + btoa(binaryStringPdf) + '" type="application/pdf" /></body></html>'; + + let w = window.open('', '_blank'); + w.document.write(htmlContent); + } + } + }, NextPage: function() { if (this.pdfDoc !== null && this.currentPage < this.pdfDoc.numPages) {
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -121,7 +121,8 @@ ThumbnailType_Pdf, ThumbnailType_Video, ThumbnailType_Loading, - ThumbnailType_Unknown + ThumbnailType_Unknown, + ThumbnailType_Unavailable }; @@ -211,6 +212,76 @@ +enum WindowingState +{ + WindowingState_None = 1, + WindowingState_Fallback = 2, + WindowingState_GlobalPreset = 3, + WindowingState_FramePreset = 4, + WindowingState_User = 5 +}; + + +class WindowingTracker +{ +private: + WindowingState state_; + OrthancStone::Windowing windowing_; + +public: + WindowingTracker() : + state_(WindowingState_None) + { + } + + WindowingState GetState() const + { + return state_; + } + + const OrthancStone::Windowing& GetWindowing() const + { + return windowing_; + } + + void Reset() + { + state_ = WindowingState_None; + windowing_ = OrthancStone::Windowing(); + } + + // Returns "true" iif. the windowing needed an update + bool Update(WindowingState newState, + const OrthancStone::Windowing& newWindowing) + { + if (newState == WindowingState_None) + { + // "Reset()" should have been called + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (newState >= state_) + { + state_ = newState; + + if (windowing_.IsNear(newWindowing)) + { + return false; + } + else + { + windowing_ = newWindowing; + return true; + } + } + else + { + return false; + } + } +}; + + class IFramesCollection : public boost::noncopyable { public: @@ -1535,6 +1606,79 @@ +class InstancesCache : public boost::noncopyable +{ +private: + // Maps "SOP Instance UID" to DICOM parameters + typedef std::map<std::string, OrthancStone::DicomInstanceParameters*> Content; + + Content content_; + + void Clear() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + content_.clear(); + } + +public: + ~InstancesCache() + { + Clear(); + } + + void Store(const std::string& sopInstanceUid, + const OrthancStone::DicomInstanceParameters& parameters) + { + Content::iterator found = content_.find(sopInstanceUid); + if (found == content_.end()) + { + content_[sopInstanceUid] = parameters.Clone(); + } + } + + class Accessor : public boost::noncopyable + { + private: + std::unique_ptr<OrthancStone::DicomInstanceParameters> parameters_; + + public: + Accessor(InstancesCache& that, + const std::string& sopInstanceUid) + { + Content::iterator found = that.content_.find(sopInstanceUid); + if (found != that.content_.end()) + { + assert(found->second != NULL); + parameters_.reset(found->second->Clone()); + } + } + + bool IsValid() const + { + return parameters_.get() != NULL; + } + + const OrthancStone::DicomInstanceParameters& GetParameters() const + { + if (IsValid()) + { + return *parameters_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + }; +}; + + + class SeriesCursor : public boost::noncopyable { public: @@ -2078,8 +2222,7 @@ const OrthancStone::Vector& normal) = 0; virtual void SignalWindowingUpdated(const ViewerViewport& viewport, - double windowingCenter, - double windowingWidth) = 0; + const OrthancStone::Windowing& windowing) = 0; virtual void SignalStoneAnnotationsChanged(const ViewerViewport& viewport, const std::string& sopInstanceUid, @@ -2189,41 +2332,18 @@ static_cast<double>(params.GetHeight())); } - GetViewport().windowingPresetCenters_.resize(params.GetWindowingPresetsCount()); - GetViewport().windowingPresetWidths_.resize(params.GetWindowingPresetsCount()); + GetViewport().windowingPresets_.resize(params.GetWindowingPresetsCount()); for (size_t i = 0; i < params.GetWindowingPresetsCount(); i++) { LOG(INFO) << "Preset windowing " << (i + 1) << "/" << params.GetWindowingPresetsCount() - << ": " << params.GetWindowingPresetCenter(i) - << "," << params.GetWindowingPresetWidth(i); - - GetViewport().windowingPresetCenters_[i] = params.GetWindowingPresetCenter(i); - GetViewport().windowingPresetWidths_[i] = params.GetWindowingPresetWidth(i); - } - - if (params.GetWindowingPresetsCount() == 0) - { - LOG(INFO) << "No preset windowing"; + << ": " << params.GetWindowingPreset(i).GetCenter() + << "," << params.GetWindowingPreset(i).GetWidth(); + + GetViewport().windowingPresets_[i] = params.GetWindowingPreset(i); } - uint32_t bitsStored, pixelRepresentation; - if (dicom.ParseUnsignedInteger32(bitsStored, Orthanc::DICOM_TAG_BITS_STORED) && - dicom.ParseUnsignedInteger32(pixelRepresentation, Orthanc::DICOM_TAG_PIXEL_REPRESENTATION)) - { - // Added in Stone Web viewer > 2.5 - const bool isSigned = (pixelRepresentation != 0); - const float maximum = powf(2.0, bitsStored); - GetViewport().windowingDefaultCenter_ = (isSigned ? 0.0f : maximum / 2.0f); - GetViewport().windowingDefaultWidth_ = maximum; - } - else - { - GetViewport().windowingDefaultCenter_ = 128; - GetViewport().windowingDefaultWidth_ = 256; - } - - GetViewport().SetWindowingPreset(); + GetViewport().SetDefaultWindowing(params); } uint32_t cineRate; @@ -2257,8 +2377,7 @@ private: std::string sopInstanceUid_; unsigned int frameNumber_; - float windowCenter_; - float windowWidth_; + OrthancStone::Windowing windowing_; bool isMonochrome1_; bool isPrefetch_; @@ -2266,15 +2385,13 @@ SetLowQualityFrame(boost::shared_ptr<ViewerViewport> viewport, const std::string& sopInstanceUid, unsigned int frameNumber, - float windowCenter, - float windowWidth, + const OrthancStone::Windowing& windowing, bool isMonochrome1, bool isPrefetch) : ICommand(viewport), sopInstanceUid_(sopInstanceUid), frameNumber_(frameNumber), - windowCenter_(windowCenter), - windowWidth_(windowWidth), + windowing_(windowing), isMonochrome1_(isMonochrome1), isPrefetch_(isPrefetch) { @@ -2320,9 +2437,11 @@ **/ - const float scaling = windowWidth_ / 255.0f; + const float center = static_cast<float>(windowing_.GetCenter()); + const float width = static_cast<float>(windowing_.GetWidth()); + const float scaling = width / 255.0f; const float offset = (OrthancStone::LinearAlgebra::IsCloseToZero(scaling) ? 0 : - (windowCenter_ - windowWidth_ / 2.0f) / scaling); + (center - width / 2.0f) / scaling); Orthanc::ImageProcessing::ShiftScale(*converted, offset, scaling, false); break; @@ -2408,17 +2527,6 @@ } else { - if (GetViewport().windowingPresetCenters_.empty()) - { - // New in Stone Web viewer 2.2: Deal with Philips multiframe - // (cf. mail from Tomas Kenda on 2021-08-17) - double windowingCenter, windowingWidth; - message.GetDicom().GetDefaultWindowing(windowingCenter, windowingWidth, frameNumber_); - GetViewport().windowingPresetCenters_.push_back(windowingCenter); - GetViewport().windowingPresetWidths_.push_back(windowingWidth); - GetViewport().SetWindowingPreset(); - } - Apply(GetViewport(), message.GetDicom(), frame.release(), sopInstanceUid_, frameNumber_); if (isPrefetch_) @@ -2440,6 +2548,7 @@ dicom.ExtractDicomSummary(tags, ORTHANC_STONE_MAX_TAG_LENGTH); OrthancStone::DicomInstanceParameters parameters(tags); + viewport.instancesCache_->Store(sopInstanceUid, parameters); std::unique_ptr<Orthanc::ImageAccessor> converted; @@ -2509,15 +2618,12 @@ boost::shared_ptr<OrthancStone::WebAssemblyViewport> viewport_; boost::shared_ptr<OrthancStone::DicomResourcesLoader> loader_; OrthancStone::DicomSource source_; - boost::shared_ptr<FramesCache> framesCache_; + boost::shared_ptr<FramesCache> framesCache_; + boost::shared_ptr<InstancesCache> instancesCache_; std::unique_ptr<IFramesCollection> frames_; std::unique_ptr<SeriesCursor> cursor_; - float windowingCenter_; - float windowingWidth_; - std::vector<float> windowingPresetCenters_; - std::vector<float> windowingPresetWidths_; - float windowingDefaultCenter_; - float windowingDefaultWidth_; + WindowingTracker windowingTracker_; + std::vector<OrthancStone::Windowing> windowingPresets_; unsigned int cineRate_; bool inverted_; bool fitNextContent_; @@ -2546,6 +2652,37 @@ std::string pendingSeriesInstanceUid_; + void UpdateWindowing(WindowingState state, + const OrthancStone::Windowing& windowing) + { + if (windowingTracker_.Update(state, windowing)) + { + UpdateCurrentTextureParameters(); + + if (observer_.get() != NULL) + { + observer_->SignalWindowingUpdated(*this, windowingTracker_.GetWindowing()); + } + } + } + + + void SetDefaultWindowing(const OrthancStone::DicomInstanceParameters& instance) + { + windowingTracker_.Reset(); + + if (instance.GetWindowingPresetsCount() == 0) + { + LOG(INFO) << "No preset windowing"; + UpdateWindowing(WindowingState_Fallback, instance.GetFallbackWindowing()); + } + else + { + UpdateWindowing(WindowingState_GlobalPreset, instance.GetWindowingPreset(0)); + } + } + + void ScheduleNextPrefetch() { while (!prefetchQueue_.empty()) @@ -2668,9 +2805,41 @@ case Orthanc::PixelFormat_Float32: { + { + // New in Stone Web viewer 2.2: Deal with Philips multiframe + // (cf. mail from Tomas Kenda on 2021-08-17) + InstancesCache::Accessor accessor(*instancesCache_, instance.GetSopInstanceUid()); + OrthancStone::Windowing windowing; + if (accessor.IsValid() && + accessor.GetParameters().LookupPerFrameWindowing(windowing, frameIndex)) + { + UpdateWindowing(WindowingState_FramePreset, windowing); + } + } + std::unique_ptr<OrthancStone::FloatTextureSceneLayer> tmp( new OrthancStone::FloatTextureSceneLayer(frame)); - tmp->SetCustomWindowing(windowingCenter_, windowingWidth_); + + if (windowingTracker_.GetState() == WindowingState_None || + windowingTracker_.GetState() == WindowingState_Fallback) + { + const Orthanc::ImageAccessor& texture = tmp->GetTexture(); + if (texture.GetFormat() != Orthanc::PixelFormat_Float32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + float minValue, maxValue; + Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, texture); + + const float center = (minValue + maxValue) / 2.0f; + const float width = maxValue - minValue; + UpdateWindowing(WindowingState_Fallback, OrthancStone::Windowing(center, width)); + } + } + + tmp->SetCustomWindowing(windowingTracker_.GetWindowing().GetCenter(), windowingTracker_.GetWindowing().GetWidth()); tmp->SetInverted(inverted_ ^ isMonochrome1); layer.reset(tmp.release()); break; @@ -3024,14 +3193,14 @@ std::map<std::string, std::string> headers, arguments; // arguments["quality"] = "10"; // Low-level quality for test purpose arguments["window"] = ( - boost::lexical_cast<std::string>(windowingCenter_) + "," + - boost::lexical_cast<std::string>(windowingWidth_) + ",linear"); + boost::lexical_cast<std::string>(windowingTracker_.GetWindowing().GetCenter()) + "," + + boost::lexical_cast<std::string>(windowingTracker_.GetWindowing().GetWidth()) + ",linear"); std::unique_ptr<OrthancStone::IOracleCommand> command( source_.CreateDicomWebCommand( uri, arguments, headers, new SetLowQualityFrame( GetSharedObserver(), instance.GetSopInstanceUid(), frameNumber, - windowingCenter_, windowingWidth_, isMonochrome1, isPrefetch))); + windowingTracker_.GetWindowing(), isMonochrome1, isPrefetch))); { std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock()); @@ -3051,7 +3220,7 @@ { dynamic_cast<OrthancStone::FloatTextureSceneLayer&>( lock->GetController().GetScene().GetLayer(LAYER_TEXTURE)). - SetCustomWindowing(windowingCenter_, windowingWidth_); + SetCustomWindowing(windowingTracker_.GetWindowing().GetCenter(), windowingTracker_.GetWindowing().GetWidth()); } lock->Invalidate(); @@ -3062,13 +3231,13 @@ const OrthancStone::DicomSource& source, const std::string& canvas, boost::shared_ptr<FramesCache> cache, + boost::shared_ptr<InstancesCache> instancesCache, bool softwareRendering, bool linearInterpolation) : context_(context), source_(source), framesCache_(cache), - windowingDefaultCenter_(128), - windowingDefaultWidth_(256), + instancesCache_(instancesCache), fitNextContent_(true), hasFocusOnInstance_(false), focusFrameNumber_(0), @@ -3105,8 +3274,6 @@ emscripten_set_wheel_callback(viewport_->GetCanvasCssSelector().c_str(), this, true, OnWheel); - SetWindowingPreset(); - stoneAnnotations_.reset(new OrthancStone::AnnotationsSceneLayer(LAYER_ANNOTATIONS_STONE)); stoneAnnotations_->SetProbedLayer(LAYER_TEXTURE); } @@ -3115,13 +3282,7 @@ void Handle(const OrthancStone::ViewportController::GrayscaleWindowingChanged& message) { // This event is triggered by the windowing mouse action, from class "GrayscaleWindowingSceneTracker" - windowingCenter_ = message.GetWindowingCenter(); - windowingWidth_ = message.GetWindowingWidth(); - - if (observer_.get() != NULL) - { - observer_->SignalWindowingUpdated(*this, message.GetWindowingCenter(), message.GetWindowingWidth()); - } + UpdateWindowing(WindowingState_User, message.GetWindowing()); } @@ -3264,12 +3425,13 @@ static boost::shared_ptr<ViewerViewport> Create(OrthancStone::WebAssemblyLoadersContext& context, const OrthancStone::DicomSource& source, const std::string& canvas, - boost::shared_ptr<FramesCache> cache, + boost::shared_ptr<FramesCache> framesCache, + boost::shared_ptr<InstancesCache> instancesCache, bool softwareRendering, bool linearInterpolation) { boost::shared_ptr<ViewerViewport> viewport( - new ViewerViewport(context, source, canvas, cache, softwareRendering, linearInterpolation)); + new ViewerViewport(context, source, canvas, framesCache, instancesCache, softwareRendering, linearInterpolation)); { std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context.Lock()); @@ -3321,7 +3483,7 @@ frames_.reset(frames); cursor_.reset(new SeriesCursor(frames_->GetFramesCount(), false)); - + if (frames_->GetFramesCount() != 0) { const OrthancStone::DicomInstanceParameters& firstInstance = frames_->GetInstanceOfFrame(0); @@ -3341,11 +3503,16 @@ cursor_.reset(new SeriesCursor(frames_->GetFramesCount(), true)); } } + + SetDefaultWindowing(firstInstance); + } + else + { + windowingTracker_.Reset(); } LOG(INFO) << "Number of frames in series: " << frames_->GetFramesCount(); - SetWindowingPreset(); ClearViewport(); prefetchQueue_.clear(); @@ -3615,33 +3782,6 @@ } - void SetWindowingPreset() - { - assert(windowingPresetCenters_.size() == windowingPresetWidths_.size()); - - if (windowingPresetCenters_.empty()) - { - SetWindowing(windowingDefaultCenter_, windowingDefaultWidth_); - } - else - { - SetWindowing(windowingPresetCenters_[0], windowingPresetWidths_[0]); - } - } - - void SetWindowing(float windowingCenter, - float windowingWidth) - { - windowingCenter_ = windowingCenter; - windowingWidth_ = windowingWidth; - UpdateCurrentTextureParameters(); - - if (observer_.get() != NULL) - { - observer_->SignalWindowingUpdated(*this, windowingCenter, windowingWidth); - } - } - void StretchWindowing() { float minValue, maxValue; @@ -3667,7 +3807,9 @@ Orthanc::ImageProcessing::GetMinMaxFloatValue(minValue, maxValue, texture); } - SetWindowing((minValue + maxValue) / 2.0f, maxValue - minValue); + const float center = (minValue + maxValue) / 2.0f; + const float width = maxValue - minValue; + UpdateWindowing(WindowingState_User, OrthancStone::Windowing(center, width)); } void FlipX() @@ -3959,17 +4101,15 @@ void FormatWindowingPresets(Json::Value& target) const { - assert(windowingPresetCenters_.size() == windowingPresetWidths_.size()); - target = Json::arrayValue; - for (size_t i = 0; i < windowingPresetCenters_.size(); i++) - { - const float c = windowingPresetCenters_[i]; - const float w = windowingPresetWidths_[i]; + for (size_t i = 0; i < windowingPresets_.size(); i++) + { + const double c = windowingPresets_[i].GetCenter(); + const double w = windowingPresets_[i].GetWidth(); std::string name = "Preset"; - if (windowingPresetCenters_.size() > 1) + if (windowingPresets_.size() > 1) { name += " " + boost::lexical_cast<std::string>(i + 1); } @@ -4052,6 +4192,12 @@ } + void SetUserWindowing(const OrthancStone::Windowing& windowing) + { + UpdateWindowing(WindowingState_User, windowing); + } + + void SetPendingSeriesInstanceUid(const std::string& seriesInstanceUid) { pendingSeriesInstanceUid_ = seriesInstanceUid; @@ -4274,8 +4420,7 @@ } virtual void SignalWindowingUpdated(const ViewerViewport& viewport, - double windowingCenter, - double windowingWidth) ORTHANC_OVERRIDE + const OrthancStone::Windowing& windowing) ORTHANC_OVERRIDE { EM_ASM({ const customEvent = document.createEvent("CustomEvent"); @@ -4286,8 +4431,8 @@ window.dispatchEvent(customEvent); }, viewport.GetCanvasId().c_str(), - static_cast<int>(boost::math::iround<double>(windowingCenter)), - static_cast<int>(boost::math::iround<double>(windowingWidth))); + static_cast<int>(boost::math::iround<double>(windowing.GetCenter())), + static_cast<int>(boost::math::iround<double>(windowing.GetWidth()))); UpdateReferenceLines(); } @@ -4359,6 +4504,7 @@ static OrthancStone::DicomSource source_; static boost::shared_ptr<FramesCache> framesCache_; +static boost::shared_ptr<InstancesCache> instancesCache_; static boost::shared_ptr<OrthancStone::WebAssemblyLoadersContext> context_; static std::string stringBuffer_; static bool softwareRendering_ = false; @@ -4409,7 +4555,7 @@ if (found == allViewports_.end()) { boost::shared_ptr<ViewerViewport> viewport( - ViewerViewport::Create(*context_, source_, canvas, framesCache_, softwareRendering_, linearInterpolation_)); + ViewerViewport::Create(*context_, source_, canvas, framesCache_, instancesCache_, softwareRendering_, linearInterpolation_)); viewport->SetMouseButtonActions(leftButtonAction_, middleButtonAction_, rightButtonAction_); viewport->AcquireObserver(new WebAssemblyObserver); viewport->SetOsiriXAnnotations(osiriXAnnotations_); @@ -4460,6 +4606,7 @@ context_->SetDicomCacheSize(128 * 1024 * 1024); // 128MB framesCache_.reset(new FramesCache); + instancesCache_.reset(new InstancesCache); osiriXAnnotations_.reset(new OrthancStone::OsiriX::CollectionOfAnnotations); DISPATCH_JAVASCRIPT_EVENT("StoneInitialized"); @@ -4891,7 +5038,7 @@ { try { - GetViewport(canvas)->SetWindowing(center, width); + GetViewport(canvas)->SetUserWindowing(OrthancStone::Windowing(center, width)); } EXTERN_CATCH_EXCEPTIONS; }
--- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake Tue Oct 22 15:56:08 2024 +0200 @@ -451,6 +451,8 @@ ${ORTHANC_STONE_ROOT}/Toolbox/UndoRedoStack.h ${ORTHANC_STONE_ROOT}/Toolbox/UnionOfRectangles.cpp ${ORTHANC_STONE_ROOT}/Toolbox/UnionOfRectangles.h + ${ORTHANC_STONE_ROOT}/Toolbox/Windowing.cpp + ${ORTHANC_STONE_ROOT}/Toolbox/Windowing.h ${ORTHANC_STONE_ROOT}/Viewport/DefaultViewportInteractor.cpp ${ORTHANC_STONE_ROOT}/Viewport/IViewport.h
--- a/OrthancStone/Sources/Loaders/SeriesFramesLoader.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Sources/Loaders/SeriesFramesLoader.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -47,8 +47,7 @@ std::string sopInstanceUid_; // Only used for debug purpose unsigned int quality_; bool hasWindowing_; - float windowingCenter_; - float windowingWidth_; + Windowing windowing_; std::unique_ptr<Orthanc::IDynamicObject> userPayload_; public: @@ -62,8 +61,6 @@ sopInstanceUid_(sopInstanceUid), quality_(quality), hasWindowing_(false), - windowingCenter_(0), - windowingWidth_(0), userPayload_(userPayload) { } @@ -83,12 +80,10 @@ return quality_; } - void SetWindowing(float center, - float width) + void SetWindowing(const Windowing& windowing) { hasWindowing_ = true; - windowingCenter_ = center; - windowingWidth_ = width; + windowing_ = windowing; } bool HasWindowing() const @@ -96,23 +91,11 @@ return hasWindowing_; } - float GetWindowingCenter() const + const Windowing& GetWindowing() const { if (hasWindowing_) { - return windowingCenter_; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - } - - float GetWindowingWidth() const - { - if (hasWindowing_) - { - return windowingWidth_; + return windowing_; } else { @@ -227,13 +210,13 @@ Orthanc::Image scaled(parameters.GetExpectedPixelFormat(), reader.GetWidth(), reader.GetHeight(), false); Orthanc::ImageProcessing::Convert(scaled, reader); - float w = payload.GetWindowingWidth(); + float w = static_cast<float>(payload.GetWindowing().GetWidth()); if (w <= 0.01f) { w = 0.01f; // Prevent division by zero } - const float c = payload.GetWindowingCenter(); + const float c = static_cast<float>(payload.GetWindowing().GetCenter()); const float scaling = w / 255.0f; const float offset = (c - w / 2.0f) / scaling; @@ -417,16 +400,15 @@ { const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); - float c, w; - parameters.GetWindowingPresetsUnion(c, w); + Windowing windowing = parameters.GetWindowingPresetsUnion(); std::map<std::string, std::string> arguments, headers; - arguments["window"] = (boost::lexical_cast<std::string>(c) + "," + - boost::lexical_cast<std::string>(w) + ",linear"); + arguments["window"] = (boost::lexical_cast<std::string>(windowing.GetCenter()) + "," + + boost::lexical_cast<std::string>(windowing.GetWidth()) + ",linear"); headers["Accept"] = "image/jpeg"; std::unique_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); - payload->SetWindowing(c, w); + payload->SetWindowing(windowing); { std::unique_ptr<ILoadersContext::ILock> lock(context_.Lock());
--- a/OrthancStone/Sources/Scene2D/GrayscaleWindowingSceneTracker.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Sources/Scene2D/GrayscaleWindowingSceneTracker.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -89,7 +89,7 @@ { if (lock_.get() != NULL) { - lock_->GetController().BroadcastGrayscaleWindowingChanged(center, width); + lock_->GetController().BroadcastGrayscaleWindowingChanged(Windowing(center, width)); } } };
--- a/OrthancStone/Sources/Scene2DViewport/ViewportController.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Sources/Scene2DViewport/ViewportController.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -151,10 +151,9 @@ BroadcastMessage(SceneTransformChanged(*this)); } - void ViewportController::BroadcastGrayscaleWindowingChanged(double windowingCenter, - double windowingWidth) + void ViewportController::BroadcastGrayscaleWindowingChanged(const Windowing& windowing) { - BroadcastMessage(GrayscaleWindowingChanged(*this, windowingCenter, windowingWidth)); + BroadcastMessage(GrayscaleWindowingChanged(*this, windowing)); } void ViewportController::FitContent(unsigned int viewportWidth,
--- a/OrthancStone/Sources/Scene2DViewport/ViewportController.h Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Sources/Scene2DViewport/ViewportController.h Tue Oct 22 15:56:08 2024 +0200 @@ -27,6 +27,7 @@ #include "../Messages/IObservable.h" #include "../Scene2D/Scene2D.h" #include "../Scene2DViewport/IFlexiblePointerTracker.h" +#include "../Toolbox/Windowing.h" #include "../Viewport/IViewportInteractor.h" #include <Compatibility.h> @@ -94,27 +95,19 @@ ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); private: - double windowingCenter_; - double windowingWidth_; + Windowing windowing_; public: GrayscaleWindowingChanged(const ViewportController& origin, - double windowingCenter, - double windowingWidth) : + const Windowing& windowing) : OriginMessage(origin), - windowingCenter_(windowingCenter), - windowingWidth_(windowingWidth) + windowing_(windowing) { } - double GetWindowingCenter() const + const Windowing& GetWindowing() const { - return windowingCenter_; - } - - double GetWindowingWidth() const - { - return windowingWidth_; + return windowing_; } }; @@ -155,8 +148,7 @@ void SetSceneToCanvasTransform(const AffineTransform2D& transform); /** Info broadcasted to the observers */ - void BroadcastGrayscaleWindowingChanged(double windowingCenter, - double windowingWidth); + void BroadcastGrayscaleWindowingChanged(const Windowing& windowing); /** Forwarded to the underlying scene, and broadcasted to the observers */ void FitContent(unsigned int viewportWidth,
--- a/OrthancStone/Sources/Toolbox/DebugDrawing2D.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Sources/Toolbox/DebugDrawing2D.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -23,6 +23,8 @@ #include "DebugDrawing2D.h" +#include <stdio.h> + namespace OrthancStone {
--- a/OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Sources/Toolbox/DicomInstanceParameters.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -32,6 +32,7 @@ #include <Images/ImageProcessing.h> #include <Logging.h> #include <OrthancException.h> +#include <SerializationToolbox.h> #include <Toolbox.h> @@ -190,29 +191,29 @@ } } - bool ok = false; + + windowingPresets_.clear(); + + Vector centers, widths; - if (LinearAlgebra::ParseVector(windowingPresetCenters_, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) && - LinearAlgebra::ParseVector(windowingPresetWidths_, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH)) + if (LinearAlgebra::ParseVector(centers, dicom, Orthanc::DICOM_TAG_WINDOW_CENTER) && + LinearAlgebra::ParseVector(widths, dicom, Orthanc::DICOM_TAG_WINDOW_WIDTH)) { - if (windowingPresetCenters_.size() == windowingPresetWidths_.size()) + if (centers.size() == widths.size()) { - ok = true; + windowingPresets_.resize(centers.size()); + + for (size_t i = 0; i < centers.size(); i++) + { + windowingPresets_[i] = Windowing(centers[i], widths[i]); + } } else { LOG(ERROR) << "Mismatch in the number of preset windowing widths/centers, ignoring this"; - ok = false; } } - if (!ok) - { - // Don't use "Vector::clear()", as it has not the same meaning as "std::vector::clear()" - windowingPresetCenters_.resize(0); - windowingPresetWidths_.resize(0); - } - // This computes the "IndexInSeries" metadata from Orthanc (check // out "Orthanc::ServerIndex::Store()") hasIndexInSeries_ = ( @@ -230,6 +231,65 @@ { instanceNumber_ = 0; } + + + static const Orthanc::DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE(0x5200, 0x9230); + static const Orthanc::DicomTag DICOM_TAG_FRAME_VOI_LUT_SEQUENCE_ATTRIBUTE(0x0028, 0x9132); + + const Orthanc::DicomValue* frames = dicom.TestAndGetValue(DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE); + if (frames != NULL && + hasNumberOfFrames_ && + frames->IsSequence()) + { + /** + * New in Stone Web viewer 2.2: Deal with Philips multiframe + * (cf. mail from Tomas Kenda on 2021-08-17). This cannot be done + * in LoadSeriesDetailsFromInstance, as the "Per Frame Functional Groups Sequence" + * is not available at that point. + **/ + + const Json::Value& sequence = frames->GetSequenceContent(); + + perFrameWindowing_.resize(numberOfFrames_); + + // This corresponds to "ParsedDicomFile::GetDefaultWindowing()" + for (Json::ArrayIndex i = 0; i < sequence.size(); i++) + { + if (i < numberOfFrames_ && + sequence[i].isMember(DICOM_TAG_FRAME_VOI_LUT_SEQUENCE_ATTRIBUTE.Format())) + { + const Json::Value& v = sequence[i][DICOM_TAG_FRAME_VOI_LUT_SEQUENCE_ATTRIBUTE.Format()]; + + static const char* KEY_VALUE = "Value"; + + if (v.isMember(KEY_VALUE) && + v[KEY_VALUE].type() == Json::arrayValue && + v[KEY_VALUE].size() >= 1 && + v[KEY_VALUE][0].isMember(Orthanc::DICOM_TAG_WINDOW_CENTER.Format()) && + v[KEY_VALUE][0].isMember(Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()) && + v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()].isMember(KEY_VALUE) && + v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()].isMember(KEY_VALUE)) + { + const Json::Value& scenter = v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_CENTER.Format()][KEY_VALUE]; + const Json::Value& swidth = v[KEY_VALUE][0][Orthanc::DICOM_TAG_WINDOW_WIDTH.Format()][KEY_VALUE]; + + double center, width; + if (scenter.isString() && + swidth.isString() && + Orthanc::SerializationToolbox::ParseDouble(center, scenter.asString()) && + Orthanc::SerializationToolbox::ParseDouble(width, swidth.asString())) + { + perFrameWindowing_[i] = Windowing(center, width); + } + else if (scenter.isNumeric() && + swidth.isNumeric()) + { + perFrameWindowing_[i] = Windowing(scenter.asDouble(), swidth.asDouble()); + } + } + } + } + } } @@ -399,18 +459,45 @@ } + Windowing DicomInstanceParameters::GetFallbackWindowing() const + { + double a, b; + if (tags_->ParseDouble(a, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE) && + tags_->ParseDouble(b, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE)) + { + const double center = (a + b) / 2.0f; + const double width = (b - a); + return Windowing(center, width); + } + + // Added in Stone Web viewer > 2.5 + uint32_t bitsStored, pixelRepresentation; + if (tags_->ParseUnsignedInteger32(bitsStored, Orthanc::DICOM_TAG_BITS_STORED) && + tags_->ParseUnsignedInteger32(pixelRepresentation, Orthanc::DICOM_TAG_PIXEL_REPRESENTATION)) + { + const bool isSigned = (pixelRepresentation != 0); + const float maximum = powf(2.0, bitsStored); + return Windowing(isSigned ? 0.0f : maximum / 2.0f, maximum); + } + else + { + // Cannot infer a suitable windowing from the available tags + return Windowing(); + } + } + + size_t DicomInstanceParameters::GetWindowingPresetsCount() const { - assert(data_.windowingPresetCenters_.size() == data_.windowingPresetWidths_.size()); - return data_.windowingPresetCenters_.size(); + return data_.windowingPresets_.size(); } - float DicomInstanceParameters::GetWindowingPresetCenter(size_t i) const + Windowing DicomInstanceParameters::GetWindowingPreset(size_t i) const { if (i < GetWindowingPresetsCount()) { - return static_cast<float>(data_.windowingPresetCenters_[i]); + return data_.windowingPresets_[i]; } else { @@ -418,32 +505,8 @@ } } - - float DicomInstanceParameters::GetWindowingPresetWidth(size_t i) const - { - if (i < GetWindowingPresetsCount()) - { - return static_cast<float>(data_.windowingPresetWidths_[i]); - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - } - - - static void GetWindowingBounds(float& low, - float& high, - double center, // in - double width) // in - { - low = static_cast<float>(center - width / 2.0); - high = static_cast<float>(center + width / 2.0); - } - - void DicomInstanceParameters::GetWindowingPresetsUnion(float& center, - float& width) const + Windowing DicomInstanceParameters::GetWindowingPresetsUnion() const { assert(tags_.get() != NULL); size_t s = GetWindowingPresetsCount(); @@ -452,48 +515,29 @@ { // Use the largest windowing given all the preset windowings // that are available in the DICOM tags - float low, high; - GetWindowingBounds(low, high, GetWindowingPresetCenter(0), GetWindowingPresetWidth(0)); + double low, high; + GetWindowingPreset(0).GetBounds(low, high); for (size_t i = 1; i < s; i++) { - float a, b; - GetWindowingBounds(a, b, GetWindowingPresetCenter(i), GetWindowingPresetWidth(i)); + double a, b; + GetWindowingPreset(i).GetBounds(a, b); low = std::min(low, a); high = std::max(high, b); } assert(low <= high); - if (LinearAlgebra::IsNear(low, high)) + if (!LinearAlgebra::IsNear(low, high)) { - // Cannot infer a suitable windowing from the available tags - center = 128.0f; - width = 256.0f; - } - else - { - center = (low + high) / 2.0f; - width = (high - low); + const double center = (low + high) / 2.0f; + const double width = (high - low); + return Windowing(center, width); } } - else - { - float a, b; - if (tags_->ParseFloat(a, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE) && - tags_->ParseFloat(b, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE) && - a < b) - { - center = (a + b) / 2.0f; - width = (b - a); - } - else - { - // Cannot infer a suitable windowing from the available tags - center = 128.0f; - width = 256.0f; - } - } + + // No preset, or presets with an empty range + return GetFallbackWindowing(); } @@ -565,7 +609,8 @@ if (GetWindowingPresetsCount() > 0) { - floatTexture.SetCustomWindowing(GetWindowingPresetCenter(0), GetWindowingPresetWidth(0)); + Windowing preset = GetWindowingPreset(0); + floatTexture.SetCustomWindowing(preset.GetCenter(), preset.GetWidth()); } switch (GetImageInformation().GetPhotometricInterpretation()) @@ -839,4 +884,19 @@ return (data_.frameOffsets_[0] > data_.frameOffsets_[1]); } } + + + bool DicomInstanceParameters::LookupPerFrameWindowing(Windowing& windowing, + unsigned int frame) const + { + if (frame < data_.perFrameWindowing_.size()) + { + windowing = data_.perFrameWindowing_[frame]; + return true; + } + else + { + return false; + } + } }
--- a/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/Sources/Toolbox/DicomInstanceParameters.h Tue Oct 22 15:56:08 2024 +0200 @@ -26,6 +26,7 @@ #include "../Scene2D/LookupTableTextureSceneLayer.h" #include "../StoneEnumerations.h" #include "../Toolbox/CoordinateSystem3D.h" +#include "Windowing.h" #include <IDynamicObject.h> #include <DicomFormat/DicomImageInformation.h> @@ -57,8 +58,7 @@ bool hasRescale_; double rescaleIntercept_; double rescaleSlope_; - Vector windowingPresetCenters_; - Vector windowingPresetWidths_; + std::vector<Windowing> windowingPresets_; bool hasIndexInSeries_; unsigned int indexInSeries_; std::string doseUnits_; @@ -67,6 +67,7 @@ bool hasPixelSpacing_; bool hasNumberOfFrames_; int32_t instanceNumber_; + std::vector<Windowing> perFrameWindowing_; explicit Data(const Orthanc::DicomMap& dicom); }; @@ -185,14 +186,13 @@ double GetRescaleSlope() const; + Windowing GetFallbackWindowing() const; + size_t GetWindowingPresetsCount() const; - float GetWindowingPresetCenter(size_t i) const; + Windowing GetWindowingPreset(size_t i) const; - float GetWindowingPresetWidth(size_t i) const; - - void GetWindowingPresetsUnion(float& center, - float& width) const; + Windowing GetWindowingPresetsUnion() const; Orthanc::PixelFormat GetExpectedPixelFormat() const; @@ -267,5 +267,8 @@ CoordinateSystem3D GetMultiFrameGeometry() const; bool IsReversedFrameOffsets() const; + + bool LookupPerFrameWindowing(Windowing& windowing, + unsigned int frame) const; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Toolbox/Windowing.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -0,0 +1,52 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#include "Windowing.h" + +#include "LinearAlgebra.h" + + +namespace OrthancStone +{ + Windowing::Windowing(double center, + double width) + { + center_ = center; + width_ = std::abs(width); + } + + + void Windowing::GetBounds(double& low, + double& high) const + { + low = center_ - width_ / 2.0; + high = center_ + width_ / 2.0; + } + + + bool Windowing::IsNear(const Windowing& other) const + { + return (LinearAlgebra::IsNear(center_, other.center_) && + LinearAlgebra::IsNear(width_, other.width_)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Toolbox/Windowing.h Tue Oct 22 15:56:08 2024 +0200 @@ -0,0 +1,59 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +namespace OrthancStone +{ + class Windowing + { + private: + double center_; + double width_; + + public: + Windowing() : + center_(128), + width_(256) + { + } + + Windowing(double center, + double width); + + double GetCenter() const + { + return center_; + } + + double GetWidth() const + { + return width_; + } + + void GetBounds(double& low, + double& high) const; + + bool IsNear(const Windowing& other) const; + }; +}
--- a/OrthancStone/UnitTestsSources/DicomTests.cpp Fri Sep 27 22:34:17 2024 +0200 +++ b/OrthancStone/UnitTestsSources/DicomTests.cpp Tue Oct 22 15:56:08 2024 +0200 @@ -66,13 +66,11 @@ ASSERT_THROW(p->GetRescaleIntercept(), Orthanc::OrthancException); ASSERT_THROW(p->GetRescaleSlope(), Orthanc::OrthancException); ASSERT_EQ(0u, p->GetWindowingPresetsCount()); - ASSERT_THROW(p->GetWindowingPresetCenter(0), Orthanc::OrthancException); - ASSERT_THROW(p->GetWindowingPresetWidth(0), Orthanc::OrthancException); + ASSERT_THROW(p->GetWindowingPreset(0), Orthanc::OrthancException); - float c, w; - p->GetWindowingPresetsUnion(c, w); - ASSERT_FLOAT_EQ(128.0f, c); - ASSERT_FLOAT_EQ(256.0f, w); + OrthancStone::Windowing w = p->GetWindowingPresetsUnion(); + ASSERT_FLOAT_EQ(128.0f, w.GetCenter()); + ASSERT_FLOAT_EQ(256.0f, w.GetWidth()); ASSERT_THROW(p->GetExpectedPixelFormat(), Orthanc::OrthancException); ASSERT_FALSE(p->HasIndexInSeries()); @@ -96,20 +94,19 @@ OrthancStone::DicomInstanceParameters p(m); ASSERT_EQ(3u, p.GetWindowingPresetsCount()); - ASSERT_FLOAT_EQ(10, p.GetWindowingPresetCenter(0)); - ASSERT_FLOAT_EQ(100, p.GetWindowingPresetCenter(1)); - ASSERT_FLOAT_EQ(1000, p.GetWindowingPresetCenter(2)); - ASSERT_FLOAT_EQ(50, p.GetWindowingPresetWidth(0)); - ASSERT_FLOAT_EQ(60, p.GetWindowingPresetWidth(1)); - ASSERT_FLOAT_EQ(70, p.GetWindowingPresetWidth(2)); + ASSERT_FLOAT_EQ(10, p.GetWindowingPreset(0).GetCenter()); + ASSERT_FLOAT_EQ(100, p.GetWindowingPreset(1).GetCenter()); + ASSERT_FLOAT_EQ(1000, p.GetWindowingPreset(2).GetCenter()); + ASSERT_FLOAT_EQ(50, p.GetWindowingPreset(0).GetWidth()); + ASSERT_FLOAT_EQ(60, p.GetWindowingPreset(1).GetWidth()); + ASSERT_FLOAT_EQ(70, p.GetWindowingPreset(2).GetWidth()); const float a = 10.0f - 50.0f / 2.0f; const float b = 1000.0f + 70.0f / 2.0f; - float c, w; - p.GetWindowingPresetsUnion(c, w); - ASSERT_FLOAT_EQ((a + b) / 2.0f, c); - ASSERT_FLOAT_EQ(b - a, w); + OrthancStone::Windowing w = p.GetWindowingPresetsUnion(); + ASSERT_FLOAT_EQ((a + b) / 2.0f, w.GetCenter()); + ASSERT_FLOAT_EQ(b - a, w.GetWidth()); }
--- a/TODO Fri Sep 27 22:34:17 2024 +0200 +++ b/TODO Tue Oct 22 15:56:08 2024 +0200 @@ -45,9 +45,6 @@ * Order the studies in the left column according to their Instance Number (0020,0013). Suggestion by Joseph Maratt. -* Add a button to download PDF: - https://discourse.orthanc-server.org/t/printing-pdf-reports-in-stone-of-orthanc/4731 - * Open using Orthanc parent/study/series identifier, or using DICOM accession number: https://discourse.orthanc-server.org/t/stone-web-viewer-what-is-the-link-to-use-through-the-access-number/4808