# HG changeset patch # User Sebastien Jodogne # Date 1699958717 -3600 # Node ID 9f7604d6b58126eb7b39804d7e23fe045b77a6df # Parent a6d5373e471c3a3eb96c40c872b3f25e9bdd358b# Parent 517ef20aef74d429a52c2b1ea4c8ac6f9a6085cd integration mainline->deep-learning diff -r a6d5373e471c -r 9f7604d6b581 Applications/Samples/WebAssembly/docker-build.sh --- a/Applications/Samples/WebAssembly/docker-build.sh Wed Oct 11 17:10:45 2023 +0200 +++ b/Applications/Samples/WebAssembly/docker-build.sh Tue Nov 14 11:45:17 2023 +0100 @@ -43,6 +43,7 @@ mkdir -p ${ROOT_DIR}/wasm-binaries docker run -t ${DOCKER_FLAGS} --rm \ + --dns=8.8.8.8 \ --user $(id -u):$(id -g) \ -v ${ROOT_DIR}:/source:ro \ -v ${ROOT_DIR}/wasm-binaries:/target:rw ${IMAGE} \ diff -r a6d5373e471c -r 9f7604d6b581 Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp --- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Wed Oct 11 17:10:45 2023 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Tue Nov 14 11:45:17 2023 +0100 @@ -204,6 +204,84 @@ +class IFramesCollection : public boost::noncopyable +{ +public: + virtual ~IFramesCollection() + { + } + + virtual size_t GetFramesCount() const = 0; + + virtual const OrthancStone::DicomInstanceParameters& GetInstanceOfFrame(size_t frameIndex) const = 0; + + virtual unsigned int GetFrameNumberInInstance(size_t frameIndex) const = 0; + + virtual bool LookupFrame(size_t& frameIndex, + const std::string& sopInstanceUid, + unsigned int frameNumber) const = 0; + + virtual bool FindClosestFrame(size_t& frameIndex, + const OrthancStone::Vector& point, + double maximumDistance) const = 0; + + static OrthancStone::CoordinateSystem3D GetFrameGeometry(const IFramesCollection& frames, + size_t frameIndex) + { + return frames.GetInstanceOfFrame(frameIndex).GetFrameGeometry(frames.GetFrameNumberInInstance(frameIndex)); + } +}; + + +class SortedFramesCollection : public IFramesCollection +{ +private: + std::unique_ptr frames_; + +public: + SortedFramesCollection(OrthancStone::SortedFrames* frames) + { + if (frames == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + frames_.reset(frames); + } + } + + virtual size_t GetFramesCount() const ORTHANC_OVERRIDE + { + return frames_->GetFramesCount(); + } + + const OrthancStone::DicomInstanceParameters& GetInstanceOfFrame(size_t frameIndex) const ORTHANC_OVERRIDE + { + return frames_->GetInstanceOfFrame(frameIndex); + } + + virtual unsigned int GetFrameNumberInInstance(size_t frameIndex) const ORTHANC_OVERRIDE + { + return frames_->GetFrameNumberInInstance(frameIndex); + } + + virtual bool LookupFrame(size_t& frameIndex, + const std::string& sopInstanceUid, + unsigned int frameNumber) const ORTHANC_OVERRIDE + { + return frames_->LookupFrame(frameIndex, sopInstanceUid, frameNumber); + } + + virtual bool FindClosestFrame(size_t& frameIndex, + const OrthancStone::Vector& point, + double maximumDistance) const ORTHANC_OVERRIDE + { + return frames_->FindClosestFrame(frameIndex, point, maximumDistance); + }; +}; + + class VirtualSeries : public boost::noncopyable { private: @@ -741,32 +819,31 @@ } } - bool SortSeriesFrames(OrthancStone::SortedFrames& target, - const std::string& seriesInstanceUid) const + IFramesCollection* GetSeriesFrames(const std::string& seriesInstanceUid) const { OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid); if (accessor.IsComplete()) { - target.Clear(); + std::unique_ptr target(new OrthancStone::SortedFrames); + target->Clear(); for (size_t i = 0; i < accessor.GetInstancesCount(); i++) { - target.AddInstance(accessor.GetInstance(i)); + target->AddInstance(accessor.GetInstance(i)); } - target.Sort(); + target->Sort(); - return true; + return new SortedFramesCollection(target.release()); } else { - return false; - } - } - - bool SortVirtualSeriesFrames(OrthancStone::SortedFrames& target, - const std::string& virtualSeriesId) const + return NULL; + } + } + + IFramesCollection* GetVirtualSeriesFrames(const std::string& virtualSeriesId) const { const std::string& seriesInstanceUid = virtualSeries_.GetSeriesInstanceUid(virtualSeriesId); @@ -776,7 +853,8 @@ { const std::list& sopInstanceUids = virtualSeries_.GetSopInstanceUids(virtualSeriesId); - target.Clear(); + std::unique_ptr target(new OrthancStone::SortedFrames); + target->Clear(); for (std::list::const_iterator it = sopInstanceUids.begin(); it != sopInstanceUids.end(); ++it) @@ -784,7 +862,7 @@ Orthanc::DicomMap instance; if (accessor.LookupInstance(instance, *it)) { - target.AddInstance(instance); + target->AddInstance(instance); } else { @@ -792,12 +870,13 @@ } } - target.Sort(); - return true; + target->Sort(); + + return new SortedFramesCollection(target.release()); } else { - return false; + return NULL; } } @@ -1557,7 +1636,6 @@ - class ViewerViewport : public OrthancStone::ObserverBase { public: @@ -1840,6 +1918,8 @@ class SetFullDicomFrame : public ICommand { private: + std::string studyInstanceUid_; + std::string seriesInstanceUid_; std::string sopInstanceUid_; unsigned int frameNumber_; int priority_; @@ -1848,12 +1928,16 @@ public: SetFullDicomFrame(boost::shared_ptr viewport, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, const std::string& sopInstanceUid, unsigned int frameNumber, int priority, bool isPrefetch, bool serverSideTranscoding) : ICommand(viewport), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), sopInstanceUid_(sopInstanceUid), frameNumber_(frameNumber), priority_(priority), @@ -1879,7 +1963,7 @@ // If we haven't tried server-side rendering yet, give it a try LOG(INFO) << "Switching to server-side transcoding"; GetViewport().serverSideTranscoding_ = true; - GetViewport().ScheduleLoadFullDicomFrame(sopInstanceUid_, frameNumber_, priority_, isPrefetch_); + GetViewport().ScheduleLoadFullDicomFrame(studyInstanceUid_, seriesInstanceUid_, sopInstanceUid_, frameNumber_, priority_, isPrefetch_); } return; } @@ -1997,7 +2081,7 @@ boost::shared_ptr loader_; OrthancStone::DicomSource source_; boost::shared_ptr framesCache_; - std::unique_ptr frames_; + std::unique_ptr frames_; std::unique_ptr cursor_; float windowingCenter_; float windowingWidth_; @@ -2387,7 +2471,7 @@ if (instance.GetSopInstanceUid() == loadedSopInstanceUid && frameNumber == loadedFrameNumber) { - const OrthancStone::CoordinateSystem3D plane = frames_->GetFrameGeometry(cursorIndex); + const OrthancStone::CoordinateSystem3D plane = IFramesCollection::GetFrameGeometry(*frames_, cursorIndex); if (quality == DisplayedFrameQuality_Low) { @@ -2426,7 +2510,9 @@ } } - void ScheduleLoadFullDicomFrame(const std::string& sopInstanceUid, + void ScheduleLoadFullDicomFrame(const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid, unsigned int frameNumber, int priority, bool isPrefetch) @@ -2436,10 +2522,10 @@ std::unique_ptr lock(context_.Lock()); lock->Schedule( GetSharedObserver(), priority, OrthancStone::ParseDicomFromWadoCommand::Create( - source_, frames_->GetStudyInstanceUid(), frames_->GetSeriesInstanceUid(), - sopInstanceUid, serverSideTranscoding_, + source_, studyInstanceUid, seriesInstanceUid, sopInstanceUid, serverSideTranscoding_, Orthanc::DicomTransferSyntax_LittleEndianExplicit, - new SetFullDicomFrame(GetSharedObserver(), sopInstanceUid, frameNumber, priority, isPrefetch, serverSideTranscoding_))); + new SetFullDicomFrame(GetSharedObserver(), studyInstanceUid, seriesInstanceUid, + sopInstanceUid, frameNumber, priority, isPrefetch, serverSideTranscoding_))); } } @@ -2449,9 +2535,11 @@ { if (frames_.get() != NULL) { + std::string studyInstanceUid = frames_->GetInstanceOfFrame(cursorIndex).GetStudyInstanceUid(); + std::string seriesInstanceUid = frames_->GetInstanceOfFrame(cursorIndex).GetSeriesInstanceUid(); std::string sopInstanceUid = frames_->GetInstanceOfFrame(cursorIndex).GetSopInstanceUid(); unsigned int frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); - ScheduleLoadFullDicomFrame(sopInstanceUid, frameNumber, priority, isPrefetch); + ScheduleLoadFullDicomFrame(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameNumber, priority, isPrefetch); } } @@ -2500,8 +2588,8 @@ bool isMonochrome1 = (instance.GetImageInformation().GetPhotometricInterpretation() == Orthanc::PhotometricInterpretation_Monochrome1); - const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() + - "/series/" + frames_->GetSeriesInstanceUid() + + const std::string uri = ("studies/" + instance.GetStudyInstanceUid() + + "/series/" + instance.GetSeriesInstanceUid() + "/instances/" + instance.GetSopInstanceUid() + "/frames/" + boost::lexical_cast(frameNumber + 1) + "/rendered"); @@ -2542,7 +2630,6 @@ } } - ViewerViewport(OrthancStone::WebAssemblyLoadersContext& context, const OrthancStone::DicomSource& source, const std::string& canvas, @@ -2637,14 +2724,12 @@ { const size_t currentCursorIndex = that.cursor_->GetCurrentIndex(); - const OrthancStone::CoordinateSystem3D current = - that.frames_->GetFrameGeometry(currentCursorIndex); + const OrthancStone::CoordinateSystem3D current = IFramesCollection::GetFrameGeometry(*that.frames_, currentCursorIndex); if (isShift && previousCursorIndex != currentCursorIndex) { - const OrthancStone::CoordinateSystem3D previous = - that.frames_->GetFrameGeometry(previousCursorIndex); + const OrthancStone::CoordinateSystem3D previous = IFramesCollection::GetFrameGeometry(*that.frames_, previousCursorIndex); that.synchronizationOffset_ += previous.GetOrigin() - current.GetOrigin(); } @@ -2791,7 +2876,7 @@ return viewport; } - void SetFrames(OrthancStone::SortedFrames* frames) + void SetFrames(IFramesCollection* frames) { if (frames == NULL) { @@ -2861,8 +2946,8 @@ GetSeriesThumbnailType(uid) != OrthancStone::SeriesThumbnailType_Video) { // Fetch the details of the series from the central instance - const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() + - "/series/" + frames_->GetSeriesInstanceUid() + + const std::string uri = ("studies/" + centralInstance.GetStudyInstanceUid() + + "/series/" + centralInstance.GetSeriesInstanceUid() + "/instances/" + centralInstance.GetSopInstanceUid() + "/metadata"); loader_->ScheduleGetDicomWeb( @@ -2918,7 +3003,7 @@ FramesCache::Accessor accessor(*framesCache_, instance.GetSopInstanceUid(), frameNumber); if (accessor.IsValid()) { - RenderCurrentScene(accessor.GetImage(), instance, frameNumber, frames_->GetFrameGeometry(cursorIndex)); + RenderCurrentScene(accessor.GetImage(), instance, frameNumber, IFramesCollection::GetFrameGeometry(*frames_, cursorIndex)); DisplayedFrameQuality quality; @@ -3029,7 +3114,7 @@ if (cursor_.get() != NULL && frames_.get() != NULL) { - plane = frames_->GetFrameGeometry(cursor_->GetCurrentIndex()); + plane = IFramesCollection::GetFrameGeometry(*frames_, cursor_->GetCurrentIndex()); return true; } else @@ -3103,7 +3188,7 @@ void SetWindowingPreset() { assert(windowingPresetCenters_.size() == windowingPresetWidths_.size()); - + if (windowingPresetCenters_.empty()) { SetWindowing(128, 256); @@ -3568,8 +3653,7 @@ { const size_t currentCursorIndex = cursor_->GetCurrentIndex(); - const OrthancStone::CoordinateSystem3D current = - frames_->GetFrameGeometry(currentCursorIndex); + const OrthancStone::CoordinateSystem3D current = IFramesCollection::GetFrameGeometry(*frames_, currentCursorIndex); observer_->SignalSynchronizedBrowsing( *this, current.GetOrigin() + synchronizationOffset_, current.GetNormal()); @@ -4476,9 +4560,9 @@ { try { - std::unique_ptr frames(new OrthancStone::SortedFrames); - - if (GetResourcesLoader().SortSeriesFrames(*frames, seriesInstanceUid)) + std::unique_ptr frames(GetResourcesLoader().GetSeriesFrames(seriesInstanceUid)); + + if (frames.get() != NULL) { GetViewport(canvas)->SetFrames(frames.release()); return 1; @@ -4499,9 +4583,9 @@ { try { - std::unique_ptr frames(new OrthancStone::SortedFrames); - - if (GetResourcesLoader().SortVirtualSeriesFrames(*frames, virtualSeriesId)) + std::unique_ptr frames(GetResourcesLoader().GetVirtualSeriesFrames(virtualSeriesId)); + + if (frames.get() != NULL) { GetViewport(canvas)->SetFrames(frames.release()); return 1; diff -r a6d5373e471c -r 9f7604d6b581 Applications/StoneWebViewer/WebAssembly/docker-build.sh --- a/Applications/StoneWebViewer/WebAssembly/docker-build.sh Wed Oct 11 17:10:45 2023 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/docker-build.sh Tue Nov 14 11:45:17 2023 +0100 @@ -50,6 +50,7 @@ mkdir -p ${ROOT_DIR}/wasm-binaries docker run -t ${DOCKER_FLAGS} --rm \ + --dns=8.8.8.8 \ --user $(id -u):$(id -g) \ -e STONE_BRANCH=${STONE_BRANCH} \ -v ${ROOT_DIR}:/source:ro \ diff -r a6d5373e471c -r 9f7604d6b581 Applications/StoneWebViewer/WebAssembly/docker-internal.sh --- a/Applications/StoneWebViewer/WebAssembly/docker-internal.sh Wed Oct 11 17:10:45 2023 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/docker-internal.sh Tue Nov 14 11:45:17 2023 +0100 @@ -44,6 +44,7 @@ -DORTHANC_STONE_INSTALL_PREFIX=/target/StoneWebViewer \ -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \ -DSTATIC_BUILD=ON \ + -DLIBCLANG=/usr/lib/llvm-4.0/lib/libclang-4.0.so \ -G Ninja ninja -j2 install diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake --- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake Tue Nov 14 11:45:17 2023 +0100 @@ -189,6 +189,7 @@ if (ENABLE_DCMTK) list(APPEND ORTHANC_STONE_SOURCES ${ORTHANC_STONE_ROOT}/Oracle/ParseDicomSuccessMessage.cpp + ${ORTHANC_STONE_ROOT}/Toolbox/DicomStructuredReport.cpp ${ORTHANC_STONE_ROOT}/Toolbox/OrthancDatasets/SimplifiedOrthancDataset.cpp ${ORTHANC_STONE_ROOT}/Toolbox/ParsedDicomCache.cpp ${ORTHANC_STONE_ROOT}/Toolbox/ParsedDicomDataset.cpp diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake --- a/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake Tue Nov 14 11:45:17 2023 +0100 @@ -271,7 +271,7 @@ else() message("Forking the Orthanc source repository using Mercurial") execute_process( - COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://hg.orthanc-server.com/orthanc/" + COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://orthanc.uclouvain.be/hg/orthanc/" WORKING_DIRECTORY ${CMAKE_BINARY_DIR} RESULT_VARIABLE Failure ) diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Resources/SyncOrthancFolder.py --- a/OrthancStone/Resources/SyncOrthancFolder.py Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/Resources/SyncOrthancFolder.py Tue Nov 14 11:45:17 2023 +0100 @@ -38,7 +38,7 @@ TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc') -REPOSITORY = 'https://hg.orthanc-server.com/orthanc/raw-file' +REPOSITORY = 'https://orthanc.uclouvain.be/hg/orthanc/raw-file' FILES = [ ('OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake', 'CMake'), diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Sources/Loaders/LoadedDicomResources.cpp --- a/OrthancStone/Sources/Loaders/LoadedDicomResources.cpp Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/Sources/Loaders/LoadedDicomResources.cpp Tue Nov 14 11:45:17 2023 +0100 @@ -276,4 +276,22 @@ return true; } } + + + bool LoadedDicomResources::LookupResource(Orthanc::DicomMap& target, + const std::string& id) const + { + Resources::const_iterator it = resources_.find(id); + + if (it == resources_.end()) + { + return false; + } + else + { + assert(it->second != NULL); + target.Assign(it->second->GetDicom()); + return true; + } + } } diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Sources/Loaders/LoadedDicomResources.h --- a/OrthancStone/Sources/Loaders/LoadedDicomResources.h Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/Sources/Loaders/LoadedDicomResources.h Tue Nov 14 11:45:17 2023 +0100 @@ -146,5 +146,8 @@ { return GetResourceInternal(index).GetSourceJson(); } + + bool LookupResource(Orthanc::DicomMap& target, + const std::string& id) const; }; } diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Sources/StoneEnumerations.cpp --- a/OrthancStone/Sources/StoneEnumerations.cpp Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/Sources/StoneEnumerations.cpp Tue Nov 14 11:45:17 2023 +0100 @@ -65,6 +65,10 @@ { return SopClassUid_DicomSeg; } + else if (s == "1.2.840.10008.5.1.4.1.1.88.33") + { + return SopClassUid_ComprehensiveSR; + } else { //LOG(INFO) << "Other SOP class UID: " << source; diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Sources/StoneEnumerations.h --- a/OrthancStone/Sources/StoneEnumerations.h Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/Sources/StoneEnumerations.h Tue Nov 14 11:45:17 2023 +0100 @@ -116,7 +116,8 @@ SopClassUid_VideoEndoscopicImageStorage, SopClassUid_VideoMicroscopicImageStorage, SopClassUid_VideoPhotographicImageStorage, - SopClassUid_DicomSeg + SopClassUid_DicomSeg, + SopClassUid_ComprehensiveSR }; enum SeriesThumbnailType diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp Tue Nov 14 11:45:17 2023 +0100 @@ -0,0 +1,610 @@ +/** + * 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-2023 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 + * . + **/ + + +#include "DicomStructuredReport.h" + +#include "../Scene2D/ScenePoint2D.h" + +#include +#include + +#include +#include +#include + + +static std::string FormatTag(const DcmTagKey& key) +{ + OFString s = key.toString(); + return std::string(s.c_str()); +} + + +static std::string GetStringValue(DcmItem& dataset, + const DcmTagKey& key) +{ + const char* value = NULL; + if (dataset.findAndGetString(key, value).good() && + value != NULL) + { + return value; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing tag in DICOM-SR: " + FormatTag(key)); + } +} + + +static DcmSequenceOfItems& GetSequenceValue(DcmItem& dataset, + const DcmTagKey& key) +{ + DcmSequenceOfItems* sequence = NULL; + if (dataset.findAndGetSequence(key, sequence).good() && + sequence != NULL) + { + return *sequence; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing sequence in DICOM-SR: " + FormatTag(key)); + } +} + + +static void CheckStringValue(DcmItem& dataset, + const DcmTagKey& key, + const std::string& expected) +{ + if (GetStringValue(dataset, key) != expected) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } +} + + +static bool IsDicomTemplate(DcmItem& dataset, + const std::string& tid) +{ + DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_ContentTemplateSequence); + + return (sequence.card() == 1 && + GetStringValue(*sequence.getItem(0), DCM_MappingResource) == "DCMR" && + GetStringValue(*sequence.getItem(0), DCM_TemplateIdentifier) == tid); +} + + +static bool IsValidConcept(DcmItem& dataset, + const DcmTagKey& key, + const std::string& scheme, + const std::string& concept) +{ + DcmSequenceOfItems& sequence = GetSequenceValue(dataset, key); + + return (sequence.card() == 1 && + GetStringValue(*sequence.getItem(0), DCM_CodingSchemeDesignator) == scheme && + GetStringValue(*sequence.getItem(0), DCM_CodeValue) == concept); +} + + +static bool IsDicomConcept(DcmItem& dataset, + const std::string& concept) +{ + return IsValidConcept(dataset, DCM_ConceptNameCodeSequence, "DCM", concept); +} + + +namespace OrthancStone +{ + void DicomStructuredReport::Structure::Copy(const Structure& other) + { + if (other.HasFrameNumber()) + { + SetFrameNumber(other.GetFrameNumber()); + } + + if (other.HasProbabilityOfCancer()) + { + SetProbabilityOfCancer(other.GetProbabilityOfCancer()); + } + } + + + DicomStructuredReport::Structure::Structure(const std::string& sopInstanceUid) : + sopInstanceUid_(sopInstanceUid), + hasFrameNumber_(false), + hasProbabilityOfCancer_(false) + { + } + + + void DicomStructuredReport::Structure::SetFrameNumber(unsigned int frame) + { + hasFrameNumber_ = true; + frameNumber_ = frame; + } + + + void DicomStructuredReport::Structure::SetProbabilityOfCancer(float probability) + { + if (probability < 0 || + probability > 100) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + hasProbabilityOfCancer_ = true; + probabilityOfCancer_ = probability; + } + } + + + unsigned int DicomStructuredReport::Structure::GetFrameNumber() const + { + if (hasFrameNumber_) + { + return frameNumber_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + float DicomStructuredReport::Structure::GetProbabilityOfCancer() const + { + if (hasProbabilityOfCancer_) + { + return probabilityOfCancer_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + DicomStructuredReport::Point::Point(const std::string& sopInstanceUid, + double x, + double y) : + Structure(sopInstanceUid), + point_(x, y) + { + } + + + DicomStructuredReport::Structure* DicomStructuredReport::Point::Clone() const + { + std::unique_ptr cloned(new Point(GetSopInstanceUid(), point_.GetX(), point_.GetY())); + cloned->Copy(*this); + return cloned.release(); + } + + + DicomStructuredReport::Polyline::Polyline(const std::string& sopInstanceUid, + const float* points, + unsigned long pointsCount) : + Structure(sopInstanceUid) + { + if (pointsCount % 2 != 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + points_.reserve(pointsCount / 2); + + for (unsigned long i = 0; i < pointsCount; i += 2) + { + points_.push_back(ScenePoint2D(points[i], points[i + 1])); + } + } + + + DicomStructuredReport::Polyline::Polyline(const std::string& sopInstanceUid, + const std::vector& points) : + Structure(sopInstanceUid), + points_(points) + { + } + + + DicomStructuredReport::Structure* DicomStructuredReport::Polyline::Clone() const + { + std::unique_ptr cloned(new Polyline(GetSopInstanceUid(), points_)); + cloned->Copy(*this); + return cloned.release(); + } + + + const ScenePoint2D& DicomStructuredReport::Polyline::GetPoint(size_t i) const + { + if (i >= points_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + return points_[i]; + } + } + + + void DicomStructuredReport::AddStructure(const std::string& sopInstanceUid, + DcmItem& group, + bool hasFrameNumber, + unsigned int frameNumber, + bool hasProbabilityOfCancer, + float probabilityOfCancer) + { + const std::string graphicType = GetStringValue(group, DCM_GraphicType); + + const Float32* coords = NULL; + unsigned long coordsCount = 0; + if (!group.findAndGetFloat32Array(DCM_GraphicData, coords, &coordsCount).good() || + (coordsCount != 0 && coords == NULL)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Cannot read coordinates for region in DICOM-SR"); + } + + std::unique_ptr structure; + + if (graphicType == "POINT") + { + if (coordsCount != 2) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + structure.reset(new Point(sopInstanceUid, coords[0], coords[1])); + } + } + else if (graphicType == "POLYLINE") + { + structure.reset(new Polyline(sopInstanceUid, coords, coordsCount)); + } + else + { + return; // Unsupported graphic type + } + + assert(structure.get() != NULL); + + if (hasFrameNumber) + { + structure->SetFrameNumber(frameNumber); + } + + if (hasProbabilityOfCancer) + { + structure->SetProbabilityOfCancer(probabilityOfCancer); + } + + structures_.push_back(structure.release()); + } + + + DicomStructuredReport::DicomStructuredReport(Orthanc::ParsedDicomFile& dicom) + { + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + + studyInstanceUid_ = GetStringValue(dataset, DCM_StudyInstanceUID); + seriesInstanceUid_ = GetStringValue(dataset, DCM_SeriesInstanceUID); + sopInstanceUid_ = GetStringValue(dataset, DCM_SOPInstanceUID); + + CheckStringValue(dataset, DCM_Modality, "SR"); + CheckStringValue(dataset, DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.88.33"); // Comprehensive SR IOD + CheckStringValue(dataset, DCM_ValueType, "CONTAINER"); + + if (!IsDicomConcept(dataset, "126000") /* Imaging measurement report */ || + !IsDicomTemplate(dataset, "1500")) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_CurrentRequestedProcedureEvidenceSequence); + + std::list tmp; + + for (unsigned long i = 0; i < sequence.card(); i++) + { + std::string studyInstanceUid = GetStringValue(*sequence.getItem(i), DCM_StudyInstanceUID); + + DcmSequenceOfItems* referencedSeries = NULL; + if (!sequence.getItem(i)->findAndGetSequence(DCM_ReferencedSeriesSequence, referencedSeries).good() || + referencedSeries == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + for (unsigned long j = 0; j < referencedSeries->card(); j++) + { + std::string seriesInstanceUid = GetStringValue(*referencedSeries->getItem(j), DCM_SeriesInstanceUID); + + DcmSequenceOfItems* referencedInstances = NULL; + if (!referencedSeries->getItem(j)->findAndGetSequence(DCM_ReferencedSOPSequence, referencedInstances).good() || + referencedInstances == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + for (unsigned int k = 0; k < referencedInstances->card(); k++) + { + std::string sopClassUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPClassUID); + std::string sopInstanceUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPInstanceUID); + + if (instancesInformation_.find(sopInstanceUid) == instancesInformation_.end()) + { + instancesInformation_[sopInstanceUid] = new ReferencedInstance(studyInstanceUid, seriesInstanceUid, sopClassUid); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Multiple occurrences of the same instance in DICOM-SR: " + sopInstanceUid); + } + + tmp.push_back(sopInstanceUid); + } + } + } + + orderedInstances_.reserve(tmp.size()); + + for (std::list::const_iterator it = tmp.begin(); it != tmp.end(); ++it) + { + orderedInstances_.push_back(*it); + } + + sequence = GetSequenceValue(dataset, DCM_ContentSequence); + + for (unsigned long i = 0; i < sequence.card(); i++) + { + DcmItem& item = *sequence.getItem(i); + + if (GetStringValue(item, DCM_RelationshipType) == "CONTAINS" && + GetStringValue(item, DCM_ValueType) == "CONTAINER" && + IsDicomConcept(item, "126010" /* Imaging measurements */)) + { + DcmSequenceOfItems& measurements = GetSequenceValue(item, DCM_ContentSequence); + + for (unsigned long j = 0; j < measurements.card(); j++) + { + DcmItem& measurement = *measurements.getItem(j); + + if (GetStringValue(measurement, DCM_RelationshipType) == "CONTAINS" && + GetStringValue(measurement, DCM_ValueType) == "CONTAINER" && + IsDicomConcept(measurement, "125007" /* Measurement group */) && + IsDicomTemplate(measurement, "1410")) + { + DcmSequenceOfItems& groups = GetSequenceValue(measurement, DCM_ContentSequence); + + bool hasProbabilityOfCancer = false; + float probabilityOfCancer = 0; + + for (unsigned int k = 0; k < groups.card(); k++) + { + DcmItem& group = *groups.getItem(k); + + if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" && + GetStringValue(group, DCM_ValueType) == "NUM" && + IsDicomConcept(group, "111047" /* Probability of cancer */)) + { + DcmSequenceOfItems& values = GetSequenceValue(group, DCM_MeasuredValueSequence); + + if (values.card() == 1 && + IsValidConcept(*values.getItem(0), DCM_MeasurementUnitsCodeSequence, "UCUM", "%")) + { + std::string value = GetStringValue(*values.getItem(0), DCM_NumericValue); + if (Orthanc::SerializationToolbox::ParseFloat(probabilityOfCancer, value)) + { + hasProbabilityOfCancer = true; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Cannot parse float in DICOM-SR: " + value); + } + } + } + } + + for (unsigned int k = 0; k < groups.card(); k++) + { + DcmItem& group = *groups.getItem(k); + + if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" && + GetStringValue(group, DCM_ValueType) == "SCOORD" && + IsDicomConcept(group, "111030" /* Image region */)) + { + DcmSequenceOfItems& regions = GetSequenceValue(group, DCM_ContentSequence); + + for (unsigned int l = 0; l < regions.card(); l++) + { + DcmItem& region = *regions.getItem(l); + + if (GetStringValue(region, DCM_RelationshipType) == "SELECTED FROM" && + GetStringValue(region, DCM_ValueType) == "IMAGE" && + IsDicomConcept(region, "111040") /* Original source */) + { + DcmSequenceOfItems& instances = GetSequenceValue(region, DCM_ReferencedSOPSequence); + if (instances.card() != 1) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Region cannot reference multiple instances in DICOM-SR"); + } + + std::string sopInstanceUid = GetStringValue(*instances.getItem(0), DCM_ReferencedSOPInstanceUID); + std::map::iterator instanceInformation = instancesInformation_.find(sopInstanceUid); + + if (instanceInformation == instancesInformation_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Referencing unknown instance in DICOM-SR: " + sopInstanceUid); + } + + assert(instanceInformation->second != NULL); + + if (instances.getItem(0)->tagExists(DCM_ReferencedFrameNumber)) + { + std::string frames = GetStringValue(*instances.getItem(0), DCM_ReferencedFrameNumber); + std::vector tokens; + Orthanc::Toolbox::SplitString(tokens, frames, '\\'); + + for (size_t m = 0; m < tokens.size(); m++) + { + uint32_t frame; + if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(frame, tokens[m]) || + frame <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + else + { + AddStructure(sopInstanceUid, group, true, frame - 1, hasProbabilityOfCancer, probabilityOfCancer); + instanceInformation->second->AddFrame(frame - 1); + } + } + } + else + { + AddStructure(sopInstanceUid, group, false, 0, hasProbabilityOfCancer, probabilityOfCancer); + instanceInformation->second->AddFrame(0); + } + } + } + } + } + } + } + } + } + } + + + DicomStructuredReport::DicomStructuredReport(const DicomStructuredReport& other) : + studyInstanceUid_(other.studyInstanceUid_), + seriesInstanceUid_(other.seriesInstanceUid_), + sopInstanceUid_(other.sopInstanceUid_), + orderedInstances_(other.orderedInstances_) + { + for (std::map::const_iterator + it = other.instancesInformation_.begin(); it != other.instancesInformation_.end(); ++it) + { + assert(it->second != NULL); + instancesInformation_[it->first] = new ReferencedInstance(*it->second); + } + + for (std::deque::const_iterator it = other.structures_.begin(); it != other.structures_.end(); ++it) + { + assert(*it != NULL); + structures_.push_back((*it)->Clone()); + } + } + + + DicomStructuredReport::~DicomStructuredReport() + { + for (std::deque::iterator it = structures_.begin(); it != structures_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + + for (std::map::iterator + it = instancesInformation_.begin(); it != instancesInformation_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void DicomStructuredReport::GetReferencedInstance(std::string& studyInstanceUid, + std::string& seriesInstanceUid, + std::string& sopInstanceUid, + std::string& sopClassUid, + size_t i) const + { + if (i >= orderedInstances_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + sopInstanceUid = orderedInstances_[i]; + + std::map::const_iterator found = instancesInformation_.find(sopInstanceUid); + if (found == instancesInformation_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + assert(found->second != NULL); + studyInstanceUid = found->second->GetStudyInstanceUid(); + seriesInstanceUid = found->second->GetSeriesInstanceUid(); + sopClassUid = found->second->GetSopClassUid(); + } + + + void DicomStructuredReport::ExportReferencedFrames(std::list& frames) const + { + frames.clear(); + + for (size_t i = 0; i < orderedInstances_.size(); i++) + { + std::map::const_iterator found = instancesInformation_.find(orderedInstances_[i]); + if (found == instancesInformation_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + assert(found->second != NULL); + + for (std::set::const_iterator frame = found->second->GetFrames().begin(); + frame != found->second->GetFrames().end(); ++frame) + { + frames.push_back(ReferencedFrame(found->second->GetStudyInstanceUid(), + found->second->GetSeriesInstanceUid(), + orderedInstances_[i], + found->second->GetSopClassUid(), *frame)); + } + } + } + + + const DicomStructuredReport::Structure& DicomStructuredReport::GetStructure(size_t index) const + { + if (index >= structures_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(structures_[index] != NULL); + return *structures_[index]; + } + } +} diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/Sources/Toolbox/DicomStructuredReport.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Toolbox/DicomStructuredReport.h Tue Nov 14 11:45:17 2023 +0100 @@ -0,0 +1,306 @@ +/** + * 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-2023 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 + * . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK != 1 +# error Support for DCMTK must be enabled +#endif + +#include "../Scene2D/ScenePoint2D.h" + +#include + +#include +#include +#include +#include + +namespace OrthancStone +{ + class DicomStructuredReport : public boost::noncopyable + { + public: + enum StructureType + { + StructureType_Point, + StructureType_Polyline + }; + + class Structure : public boost::noncopyable + { + private: + std::string sopInstanceUid_; + bool hasFrameNumber_; + unsigned int frameNumber_; + bool hasProbabilityOfCancer_; + float probabilityOfCancer_; + + protected: + void Copy(const Structure& other); + + public: + Structure(const std::string& sopInstanceUid); + + virtual ~Structure() + { + } + + virtual Structure* Clone() const = 0; + + virtual StructureType GetType() const = 0; + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + void SetFrameNumber(unsigned int frame); + + void SetProbabilityOfCancer(float probability); + + bool HasFrameNumber() const + { + return hasFrameNumber_; + } + + bool HasProbabilityOfCancer() const + { + return hasProbabilityOfCancer_; + } + + unsigned int GetFrameNumber() const; + + float GetProbabilityOfCancer() const; + }; + + + class Point : public Structure + { + private: + ScenePoint2D point_; + + public: + Point(const std::string& sopInstanceUid, + double x, + double y); + + virtual Structure* Clone() const ORTHANC_OVERRIDE; + + virtual StructureType GetType() const ORTHANC_OVERRIDE + { + return StructureType_Point; + } + + const ScenePoint2D& GetPoint() const + { + return point_; + } + }; + + + class Polyline : public Structure + { + private: + std::vector points_; + + public: + Polyline(const std::string& sopInstanceUid, + const float* points, + unsigned long pointsCount); + + Polyline(const std::string& sopInstanceUid, + const std::vector& points); + + virtual Structure* Clone() const ORTHANC_OVERRIDE; + + virtual StructureType GetType() const ORTHANC_OVERRIDE + { + return StructureType_Polyline; + } + + size_t GetSize() const + { + return points_.size(); + } + + const ScenePoint2D& GetPoint(size_t i) const; + }; + + + private: + class ReferencedInstance + { + private: + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + std::string sopClassUid_; + std::set frames_; + + public: + ReferencedInstance(const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopClassUid) : + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + sopClassUid_(sopClassUid) + { + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetSopClassUid() const + { + return sopClassUid_; + } + + void AddFrame(unsigned int frame) + { + frames_.insert(frame); + } + + const std::set& GetFrames() const + { + return frames_; + } + }; + + + void AddStructure(const std::string& sopInstanceUid, + DcmItem& group, + bool hasFrameNumber, + unsigned int frameNumber, + bool hasProbabilityOfCancer, + float probabilityOfCancer); + + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + std::string sopInstanceUid_; + std::map instancesInformation_; + std::vector orderedInstances_; + std::deque structures_; + + public: + class ReferencedFrame + { + private: + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + std::string sopInstanceUid_; + std::string sopClassUid_; + unsigned int frameNumber_; + + public: + ReferencedFrame(const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid, + const std::string& sopClassUid, + unsigned int frameNumber) : + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + sopInstanceUid_(sopInstanceUid), + sopClassUid_(sopClassUid), + frameNumber_(frameNumber) + { + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + const std::string& GetSopClassUid() const + { + return sopClassUid_; + } + + unsigned int GetFrameNumber() const + { + return frameNumber_; + } + }; + + DicomStructuredReport(Orthanc::ParsedDicomFile& dicom); + + DicomStructuredReport(const DicomStructuredReport& other); // Copy constructor + + ~DicomStructuredReport(); + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + size_t GetReferencedInstancesCount() const + { + return orderedInstances_.size(); + } + + void GetReferencedInstance(std::string& studyInstanceUid, + std::string& seriesInstanceUid, + std::string& sopInstanceUid, + std::string& sopClassUid, + size_t i) const; + + void ExportReferencedFrames(std::list& frames) const; + + size_t GetStructuresCount() const + { + return structures_.size(); + } + + const Structure& GetStructure(size_t index) const; + }; +} diff -r a6d5373e471c -r 9f7604d6b581 OrthancStone/UnitTestsSources/UnitTestsMain.cpp --- a/OrthancStone/UnitTestsSources/UnitTestsMain.cpp Wed Oct 11 17:10:45 2023 +0200 +++ b/OrthancStone/UnitTestsSources/UnitTestsMain.cpp Tue Nov 14 11:45:17 2023 +0100 @@ -44,6 +44,7 @@ ASSERT_EQ(SopClassUid_VideoEndoscopicImageStorage, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.77.1.1.1")); ASSERT_EQ(SopClassUid_VideoMicroscopicImageStorage, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.77.1.2.1")); ASSERT_EQ(SopClassUid_VideoPhotographicImageStorage, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.77.1.4.1")); + ASSERT_EQ(SopClassUid_ComprehensiveSR, StringToSopClassUid("1.2.840.10008.5.1.4.1.1.88.33")); ASSERT_EQ(SopClassUid_Other, StringToSopClassUid("nope")); ASSERT_EQ(SeriesThumbnailType_Pdf, GetSeriesThumbnailType(SopClassUid_EncapsulatedPdf)); diff -r a6d5373e471c -r 9f7604d6b581 RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp --- a/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Wed Oct 11 17:10:45 2023 +0200 +++ b/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Tue Nov 14 11:45:17 2023 +0100 @@ -79,6 +79,10 @@ } } + void ResetGlobalContext() + { + globalContext_ = NULL; + } bool HasGlobalContext() { diff -r a6d5373e471c -r 9f7604d6b581 RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h --- a/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Wed Oct 11 17:10:45 2023 +0200 +++ b/RenderingPlugin/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h Tue Nov 14 11:45:17 2023 +0100 @@ -137,6 +137,8 @@ void SetGlobalContext(OrthancPluginContext* context); + void ResetGlobalContext(); + bool HasGlobalContext(); OrthancPluginContext* GetGlobalContext(); diff -r a6d5373e471c -r 9f7604d6b581 RenderingPlugin/Resources/SyncOrthancFolder.py --- a/RenderingPlugin/Resources/SyncOrthancFolder.py Wed Oct 11 17:10:45 2023 +0200 +++ b/RenderingPlugin/Resources/SyncOrthancFolder.py Tue Nov 14 11:45:17 2023 +0100 @@ -38,7 +38,7 @@ TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc') PLUGIN_SDK_VERSION = '1.0.0' -REPOSITORY = 'https://hg.orthanc-server.com/orthanc/raw-file' +REPOSITORY = 'https://orthanc.uclouvain.be/hg/orthanc/raw-file' FILES = [ ('OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h', 'Plugins'),