# HG changeset patch # User Sebastien Jodogne # Date 1788359404 -7200 # Node ID 41f3ecfa81bddcac6447bf939c8c8a290d960a77 # Parent bcf6c5f43f1df3a6400b37554444762cde5b2b00 reorganization diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/Annotations/AnnotationsRestApi.cpp --- a/ViewerPlugin/Annotations/AnnotationsRestApi.cpp Wed Sep 02 16:09:00 2026 +0200 +++ b/ViewerPlugin/Annotations/AnnotationsRestApi.cpp Wed Sep 02 16:30:04 2026 +0200 @@ -26,11 +26,11 @@ #include "../ViewerConfiguration.h" #include "../ViewerToolbox.h" -#include "AnnotationsWorkspace.h" +#include "CachedAnnotationsWorkspace.h" #include "IAuthenticatedUser.h" +#include "UserFeatures.h" #include -#include #include #include @@ -42,56 +42,11 @@ static const char* const KEY_FEATURES = "features"; static const char* const KEY_ID = "id"; static const char* const KEY_LAYERS = "layers"; - static const char* const KEY_LAYER_ID = "layer-id"; static const char* const KEY_NAME = "name"; static const char* const KEY_PUBLIC = "public"; - static const char* const KEY_TYPE = "type"; - static const char* const KEY_VERSION = "version"; static const char* const KEY_PROJECT_NAME = "project-name"; static const char* const KEY_PROJECT_DESCRIPTION = "project-description"; - - - class CachedAnnotationsWorkspace : public boost::noncopyable - { - private: - boost::shared_ptr cached_; - - static Orthanc::SharedObjectCache& GetCache() - { - static boost::mutex mutex; - static std::unique_ptr cache; - - { - boost::mutex::scoped_lock lock(mutex); - - if (cache.get() == NULL) - { - cache.reset(new Orthanc::SharedObjectCache(ViewerConfiguration::GetInstance().GetAnnotationsCacheSize())); - } - - return *cache; - } - } - - public: - CachedAnnotationsWorkspace(const AnnotationsWorkspaceId& id) - { - const std::string key = id.GetInfoKey(); - - cached_ = GetCache().GetCachedValue(key); - - if (cached_.get() == NULL) - { - cached_.reset(new AnnotationsWorkspace(id)); - GetCache().Store(key, cached_, 1); - } - } - - AnnotationsWorkspace& GetContent() const - { - return dynamic_cast(*cached_); - } - }; + static const char* const KEY_LAYER_ID = "layer-id"; class AnnotationsCommandContext : public boost::noncopyable @@ -296,115 +251,6 @@ } - class UserFeatures : public Orthanc::IDynamicObject - { - private: - Orthanc::ReaderWriterLock mutex_; - std::string key_; - Json::Value content_; - - void Load() - { - content_ = Json::arrayValue; - - std::string compressed; - if (ViewerToolbox::LookupKeyValueStore(compressed, key_)) - { - std::string uncompressed; - Orthanc::GzipCompressor compressor; - Orthanc::IBufferCompressor::Uncompress(uncompressed, compressor, compressed); - - Json::Value unserialized; - - if (!Orthanc::Toolbox::ReadJson(unserialized, uncompressed) || - !unserialized.isObject() || - !unserialized.isMember(KEY_FEATURES) || - !unserialized[KEY_FEATURES].isArray()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - - const unsigned int version = Orthanc::SerializationToolbox::ReadUnsignedInteger(unserialized, KEY_VERSION); - - if (version == ORTHANC_WSI_ANNOTATIONS_VERSION) - { - content_ = unserialized[KEY_FEATURES]; - } - else - { - switch (version) - { - // Implement version conversion here - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Cannot load annotations from version: " + - boost::lexical_cast(version)); - } - } - } - } - - void Save() const - { - assert(content_.isArray()); - - Json::Value unserialized; - unserialized[KEY_VERSION] = static_cast(ORTHANC_WSI_ANNOTATIONS_VERSION); - unserialized[KEY_FEATURES] = content_; - - std::string serialized; - Orthanc::Toolbox::WriteFastJson(serialized, unserialized); - - std::string compressed; - Orthanc::GzipCompressor compressor; - Orthanc::IBufferCompressor::Compress(compressed, compressor, serialized); - - ViewerToolbox::SetKeyValueStore(key_, compressed); - } - - public: - UserFeatures(const std::string& key) : - key_(key) - { - Load(); - } - - void GetContent(Json::Value& target) - { - Orthanc::ReaderWriterLock::ReadLock lock(mutex_); - - assert(content_.isArray()); - target = content_; - } - - void SetContent(const Json::Value& content) - { - if (!content.isArray()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - - for (Json::Value::ArrayIndex i = 0; i < content.size(); i++) - { - if (!content[i].isObject() || - !content[i].isMember(KEY_LAYER_ID) || - !content[i].isMember(KEY_TYPE) || - !content[i][KEY_LAYER_ID].isString() || - !content[i][KEY_TYPE].isString()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } - } - - { - Orthanc::ReaderWriterLock::WriteLock lock(mutex_); - content_ = content; - Save(); - } - } - }; - - class CachedUserFeatures : public boost::noncopyable { private: diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/Annotations/AnnotationsWorkspace.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.cpp Wed Sep 02 16:30:04 2026 +0200 @@ -0,0 +1,420 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "AnnotationsWorkspace.h" + +#include "../ViewerToolbox.h" + +#include + +#include + + +static const char* const KEY_ACTIVE_USERS = "active-users"; + + +namespace OrthancWSI +{ + class AnnotationsWorkspace::PersistentInfo : public ISerializable + { + private: + std::set activeUsers_; + + public: + PersistentInfo() + { + } + + + PersistentInfo(const Json::Value& serialized) + { + const Json::Value& users = serialized[KEY_ACTIVE_USERS]; + + if (!users.isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + for (Json::Value::ArrayIndex i = 0; i < users.size(); i++) + { + activeUsers_.insert(UserId(users[i])); + } + } + + + // Return "true" iff. the user was not already tagged as active + bool AddActiveUser(const UserId& user) + { + if (activeUsers_.find(user) == activeUsers_.end()) + { + activeUsers_.insert(user); + return true; + } + else + { + return false; + } + } + + + const std::set& GetActiveUsers() const + { + return activeUsers_; + } + + + virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE + { + Json::Value users = Json::arrayValue; + for (std::set::const_iterator it = activeUsers_.begin(); it != activeUsers_.end(); ++it) + { + Json::Value user; + it->Serialize(user); + users.append(user); + } + + serialized = Json::objectValue; + serialized[KEY_ACTIVE_USERS] = users; + } + }; + + + void AnnotationsWorkspace::Load(const UserId& user) + { + const std::string key = id_.GetSettingsKey(user); + + Json::Value layers; + if (ViewerToolbox::LookupKeyValueStore(layers, key)) + { + std::unique_ptr item(new UserAnnotationsSettings(layers)); + + if (content_.find(user) == content_.end()) // Should never be false + { + content_[user] = item.release(); + } + } + } + + + AnnotationsWorkspace::AnnotationsWorkspace(const AnnotationsWorkspaceId& id) : + id_(id), + projectInformation_(id.GetProjectId()) + { + const std::string key = id.GetInfoKey(); + + Json::Value info; + + if (ViewerToolbox::LookupKeyValueStore(info, key)) + { + persistentInfo_.reset(new PersistentInfo(info)); + + for (std::set::const_iterator it = persistentInfo_->GetActiveUsers().begin(); + it != persistentInfo_->GetActiveUsers().end(); ++it) + { + Load(*it); + } + } + else + { + persistentInfo_.reset(new PersistentInfo); + persistentInfo_->Serialize(info); + ViewerToolbox::SetKeyValueStore(key, info); + } + } + + + AnnotationsWorkspace::~AnnotationsWorkspace() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void AnnotationsWorkspace::SearchActiveUsers(std::set& target, + const std::string& query) + { + Orthanc::ReaderWriterLock::ReadLock lock(mutex_); + + target.clear(); + + const boost::regex re(query); + const std::set& activeUsers = persistentInfo_->GetActiveUsers(); + + for (std::set::const_iterator it = activeUsers.begin(); it != activeUsers.end(); ++it) + { + if (boost::regex_search(it->GetName(), re)) + { + target.insert(*it); + } + } + } + + + void AnnotationsWorkspace::ListUsersSharingLayerWith(std::set& target, + const UserId& user) + { + Orthanc::ReaderWriterLock::ReadLock lock(mutex_); + + target.clear(); + + // Loop over all the users in this workspace + for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + if (!user.Equals(it->first)) // Don't add self + { + LayersCollection::Iterator iterator(it->second->GetUserLayers()); + + // Loop over all the user layers in this workspace + while (!iterator.IsDone()) + { + const UserLayer& layer = dynamic_cast(iterator.GetLayer()); + + if (layer.IsSharedWith(user)) + { + target.insert(it->first); + break; + } + else + { + iterator.Next(); + } + } + } + } + } + + + AnnotationsWorkspace::UserReader::UserReader(AnnotationsWorkspace& that, + const UserId& userId) : + lock_(that.mutex_), + that_(that), + userId_(userId) + { + Content::const_iterator found = that.content_.find(userId); + + if (found == that.content_.end()) + { + userSettings_ = NULL; + } + else + { + assert(found->second != NULL); + userSettings_ = found->second; + } + } + + + void AnnotationsWorkspace::UserReader::ListLayers(Json::Value& serialized) const + { + serialized = Json::objectValue; + + if (IsValid()) + { + userSettings_->Serialize(serialized); + } + else + { + UserAnnotationsSettings empty; + empty.Serialize(serialized); + } + } + + + void AnnotationsWorkspace::UserReader::ListLayersSharedWith(Json::Value& target, + const UserId& user) const + { + target = Json::arrayValue; + + if (IsValid()) + { + LayersCollection::Iterator iterator(userSettings_->GetUserLayers()); + + while (!iterator.IsDone()) + { + const UserLayer& layer = dynamic_cast(iterator.GetLayer()); + + if (layer.IsSharedWith(user)) + { + Json::Value item; + layer.Serialize(item); + target.append(item); + } + + iterator.Next(); + } + } + } + + + void AnnotationsWorkspace::UserReader::ListImportedLayers(std::set& authors, + std::set& layerIds) const + { + authors.clear(); + layerIds.clear(); + + if (IsValid()) + { + LayersCollection::Iterator iterator(userSettings_->GetImportedLayers()); + + while (!iterator.IsDone()) + { + const ImportedLayer& layer = dynamic_cast(iterator.GetLayer()); + + Content::const_iterator found = that_.content_.find(layer.GetAuthor()); + + if (found != that_.content_.end()) + { + assert(found->second != NULL); + + // Check that the layer has not been deleted in the meantime by its author, + // and that the layer is still shared with this user + if (found->second->GetUserLayers().HasLayer(layer.GetId())) + { + const UserLayer& authorLayer = found->second->GetUserLayer(layer.GetId()); + + if (authorLayer.IsSharedWith(userId_)) + { + authors.insert(layer.GetAuthor()); + layerIds.insert(layer.GetId()); + } + } + } + + iterator.Next(); + } + } + } + + + void AnnotationsWorkspace::UserWriter::Commit() + { + ISerializable::SetKeyValueStore(that_.id_.GetSettingsKey(userId_), *userSettings_); + } + + + AnnotationsWorkspace::UserWriter::UserWriter(AnnotationsWorkspace& that, + const UserId& userId) : + lock_(that.mutex_), + that_(that), + userId_(userId) + { + if (that.persistentInfo_->AddActiveUser(userId_)) + { + // Only update the key-value store if this is the first time we meet this user + ISerializable::SetKeyValueStore(that.id_.GetInfoKey(), *that.persistentInfo_); + } + + Content::iterator found = that.content_.find(userId_); + + if (found == that.content_.end()) + { + std::unique_ptr layers(new UserAnnotationsSettings); + userSettings_ = layers.get(); + that.content_[userId_] = layers.release(); + Commit(); + } + else + { + assert(found->second != NULL); + userSettings_ = found->second; + } + } + + + void AnnotationsWorkspace::UserWriter::CreateUserLayer(Json::Value& answer) + { + assert(userSettings_ != NULL); + + const std::string layerId = userSettings_->CreateUserLayer(); + Commit(); + + UserLayer& layer = userSettings_->GetUserLayer(layerId); + layer.Serialize(answer); + } + + + void AnnotationsWorkspace::UserWriter::UpdateUserLayer(const UserLayer& updated) + { + assert(userSettings_ != NULL); + + UserLayer& layer = userSettings_->GetUserLayer(updated.GetId()); + layer.Assign(updated); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::DeleteUserLayer(const std::string& layerId) + { + assert(userSettings_ != NULL); + userSettings_->GetUserLayers().DeleteLayer(layerId); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::ImportLayer(const UserId& author, + const std::string& layerId) + { + assert(userSettings_ != NULL); + + Content::const_iterator found = that_.content_.find(author); + if (found == that_.content_.end()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + + assert(found->second != NULL); + const UserAnnotationsSettings& authorData = *found->second; + + UserLayer& layer = authorData.GetUserLayer(layerId); + if (!layer.IsSharedWith(userId_)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + } + + userSettings_->ImportLayer(author, layer); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::RemoveImportedLayer(const std::string& layerId) + { + assert(userSettings_ != NULL); + userSettings_->GetImportedLayers().DeleteLayer(layerId); + Commit(); + } + + + void AnnotationsWorkspace::UserWriter::UpdateImportedLayer(const ImportedLayer& updated) + { + assert(userSettings_ != NULL); + + ImportedLayer& layer = userSettings_->GetImportedLayer(updated.GetId()); + layer.Assign(updated); + Commit(); + } +} diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/Annotations/AnnotationsWorkspace.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.h Wed Sep 02 16:30:04 2026 +0200 @@ -0,0 +1,133 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "AnnotationsWorkspaceId.h" +#include "ProjectInformation.h" +#include "UserAnnotationsSettings.h" + +#include +#include + + +namespace OrthancWSI +{ + class AnnotationsWorkspace : public Orthanc::IDynamicObject + { + private: + class PersistentInfo; + + void Load(const UserId& user); + + typedef std::map Content; + + Orthanc::ReaderWriterLock mutex_; + AnnotationsWorkspaceId id_; + std::unique_ptr persistentInfo_; + Content content_; + ProjectInformation projectInformation_; + + public: + AnnotationsWorkspace(const AnnotationsWorkspaceId& id); + + ~AnnotationsWorkspace(); + + const AnnotationsWorkspaceId& GetId() const + { + return id_; + } + + std::string GetProjectName() + { + return projectInformation_.GetName(); + } + + std::string GetProjectDescription() + { + return projectInformation_.GetDescription(); + } + + void SearchActiveUsers(std::set& target, + const std::string& query); + + void ListUsersSharingLayerWith(std::set& target, + const UserId& user); + + + class UserReader : public boost::noncopyable + { + private: + Orthanc::ReaderWriterLock::ReadLock lock_; + AnnotationsWorkspace& that_; + UserId userId_; + const UserAnnotationsSettings* userSettings_; + + public: + UserReader(AnnotationsWorkspace& that, + const UserId& userId); + + bool IsValid() const + { + return userSettings_ != NULL; + } + + void ListLayers(Json::Value& serialized) const; + + void ListLayersSharedWith(Json::Value& target, + const UserId& user) const; + + void ListImportedLayers(std::set& authors, + std::set& layerIds) const; + }; + + + class UserWriter : public boost::noncopyable + { + private: + Orthanc::ReaderWriterLock::WriteLock lock_; + AnnotationsWorkspace& that_; + UserId userId_; + UserAnnotationsSettings* userSettings_; + + void Commit(); + + public: + UserWriter(AnnotationsWorkspace& that, + const UserId& userId); + + void CreateUserLayer(Json::Value& answer); + + void UpdateUserLayer(const UserLayer& updated); + + void DeleteUserLayer(const std::string& layerId); + + void ImportLayer(const UserId& author, + const std::string& layerId); + + void RemoveImportedLayer(const std::string& layerId); + + void UpdateImportedLayer(const ImportedLayer& updated); + }; + }; +} diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/Annotations/CachedAnnotationsWorkspace.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/CachedAnnotationsWorkspace.cpp Wed Sep 02 16:30:04 2026 +0200 @@ -0,0 +1,65 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "CachedAnnotationsWorkspace.h" + +#include "../ViewerConfiguration.h" + +#include + + +static Orthanc::SharedObjectCache& GetCache() +{ + static boost::mutex mutex; + static std::unique_ptr cache; + + { + boost::mutex::scoped_lock lock(mutex); + + if (cache.get() == NULL) + { + const unsigned int cacheSize = OrthancWSI::ViewerConfiguration::GetInstance().GetAnnotationsCacheSize(); + cache.reset(new Orthanc::SharedObjectCache(cacheSize)); + } + + return *cache; + } +} + + +namespace OrthancWSI +{ + CachedAnnotationsWorkspace::CachedAnnotationsWorkspace(const AnnotationsWorkspaceId& id) + { + const std::string key = id.GetInfoKey(); + + cached_ = GetCache().GetCachedValue(key); + + if (cached_.get() == NULL) + { + cached_.reset(new AnnotationsWorkspace(id)); + GetCache().Store(key, cached_, 1); + } + } +} diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/Annotations/CachedAnnotationsWorkspace.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/CachedAnnotationsWorkspace.h Wed Sep 02 16:30:04 2026 +0200 @@ -0,0 +1,46 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "AnnotationsWorkspace.h" + +#include + + +namespace OrthancWSI +{ + class CachedAnnotationsWorkspace : public boost::noncopyable + { + private: + boost::shared_ptr cached_; + + public: + CachedAnnotationsWorkspace(const AnnotationsWorkspaceId& id); + + AnnotationsWorkspace& GetContent() const + { + return dynamic_cast(*cached_); + } + }; +} diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/Annotations/UserFeatures.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserFeatures.cpp Wed Sep 02 16:30:04 2026 +0200 @@ -0,0 +1,147 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "../../Framework/PrecompiledHeadersWSI.h" +#include "UserFeatures.h" + +#include "../ViewerToolbox.h" + +#include +#include +#include +#include + +#include + + +static const char* const KEY_FEATURES = "features"; +static const char* const KEY_LAYER_ID = "layer-id"; +static const char* const KEY_TYPE = "type"; +static const char* const KEY_VERSION = "version"; + + +namespace OrthancWSI +{ + void UserFeatures::Load() + { + content_ = Json::arrayValue; + + std::string compressed; + if (ViewerToolbox::LookupKeyValueStore(compressed, key_)) + { + std::string uncompressed; + Orthanc::GzipCompressor compressor; + Orthanc::IBufferCompressor::Uncompress(uncompressed, compressor, compressed); + + Json::Value unserialized; + + if (!Orthanc::Toolbox::ReadJson(unserialized, uncompressed) || + !unserialized.isObject() || + !unserialized.isMember(KEY_FEATURES) || + !unserialized[KEY_FEATURES].isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + const unsigned int version = Orthanc::SerializationToolbox::ReadUnsignedInteger(unserialized, KEY_VERSION); + + if (version == ORTHANC_WSI_ANNOTATIONS_VERSION) + { + content_ = unserialized[KEY_FEATURES]; + } + else + { + switch (version) + { + // Implement version conversion here + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Cannot load annotations from version: " + + boost::lexical_cast(version)); + } + } + } + } + + + void UserFeatures::Save() const + { + assert(content_.isArray()); + + Json::Value unserialized; + unserialized[KEY_VERSION] = static_cast(ORTHANC_WSI_ANNOTATIONS_VERSION); + unserialized[KEY_FEATURES] = content_; + + std::string serialized; + Orthanc::Toolbox::WriteFastJson(serialized, unserialized); + + std::string compressed; + Orthanc::GzipCompressor compressor; + Orthanc::IBufferCompressor::Compress(compressed, compressor, serialized); + + ViewerToolbox::SetKeyValueStore(key_, compressed); + } + + + UserFeatures::UserFeatures(const std::string& key) : + key_(key) + { + Load(); + } + + + void UserFeatures::GetContent(Json::Value& target) + { + Orthanc::ReaderWriterLock::ReadLock lock(mutex_); + + assert(content_.isArray()); + target = content_; + } + + + void UserFeatures::SetContent(const Json::Value& content) + { + if (!content.isArray()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + for (Json::Value::ArrayIndex i = 0; i < content.size(); i++) + { + if (!content[i].isObject() || + !content[i].isMember(KEY_LAYER_ID) || + !content[i].isMember(KEY_TYPE) || + !content[i][KEY_LAYER_ID].isString() || + !content[i][KEY_TYPE].isString()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + } + + { + Orthanc::ReaderWriterLock::WriteLock lock(mutex_); + content_ = content; + Save(); + } + } +} diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/Annotations/UserFeatures.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ViewerPlugin/Annotations/UserFeatures.h Wed Sep 02 16:30:04 2026 +0200 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +#include +#include + + +namespace OrthancWSI +{ + class UserFeatures : public Orthanc::IDynamicObject + { + private: + Orthanc::ReaderWriterLock mutex_; + std::string key_; + Json::Value content_; + + void Load(); + + void Save() const; + + public: + UserFeatures(const std::string& key); + + void GetContent(Json::Value& target); + + void SetContent(const Json::Value& content); + }; +} diff -r bcf6c5f43f1d -r 41f3ecfa81bd ViewerPlugin/CMakeLists.txt --- a/ViewerPlugin/CMakeLists.txt Wed Sep 02 16:09:00 2026 +0200 +++ b/ViewerPlugin/CMakeLists.txt Wed Sep 02 16:30:04 2026 +0200 @@ -188,12 +188,14 @@ Annotations/AnnotationsRestApi.cpp Annotations/AnnotationsWorkspace.cpp Annotations/AnnotationsWorkspaceId.cpp + Annotations/CachedAnnotationsWorkspace.cpp Annotations/IAuthenticatedUser.cpp Annotations/ISerializable.cpp Annotations/ImportedLayer.cpp Annotations/LayersCollection.cpp Annotations/ProjectInformation.cpp Annotations/UserAnnotationsSettings.cpp + Annotations/UserFeatures.cpp Annotations/UserId.cpp Annotations/UserLayer.cpp DicomPyramidCache.cpp