changeset 573:bcf6c5f43f1d annotations

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 02 Sep 2026 16:09:00 +0200
parents 3efdbcfe34ab
children 41f3ecfa81bd
files ViewerPlugin/Annotations/AnnotationsRestApi.cpp ViewerPlugin/Annotations/UserAnnotationsSettings.cpp ViewerPlugin/Annotations/UserAnnotationsSettings.h ViewerPlugin/CMakeLists.txt
diffstat 4 files changed, 229 insertions(+), 568 deletions(-) [+]
line wrap: on
line diff
--- a/ViewerPlugin/Annotations/AnnotationsRestApi.cpp	Tue Sep 01 22:14:57 2026 +0200
+++ b/ViewerPlugin/Annotations/AnnotationsRestApi.cpp	Wed Sep 02 16:09:00 2026 +0200
@@ -26,23 +26,16 @@
 
 #include "../ViewerConfiguration.h"
 #include "../ViewerToolbox.h"
-#include "AnnotationsWorkspaceId.h"
+#include "AnnotationsWorkspace.h"
 #include "IAuthenticatedUser.h"
-#include "ImportedLayer.h"
-#include "LayersCollection.h"
-#include "ProjectInformation.h"
-#include "UserLayer.h"
 
 #include <Cache/SharedObjectCache.h>
 #include <Compression/GzipCompressor.h>
 #include <Logging.h>
-#include <MultiThreading/ReaderWriterLock.h>
 #include <SerializationToolbox.h>
 
 #include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
 
-#include <boost/regex.hpp>
-
 
 namespace OrthancWSI
 {
@@ -54,568 +47,8 @@
   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_ACTIVE_USERS = "active-users";
   static const char* const KEY_PROJECT_NAME = "project-name";
   static const char* const KEY_PROJECT_DESCRIPTION = "project-description";
-  static const char* const KEY_USER_LAYERS = "user-layers";
-  static const char* const KEY_IMPORTED_LAYERS = "imported-layers";
-
-
-  class UserAnnotationsSettings : public ISerializable
-  {
-  private:
-    LayersCollection  userLayers_;
-    LayersCollection  importedLayers_;
-
-  public:
-    UserAnnotationsSettings()
-    {
-    }
-
-    UserAnnotationsSettings(const Json::Value& serialized)
-    {
-      if (!serialized.isObject() ||
-          !serialized.isMember(KEY_USER_LAYERS) ||
-          !serialized.isMember(KEY_IMPORTED_LAYERS) ||
-          !serialized[KEY_USER_LAYERS].isArray() ||
-          !serialized[KEY_IMPORTED_LAYERS].isArray())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-      else
-      {
-        const Json::Value& a = serialized[KEY_USER_LAYERS];
-        for (Json::Value::ArrayIndex i = 0; i < a.size(); i++)
-        {
-          userLayers_.AddLayer(new UserLayer(a[i]));
-        }
-
-        const Json::Value& b = serialized[KEY_IMPORTED_LAYERS];
-        for (Json::Value::ArrayIndex i = 0; i < b.size(); i++)
-        {
-          importedLayers_.AddLayer(new ImportedLayer(b[i]));
-        }
-      }
-    }
-
-    std::string AddUserLayer(UserLayer* layer)
-    {
-      std::unique_ptr<UserLayer> protection(layer);
-
-      if (layer == NULL)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
-      }
-
-      const std::string id = protection->GetId();
-
-      userLayers_.AddLayer(protection.release());
-
-      return id;
-    }
-
-    UserLayer& GetUserLayer(const std::string& layerId) const
-    {
-      return dynamic_cast<UserLayer&>(userLayers_.GetLayer(layerId));
-    }
-
-    ImportedLayer& GetImportedLayer(const std::string& layerId) const
-    {
-      return dynamic_cast<ImportedLayer&>(importedLayers_.GetLayer(layerId));
-    }
-
-    LayersCollection& GetUserLayers()
-    {
-      return userLayers_;
-    }
-
-    const LayersCollection& GetUserLayers() const
-    {
-      return userLayers_;
-    }
-
-    LayersCollection& GetImportedLayers()
-    {
-      return importedLayers_;
-    }
-
-    const LayersCollection& GetImportedLayers() const
-    {
-      return importedLayers_;
-    }
-
-    std::string CreateUserLayer()
-    {
-      static const uint8_t PALETTE[] = {
-        0xe6, 0x39, 0x46,  // red: #e63946
-        0x2a, 0x9d, 0x8f,
-        0xe9, 0xc4, 0x6a,
-        0x26, 0x46, 0x53,
-        0xf4, 0xa2, 0x61
-      };
-
-      static const size_t PALETTE_SIZE = sizeof(PALETTE) / (3 * sizeof(uint8_t));
-
-      size_t item = userLayers_.GetSize() % PALETTE_SIZE;
-
-      BackgroundColor color(PALETTE[3 * item],
-                            PALETTE[3 * item + 1],
-                            PALETTE[3 * item + 2]);
-
-      std::string name;
-      if (userLayers_.GetSize() == 0)
-      {
-        name = "Default";
-      }
-      else
-      {
-        name = "Layer " + boost::lexical_cast<std::string>(userLayers_.GetSize() + 1);
-      }
-
-      return AddUserLayer(new UserLayer(color, name));
-    }
-
-    virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE
-    {
-      serialized = Json::objectValue;
-      userLayers_.Serialize(serialized[KEY_USER_LAYERS]);
-      importedLayers_.Serialize(serialized[KEY_IMPORTED_LAYERS]);
-    }
-
-    void ImportLayer(const UserId& author,
-                     const UserLayer& layer)
-    {
-      if (importedLayers_.HasLayer(layer.GetId()))
-      {
-        LOG(INFO) << "Cannot re-import already imported layer: " << layer.GetId();
-      }
-      else
-      {
-        importedLayers_.AddLayer(new ImportedLayer(author, layer));
-      }
-    }
-  };
-
-
-  class AnnotationsWorkspace : public Orthanc::IDynamicObject
-  {
-  private:
-    class PersistentInfo : public ISerializable
-    {
-    private:
-      std::set<UserId>   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<UserId>& GetActiveUsers() const
-      {
-        return activeUsers_;
-      }
-
-      virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE
-      {
-        Json::Value users = Json::arrayValue;
-        for (std::set<UserId>::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 Load(const UserId& user)
-    {
-      const std::string key = id_.GetSettingsKey(user);
-
-      Json::Value layers;
-      if (ViewerToolbox::LookupKeyValueStore(layers, key))
-      {
-        std::unique_ptr<UserAnnotationsSettings> item(new UserAnnotationsSettings(layers));
-
-        if (content_.find(user) == content_.end())  // Should never be false
-        {
-          content_[user] = item.release();
-        }
-      }
-    }
-
-
-    typedef std::map<UserId, UserAnnotationsSettings*>   Content;
-
-    Orthanc::ReaderWriterLock        mutex_;
-    AnnotationsWorkspaceId           id_;
-    std::unique_ptr<PersistentInfo>  persistentInfo_;
-    Content                          content_;
-    ProjectInformation               projectInformation_;
-
-  public:
-    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<UserId>::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()
-    {
-      for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
-      {
-        assert(it->second != NULL);
-        delete it->second;
-      }
-    }
-
-    const AnnotationsWorkspaceId& GetId() const
-    {
-      return id_;
-    }
-
-    std::string GetProjectName()
-    {
-      return projectInformation_.GetName();
-    }
-
-    std::string GetProjectDescription()
-    {
-      return projectInformation_.GetDescription();
-    }
-
-
-    void SearchActiveUsers(std::set<UserId>& target,
-                           const std::string& query)
-    {
-      Orthanc::ReaderWriterLock::ReadLock lock(mutex_);
-
-      target.clear();
-
-      const boost::regex re(query);
-      const std::set<UserId>& activeUsers = persistentInfo_->GetActiveUsers();
-
-      for (std::set<UserId>::const_iterator it = activeUsers.begin(); it != activeUsers.end(); ++it)
-      {
-        if (boost::regex_search(it->GetName(), re))
-        {
-          target.insert(*it);
-        }
-      }
-    }
-
-
-    void ListUsersSharingLayerWith(std::set<UserId>& 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<const UserLayer&>(iterator.GetLayer());
-
-            if (layer.IsSharedWith(user))
-            {
-              target.insert(it->first);
-              break;
-            }
-            else
-            {
-              iterator.Next();
-            }
-          }
-        }
-      }
-    }
-
-
-    class UserReader : public boost::noncopyable
-    {
-    private:
-      Orthanc::ReaderWriterLock::ReadLock lock_;
-      AnnotationsWorkspace&               that_;
-      UserId                              userId_;
-      const UserAnnotationsSettings*      userSettings_;
-
-    public:
-      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;
-        }
-      }
-
-      bool IsValid() const
-      {
-        return userSettings_ != NULL;
-      }
-
-      void ListLayers(Json::Value& serialized) const
-      {
-        serialized = Json::objectValue;
-
-        if (IsValid())
-        {
-          userSettings_->Serialize(serialized);
-        }
-        else
-        {
-          serialized[KEY_USER_LAYERS] = Json::arrayValue;
-          serialized[KEY_IMPORTED_LAYERS] = Json::arrayValue;
-        }
-      }
-
-      void 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<const UserLayer&>(iterator.GetLayer());
-
-            if (layer.IsSharedWith(user))
-            {
-              Json::Value item;
-              layer.Serialize(item);
-              target.append(item);
-            }
-
-            iterator.Next();
-          }
-        }
-      }
-
-      void ListImportedLayers(std::set<UserId>& authors,
-                            std::set<std::string>& layerIds) const
-      {
-        authors.clear();
-        layerIds.clear();
-
-        if (IsValid())
-        {
-          LayersCollection::Iterator iterator(userSettings_->GetImportedLayers());
-
-          while (!iterator.IsDone())
-          {
-            const ImportedLayer& layer = dynamic_cast<const ImportedLayer&>(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();
-          }
-        }
-      }
-    };
-
-
-    class UserWriter : public boost::noncopyable
-    {
-    private:
-      Orthanc::ReaderWriterLock::WriteLock  lock_;
-      AnnotationsWorkspace&                 that_;
-      UserId                                userId_;
-      UserAnnotationsSettings*              userSettings_;
-
-      void Commit()
-      {
-        ISerializable::SetKeyValueStore(that_.id_.GetSettingsKey(userId_), *userSettings_);
-      }
-
-    public:
-      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<UserAnnotationsSettings> layers(new UserAnnotationsSettings);
-          userSettings_ = layers.get();
-          that.content_[userId_] = layers.release();
-          Commit();
-        }
-        else
-        {
-          assert(found->second != NULL);
-          userSettings_ = found->second;
-        }
-      }
-
-      void CreateUserLayer(Json::Value& answer)
-      {
-        assert(userSettings_ != NULL);
-
-        const std::string layerId = userSettings_->CreateUserLayer();
-        Commit();
-
-        UserLayer& layer = userSettings_->GetUserLayer(layerId);
-        layer.Serialize(answer);
-      }
-
-      void UpdateUserLayer(const UserLayer& updated)
-      {
-        assert(userSettings_ != NULL);
-
-        UserLayer& layer = userSettings_->GetUserLayer(updated.GetId());
-        layer.Assign(updated);
-        Commit();
-      }
-
-      void DeleteUserLayer(const std::string& layerId)
-      {
-        assert(userSettings_ != NULL);
-        userSettings_->GetUserLayers().DeleteLayer(layerId);
-        Commit();
-      }
-
-      void 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 RemoveImportedLayer(const std::string& layerId)
-      {
-        assert(userSettings_ != NULL);
-        userSettings_->GetImportedLayers().DeleteLayer(layerId);
-        Commit();
-      }
-
-      void UpdateImportedLayer(const ImportedLayer& updated)
-      {
-        assert(userSettings_ != NULL);
-
-        ImportedLayer& layer = userSettings_->GetImportedLayer(updated.GetId());
-        layer.Assign(updated);
-        Commit();
-      }
-    };
-  };
 
 
   class CachedAnnotationsWorkspace : public boost::noncopyable
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserAnnotationsSettings.cpp	Wed Sep 02 16:09:00 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../Framework/PrecompiledHeadersWSI.h"
+#include "UserAnnotationsSettings.h"
+
+#include <Logging.h>
+#include <OrthancException.h>
+
+#include <boost/lexical_cast.hpp>
+
+
+static const char* const KEY_USER_LAYERS = "user-layers";
+static const char* const KEY_IMPORTED_LAYERS = "imported-layers";
+
+
+namespace OrthancWSI
+{
+  UserAnnotationsSettings::UserAnnotationsSettings(const Json::Value& serialized)
+  {
+    if (!serialized.isObject() ||
+        !serialized.isMember(KEY_USER_LAYERS) ||
+        !serialized.isMember(KEY_IMPORTED_LAYERS) ||
+        !serialized[KEY_USER_LAYERS].isArray() ||
+        !serialized[KEY_IMPORTED_LAYERS].isArray())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      const Json::Value& a = serialized[KEY_USER_LAYERS];
+      for (Json::Value::ArrayIndex i = 0; i < a.size(); i++)
+      {
+        userLayers_.AddLayer(new UserLayer(a[i]));
+      }
+
+      const Json::Value& b = serialized[KEY_IMPORTED_LAYERS];
+      for (Json::Value::ArrayIndex i = 0; i < b.size(); i++)
+      {
+        importedLayers_.AddLayer(new ImportedLayer(b[i]));
+      }
+    }
+  }
+
+
+  std::string UserAnnotationsSettings::AddUserLayer(UserLayer* layer)
+  {
+    std::unique_ptr<UserLayer> protection(layer);
+
+    if (layer == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    const std::string id = protection->GetId();
+
+    userLayers_.AddLayer(protection.release());
+
+    return id;
+  }
+
+
+  UserLayer& UserAnnotationsSettings::GetUserLayer(const std::string& layerId) const
+  {
+    return dynamic_cast<UserLayer&>(userLayers_.GetLayer(layerId));
+  }
+
+
+  ImportedLayer& UserAnnotationsSettings::GetImportedLayer(const std::string& layerId) const
+  {
+    return dynamic_cast<ImportedLayer&>(importedLayers_.GetLayer(layerId));
+  }
+
+
+  std::string UserAnnotationsSettings::CreateUserLayer()
+  {
+    static const uint8_t PALETTE[] = {
+      0xe6, 0x39, 0x46,  // red: #e63946
+      0x2a, 0x9d, 0x8f,
+      0xe9, 0xc4, 0x6a,
+      0x26, 0x46, 0x53,
+      0xf4, 0xa2, 0x61
+    };
+
+    static const size_t PALETTE_SIZE = sizeof(PALETTE) / (3 * sizeof(uint8_t));
+
+    size_t item = userLayers_.GetSize() % PALETTE_SIZE;
+
+    BackgroundColor color(PALETTE[3 * item],
+                          PALETTE[3 * item + 1],
+                          PALETTE[3 * item + 2]);
+
+    std::string name;
+    if (userLayers_.GetSize() == 0)
+    {
+      name = "Default";
+    }
+    else
+    {
+      name = "Layer " + boost::lexical_cast<std::string>(userLayers_.GetSize() + 1);
+    }
+
+    return AddUserLayer(new UserLayer(color, name));
+  }
+
+
+  void UserAnnotationsSettings::Serialize(Json::Value& serialized) const
+  {
+    serialized = Json::objectValue;
+    userLayers_.Serialize(serialized[KEY_USER_LAYERS]);
+    importedLayers_.Serialize(serialized[KEY_IMPORTED_LAYERS]);
+  }
+
+
+  void UserAnnotationsSettings::ImportLayer(const UserId& author,
+                                            const UserLayer& layer)
+  {
+    if (importedLayers_.HasLayer(layer.GetId()))
+    {
+      LOG(INFO) << "Cannot re-import already imported layer: " << layer.GetId();
+    }
+    else
+    {
+      importedLayers_.AddLayer(new ImportedLayer(author, layer));
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserAnnotationsSettings.h	Wed Sep 02 16:09:00 2026 +0200
@@ -0,0 +1,79 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "LayersCollection.h"
+#include "UserLayer.h"
+#include "ImportedLayer.h"
+
+
+namespace OrthancWSI
+{
+  class UserAnnotationsSettings : public ISerializable
+  {
+  private:
+    LayersCollection  userLayers_;
+    LayersCollection  importedLayers_;
+
+  public:
+    UserAnnotationsSettings()
+    {
+    }
+
+    UserAnnotationsSettings(const Json::Value& serialized);
+
+    std::string AddUserLayer(UserLayer* layer);
+
+    UserLayer& GetUserLayer(const std::string& layerId) const;
+
+    ImportedLayer& GetImportedLayer(const std::string& layerId) const;
+
+    LayersCollection& GetUserLayers()
+    {
+      return userLayers_;
+    }
+
+    const LayersCollection& GetUserLayers() const
+    {
+      return userLayers_;
+    }
+
+    LayersCollection& GetImportedLayers()
+    {
+      return importedLayers_;
+    }
+
+    const LayersCollection& GetImportedLayers() const
+    {
+      return importedLayers_;
+    }
+
+    std::string CreateUserLayer();
+
+    virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE;
+
+    void ImportLayer(const UserId& author,
+                     const UserLayer& layer);
+  };
+}
--- a/ViewerPlugin/CMakeLists.txt	Tue Sep 01 22:14:57 2026 +0200
+++ b/ViewerPlugin/CMakeLists.txt	Wed Sep 02 16:09:00 2026 +0200
@@ -186,12 +186,14 @@
 
 set(ORTHANC_WSI_SOURCES
   Annotations/AnnotationsRestApi.cpp
+  Annotations/AnnotationsWorkspace.cpp
   Annotations/AnnotationsWorkspaceId.cpp
   Annotations/IAuthenticatedUser.cpp
   Annotations/ISerializable.cpp
   Annotations/ImportedLayer.cpp
   Annotations/LayersCollection.cpp
   Annotations/ProjectInformation.cpp
+  Annotations/UserAnnotationsSettings.cpp
   Annotations/UserId.cpp
   Annotations/UserLayer.cpp
   DicomPyramidCache.cpp