# HG changeset patch # User Sebastien Jodogne # Date 1575896317 -3600 # Node ID c471a0aa137b0d5c939af2311d1e50f2dee9bb87 # Parent a1c0c9c9f9af369ebf5aef690077a936a1a0df3d adding the next generation of loaders diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/DicomResourcesLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomResourcesLoader.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,905 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "DicomResourcesLoader.h" + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomFromFileCommand.h" +# include +#endif + +#include + +namespace OrthancStone +{ + static std::string GetUri(Orthanc::ResourceType level) + { + switch (level) + { + case Orthanc::ResourceType_Patient: + return "patients"; + + case Orthanc::ResourceType_Study: + return "studies"; + + case Orthanc::ResourceType_Series: + return "series"; + + case Orthanc::ResourceType_Instance: + return "instances"; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + class DicomResourcesLoader::Handler : public Orthanc::IDynamicObject + { + private: + boost::shared_ptr loader_; + boost::shared_ptr target_; + int priority_; + DicomSource source_; + boost::shared_ptr userPayload_; + + public: + Handler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + loader_(loader), + target_(target), + priority_(priority), + source_(source), + userPayload_(userPayload) + { + if (!loader || + !target) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + virtual ~Handler() + { + } + + void BroadcastSuccess() + { + SuccessMessage message(*loader_, target_, priority_, source_, userPayload_.get()); + loader_->BroadcastMessage(message); + } + + boost::shared_ptr GetLoader() + { + assert(loader_); + return loader_; + } + + boost::shared_ptr GetTarget() + { + assert(target_); + return target_; + } + + int GetPriority() const + { + return priority_; + } + + const DicomSource& GetSource() const + { + return source_; + } + + const boost::shared_ptr GetUserPayload() const + { + return userPayload_; + } + }; + + + class DicomResourcesLoader::StringHandler : public DicomResourcesLoader::Handler + { + public: + StringHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + Handler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) = 0; + + virtual void HandleString(const std::string& body) + { + Json::Reader reader; + Json::Value value; + if (reader.parse(body, value)) + { + HandleJson(value); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + }; + + + class DicomResourcesLoader::DicomWebHandler : public StringHandler + { + public: + DicomWebHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + StringHandler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + GetTarget()->AddFromDicomWeb(body); + BroadcastSuccess(); + } + }; + + + class DicomResourcesLoader::OrthancHandler : public StringHandler + { + private: + boost::shared_ptr remainingCommands_; + + protected: + void CloseCommand() + { + assert(remainingCommands_); + + if (*remainingCommands_ == 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + (*remainingCommands_) --; + + if (*remainingCommands_ == 0) + { + BroadcastSuccess(); + } + } + + public: + OrthancHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) : + StringHandler(loader, target, priority, source, userPayload), + remainingCommands_(remainingCommands) + { + if (!remainingCommands) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + (*remainingCommands) ++; + } + + boost::shared_ptr GetRemainingCommands() + { + assert(remainingCommands_); + return remainingCommands_; + } + }; + + + class DicomResourcesLoader::OrthancInstanceTagsHandler : public OrthancHandler + { + public: + OrthancInstanceTagsHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + GetTarget()->AddFromOrthanc(body); + CloseCommand(); + } + }; + + + class DicomResourcesLoader::OrthancOneChildInstanceHandler : public OrthancHandler + { + public: + OrthancOneChildInstanceHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + static const char* const ID = "ID"; + + if (body.type() == Json::arrayValue) + { + if (body.size() > 0) + { + if (body[0].type() == Json::objectValue && + body[0].isMember(ID) && + body[0][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), body[0][ID].asString(), GetRemainingCommands(), GetUserPayload()); + CloseCommand(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + }; + + + class DicomResourcesLoader::OrthancAllChildrenInstancesHandler : public OrthancHandler + { + private: + Orthanc::ResourceType bottomLevel_; + + public: + OrthancAllChildrenInstancesHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr remainingCommands, + Orthanc::ResourceType bottomLevel, + boost::shared_ptr userPayload) : + OrthancHandler(loader, target, priority, source, remainingCommands, userPayload), + bottomLevel_(bottomLevel) + { + } + + virtual void HandleJson(const Json::Value& body) + { + static const char* const ID = "ID"; + static const char* const INSTANCES = "Instances"; + + if (body.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < body.size(); i++) + { + switch (bottomLevel_) + { + case Orthanc::ResourceType_Patient: + case Orthanc::ResourceType_Study: + if (body[i].type() == Json::objectValue && + body[i].isMember(ID) && + body[i][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancOneChildInstance + (GetTarget(), GetPriority(), GetSource(), bottomLevel_, + body[i][ID].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + break; + + case Orthanc::ResourceType_Series: + // At the series level, avoid a call to + // "/series/.../instances", as we already have this + // information in the JSON + if (body[i].type() == Json::objectValue && + body[i].isMember(INSTANCES) && + body[i][INSTANCES].type() == Json::arrayValue) + { + if (body[i][INSTANCES].size() > 0) + { + if (body[i][INSTANCES][0].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), + body[i][INSTANCES][0].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + + break; + + case Orthanc::ResourceType_Instance: + if (body[i].type() == Json::objectValue && + body[i].isMember(ID) && + body[i][ID].type() == Json::stringValue) + { + GetLoader()->ScheduleLoadOrthancInstanceTags + (GetTarget(), GetPriority(), GetSource(), + body[i][ID].asString(), GetRemainingCommands(), GetUserPayload()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + } + + CloseCommand(); + } + }; + + +#if ORTHANC_ENABLE_DCMTK == 1 + static void ExploreDicomDir(OrthancStone::LoadedDicomResources& instances, + const Orthanc::ParsedDicomDir& dicomDir, + Orthanc::ResourceType level, + size_t index, + const Orthanc::DicomMap& parent) + { + std::string expectedType; + + switch (level) + { + case Orthanc::ResourceType_Patient: + expectedType = "PATIENT"; + break; + + case Orthanc::ResourceType_Study: + expectedType = "STUDY"; + break; + + case Orthanc::ResourceType_Series: + expectedType = "SERIES"; + break; + + case Orthanc::ResourceType_Instance: + expectedType = "IMAGE"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + for (;;) + { + std::auto_ptr current(dicomDir.GetItem(index).Clone()); + current->RemoveBinaryTags(); + current->Merge(parent); + + std::string type; + if (!current->LookupStringValue(type, Orthanc::DICOM_TAG_DIRECTORY_RECORD_TYPE, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + if (type == expectedType) + { + if (level == Orthanc::ResourceType_Instance) + { + instances.AddResource(*current); + } + else + { + size_t lower; + if (dicomDir.LookupLower(lower, index)) + { + ExploreDicomDir(instances, dicomDir, Orthanc::GetChildResourceType(level), lower, *current); + } + } + } + + size_t next; + if (dicomDir.LookupNext(next, index)) + { + index = next; + } + else + { + return; + } + } + } +#endif + + +#if ORTHANC_ENABLE_DCMTK == 1 + void DicomResourcesLoader::GetDicomDirInstances(LoadedDicomResources& target, + const Orthanc::ParsedDicomDir& dicomDir) + { + Orthanc::DicomMap parent; + ExploreDicomDir(target, dicomDir, Orthanc::ResourceType_Patient, 0, parent); + } +#endif + + +#if ORTHANC_ENABLE_DCMTK == 1 + class DicomResourcesLoader::DicomDirHandler : public StringHandler + { + public: + DicomDirHandler(boost::shared_ptr loader, + boost::shared_ptr target, + int priority, + const DicomSource& source, + boost::shared_ptr userPayload) : + StringHandler(loader, target, priority, source, userPayload) + { + } + + virtual void HandleJson(const Json::Value& body) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + virtual void HandleString(const std::string& body) + { + Orthanc::ParsedDicomDir dicomDir(body); + GetDicomDirInstances(*GetTarget(), dicomDir); + BroadcastSuccess(); + } + }; +#endif + + + void DicomResourcesLoader::Handle(const HttpCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer()); + } + } + + + void DicomResourcesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast(message.GetOrigin().GetPayload()).HandleString(message.GetAnswer()); + } + } + + + void DicomResourcesLoader::Handle(const ReadFileCommand::SuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + dynamic_cast(message.GetOrigin().GetPayload()).HandleString(message.GetContent()); + } + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void DicomResourcesLoader::Handle(const ParseDicomSuccessMessage& message) + { + if (message.GetOrigin().HasPayload()) + { + Handler& handler = dynamic_cast(message.GetOrigin().GetPayload()); + + std::set ignoreTagLength; + ignoreTagLength.insert(Orthanc::DICOM_TAG_GRID_FRAME_OFFSET_VECTOR); // Needed for RT-DOSE + + Orthanc::DicomMap summary; + message.GetDicom().ExtractDicomSummary(summary, ignoreTagLength); + handler.GetTarget()->AddResource(summary); + + handler.BroadcastSuccess(); + } + } +#endif + + + void DicomResourcesLoader::Handle(const OracleCommandExceptionMessage& message) + { + // TODO + LOG(ERROR) << "Exception: " << message.GetException().What(); + } + + + void DicomResourcesLoader::ScheduleLoadOrthancInstanceTags(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& instanceId, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instanceId + "/tags"); + command->AcquirePayload(new OrthancInstanceTagsHandler(shared_from_this(), target, priority, + source, remainingCommands, userPayload)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleLoadOrthancOneChildInstance(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload) + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/" + GetUri(level) + "/" + id + "/instances"); + command->AcquirePayload(new OrthancOneChildInstanceHandler(shared_from_this(), target, priority, + source, remainingCommands, userPayload)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + + const Orthanc::IDynamicObject& DicomResourcesLoader::SuccessMessage::GetUserPayload() const + { + if (userPayload_ == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *userPayload_; + } + } + + + boost::shared_ptr DicomResourcesLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr result(new DicomResourcesLoader(stone.GetContext())); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + result->Register(stone.GetOracleObservable(), &DicomResourcesLoader::Handle); +#endif + + return boost::shared_ptr(result); + } + + + static void SetIncludeTags(std::map& arguments, + const std::set& includeTags) + { + if (!includeTags.empty()) + { + std::string s; + bool first = true; + + for (std::set::const_iterator + it = includeTags.begin(); it != includeTags.end(); ++it) + { + if (first) + { + first = false; + } + else + { + s += ","; + } + + char buf[16]; + sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement()); + s += std::string(buf); + } + + arguments["includefield"] = s; + } + } + + + void DicomResourcesLoader::ScheduleWado(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& uri, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source"); + } + + std::map arguments, headers; + SetIncludeTags(arguments, includeTags); + + std::auto_ptr command( + source.CreateDicomWebCommand(uri, arguments, headers, + new DicomWebHandler(shared_from_this(), target, priority, source, protection))); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleQido(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const Orthanc::DicomMap& filter, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMweb source"); + } + + std::string uri; + switch (level) + { + case Orthanc::ResourceType_Study: + uri = "/studies"; + break; + + case Orthanc::ResourceType_Series: + uri = "/series"; + break; + + case Orthanc::ResourceType_Instance: + uri = "/instances"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + std::set tags; + filter.GetTags(tags); + + std::map arguments, headers; + + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::string s; + if (filter.LookupStringValue(s, *it, false /* no binary */)) + { + char buf[16]; + sprintf(buf, "%04X%04X", it->GetGroup(), it->GetElement()); + arguments[buf] = s; + } + } + + SetIncludeTags(arguments, includeTags); + + std::auto_ptr command( + source.CreateDicomWebCommand(uri, arguments, headers, + new DicomWebHandler(shared_from_this(), target, priority, source, protection))); + + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + + + void DicomResourcesLoader::ScheduleLoadOrthancResources(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType topLevel, + const std::string& topId, + Orthanc::ResourceType bottomLevel, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsOrthanc()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not an Orthanc source"); + } + + bool ok = false; + + switch (topLevel) + { + case Orthanc::ResourceType_Patient: + ok = (bottomLevel == Orthanc::ResourceType_Patient || + bottomLevel == Orthanc::ResourceType_Study || + bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Study: + ok = (bottomLevel == Orthanc::ResourceType_Study || + bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Series: + ok = (bottomLevel == Orthanc::ResourceType_Series || + bottomLevel == Orthanc::ResourceType_Instance); + break; + + case Orthanc::ResourceType_Instance: + ok = (bottomLevel == Orthanc::ResourceType_Instance); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (!ok) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + boost::shared_ptr remainingCommands(new unsigned int(0)); + + if (topLevel == Orthanc::ResourceType_Instance) + { + ScheduleLoadOrthancInstanceTags(target, priority, source, topId, remainingCommands, protection); + } + else if (topLevel == bottomLevel) + { + ScheduleLoadOrthancOneChildInstance(target, priority, source, topLevel, topId, remainingCommands, protection); + } + else + { + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/" + GetUri(topLevel) + "/" + topId + "/" + GetUri(bottomLevel)); + command->AcquirePayload(new OrthancAllChildrenInstancesHandler + (shared_from_this(), target, priority, source, + remainingCommands, bottomLevel, protection)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + } + + + void DicomResourcesLoader::ScheduleLoadDicomDir(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + + if (!source.IsDicomDir()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Not a DICOMDIR source"); + } + + if (target->GetIndexedTag() == Orthanc::DICOM_TAG_SOP_INSTANCE_UID) + { + LOG(WARNING) << "If loading DICOMDIR, it is advised to index tag " + << "ReferencedSopInstanceUidInFile (0004,1511)"; + } + +#if ORTHANC_ENABLE_DCMTK == 1 + std::auto_ptr command(new ReadFileCommand(path)); + command->AcquirePayload(new DicomDirHandler(shared_from_this(), target, priority, source, protection)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOMDIR"); +#endif + } + + + void DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + bool includePixelData, + Orthanc::IDynamicObject* userPayload) + { + boost::shared_ptr protection(userPayload); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::auto_ptr command(new ParseDicomFromFileCommand(path)); + command->SetPixelDataIncluded(includePixelData); + command->AcquirePayload(new Handler(shared_from_this(), target, priority, source, protection)); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOM files"); +#endif + } + + + bool DicomResourcesLoader::ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& dicomDirPath, + const Orthanc::DicomMap& dicomDirEntry, + bool includePixelData, + Orthanc::IDynamicObject* userPayload) + { + std::auto_ptr protection(userPayload); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::string file; + if (dicomDirEntry.LookupStringValue(file, Orthanc::DICOM_TAG_REFERENCED_FILE_ID, false)) + { + ScheduleLoadDicomFile(target, priority, source, ParseDicomFromFileCommand::GetDicomDirPath(dicomDirPath, file), + includePixelData, protection.release()); + return true; + } + else + { + return false; + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "DCMTK is disabled, cannot load DICOM files"); +#endif + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/DicomResourcesLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomResourcesLoader.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,220 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomFromFileCommand.h" +# include +#endif + +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "../Oracle/ReadFileCommand.h" +#include "DicomSource.h" +#include "ILoaderFactory.h" +#include "LoadedDicomResources.h" +#include "OracleScheduler.h" + +namespace OrthancStone +{ + class DicomResourcesLoader : + public ObserverBase, + public IObservable + { + private: + class Handler; + class StringHandler; + class DicomWebHandler; + class OrthancHandler; + class OrthancInstanceTagsHandler; + class OrthancOneChildInstanceHandler; + class OrthancAllChildrenInstancesHandler; + +#if ORTHANC_ENABLE_DCMTK == 1 + class DicomDirHandler; +#endif + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const ReadFileCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void ScheduleLoadOrthancInstanceTags(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& instanceId, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload); + + void ScheduleLoadOrthancOneChildInstance(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + boost::shared_ptr remainingCommands, + boost::shared_ptr userPayload); + + DicomResourcesLoader(ILoadersContext& context) : + context_(context) + { + } + + ILoadersContext& context_; + + + public: + class SuccessMessage : public OrthancStone::OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + boost::shared_ptr resources_; + int priority_; + const DicomSource& source_; + const Orthanc::IDynamicObject* userPayload_; + + public: + SuccessMessage(const DicomResourcesLoader& origin, + boost::shared_ptr resources, + int priority, + const DicomSource& source, + const Orthanc::IDynamicObject* userPayload) : + OriginMessage(origin), + resources_(resources), + priority_(priority), + source_(source), + userPayload_(userPayload) + { + } + + int GetPriority() const + { + return priority_; + } + + const boost::shared_ptr GetResources() const + { + return resources_; + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + bool HasUserPayload() const + { + return userPayload_ != NULL; + } + + const Orthanc::IDynamicObject& GetUserPayload() const; + }; + + + class Factory : public ILoaderFactory + { + public: + virtual boost::shared_ptr Create(ILoadersContext::ILock& stone); + }; + + void ScheduleWado(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& uri, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload); + + void ScheduleWado(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& uri, + Orthanc::IDynamicObject* userPayload) + { + std::set includeTags; + ScheduleWado(target, priority, source, uri, includeTags, userPayload); + } + + void ScheduleQido(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const Orthanc::DicomMap& filter, + const std::set& includeTags, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadOrthancResources(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType topLevel, + const std::string& topId, + Orthanc::ResourceType bottomLevel, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadOrthancResource(boost::shared_ptr target, + int priority, + const DicomSource& source, + Orthanc::ResourceType level, + const std::string& id, + Orthanc::IDynamicObject* userPayload) + { + ScheduleLoadOrthancResources(target, priority, source, level, id, level, userPayload); + } + +#if ORTHANC_ENABLE_DCMTK == 1 + static void GetDicomDirInstances(LoadedDicomResources& target, + const Orthanc::ParsedDicomDir& dicomDir); +#endif + + void ScheduleLoadDicomDir(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + Orthanc::IDynamicObject* userPayload); + + void ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& path, + bool includePixelData, + Orthanc::IDynamicObject* userPayload); + + bool ScheduleLoadDicomFile(boost::shared_ptr target, + int priority, + const DicomSource& source, + const std::string& dicomDirPath, + const Orthanc::DicomMap& dicomDirEntry, + bool includePixelData, + Orthanc::IDynamicObject* userPayload); + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/DicomSource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomSource.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,356 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "DicomSource.h" + +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OrthancRestApiCommand.h" + +#include + +#include + +namespace OrthancStone +{ + static std::string EncodeGetArguments(const std::string& uri, + const std::map& arguments) + { + std::string s = uri; + bool first = true; + + for (std::map::const_iterator + it = arguments.begin(); it != arguments.end(); ++it) + { + if (first) + { + s += "?"; + first = false; + } + else + { + s += "&"; + } + + s += it->first + "=" + it->second; + } + + // TODO: Call Orthanc::Toolbox::UriEncode() ? + + return s; + } + + + void DicomSource::SetOrthancSource(const Orthanc::WebServiceParameters& parameters) + { + type_ = DicomSourceType_Orthanc; + webService_ = parameters; + hasOrthancWebViewer1_ = false; + hasOrthancAdvancedPreview_ = false; + } + + + void DicomSource::SetOrthancSource() + { + Orthanc::WebServiceParameters parameters; + parameters.SetUrl("http://localhost:8042/"); + SetOrthancSource(parameters); + } + + + const Orthanc::WebServiceParameters& DicomSource::GetOrthancParameters() const + { + if (type_ == DicomSourceType_Orthanc || + type_ == DicomSourceType_DicomWebThroughOrthanc) + { + return webService_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomSource::SetDicomDirSource() + { + type_ = DicomSourceType_DicomDir; + } + + + void DicomSource::SetDicomWebSource(const std::string& baseUrl) + { + type_ = DicomSourceType_DicomWeb; + webService_.SetUrl(baseUrl); + webService_.ClearCredentials(); + } + + + void DicomSource::SetDicomWebSource(const std::string& baseUrl, + const std::string& username, + const std::string& password) + { + type_ = DicomSourceType_DicomWeb; + webService_.SetUrl(baseUrl); + webService_.SetCredentials(username, password); + } + + + void DicomSource::SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters, + const std::string& dicomWebRoot, + const std::string& serverName) + { + type_ = DicomSourceType_DicomWebThroughOrthanc; + webService_ = orthancParameters; + orthancDicomWebRoot_ = dicomWebRoot; + serverName_ = serverName; + } + + + void DicomSource::SetDicomWebThroughOrthancSource(const std::string& serverName) + { + Orthanc::WebServiceParameters orthanc; + orthanc.SetUrl("http://localhost:8042/"); + SetDicomWebThroughOrthancSource(orthanc, "/dicom-web/", serverName); + } + + + bool DicomSource::IsDicomWeb() const + { + return (type_ == DicomSourceType_DicomWeb || + type_ == DicomSourceType_DicomWebThroughOrthanc); + } + + + IOracleCommand* DicomSource::CreateDicomWebCommand(const std::string& uri, + const std::map& arguments, + const std::map& headers, + Orthanc::IDynamicObject* payload) const + { + std::auto_ptr protection(payload); + + switch (type_) + { + case DicomSourceType_DicomWeb: + { + std::auto_ptr command(new HttpCommand); + + command->SetMethod(Orthanc::HttpMethod_Get); + command->SetUrl(webService_.GetUrl() + "/" + EncodeGetArguments(uri, arguments)); + command->SetHttpHeaders(webService_.GetHttpHeaders()); + + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + command->SetHttpHeader(it->first, it->second); + } + + if (!webService_.GetUsername().empty()) + { + command->SetCredentials(webService_.GetUsername(), webService_.GetPassword()); + } + + if (protection.get()) + { + command->AcquirePayload(protection.release()); + } + + return command.release(); + } + + case DicomSourceType_DicomWebThroughOrthanc: + { + Json::Value args = Json::objectValue; + for (std::map::const_iterator + it = arguments.begin(); it != arguments.end(); ++it) + { + args[it->first] = it->second; + } + + Json::Value h = Json::objectValue; + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + h[it->first] = it->second; + } + + Json::Value body = Json::objectValue; + body["Uri"] = uri; + body["Arguments"] = args; + body["Headers"] = h; + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetMethod(Orthanc::HttpMethod_Post); + command->SetUri(orthancDicomWebRoot_ + "/servers/" + serverName_ + "/get"); + command->SetBody(body); + + if (protection.get()) + { + command->AcquirePayload(protection.release()); + } + + return command.release(); + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void DicomSource::AutodetectOrthancFeatures(const std::string& system, + const std::string& plugins) + { + static const char* const REST_API_VERSION = "ApiVersion"; + + if (IsDicomWeb()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + Json::Value a, b; + Json::Reader reader; + if (reader.parse(system, a) && + reader.parse(plugins, b) && + a.type() == Json::objectValue && + b.type() == Json::arrayValue && + a.isMember(REST_API_VERSION) && + a[REST_API_VERSION].type() == Json::intValue) + { + SetOrthancAdvancedPreview(a[REST_API_VERSION].asInt() >= 5); + + hasOrthancWebViewer1_ = false; + + for (Json::Value::ArrayIndex i = 0; i < b.size(); i++) + { + if (b[i].type() != Json::stringValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + if (boost::iequals(b[i].asString(), "web-viewer")) + { + hasOrthancWebViewer1_ = true; + } + } + } + else + { + printf("[%s] [%s]\n", system.c_str(), plugins.c_str()); + + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + } + + + void DicomSource::SetOrthancWebViewer1(bool hasPlugin) + { + if (IsOrthanc()) + { + hasOrthancWebViewer1_ = hasPlugin; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasOrthancWebViewer1() const + { + if (IsOrthanc()) + { + return hasOrthancWebViewer1_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void DicomSource::SetOrthancAdvancedPreview(bool hasFeature) + { + if (IsOrthanc()) + { + hasOrthancAdvancedPreview_ = hasFeature; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasOrthancAdvancedPreview() const + { + if (IsOrthanc()) + { + return hasOrthancAdvancedPreview_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + void DicomSource::SetDicomWebRendered(bool hasFeature) + { + if (IsDicomWeb()) + { + hasDicomWebRendered_ = hasFeature; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + bool DicomSource::HasDicomWebRendered() const + { + if (IsDicomWeb()) + { + return hasDicomWebRendered_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + unsigned int DicomSource::GetQualityCount() const + { + if (IsDicomWeb()) + { + return (HasDicomWebRendered() ? 2 : 1); + } + else if (IsOrthanc()) + { + return (HasOrthancWebViewer1() || + HasOrthancAdvancedPreview() ? 2 : 1); + } + else if (IsDicomDir()) + { + return 1; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/DicomSource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomSource.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,118 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "../Oracle/IOracleCommand.h" + +#include + +namespace OrthancStone +{ + enum DicomSourceType + { + DicomSourceType_Orthanc, + DicomSourceType_DicomWeb, + DicomSourceType_DicomWebThroughOrthanc, + DicomSourceType_DicomDir + }; + + + class DicomSource + { + private: + DicomSourceType type_; + Orthanc::WebServiceParameters webService_; + std::string orthancDicomWebRoot_; + std::string serverName_; + bool hasOrthancWebViewer1_; + bool hasOrthancAdvancedPreview_; + bool hasDicomWebRendered_; + + public: + DicomSource() : + hasOrthancWebViewer1_(false), + hasOrthancAdvancedPreview_(false), + hasDicomWebRendered_(false) + { + SetOrthancSource(); + } + + DicomSourceType GetType() const + { + return type_; + } + + void SetOrthancSource(); + + void SetOrthancSource(const Orthanc::WebServiceParameters& parameters); + + const Orthanc::WebServiceParameters& GetOrthancParameters() const; + + void SetDicomDirSource(); + + void SetDicomWebSource(const std::string& baseUrl); + + void SetDicomWebSource(const std::string& baseUrl, + const std::string& username, + const std::string& password); + + void SetDicomWebThroughOrthancSource(const Orthanc::WebServiceParameters& orthancParameters, + const std::string& dicomWebRoot, + const std::string& serverName); + + void SetDicomWebThroughOrthancSource(const std::string& serverName); + + bool IsDicomWeb() const; + + bool IsOrthanc() const + { + return type_ == DicomSourceType_Orthanc; + } + + bool IsDicomDir() const + { + return type_ == DicomSourceType_DicomDir; + } + + IOracleCommand* CreateDicomWebCommand(const std::string& uri, + const std::map& arguments, + const std::map& headers, + Orthanc::IDynamicObject* payload /* takes ownership */) const; + + void AutodetectOrthancFeatures(const std::string& system, + const std::string& plugins); + + void SetOrthancWebViewer1(bool hasPlugin); + + bool HasOrthancWebViewer1() const; + + void SetOrthancAdvancedPreview(bool hasFeature); + + bool HasOrthancAdvancedPreview() const; + + void SetDicomWebRendered(bool hasFeature); + + bool HasDicomWebRendered() const; + + unsigned int GetQualityCount() const; + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/DicomVolumeLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomVolumeLoader.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,182 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "DicomVolumeLoader.h" + +#include + +namespace OrthancStone +{ + DicomVolumeLoader::DicomVolumeLoader(boost::shared_ptr& framesLoader, + bool computeRange) : + framesLoader_(framesLoader), + isValid_(false), + started_(false), + remaining_(0) + { + volume_.reset(new OrthancStone::DicomVolumeImage); + + const SeriesOrderedFrames& frames = framesLoader_->GetOrderedFrames(); + + if (frames.IsRegular3DVolume() && + frames.GetFramesCount() > 0) + { + // TODO - Is "0" the good choice for the reference frame? + // Shouldn't we use "count - 1" depending on the direction + // of the normal? + const OrthancStone::DicomInstanceParameters& parameters = frames.GetInstanceParameters(0); + + OrthancStone::CoordinateSystem3D plane(frames.GetInstance(0)); + + OrthancStone::VolumeImageGeometry geometry; + geometry.SetSizeInVoxels(parameters.GetImageInformation().GetWidth(), + parameters.GetImageInformation().GetHeight(), + static_cast(frames.GetFramesCount())); + geometry.SetAxialGeometry(plane); + + double spacing; + if (parameters.GetSopClassUid() == SopClassUid_RTDose) + { + if (!parameters.ComputeRegularSpacing(spacing)) + { + LOG(WARNING) << "Unable to compute the spacing in a RT-DOSE instance"; + spacing = frames.GetSpacingBetweenSlices(); + } + } + else + { + spacing = frames.GetSpacingBetweenSlices(); + } + + geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), + parameters.GetPixelSpacingY(), spacing); + volume_->Initialize(geometry, parameters.GetExpectedPixelFormat(), computeRange); + volume_->GetPixelData().Clear(); + volume_->SetDicomParameters(parameters); + + remaining_ = frames.GetFramesCount(); + isValid_ = true; + } + else + { + LOG(WARNING) << "Not a regular 3D volume"; + } + } + + + void DicomVolumeLoader::Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message) + { + if (remaining_ == 0 || + !message.HasUserPayload()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + if (message.GetImage().GetWidth() != volume_->GetPixelData().GetWidth() || + message.GetImage().GetHeight() != volume_->GetPixelData().GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + + if (message.GetImage().GetFormat() != volume_->GetPixelData().GetFormat()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat); + } + + if (message.GetFrameIndex() >= volume_->GetPixelData().GetDepth()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + size_t frameIndex = dynamic_cast&>(message.GetUserPayload()).GetValue(); + + { + ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, frameIndex); + Orthanc::ImageProcessing::Copy(writer.GetAccessor(), message.GetImage()); + } + + volume_->IncrementRevision(); + + { + VolumeUpdatedMessage updated(*this, frameIndex); + BroadcastMessage(updated); + } + + remaining_--; + + if (remaining_ == 0) + { + VolumeReadyMessage ready(*this); + BroadcastMessage(ready); + } + } + + + DicomVolumeLoader::Factory::Factory(LoadedDicomResources& instances) : + framesFactory_(instances), + computeRange_(false) + { + } + + DicomVolumeLoader::Factory::Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata) : + framesFactory_(metadata.GetInstances()), + computeRange_(false) + { + SetDicomDir(metadata.GetDicomDirPath(), metadata.GetDicomDir()); // Only useful for DICOMDIR sources + } + + + boost::shared_ptr DicomVolumeLoader::Factory::Create(ILoadersContext::ILock& context) + { + boost::shared_ptr frames = + boost::dynamic_pointer_cast(framesFactory_.Create(context)); + + boost::shared_ptr volume(new DicomVolumeLoader(frames, computeRange_)); + volume->Register(*frames, &DicomVolumeLoader::Handle); + + return volume; + } + + void DicomVolumeLoader::Start(int priority, + const DicomSource& source) + { + if (started_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + started_ = true; + + if (IsValid()) + { + for (size_t i = 0; i < GetOrderedFrames().GetFramesCount(); i++) + { + framesLoader_->ScheduleLoadFrame(priority, source, i, source.GetQualityCount() - 1, + new Orthanc::SingleValueObject(i)); + } + } + else + { + VolumeReadyMessage ready(*this); + BroadcastMessage(ready); + } + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/DicomVolumeLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/DicomVolumeLoader.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,141 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "../Volumes/DicomVolumeImage.h" +#include "SeriesFramesLoader.h" +#include "SeriesMetadataLoader.h" + +namespace OrthancStone +{ + class DicomVolumeLoader : + public ObserverBase, + public IObservable + { + private: + boost::shared_ptr framesLoader_; + boost::shared_ptr volume_; + bool isValid_; + bool started_; + size_t remaining_; + + DicomVolumeLoader(boost::shared_ptr& framesLoader, + bool computeRange); + + void Handle(const OrthancStone::SeriesFramesLoader::FrameLoadedMessage& message); + + public: + class VolumeReadyMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + public: + VolumeReadyMessage(const DicomVolumeLoader& loader) : + OriginMessage(loader) + { + } + + const DicomVolumeImage& GetVolume() const + { + assert(GetOrigin().GetVolume()); + return *GetOrigin().GetVolume(); + } + }; + + + class VolumeUpdatedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + unsigned int axial_; + + public: + VolumeUpdatedMessage(const DicomVolumeLoader& loader, + unsigned int axial) : + OriginMessage(loader), + axial_(axial) + { + } + + unsigned int GetAxialIndex() const + { + return axial_; + } + + const DicomVolumeImage& GetVolume() const + { + assert(GetOrigin().GetVolume()); + return *GetOrigin().GetVolume(); + } + }; + + + class Factory : public ILoaderFactory + { + private: + SeriesFramesLoader::Factory framesFactory_; + bool computeRange_; + + public: + Factory(LoadedDicomResources& instances); + + Factory(const SeriesMetadataLoader::SeriesLoadedMessage& metadata); + + void SetComputeRange(bool computeRange) + { + computeRange_ = computeRange; + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir) + { + framesFactory_.SetDicomDir(dicomDirPath, dicomDir); + } + + virtual boost::shared_ptr Create(ILoadersContext::ILock& context) ORTHANC_OVERRIDE; + }; + + bool IsValid() const + { + return isValid_; + } + + bool IsFullyLoaded() const + { + return remaining_ == 0; + } + + boost::shared_ptr GetVolume() const + { + return volume_; + } + + const SeriesOrderedFrames& GetOrderedFrames() const + { + return framesLoader_->GetOrderedFrames(); + } + + void Start(int priority, + const DicomSource& source); + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/GenericLoadersContext.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/GenericLoadersContext.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,185 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "GenericLoadersContext.h" + +namespace OrthancStone +{ + class GenericLoadersContext::Locker : public ILoadersContext::ILock + { + private: + GenericLoadersContext& that_; + boost::recursive_mutex::scoped_lock lock_; + + public: + Locker(GenericLoadersContext& that) : + that_(that), + lock_(that.mutex_) + { + } + + virtual ILoadersContext& GetContext() const ORTHANC_OVERRIDE + { + return that_; + }; + + virtual void AddLoader(boost::shared_ptr loader) ORTHANC_OVERRIDE + { + that_.loaders_.push_back(loader); + } + + virtual IObservable& GetOracleObservable() const ORTHANC_OVERRIDE + { + return that_.oracleObservable_; + } + + virtual void Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */) ORTHANC_OVERRIDE + { + that_.scheduler_->Schedule(receiver, priority, command); + }; + + virtual void CancelRequests(boost::shared_ptr receiver) ORTHANC_OVERRIDE + { + that_.scheduler_->CancelRequests(receiver); + } + + virtual void CancelAllRequests() ORTHANC_OVERRIDE + { + that_.scheduler_->CancelAllRequests(); + } + }; + + + void GenericLoadersContext::EmitMessage(boost::weak_ptr observer, + const IMessage& message) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + //LOG(INFO) << " inside emit lock: " << message.GetIdentifier().AsString(); + oracleObservable_.EmitMessage(observer, message); + //LOG(INFO) << " outside emit lock"; + } + + + GenericLoadersContext::GenericLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + oracle_.reset(new ThreadedOracle(*this)); + scheduler_ = OracleScheduler::Create(*oracle_, oracleObservable_, *this, + maxHighPriority, maxStandardPriority, maxLowPriority); + } + + + GenericLoadersContext::~GenericLoadersContext() + { + LOG(WARNING) << "scheduled commands: " << scheduler_->GetTotalScheduled() + << ", processed commands: " << scheduler_->GetTotalProcessed(); + scheduler_.reset(); + //LOG(INFO) << "counter: " << scheduler_.use_count(); + } + + + void GenericLoadersContext::SetOrthancParameters(const Orthanc::WebServiceParameters& parameters) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetOrthancParameters(parameters); + } + + + void GenericLoadersContext::SetRootDirectory(const std::string& root) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetRootDirectory(root); + } + + + void GenericLoadersContext::SetDicomCacheSize(size_t size) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->SetDicomCacheSize(size); + } + + + void GenericLoadersContext::StartOracle() + { + boost::recursive_mutex::scoped_lock lock(mutex_); + oracle_->Start(); + //LOG(INFO) << "STARTED ORACLE"; + } + + + void GenericLoadersContext::StopOracle() + { + /** + * DON'T lock "mutex_" here, otherwise Stone won't be able to + * stop if one command being executed by the oracle has to emit + * a message (method "EmitMessage()" would have to lock the + * mutex too). + **/ + + //LOG(INFO) << "STOPPING ORACLE"; + oracle_->Stop(); + //LOG(INFO) << "STOPPED ORACLE"; + } + + + void GenericLoadersContext::WaitUntilComplete() + { + for (;;) + { + { + boost::recursive_mutex::scoped_lock lock(mutex_); + if (scheduler_ && + scheduler_->GetTotalScheduled() == scheduler_->GetTotalProcessed()) + { + return; + } + } + + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + } + } + + + ILoadersContext::ILock* GenericLoadersContext::Lock() + { + return new Locker(*this); + } + + + void GenericLoadersContext::GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) + { + boost::recursive_mutex::scoped_lock lock(mutex_); + if (scheduler_) + { + scheduledCommands = scheduler_->GetTotalScheduled(); + processedCommands = scheduler_->GetTotalProcessed(); + } + else + { + scheduledCommands = 0; + processedCommands = 0; + } + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/GenericLoadersContext.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/GenericLoadersContext.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,85 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "../Messages/IMessageEmitter.h" +#include "../Oracle/ThreadedOracle.h" +#include "DicomSource.h" +#include "ILoaderFactory.h" +#include "OracleScheduler.h" + +#include +#include + +#include + +namespace OrthancStone +{ + class GenericLoadersContext : + public ILoadersContext, + private IMessageEmitter + { + private: + class Locker; + + // "Recursive mutex" is necessary, to be able to run + // "ILoaderFactory" from a message handler triggered by + // "EmitMessage()" + boost::recursive_mutex mutex_; + + IObservable oracleObservable_; + std::auto_ptr oracle_; + boost::shared_ptr scheduler_; + + // Necessary to keep the loaders persistent (including global + // function promises), after the function that created them is + // left. This avoids creating one global variable for each loader. + std::list< boost::shared_ptr > loaders_; + + virtual void EmitMessage(boost::weak_ptr observer, + const IMessage& message); + + public: + GenericLoadersContext(unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + virtual ~GenericLoadersContext(); + + virtual ILock* Lock() ORTHANC_OVERRIDE; + + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) ORTHANC_OVERRIDE; + + void SetOrthancParameters(const Orthanc::WebServiceParameters& parameters); + + void SetRootDirectory(const std::string& root); + + void SetDicomCacheSize(size_t size); + + void StartOracle(); + + void StopOracle(); + + void WaitUntilComplete(); + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/ILoaderFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/ILoaderFactory.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,41 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "ILoadersContext.h" + +namespace OrthancStone +{ + class ILoaderFactory : public boost::noncopyable + { + public: + virtual ~ILoaderFactory() + { + } + + /** + * Factory function that creates a new loader, to be used by the + * Stone loaders context. + **/ + virtual boost::shared_ptr Create(ILoadersContext::ILock& context) = 0; + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/ILoadersContext.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/ILoadersContext.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,122 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "../Messages/IObserver.h" +#include "../Messages/IObservable.h" +#include "../Oracle/IOracleCommand.h" + +#include + +namespace OrthancStone +{ + class ILoadersContext : public boost::noncopyable + { + public: + class ILock : public boost::noncopyable + { + public: + virtual ~ILock() + { + } + + /** + * This method is useful for loaders that must be able to + * re-lock the Stone loaders context in the future (for instance + * to schedule new commands once some command is processed). + **/ + virtual ILoadersContext& GetContext() const = 0; + + /** + * Get a reference to the observable against which a loader must + * listen to be informed of messages issued by the oracle once + * some command is processed. + **/ + virtual IObservable& GetOracleObservable() const = 0; + + /** + * Schedule a new command for further processing by the + * oracle. The "receiver" argument indicates to which object the + * notification messages are sent by the oracle upon completion + * of the command. The command is possibly not directly sent to + * the oracle: Instead, an internal "OracleScheduler" object is + * often used as a priority queue to rule the order in which + * commands are actually sent to the oracle. Hence the + * "priority" argument (commands with lower value are executed + * first). + **/ + virtual void Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */) = 0; + + /** + * Cancel all the commands that are waiting in the + * "OracleScheduler" queue and that are linked to the given + * receiver (i.e. the observer that was specified at the time + * method "Schedule()" was called). This is useful for real-time + * processing, as it allows to replace commands that were + * scheduled in the past by more urgent commands. + * + * Note that this call does not affect commands that would have + * already be sent to the oracle. As a consequence, the receiver + * might still receive messages that were sent to the oracle + * before the cancellation (be prepared to handle such + * messages). + **/ + virtual void CancelRequests(boost::shared_ptr receiver) = 0; + + /** + * Same as "CancelRequests()", but targets all the receivers. + **/ + virtual void CancelAllRequests() = 0; + + /** + * Add a reference to the given observer in the Stone loaders + * context. This can be used to match the lifetime of a loader + * with the lifetime of the Stone context: This is useful if + * your Stone application does not keep a reference to the + * loader by itself (typically in global promises), which would + * make the loader disappear as soon as the scope of the + * variable is left. + **/ + virtual void AddLoader(boost::shared_ptr loader) = 0; + }; + + /** + * Locks the Stone loaders context, to give access to its + * underlying features. This is important for Stone applications + * running in a multi-threaded environment, for which a global + * mutex is locked. + **/ + virtual ILock* Lock() = 0; + + /** + * Returns the number of commands that were scheduled and + * processed using the "ILock::Schedule()" method. By "processed" + * commands, we refer to the number of commands that were either + * executed by the oracle, or canceled by the user. So the + * counting sequences are monotonically increasing over time. + **/ + virtual void GetStatistics(uint64_t& scheduledCommands, + uint64_t& processedCommands) = 0; + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/LoadedDicomResources.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoadedDicomResources.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,216 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "LoadedDicomResources.h" + +#include + +#include + + +namespace OrthancStone +{ + void LoadedDicomResources::Flatten() + { + // Lazy generation of a "std::vector" from the "std::map" + if (flattened_.empty()) + { + flattened_.resize(resources_.size()); + + size_t pos = 0; + for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + flattened_[pos++] = it->second; + } + } + else + { + // No need to flatten + assert(flattened_.size() == resources_.size()); + } + } + + + void LoadedDicomResources::AddFromDicomWebInternal(const Json::Value& dicomweb) + { + assert(dicomweb.type() == Json::objectValue); + Orthanc::DicomMap dicom; + dicom.FromDicomWeb(dicomweb); + AddResource(dicom); + } + + + LoadedDicomResources::LoadedDicomResources(const LoadedDicomResources& other, + const Orthanc::DicomTag& indexedTag) : + indexedTag_(indexedTag) + { + for (Resources::const_iterator it = other.resources_.begin(); + it != other.resources_.end(); ++it) + { + assert(it->second != NULL); + AddResource(*it->second); + } + } + + void LoadedDicomResources::Clear() + { + for (Resources::iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + resources_.clear(); + flattened_.clear(); + } + + + Orthanc::DicomMap& LoadedDicomResources::GetResource(size_t index) + { + Flatten(); + + if (index >= flattened_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(flattened_[index] != NULL); + return *flattened_[index]; + } + } + + + bool LoadedDicomResources::LookupStringValue(std::string& target, + const std::string& id, + const Orthanc::DicomTag& tag) const + { + Resources::const_iterator found = resources_.find(id); + + if (found == resources_.end()) + { + return false; + } + else + { + assert(found->second != NULL); + return found->second->LookupStringValue(target, tag, false); + } + } + + + void LoadedDicomResources::AddResource(const Orthanc::DicomMap& dicom) + { + std::string id; + + if (dicom.LookupStringValue(id, indexedTag_, false /* no binary value */) && + resources_.find(id) == resources_.end() /* Don't index twice the same resource */) + { + resources_[id] = dicom.Clone(); + flattened_.clear(); // Invalidate the flattened version + } + } + + + void LoadedDicomResources::AddFromOrthanc(const Json::Value& tags) + { + Orthanc::DicomMap dicom; + dicom.FromDicomAsJson(tags); + AddResource(dicom); + } + + + void LoadedDicomResources::AddFromDicomWeb(const Json::Value& dicomweb) + { + if (dicomweb.type() == Json::objectValue) + { + AddFromDicomWebInternal(dicomweb); + } + else if (dicomweb.type() == Json::arrayValue) + { + for (Json::Value::ArrayIndex i = 0; i < dicomweb.size(); i++) + { + if (dicomweb[i].type() == Json::objectValue) + { + AddFromDicomWebInternal(dicomweb[i]); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + } + + + bool LoadedDicomResources::LookupTagValueConsensus(std::string& target, + const Orthanc::DicomTag& tag) const + { + typedef std::map Counter; + + Counter counter; + + for (Resources::const_iterator it = resources_.begin(); it != resources_.end(); ++it) + { + assert(it->second != NULL); + + std::string value; + if (it->second->LookupStringValue(value, tag, false)) + { + Counter::iterator found = counter.find(value); + if (found == counter.end()) + { + counter[value] = 1; + } + else + { + found->second ++; + } + } + } + + Counter::const_iterator best = counter.end(); + + for (Counter::const_iterator it = counter.begin(); it != counter.end(); ++it) + { + if (best == counter.end() || + best->second < it->second) + { + best = it; + } + } + + if (best == counter.end()) + { + return false; + } + else + { + target = best->first; + return true; + } + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/LoadedDicomResources.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/LoadedDicomResources.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,89 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include + + +namespace OrthancStone +{ + class LoadedDicomResources : public boost::noncopyable + { + private: + typedef std::map Resources; + + Orthanc::DicomTag indexedTag_; + Resources resources_; + std::vector flattened_; + + void Flatten(); + + void AddFromDicomWebInternal(const Json::Value& dicomweb); + + public: + LoadedDicomResources(const Orthanc::DicomTag& indexedTag) : + indexedTag_(indexedTag) + { + } + + // Re-index another set of resources using another tag + LoadedDicomResources(const LoadedDicomResources& other, + const Orthanc::DicomTag& indexedTag); + + ~LoadedDicomResources() + { + Clear(); + } + + const Orthanc::DicomTag& GetIndexedTag() const + { + return indexedTag_; + } + + void Clear(); + + size_t GetSize() const + { + return resources_.size(); + } + + Orthanc::DicomMap& GetResource(size_t index); + + bool HasResource(const std::string& id) const + { + return resources_.find(id) != resources_.end(); + } + + bool LookupStringValue(std::string& target, + const std::string& id, + const Orthanc::DicomTag& tag) const; + + void AddResource(const Orthanc::DicomMap& dicom); + + void AddFromOrthanc(const Json::Value& tags); + + void AddFromDicomWeb(const Json::Value& dicomweb); + + bool LookupTagValueConsensus(std::string& target, + const Orthanc::DicomTag& tag) const; + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/OracleScheduler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OracleScheduler.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,557 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "OracleScheduler.h" + +#include "../Oracle/ParseDicomFromFileCommand.h" + +namespace OrthancStone +{ + class OracleScheduler::ReceiverPayload : public Orthanc::IDynamicObject + { + private: + Priority priority_; + boost::weak_ptr receiver_; + std::auto_ptr command_; + + public: + ReceiverPayload(Priority priority, + boost::weak_ptr receiver, + IOracleCommand* command) : + priority_(priority), + receiver_(receiver), + command_(command) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + Priority GetActivePriority() const + { + return priority_; + } + + boost::weak_ptr GetOriginalReceiver() const + { + return receiver_; + } + + const IOracleCommand& GetOriginalCommand() const + { + assert(command_.get() != NULL); + return *command_; + } + }; + + + class OracleScheduler::ScheduledCommand : public boost::noncopyable + { + private: + boost::weak_ptr receiver_; + std::auto_ptr command_; + + public: + ScheduledCommand(boost::shared_ptr receiver, + IOracleCommand* command) : + receiver_(receiver), + command_(command) + { + if (command == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + boost::weak_ptr GetReceiver() + { + return receiver_; + } + + bool IsSameReceiver(boost::shared_ptr receiver) const + { + boost::shared_ptr lock(receiver_.lock()); + + return (lock && + lock.get() == receiver.get()); + } + + IOracleCommand* WrapCommand(Priority priority) + { + if (command_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + std::auto_ptr wrapped(command_->Clone()); + dynamic_cast(*wrapped).AcquirePayload(new ReceiverPayload(priority, receiver_, command_.release())); + return wrapped.release(); + } + } + }; + + + + void OracleScheduler::ClearQueue(Queue& queue) + { + for (Queue::iterator it = queue.begin(); it != queue.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + + totalProcessed_ ++; + } + + queue.clear(); + } + + + void OracleScheduler::RemoveReceiverFromQueue(Queue& queue, + boost::shared_ptr receiver) + { + if (!receiver) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + Queue tmp; + + for (Queue::iterator it = queue.begin(); it != queue.end(); ++it) + { + assert(it->second != NULL); + + if (!(it->second->IsSameReceiver(receiver))) + { + // This promise is still active + tmp.insert(std::make_pair(it->first, it->second)); + } + else + { + delete it->second; + + totalProcessed_ ++; + } + } + + queue = tmp; + } + + + void OracleScheduler::CheckInvariants() const + { +#ifndef NDEBUG + /*char buf[1024]; + sprintf(buf, "active: %d %d %d ; pending: %lu %lu %lu", + activeHighPriorityCommands_, activeStandardPriorityCommands_, activeLowPriorityCommands_, + highPriorityQueue_.size(), standardPriorityQueue_.size(), lowPriorityQueue_.size()); + LOG(INFO) << buf;*/ + + assert(activeHighPriorityCommands_ <= maxHighPriorityCommands_); + assert(activeStandardPriorityCommands_ <= maxStandardPriorityCommands_); + assert(activeLowPriorityCommands_ <= maxLowPriorityCommands_); + assert(totalProcessed_ <= totalScheduled_); + + for (Queue::const_iterator it = standardPriorityQueue_.begin(); it != standardPriorityQueue_.end(); ++it) + { + assert(it->first > PRIORITY_HIGH && + it->first < PRIORITY_LOW); + } + + for (Queue::const_iterator it = highPriorityQueue_.begin(); it != highPriorityQueue_.end(); ++it) + { + assert(it->first <= PRIORITY_HIGH); + } + + for (Queue::const_iterator it = lowPriorityQueue_.begin(); it != lowPriorityQueue_.end(); ++it) + { + assert(it->first >= PRIORITY_LOW); + } +#endif + } + + + void OracleScheduler::SpawnFromQueue(Queue& queue, + Priority priority) + { + CheckInvariants(); + + Queue::iterator item = queue.begin(); + assert(item != queue.end()); + + std::auto_ptr command(dynamic_cast(item->second)); + queue.erase(item); + + if (command.get() != NULL) + { + /** + * Only schedule the command for execution in the oracle, if its + * receiver has not been destroyed yet. + **/ + boost::shared_ptr observer(command->GetReceiver().lock()); + if (observer) + { + if (oracle_.Schedule(GetSharedObserver(), command->WrapCommand(priority))) + { + /** + * Executing this code if "Schedule()" returned "false" + * above, will result in a memory leak within + * "OracleScheduler", as the scheduler believes that some + * command is still active (i.e. pending to be executed by + * the oracle), hereby stalling the scheduler during its + * destruction, and not freeing the + * "shared_ptr" of the Stone context (check + * out "sjo-playground/WebViewer/Backend/Leak") + **/ + + switch (priority) + { + case Priority_High: + activeHighPriorityCommands_ ++; + break; + + case Priority_Standard: + activeStandardPriorityCommands_ ++; + break; + + case Priority_Low: + activeLowPriorityCommands_ ++; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + else + { + totalProcessed_ ++; + } + } + } + else + { + LOG(ERROR) << "NULL command, should never happen"; + } + + CheckInvariants(); + } + + + void OracleScheduler::SpawnCommands() + { + // Send as many commands as possible to the oracle + while (!highPriorityQueue_.empty()) + { + if (activeHighPriorityCommands_ < maxHighPriorityCommands_) + { + // First fill the high-priority lane + SpawnFromQueue(highPriorityQueue_, Priority_High); + } + else if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_) + { + // There remain too many high-priority commands for the + // high-priority lane, schedule them to the standard-priority lanes + SpawnFromQueue(highPriorityQueue_, Priority_Standard); + } + else if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(highPriorityQueue_, Priority_Low); + } + else + { + return; // No slot available + } + } + + while (!standardPriorityQueue_.empty()) + { + if (activeStandardPriorityCommands_ < maxStandardPriorityCommands_) + { + SpawnFromQueue(standardPriorityQueue_, Priority_Standard); + } + else if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(standardPriorityQueue_, Priority_Low); + } + else + { + return; + } + } + + while (!lowPriorityQueue_.empty()) + { + if (activeLowPriorityCommands_ < maxLowPriorityCommands_) + { + SpawnFromQueue(lowPriorityQueue_, Priority_Low); + } + else + { + return; + } + } + } + + + void OracleScheduler::RemoveActiveCommand(const ReceiverPayload& payload) + { + CheckInvariants(); + + totalProcessed_ ++; + + switch (payload.GetActivePriority()) + { + case Priority_High: + assert(activeHighPriorityCommands_ > 0); + activeHighPriorityCommands_ --; + break; + + case Priority_Standard: + assert(activeStandardPriorityCommands_ > 0); + activeStandardPriorityCommands_ --; + break; + + case Priority_Low: + assert(activeLowPriorityCommands_ > 0); + activeLowPriorityCommands_ --; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + SpawnCommands(); + + CheckInvariants(); + } + + + void OracleScheduler::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + GetOrthancImageCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetImage(), message.GetMimeType()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + GetOrthancWebViewerJpegCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetImage()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const HttpCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + HttpCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetAnswerHeaders(), message.GetAnswer()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + OrthancRestApiCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetAnswerHeaders(), message.GetAnswer()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void OracleScheduler::Handle(const ParseDicomSuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + ParseDicomSuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetDicom(), message.GetFileSize(), message.HasPixelData()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } +#endif + + + void OracleScheduler::Handle(const ReadFileCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + const ReceiverPayload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + + RemoveActiveCommand(payload); + + ReadFileCommand::SuccessMessage bis( + dynamic_cast(payload.GetOriginalCommand()), + message.GetContent()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + void OracleScheduler::Handle(const OracleCommandExceptionMessage& message) + { + const OracleCommandBase& command = dynamic_cast(message.GetOrigin()); + + assert(command.HasPayload()); + const ReceiverPayload& payload = dynamic_cast(command.GetPayload()); + + RemoveActiveCommand(payload); + + OracleCommandExceptionMessage bis(payload.GetOriginalCommand(), message.GetException()); + emitter_.EmitMessage(payload.GetOriginalReceiver(), bis); + } + + + OracleScheduler::OracleScheduler(IOracle& oracle, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) : + oracle_(oracle), + emitter_(emitter), + maxHighPriorityCommands_(maxHighPriority), + maxStandardPriorityCommands_(maxStandardPriority), + maxLowPriorityCommands_(maxLowPriority), + activeHighPriorityCommands_(0), + activeStandardPriorityCommands_(0), + activeLowPriorityCommands_(0), + totalScheduled_(0), + totalProcessed_(0) + { + assert(PRIORITY_HIGH < 0 && + PRIORITY_LOW > 0); + + if (maxLowPriority <= 0) + { + // There must be at least 1 lane available to deal with low-priority commands + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + boost::shared_ptr OracleScheduler::Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority) + { + boost::shared_ptr scheduler + (new OracleScheduler(oracle, emitter, maxHighPriority, maxStandardPriority, maxLowPriority)); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + scheduler->Register(oracleObservable, &OracleScheduler::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + scheduler->Register(oracleObservable, &OracleScheduler::Handle); +#endif + + return scheduler; + } + + + OracleScheduler::~OracleScheduler() + { + CancelAllRequests(); + } + + + void OracleScheduler::CancelRequests(boost::shared_ptr receiver) + { + RemoveReceiverFromQueue(standardPriorityQueue_, receiver); + RemoveReceiverFromQueue(highPriorityQueue_, receiver); + RemoveReceiverFromQueue(lowPriorityQueue_, receiver); + } + + + void OracleScheduler::CancelAllRequests() + { + ClearQueue(standardPriorityQueue_); + ClearQueue(highPriorityQueue_); + ClearQueue(lowPriorityQueue_); + } + + + void OracleScheduler::Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */) + { + std::auto_ptr pending(new ScheduledCommand(receiver, dynamic_cast(command))); + + /** + * Safeguard to remember that a new "Handle()" method and a call + * to "scheduler->Register()" must be implemented for each + * possible oracle command. + **/ + assert(command->GetType() == IOracleCommand::Type_GetOrthancImage || + command->GetType() == IOracleCommand::Type_GetOrthancWebViewerJpeg || + command->GetType() == IOracleCommand::Type_Http || + command->GetType() == IOracleCommand::Type_OrthancRestApi || + command->GetType() == IOracleCommand::Type_ParseDicomFromFile || + command->GetType() == IOracleCommand::Type_ParseDicomFromWado || + command->GetType() == IOracleCommand::Type_ReadFile); + + if (priority <= PRIORITY_HIGH) + { + highPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + else if (priority >= PRIORITY_LOW) + { + lowPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + else + { + standardPriorityQueue_.insert(std::make_pair(priority, pending.release())); + } + + totalScheduled_ ++; + + SpawnCommands(); + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/OracleScheduler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/OracleScheduler.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,167 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "../Messages/IMessageEmitter.h" +#include "../Messages/ObserverBase.h" +#include "../Oracle/GetOrthancImageCommand.h" +#include "../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../Oracle/HttpCommand.h" +#include "../Oracle/IOracle.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "../Oracle/ReadFileCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../Oracle/ParseDicomSuccessMessage.h" +#endif + +namespace OrthancStone +{ + class OracleScheduler : public ObserverBase + { + public: + static const int PRIORITY_HIGH = -1; + static const int PRIORITY_LOW = 100; + + private: + enum Priority + { + Priority_Low, + Priority_Standard, + Priority_High + }; + + class ReceiverPayload; + class ScheduledCommand; + + typedef std::multimap Queue; + + IOracle& oracle_; + IMessageEmitter& emitter_; + Queue standardPriorityQueue_; + Queue highPriorityQueue_; + Queue lowPriorityQueue_; + unsigned int maxHighPriorityCommands_; // Used if priority <= PRIORITY_HIGH + unsigned int maxStandardPriorityCommands_; + unsigned int maxLowPriorityCommands_; // Used if priority >= PRIORITY_LOW + unsigned int activeHighPriorityCommands_; + unsigned int activeStandardPriorityCommands_; + unsigned int activeLowPriorityCommands_; + uint64_t totalScheduled_; + uint64_t totalProcessed_; + + void ClearQueue(Queue& queue); + + void RemoveReceiverFromQueue(Queue& queue, + boost::shared_ptr receiver); + + void CheckInvariants() const; + + void SpawnFromQueue(Queue& queue, + Priority priority); + + void SpawnCommands(); + + void RemoveActiveCommand(const ReceiverPayload& payload); + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void Handle(const ReadFileCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + + OracleScheduler(IOracle& oracle, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + public: + static boost::shared_ptr Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter) + { + return Create(oracle, oracleObservable, emitter, 1, 4, 1); + } + + static boost::shared_ptr Create(IOracle& oracle, + IObservable& oracleObservable, + IMessageEmitter& emitter, + unsigned int maxHighPriority, + unsigned int maxStandardPriority, + unsigned int maxLowPriority); + + ~OracleScheduler(); + + unsigned int GetMaxHighPriorityCommands() const + { + return maxHighPriorityCommands_; + } + + unsigned int GetMaxStandardPriorityCommands() const + { + return maxStandardPriorityCommands_; + } + + unsigned int GetMaxLowPriorityCommands() const + { + return maxLowPriorityCommands_; + } + + uint64_t GetTotalScheduled() const + { + return totalScheduled_; + } + + uint64_t GetTotalProcessed() const + { + return totalProcessed_; + } + + // Cancel the HTTP requests that are still pending in the queues, + // and that are associated with the given receiver. Note that the + // receiver might still receive answers to HTTP requests that were + // already submitted to the oracle. + void CancelRequests(boost::shared_ptr receiver); + + void CancelAllRequests(); + + void Schedule(boost::shared_ptr receiver, + int priority, + IOracleCommand* command /* Takes ownership */); + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesFramesLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesFramesLoader.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,548 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "SeriesFramesLoader.h" + +#include "../Oracle/ParseDicomFromFileCommand.h" +#include "../Oracle/ParseDicomFromWadoCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include +#endif + +#include +#include +#include +#include + +#include + +namespace OrthancStone +{ + class SeriesFramesLoader::Payload : public Orthanc::IDynamicObject + { + private: + DicomSource source_; + size_t seriesIndex_; + std::string sopInstanceUid_; // Only used for debug purpose + unsigned int quality_; + bool hasWindowing_; + float windowingCenter_; + float windowingWidth_; + std::auto_ptr userPayload_; + + public: + Payload(const DicomSource& source, + size_t seriesIndex, + const std::string& sopInstanceUid, + unsigned int quality, + Orthanc::IDynamicObject* userPayload) : + source_(source), + seriesIndex_(seriesIndex), + sopInstanceUid_(sopInstanceUid), + quality_(quality), + hasWindowing_(false), + userPayload_(userPayload) + { + } + + size_t GetSeriesIndex() const + { + return seriesIndex_; + } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + unsigned int GetQuality() const + { + return quality_; + } + + void SetWindowing(float center, + float width) + { + hasWindowing_ = true; + windowingCenter_ = center; + windowingWidth_ = width; + } + + bool HasWindowing() const + { + return hasWindowing_; + } + + float GetWindowingCenter() const + { + if (hasWindowing_) + { + return windowingCenter_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + float GetWindowingWidth() const + { + if (hasWindowing_) + { + return windowingWidth_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + const DicomSource& GetSource() const + { + return source_; + } + + Orthanc::IDynamicObject* GetUserPayload() const + { + return userPayload_.get(); + } + }; + + + SeriesFramesLoader::SeriesFramesLoader(ILoadersContext& context, + LoadedDicomResources& instances, + const std::string& dicomDirPath, + boost::shared_ptr dicomDir) : + context_(context), + frames_(instances), + dicomDirPath_(dicomDirPath), + dicomDir_(dicomDir) + { + } + + + void SeriesFramesLoader::EmitMessage(const Payload& payload, + const Orthanc::ImageAccessor& image) + { + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); + const Orthanc::DicomMap& instance = frames_.GetInstance(payload.GetSeriesIndex()); + size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); + + if (frameIndex >= parameters.GetImageInformation().GetNumberOfFrames() || + payload.GetSopInstanceUid() != parameters.GetSopInstanceUid()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + LOG(TRACE) << "Decoded instance " << payload.GetSopInstanceUid() << ", frame " + << frameIndex << ": " << image.GetWidth() << "x" + << image.GetHeight() << ", " << Orthanc::EnumerationToString(image.GetFormat()) + << ", quality " << payload.GetQuality(); + + FrameLoadedMessage message(*this, frameIndex, payload.GetQuality(), image, instance, parameters, payload.GetUserPayload()); + BroadcastMessage(message); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void SeriesFramesLoader::HandleDicom(const Payload& payload, + Orthanc::ParsedDicomFile& dicom) + { + size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); + + std::auto_ptr decoded; + decoded.reset(Orthanc::DicomImageDecoder::Decode(dicom, frameIndex)); + + if (decoded.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + EmitMessage(payload, *decoded); + } +#endif + + + void SeriesFramesLoader::HandleDicomWebRendered(const Payload& payload, + const std::string& body, + const std::map& headers) + { + assert(payload.GetSource().IsDicomWeb() && + payload.HasWindowing()); + + bool ok = false; + for (std::map::const_iterator it = headers.begin(); + it != headers.end(); ++it) + { + if (boost::iequals("content-type", it->first) && + boost::iequals(Orthanc::MIME_JPEG, it->second)) + { + ok = true; + break; + } + } + + if (!ok) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "The WADO-RS server has not generated a JPEG image on /rendered"); + } + + Orthanc::JpegReader reader; + reader.ReadFromMemory(body); + + switch (reader.GetFormat()) + { + case Orthanc::PixelFormat_RGB24: + EmitMessage(payload, reader); + break; + + case Orthanc::PixelFormat_Grayscale8: + { + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); + + Orthanc::Image scaled(parameters.GetExpectedPixelFormat(), reader.GetWidth(), reader.GetHeight(), false); + Orthanc::ImageProcessing::Convert(scaled, reader); + + float w = payload.GetWindowingWidth(); + if (w <= 0.01f) + { + w = 0.01; // Prevent division by zero + } + + const float c = payload.GetWindowingCenter(); + const float scaling = w / 255.0f; + const float offset = (c - w / 2.0f) / scaling; + + Orthanc::ImageProcessing::ShiftScale(scaled, offset, scaling, false /* truncation to speed up */); + EmitMessage(payload, scaled); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + void SeriesFramesLoader::Handle(const ParseDicomSuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + if ((payload.GetSource().IsDicomDir() || + payload.GetSource().IsDicomWeb()) && + message.HasPixelData()) + { + HandleDicom(dynamic_cast(message.GetOrigin().GetPayload()), message.GetDicom()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } +#endif + + + void SeriesFramesLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + assert(payload.GetSource().IsOrthanc()); + + EmitMessage(payload, message.GetImage()); + } + + + void SeriesFramesLoader::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + const Payload& payload = dynamic_cast(message.GetOrigin().GetPayload()); + assert(payload.GetSource().IsOrthanc()); + + EmitMessage(payload, message.GetImage()); + } + + + void SeriesFramesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + // This is to handle "/rendered" in DICOMweb + assert(message.GetOrigin().HasPayload()); + HandleDicomWebRendered(dynamic_cast(message.GetOrigin().GetPayload()), + message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesFramesLoader::Handle(const HttpCommand::SuccessMessage& message) + { + // This is to handle "/rendered" in DICOMweb + assert(message.GetOrigin().HasPayload()); + HandleDicomWebRendered(dynamic_cast(message.GetOrigin().GetPayload()), + message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesFramesLoader::GetPreviewWindowing(float& center, + float& width, + size_t index) const + { + const Orthanc::DicomMap& instance = frames_.GetInstance(index); + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); + + if (parameters.HasDefaultWindowing()) + { + // TODO - Handle multiple presets (take the largest width) + center = parameters.GetDefaultWindowingCenter(); + width = parameters.GetDefaultWindowingWidth(); + } + else + { + float a, b; + if (instance.ParseFloat(a, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE) && + instance.ParseFloat(b, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE) && + a < b) + { + center = (a + b) / 2.0f; + width = (b - a); + } + else + { + // Cannot infer a suitable windowing from the available tags + center = 128.0f; + width = 256.0f; + } + } + } + + + Orthanc::IDynamicObject& SeriesFramesLoader::FrameLoadedMessage::GetUserPayload() const + { + if (userPayload_) + { + return *userPayload_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void SeriesFramesLoader::Factory::SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir) + { + dicomDirPath_ = dicomDirPath; + dicomDir_ = dicomDir; + } + + + boost::shared_ptr SeriesFramesLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr loader( + new SeriesFramesLoader(stone.GetContext(), instances_, dicomDirPath_, dicomDir_)); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); + +#if ORTHANC_ENABLE_DCMTK == 1 + loader->Register(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); +#endif + + return loader; + } + + + void SeriesFramesLoader::ScheduleLoadFrame(int priority, + const DicomSource& source, + size_t index, + unsigned int quality, + Orthanc::IDynamicObject* userPayload) + { + std::auto_ptr protection(userPayload); + + if (index >= frames_.GetFramesCount() || + quality >= source.GetQualityCount()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + const Orthanc::DicomMap& instance = frames_.GetInstance(index); + + std::string sopInstanceUid; + if (!instance.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing SOPInstanceUID in a DICOM instance"); + } + + if (source.IsDicomDir()) + { + if (dicomDir_.get() == NULL) + { + // Should have been set in the factory + throw Orthanc::OrthancException( + Orthanc::ErrorCode_BadSequenceOfCalls, + "SeriesFramesLoader::Factory::SetDicomDir() should have been called"); + } + + assert(quality == 0); + + std::string file; + if (dicomDir_->LookupStringValue(file, sopInstanceUid, Orthanc::DICOM_TAG_REFERENCED_FILE_ID)) + { + std::auto_ptr command(new ParseDicomFromFileCommand(dicomDirPath_, file)); + command->SetPixelDataIncluded(true); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + else + { + LOG(WARNING) << "Missing tag ReferencedFileID in a DICOMDIR entry"; + } + } + else if (source.IsDicomWeb()) + { + std::string studyInstanceUid, seriesInstanceUid; + if (!instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); + } + + const std::string uri = ("/studies/" + studyInstanceUid + + "/series/" + seriesInstanceUid + + "/instances/" + sopInstanceUid); + + if (source.HasDicomWebRendered() && + quality == 0) + { + float c, w; + GetPreviewWindowing(c, w, index); + + std::map arguments, headers; + arguments["window"] = (boost::lexical_cast(c) + "," + + boost::lexical_cast(w) + ",linear"); + headers["Accept"] = "image/jpeg"; + + std::auto_ptr payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + payload->SetWindowing(c, w); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, + source.CreateDicomWebCommand(uri + "/rendered", arguments, headers, payload.release())); + } + } + else + { + assert((source.HasDicomWebRendered() && quality == 1) || + (!source.HasDicomWebRendered() && quality == 0)); + +#if ORTHANC_ENABLE_DCMTK == 1 + std::auto_ptr payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + const std::map empty; + + std::auto_ptr command( + new ParseDicomFromWadoCommand(sopInstanceUid, source.CreateDicomWebCommand(uri, empty, empty, NULL))); + command->AcquirePayload(payload.release()); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK is not enabled, cannot parse a DICOM instance"); +#endif + } + } + else if (source.IsOrthanc()) + { + std::string orthancId; + + { + std::string patientId, studyInstanceUid, seriesInstanceUid; + if (!instance.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) || + !instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); + } + + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + orthancId = hasher.HashInstance(); + } + + const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); + + if (quality == 0 && source.HasOrthancWebViewer1()) + { + std::auto_ptr command(new GetOrthancWebViewerJpegCommand); + command->SetInstance(orthancId); + command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + else if (quality == 0 && source.HasOrthancAdvancedPreview()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + else + { + assert(quality <= 1); + assert(quality == 0 || + source.HasOrthancWebViewer1() || + source.HasOrthancAdvancedPreview()); + + std::auto_ptr command(new GetOrthancImageCommand); + command->SetFrameUri(orthancId, frames_.GetFrameIndex(index), parameters.GetExpectedPixelFormat()); + command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); + command->SetHttpHeader("Accept", Orthanc::MIME_PAM); + command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); + + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority, command.release()); + } + } + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesFramesLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesFramesLoader.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,176 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "OracleScheduler.h" +#include "DicomSource.h" +#include "SeriesOrderedFrames.h" +#include "ILoaderFactory.h" + +namespace OrthancStone +{ + class SeriesFramesLoader : + public ObserverBase, + public IObservable + { + private: + class Payload; + + ILoadersContext& context_; + SeriesOrderedFrames frames_; + std::string dicomDirPath_; + boost::shared_ptr dicomDir_; + + SeriesFramesLoader(ILoadersContext& context, + LoadedDicomResources& instances, + const std::string& dicomDirPath, + boost::shared_ptr dicomDir); + + void EmitMessage(const Payload& payload, + const Orthanc::ImageAccessor& image); + +#if ORTHANC_ENABLE_DCMTK == 1 + void HandleDicom(const Payload& payload, + Orthanc::ParsedDicomFile& dicom); +#endif + + void HandleDicomWebRendered(const Payload& payload, + const std::string& body, + const std::map& headers); + +#if ORTHANC_ENABLE_DCMTK == 1 + void Handle(const ParseDicomSuccessMessage& message); +#endif + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const HttpCommand::SuccessMessage& message); + + void GetPreviewWindowing(float& center, + float& width, + size_t index) const; + + public: + class FrameLoadedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + size_t frameIndex_; + unsigned int quality_; + const Orthanc::ImageAccessor& image_; + const Orthanc::DicomMap& instance_; + const DicomInstanceParameters& parameters_; + Orthanc::IDynamicObject* userPayload_; // Ownership is maintained by the caller + + public: + FrameLoadedMessage(const SeriesFramesLoader& loader, + size_t frameIndex, + unsigned int quality, + const Orthanc::ImageAccessor& image, + const Orthanc::DicomMap& instance, + const DicomInstanceParameters& parameters, + Orthanc::IDynamicObject* userPayload) : + OriginMessage(loader), + frameIndex_(frameIndex), + quality_(quality), + image_(image), + instance_(instance), + parameters_(parameters), + userPayload_(userPayload) + { + } + + size_t GetFrameIndex() const + { + return frameIndex_; + } + + unsigned int GetQuality() const + { + return quality_; + } + + const Orthanc::ImageAccessor& GetImage() const + { + return image_; + } + + const Orthanc::DicomMap& GetInstance() const + { + return instance_; + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + return parameters_; + } + + bool HasUserPayload() const + { + return userPayload_ != NULL; + } + + Orthanc::IDynamicObject& GetUserPayload() const; + }; + + + class Factory : public ILoaderFactory + { + private: + LoadedDicomResources& instances_; + std::string dicomDirPath_; + boost::shared_ptr dicomDir_; + + public: + // No "const" because "LoadedDicomResources::GetResource()" will call "Flatten()" + Factory(LoadedDicomResources& instances) : + instances_(instances) + { + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir); + + virtual boost::shared_ptr Create(ILoadersContext::ILock& context); + }; + + const SeriesOrderedFrames& GetOrderedFrames() const + { + return frames_; + } + + void ScheduleLoadFrame(int priority, + const DicomSource& source, + size_t index, + unsigned int quality, + Orthanc::IDynamicObject* userPayload /* transfer ownership */); + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesMetadataLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesMetadataLoader.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,347 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "SeriesMetadataLoader.h" + +#include + +namespace OrthancStone +{ + SeriesMetadataLoader::SeriesMetadataLoader(boost::shared_ptr& loader) : + loader_(loader), + state_(State_Setup) + { + } + + + bool SeriesMetadataLoader::IsScheduledWithHigherPriority(const std::string& seriesInstanceUid, + int priority) const + { + if (series_.find(seriesInstanceUid) != series_.end()) + { + // This series is readily available + return true; + } + else + { + std::map::const_iterator found = scheduled_.find(seriesInstanceUid); + + return (found != scheduled_.end() && + found->second < priority); + } + } + + + void SeriesMetadataLoader::Handle(const DicomResourcesLoader::SuccessMessage& message) + { + assert(message.GetResources()); + + switch (state_) + { + case State_Setup: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + + case State_Default: + { + std::string studyInstanceUid; + std::string seriesInstanceUid; + + if (message.GetResources()->LookupTagValueConsensus(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID) && + message.GetResources()->LookupTagValueConsensus(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID)) + { + series_[seriesInstanceUid] = message.GetResources(); + + SeriesLoadedMessage loadedMessage(*this, message.GetDicomSource(), studyInstanceUid, + seriesInstanceUid, *message.GetResources()); + BroadcastMessage(loadedMessage); + } + + break; + } + + case State_DicomDir: + { + assert(!dicomDir_); + assert(seriesSize_.empty()); + + dicomDir_ = message.GetResources(); + + for (size_t i = 0; i < message.GetResources()->GetSize(); i++) + { + std::string seriesInstanceUid; + if (message.GetResources()->GetResource(i).LookupStringValue + (seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + boost::shared_ptr target + (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + + if (loader_->ScheduleLoadDicomFile(target, message.GetPriority(), message.GetDicomSource(), dicomDirPath_, + message.GetResources()->GetResource(i), false /* no need for pixel data */, + NULL /* TODO PAYLOAD */)) + { + std::map::iterator found = seriesSize_.find(seriesInstanceUid); + if (found == seriesSize_.end()) + { + series_[seriesInstanceUid].reset + (new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + seriesSize_[seriesInstanceUid] = 1; + } + else + { + found->second ++; + } + } + } + } + + LOG(INFO) << "Read a DICOMDIR containing " << seriesSize_.size() << " series"; + + state_ = State_DicomFile; + break; + } + + case State_DicomFile: + { + assert(dicomDir_); + assert(message.GetResources()->GetSize() <= 1); // Could be zero if corrupted DICOM instance + + if (message.GetResources()->GetSize() == 1) + { + const Orthanc::DicomMap& instance = message.GetResources()->GetResource(0); + + std::string studyInstanceUid; + std::string seriesInstanceUid; + if (instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) && + instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + Series::const_iterator series = series_.find(seriesInstanceUid); + std::map::const_iterator size = seriesSize_.find(seriesInstanceUid); + + if (series == series_.end() || + size == seriesSize_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + series->second->AddResource(instance); + + if (series->second->GetSize() > size->second) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else if (series->second->GetSize() == size->second) + { + // The series is complete + SeriesLoadedMessage loadedMessage( + *this, message.GetDicomSource(), + studyInstanceUid, seriesInstanceUid, *series->second); + loadedMessage.SetDicomDir(dicomDirPath_, dicomDir_); + BroadcastMessage(loadedMessage); + } + } + } + } + + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + + SeriesMetadataLoader::SeriesLoadedMessage::SeriesLoadedMessage( + const SeriesMetadataLoader& loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + LoadedDicomResources& instances) : + OriginMessage(loader), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + instances_(instances) + { + LOG(INFO) << "Loaded series " << seriesInstanceUid + << ", number of instances: " << instances_.GetSize(); + } + + + boost::shared_ptr SeriesMetadataLoader::Factory::Create(ILoadersContext::ILock& context) + { + DicomResourcesLoader::Factory factory; + boost::shared_ptr loader + (boost::dynamic_pointer_cast(factory.Create(context))); + + boost::shared_ptr obj(new SeriesMetadataLoader(loader)); + obj->Register(*loader, &SeriesMetadataLoader::Handle); + return obj; + } + + + SeriesMetadataLoader::Accessor::Accessor(SeriesMetadataLoader& that, + const std::string& seriesInstanceUid) + { + Series::const_iterator found = that.series_.find(seriesInstanceUid); + if (found != that.series_.end()) + { + assert(found->second != NULL); + series_ = found->second; + } + } + + + size_t SeriesMetadataLoader::Accessor::GetInstancesCount() const + { + if (IsComplete()) + { + return series_->GetSize(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + const Orthanc::DicomMap& SeriesMetadataLoader::Accessor::GetInstance(size_t index) const + { + if (IsComplete()) + { + return series_->GetResource(index); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } + + + void SeriesMetadataLoader::ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (state_ != State_Setup && + state_ != State_Default) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader is working in DICOMDIR state"); + } + + state_ = State_Default; + + // Only re-schedule the loading if the previous loading was with lower priority + if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority)) + { + if (source.IsDicomWeb()) + { + boost::shared_ptr target + (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + loader_->ScheduleWado(target, priority, source, + "/studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + "/metadata", + NULL /* TODO PAYLOAD */); + + scheduled_[seriesInstanceUid] = priority; + } + else if (source.IsOrthanc()) + { + // This flavor of the method is only available with DICOMweb, as + // Orthanc requires the "PatientID" to be known + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The PatientID must be provided on Orthanc sources"); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + + + void SeriesMetadataLoader::ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (state_ != State_Setup && + state_ != State_Default) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader is working in DICOMDIR state"); + } + + state_ = State_Default; + + if (source.IsDicomWeb()) + { + ScheduleLoadSeries(priority, source, studyInstanceUid, seriesInstanceUid); + } + else if (!IsScheduledWithHigherPriority(seriesInstanceUid, priority)) + { + if (source.IsOrthanc()) + { + // Dummy SOP Instance UID, as we are working at the "series" level + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy"); + + boost::shared_ptr target + (new LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)); + + loader_->ScheduleLoadOrthancResources(target, priority, source, Orthanc::ResourceType_Series, + hasher.HashSeries(), Orthanc::ResourceType_Instance, + NULL /* TODO PAYLOAD */); + + scheduled_[seriesInstanceUid] = priority; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + } + + + void SeriesMetadataLoader::ScheduleLoadDicomDir(int priority, + const DicomSource& source, + const std::string& path) + { + if (!source.IsDicomDir()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + if (state_ != State_Setup) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, + "The loader cannot load two different DICOMDIR"); + } + + state_ = State_DicomDir; + dicomDirPath_ = path; + boost::shared_ptr dicomDir + (new LoadedDicomResources(Orthanc::DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE)); + loader_->ScheduleLoadDicomDir(dicomDir, priority, source, path, + NULL /* TODO PAYLOAD */); + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesMetadataLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesMetadataLoader.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,170 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "DicomResourcesLoader.h" + +namespace OrthancStone +{ + class SeriesMetadataLoader : + public ObserverBase, + public IObservable + { + private: + enum State + { + State_Setup, + State_Default, + State_DicomDir, + State_DicomFile + }; + + typedef std::map > Series; + + boost::shared_ptr loader_; + State state_; + std::map scheduled_; // Maps a "SeriesInstanceUID" to a priority + Series series_; + boost::shared_ptr dicomDir_; + std::string dicomDirPath_; + std::map seriesSize_; + + SeriesMetadataLoader(boost::shared_ptr& loader); + + bool IsScheduledWithHigherPriority(const std::string& seriesInstanceUid, + int priority) const; + + void Handle(const DicomResourcesLoader::SuccessMessage& message); + + public: + class SeriesLoadedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const DicomSource& source_; + const std::string& studyInstanceUid_; + const std::string& seriesInstanceUid_; + LoadedDicomResources& instances_; + std::string dicomDirPath_; + boost::shared_ptr dicomDir_; + + public: + SeriesLoadedMessage(const SeriesMetadataLoader& loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + LoadedDicomResources& instances); + + const DicomSource& GetDicomSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + size_t GetInstancesCount() const + { + return instances_.GetSize(); + } + + const Orthanc::DicomMap& GetInstance(size_t index) const + { + return instances_.GetResource(index); + } + + LoadedDicomResources& GetInstances() const + { + return instances_; + } + + void SetDicomDir(const std::string& dicomDirPath, + boost::shared_ptr dicomDir) + { + dicomDirPath_ = dicomDirPath; + dicomDir_ = dicomDir; + } + + const std::string& GetDicomDirPath() const + { + return dicomDirPath_; + } + + // Will be NULL on non-DICOMDIR sources + boost::shared_ptr GetDicomDir() const + { + return dicomDir_; + } + }; + + + class Factory : public ILoaderFactory + { + public: + virtual boost::shared_ptr Create(ILoadersContext::ILock& context); + }; + + + class Accessor : public boost::noncopyable + { + private: + boost::shared_ptr series_; + + public: + Accessor(SeriesMetadataLoader& that, + const std::string& seriesInstanceUid); + + bool IsComplete() const + { + return series_ != NULL; + } + + size_t GetInstancesCount() const; + + const Orthanc::DicomMap& GetInstance(size_t index) const; + }; + + + void ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + + void ScheduleLoadSeries(int priority, + const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + + void ScheduleLoadDicomDir(int priority, + const DicomSource& source, + const std::string& path); + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesOrderedFrames.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesOrderedFrames.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,343 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "../Toolbox/SlicesSorter.h" +#include "SeriesOrderedFrames.h" + +#include + +namespace OrthancStone +{ + class SeriesOrderedFrames::Instance : public boost::noncopyable + { + private: + std::auto_ptr dicom_; + DicomInstanceParameters parameters_; + + public: + Instance(const Orthanc::DicomMap& dicom) : + dicom_(dicom.Clone()), + parameters_(dicom) + { + } + + const Orthanc::DicomMap& GetInstance() const + { + return *dicom_; + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + return parameters_; + } + + bool Lookup3DGeometry(CoordinateSystem3D& target) const + { + try + { + std::string imagePositionPatient, imageOrientationPatient; + if (dicom_->LookupStringValue(imagePositionPatient, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT, false) && + dicom_->LookupStringValue(imageOrientationPatient, Orthanc::DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false)) + { + target = CoordinateSystem3D(imagePositionPatient, imageOrientationPatient); + return true; + } + } + catch (Orthanc::OrthancException&) + { + } + + return false; + } + + bool LookupIndexInSeries(int& target) const + { + std::string value; + + if (dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_INSTANCE_NUMBER, false) || + dicom_->LookupStringValue(value, Orthanc::DICOM_TAG_IMAGE_INDEX, false)) + { + try + { + target = boost::lexical_cast(value); + return true; + } + catch (boost::bad_lexical_cast&) + { + } + } + + return false; + } + }; + + + class SeriesOrderedFrames::Frame : public boost::noncopyable + { + private: + const Instance* instance_; + unsigned int frameIndex_; + + public: + Frame(const Instance& instance, + unsigned int frameIndex) : + instance_(&instance), + frameIndex_(frameIndex) + { + if (frameIndex_ >= instance.GetInstanceParameters().GetImageInformation().GetNumberOfFrames()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + const Orthanc::DicomMap& GetInstance() const + { + assert(instance_ != NULL); + return instance_->GetInstance(); + } + + const DicomInstanceParameters& GetInstanceParameters() const + { + assert(instance_ != NULL); + return instance_->GetInstanceParameters(); + } + + unsigned int GetFrameIndex() const + { + return frameIndex_; + } + }; + + + class SeriesOrderedFrames::InstanceWithIndexInSeries + { + private: + const Instance* instance_; // Don't use a reference to make "std::sort()" happy + int index_; + + public: + InstanceWithIndexInSeries(const Instance& instance) : + instance_(&instance) + { + if (!instance_->LookupIndexInSeries(index_)) + { + index_ = std::numeric_limits::max(); + } + } + + const Instance& GetInstance() const + { + return *instance_; + } + + int GetIndexInSeries() const + { + return index_; + } + + bool operator< (const InstanceWithIndexInSeries& other) const + { + return (index_ < other.index_); + } + }; + + + void SeriesOrderedFrames::Clear() + { + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + delete instances_[i]; + } + + for (size_t i = 0; i < orderedFrames_.size(); i++) + { + assert(orderedFrames_[i] != NULL); + delete orderedFrames_[i]; + } + + instances_.clear(); + orderedFrames_.clear(); + } + + + bool SeriesOrderedFrames::Sort3DVolume() + { + SlicesSorter sorter; + sorter.Reserve(instances_.size()); + + for (size_t i = 0; i < instances_.size(); i++) + { + CoordinateSystem3D geometry; + if (instances_[i]->Lookup3DGeometry(geometry)) + { + sorter.AddSlice(geometry, new Orthanc::SingleValueObject(instances_[i])); + } + else + { + return false; // Not a 3D volume + } + } + + if (!sorter.Sort() || + sorter.GetSlicesCount() != instances_.size() || + !sorter.AreAllSlicesDistinct()) + { + return false; + } + else + { + for (size_t i = 0; i < sorter.GetSlicesCount(); i++) + { + assert(sorter.HasSlicePayload(i)); + + const Orthanc::SingleValueObject& payload = + dynamic_cast&>(sorter.GetSlicePayload(i)); + + assert(payload.GetValue() != NULL); + + for (size_t j = 0; j < payload.GetValue()->GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++) + { + orderedFrames_.push_back(new Frame(*payload.GetValue(), j)); + } + } + + isRegular_ = sorter.ComputeSpacingBetweenSlices(spacingBetweenSlices_); + return true; + } + } + + + void SeriesOrderedFrames::SortIndexInSeries() + { + std::vector tmp; + tmp.reserve(instances_.size()); + + for (size_t i = 0; i < instances_.size(); i++) + { + assert(instances_[i] != NULL); + tmp.push_back(InstanceWithIndexInSeries(*instances_[i])); + } + + std::sort(tmp.begin(), tmp.end()); + + for (size_t i = 0; i < tmp.size(); i++) + { + for (size_t j = 0; j < tmp[i].GetInstance().GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); j++) + { + orderedFrames_.push_back(new Frame(tmp[i].GetInstance(), j)); + } + } + } + + + const SeriesOrderedFrames::Frame& SeriesOrderedFrames::GetFrame(size_t seriesIndex) const + { + if (seriesIndex >= orderedFrames_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(orderedFrames_[seriesIndex] != NULL); + return *(orderedFrames_[seriesIndex]); + } + } + + + SeriesOrderedFrames::SeriesOrderedFrames(LoadedDicomResources& instances) : + isVolume_(false), + isRegular_(false), + spacingBetweenSlices_(0) + { + instances_.reserve(instances.GetSize()); + + size_t numberOfFrames = 0; + + for (size_t i = 0; i < instances.GetSize(); i++) + { + try + { + std::auto_ptr instance(new Instance(instances.GetResource(i))); + numberOfFrames += instance->GetInstanceParameters().GetImageInformation().GetNumberOfFrames(); + instances_.push_back(instance.release()); + } + catch (Orthanc::OrthancException&) + { + // The instance has not all the required DICOM tags, skip it + } + } + + orderedFrames_.reserve(numberOfFrames); + + if (Sort3DVolume()) + { + isVolume_ = true; + + if (isRegular_) + { + LOG(INFO) << "Regular 3D volume detected"; + } + else + { + LOG(INFO) << "Non-regular 3D volume detected"; + } + } + else + { + LOG(INFO) << "Series is not a 3D volume, sorting by index"; + SortIndexInSeries(); + } + + LOG(INFO) << "Number of frames: " << orderedFrames_.size(); + } + + + unsigned int SeriesOrderedFrames::GetFrameIndex(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetFrameIndex(); + } + + + const Orthanc::DicomMap& SeriesOrderedFrames::GetInstance(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetInstance(); + } + + + const DicomInstanceParameters& SeriesOrderedFrames::GetInstanceParameters(size_t seriesIndex) const + { + return GetFrame(seriesIndex).GetInstanceParameters(); + } + + + double SeriesOrderedFrames::GetSpacingBetweenSlices() const + { + if (IsRegular3DVolume()) + { + return spacingBetweenSlices_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesOrderedFrames.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesOrderedFrames.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,85 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "LoadedDicomResources.h" + +#include "../Toolbox/DicomInstanceParameters.h" + +namespace OrthancStone +{ + class SeriesOrderedFrames : public boost::noncopyable + { + private: + class Instance; + class Frame; + class InstanceWithIndexInSeries; + + std::vector instances_; + std::vector orderedFrames_; + bool isVolume_; + bool isRegular_; + double spacingBetweenSlices_; + + void Clear(); + + bool Sort3DVolume(); + + void SortIndexInSeries(); + + const Frame& GetFrame(size_t seriesIndex) const; + + public: + SeriesOrderedFrames(LoadedDicomResources& instances); + + ~SeriesOrderedFrames() + { + Clear(); + } + + size_t GetFramesCount() const + { + return orderedFrames_.size(); + } + + unsigned int GetFrameIndex(size_t seriesIndex) const; + + const Orthanc::DicomMap& GetInstance(size_t seriesIndex) const; + + const DicomInstanceParameters& GetInstanceParameters(size_t seriesIndex) const; + + // Are all frames parallel and aligned? + bool Is3DVolume() const + { + return isVolume_; + } + + // Are all frames parallel, aligned and evenly spaced? + bool IsRegular3DVolume() const + { + return isRegular_; + } + + // Only available on regular 3D volumes + double GetSpacingBetweenSlices() const; + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesThumbnailsLoader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesThumbnailsLoader.cpp Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,554 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#include "SeriesThumbnailsLoader.h" + +#include +#include +#include +#include +#include + +#include + +static const unsigned int JPEG_QUALITY = 70; // Only used for Orthanc source + +namespace OrthancStone +{ + static SeriesThumbnailType ExtractSopClassUid(const std::string& sopClassUid) + { + if (sopClassUid == "1.2.840.10008.5.1.4.1.1.104.1") // Encapsulated PDF Storage + { + return SeriesThumbnailType_Pdf; + } + else if (sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.1.1" || // Video Endoscopic Image Storage + sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.2.1" || // Video Microscopic Image Storage + sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.4.1") // Video Photographic Image Storage + { + return SeriesThumbnailType_Video; + } + else + { + return SeriesThumbnailType_Unknown; + } + } + + + SeriesThumbnailsLoader::Thumbnail::Thumbnail(const std::string& image, + const std::string& mime) : + type_(SeriesThumbnailType_Image), + image_(image), + mime_(mime) + { + } + + + SeriesThumbnailsLoader::Thumbnail::Thumbnail(SeriesThumbnailType type) : + type_(type) + { + if (type == SeriesThumbnailType_Image) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + + + void SeriesThumbnailsLoader::AcquireThumbnail(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + SeriesThumbnailsLoader::Thumbnail* thumbnail) + { + assert(thumbnail != NULL); + + std::auto_ptr protection(thumbnail); + + Thumbnails::iterator found = thumbnails_.find(seriesInstanceUid); + if (found == thumbnails_.end()) + { + thumbnails_[seriesInstanceUid] = protection.release(); + } + else + { + assert(found->second != NULL); + delete found->second; + found->second = protection.release(); + } + + ThumbnailLoadedMessage message(*this, source, studyInstanceUid, seriesInstanceUid, *thumbnail); + BroadcastMessage(message); + } + + + class SeriesThumbnailsLoader::Handler : public Orthanc::IDynamicObject + { + private: + boost::shared_ptr loader_; + DicomSource source_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + + public: + Handler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + loader_(loader), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid) + { + if (!loader) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + boost::shared_ptr GetLoader() + { + return loader_; + } + + const DicomSource& GetSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) = 0; + + virtual void HandleError() + { + LOG(INFO) << "Cannot generate thumbnail for SeriesInstanceUID: " << seriesInstanceUid_; + } + }; + + + class SeriesThumbnailsLoader::DicomWebSopClassHandler : public SeriesThumbnailsLoader::Handler + { + private: + static bool GetSopClassUid(std::string& sopClassUid, + const Json::Value& json) + { + Orthanc::DicomMap dicom; + dicom.FromDicomWeb(json); + + return dicom.LookupStringValue(sopClassUid, Orthanc::DICOM_TAG_SOP_CLASS_UID, false); + } + + public: + DicomWebSopClassHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + Json::Reader reader; + Json::Value value; + + if (!reader.parse(body, value) || + value.type() != Json::arrayValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + else + { + SeriesThumbnailType type = SeriesThumbnailType_Unknown; + + std::string sopClassUid; + if (value.size() > 0 && + GetSopClassUid(sopClassUid, value[0])) + { + bool ok = true; + + for (Json::Value::ArrayIndex i = 1; i < value.size() && ok; i++) + { + std::string s; + if (!GetSopClassUid(s, value[i]) || + s != sopClassUid) + { + ok = false; + } + } + + if (ok) + { + type = ExtractSopClassUid(sopClassUid); + } + } + + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(type)); + } + } + }; + + + class SeriesThumbnailsLoader::DicomWebThumbnailHandler : public SeriesThumbnailsLoader::Handler + { + public: + DicomWebThumbnailHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + std::string mime = Orthanc::MIME_JPEG; + for (std::map::const_iterator + it = headers.begin(); it != headers.end(); ++it) + { + if (boost::iequals(it->first, "content-type")) + { + mime = it->second; + } + } + + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(body, mime)); + } + + virtual void HandleError() + { + // The DICOMweb wasn't able to generate a thumbnail, try to + // retrieve the SopClassUID tag using QIDO-RS + + std::map arguments, headers; + arguments["0020000D"] = GetStudyInstanceUid(); + arguments["0020000E"] = GetSeriesInstanceUid(); + arguments["includefield"] = "00080016"; + + std::auto_ptr command( + GetSource().CreateDicomWebCommand( + "/instances", arguments, headers, new DicomWebSopClassHandler( + GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid()))); + GetLoader()->Schedule(command.release()); + } + }; + + + class SeriesThumbnailsLoader::ThumbnailInformation : public Orthanc::IDynamicObject + { + private: + DicomSource source_; + std::string studyInstanceUid_; + std::string seriesInstanceUid_; + + public: + ThumbnailInformation(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid) + { + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + }; + + + class SeriesThumbnailsLoader::OrthancSopClassHandler : public SeriesThumbnailsLoader::Handler + { + private: + std::string instanceId_; + + public: + OrthancSopClassHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& instanceId) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid), + instanceId_(instanceId) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + SeriesThumbnailType type = ExtractSopClassUid(body); + + if (type == SeriesThumbnailType_Pdf || + type == SeriesThumbnailType_Video) + { + GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(), + GetSeriesInstanceUid(), new Thumbnail(type)); + } + else + { + std::auto_ptr command(new GetOrthancImageCommand); + command->SetUri("/instances/" + instanceId_ + "/preview"); + command->SetHttpHeader("Accept", Orthanc::MIME_JPEG); + command->AcquirePayload(new ThumbnailInformation( + GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid())); + GetLoader()->Schedule(command.release()); + } + } + }; + + + class SeriesThumbnailsLoader::SelectOrthancInstanceHandler : public SeriesThumbnailsLoader::Handler + { + public: + SelectOrthancInstanceHandler(boost::shared_ptr loader, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) : + Handler(loader, source, studyInstanceUid, seriesInstanceUid) + { + } + + virtual void HandleSuccess(const std::string& body, + const std::map& headers) + { + static const char* const INSTANCES = "Instances"; + + Json::Value json; + Json::Reader reader; + if (!reader.parse(body, json) || + json.type() != Json::objectValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + + if (json.isMember(INSTANCES) && + json[INSTANCES].type() == Json::arrayValue && + json[INSTANCES].size() > 0) + { + // Select one instance of the series to generate the thumbnail + Json::Value::ArrayIndex index = json[INSTANCES].size() / 2; + if (json[INSTANCES][index].type() == Json::stringValue) + { + std::map arguments, headers; + arguments["quality"] = boost::lexical_cast(JPEG_QUALITY); + headers["Accept"] = Orthanc::MIME_JPEG; + + const std::string instance = json[INSTANCES][index].asString(); + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/instances/" + instance + "/metadata/SopClassUid"); + command->AcquirePayload( + new OrthancSopClassHandler( + GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid(), instance)); + GetLoader()->Schedule(command.release()); + } + } + } + }; + + + void SeriesThumbnailsLoader::Schedule(IOracleCommand* command) + { + std::auto_ptr lock(context_.Lock()); + lock->Schedule(GetSharedObserver(), priority_, command); + } + + + void SeriesThumbnailsLoader::Handle(const HttpCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + dynamic_cast(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesThumbnailsLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + dynamic_cast(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders()); + } + + + void SeriesThumbnailsLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message) + { + assert(message.GetOrigin().HasPayload()); + + std::auto_ptr resized(Orthanc::ImageProcessing::FitSize(message.GetImage(), width_, height_)); + + std::string jpeg; + Orthanc::JpegWriter writer; + writer.SetQuality(JPEG_QUALITY); + writer.WriteToMemory(jpeg, *resized); + + const ThumbnailInformation& info = dynamic_cast(message.GetOrigin().GetPayload()); + AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(), + info.GetSeriesInstanceUid(), new Thumbnail(jpeg, Orthanc::MIME_JPEG)); + } + + + void SeriesThumbnailsLoader::Handle(const OracleCommandExceptionMessage& message) + { + const OracleCommandBase& command = dynamic_cast(message.GetOrigin()); + assert(command.HasPayload()); + dynamic_cast(command.GetPayload()).HandleError(); + } + + + SeriesThumbnailsLoader::SeriesThumbnailsLoader(ILoadersContext& context, + int priority) : + context_(context), + priority_(priority), + width_(128), + height_(128) + { + } + + + boost::shared_ptr SeriesThumbnailsLoader::Factory::Create(ILoadersContext::ILock& stone) + { + boost::shared_ptr result(new SeriesThumbnailsLoader(stone.GetContext(), priority_)); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + result->Register(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle); + return result; + } + + + void SeriesThumbnailsLoader::SetThumbnailSize(unsigned int width, + unsigned int height) + { + if (width <= 0 || + height <= 0) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + width_ = width; + height_ = height; + } + } + + + void SeriesThumbnailsLoader::Clear() + { + for (Thumbnails::iterator it = thumbnails_.begin(); it != thumbnails_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + thumbnails_.clear(); + } + + + SeriesThumbnailType SeriesThumbnailsLoader::GetSeriesThumbnail(std::string& image, + std::string& mime, + const std::string& seriesInstanceUid) const + { + Thumbnails::const_iterator found = thumbnails_.find(seriesInstanceUid); + + if (found == thumbnails_.end()) + { + return SeriesThumbnailType_Unknown; + } + else + { + assert(found->second != NULL); + image.assign(found->second->GetImage()); + mime.assign(found->second->GetMime()); + return found->second->GetType(); + } + } + + + void SeriesThumbnailsLoader::ScheduleLoadThumbnail(const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid) + { + if (source.IsDicomWeb()) + { + if (!source.HasDicomWebRendered()) + { + // TODO - Could use DCMTK here + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "DICOMweb server is not able to generate renderings of DICOM series"); + } + + const std::string uri = ("/studies/" + studyInstanceUid + + "/series/" + seriesInstanceUid + "/rendered"); + + std::map arguments, headers; + arguments["viewport"] = (boost::lexical_cast(width_) + "," + + boost::lexical_cast(height_)); + + // Needed to set this header explicitly, as long as emscripten + // does not include macro "EMSCRIPTEN_FETCH_RESPONSE_HEADERS" + // https://github.com/emscripten-core/emscripten/pull/8486 + headers["Accept"] = Orthanc::MIME_JPEG; + + std::auto_ptr command( + source.CreateDicomWebCommand( + uri, arguments, headers, new DicomWebThumbnailHandler( + shared_from_this(), source, studyInstanceUid, seriesInstanceUid))); + Schedule(command.release()); + } + else if (source.IsOrthanc()) + { + // Dummy SOP Instance UID, as we are working at the "series" level + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy"); + + std::auto_ptr command(new OrthancRestApiCommand); + command->SetUri("/series/" + hasher.HashSeries()); + command->AcquirePayload(new SelectOrthancInstanceHandler( + shared_from_this(), source, studyInstanceUid, seriesInstanceUid)); + Schedule(command.release()); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "Can only load thumbnails from Orthanc or DICOMweb"); + } + } +} diff -r a1c0c9c9f9af -r c471a0aa137b Framework/Loaders/SeriesThumbnailsLoader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Loaders/SeriesThumbnailsLoader.h Mon Dec 09 13:58:37 2019 +0100 @@ -0,0 +1,209 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., 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 . + **/ + + +#pragma once + +#include "../Oracle/GetOrthancImageCommand.h" +#include "../Oracle/HttpCommand.h" +#include "../Oracle/OracleCommandExceptionMessage.h" +#include "../Oracle/OrthancRestApiCommand.h" +#include "DicomSource.h" +#include "ILoaderFactory.h" +#include "OracleScheduler.h" + + +namespace OrthancStone +{ + enum SeriesThumbnailType + { + SeriesThumbnailType_Unknown = 1, + SeriesThumbnailType_Pdf = 2, + SeriesThumbnailType_Video = 3, + SeriesThumbnailType_Image = 4 + }; + + + class SeriesThumbnailsLoader : + public IObservable, + public ObserverBase + { + private: + class Thumbnail : public boost::noncopyable + { + private: + SeriesThumbnailType type_; + std::string image_; + std::string mime_; + + public: + Thumbnail(const std::string& image, + const std::string& mime); + + Thumbnail(SeriesThumbnailType type); + + SeriesThumbnailType GetType() const + { + return type_; + } + + const std::string& GetImage() const + { + return image_; + } + + const std::string& GetMime() const + { + return mime_; + } + }; + + public: + class ThumbnailLoadedMessage : public OriginMessage + { + ORTHANC_STONE_MESSAGE(__FILE__, __LINE__); + + private: + const DicomSource& source_; + const std::string& studyInstanceUid_; + const std::string& seriesInstanceUid_; + const Thumbnail& thumbnail_; + + public: + ThumbnailLoadedMessage(const SeriesThumbnailsLoader& origin, + const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const Thumbnail& thumbnail) : + OriginMessage(origin), + source_(source), + studyInstanceUid_(studyInstanceUid), + seriesInstanceUid_(seriesInstanceUid), + thumbnail_(thumbnail) + { + } + + const DicomSource& GetDicomSource() const + { + return source_; + } + + SeriesThumbnailType GetType() const + { + return thumbnail_.GetType(); + } + + const std::string& GetStudyInstanceUid() const + { + return studyInstanceUid_; + } + + const std::string& GetSeriesInstanceUid() const + { + return seriesInstanceUid_; + } + + const std::string& GetEncodedImage() const + { + return thumbnail_.GetImage(); + } + + const std::string& GetMime() const + { + return thumbnail_.GetMime(); + } + }; + + private: + class Handler; + class DicomWebSopClassHandler; + class DicomWebThumbnailHandler; + class ThumbnailInformation; + class OrthancSopClassHandler; + class SelectOrthancInstanceHandler; + + // Maps a "Series Instance UID" to a thumbnail + typedef std::map Thumbnails; + + ILoadersContext& context_; + Thumbnails thumbnails_; + int priority_; + unsigned int width_; + unsigned int height_; + + void AcquireThumbnail(const DicomSource& source, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + Thumbnail* thumbnail /* takes ownership */); + + void Schedule(IOracleCommand* command); + + void Handle(const HttpCommand::SuccessMessage& message); + + void Handle(const OrthancRestApiCommand::SuccessMessage& message); + + void Handle(const GetOrthancImageCommand::SuccessMessage& message); + + void Handle(const OracleCommandExceptionMessage& message); + + SeriesThumbnailsLoader(ILoadersContext& context, + int priority); + + public: + class Factory : public ILoaderFactory + { + private: + int priority_; + + public: + Factory() : + priority_(0) + { + } + + void SetPriority(int priority) + { + priority_ = priority; + } + + virtual boost::shared_ptr Create(ILoadersContext::ILock& context); + }; + + + virtual ~SeriesThumbnailsLoader() + { + Clear(); + } + + void SetThumbnailSize(unsigned int width, + unsigned int height); + + void Clear(); + + SeriesThumbnailType GetSeriesThumbnail(std::string& image, + std::string& mime, + const std::string& seriesInstanceUid) const; + + void ScheduleLoadThumbnail(const DicomSource& source, + const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid); + }; +} diff -r a1c0c9c9f9af -r c471a0aa137b Resources/CMake/OrthancStoneConfiguration.cmake --- a/Resources/CMake/OrthancStoneConfiguration.cmake Sun Dec 08 12:06:44 2019 +0100 +++ b/Resources/CMake/OrthancStoneConfiguration.cmake Mon Dec 09 13:58:37 2019 +0100 @@ -252,13 +252,14 @@ if (NOT ORTHANC_SANDBOXED) set(PLATFORM_SOURCES - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp - ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/GenericLoadersContext.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/DelayedCallCommand.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/Oracle.cpp ${ORTHANC_STONE_ROOT}/Platforms/Generic/OracleDelayedCallExecutor.h + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceCommandBase.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceDeleteCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServiceGetCommand.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Generic/WebServicePostCommand.cpp ) if (ENABLE_SDL) @@ -469,8 +470,17 @@ ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.h + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomResourcesLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomSource.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomVolumeLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingItemsSorter.h ${ORTHANC_STONE_ROOT}/Framework/Loaders/IFetchingStrategy.h + ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoadedDicomResources.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/OracleScheduler.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesFramesLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesMetadataLoader.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesOrderedFrames.cpp + ${ORTHANC_STONE_ROOT}/Framework/Loaders/SeriesThumbnailsLoader.cpp ${ORTHANC_STONE_ROOT}/Framework/Messages/ICallable.h ${ORTHANC_STONE_ROOT}/Framework/Messages/IMessage.h