Mercurial > hg > orthanc-education
changeset 50:3c0e19a754a1
added missing header
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Wed, 22 Oct 2025 10:40:29 +0200 |
| parents | 2daaa1067e72 (diff) 7a0157cbfe02 (current diff) |
| children | 87429291063c |
| files | Sources/Dicomization/DicomizationJob.cpp |
| diffstat | 18 files changed, 1081 insertions(+), 644 deletions(-) [+] |
line wrap: on
line diff
--- a/.reuse/dep5 Wed Oct 22 10:24:53 2025 +0200 +++ b/.reuse/dep5 Wed Oct 22 10:40:29 2025 +0200 @@ -15,6 +15,7 @@ License: CC0-1.0 Files: + Resources/CMake/Reproc.patch WebApplication/*.html Copyright: 2024-2025 Sebastien Jodogne, EPL UCLouvain (Belgium) License: AGPL-3.0-or-later
--- a/CMakeLists.txt Wed Oct 22 10:24:53 2025 +0200 +++ b/CMakeLists.txt Wed Oct 22 10:40:29 2025 +0200 @@ -197,8 +197,11 @@ ${REPROC_SOURCES} Sources/Dicomization/ActiveUploads.cpp + Sources/Dicomization/DicomizationJob.cpp Sources/Dicomization/ProcessRunner.cpp + Sources/Dicomization/SharedLogs.cpp Sources/Dicomization/TemporaryDirectory.cpp + Sources/Dicomization/WholeSlideImagingDicomizer.cpp Sources/EducationConfiguration.cpp Sources/EducationEnumerations.cpp Sources/EducationRestApi.cpp @@ -221,6 +224,8 @@ add_dependencies(OrthancEducation AutogeneratedTarget) +set_target_properties(OrthancEducation PROPERTIES C_STANDARD 99) + set_target_properties(OrthancEducation PROPERTIES VERSION ${ORTHANC_PLUGIN_VERSION}
--- a/Resources/CMake/Reproc.cmake Wed Oct 22 10:24:53 2025 +0200 +++ b/Resources/CMake/Reproc.cmake Wed Oct 22 10:40:29 2025 +0200 @@ -20,11 +20,32 @@ set(REPROC_SOURCES_DIR ${CMAKE_BINARY_DIR}/reproc-14.2.5) + +if (IS_DIRECTORY "${REPROC_SOURCES_DIR}") + set(FirstRun OFF) +else() + set(FirstRun ON) +endif() + DownloadPackage( "9ea81a0c1eef6b8f76463d41a86e8ddd" "https://orthanc.uclouvain.be/downloads/third-party-downloads/reproc-14.2.5.tar.gz" "${REPROC_SOURCES_DIR}") +if (FirstRun) + # Apply the patches + execute_process( + COMMAND ${PATCH_EXECUTABLE} -p0 -N -i + ${CMAKE_CURRENT_LIST_DIR}/Reproc.patch + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE Failure + ) + + if (Failure) + message(FATAL_ERROR "Error while patching a file") + endif() +endif() + include_directories(${REPROC_SOURCES_DIR}/reproc/include/) set(REPROC_SOURCES
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/Reproc.patch Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,43 @@ +diff -r -urEb reproc-14.2.5.orig/reproc/src/error.windows.c reproc-14.2.5/reproc/src/error.windows.c +--- reproc-14.2.5.orig/reproc/src/error.windows.c 2025-10-21 22:17:59.386258877 +0200 ++++ reproc-14.2.5/reproc/src/error.windows.c 2025-10-21 22:50:15.322957140 +0200 +@@ -8,8 +8,8 @@ + + #include <limits.h> + #include <stdlib.h> +-#include <windows.h> + #include <winsock2.h> ++#include <windows.h> + + #include <reproc/reproc.h> + +diff -r -urEb reproc-14.2.5.orig/reproc/src/macro.h reproc-14.2.5/reproc/src/macro.h +--- reproc-14.2.5.orig/reproc/src/macro.h 2025-10-21 22:17:59.386258877 +0200 ++++ reproc-14.2.5/reproc/src/macro.h 2025-10-21 22:37:53.283518598 +0200 +@@ -6,6 +6,13 @@ + + #if defined(_WIN32) && !defined(__MINGW32__) + #define THREAD_LOCAL __declspec(thread) ++#elif defined(__LSB_VERSION__) ++/** ++ * Linux Standard Base generates the following error if trying to use ++ * the thread local storage (TLS): "undefined reference to ++ * `__tls_get_addr'" ++ **/ ++ #define THREAD_LOCAL + #else + #define THREAD_LOCAL __thread + #endif +diff -r -urEb reproc-14.2.5.orig/reproc/src/pipe.windows.c reproc-14.2.5/reproc/src/pipe.windows.c +--- reproc-14.2.5.orig/reproc/src/pipe.windows.c 2025-10-21 22:17:59.386258877 +0200 ++++ reproc-14.2.5/reproc/src/pipe.windows.c 2025-10-21 22:50:04.247023847 +0200 +@@ -8,8 +8,8 @@ + + #include <limits.h> + #include <stdlib.h> +-#include <windows.h> + #include <winsock2.h> ++#include <windows.h> + + #include "error.h" + #include "handle.h"
--- a/Sources/Dicomization/ActiveUploads.cpp Wed Oct 22 10:24:53 2025 +0200 +++ b/Sources/Dicomization/ActiveUploads.cpp Wed Oct 22 10:40:29 2025 +0200 @@ -28,6 +28,8 @@ #include <boost/iostreams/device/file_descriptor.hpp> #include <boost/iostreams/stream.hpp> +#include <boost/filesystem.hpp> +#include <fstream> static boost::posix_time::ptime GetNow() @@ -45,7 +47,7 @@ boost::posix_time::ptime lastUpdate_; public: - Upload(uint64_t fileSize) : + explicit Upload(uint64_t fileSize) : file_(new Orthanc::TemporaryFile), pos_(0), fileSize_(fileSize),
--- a/Sources/Dicomization/ActiveUploads.h Wed Oct 22 10:24:53 2025 +0200 +++ b/Sources/Dicomization/ActiveUploads.h Wed Oct 22 10:40:29 2025 +0200 @@ -27,6 +27,7 @@ #include <TemporaryFile.h> #include <boost/thread/mutex.hpp> +#include <map> class ActiveUploads : public boost::noncopyable
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Dicomization/DicomizationJob.cpp Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,219 @@ +/** + * SPDX-FileCopyrightText: 2024-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Orthanc for Education + * Copyright (C) 2024-2025 Sebastien Jodogne, EPL 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 "DicomizationJob.h" + +#include "ActiveUploads.h" + +#include <OrthancException.h> + + +void DicomizationJob::Worker(DicomizationJob* that) +{ + assert(that != NULL); + + std::unique_ptr<Orthanc::TemporaryFile> upload; + + try + { + upload.reset(ActiveUploads::GetInstance().ReleaseTemporaryFile(that->uploadId_)); + } + catch (Orthanc::OrthancException&) + { + boost::mutex::scoped_lock lock(that->mutex_); + that->status_ = Status_Failure; + return; + } + + assert(upload.get() != NULL); + + bool success; + + try + { + success = that->dicomizer_->Execute(upload, that->logs_, that->stopped_); + } + catch (Orthanc::OrthancException& e) + { + success = false; + } + catch (...) + { + success = false; + } + + { + boost::mutex::scoped_lock lock(that->mutex_); + that->status_ = (success ? Status_Success : Status_Failure); + } +} + + +DicomizationJob::DicomizationJob(const std::string& uploadId, + IDicomizer* dicomizer) : + uploadId_(uploadId), + dicomizer_(dicomizer), + stopped_(false), + status_(Status_Running) +{ + if (dicomizer == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + name_ = dicomizer->GetName(); + jobType_ = dicomizer->GetJobType(); +} + + +DicomizationJob::~DicomizationJob() +{ + if (thread_.joinable()) + { + thread_.join(); + } +} + + +void DicomizationJob::Start() +{ + thread_ = boost::thread(Worker, this); +} + + +Orthanc::JobStepResult DicomizationJob::Step(const std::string& jobId) +{ + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + + { + boost::mutex::scoped_lock lock(mutex_); + if (status_ == Status_Success || + status_ == Status_Failure) + { + return (status_ == Status_Success ? + Orthanc::JobStepResult::Success() : + Orthanc::JobStepResult::Failure(Orthanc::ErrorCode_InternalError, "")); + } + } + + return Orthanc::JobStepResult::Continue(); +} + + +void DicomizationJob::Reset() +{ + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); +} + + +void DicomizationJob::Stop(Orthanc::JobStopReason reason) +{ + if (reason == Orthanc::JobStopReason_Canceled) + { + stopped_ = true; + } + + if (thread_.joinable()) + { + thread_.join(); + } +} + + +void DicomizationJob::GetPublicContent(Json::Value& value) const +{ + std::string logs; + const_cast<SharedLogs&>(logs_).GetContent(logs); + + value = Json::objectValue; + value["logs"] = logs; + value["name"] = name_; +} + + +bool DicomizationJob::GetOutput(std::string& output, + Orthanc::MimeType& mime, + std::string& filename, + const std::string& key) +{ + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); +} + + +bool DicomizationJob::DeleteOutput(const std::string& key) +{ + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); +} + + +void DicomizationJob::DeleteAllOutputs() +{ + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); +} + + +bool DicomizationJob::GetUserData(Json::Value& userData) const +{ + return false; +} + + +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 10) +void DicomizationJob::SetUserData(const Json::Value& userData) +{ + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); +} +#endif + + + +static boost::mutex engineMutex_; +static std::unique_ptr<Orthanc::JobsEngine> engine_; + + +Orthanc::JobsEngine& DicomizationJob::GetJobsEngine() +{ + boost::mutex::scoped_lock lock(engineMutex_); + + if (engine_.get() == NULL) + { + engine_.reset(new Orthanc::JobsEngine(20)); // Only keep 20 completed jobs + engine_->SetWorkersCount(1); + engine_->Start(); + } + + return *engine_; +} + + +void DicomizationJob::FinalizeJobsEngine() +{ + boost::mutex::scoped_lock lock(engineMutex_); + + if (engine_.get() != NULL) + { + engine_->Stop(); + engine_.reset(NULL); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Dicomization/DicomizationJob.h Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,105 @@ +/** + * SPDX-FileCopyrightText: 2024-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Orthanc for Education + * Copyright (C) 2024-2025 Sebastien Jodogne, EPL 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 "IDicomizer.h" + +#include <Compatibility.h> +#include <JobsEngine/JobsEngine.h> + + +class DicomizationJob : public Orthanc::IJob +{ +private: + enum Status + { + Status_Running, + Status_Success, + Status_Failure + }; + + std::string uploadId_; + std::unique_ptr<IDicomizer> dicomizer_; + std::string name_; + std::string jobType_; + SharedLogs logs_; + boost::thread thread_; + bool stopped_; + + boost::mutex mutex_; // To protect "status_" + Status status_; + + static void Worker(DicomizationJob* that); + +public: + DicomizationJob(const std::string& uploadId, + IDicomizer* dicomizer); + + virtual ~DicomizationJob(); + + virtual void Start() ORTHANC_OVERRIDE; + + virtual Orthanc::JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; + + virtual void Reset() ORTHANC_OVERRIDE; + + virtual void Stop(Orthanc::JobStopReason reason) ORTHANC_OVERRIDE; + + virtual float GetProgress() const ORTHANC_OVERRIDE + { + return 0; + } + + virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE + { + target = jobType_; + } + + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; + + virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE + { + return false; + } + + virtual bool GetOutput(std::string& output, + Orthanc::MimeType& mime, + std::string& filename, + const std::string& key) ORTHANC_OVERRIDE; + + virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE; + + virtual void DeleteAllOutputs() ORTHANC_OVERRIDE; + + virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE; + +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 10) + virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE; +#endif + + static Orthanc::JobsEngine& GetJobsEngine(); + + static void FinalizeJobsEngine(); +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Dicomization/IDicomizer.h Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,47 @@ +/** + * SPDX-FileCopyrightText: 2024-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Orthanc for Education + * Copyright (C) 2024-2025 Sebastien Jodogne, EPL 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 "SharedLogs.h" + +#include <Compatibility.h> +#include <TemporaryFile.h> + + +class IDicomizer : public boost::noncopyable +{ +public: + virtual ~IDicomizer() + { + } + + virtual std::string GetName() = 0; + + virtual std::string GetJobType() = 0; + + virtual bool Execute(std::unique_ptr<Orthanc::TemporaryFile>& upload, + SharedLogs& logs, + const bool& stopped) = 0; +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Dicomization/SharedLogs.cpp Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,39 @@ +/** + * SPDX-FileCopyrightText: 2024-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Orthanc for Education + * Copyright (C) 2024-2025 Sebastien Jodogne, EPL 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 "SharedLogs.h" + + +void SharedLogs::Append(const std::string& data) +{ + boost::mutex::scoped_lock lock(mutex_); + content_ += data; +} + + +void SharedLogs::GetContent(std::string& content) +{ + boost::mutex::scoped_lock lock(mutex_); + content = content_; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Dicomization/SharedLogs.h Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,42 @@ +/** + * SPDX-FileCopyrightText: 2024-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Orthanc for Education + * Copyright (C) 2024-2025 Sebastien Jodogne, EPL 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 <boost/noncopyable.hpp> +#include <boost/thread/mutex.hpp> +#include <string> + + +class SharedLogs : public boost::noncopyable +{ +private: + boost::mutex mutex_; + std::string content_; + +public: + void Append(const std::string& data); + + void GetContent(std::string& content); +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Dicomization/WholeSlideImagingDicomizer.cpp Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,315 @@ +/** + * SPDX-FileCopyrightText: 2024-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Orthanc for Education + * Copyright (C) 2024-2025 Sebastien Jodogne, EPL 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 "WholeSlideImagingDicomizer.h" + +#include "../EducationConfiguration.h" +#include "ProcessRunner.h" + +#include <Compression/ZipReader.h> +#include <OrthancException.h> +#include <SystemToolbox.h> + +#include <boost/algorithm/string/predicate.hpp> +#include <boost/thread.hpp> + + +bool WholeSlideImagingDicomizer::Unzip(TemporaryDirectory& target, + std::string& unzipMaster, + const Orthanc::TemporaryFile& zip, + const bool& stopped) +{ + std::unique_ptr<Orthanc::ZipReader> reader(Orthanc::ZipReader::CreateFromFile(zip.GetPath())); + + std::string filename, content; + while (reader->ReadNextFile(filename, content)) + { + if (stopped) + { + return false; + } + + // Ignore directories in the ZIP + if (!boost::ends_with(filename, "/")) + { + target.WriteFile(filename, content); + + boost::filesystem::path path(target.GetPath(filename)); + + const std::string& extension = path.extension().string(); + + if (extension == ".mrxs" || + extension == ".ndpi" || + extension == ".scn" || + extension == ".tif" || + extension == ".tiff" || + extension == ".png" || + extension == ".jpg" || + extension == ".jpeg") + { + if (unzipMaster.empty()) + { + unzipMaster = path.string(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "ZIP file containing multiple candidate whole-slide images"); + } + } + } + } + + if (unzipMaster.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "ZIP file containing no whole-slide image"); + } + + return true; +} + + +void WholeSlideImagingDicomizer::PrepareArguments(std::list<std::string>& args) const +{ + if (reconstructPyramid_) + { + args.push_back("--pyramid"); + args.push_back("1"); + } + + if (forceOpenSlide_) + { + args.push_back("--force-openslide"); + args.push_back("1"); + } + + args.push_back("--color"); + + { + char color[32]; + sprintf(color, "%d,%d,%d", backgroundRed_, backgroundGreen_, backgroundBlue_); + args.push_back(color); + } + + const std::string openslide = EducationConfiguration::GetInstance().GetPathToOpenSlide(); + if (!openslide.empty()) + { + args.push_back("--openslide"); + args.push_back(openslide); + } +} + + +bool WholeSlideImagingDicomizer::ExecuteDicomizer(const std::string& dicomizer, + const std::list<std::string>& args, + SharedLogs& logs, + const bool& stopped) +{ + ProcessRunner runner; + runner.Start(dicomizer, args, ProcessRunner::Stream_Error); + + while (runner.IsRunning()) + { + if (stopped) + { + runner.Terminate(); + return false; + } + + std::string s; + runner.Read(s); + logs.Append(s); + + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + } + + { + std::string s; + runner.Read(s); + logs.Append(s); + } + + return (runner.GetExitCode() == 0); +} + + +bool WholeSlideImagingDicomizer::UploadDicomToOrthanc(const TemporaryDirectory& target, + const bool& stopped) +{ + boost::filesystem::directory_iterator iterator(target.GetRoot()); + boost::filesystem::directory_iterator end; + + while (iterator != end) + { + if (stopped) + { + return false; + } + + std::string content; + +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 10) + Orthanc::SystemToolbox::ReadFile(content, iterator->path()); +#else + Orthanc::SystemToolbox::ReadFile(content, iterator->path().string()); +#endif + + if (!content.empty()) + { + Json::Value answer; + if (!OrthancPlugins::RestApiPost(answer, "/instances", content.c_str(), content.size(), false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Cannot upload a DICOM-ized file"); + } + } + + ++iterator; + } + + return true; +} + + +WholeSlideImagingDicomizer::WholeSlideImagingDicomizer() : + studyDescription_("Whole-slide image"), + backgroundRed_(255), + backgroundGreen_(255), + backgroundBlue_(255), + forceOpenSlide_(false), + reconstructPyramid_(true) +{ +} + + +void WholeSlideImagingDicomizer::SetBackgroundColor(uint8_t red, + uint8_t green, + uint8_t blue) +{ + backgroundRed_ = red; + backgroundGreen_ = green; + backgroundBlue_ = blue; +} + + +bool WholeSlideImagingDicomizer::Execute(std::unique_ptr<Orthanc::TemporaryFile>& upload, + SharedLogs& logs, + const bool& stopped) +{ + assert(upload.get() != NULL); + + const std::string dicomizer = EducationConfiguration::GetInstance().GetPathToWsiDicomizer(); + if (dicomizer.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "No DICOM-izer is configured for whole-slide images"); + } + + std::unique_ptr<TemporaryDirectory> unzip; + std::string unzipMaster; + + if (IsZipFile(upload->GetPath())) + { + unzip.reset(new TemporaryDirectory); + + if (!Unzip(*unzip, unzipMaster, *upload, stopped)) + { + return false; + } + + // We don't need the ZIP file anymore + upload.reset(NULL); + } + + Orthanc::TemporaryFile dataset; + + { + Json::Value json; + json["StudyDescription"] = studyDescription_; + + std::string s; + Orthanc::Toolbox::WriteFastJson(s, json); + dataset.Write(s); + } + + std::list<std::string> args; + PrepareArguments(args); + + args.push_back("--dataset"); + +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 10) + args.push_back(dataset.GetPath().string()); +#else + args.push_back(dataset.GetPath()); +#endif + + if (unzip.get() == NULL) + { +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 10) + args.push_back(upload->GetPath().string()); +#else + args.push_back(upload->GetPath()); +#endif + } + else + { + args.push_back(unzipMaster); + } + + std::unique_ptr<TemporaryDirectory> target(new TemporaryDirectory); + args.push_back("--folder"); + args.push_back(target->GetRoot().string()); + + if (!ExecuteDicomizer(dicomizer, args, logs, stopped)) + { + return false; + } + + unzip.reset(NULL); + upload.reset(NULL); + + return UploadDicomToOrthanc(*target, stopped); +} + + +bool WholeSlideImagingDicomizer::IsZipFile(const boost::filesystem::path& path) +{ + std::string header; + +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 10) + Orthanc::SystemToolbox::ReadFileRange(header, path, 0, 4, false /* don't throw exception */); +#else + Orthanc::SystemToolbox::ReadFileRange(header, path.string(), 0, 4, false /* don't throw exception */); +#endif + + if (header.size() != 4) + { + return false; + } + else + { + // https://en.wikipedia.org/wiki/List_of_file_signatures + const uint8_t *b = reinterpret_cast<const uint8_t*>(header.c_str()); + return ((b[0] == 0x50 && b[1] == 0x4b && b[2] == 0x03 && b[3] == 0x04) || + (b[0] == 0x50 && b[1] == 0x4b && b[2] == 0x05 && b[3] == 0x06) || + (b[0] == 0x50 && b[1] == 0x4b && b[2] == 0x07 && b[3] == 0x08)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Dicomization/WholeSlideImagingDicomizer.h Wed Oct 22 10:40:29 2025 +0200 @@ -0,0 +1,95 @@ +/** + * SPDX-FileCopyrightText: 2024-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Orthanc for Education + * Copyright (C) 2024-2025 Sebastien Jodogne, EPL 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 "IDicomizer.h" +#include "TemporaryDirectory.h" + +#include <list> + + +class WholeSlideImagingDicomizer : public IDicomizer +{ +private: + std::string studyDescription_; + uint8_t backgroundRed_; + uint8_t backgroundGreen_; + uint8_t backgroundBlue_; + bool forceOpenSlide_; + bool reconstructPyramid_; + + static bool Unzip(TemporaryDirectory& target, + std::string& unzipMaster, + const Orthanc::TemporaryFile& zip, + const bool& stopped); + + void PrepareArguments(std::list<std::string>& args) const; + + static bool ExecuteDicomizer(const std::string& dicomizer, + const std::list<std::string>& args, + SharedLogs& logs, + const bool& stopped); + + static bool UploadDicomToOrthanc(const TemporaryDirectory& target, + const bool& stopped); + +public: + WholeSlideImagingDicomizer(); + + void SetStudyDescription(const std::string& studyDescription) + { + studyDescription_ = studyDescription; + } + + void SetBackgroundColor(uint8_t red, + uint8_t green, + uint8_t blue); + + void SetForceOpenSlide(bool force) + { + forceOpenSlide_ = force; + } + + void SetReconstructPyramid(bool reconstruct) + { + reconstructPyramid_ = reconstruct; + } + + virtual std::string GetName() ORTHANC_OVERRIDE + { + return studyDescription_; + } + + virtual std::string GetJobType() ORTHANC_OVERRIDE + { + return "wsi"; + } + + virtual bool Execute(std::unique_ptr<Orthanc::TemporaryFile>& upload, + SharedLogs& logs, + const bool& stopped) ORTHANC_OVERRIDE; + + static bool IsZipFile(const boost::filesystem::path& path); +};
--- a/Sources/EducationRestApi.cpp Wed Oct 22 10:24:53 2025 +0200 +++ b/Sources/EducationRestApi.cpp Wed Oct 22 10:40:29 2025 +0200 @@ -25,8 +25,8 @@ #include "EducationRestApi.h" #include "Dicomization/ActiveUploads.h" -#include "Dicomization/ProcessRunner.h" -#include "Dicomization/TemporaryDirectory.h" +#include "Dicomization/DicomizationJob.h" +#include "Dicomization/WholeSlideImagingDicomizer.h" #include "EducationConfiguration.h" #include "LTI/LTIRoutes.h" #include "OrthancDatabase.h" @@ -459,111 +459,113 @@ - -class Thumbnail : public boost::noncopyable +namespace { -private: - const OrthancPlugins::OrthancImage& source_; - std::unique_ptr<Orthanc::ImageAccessor> modified_; + class Thumbnail : public boost::noncopyable + { + private: + const OrthancPlugins::OrthancImage& source_; + std::unique_ptr<Orthanc::ImageAccessor> modified_; -public: - explicit Thumbnail(const OrthancPlugins::OrthancImage& source) : - source_(source) - { - } + public: + explicit Thumbnail(const OrthancPlugins::OrthancImage& source) : + source_(source) + { + } - Orthanc::PixelFormat GetFormat() const - { - if (modified_.get() == NULL) + Orthanc::PixelFormat GetFormat() const { - switch (source_.GetPixelFormat()) + if (modified_.get() == NULL) { - case OrthancPluginPixelFormat_Grayscale8: - return Orthanc::PixelFormat_Grayscale8; + switch (source_.GetPixelFormat()) + { + case OrthancPluginPixelFormat_Grayscale8: + return Orthanc::PixelFormat_Grayscale8; + + case OrthancPluginPixelFormat_RGB24: + return Orthanc::PixelFormat_RGB24; - case OrthancPluginPixelFormat_RGB24: - return Orthanc::PixelFormat_RGB24; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + else + { + return modified_->GetFormat(); } } - else - { - return modified_->GetFormat(); - } - } - unsigned int GetWidth() const - { - if (modified_.get() == NULL) - { - return source_.GetWidth(); - } - else - { - return modified_->GetWidth(); - } - } - - unsigned int GetHeight() const - { - if (modified_.get() == NULL) - { - return source_.GetHeight(); - } - else + unsigned int GetWidth() const { - return modified_->GetHeight(); - } - } - - void GetAccessor(Orthanc::ImageAccessor& accessor) const - { - if (modified_.get() == NULL) - { - accessor.AssignReadOnly(GetFormat(), source_.GetWidth(), source_.GetHeight(), source_.GetPitch(), source_.GetBuffer()); - } - else - { - modified_->GetReadOnlyAccessor(accessor); + if (modified_.get() == NULL) + { + return source_.GetWidth(); + } + else + { + return modified_->GetWidth(); + } } - } - void Resize(unsigned int width, - unsigned int height, - bool smooth) - { - Orthanc::ImageAccessor current; - GetAccessor(current); - - std::unique_ptr<Orthanc::ImageAccessor> resized(new Orthanc::Image(current.GetFormat(), width, height, false)); - - if (smooth && - width < current.GetWidth() && - height < current.GetHeight()) // Only smooth if downscaling + unsigned int GetHeight() const { if (modified_.get() == NULL) { - std::unique_ptr<Orthanc::ImageAccessor> smoothed(Orthanc::Image::Clone(current)); - Orthanc::ImageProcessing::SmoothGaussian5x5(*smoothed, false); - Orthanc::ImageProcessing::Resize(*resized, *smoothed); + return source_.GetHeight(); + } + else + { + return modified_->GetHeight(); + } + } + + void GetAccessor(Orthanc::ImageAccessor& accessor) const + { + if (modified_.get() == NULL) + { + accessor.AssignReadOnly(GetFormat(), source_.GetWidth(), source_.GetHeight(), source_.GetPitch(), source_.GetBuffer()); } else { - // The smoothing can be done inplace, as "resized" will overwrite "modified_" - Orthanc::ImageProcessing::SmoothGaussian5x5(*modified_, false); - Orthanc::ImageProcessing::Resize(*resized, *modified_); + modified_->GetReadOnlyAccessor(accessor); } } - else + + void Resize(unsigned int width, + unsigned int height, + bool smooth) { - Orthanc::ImageProcessing::Resize(*resized, current); + Orthanc::ImageAccessor current; + GetAccessor(current); + + std::unique_ptr<Orthanc::ImageAccessor> resized(new Orthanc::Image(current.GetFormat(), width, height, false)); + + if (smooth && + width < current.GetWidth() && + height < current.GetHeight()) // Only smooth if downscaling + { + if (modified_.get() == NULL) + { + std::unique_ptr<Orthanc::ImageAccessor> smoothed(Orthanc::Image::Clone(current)); + Orthanc::ImageProcessing::SmoothGaussian5x5(*smoothed, false); + Orthanc::ImageProcessing::Resize(*resized, *smoothed); + } + else + { + // The smoothing can be done inplace, as "resized" will overwrite "modified_" + Orthanc::ImageProcessing::SmoothGaussian5x5(*modified_, false); + Orthanc::ImageProcessing::Resize(*resized, *modified_); + } + } + else + { + Orthanc::ImageProcessing::Resize(*resized, current); + } + + modified_.reset(resized.release()); } - - modified_.reset(resized.release()); - } -}; + }; +} @@ -1239,547 +1241,24 @@ } -class SharedOutputBuffer : public boost::noncopyable -{ -private: - boost::mutex mutex_; - std::string content_; - -public: - void Append(const std::string& data) - { - boost::mutex::scoped_lock lock(mutex_); - content_ += data; - } - - void GetContent(std::string& content) - { - boost::mutex::scoped_lock lock(mutex_); - content = content_; - } -}; - - -class IDicomizer : public boost::noncopyable -{ -public: - virtual ~IDicomizer() - { - } - - virtual std::string GetName() = 0; - - virtual std::string GetJobType() = 0; - - virtual bool Execute(std::unique_ptr<Orthanc::TemporaryFile>& upload, - SharedOutputBuffer& logs, - const bool& stopped) = 0; -}; - - - -#include <Compression/ZipReader.h> - - -static bool IsZipFile(const boost::filesystem::path& path) -{ - std::string header; - Orthanc::SystemToolbox::ReadFileRange(header, path, 0, 4, false /* don't throw exception */); - - if (header.size() != 4) - { - return false; - } - else - { - // https://en.wikipedia.org/wiki/List_of_file_signatures - const uint8_t *b = reinterpret_cast<const uint8_t*>(header.c_str()); - return ((b[0] == 0x50 && b[1] == 0x4b && b[2] == 0x03 && b[3] == 0x04) || - (b[0] == 0x50 && b[1] == 0x4b && b[2] == 0x05 && b[3] == 0x06) || - (b[0] == 0x50 && b[1] == 0x4b && b[2] == 0x07 && b[3] == 0x08)); - } -} - - -class WholeSlideImagingDicomizer : public IDicomizer -{ -private: - std::string studyDescription_; - uint8_t backgroundRed_; - uint8_t backgroundGreen_; - uint8_t backgroundBlue_; - bool forceOpenSlide_; - bool reconstructPyramid_; - - static bool Unzip(TemporaryDirectory& target, - std::string& unzipMaster, - const Orthanc::TemporaryFile& zip, - const bool& stopped) - { - std::unique_ptr<Orthanc::ZipReader> reader(Orthanc::ZipReader::CreateFromFile(zip.GetPath())); - - std::string filename, content; - while (reader->ReadNextFile(filename, content)) - { - if (stopped) - { - return false; - } - - // Ignore directories in the ZIP - if (!boost::ends_with(filename, "/")) - { - target.WriteFile(filename, content); - - boost::filesystem::path path(target.GetPath(filename)); - - const std::string& extension = path.extension().string(); - - if (extension == ".mrxs" || - extension == ".ndpi" || - extension == ".scn" || - extension == ".tif" || - extension == ".tiff" || - extension == ".png" || - extension == ".jpg" || - extension == ".jpeg") - { - if (unzipMaster.empty()) - { - unzipMaster = path.string(); - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "ZIP file containing multiple candidate whole-slide images"); - } - } - } - } - - if (unzipMaster.empty()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "ZIP file containing no whole-slide image"); - } - - return true; - } - - void PrepareArguments(std::list<std::string>& args) const - { - if (reconstructPyramid_) - { - args.push_back("--pyramid"); - args.push_back("1"); - } - - if (forceOpenSlide_) - { - args.push_back("--force-openslide"); - args.push_back("1"); - } - - args.push_back("--color"); - - { - char color[32]; - sprintf(color, "%d,%d,%d", backgroundRed_, backgroundGreen_, backgroundBlue_); - args.push_back(color); - } - - const std::string openslide = EducationConfiguration::GetInstance().GetPathToOpenSlide(); - if (!openslide.empty()) - { - args.push_back("--openslide"); - args.push_back(openslide); - } - } - - static bool ExecuteDicomizer(const std::string& dicomizer, - const std::list<std::string>& args, - SharedOutputBuffer& logs, - const bool& stopped) - { - ProcessRunner runner; - runner.Start(dicomizer, args, ProcessRunner::Stream_Error); - - while (runner.IsRunning()) - { - if (stopped) - { - runner.Terminate(); - return false; - } - - std::string s; - runner.Read(s); - logs.Append(s); - - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); - } - - { - std::string s; - runner.Read(s); - logs.Append(s); - } - - return (runner.GetExitCode() == 0); - } - - static bool UploadDicomToOrthanc(const TemporaryDirectory& target, - const bool& stopped) - { - boost::filesystem::directory_iterator iterator(target.GetRoot()); - boost::filesystem::directory_iterator end; - - while (iterator != end) - { - if (stopped) - { - return false; - } - - std::string content; - Orthanc::SystemToolbox::ReadFile(content, iterator->path()); - - if (!content.empty()) - { - Json::Value answer; - if (!OrthancPlugins::RestApiPost(answer, "/instances", content.c_str(), content.size(), false)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Cannot upload a DICOM-ized file"); - } - } - - ++iterator; - } - - return true; - } - -public: - WholeSlideImagingDicomizer() : - studyDescription_("Whole-slide image"), - backgroundRed_(255), - backgroundGreen_(255), - backgroundBlue_(255), - forceOpenSlide_(false), - reconstructPyramid_(true) - { - } - - void SetStudyDescription(const std::string& studyDescription) - { - studyDescription_ = studyDescription; - } - - void SetBackgroundColor(uint8_t red, - uint8_t green, - uint8_t blue) - { - backgroundRed_ = red; - backgroundGreen_ = green; - backgroundBlue_ = blue; - } - - void SetForceOpenSlide(bool force) - { - forceOpenSlide_ = force; - } - - void SetReconstructPyramid(bool reconstruct) - { - reconstructPyramid_ = reconstruct; - } - - virtual std::string GetName() ORTHANC_OVERRIDE - { - return studyDescription_; - } - - virtual std::string GetJobType() ORTHANC_OVERRIDE - { - return "wsi"; - } - - virtual bool Execute(std::unique_ptr<Orthanc::TemporaryFile>& upload, - SharedOutputBuffer& logs, - const bool& stopped) ORTHANC_OVERRIDE - { - assert(upload.get() != NULL); - - const std::string dicomizer = EducationConfiguration::GetInstance().GetPathToWsiDicomizer(); - if (dicomizer.empty()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "No DICOM-izer is configured for whole-slide images"); - } - - std::unique_ptr<TemporaryDirectory> unzip; - std::string unzipMaster; - - if (IsZipFile(upload->GetPath())) - { - unzip.reset(new TemporaryDirectory); - - if (!Unzip(*unzip, unzipMaster, *upload, stopped)) - { - return false; - } - - // We don't need the ZIP file anymore - upload.reset(NULL); - } - - Orthanc::TemporaryFile dataset; - - { - Json::Value json; - json["StudyDescription"] = studyDescription_; - - std::string s; - Orthanc::Toolbox::WriteFastJson(s, json); - dataset.Write(s); - } - - std::list<std::string> args; - PrepareArguments(args); - - args.push_back("--dataset"); - args.push_back(dataset.GetPath().string()); - - if (unzip.get() == NULL) - { - args.push_back(upload->GetPath().string()); - } - else - { - args.push_back(unzipMaster); - } - - std::unique_ptr<TemporaryDirectory> target(new TemporaryDirectory); - args.push_back("--folder"); - args.push_back(target->GetRoot().string()); - - if (!ExecuteDicomizer(dicomizer, args, logs, stopped)) - { - return false; - } - - unzip.reset(NULL); - upload.reset(NULL); - - return UploadDicomToOrthanc(*target, stopped); - } -}; - - - -#include <JobsEngine/JobsEngine.h> - -static Orthanc::JobsEngine engine_(20); // Only keep 20 completed jobs - - -class DicomizerJob : public Orthanc::IJob -{ -private: - enum Status - { - Status_Running, - Status_Success, - Status_Failure - }; - - std::string uploadId_; - std::unique_ptr<IDicomizer> dicomizer_; - std::string name_; - std::string jobType_; - SharedOutputBuffer logs_; - boost::thread thread_; - bool stopped_; - - boost::mutex mutex_; // To protect "status_" - Status status_; - - static void Worker(DicomizerJob* that) - { - assert(that != NULL); - - std::unique_ptr<Orthanc::TemporaryFile> upload; - - try - { - upload.reset(ActiveUploads::GetInstance().ReleaseTemporaryFile(that->uploadId_)); - } - catch (Orthanc::OrthancException&) - { - boost::mutex::scoped_lock lock(that->mutex_); - that->status_ = Status_Failure; - return; - } - - assert(upload.get() != NULL); - - bool success; - - try - { - success = that->dicomizer_->Execute(upload, that->logs_, that->stopped_); - } - catch (Orthanc::OrthancException& e) - { - success = false; - } - catch (...) - { - success = false; - } - - { - boost::mutex::scoped_lock lock(that->mutex_); - that->status_ = (success ? Status_Success : Status_Failure); - } - } - -public: - DicomizerJob(const std::string& uploadId, - IDicomizer* dicomizer) : - uploadId_(uploadId), - dicomizer_(dicomizer), - stopped_(false), - status_(Status_Running) - { - if (dicomizer == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - name_ = dicomizer->GetName(); - jobType_ = dicomizer->GetJobType(); - } - - virtual ~DicomizerJob() - { - if (thread_.joinable()) - { - thread_.join(); - } - } - - virtual void Start() ORTHANC_OVERRIDE - { - thread_ = boost::thread(Worker, this); - } - - virtual Orthanc::JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE - { - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); - - { - boost::mutex::scoped_lock lock(mutex_); - if (status_ == Status_Success || - status_ == Status_Failure) - { - return (status_ == Status_Success ? - Orthanc::JobStepResult::Success() : - Orthanc::JobStepResult::Failure(Orthanc::ErrorCode_InternalError, "")); - } - } - - return Orthanc::JobStepResult::Continue(); - } - - virtual void Reset() ORTHANC_OVERRIDE - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - virtual void Stop(Orthanc::JobStopReason reason) ORTHANC_OVERRIDE - { - if (reason == Orthanc::JobStopReason_Canceled) - { - stopped_ = true; - } - - if (thread_.joinable()) - { - thread_.join(); - } - } - - virtual float GetProgress() const ORTHANC_OVERRIDE - { - return 0; - } - - virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE - { - target = jobType_; - } - - virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE - { - std::string logs; - const_cast<SharedOutputBuffer&>(logs_).GetContent(logs); - - value = Json::objectValue; - value["logs"] = logs; - value["name"] = name_; - } - - virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE - { - return false; - } - - virtual bool GetOutput(std::string& output, - Orthanc::MimeType& mime, - std::string& filename, - const std::string& key) ORTHANC_OVERRIDE - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - virtual void DeleteAllOutputs() ORTHANC_OVERRIDE - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE - { - return false; - } - - virtual void SetUserData(const Json::Value& userData) ORTHANC_OVERRIDE - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } -}; - - - -void Dicomization(OrthancPluginRestOutput* output, - const std::string& url, - const OrthancPluginHttpRequest* request, - const AuthenticatedUser& user) +void StartDicomization(OrthancPluginRestOutput* output, + const std::string& url, + const OrthancPluginHttpRequest* request, + const AuthenticatedUser& user) { assert(user.GetRole() == Role_Administrator); if (request->method == OrthancPluginHttpMethod_Get) { std::set<std::string> jobs; - engine_.GetRegistry().ListJobs(jobs); + DicomizationJob::GetJobsEngine().GetRegistry().ListJobs(jobs); Json::Value answer = Json::arrayValue; for (std::set<std::string>::const_iterator it = jobs.begin(); it != jobs.end(); ++it) { Orthanc::JobInfo info; - if (engine_.GetRegistry().GetJobInfo(info, *it)) + if (DicomizationJob::GetJobsEngine().GetRegistry().GetJobInfo(info, *it)) { Json::Value item; item["id"] = *it; @@ -1853,7 +1332,7 @@ throw; } - engine_.GetRegistry().Submit(new DicomizerJob(uploadId, dicomizer.release()), 0 /* priority */); + DicomizationJob::GetJobsEngine().GetRegistry().Submit(new DicomizationJob(uploadId, dicomizer.release()), 0 /* priority */); HttpToolbox::AnswerText(output, ""); } @@ -1865,6 +1344,27 @@ } +void CancelDicomization(OrthancPluginRestOutput* output, + const std::string& url, + const OrthancPluginHttpRequest* request, + const AuthenticatedUser& user) +{ + assert(user.GetRole() == Role_Administrator); + + const std::string jobId(request->groups[0]); + + if (request->method == OrthancPluginHttpMethod_Delete) + { + DicomizationJob::GetJobsEngine().GetRegistry().Cancel(jobId); + HttpToolbox::AnswerText(output, ""); + } + else + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "DELETE"); + } +} + + void GetDicomizationLogs(OrthancPluginRestOutput* output, const std::string& url, const OrthancPluginHttpRequest* request, @@ -1875,7 +1375,7 @@ const std::string jobId(request->groups[0]); Orthanc::JobInfo info; - if (engine_.GetRegistry().GetJobInfo(info, jobId)) + if (DicomizationJob::GetJobsEngine().GetRegistry().GetJobInfo(info, jobId)) { std::string logs = Orthanc::SerializationToolbox::ReadString(info.GetStatus().GetPublicContent(), "logs"); HttpToolbox::AnswerText(output, logs); @@ -1887,9 +1387,6 @@ } - - - void RegisterEducationRestApiRoutes() { /** @@ -1940,11 +1437,9 @@ RestApiRouter::RegisterAdministratorRoute<SetLtiClientId>("/education/api/config/lti-client-id"); RestApiRouter::RegisterAdministratorRoute<UploadFile>("/education/api/upload"); - RestApiRouter::RegisterAdministratorRoute<Dicomization>("/education/api/dicomization"); + RestApiRouter::RegisterAdministratorRoute<StartDicomization>("/education/api/dicomization"); + RestApiRouter::RegisterAdministratorRoute<CancelDicomization>("/education/api/dicomization/{}"); RestApiRouter::RegisterAdministratorGetRoute<GetDicomizationLogs>("/education/api/dicomization/{}/logs"); - - engine_.SetWorkersCount(1); - engine_.Start(); } @@ -1966,9 +1461,3 @@ return NULL; } - - -void FinalizeEducationJobsEngine() -{ - engine_.Stop(); -}
--- a/Sources/EducationRestApi.h Wed Oct 22 10:24:53 2025 +0200 +++ b/Sources/EducationRestApi.h Wed Oct 22 10:40:29 2025 +0200 @@ -31,6 +31,3 @@ void RegisterEducationRestApiRoutes(); AuthenticatedUser* AuthenticateFromEducationCookie(const std::list<HttpToolbox::Cookie>& cookies); - -void FinalizeEducationJobsEngine(); -
--- a/Sources/Plugin.cpp Wed Oct 22 10:24:53 2025 +0200 +++ b/Sources/Plugin.cpp Wed Oct 22 10:40:29 2025 +0200 @@ -23,6 +23,7 @@ #include "Dicomization/ActiveUploads.h" +#include "Dicomization/DicomizationJob.h" #include "EducationConfiguration.h" #include "EducationRestApi.h" #include "LTI/LTIRoutes.h" @@ -402,7 +403,7 @@ static boost::thread uploadsCleanerThread_; -static bool uploadsCleanerContinue_; +static bool uploadsCleanerContinue_ = false; static void UploadsCleaner() { @@ -475,10 +476,6 @@ break; } - case OrthancPluginChangeType_OrthancStopped: - FinalizeEducationJobsEngine(); - break; - default: break; } @@ -757,6 +754,8 @@ uploadsCleanerThread_.join(); } + DicomizationJob::FinalizeJobsEngine(); + Orthanc::Toolbox::FinalizeOpenSsl(); Orthanc::Logging::Finalize(); }
--- a/WebApplication/dashboard.html Wed Oct 22 10:24:53 2025 +0200 +++ b/WebApplication/dashboard.html Wed Oct 22 10:40:29 2025 +0200 @@ -103,7 +103,7 @@ </p> <p> This development was partially funded by the - <a href="https://www.lespecialiste.be/fr/actualites/e-health/laquo-un-hopital-virtuel-pour-ameliorer-l-rsquo-enseignement-en-informatique-medical-raquo.html" target="_blank">Virtual Hospital grant</a> at + <a href="https://www.virtual-hospital.org/" target="_blank">Virtual Hospital grant</a> at <a href="https://www.uclouvain.be/facultes/epl" target="_blank">Louvain School of Engineering (EPL)</a>. The platform is built on the top of <a href="https://orthanc.uclouvain.be/" target="_blank">Orthanc</a>, a free and open-source ecosystem for medical imaging that is developed at UCLouvain. @@ -461,6 +461,10 @@ <div class="row justify-content-center"> <div class="col-8"> + <p> + <button type="button" class="btn btn-lg btn-primary" @click="reloadDicomizations()">Refresh</button> + </p> + <table class="table align-middle"> <colgroup> <col style="width: 1%;"> @@ -475,12 +479,12 @@ <th scope="col">Date</th> <th scope="col">Type</th> <th scope="col">Filename</th> - <th scope="col">Logs</th> + <th scope="col"></th> </tr> </thead> <tbody> <tr v-for="item in dicomizations"> - <td> + <td class="text-nowrap"> <span class="badge bg-success" v-if="item.is_success">success</span> <span class="badge bg-danger" v-if="item.is_failure">failure</span> <span class="badge bg-secondary" v-if="!item.is_success && !item.is_failure">{{item.status.toLowerCase()}}</span> @@ -488,8 +492,13 @@ <td class="text-nowrap">{{item.date}}</td> <td>{{item.type}}</td> <td>{{item.name}}</td> - <td> - <button class="btn btn-sm btn-primary ms-2"> + <td class="text-nowrap text-end"> + <button class="btn btn-sm btn-danger" v-if="!item.is_success && !item.is_failure" + data-bs-toggle="tooltip" data-bs-placement="top" title="Cancel the DICOM-ization"> + <i class="fa fa-trash" @click="cancelDicomization(item)"></i> + </button> + <button class="btn btn-sm btn-primary" + data-bs-toggle="tooltip" data-bs-placement="top" title="Open the DICOM-ization logs"> <i class="fa fa-file-text" @click="openLogs(item)"></i> </button> </td>
--- a/WebApplication/dashboard.js Wed Oct 22 10:24:53 2025 +0200 +++ b/WebApplication/dashboard.js Wed Oct 22 10:40:29 2025 +0200 @@ -140,7 +140,7 @@ document.getElementById('pills-images-tab').addEventListener('shown.bs.tab', function (event) { that.reloadProjectsParameters(); - that.projectIdForImages = '_no-project'; + that.projectIdForImages = ''; that.filter = ''; that.selectedViewer = that.config.default_viewer; }); @@ -617,6 +617,14 @@ }); }, + cancelDicomization: function(dicomization) { + var that = this; + axios.delete('../api/dicomization/' + dicomization.id) + .then(function() { + that.reloadDicomizations(); + }); + }, + openLogs: function(dicomization) { var url = '../api/dicomization/' + dicomization.id + '/logs'; window.open(url, '_blank').focus();
