changeset 602:f72b20cbd84e

integration annotations->mainline
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 07 Sep 2026 20:58:51 +0200
parents 164565ec35c3 (current diff) a30dd5ee45c6 (diff)
children 5b6af8d1f3a3
files
diffstat 88 files changed, 18406 insertions(+), 986 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/ApplicationToolbox.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Applications/ApplicationToolbox.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -91,7 +91,7 @@
 
 
     static void PrintProgress(BagOfTasksProcessor::Handle* handle,
-                              bool* done)
+                              const bool* done)
     {
       unsigned int previous = 0;
 
--- a/Applications/CMakeLists.txt	Mon Sep 07 15:12:02 2026 +0200
+++ b/Applications/CMakeLists.txt	Mon Sep 07 20:58:51 2026 +0200
@@ -129,7 +129,7 @@
   ${ORTHANC_WSI_DIR}/Framework/ColorSpaces.cpp
   ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp
   ${ORTHANC_WSI_DIR}/Framework/DicomizerParameters.cpp
-  ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp
+  ${ORTHANC_WSI_DIR}/Framework/FrameworkEnumerations.cpp
   ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp
   ${ORTHANC_WSI_DIR}/Framework/ImagedVolumeParameters.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/CytomineImage.cpp
--- a/Framework/Algorithms/TranscodeTileCommand.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Algorithms/TranscodeTileCommand.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -86,7 +86,7 @@
           }
           else
           {
-            printf("ICI\n");
+            // TODO
           }
         }
       }
--- a/Framework/BackgroundColor.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/BackgroundColor.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -25,6 +25,8 @@
 
 #include "ImageToolbox.h"
 
+#include <iostream>
+
 #include <OrthancException.h>
 
 #include <boost/lexical_cast.hpp>
@@ -106,6 +108,21 @@
   }
 
 
+  std::string BackgroundColor::ToHexadecimalString() const
+  {
+    if (present_)
+    {
+      char tmp[32];
+      sprintf(tmp, "#%02x%02x%02x", red_, green_, blue_);
+      return tmp;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);  // Should have called "IsPresent()"
+    }
+  }
+
+
   std::string BackgroundColor::ToHexadecimalString(uint8_t defaultRed,
                                                    uint8_t defaultGreen,
                                                    uint8_t defaultBlue) const
--- a/Framework/BackgroundColor.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/BackgroundColor.h	Mon Sep 07 20:58:51 2026 +0200
@@ -74,6 +74,8 @@
 
     std::string Format() const;
 
+    std::string ToHexadecimalString() const;  // Will throw if absent
+
     std::string ToHexadecimalString(uint8_t defaultRed,
                                     uint8_t defaultGreen,
                                     uint8_t defaultBlue) const;
--- a/Framework/Enumerations.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,224 +0,0 @@
-/**
- * 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 "PrecompiledHeadersWSI.h"
-#include "Enumerations.h"
-
-#include "Jpeg2000Reader.h"
-
-#include <Logging.h>
-#include <OrthancException.h>
-#include <SystemToolbox.h>
-#include <Toolbox.h>
-
-#include <string.h>
-#include <boost/algorithm/string/predicate.hpp>
-
-#define HEADER(s) (const void*) (s), sizeof(s)-1
-
-namespace OrthancWSI
-{
-  const char* EnumerationToString(ImageCompression compression)
-  {
-    switch (compression)
-    {
-      case ImageCompression_Unknown:
-        return "Unknown";
-
-      case ImageCompression_None:
-        return "Raw image";
-
-      case ImageCompression_Png:
-        return "PNG";
-
-      case ImageCompression_Jpeg:
-        return "JPEG";
-
-      case ImageCompression_Jpeg2000:
-        return "JPEG2000";
-
-      case ImageCompression_Tiff:
-        return "TIFF";
-
-      case ImageCompression_Dicom:
-        return "DICOM";
-
-      case ImageCompression_JpegLS:
-        return "JPEG-LS";
-
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  static bool MatchHeader(const void* actual,
-                          size_t actualSize,
-                          const void* expected,
-                          size_t expectedSize)
-  {
-    if (actualSize < expectedSize)
-    {
-      return false;
-    }
-    else
-    {
-      return memcmp(actual, expected, expectedSize) == 0;
-    }
-  }
-
-
-  ImageCompression DetectFormatFromFile(const std::string& path)
-  {
-    std::string lower;
-    Orthanc::Toolbox::ToLowerCase(lower, path);
-
-    std::string header;
-    Orthanc::SystemToolbox::ReadHeader(header, path, 256);
-
-    ImageCompression tmp = DetectFormatFromMemory(header.c_str(), header.size());
-    if (tmp != ImageCompression_Unknown)
-    {
-      if (tmp == ImageCompression_Jpeg &&
-          boost::algorithm::ends_with(lower, ".mrxs"))
-      {
-        /**
-         * Special case of MIRAX / 3DHISTECH images, that contain a JPEG
-         * thumbnail, and that are thus confused with JPEG.
-         * https://bitbucket.org/sjodogne/orthanc/issues/163/
-         **/
-        
-        LOG(WARNING) << "The file extension \".mrxs\" indicates a MIRAX / 3DHISTECH image, "
-                     << "skipping auto-detection of the file format";
-        return ImageCompression_Unknown;
-      }
-      else if (tmp == ImageCompression_Tiff &&
-               boost::algorithm::ends_with(lower, ".ndpi"))
-      {
-        LOG(WARNING) << "The file extension \".ndpi\" indicates a Hamamatsu image, "
-                     << "use the flag \"--force-openslide 1\" if you do not have enough RAM to store the entire image";
-        return ImageCompression_Tiff;
-      }
-      else if (tmp == ImageCompression_Tiff &&
-               boost::algorithm::ends_with(lower, ".scn"))
-      {
-        LOG(WARNING) << "The file extension \".scn\" indicates a Leica image, "
-                     << "use the flag \"--reencode 1\" or \"--force-openslide 1\" if you encounter problems";
-        return ImageCompression_Tiff;
-      }
-      else
-      {
-        return tmp;
-      }
-    }
-
-    // Cannot detect the format using the header, fallback to the use
-    // of the filename extension
-    
-    if (boost::algorithm::ends_with(lower, ".jpeg") ||
-        boost::algorithm::ends_with(lower, ".jpg"))
-    {
-      return ImageCompression_Jpeg;
-    }
-
-    if (boost::algorithm::ends_with(lower, ".png"))
-    {
-      return ImageCompression_Png;
-    }
-
-    if (boost::algorithm::ends_with(lower, ".tiff") ||
-        boost::algorithm::ends_with(lower, ".tif"))
-    {
-      return ImageCompression_Tiff;
-    }
-
-    if (boost::algorithm::ends_with(lower, ".jp2") ||
-        boost::algorithm::ends_with(lower, ".j2k"))
-    {
-      return ImageCompression_Jpeg2000;
-    }
-
-    if (boost::algorithm::ends_with(lower, ".dcm"))
-    {
-      return ImageCompression_Dicom;
-    }
-
-    return ImageCompression_Unknown;
-  }
-
-
-  ImageCompression DetectFormatFromMemory(const void* buffer,
-                                          size_t size) 
-  {
-    if (MatchHeader(buffer, size, HEADER("\377\330\377")))
-    {
-      return ImageCompression_Jpeg;
-    }
-
-    if (MatchHeader(buffer, size, HEADER("\xff\x4f\xff\x51")) ||
-        MatchHeader(buffer, size, HEADER("\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a")))
-    {
-      return ImageCompression_Jpeg2000;
-    }
-
-    if (MatchHeader(buffer, size, HEADER("\211PNG\r\n\032\n")))
-    {
-      return ImageCompression_Png;
-    }
-
-    if (MatchHeader(buffer, size, HEADER("\115\115\000\052")) ||
-        MatchHeader(buffer, size, HEADER("\111\111\052\000")) ||
-        MatchHeader(buffer, size, HEADER("\115\115\000\053\000\010\000\000")) ||
-        MatchHeader(buffer, size, HEADER("\111\111\053\000\010\000\000\000")))
-    {
-      return ImageCompression_Tiff;
-    }
-
-    if (size >= 128 + 4 &&
-        MatchHeader(reinterpret_cast<const uint8_t*>(buffer) + 128, size - 128, HEADER("DICM")))
-    {
-      bool ok = true;
-      for (size_t i = 0; ok && i < 128; i++)
-      {
-        if (reinterpret_cast<const uint8_t*>(buffer)[i] != 0)
-        {
-          ok = false;
-        }
-      }
-
-      if (ok)
-      {
-        return ImageCompression_Dicom;
-      }
-    }        
-
-    Jpeg2000Format jpeg2000 = Jpeg2000Reader::DetectFormatFromMemory(buffer, size);
-    if (jpeg2000 == Jpeg2000Format_JP2 ||
-        jpeg2000 == Jpeg2000Format_J2K)
-    {
-      return ImageCompression_Jpeg2000;
-    }
-    
-    return ImageCompression_Unknown;
-  }
-}
--- a/Framework/Enumerations.h	Mon Sep 07 15:12:02 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * 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 <Enumerations.h>
-
-#include <stdint.h>
-#include <string>
-
-namespace OrthancWSI
-{
-  static const char* const VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD = "1.2.840.10008.5.1.4.1.1.77.1.6";
-
-  // WARNING - Don't change the enum values below, as this would break
-  // serialization of "DicomPyramidInstance"
-  enum ImageCompression
-  {
-    ImageCompression_Unknown = 1,
-    ImageCompression_None = 2,
-    ImageCompression_Dicom = 3,
-    ImageCompression_Png = 4,
-    ImageCompression_Jpeg = 5,
-    ImageCompression_Jpeg2000 = 6,
-    ImageCompression_Tiff = 7,
-    ImageCompression_UseOrthancPreview = 8,
-    ImageCompression_JpegLS = 9
-  };
-
-  enum OpticalPath
-  {
-    OpticalPath_None,
-    OpticalPath_Brightfield
-  };
-
-  const char* EnumerationToString(ImageCompression compression);
-
-  ImageCompression DetectFormatFromFile(const std::string& path);
-
-  ImageCompression DetectFormatFromMemory(const void* buffer,
-                                          size_t size);
-
-  inline unsigned int CeilingDivision(unsigned int a,
-                                      unsigned int b)
-  {
-    if (a % b == 0)
-    {
-      return a / b;
-    }
-    else
-    {
-      return a / b + 1;
-    }
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/FrameworkEnumerations.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,224 @@
+/**
+ * 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 "PrecompiledHeadersWSI.h"
+#include "FrameworkEnumerations.h"
+
+#include "Jpeg2000Reader.h"
+
+#include <Logging.h>
+#include <OrthancException.h>
+#include <SystemToolbox.h>
+#include <Toolbox.h>
+
+#include <string.h>
+#include <boost/algorithm/string/predicate.hpp>
+
+#define HEADER(s) reinterpret_cast<const void*>(s), sizeof(s) - 1
+
+namespace OrthancWSI
+{
+  const char* EnumerationToString(ImageCompression compression)
+  {
+    switch (compression)
+    {
+      case ImageCompression_Unknown:
+        return "Unknown";
+
+      case ImageCompression_None:
+        return "Raw image";
+
+      case ImageCompression_Png:
+        return "PNG";
+
+      case ImageCompression_Jpeg:
+        return "JPEG";
+
+      case ImageCompression_Jpeg2000:
+        return "JPEG2000";
+
+      case ImageCompression_Tiff:
+        return "TIFF";
+
+      case ImageCompression_Dicom:
+        return "DICOM";
+
+      case ImageCompression_JpegLS:
+        return "JPEG-LS";
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static bool MatchHeader(const void* actual,
+                          size_t actualSize,
+                          const void* expected,
+                          size_t expectedSize)
+  {
+    if (actualSize < expectedSize)
+    {
+      return false;
+    }
+    else
+    {
+      return memcmp(actual, expected, expectedSize) == 0;
+    }
+  }
+
+
+  ImageCompression DetectFormatFromFile(const std::string& path)
+  {
+    std::string lower;
+    Orthanc::Toolbox::ToLowerCase(lower, path);
+
+    std::string header;
+    Orthanc::SystemToolbox::ReadHeader(header, path, 256);
+
+    ImageCompression tmp = DetectFormatFromMemory(header.c_str(), header.size());
+    if (tmp != ImageCompression_Unknown)
+    {
+      if (tmp == ImageCompression_Jpeg &&
+          boost::algorithm::ends_with(lower, ".mrxs"))
+      {
+        /**
+         * Special case of MIRAX / 3DHISTECH images, that contain a JPEG
+         * thumbnail, and that are thus confused with JPEG.
+         * https://bitbucket.org/sjodogne/orthanc/issues/163/
+         **/
+        
+        LOG(WARNING) << "The file extension \".mrxs\" indicates a MIRAX / 3DHISTECH image, "
+                     << "skipping auto-detection of the file format";
+        return ImageCompression_Unknown;
+      }
+      else if (tmp == ImageCompression_Tiff &&
+               boost::algorithm::ends_with(lower, ".ndpi"))
+      {
+        LOG(WARNING) << "The file extension \".ndpi\" indicates a Hamamatsu image, "
+                     << "use the flag \"--force-openslide 1\" if you do not have enough RAM to store the entire image";
+        return ImageCompression_Tiff;
+      }
+      else if (tmp == ImageCompression_Tiff &&
+               boost::algorithm::ends_with(lower, ".scn"))
+      {
+        LOG(WARNING) << "The file extension \".scn\" indicates a Leica image, "
+                     << "use the flag \"--reencode 1\" or \"--force-openslide 1\" if you encounter problems";
+        return ImageCompression_Tiff;
+      }
+      else
+      {
+        return tmp;
+      }
+    }
+
+    // Cannot detect the format using the header, fallback to the use
+    // of the filename extension
+    
+    if (boost::algorithm::ends_with(lower, ".jpeg") ||
+        boost::algorithm::ends_with(lower, ".jpg"))
+    {
+      return ImageCompression_Jpeg;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".png"))
+    {
+      return ImageCompression_Png;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".tiff") ||
+        boost::algorithm::ends_with(lower, ".tif"))
+    {
+      return ImageCompression_Tiff;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".jp2") ||
+        boost::algorithm::ends_with(lower, ".j2k"))
+    {
+      return ImageCompression_Jpeg2000;
+    }
+
+    if (boost::algorithm::ends_with(lower, ".dcm"))
+    {
+      return ImageCompression_Dicom;
+    }
+
+    return ImageCompression_Unknown;
+  }
+
+
+  ImageCompression DetectFormatFromMemory(const void* buffer,
+                                          size_t size) 
+  {
+    if (MatchHeader(buffer, size, HEADER("\377\330\377")))
+    {
+      return ImageCompression_Jpeg;
+    }
+
+    if (MatchHeader(buffer, size, HEADER("\xff\x4f\xff\x51")) ||
+        MatchHeader(buffer, size, HEADER("\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a")))
+    {
+      return ImageCompression_Jpeg2000;
+    }
+
+    if (MatchHeader(buffer, size, HEADER("\211PNG\r\n\032\n")))
+    {
+      return ImageCompression_Png;
+    }
+
+    if (MatchHeader(buffer, size, HEADER("\115\115\000\052")) ||
+        MatchHeader(buffer, size, HEADER("\111\111\052\000")) ||
+        MatchHeader(buffer, size, HEADER("\115\115\000\053\000\010\000\000")) ||
+        MatchHeader(buffer, size, HEADER("\111\111\053\000\010\000\000\000")))
+    {
+      return ImageCompression_Tiff;
+    }
+
+    if (size >= 128 + 4 &&
+        MatchHeader(reinterpret_cast<const uint8_t*>(buffer) + 128, size - 128, HEADER("DICM")))
+    {
+      bool ok = true;
+      for (size_t i = 0; ok && i < 128; i++)
+      {
+        if (reinterpret_cast<const uint8_t*>(buffer)[i] != 0)
+        {
+          ok = false;
+        }
+      }
+
+      if (ok)
+      {
+        return ImageCompression_Dicom;
+      }
+    }        
+
+    Jpeg2000Format jpeg2000 = Jpeg2000Reader::DetectFormatFromMemory(buffer, size);
+    if (jpeg2000 == Jpeg2000Format_JP2 ||
+        jpeg2000 == Jpeg2000Format_J2K)
+    {
+      return ImageCompression_Jpeg2000;
+    }
+    
+    return ImageCompression_Unknown;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/FrameworkEnumerations.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,95 @@
+/**
+ * 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 <Enumerations.h>
+
+#include <stdint.h>
+#include <string>
+
+namespace OrthancWSI
+{
+  static const char* const VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE_IOD = "1.2.840.10008.5.1.4.1.1.77.1.6";
+
+  // WARNING - Don't change the enum values below, as this would break
+  // serialization of "DicomPyramidInstance"
+  enum ImageCompression
+  {
+    ImageCompression_Unknown = 1,
+    ImageCompression_None = 2,
+    ImageCompression_Dicom = 3,
+    ImageCompression_Png = 4,
+    ImageCompression_Jpeg = 5,
+    ImageCompression_Jpeg2000 = 6,
+    ImageCompression_Tiff = 7,
+    ImageCompression_UseOrthancPreview = 8,
+    ImageCompression_JpegLS = 9
+  };
+
+  enum OpticalPath
+  {
+    OpticalPath_None,
+    OpticalPath_Brightfield
+  };
+
+
+  // Used by the viewer
+  enum AuthenticationSource
+  {
+    AuthenticationSource_None,
+    AuthenticationSource_RegisteredUsers,
+    AuthenticationSource_Plugin,
+    AuthenticationSource_HttpHeader
+  };
+
+
+  // Used by the viewer
+  enum ProjectRole
+  {
+    ProjectRole_Instructor,
+    ProjectRole_Learner,
+    ProjectRole_Guest
+  };
+
+
+  const char* EnumerationToString(ImageCompression compression);
+
+  ImageCompression DetectFormatFromFile(const std::string& path);
+
+  ImageCompression DetectFormatFromMemory(const void* buffer,
+                                          size_t size);
+
+  inline unsigned int CeilingDivision(unsigned int a,
+                                      unsigned int b)
+  {
+    if (a % b == 0)
+    {
+      return a / b;
+    }
+    else
+    {
+      return a / b + 1;
+    }
+  }
+}
--- a/Framework/Inputs/DecodedPyramidCache.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/DecodedPyramidCache.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -161,7 +161,15 @@
     while (!cache_.IsEmpty())
     {
       CachedPyramid* pyramid = NULL;
-      cache_.RemoveOldest(pyramid);
+
+      try
+      {
+        cache_.RemoveOldest(pyramid);
+      }
+      catch (Orthanc::OrthancException&)
+      {
+        // Should never happen, don't throw exceptions in destructor
+      }
 
       if (pyramid != NULL)
       {
--- a/Framework/Inputs/DicomPyramid.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/DicomPyramid.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -39,8 +39,8 @@
 {
   struct DicomPyramid::Comparator
   {
-    bool operator() (DicomPyramidInstance* const& a,
-                     DicomPyramidInstance* const& b) const
+    bool operator() (const DicomPyramidInstance* const& a,
+                     const DicomPyramidInstance* const& b) const
     {
       return a->GetTotalWidth() > b->GetTotalWidth();
     }
--- a/Framework/Inputs/DicomPyramid.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/DicomPyramid.h	Mon Sep 07 20:58:51 2026 +0200
@@ -54,7 +54,7 @@
                  const std::string& seriesId,
                  bool useCache);
 
-    virtual ~DicomPyramid()
+    virtual ~DicomPyramid() ORTHANC_OVERRIDE
     {
       Clear();
     }
--- a/Framework/Inputs/DicomPyramidInstance.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/DicomPyramidInstance.h	Mon Sep 07 20:58:51 2026 +0200
@@ -25,7 +25,7 @@
 
 #include "../../Resources/Orthanc/Stone/IOrthancConnection.h"
 #include "../BackgroundColor.h"
-#include "../Enumerations.h"
+#include "../FrameworkEnumerations.h"
 
 #include <boost/noncopyable.hpp>
 #include <vector>
--- a/Framework/Inputs/ITiledPyramid.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/ITiledPyramid.h	Mon Sep 07 20:58:51 2026 +0200
@@ -24,7 +24,7 @@
 #pragma once
 
 #include "../BackgroundColor.h"
-#include "../Enumerations.h"
+#include "../FrameworkEnumerations.h"
 
 #include <Compatibility.h>
 #include <Images/ImageAccessor.h>
--- a/Framework/Inputs/OnTheFlyPyramid.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/OnTheFlyPyramid.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -84,7 +84,7 @@
       Orthanc::ImageProcessing::Convert(*baseLevel_, *protection);
     }
 
-    Orthanc::ImageAccessor* current = baseLevel_.get();
+    const Orthanc::ImageAccessor* current = baseLevel_.get();
     while (current->GetWidth() > tileWidth_ ||
            current->GetHeight() > tileHeight_)
     {
--- a/Framework/Inputs/OnTheFlyPyramid.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/OnTheFlyPyramid.h	Mon Sep 07 20:58:51 2026 +0200
@@ -53,7 +53,7 @@
                     unsigned int tileHeight,
                     bool smooth);
 
-    virtual ~OnTheFlyPyramid();
+    virtual ~OnTheFlyPyramid() ORTHANC_OVERRIDE;
 
     const Orthanc::ImageAccessor& GetLevel(unsigned int level) const;
 
--- a/Framework/Inputs/OpenSlideLibrary.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/OpenSlideLibrary.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -39,14 +39,14 @@
   OpenSlideLibrary::OpenSlideLibrary(const std::string& path) :
     library_(path)
   {
-    close_ = (FunctionClose) library_.GetFunction("openslide_close");
-    getLevelCount_ = (FunctionGetLevelCount) library_.GetFunction("openslide_get_level_count");
-    getLevelDimensions_ = (FunctionGetLevelDimensions) library_.GetFunction("openslide_get_level_dimensions");
-    getLevelDownsample_ = (FunctionGetLevelDownsample) library_.GetFunction("openslide_get_level_downsample");
-    open_ = (FunctionOpen) library_.GetFunction("openslide_open");
-    readRegion_ = (FunctionReadRegion) library_.GetFunction("openslide_read_region");
-    getPropertyNames_ = (FunctionGetPropertyNames) library_.GetFunction("openslide_get_property_names");
-    getPropertyValue_ = (FunctionGetPropertyValue) library_.GetFunction("openslide_get_property_value");
+    close_ = reinterpret_cast<FunctionClose>(library_.GetFunction("openslide_close"));
+    getLevelCount_ = reinterpret_cast<FunctionGetLevelCount>(library_.GetFunction("openslide_get_level_count"));
+    getLevelDimensions_ = reinterpret_cast<FunctionGetLevelDimensions>(library_.GetFunction("openslide_get_level_dimensions"));
+    getLevelDownsample_ = reinterpret_cast<FunctionGetLevelDownsample>(library_.GetFunction("openslide_get_level_downsample"));
+    open_ = reinterpret_cast<FunctionOpen>(library_.GetFunction("openslide_open"));
+    readRegion_ = reinterpret_cast<FunctionReadRegion>(library_.GetFunction("openslide_read_region"));
+    getPropertyNames_ = reinterpret_cast<FunctionGetPropertyNames>(library_.GetFunction("openslide_get_property_names"));
+    getPropertyValue_ = reinterpret_cast<FunctionGetPropertyValue>(library_.GetFunction("openslide_get_property_value"));
   }
 
 
--- a/Framework/Inputs/TiledPyramidStatistics.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Inputs/TiledPyramidStatistics.h	Mon Sep 07 20:58:51 2026 +0200
@@ -40,7 +40,7 @@
   public:
     explicit TiledPyramidStatistics(ITiledPyramid& source);   // Takes ownership
 
-    virtual ~TiledPyramidStatistics();
+    virtual ~TiledPyramidStatistics() ORTHANC_OVERRIDE;
 
     virtual unsigned int GetLevelCount() const ORTHANC_OVERRIDE
     {
--- a/Framework/Jpeg2000Reader.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Jpeg2000Reader.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -243,7 +243,7 @@
 #endif
 
     public:
-      OpenJpegInput(OpenJpegDecoder& decoder,
+      OpenJpegInput(const OpenJpegDecoder& decoder,
                     const void* buffer,
                     size_t size) :
         buffer_(reinterpret_cast<const uint8_t*>(buffer)),
--- a/Framework/Jpeg2000Writer.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Jpeg2000Writer.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -248,7 +248,7 @@
 #endif
 
     public:
-      explicit OpenJpegOutput(OpenJpegEncoder& encoder) :
+      explicit OpenJpegOutput(const OpenJpegEncoder& encoder) :
         cio_(NULL)
       {
 #if ORTHANC_OPENJPEG_MAJOR_VERSION == 1
--- a/Framework/Outputs/DicomPyramidWriter.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Outputs/DicomPyramidWriter.h	Mon Sep 07 20:58:51 2026 +0200
@@ -79,7 +79,7 @@
                        const ImagedVolumeParameters&  volume,
                        Orthanc::PhotometricInterpretation photometric);
 
-    virtual ~DicomPyramidWriter();
+    virtual ~DicomPyramidWriter() ORTHANC_OVERRIDE;
 
     virtual void Flush() ORTHANC_OVERRIDE;
   };
--- a/Framework/Outputs/HierarchicalTiffWriter.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Outputs/HierarchicalTiffWriter.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -81,8 +81,8 @@
 
   struct HierarchicalTiffWriter::Comparator
   {
-    inline bool operator() (PendingTile* const& a,
-                            PendingTile* const& b)
+    inline bool operator() (const PendingTile* const& a,
+                            const PendingTile* const& b)
     {
       if (a->GetLevel() < b->GetLevel())
       {
--- a/Framework/Outputs/HierarchicalTiffWriter.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Outputs/HierarchicalTiffWriter.h	Mon Sep 07 20:58:51 2026 +0200
@@ -86,7 +86,7 @@
                            unsigned int tileHeight,
                            Orthanc::PhotometricInterpretation photometric);
 
-    virtual ~HierarchicalTiffWriter();
+    virtual ~HierarchicalTiffWriter() ORTHANC_OVERRIDE;
 
     virtual void Flush() ORTHANC_OVERRIDE;
   };
--- a/Framework/Outputs/IPyramidWriter.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Outputs/IPyramidWriter.h	Mon Sep 07 20:58:51 2026 +0200
@@ -23,7 +23,8 @@
 
 #pragma once
 
-#include "../Enumerations.h"
+#include "../FrameworkEnumerations.h"
+
 #include <Images/ImageAccessor.h>
 
 #include <boost/noncopyable.hpp>
--- a/Framework/Outputs/InMemoryTiledImage.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Outputs/InMemoryTiledImage.h	Mon Sep 07 20:58:51 2026 +0200
@@ -58,7 +58,7 @@
                        Orthanc::PhotometricInterpretation photometric,
                        BackgroundColor backgroundColor);
 
-    virtual ~InMemoryTiledImage();
+    virtual ~InMemoryTiledImage() ORTHANC_OVERRIDE;
 
     virtual unsigned int GetLevelCount() const ORTHANC_OVERRIDE
     {
--- a/Framework/Outputs/MultiframeDicomWriter.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/Outputs/MultiframeDicomWriter.h	Mon Sep 07 20:58:51 2026 +0200
@@ -23,7 +23,7 @@
 
 #pragma once
 
-#include "../Enumerations.h"
+#include "../FrameworkEnumerations.h"
 
 #include <Compatibility.h>  // For std::unique_ptr
 #include <ChunkedBuffer.h>
--- a/Framework/TiffReader.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Framework/TiffReader.h	Mon Sep 07 20:58:51 2026 +0200
@@ -23,7 +23,7 @@
 
 #pragma once
 
-#include "Enumerations.h"
+#include "FrameworkEnumerations.h"
 
 #include <tiff.h>
 #include <tiffio.h>
--- a/NEWS	Mon Sep 07 15:12:02 2026 +0200
+++ b/NEWS	Mon Sep 07 20:58:51 2026 +0200
@@ -1,6 +1,29 @@
 Pending changes in the mainline
 ===============================
 
+=> Recommended SDK version: 1.12.9 <=
+=> Minimum SDK version: 1.7.0 <=
+
+* Support for annotations
+* Persistence of annotations in the Orthanc database (SDK 1.12.8 is required)
+
+Configuration
+-------------
+
+* New configuration option "EnableAnnotations" to enable annotations
+* New configuration option "AuthenticationSource" to enable per-user annotations:
+  - "None" uses the administrative Orthanc user (default)
+  - "RegisteredUsers" relies on the HTTP basic authentication that is built in Orthanc
+  - "HttpHeader" takes the user out of the HTTP header specified in "AuthenticationHttpHeader" option
+  - "Plugin" can be used if the "orthanc-education" plugin is installed
+* New configuration option "Instructors" to distinguish between learners
+  and instructors, only if the authentication source is "HttpHeader"
+* New configuration option "EnableAnnotationsSharing" to enable sharing of annotations
+* New configuration option "EnableLearnerToLearnerSharing" to enable sharing between learners
+
+Maintenance
+-----------
+
 * OrthancWSIDicomizer:
   - Fill the DICOM tag "Objective Lens Power Attribute" (0048,0112)
     with the scanner magnification, either when using OpenSlide or
--- a/Resources/CMake/JavaScriptLibraries.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/CMake/JavaScriptLibraries.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -31,6 +31,23 @@
   "${BASE_URL}/bootstrap-5.3.3.zip"
   "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3")
 
+DownloadPackage(
+  "6e819ef7fcd49bb13cf809fd8c5fb20b"
+  "${BASE_URL}/bootstrap-icons-1.13.1.zip"
+  "${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1")
+
+DownloadPackage(
+  "8242afdc5bd44105d9dc9e6535315484"
+  "${BASE_URL}/dicom-web/vuejs-2.6.10.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10")
+
+# axios v0.31.0 is the last release before Axios removed the committed dist/ artifacts
+# https://github.com/axios/axios/releases?page=2#release-v0.31.0
+DownloadPackage(
+  "e1fe2cd9270b513874aea946977f2d47"
+  "${BASE_URL}/axios-0.31.0.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/axios-0.31.0")
+
 # curl -L https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js | gzip > /tmp/popper-2.11.8.min.js.gz
 
 DownloadCompressedFile(
@@ -38,22 +55,65 @@
   "${BASE_URL}/WSI/popper-2.11.8.min.js.gz"
   "${CMAKE_CURRENT_BINARY_DIR}/popper.min.js")
 
+DownloadFile(
+  "aa2c54fdbbbeb1bb056793ec480928eb"
+  "${BASE_URL}/WSI/modern-screenshot-4.7.0.js")
+
 
 set(JAVASCRIPT_LIBS_DIR  ${CMAKE_CURRENT_BINARY_DIR}/javascript-libs)
 file(MAKE_DIRECTORY ${JAVASCRIPT_LIBS_DIR})
 
 
 file(COPY
+  ${CMAKE_CURRENT_BINARY_DIR}/axios-0.31.0/dist/axios.min.js
+  ${CMAKE_CURRENT_BINARY_DIR}/axios-0.31.0/dist/axios.min.js.map
   ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3/dist/js/bootstrap.min.js
   ${CMAKE_CURRENT_BINARY_DIR}/openlayers-10.6.1-package/dist/ol.js
   ${CMAKE_CURRENT_BINARY_DIR}/popper.min.js
+  ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js
+  ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/modern-screenshot-4.7.0.js
   DESTINATION
   ${JAVASCRIPT_LIBS_DIR}/js
   )
 
+file(RENAME
+  ${JAVASCRIPT_LIBS_DIR}/js/modern-screenshot-4.7.0.js
+  ${JAVASCRIPT_LIBS_DIR}/js/modern-screenshot.js
+  )
+
 file(COPY
   ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3/dist/css/bootstrap.min.css
   ${CMAKE_CURRENT_BINARY_DIR}/openlayers-10.6.1-package/ol.css
   DESTINATION
   ${JAVASCRIPT_LIBS_DIR}/css
   )
+
+file(COPY
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrow-clockwise.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrow-up-right.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrows-fullscreen.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrows-move.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/arrows.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/brightness-high.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/camera.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/chevron-compact-left.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/chevron-compact-right.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/circle.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/cloud-download.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/eye-slash.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/eye.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/file-earmark-plus.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/geo-alt.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/hand-index.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/pen.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/pentagon.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/share.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/square.svg
+  ${CMAKE_CURRENT_BINARY_DIR}/icons-1.13.1/icons/trash.svg
+
+  ${CMAKE_CURRENT_LIST_DIR}/../Icons/freehand-area-svgrepo-com.svg
+  ${CMAKE_CURRENT_LIST_DIR}/../Icons/freehand-svgrepo-com.svg
+
+  DESTINATION
+  ${JAVASCRIPT_LIBS_DIR}/svg
+  )
--- a/Resources/CMake/Version.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/CMake/Version.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -21,16 +21,19 @@
 
 set(ORTHANC_WSI_VERSION "mainline")
 
+set(ORTHANC_WSI_ANNOTATIONS_VERSION "1")  # Must be an integer
+
 if (ORTHANC_WSI_VERSION STREQUAL "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
 else()
-  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.9")
+  set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.13.0")
   set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
 endif()
 
 add_definitions(
   -DORTHANC_WSI_VERSION="${ORTHANC_WSI_VERSION}"
+  -DORTHANC_WSI_ANNOTATIONS_VERSION=${ORTHANC_WSI_ANNOTATIONS_VERSION}
   )
 
 set(ORTHANC_FRAMEWORK_VERSION "${ORTHANC_FRAMEWORK_DEFAULT_VERSION}" CACHE STRING "Version of the Orthanc framework")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Icons/README.txt	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,2 @@
+These free SVG icons come from:
+https://www.svgrepo.com/vectors/freehand/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Icons/freehand-area-svgrepo-com.svg	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M21.8 15.66c0 3.479-1.17 6.14-4.553 6.14-3.743 0-5.414-2.337-8.7-2.337-.844 0-1.535.154-2.22.154A4.14 4.14 0 0 1 2 15.504c0-2.329 2.956-2.756 3.19-7.521C5.373 4.254 8.23 2.2 11.565 2.2 17.521 2.2 21.8 8.757 21.8 15.66zm-1 0c0-6.017-3.711-12.46-9.235-12.46-2.37 0-5.2 1.265-5.376 4.832a8.817 8.817 0 0 1-2.272 5.716C3.314 14.493 3 14.91 3 15.504c0 3.077 3.293 3.113 3.326 3.113a8.45 8.45 0 0 0 .89-.066 12.225 12.225 0 0 1 1.33-.088 10.29 10.29 0 0 1 4.465 1.2 9.621 9.621 0 0 0 4.235 1.137c.879 0 3.554 0 3.554-5.14z"/><path opacity=".25" d="M17.246 20.8a9.621 9.621 0 0 1-4.235-1.137 10.29 10.29 0 0 0-4.464-1.2 12.225 12.225 0 0 0-1.331.088 8.45 8.45 0 0 1-.89.066C6.293 18.617 3 18.58 3 15.504c0-.594.314-1.01.917-1.756a8.817 8.817 0 0 0 2.272-5.716C6.364 4.465 9.194 3.2 11.565 3.2c5.524 0 9.235 6.443 9.235 12.46 0 5.14-2.675 5.14-3.554 5.14z"/><path fill="none" d="M0 0h24v24H0z"/></svg>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Icons/freehand-svgrepo-com.svg	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23 14.25A3.88 3.88 0 0 0 19.25 10C16.314 10 15 12.763 15 15.5a6.493 6.493 0 0 0 .95 3.516 7.005 7.005 0 0 1-4.905-1.566A3.255 3.255 0 0 1 10 15a9.084 9.084 0 0 1 1.555-3.894A8.31 8.31 0 0 0 13 7.5 2.276 2.276 0 0 0 10.5 5c-.919 0-1.795 1.072-2.81 2.314C6.714 8.511 5.498 10 4.5 10 3.684 10 2 9.51 2 8c0-1.848 2.703-4.028 4.002-5.076l.266-.215-.632-.775-.262.212C3.845 3.379 1 5.675 1 8c0 2.07 2.047 3 3.5 3 1.473 0 2.797-1.622 3.965-3.053C9.174 7.08 10.055 6 10.5 6c1.038 0 1.5.463 1.5 1.5a7.868 7.868 0 0 1-1.313 3.11A9.681 9.681 0 0 0 9 15a4.275 4.275 0 0 0 1.357 3.176A8.438 8.438 0 0 0 16.5 20c.072 0 .144-.001.215-.003a11.08 11.08 0 0 0 6.326 2.871l.167-.986a11.16 11.16 0 0 1-5.178-2.024A5.937 5.937 0 0 0 23 14.25zm-7 1.25c0-2.24 1.005-4.5 3.25-4.5.951 0 2.75.68 2.75 3.25a5.033 5.033 0 0 1-4.857 4.722A5.396 5.396 0 0 1 16 15.5z"/><path fill="none" d="M0 0h24v24H0z"/></svg>
\ No newline at end of file
--- a/Resources/Orthanc/CMake/AutoGeneratedCode.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/CMake/AutoGeneratedCode.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -76,5 +76,5 @@
 
   list(APPEND AUTOGENERATED_SOURCES
     "${TARGET_BASE}.cpp"
-    ) 
+    )
 endmacro()
--- a/Resources/Orthanc/CMake/Compiler.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/CMake/Compiler.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -43,7 +43,7 @@
 include(CheckLibraryExists)
 
 if ((CMAKE_CROSSCOMPILING AND NOT
-      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR    
+      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR
     "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
   # Cross-compilation necessarily implies standalone and static build
   SET(STATIC_BUILD ON)
@@ -70,7 +70,7 @@
   set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long")
 
   # --std=c99 makes libcurl not to compile
-  # -pedantic gives a lot of warnings on OpenSSL 
+  # -pedantic gives a lot of warnings on OpenSSL
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -Wno-variadic-macros")
 
   if (CMAKE_CROSSCOMPILING)
@@ -85,12 +85,12 @@
   foreach(flag_var
     CMAKE_C_FLAGS_DEBUG
     CMAKE_CXX_FLAGS_DEBUG
-    CMAKE_C_FLAGS_RELEASE 
+    CMAKE_C_FLAGS_RELEASE
     CMAKE_CXX_FLAGS_RELEASE
-    CMAKE_C_FLAGS_MINSIZEREL 
-    CMAKE_CXX_FLAGS_MINSIZEREL 
-    CMAKE_C_FLAGS_RELWITHDEBINFO 
-    CMAKE_CXX_FLAGS_RELWITHDEBINFO) 
+    CMAKE_C_FLAGS_MINSIZEREL
+    CMAKE_CXX_FLAGS_MINSIZEREL
+    CMAKE_C_FLAGS_RELWITHDEBINFO
+    CMAKE_CXX_FLAGS_RELWITHDEBINFO)
     string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
     string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}")
   endforeach(flag_var)
@@ -109,7 +109,7 @@
     set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
   endif()
-    
+
   add_definitions(
     -D_CRT_SECURE_NO_WARNINGS=1
     -D_CRT_SECURE_NO_DEPRECATE=1
@@ -188,7 +188,7 @@
     # for LFS (Large File Support).
     # https://ohse.de/uwe/articles/lfs.html
     add_definitions(
-      -D_LARGEFILE64_SOURCE=1 
+      -D_LARGEFILE64_SOURCE=1
       -D_FILE_OFFSET_BITS=64
       )
   endif()
@@ -254,14 +254,14 @@
   add_definitions(
     -D_XOPEN_SOURCE=1
     )
-  
+
   # Linking with iconv breaks the Universal builds on modern compilers
   # link_libraries(iconv)
 
 elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
   message("Building using Emscripten (for WebAssembly or asm.js targets)")
   include(${CMAKE_CURRENT_LIST_DIR}/EmscriptenParameters.cmake)
-  
+
 elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
 
 else()
--- a/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -177,6 +177,10 @@
         set(ORTHANC_FRAMEWORK_MD5 "66b5a2ee60706c4a502896083b9e1a01")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.10")
         set(ORTHANC_FRAMEWORK_MD5 "d5e1ba442104c89a24013cb859a9d6bf")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.11")
+        set(ORTHANC_FRAMEWORK_MD5 "389b273b64b513ba8fc3233f34201cc1")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.13.0")
+        set(ORTHANC_FRAMEWORK_MD5 "cc95d3e509612b541e10e91c831fbfe3")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -231,6 +235,16 @@
         # for BlockingSharedMessageQueue.WaitEmpty()
         set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
         set(ORTHANC_FRAMEWORK_MD5 "c037cd2ddbe1b65b431692855483161b")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "56eb61c86f93")
+        # OE2 1.11.0 (framework post-1.12.10)
+        # for HttpClient that returns the answer body in case of HTTP error
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
+        set(ORTHANC_FRAMEWORK_MD5 "665f8aa70d7c5091bc20da37cf664910")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "004b351797fe")
+        # PixelsMasker 0.1.2 (framework post-1.12.11)
+        # for ScopedThreadNameSetter
+        set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
+        set(ORTHANC_FRAMEWORK_MD5 "f078ca997217b831ab3f6741f08a8c07")
       endif()
     endif()
   endif()
@@ -250,7 +264,7 @@
 
 if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
   find_program(ORTHANC_FRAMEWORK_HG hg)
-  
+
   if (${ORTHANC_FRAMEWORK_HG} MATCHES "ORTHANC_FRAMEWORK_HG-NOTFOUND")
     message(FATAL_ERROR "Please install Mercurial")
   endif()
@@ -260,8 +274,8 @@
 if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
     ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
   if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
-    find_program(ORTHANC_FRAMEWORK_7ZIP 7z 
-      PATHS 
+    find_program(ORTHANC_FRAMEWORK_7ZIP 7z
+      PATHS
       "$ENV{ProgramFiles}/7-Zip"
       "$ENV{ProgramW6432}/7-Zip"
       )
@@ -289,7 +303,7 @@
       ORTHANC_FRAMEWORK_ROOT STREQUAL "")
     message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ROOT must provide the path to the sources of Orthanc")
   endif()
-  
+
   if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT})
     message(FATAL_ERROR "Non-existing directory: ${ORTHANC_FRAMEWORK_ROOT}")
   endif()
@@ -314,14 +328,14 @@
       COMMAND ${ORTHANC_FRAMEWORK_HG} pull
       WORKING_DIRECTORY ${ORTHANC_ROOT}
       RESULT_VARIABLE Failure
-      )    
+      )
   else()
     message("Forking the Orthanc source repository using Mercurial")
     execute_process(
       COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://orthanc.uclouvain.be/hg/orthanc/"
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
-      )    
+      )
   endif()
 
   if (Failure OR NOT EXISTS ${ORTHANC_ROOT})
@@ -384,14 +398,14 @@
     message("Downloading: ${ORTHANC_FRAMEWORK_URL}")
 
     file(DOWNLOAD
-      "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}" 
+      "${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}"
       SHOW_PROGRESS EXPECTED_MD5 "${ORTHANC_FRAMEWORK_MD5}"
       TIMEOUT 60
       INACTIVITY_TIMEOUT 60
       )
   else()
     message("Using local copy of: ${ORTHANC_FRAMEWORK_URL}")
-  endif()  
+  endif()
 endif()
 
 
@@ -427,7 +441,7 @@
     if (NOT ORTHANC_FRAMEWORK_ARCHIVE MATCHES ".tar.gz$")
       message(FATAL_ERROR "Archive should have the \".tar.gz\" extension: ${ORTHANC_FRAMEWORK_ARCHIVE}")
     endif()
-    
+
     message("Uncompressing: ${ORTHANC_FRAMEWORK_ARCHIVE}")
 
     if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
@@ -440,7 +454,7 @@
         RESULT_VARIABLE Failure
         OUTPUT_QUIET
         )
-      
+
       if (Failure)
         message(FATAL_ERROR "Error while running the uncompression tool")
       endif()
@@ -462,7 +476,7 @@
         RESULT_VARIABLE Failure
         )
     endif()
-   
+
     if (Failure)
       message(FATAL_ERROR "Error while running the uncompression tool")
     endif()
@@ -528,10 +542,10 @@
     set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libstdc++")
     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++")
   endif()
-  
+
   include(CheckIncludeFile)
   include(CheckIncludeFileCXX)
-  
+
   if(CMAKE_VERSION VERSION_GREATER "3.11")
     find_package(Python REQUIRED COMPONENTS Interpreter)
     set(PYTHON_EXECUTABLE ${Python_EXECUTABLE})
@@ -539,7 +553,7 @@
     include(FindPythonInterp)
     find_package(PythonInterp REQUIRED)
   endif()
-  
+
   include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
   include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
@@ -600,19 +614,19 @@
   if (${ORTHANC_FRAMEWORK_INCLUDE_DIR} STREQUAL "ORTHANC_FRAMEWORK_INCLUDE_DIR-NOTFOUND")
     message(FATAL_ERROR "Cannot locate the OrthancFramework.h header")
   endif()
-  
+
   message("Orthanc framework include dir: ${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
   include_directories(${ORTHANC_FRAMEWORK_INCLUDE_DIR})
 
   if (ORTHANC_FRAMEWORK_USE_SHARED)
     set(CMAKE_REQUIRED_INCLUDES "${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
     set(CMAKE_REQUIRED_LIBRARIES "${ORTHANC_FRAMEWORK_LIBRARIES}")
-    
+
     check_cxx_symbol_exists("Orthanc::InitializeFramework" "OrthancFramework.h" HAVE_ORTHANC_FRAMEWORK)
     if (NOT HAVE_ORTHANC_FRAMEWORK)
       message(FATAL_ERROR "Cannot find the Orthanc framework")
     endif()
-    
+
     unset(CMAKE_REQUIRED_INCLUDES)
     unset(CMAKE_REQUIRED_LIBRARIES)
   endif()
--- a/Resources/Orthanc/CMake/DownloadPackage.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/CMake/DownloadPackage.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -59,8 +59,8 @@
 ##
 
 if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
-  find_program(ZIP_EXECUTABLE 7z 
-    PATHS 
+  find_program(ZIP_EXECUTABLE 7z
+    PATHS
     "$ENV{ProgramFiles}/7-Zip"
     "$ENV{ProgramW6432}/7-Zip"
     )
@@ -92,7 +92,7 @@
 
   set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}")
   if (NOT EXISTS "${TMP_PATH}")
-    message("Downloading ${Url}")
+    message("Downloading ${Url} since the file was not found in ${TMP_PATH}")
 
     # This fixes issue 6: "I think cmake shouldn't download the
     # packages which are not in the system, it should stop and let
@@ -124,7 +124,7 @@
       file(REMOVE ${TMP_PATH})
       message(FATAL_ERROR "Cannot download file: ${Url}")
     endif()
-    
+
   else()
     message("Using local copy of ${Url}")
 
@@ -143,7 +143,7 @@
 macro(DownloadPackage MD5 Url TargetDirectory)
   if (NOT IS_DIRECTORY "${TargetDirectory}")
     DownloadFile("${MD5}" "${Url}")
-    
+
     GetUrlExtension(TMP_EXTENSION "${Url}")
     #message(${TMP_EXTENSION})
     message("Uncompressing ${TMP_FILENAME}")
@@ -152,7 +152,7 @@
       # How to silently extract files using 7-zip
       # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
 
-      if (("${TMP_EXTENSION}" STREQUAL "gz") OR 
+      if (("${TMP_EXTENSION}" STREQUAL "gz") OR
           ("${TMP_EXTENSION}" STREQUAL "tgz") OR
           ("${TMP_EXTENSION}" STREQUAL "xz"))
         execute_process(
@@ -221,7 +221,7 @@
         message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}")
       endif()
     endif()
-   
+
     if (Failure)
       message(FATAL_ERROR "Error while running the uncompression tool")
     endif()
@@ -237,7 +237,7 @@
 macro(DownloadCompressedFile MD5 Url TargetFile)
   if (NOT EXISTS "${TargetFile}")
     DownloadFile("${MD5}" "${Url}")
-    
+
     GetUrlExtension(TMP_EXTENSION "${Url}")
     #message(${TMP_EXTENSION})
     message("Uncompressing ${TMP_FILENAME}")
@@ -275,7 +275,7 @@
         message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
       endif()
     endif()
-   
+
     if (Failure)
       message(FATAL_ERROR "Error while running the uncompression tool")
     endif()
--- a/Resources/Orthanc/CMake/EmbedResources.py	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/CMake/EmbedResources.py	Mon Sep 07 20:58:51 2026 +0200
@@ -90,7 +90,7 @@
 
     if resourceName in resources:
         raise Exception("Twice the same resource: " + resourceName)
-    
+
     if os.path.isdir(pathName):
         # The resource is a directory: Recursively explore its files
         content = {}
@@ -165,7 +165,7 @@
 
 for ns in NAMESPACE.split('.'):
     header.write('namespace %s {\n' % ns)
-    
+
 
 header.write("""
     enum FileResourceId
@@ -177,7 +177,7 @@
     if resources[name]['Type'] == 'File':
         if isFirst:
             isFirst = False
-        else:    
+        else:
             header.write(',\n')
         header.write('      %s' % name)
 
@@ -193,7 +193,7 @@
     if resources[name]['Type'] == 'Directory':
         if isFirst:
             isFirst = False
-        else:    
+        else:
             header.write(',\n')
         header.write('      %s' % name)
 
@@ -235,8 +235,8 @@
 
     # http://stackoverflow.com/a/1035360
     pos = 0
-    buffer = []  # instead of appending a few bytes at a time to the cpp file, 
-                 # we first append each chunk to a list, join it and write it 
+    buffer = []  # instead of appending a few bytes at a time to the cpp file,
+                 # we first append each chunk to a list, join it and write it
                  # to the file.  We've measured that it was 2-3 times faster in python3.
                  # Note that speed is important since if generation is too slow,
                  # cmake might try to compile the EmbeddedResources.cpp file while it is
--- a/Resources/Orthanc/LinuxStandardBaseToolchain.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/LinuxStandardBaseToolchain.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -62,7 +62,7 @@
 # which compilers to use for C and C++
 SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc)
 
-if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
+if (${CMAKE_VERSION} VERSION_LESS "3.6.0")
   CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU)
 else()
   SET(CMAKE_CXX_COMPILER ${LSB_PATH}/bin/lsbc++)
@@ -72,7 +72,7 @@
 SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH})
 
 # adjust the default behaviour of the FIND_XXX() commands:
-# search headers and libraries in the target environment, search 
+# search headers and libraries in the target environment, search
 # programs in the host environment
 SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
 SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
--- a/Resources/Orthanc/MinGW-W64-Toolchain32.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/MinGW-W64-Toolchain32.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -32,7 +32,7 @@
 set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
 
 # adjust the default behaviour of the FIND_XXX() commands:
-# search headers and libraries in the target environment, search 
+# search headers and libraries in the target environment, search
 # programs in the host environment
 set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
 set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
--- a/Resources/Orthanc/MinGW-W64-Toolchain64.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/MinGW-W64-Toolchain64.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -32,7 +32,7 @@
 set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
 
 # adjust the default behaviour of the FIND_XXX() commands:
-# search headers and libraries in the target environment, search 
+# search headers and libraries in the target environment, search
 # programs in the host environment
 set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
 set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
--- a/Resources/Orthanc/MinGWToolchain.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/MinGWToolchain.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -32,7 +32,7 @@
 set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc)
 
 # adjust the default behaviour of the FIND_XXX() commands:
-# search headers and libraries in the target environment, search 
+# search headers and libraries in the target environment, search
 # programs in the host environment
 set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
 set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -10,7 +10,7 @@
  * modify it under the terms of the GNU 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
@@ -28,7 +28,7 @@
 #include <boost/move/unique_ptr.hpp>
 #include <boost/thread.hpp>
 #include <boost/algorithm/string/join.hpp>
-
+#include <limits>
 
 #include <json/reader.h>
 #include <json/version.h>
@@ -61,6 +61,13 @@
 #endif
 
 
+#ifdef _MSC_VER
+#  define ORTHANC_SCANF sscanf_s
+#else
+#  define ORTHANC_SCANF sscanf
+#endif
+
+
 namespace OrthancPlugins
 {
   static OrthancPluginContext* globalContext_ = NULL;
@@ -117,7 +124,7 @@
   }
 
 
-#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1
   void LogMessage(OrthancPluginLogLevel level,
                   const char* file,
                   uint32_t line,
@@ -125,7 +132,7 @@
   {
     if (HasGlobalContext())
     {
-#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1
       const char* pluginName = (pluginName_.empty() ? NULL : pluginName_.c_str());
       OrthancPluginLogMessage(GetGlobalContext(), message.c_str(), pluginName, file, line, OrthancPluginLogCategory_Generic, level);
 #else
@@ -184,7 +191,7 @@
       // Prevent using garbage information
       buffer_.data = NULL;
       buffer_.size = 0;
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 
@@ -209,7 +216,7 @@
     }
     else
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 
@@ -227,7 +234,7 @@
     {
       Clear();
     }
-    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&)
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) // NOLINT(bugprone-empty-catch)
     {
       // Don't throw exceptions in destructors
     }
@@ -264,7 +271,7 @@
     }
     else
     {
-      if (size > 0)
+      if (buffer != NULL && size > 0)
       {
         memcpy(buffer_.data, buffer, size);
       }
@@ -365,7 +372,7 @@
   }
 
 
-#if (HAS_ORTHANC_PLUGIN_PEERS == 1) || (HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1) || (HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1)
+#if (HAS_ORTHANC_PLUGINS_PEERS == 1) || (HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1) || (HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1)
   static void DecodeHttpHeaders(HttpHeaders& target,
                                 const MemoryBuffer& source)
   {
@@ -411,7 +418,7 @@
       {
         headersKeys_.push_back(it->first.c_str());
         headersValues_.push_back(it->second.c_str());
-      }      
+      }
     }
 
     const char* const* GetKeys()
@@ -439,7 +446,7 @@
     PluginHttpHeaders headers(httpHeaders);
 
     return CheckHttp(OrthancPluginRestApiGet2(
-                       GetGlobalContext(), &buffer_, uri.c_str(), 
+                       GetGlobalContext(), &buffer_, uri.c_str(),
                        headers.GetSize(),
                        headers.GetKeys(),
                        headers.GetValues(), applyPlugins));
@@ -451,7 +458,7 @@
                                  bool applyPlugins)
   {
     Clear();
-    
+
     // Cast for compatibility with Orthanc SDK <= 1.5.6
     const char* b = reinterpret_cast<const char*>(body);
 
@@ -465,7 +472,7 @@
     }
   }
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
 
   bool MemoryBuffer::RestApiPost(const std::string& uri,
                                  const void* body,
@@ -478,7 +485,7 @@
 
     PluginHttpHeaders headers(httpHeaders);
 
-    return CheckHttp(OrthancPluginCallRestApi(GetGlobalContext(), 
+    return CheckHttp(OrthancPluginCallRestApi(GetGlobalContext(),
                                               &buffer_,
                                               *answerHeaders,
                                               &httpStatus,
@@ -534,10 +541,10 @@
 #else
     Json::CharReaderBuilder builder;
     builder.settings_["collectComments"] = collectComments;
-    
+
     const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
     assert(reader.get() != NULL);
-    
+
     JSONCPP_STRING err;
     if (reader->parse(reinterpret_cast<const char*>(buffer),
                       reinterpret_cast<const char*>(buffer) + size, &target, &err))
@@ -558,7 +565,7 @@
   {
     return ReadJson(target, source.empty() ? NULL : source.c_str(), source.size());
   }
-  
+
 
   bool ReadJson(Json::Value& target,
                 const void* buffer,
@@ -566,14 +573,14 @@
   {
     return ReadJsonInternal(target, buffer, size, true);
   }
-  
+
 
   bool ReadJsonWithoutComments(Json::Value& target,
                                const std::string& source)
   {
     return ReadJsonWithoutComments(target, source.empty() ? NULL : source.c_str(), source.size());
   }
-  
+
 
   bool ReadJsonWithoutComments(Json::Value& target,
                                const void* buffer,
@@ -595,7 +602,7 @@
     target = Json::writeString(builder, source);
 #endif
   }
-  
+
 
   void WriteStyledJson(std::string& target,
                        const Json::Value& source)
@@ -675,7 +682,7 @@
     {
       Clear();
     }
-    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&)
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) // NOLINT(bugprone-empty-catch)
     {
       // Don't throw exceptions in destructors
     }
@@ -839,7 +846,7 @@
     }
     else
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(error);
     }
   }
 
@@ -862,7 +869,7 @@
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
-    
+
 
   OrthancConfiguration::OrthancConfiguration()
   {
@@ -977,7 +984,15 @@
         return true;
 
       case Json::uintValue:
-        target = configuration_[key].asUInt();
+        if (configuration_[key].asUInt() > static_cast<unsigned int>(std::numeric_limits<int>::max()))
+        {
+          ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"" + GetPath(key) +
+                                    "\" is too large to fit in an integer");
+
+          ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+        }
+
+        target = static_cast<int>(configuration_[key].asUInt());
         return true;
 
       default:
@@ -1338,7 +1353,7 @@
     {
       Clear();
     }
-    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&)
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS&) // NOLINT(bugprone-empty-catch)
     {
       // Don't throw exceptions in destructors
     }
@@ -1473,7 +1488,7 @@
   }
 
 
-#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
+#if HAS_ORTHANC_PLUGINS_FIND_MATCHER == 1
   FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) :
     matcher_(NULL),
     worklist_(worklist)
@@ -1542,13 +1557,27 @@
     }
   }
 
-#endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */
+#endif /* HAS_ORTHANC_PLUGINS_FIND_MATCHER == 1 */
+
+  static void CheckAnswerSizeIsLessThan4GB(const std::string& answer)
+  {
+    if (answer.size() > static_cast<size_t>(std::numeric_limits<uint32_t>::max()))
+    {
+#if HAS_ORTHANC_EXCEPTION == 1
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Cannot send HTTP response larger than 4GB");
+#else
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot send HTTP response larger than 4GB");
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+#endif
+    }
+  }
 
   void AnswerJson(const Json::Value& value,
                   OrthancPluginRestOutput* output)
   {
     std::string bodyString;
-    WriteStyledJson(bodyString, value);    
+    WriteStyledJson(bodyString, value);
+    CheckAnswerSizeIsLessThan4GB(bodyString);
     OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json");
   }
 
@@ -1556,6 +1585,7 @@
                     const char* mimeType,
                     OrthancPluginRestOutput* output)
   {
+    CheckAnswerSizeIsLessThan4GB(answer);
     OrthancPluginAnswerBuffer(GetGlobalContext(), output, answer.c_str(), answer.size(), mimeType);
   }
 
@@ -1564,6 +1594,26 @@
     OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError);
   }
 
+  void AnswerHttpError(uint16_t httpError,
+                       OrthancPluginRestOutput* output,
+                       const std::string& answer,
+                       const char* mimeType)
+  {
+    CheckAnswerSizeIsLessThan4GB(answer);
+
+    OrthancPluginSetHttpHeader(GetGlobalContext(),
+                               output,
+                               "content-type",
+                               mimeType);
+
+    OrthancPluginSendHttpStatus(GetGlobalContext(),
+                                output,
+                                httpError,
+                                answer.c_str(),
+                                static_cast<uint32_t>(answer.size()));
+  }
+
+
   void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods)
   {
     OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods);
@@ -1691,7 +1741,7 @@
     }
   }
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
@@ -1786,7 +1836,7 @@
     }
     else
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(error);
     }
   }
 
@@ -1815,12 +1865,6 @@
       return true;
     }
 
-#ifdef _MSC_VER
-#define ORTHANC_SCANF sscanf_s
-#else
-#define ORTHANC_SCANF sscanf
-#endif
-
     // Parse the version
     int aa, bb, cc = 0;
     if ((ORTHANC_SCANF(version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 &&
@@ -1908,7 +1952,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_PEERS == 1
+#if HAS_ORTHANC_PLUGINS_PEERS == 1
   size_t OrthancPeers::GetPeerIndex(const std::string& name) const
   {
     size_t index;
@@ -1932,7 +1976,7 @@
 
     if (peers_ == NULL)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin);
     }
 
     uint32_t count = OrthancPluginGetPeersCount(GetGlobalContext(), peers_);
@@ -1943,7 +1987,7 @@
       if (name == NULL)
       {
         OrthancPluginFreePeers(GetGlobalContext(), peers_);
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+        ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin);
       }
 
       index_[name] = i;
@@ -1981,14 +2025,14 @@
   {
     if (index >= index_.size())
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
     else
     {
       const char* s = OrthancPluginGetPeerName(GetGlobalContext(), peers_, static_cast<uint32_t>(index));
       if (s == NULL)
       {
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+        ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin);
       }
       else
       {
@@ -2002,14 +2046,14 @@
   {
     if (index >= index_.size())
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
     else
     {
       const char* s = OrthancPluginGetPeerUrl(GetGlobalContext(), peers_, static_cast<uint32_t>(index));
       if (s == NULL)
       {
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+        ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin);
       }
       else
       {
@@ -2031,7 +2075,7 @@
   {
     if (index >= index_.size())
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
     else
     {
@@ -2064,7 +2108,7 @@
   {
     if (index >= index_.size())
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
 
     OrthancPlugins::MemoryBuffer answer;
@@ -2152,7 +2196,7 @@
                             size_t index,
                             const std::string& uri,
                             const std::string& body,
-                            const HttpHeaders& headers, 
+                            const HttpHeaders& headers,
                             unsigned int timeout) const
   {
     MemoryBuffer buffer;
@@ -2174,7 +2218,7 @@
                             size_t index,
                             const std::string& uri,
                             const std::string& body,
-                            const HttpHeaders& headers, 
+                            const HttpHeaders& headers,
                             unsigned int timeout) const
   {
     MemoryBuffer buffer;
@@ -2262,7 +2306,7 @@
   {
     if (index >= index_.size())
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
 
     if (body.size() > 0xffffffffu)
@@ -2285,7 +2329,7 @@
     {
       target.Swap(answer);
       DecodeHttpHeaders(answerHeaders, answerHeadersBuffer);
-      
+
       return (status == 200);
     }
     else
@@ -2302,7 +2346,7 @@
   {
     if (index >= index_.size())
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
 
     if (body.size() > 0xffffffffu)
@@ -2348,7 +2392,7 @@
   {
     if (index >= index_.size())
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
 
     OrthancPlugins::MemoryBuffer answer;
@@ -2389,7 +2433,7 @@
    ** JOBS
    ******************************************************************/
 
-#if HAS_ORTHANC_PLUGIN_JOB == 1
+#if HAS_ORTHANC_PLUGINS_JOB == 1
   void OrthancJob::CallbackFinalize(void* job)
   {
     if (job != NULL)
@@ -2428,7 +2472,7 @@
       {
         memcpy(target->data, source.c_str(), source.size());
       }
-      
+
       return OrthancPluginErrorCode_Success;
     }
   }
@@ -2471,7 +2515,7 @@
   {
     assert(job != NULL);
     OrthancJob& that = *reinterpret_cast<OrthancJob*>(job);
-    
+
     if (that.hasSerialized_)
     {
       if (CopyStringToMemoryBuffer(target, that.serialized_) == OrthancPluginErrorCode_Success)
@@ -2587,7 +2631,7 @@
 
     if (content.type() != Json::objectValue)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat);
     }
     else
     {
@@ -2607,7 +2651,7 @@
   {
     if (serialized.type() != Json::objectValue)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_BadFileFormat);
     }
     else
     {
@@ -2622,7 +2666,7 @@
     if (progress < 0 ||
         progress > 1)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);
     }
 
     progress_ = progress;
@@ -2642,7 +2686,7 @@
   {
     if (job == NULL)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
     }
 
     OrthancPluginJob* orthanc =
@@ -2657,7 +2701,7 @@
 
     if (orthanc == NULL)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin);
     }
     else
     {
@@ -2671,7 +2715,7 @@
   {
     if (job == NULL)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
     }
 
     OrthancPluginJob* orthanc = Create(job);
@@ -2682,7 +2726,7 @@
     {
       ORTHANC_PLUGINS_LOG_ERROR("Plugin cannot submit job");
       OrthancPluginFreeJob(GetGlobalContext(), orthanc);
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_Plugin);
     }
     else
     {
@@ -2710,7 +2754,7 @@
           !status.isMember("State") ||
           status["State"].type() != Json::stringValue)
       {
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InexistentItem);        
+        ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_InexistentItem);
       }
 
       const std::string state = status["State"].asString();
@@ -2739,30 +2783,30 @@
         if (!status.isMember("ErrorCode") ||
             status["ErrorCode"].type() != Json::intValue)
         {
-          ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError);
+          ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_InternalError);
         }
         else
         {
           if (!status.isMember("ErrorDescription") ||
               status["ErrorDescription"].type() != Json::stringValue)
           {
-            ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());
+            ORTHANC_PLUGINS_THROW_ERROR_CODE(status["ErrorCode"].asInt());
           }
           else
           {
-  #if HAS_ORTHANC_EXCEPTION == 1
+#if HAS_ORTHANC_EXCEPTION == 1
             throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(status["ErrorCode"].asInt()),
                                             status["ErrorDescription"].asString());
-  #else
+#else
             ORTHANC_PLUGINS_LOG_ERROR("Exception while executing the job: " + status["ErrorDescription"].asString());
-            ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());          
-  #endif
+            ORTHANC_PLUGINS_THROW_ERROR_CODE(status["ErrorCode"].asInt());
+#endif
           }
         }
       }
       else
       {
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError);
+        ORTHANC_PLUGINS_THROW_ERROR_CODE(OrthancPluginErrorCode_InternalError);
       }
     }
   }
@@ -2777,7 +2821,7 @@
     static const char* KEY_PRIORITY = "Priority";
 
     boost::movelib::unique_ptr<OrthancJob> protection(job);
-  
+
     if (body.type() != Json::objectValue)
     {
 #if HAS_ORTHANC_EXCEPTION == 1
@@ -2790,7 +2834,7 @@
     }
 
     bool synchronous = true;
-  
+
     if (body.isMember(KEY_SYNCHRONOUS))
     {
       if (body[KEY_SYNCHRONOUS].type() != Json::booleanValue)
@@ -2849,7 +2893,7 @@
         priority = !body[KEY_PRIORITY].asInt();
       }
     }
-  
+
     Json::Value result;
 
     if (synchronous)
@@ -2879,13 +2923,13 @@
    ** METRICS
    ******************************************************************/
 
-#if HAS_ORTHANC_PLUGIN_METRICS == 1
+#if HAS_ORTHANC_PLUGINS_METRICS == 1
   MetricsTimer::MetricsTimer(const char* name) :
     name_(name)
   {
     start_ = boost::posix_time::microsec_clock::universal_time();
   }
-  
+
   MetricsTimer::~MetricsTimer()
   {
     const boost::posix_time::ptime stop = boost::posix_time::microsec_clock::universal_time();
@@ -2902,7 +2946,7 @@
    ** HTTP CLIENT
    ******************************************************************/
 
-#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1
   class HttpClient::RequestBodyWrapper : public boost::noncopyable
   {
   private:
@@ -2921,18 +2965,18 @@
       body_(body),
       done_(false)
     {
-    }      
+    }
 
     static uint8_t IsDone(void* body)
     {
       return GetObject(body).done_;
     }
-    
+
     static const void* GetChunkData(void* body)
     {
       return GetObject(body).chunk_.c_str();
     }
-    
+
     static uint32_t GetChunkSize(void* body)
     {
       return static_cast<uint32_t>(GetObject(body).chunk_.size());
@@ -2941,7 +2985,7 @@
     static OrthancPluginErrorCode Next(void* body)
     {
       RequestBodyWrapper& that = GetObject(body);
-        
+
       if (that.done_)
       {
         return OrthancPluginErrorCode_BadSequenceOfCalls;
@@ -2962,11 +3006,11 @@
           return OrthancPluginErrorCode_Plugin;
         }
       }
-    }    
+    }
   };
 
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1
   static OrthancPluginErrorCode AnswerAddHeaderCallback(void* answer,
                                                         const char* key,
                                                         const char* value)
@@ -2990,7 +3034,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1
   static OrthancPluginErrorCode AnswerAddChunkCallback(void* answer,
                                                        const void* data,
                                                        uint32_t size)
@@ -3034,7 +3078,7 @@
     }
   }
 
-  
+
   void HttpClient::SetCredentials(const std::string& username,
                                   const std::string& password)
   {
@@ -3042,7 +3086,7 @@
     password_ = password;
   }
 
-  
+
   void HttpClient::ClearCredentials()
   {
     username_.clear();
@@ -3059,7 +3103,7 @@
     certificateKeyPassword_ = keyPassword;
   }
 
-  
+
   void HttpClient::ClearCertificate()
   {
     certificateFile_.clear();
@@ -3074,21 +3118,21 @@
     chunkedBody_ = NULL;
   }
 
-  
+
   void HttpClient::SwapBody(std::string& body)
   {
     fullBody_.swap(body);
     chunkedBody_ = NULL;
   }
 
-  
+
   void HttpClient::SetBody(const std::string& body)
   {
     fullBody_ = body;
     chunkedBody_ = NULL;
   }
 
-  
+
   void HttpClient::SetBody(IRequestBody& body)
   {
     fullBody_.clear();
@@ -3254,7 +3298,7 @@
     };
 
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1
     class MemoryAnswer : public HttpClient::IAnswer
     {
     private:
@@ -3288,7 +3332,7 @@
   }
 
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1
   void HttpClient::ExecuteWithStream(uint16_t& httpStatus,
                                      IAnswer& answer,
                                      IRequestBody& body) const
@@ -3317,7 +3361,7 @@
     }
 
     RequestBodyWrapper request(body);
-        
+
     OrthancPluginErrorCode error = OrthancPluginChunkedHttpClient(
       GetGlobalContext(),
       &answer,
@@ -3344,10 +3388,10 @@
 
     if (error != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
-    }
-  }
-#endif    
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(error);
+    }
+  }
+#endif
 
   void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus,
                                         HttpHeaders& answerHeaders,
@@ -3386,7 +3430,7 @@
 
     if (error != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(error);
     }
 
     DecodeHttpHeaders(answerHeaders, answerHeadersBuffer);
@@ -3396,7 +3440,7 @@
 
   void HttpClient::Execute(IAnswer& answer)
   {
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1
     if (allowChunkedTransfers_)
     {
       if (chunkedBody_ != NULL)
@@ -3412,7 +3456,7 @@
       return;
     }
 #endif
-    
+
     // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
     // transfers are disabled. This results in higher memory usage
     // (all chunks from the answer body are sent at once)
@@ -3421,10 +3465,10 @@
     std::string answerBody;
     Execute(answerHeaders, answerBody);
 
-    for (HttpHeaders::const_iterator it = answerHeaders.begin(); 
+    for (HttpHeaders::const_iterator it = answerHeaders.begin();
          it != answerHeaders.end(); ++it)
     {
-      answer.AddHeader(it->first, it->second);      
+      answer.AddHeader(it->first, it->second);
     }
 
     if (!answerBody.empty())
@@ -3437,7 +3481,7 @@
   void HttpClient::Execute(HttpHeaders& answerHeaders /* out */,
                            std::string& answerBody /* out */)
   {
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1
     if (allowChunkedTransfers_)
     {
       MemoryAnswer answer;
@@ -3447,7 +3491,7 @@
       return;
     }
 #endif
-    
+
     // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
     // transfers are disabled. This results in higher memory usage
     // (all chunks from the request body are sent at once)
@@ -3455,7 +3499,7 @@
     if (chunkedBody_ != NULL)
     {
       ChunkedBuffer buffer;
-      
+
       std::string chunk;
       while (chunkedBody_->ReadNextChunk(chunk))
       {
@@ -3479,7 +3523,7 @@
   {
     std::string body;
     Execute(answerHeaders, body);
-    
+
     if (!ReadJson(answerBody, body))
     {
       ORTHANC_PLUGINS_LOG_ERROR("Cannot convert HTTP answer body to JSON");
@@ -3495,7 +3539,7 @@
     Execute(answerHeaders, body);
   }
 
-#endif  /* HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 */
+#endif  /* HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1 */
 
 
 
@@ -3512,7 +3556,7 @@
                           const OrthancPluginHttpRequest* request)
     {
     }
-  
+
     IChunkedRequestReader *NullChunkedRestCallback(const char* url,
                                                    const OrthancPluginHttpRequest* request)
     {
@@ -3520,7 +3564,7 @@
     }
 
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER == 1
 
     OrthancPluginErrorCode ChunkedRequestReaderAddChunk(
       OrthancPluginServerChunkedRequestReader* reader,
@@ -3551,7 +3595,7 @@
       }
     }
 
-    
+
     OrthancPluginErrorCode ChunkedRequestReaderExecute(
       OrthancPluginServerChunkedRequestReader* reader,
       OrthancPluginRestOutput*                 output)
@@ -3580,7 +3624,7 @@
       }
     }
 
-    
+
     void ChunkedRequestReaderFinalize(
       OrthancPluginServerChunkedRequestReader* reader)
     {
@@ -3591,7 +3635,7 @@
     }
 
 #else
-    
+
     OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
                                                     const char* url,
                                                     const OrthancPluginHttpRequest* request,
@@ -3615,7 +3659,7 @@
           {
             allowed += ",";
           }
-        
+
           allowed += "POST";
         }
 
@@ -3625,7 +3669,7 @@
           {
             allowed += ",";
           }
-        
+
           allowed += "DELETE";
         }
 
@@ -3635,10 +3679,10 @@
           {
             allowed += ",";
           }
-        
+
           allowed += "PUT";
         }
-      
+
         switch (request->method)
         {
           case OrthancPluginHttpMethod_Get:
@@ -3715,7 +3759,7 @@
       }
       catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
       {
-#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1
+#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS == 1
         if (HasGlobalContext() &&
             e.HasDetails())
         {
@@ -3742,7 +3786,7 @@
   }
 
 
-#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+#if HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP == 1
   OrthancPluginErrorCode IStorageCommitmentScpHandler::Lookup(
     OrthancPluginStorageCommitmentFailureReason* target,
     void* rawHandler,
@@ -3751,7 +3795,7 @@
   {
     assert(target != NULL &&
            rawHandler != NULL);
-      
+
     try
     {
       IStorageCommitmentScpHandler& handler = *reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler);
@@ -3770,7 +3814,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+#if HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP == 1
   void IStorageCommitmentScpHandler::Destructor(void* rawHandler)
   {
     assert(rawHandler != NULL);
@@ -3779,7 +3823,7 @@
 #endif
 
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
   DicomInstance::DicomInstance(const OrthancPluginDicomInstance* instance) :
     toFree_(false),
     instance_(instance)
@@ -3820,7 +3864,7 @@
 #endif
   }
 
-  
+
   std::string DicomInstance::GetRemoteAet() const
   {
     const char* s = OrthancPluginGetInstanceRemoteAet(GetGlobalContext(), instance_);
@@ -3841,7 +3885,7 @@
     s.Assign(OrthancPluginGetInstanceJson(GetGlobalContext(), instance_));
     s.ToJson(target);
   }
-  
+
 
   void DicomInstance::GetSimplifiedJson(Json::Value& target) const
   {
@@ -3863,7 +3907,7 @@
   }
 #endif
 
-  
+
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
   bool DicomInstance::HasPixelData() const
   {
@@ -3880,7 +3924,7 @@
 #endif
 
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)  
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
   void DicomInstance::GetRawFrame(std::string& target,
                                   unsigned int frameIndex) const
   {
@@ -3894,13 +3938,13 @@
     }
     else
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 #endif
 
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)  
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
   OrthancImage* DicomInstance::GetDecodedFrame(unsigned int frameIndex) const
   {
     OrthancPluginImage* image = OrthancPluginGetInstanceDecodedFrame(
@@ -3915,7 +3959,7 @@
       return new OrthancImage(image);
     }
   }
-#endif  
+#endif
 
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
@@ -3931,11 +3975,11 @@
     }
     else
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 #endif
-  
+
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
   DicomInstance* DicomInstance::Transcode(const void* buffer,
@@ -3980,7 +4024,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   static std::vector<std::string> WebDavConvertPath(uint32_t pathSize,
                                                     const char* const*  pathItems)
   {
@@ -3994,9 +4038,9 @@
     return result;
   }
 #endif
-  
-    
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+
+
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   static OrthancPluginErrorCode WebDavIsExistingFolder(uint8_t*            isExisting,
                                                        uint32_t            pathSize,
                                                        const char* const*  pathItems,
@@ -4020,8 +4064,8 @@
   }
 #endif
 
-  
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   static OrthancPluginErrorCode WebDavListFolder(uint8_t*                        isExisting,
                                                  OrthancPluginWebDavCollection*  collection,
                                                  OrthancPluginWebDavAddFile      addFile,
@@ -4031,12 +4075,12 @@
                                                  void*                           payload)
   {
     IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload);
-      
+
     try
     {
       std::list<IWebDavCollection::FileInfo> files;
       std::list<IWebDavCollection::FolderInfo> subfolders;
-      
+
       if (!that.ListFolder(files, subfolders, WebDavConvertPath(pathSize, pathItems)))
       {
         *isExisting = 0;
@@ -4044,33 +4088,33 @@
       else
       {
         *isExisting = 1;
-      
+
         for (std::list<IWebDavCollection::FileInfo>::const_iterator
                it = files.begin(); it != files.end(); ++it)
         {
           OrthancPluginErrorCode code = addFile(
             collection, it->GetName().c_str(), it->GetContentSize(),
             it->GetMimeType().c_str(), it->GetDateTime().c_str());
-        
+
           if (code != OrthancPluginErrorCode_Success)
           {
             return code;
           }
         }
-      
+
         for (std::list<IWebDavCollection::FolderInfo>::const_iterator it =
                subfolders.begin(); it != subfolders.end(); ++it)
         {
           OrthancPluginErrorCode code = addFolder(
             collection, it->GetName().c_str(), it->GetDateTime().c_str());
-        
+
           if (code != OrthancPluginErrorCode_Success)
           {
             return code;
           }
         }
       }
-      
+
       return OrthancPluginErrorCode_Success;
     }
     catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
@@ -4082,10 +4126,10 @@
       return OrthancPluginErrorCode_Plugin;
     }
   }
-#endif    
-
-
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+#endif
+
+
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   static OrthancPluginErrorCode WebDavRetrieveFile(OrthancPluginWebDavCollection*   collection,
                                                    OrthancPluginWebDavRetrieveFile  retrieveFile,
                                                    uint32_t                         pathSize,
@@ -4097,7 +4141,7 @@
     try
     {
       std::string content, mime, dateTime;
-        
+
       if (that.GetFile(content, mime, dateTime, WebDavConvertPath(pathSize, pathItems)))
       {
         return retrieveFile(collection, content.empty() ? NULL : content.c_str(),
@@ -4117,11 +4161,11 @@
     {
       return OrthancPluginErrorCode_InternalError;
     }
-  }  
+  }
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   static OrthancPluginErrorCode WebDavStoreFileCallback(uint8_t*            isReadOnly, /* out */
                                                         uint32_t            pathSize,
                                                         const char* const*  pathItems,
@@ -4137,7 +4181,7 @@
       {
         ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
       }
-      
+
       *isReadOnly = (that.StoreFile(WebDavConvertPath(pathSize, pathItems), data,
                                     static_cast<size_t>(size)) ? 1 : 0);
       return OrthancPluginErrorCode_Success;
@@ -4153,8 +4197,8 @@
   }
 #endif
 
-  
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   static OrthancPluginErrorCode WebDavCreateFolderCallback(uint8_t*            isReadOnly, /* out */
                                                            uint32_t            pathSize,
                                                            const char* const*  pathItems,
@@ -4177,9 +4221,9 @@
     }
   }
 #endif
-  
-  
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+
+
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   static OrthancPluginErrorCode WebDavDeleteItemCallback(uint8_t*            isReadOnly, /* out */
                                                          uint32_t            pathSize,
                                                          const char* const*  pathItems,
@@ -4203,8 +4247,8 @@
   }
 #endif
 
-  
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   void IWebDavCollection::Register(const std::string& uri,
                                    IWebDavCollection& collection)
   {
@@ -4214,7 +4258,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 #endif
@@ -4226,7 +4270,7 @@
     for (uint32_t i = 0; i < request->getCount; ++i)
     {
       result[request->getKeys[i]] = request->getValues[i];
-    }    
+    }
   }
 
   void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request)
@@ -4236,7 +4280,7 @@
     for (uint32_t i = 0; i < request->headersCount; ++i)
     {
       result[request->headersKeys[i]] = request->headersValues[i];
-    }    
+    }
   }
 
   void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request)
@@ -4304,7 +4348,7 @@
   }
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   RestApiClient::RestApiClient() :
     method_(OrthancPluginHttpMethod_Get),
     path_("/"),
@@ -4338,7 +4382,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   void RestApiClient::AddRequestHeader(const std::string& key,
                                        const std::string& value)
   {
@@ -4354,7 +4398,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   void RestApiClient::SetRequestHeader(const std::string& key,
                                        const std::string& value)
   {
@@ -4363,7 +4407,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   bool RestApiClient::Execute()
   {
     if (requestBody_.size() > 0xffffffffu)
@@ -4405,7 +4449,7 @@
       }
       else
       {
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+        ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
       }
     }
   }
@@ -4430,7 +4474,7 @@
           mimeType = h->second.c_str();
         }
       }
-      
+
       AnswerString(answerBody_, mimeType, output);
     }
     else
@@ -4446,7 +4490,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   uint16_t RestApiClient::GetHttpStatus() const
   {
     if (httpStatus_ == 0)
@@ -4461,7 +4505,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   bool RestApiClient::LookupAnswerHeader(std::string& value,
                                          const std::string& key) const
   {
@@ -4486,7 +4530,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   const std::string& RestApiClient::GetAnswerBody() const
   {
     if (httpStatus_ == 0)
@@ -4501,7 +4545,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   KeyValueStore::Iterator::Iterator(OrthancPluginKeysValuesIterator  *iterator) :
     iterator_(iterator)
   {
@@ -4513,7 +4557,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   KeyValueStore::Iterator::~Iterator()
   {
     OrthancPluginFreeKeysValuesIterator(OrthancPlugins::GetGlobalContext(), iterator_);
@@ -4521,7 +4565,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   bool KeyValueStore::Iterator::Next()
   {
     uint8_t done;
@@ -4529,7 +4573,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
     else
     {
@@ -4539,7 +4583,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   std::string KeyValueStore::Iterator::GetKey() const
   {
     const char* s = OrthancPluginKeysValuesIteratorGetKey(OrthancPlugins::GetGlobalContext(), iterator_);
@@ -4555,7 +4599,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   void KeyValueStore::Iterator::GetValue(std::string& value) const
   {
     OrthancPlugins::MemoryBuffer valueBuffer;
@@ -4563,7 +4607,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
     else
     {
@@ -4573,7 +4617,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   void KeyValueStore::Store(const std::string& key,
                             const void* value,
                             size_t valueSize)
@@ -4587,13 +4631,13 @@
                                                              key.c_str(), value, static_cast<uint32_t>(valueSize));
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   bool KeyValueStore::GetValue(std::string& value,
                                const std::string& key)
   {
@@ -4604,7 +4648,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
     else if (found)
     {
@@ -4619,7 +4663,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   void KeyValueStore::DeleteKey(const std::string& key)
   {
     OrthancPluginErrorCode code = OrthancPluginDeleteKeyValue(OrthancPlugins::GetGlobalContext(),
@@ -4627,13 +4671,13 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   KeyValueStore::Iterator* KeyValueStore::CreateIterator()
   {
     return new Iterator(OrthancPluginCreateKeysValuesIterator(OrthancPlugins::GetGlobalContext(), storeId_.c_str()));
@@ -4641,7 +4685,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+#if HAS_ORTHANC_PLUGINS_QUEUES == 1
   void Queue::Enqueue(const void* value,
                       size_t valueSize)
   {
@@ -4655,13 +4699,13 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
   }
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+#if HAS_ORTHANC_PLUGINS_QUEUES == 1
   bool Queue::DequeueInternal(std::string& value,
                               OrthancPluginQueueOrigin origin)
   {
@@ -4676,7 +4720,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
     else if (found)
     {
@@ -4691,7 +4735,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+#if HAS_ORTHANC_PLUGINS_QUEUES == 1
   uint64_t Queue::GetSize()
   {
     uint64_t size = 0;
@@ -4699,7 +4743,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
     else
     {
@@ -4709,7 +4753,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
   bool Queue::ReserveInternal(std::string& value, uint64_t& valueId, OrthancPluginQueueOrigin origin, uint32_t releaseTimeout)
   {
     uint8_t found = false;
@@ -4720,7 +4764,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+      ORTHANC_PLUGINS_THROW_ERROR_CODE(code);
     }
     else if (found)
     {
@@ -4735,7 +4779,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
   bool Queue::ReserveBack(std::string& value, uint64_t& valueId, uint32_t releaseTimeout)
   {
     return ReserveInternal(value, valueId, OrthancPluginQueueOrigin_Back, releaseTimeout);
@@ -4743,7 +4787,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
   bool Queue::ReserveFront(std::string& value, uint64_t& valueId, uint32_t releaseTimeout)
   {
     return ReserveInternal(value, valueId, OrthancPluginQueueOrigin_Front, releaseTimeout);
@@ -4751,7 +4795,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
   void Queue::Acknowledge(uint64_t valueId)
   {
     OrthancPluginAcknowledgeQueueValue(OrthancPlugins::GetGlobalContext(), queueId_.c_str(), valueId);
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Mon Sep 07 20:58:51 2026 +0200
@@ -10,7 +10,7 @@
  * modify it under the terms of the GNU 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
@@ -27,7 +27,6 @@
 
 #include <orthanc/OrthancCPlugin.h>
 #include <boost/noncopyable.hpp>
-#include <boost/lexical_cast.hpp>
 #include <boost/thread/mutex.hpp>
 #include <boost/date_time/posix_time/posix_time.hpp>
 #include <json/value.h>
@@ -41,7 +40,7 @@
 /**
  * The definition of ORTHANC_PLUGINS_VERSION_IS_ABOVE below is for
  * backward compatibility with Orthanc SDK <= 1.3.0.
- * 
+ *
  *   $ hg diff -r Orthanc-1.3.0:Orthanc-1.3.1 ../../../Plugins/Include/orthanc/OrthancCPlugin.h
  *
  **/
@@ -67,98 +66,98 @@
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0)
 // The "OrthancPluginFindMatcher()" primitive was introduced in Orthanc 1.2.0
-#  define HAS_ORTHANC_PLUGIN_FIND_MATCHER  1
+#  define HAS_ORTHANC_PLUGINS_FIND_MATCHER  1
 #else
-#  define HAS_ORTHANC_PLUGIN_FIND_MATCHER  0
+#  define HAS_ORTHANC_PLUGINS_FIND_MATCHER  0
 #endif
 
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 4, 2)
-#  define HAS_ORTHANC_PLUGIN_PEERS  1
-#  define HAS_ORTHANC_PLUGIN_JOB    1
+#  define HAS_ORTHANC_PLUGINS_PEERS  1
+#  define HAS_ORTHANC_PLUGINS_JOB    1
 #else
-#  define HAS_ORTHANC_PLUGIN_PEERS  0
-#  define HAS_ORTHANC_PLUGIN_JOB    0
+#  define HAS_ORTHANC_PLUGINS_PEERS  0
+#  define HAS_ORTHANC_PLUGINS_JOB    0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
-#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  1
+#  define HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS  1
 #else
-#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  0
+#  define HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 4)
-#  define HAS_ORTHANC_PLUGIN_METRICS  1
+#  define HAS_ORTHANC_PLUGINS_METRICS  1
 #else
-#  define HAS_ORTHANC_PLUGIN_METRICS  0
+#  define HAS_ORTHANC_PLUGINS_METRICS  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 1, 0)
-#  define HAS_ORTHANC_PLUGIN_HTTP_CLIENT  1
+#  define HAS_ORTHANC_PLUGINS_HTTP_CLIENT  1
 #else
-#  define HAS_ORTHANC_PLUGIN_HTTP_CLIENT  0
+#  define HAS_ORTHANC_PLUGINS_HTTP_CLIENT  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT  1
+#  define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT  1
 #else
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT  0
+#  define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER  1
+#  define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER  1
 #else
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER  0
+#  define HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 0)
-#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  1
+#  define HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP  1
 #else
-#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  0
+#  define HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2)
-#  define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API  1
+#  define HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API  1
 #else
-#  define HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API  0
+#  define HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 10, 1)
-#  define HAS_ORTHANC_PLUGIN_WEBDAV  1
+#  define HAS_ORTHANC_PLUGINS_WEBDAV  1
 #else
-#  define HAS_ORTHANC_PLUGIN_WEBDAV  0
+#  define HAS_ORTHANC_PLUGINS_WEBDAV  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
-#  define HAS_ORTHANC_PLUGIN_LOG_MESSAGE  1
+#  define HAS_ORTHANC_PLUGINS_LOG_MESSAGE  1
 #else
-#  define HAS_ORTHANC_PLUGIN_LOG_MESSAGE  0
+#  define HAS_ORTHANC_PLUGINS_LOG_MESSAGE  0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
-#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  1
-#  define HAS_ORTHANC_PLUGIN_QUEUES            1
+#  define HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES  1
+#  define HAS_ORTHANC_PLUGINS_QUEUES            1
 #else
-#  define HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES  0
-#  define HAS_ORTHANC_PLUGIN_QUEUES            0
+#  define HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES  0
+#  define HAS_ORTHANC_PLUGINS_QUEUES            0
 #endif
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 10)
-#  define HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE   1
+#  define HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE   1
 #else
-#  define HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE   0
+#  define HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE   0
 #endif
 
 
 // Macro to tag a function as having been deprecated
 #if (__cplusplus >= 201402L)  // C++14
-#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) [[deprecated]] f
+#  define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(f) [[deprecated]] f
 #elif defined(__GNUC__) || defined(__clang__)
-#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) f __attribute__((deprecated))
+#  define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(f) f __attribute__((deprecated))
 #elif defined(_MSC_VER)
-#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(f) __declspec(deprecated) f
+#  define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(f) __declspec(deprecated) f
 #else
-#  define ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED
+#  define ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED
 #endif
 
 
@@ -172,7 +171,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1
 #  define ORTHANC_PLUGINS_LOG_ERROR(msg)   ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Error, __ORTHANC_FILE__, __LINE__, msg)
 #  define ORTHANC_PLUGINS_LOG_WARNING(msg) ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Warning, __ORTHANC_FILE__, __LINE__, msg)
 #  define ORTHANC_PLUGINS_LOG_INFO(msg)    ::OrthancPlugins::LogMessage(OrthancPluginLogLevel_Info, __ORTHANC_FILE__, __LINE__, msg)
@@ -204,7 +203,7 @@
 
   OrthancPluginContext* GetGlobalContext();
 
-  
+
   class OrthancImage;
 
 
@@ -297,7 +296,7 @@
                      const Json::Value& body,
                      bool applyPlugins);
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
     bool RestApiPost(const std::string& uri,
                      const Json::Value& body,
                      const HttpHeaders& httpHeaders,
@@ -394,9 +393,9 @@
     void ToString(std::string& target) const;
 
     void ToJson(Json::Value& target) const;
-  
+
     void ToJsonWithoutComments(Json::Value& target) const;
-};
+  };
 
 
   class OrthancConfiguration : public boost::noncopyable
@@ -408,7 +407,7 @@
     std::string GetPath(const std::string& key) const;
 
     void LoadConfiguration();
-    
+
   public:
     OrthancConfiguration(); // loads the full Orthanc configuration
 
@@ -428,7 +427,7 @@
 
     bool LookupStringValue(std::string& target,
                            const std::string& key) const;
-    
+
     bool LookupIntegerValue(int& target,
                             const std::string& key) const;
 
@@ -511,7 +510,7 @@
     unsigned int GetHeight() const;
 
     unsigned int GetPitch() const;
-    
+
     void* GetBuffer() const;
 
     const OrthancPluginImage* GetObject() const
@@ -528,14 +527,14 @@
 
     void AnswerJpegImage(OrthancPluginRestOutput* output,
                          uint8_t quality) const;
-    
+
     void* GetWriteableBuffer();
 
     OrthancPluginImage* Release();
   };
 
 
-#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
+#if HAS_ORTHANC_PLUGINS_FIND_MATCHER == 1
   class FindMatcher : public boost::noncopyable
   {
   private:
@@ -574,13 +573,13 @@
 
   bool ReadJson(Json::Value& target,
                 const std::string& source);
-  
+
   bool ReadJson(Json::Value& target,
                 const void* buffer,
                 size_t size);
 
   bool ReadJsonWithoutComments(Json::Value& target,
-                               const std::string& source);  
+                               const std::string& source);
 
   bool ReadJsonWithoutComments(Json::Value& target,
                                const void* buffer,
@@ -622,7 +621,7 @@
                    size_t bodySize,
                    bool applyPlugins);
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
@@ -690,36 +689,41 @@
   void AnswerHttpError(uint16_t httpError,
                        OrthancPluginRestOutput* output);
 
+  void AnswerHttpError(uint16_t httpError,
+                       OrthancPluginRestOutput* output,
+                       const std::string& answer,
+                       const char* mimeType);
+
   void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods);
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
   const char* AutodetectMimeType(const std::string& path);
 #endif
 
-#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1
   void LogMessage(OrthancPluginLogLevel level,
                   const char* file,
                   uint32_t line,
                   const std::string& message);
 #endif
 
-#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1
   // Use macro ORTHANC_PLUGINS_LOG_ERROR() instead
-  ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogError(const std::string& message));
+  ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(void LogError(const std::string& message));
 #else
   void LogError(const std::string& message);
 #endif
 
-#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1
   // Use macro ORTHANC_PLUGINS_LOG_WARNING() instead
-  ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogWarning(const std::string& message));
+  ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(void LogWarning(const std::string& message));
 #else
   void LogWarning(const std::string& message);
 #endif
 
-#if HAS_ORTHANC_PLUGIN_LOG_MESSAGE == 1
+#if HAS_ORTHANC_PLUGINS_LOG_MESSAGE == 1
   // Use macro ORTHANC_PLUGINS_LOG_INFO() instead
-  ORTHANC_PLUGIN_CPP_WRAPPER_DEPRECATED(void LogInfo(const std::string& message));
+  ORTHANC_PLUGINS_CPP_WRAPPER_DEPRECATED(void LogInfo(const std::string& message));
 #else
   void LogInfo(const std::string& message);
 #endif
@@ -727,7 +731,7 @@
   void ReportMinimalOrthancVersion(unsigned int major,
                                    unsigned int minor,
                                    unsigned int revision);
-  
+
   bool CheckMinimalOrthancVersion(unsigned int major,
                                   unsigned int minor,
                                   unsigned int revision);
@@ -751,7 +755,7 @@
       }
       catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
       {
-#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1
+#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGINS_EXCEPTION_DETAILS == 1
         if (HasGlobalContext() &&
             e.HasDetails())
         {
@@ -776,7 +780,7 @@
     }
   }
 
-  
+
   template <RestCallback Callback>
   void RegisterRestCallback(const std::string& uri,
                             bool isThreadSafe)
@@ -794,7 +798,7 @@
   }
 
 
-#if HAS_ORTHANC_PLUGIN_PEERS == 1
+#if HAS_ORTHANC_PLUGINS_PEERS == 1
   class OrthancPeers : public boost::noncopyable
   {
   private:
@@ -902,7 +906,7 @@
                 const std::string& body,
                 const HttpHeaders& headers,
                 unsigned int timeout) const;
-    
+
     bool DoPost(Json::Value& target,
                 HttpHeaders& answerHeaders,
                 size_t index,
@@ -939,7 +943,7 @@
 
 
 
-#if HAS_ORTHANC_PLUGIN_JOB == 1
+#if HAS_ORTHANC_PLUGINS_JOB == 1
   class OrthancJob : public boost::noncopyable
   {
   private:
@@ -985,10 +989,10 @@
     void UpdateSerialized(const Json::Value& serialized);
 
     void UpdateProgress(float progress);
-    
+
   public:
     explicit OrthancJob(const std::string& jobType);
-    
+
     virtual ~OrthancJob()
     {
     }
@@ -996,7 +1000,7 @@
     virtual OrthancPluginJobStepStatus Step() = 0;
 
     virtual void Stop(OrthancPluginJobStopReason reason) = 0;
-    
+
     virtual void Reset() = 0;
 
     static OrthancPluginJob* Create(OrthancJob* job /* takes ownership */);
@@ -1018,14 +1022,27 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_METRICS == 1
+#if HAS_ORTHANC_PLUGINS_METRICS == 1
   inline void SetMetricsValue(const char* name,
                               float value)
   {
     OrthancPluginSetMetricsValue(GetGlobalContext(), name,
                                  value, OrthancPluginMetricsType_Default);
   }
+#endif
 
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 1)
+  inline void SetMetricsValue(const char* name,
+                              int64_t value)
+  {
+    OrthancPluginSetMetricsIntegerValue(GetGlobalContext(), name,
+                                        value, OrthancPluginMetricsType_Default);
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGINS_METRICS == 1
   class MetricsTimer : public boost::noncopyable
   {
   private:
@@ -1040,7 +1057,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_HTTP_CLIENT == 1
   class HttpClient : public boost::noncopyable
   {
   public:
@@ -1088,7 +1105,7 @@
     IRequestBody*            chunkedBody_;
     bool                     allowChunkedTransfers_;
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_CLIENT == 1
     void ExecuteWithStream(uint16_t& httpStatus,  // out
                            IAnswer& answer,       // out
                            IRequestBody& body) const;
@@ -1098,7 +1115,7 @@
                               HttpHeaders& answerHeaders,  // out
                               std::string& answerBody,     // out
                               const std::string& body) const;
-    
+
   public:
     HttpClient();
 
@@ -1213,16 +1230,16 @@
     void NullRestCallback(OrthancPluginRestOutput* output,
                           const char* url,
                           const OrthancPluginHttpRequest* request);
-  
+
     IChunkedRequestReader *NullChunkedRestCallback(const char* url,
                                                    const OrthancPluginHttpRequest* request);
 
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER == 1
     template <ChunkedRestCallback Callback>
     static OrthancPluginErrorCode ChunkedProtect(OrthancPluginServerChunkedRequestReader** reader,
-                                                const char* url,
-                                                const OrthancPluginHttpRequest* request)
+                                                 const char* url,
+                                                 const OrthancPluginHttpRequest* request)
     {
       try
       {
@@ -1269,7 +1286,7 @@
     void ChunkedRequestReaderFinalize(
       OrthancPluginServerChunkedRequestReader* reader);
 
-#else  
+#else
 
     OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
                                                     const char* url,
@@ -1310,7 +1327,7 @@
   public:
     static void Apply(const std::string& uri)
     {
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
+#if HAS_ORTHANC_PLUGINS_CHUNKED_HTTP_SERVER == 1
       OrthancPluginRegisterChunkedRestCallback(
         GetGlobalContext(), uri.c_str(),
         GetHandler == Internals::NullRestCallback         ? NULL : Internals::Protect<GetHandler>,
@@ -1322,25 +1339,25 @@
         Internals::ChunkedRequestReaderFinalize);
 #else
       OrthancPluginRegisterRestCallbackNoLock(
-        GetGlobalContext(), uri.c_str(), 
+        GetGlobalContext(), uri.c_str(),
         Internals::ChunkedRestCompatibility<GetHandler, PostHandler, DeleteHandler, PutHandler>);
 #endif
     }
   };
 
-  
+
 
-#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+#if HAS_ORTHANC_PLUGINS_STORAGE_COMMITMENT_SCP == 1
   class IStorageCommitmentScpHandler : public boost::noncopyable
   {
   public:
     virtual ~IStorageCommitmentScpHandler()
     {
     }
-    
+
     virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
                                                                const std::string& sopInstanceUid) = 0;
-    
+
     static OrthancPluginErrorCode Lookup(OrthancPluginStorageCommitmentFailureReason* target,
                                          void* rawHandler,
                                          const char* sopClassUid,
@@ -1356,14 +1373,14 @@
   private:
     bool toFree_;
 
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
     const OrthancPluginDicomInstance*  instance_;
 #else
     OrthancPluginDicomInstance*  instance_;
 #endif
-    
+
   public:
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
     explicit DicomInstance(const OrthancPluginDicomInstance* instance);
 #else
     explicit DicomInstance(OrthancPluginDicomInstance* instance);
@@ -1443,15 +1460,15 @@
   };
 
 // helper method to convert Http headers from the plugin SDK to a std::map
-void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request);
+  void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request);
 
 // helper method to re-serialize the get arguments from the SDK into a string
-void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request);
+  void SerializeGetArguments(std::string& output, const OrthancPluginHttpRequest* request);
 
 // helper method to convert Get arguments from the plugin SDK to a std::map
-void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request);
+  void GetGetArguments(GetArguments& result, const OrthancPluginHttpRequest* request);
 
-#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+#if HAS_ORTHANC_PLUGINS_WEBDAV == 1
   class IWebDavCollection : public boost::noncopyable
   {
   public:
@@ -1498,7 +1515,7 @@
         return dateTime_;
       }
     };
-  
+
     class FolderInfo
     {
     private:
@@ -1523,7 +1540,7 @@
         return dateTime_;
       }
     };
-  
+
     virtual ~IWebDavCollection()
     {
     }
@@ -1533,7 +1550,7 @@
     virtual bool ListFolder(std::list<FileInfo>& files,
                             std::list<FolderInfo>& subfolders,
                             const std::vector<std::string>& path) = 0;
-  
+
     virtual bool GetFile(std::string& content /* out */,
                          std::string& mime /* out */,
                          std::string& dateTime /* out */,
@@ -1562,7 +1579,7 @@
                              const std::string& javascript);
 
 
-#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+#if HAS_ORTHANC_PLUGINS_GENERIC_CALL_REST_API == 1
   class RestApiClient : public boost::noncopyable
   {
   private:
@@ -1580,7 +1597,7 @@
 
   public:
     RestApiClient();
-    
+
     // used to forward a call from the plugin to the core
     RestApiClient(const char* url,
                   const OrthancPluginHttpRequest* request);
@@ -1662,7 +1679,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_KEY_VALUE_STORES == 1
+#if HAS_ORTHANC_PLUGINS_KEY_VALUE_STORES == 1
   class KeyValueStore : public boost::noncopyable
   {
   public:
@@ -1717,7 +1734,7 @@
 #endif
 
 
-#if HAS_ORTHANC_PLUGIN_QUEUES == 1
+#if HAS_ORTHANC_PLUGINS_QUEUES == 1
   class Queue : public boost::noncopyable
   {
   private:
@@ -1725,7 +1742,7 @@
 
     bool DequeueInternal(std::string& value, OrthancPluginQueueOrigin origin);
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
     bool ReserveInternal(std::string& value, uint64_t& valueId, OrthancPluginQueueOrigin origin, uint32_t releaseTimeout);
 #endif
 
@@ -1748,7 +1765,7 @@
       Enqueue(value.empty() ? NULL : value.c_str(), value.size());
     }
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
     // Use ReserveBack() instead
     ORTHANC_PLUGIN_DEPRECATED
 #endif
@@ -1757,7 +1774,7 @@
       return DequeueInternal(value, OrthancPluginQueueOrigin_Back);
     }
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
     // Use ReserveFront() instead
     ORTHANC_PLUGIN_DEPRECATED
 #endif
@@ -1768,15 +1785,15 @@
 
     uint64_t GetSize();
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
     bool ReserveBack(std::string& value, uint64_t& valueId, uint32_t releaseTimeout);
 #endif
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
     bool ReserveFront(std::string& value, uint64_t& valueId, uint32_t releaseTimeout);
 #endif
 
-#if HAS_ORTHANC_PLUGIN_RESERVE_QUEUE_VALUE == 1
+#if HAS_ORTHANC_PLUGINS_RESERVE_QUEUE_VALUE == 1
     void Acknowledge(uint64_t valueId);
 #endif
   };
--- a/Resources/Orthanc/Plugins/OrthancPluginException.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginException.h	Mon Sep 07 20:58:51 2026 +0200
@@ -10,7 +10,7 @@
  * modify it under the terms of the GNU 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
@@ -41,13 +41,45 @@
 #endif
 
 
-#define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code)                   \
+#if HAS_ORTHANC_EXCEPTION == 1 && defined(__ORTHANC_FILE__)   // the OrthancException class accepts a "details" argument -> add the file and line number
+
+#  include <boost/lexical_cast.hpp>
+
+#  define ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE_HELPER(line) #line
+#  define ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE(line) ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE_HELPER(line)
+#  define ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO_HELPER(errorCode, errorCodeStr) \
+  throw ::Orthanc::OrthancException(                                    \
+    errorCode, "Plugin error code " + errorCodeStr + " triggered from " __ORTHANC_FILE__ ":" \
+    ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE(__LINE__))
+
+#  define ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO(errorCode)      \
+  throw ::Orthanc::OrthancException(                                    \
+    errorCode, #errorCode " triggered from " __ORTHANC_FILE__ ":"       \
+    ORTHANC_PLUGINS_EXCEPTION_STRINGIFY_LINE(__LINE__))
+
+#  define ORTHANC_PLUGINS_THROW_ERROR_CODE(code)                        \
+  ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO_HELPER(                 \
+    static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code),               \
+    boost::lexical_cast<std::string>(code))
+
+#  define ORTHANC_PLUGINS_THROW_EXCEPTION(code)                         \
+  ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO_HELPER(                 \
+    ORTHANC_PLUGINS_GET_ERROR_CODE(code),                               \
+    boost::lexical_cast<std::string>(ORTHANC_PLUGINS_GET_ERROR_CODE(code)))
+
+#else // the PluginException does not accept a "details" argument
+
+#  define ORTHANC_PLUGINS_THROW_ERROR_CODE(code)                        \
   throw ORTHANC_PLUGINS_EXCEPTION_CLASS(static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code));
 
+#  define ORTHANC_PLUGINS_THROW_WITH_FILE_AND_LINE_INFO(errorCode)      \
+  ORTHANC_PLUGINS_THROW_ERROR_CODE(errorCode)
 
-#define ORTHANC_PLUGINS_THROW_EXCEPTION(code)                           \
+#  define ORTHANC_PLUGINS_THROW_EXCEPTION(code)                         \
   throw ORTHANC_PLUGINS_EXCEPTION_CLASS(ORTHANC_PLUGINS_GET_ERROR_CODE(code));
-                                                  
+
+#endif
+
 
 #define ORTHANC_PLUGINS_CHECK_ERROR(code)                           \
   if (code != ORTHANC_PLUGINS_GET_ERROR_CODE(Success))              \
--- a/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake	Mon Sep 07 20:58:51 2026 +0200
@@ -9,7 +9,7 @@
 # modify it under the terms of the GNU 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Orthanc/Sdk-1.12.9/orthanc/OrthancCPlugin.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,10711 @@
+/**
+ * \mainpage
+ *
+ * This C/C++ SDK allows external developers to create plugins that
+ * can be loaded into Orthanc to extend its functionality. Each
+ * Orthanc plugin must expose 4 public functions with the following
+ * signatures:
+ * 
+ * -# <tt>int32_t OrthancPluginInitialize(const OrthancPluginContext* context)</tt>:
+ *    This function is invoked by Orthanc when it loads the plugin on startup.
+ *    The plugin must:
+ *    - Check its compatibility with the Orthanc version using
+ *      ::OrthancPluginCheckVersion().
+ *    - Store the context pointer so that it can use the plugin 
+ *      services of Orthanc.
+ *    - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback().
+ *    - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
+ *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
+ *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea3().
+ *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV4().
+ *    - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback().
+ *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
+ *    - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback().
+ *    - Possibly register a custom decoder for DICOM images using OrthancPluginRegisterDecodeImageCallback().
+ *    - Possibly register a callback to filter incoming HTTP requests using OrthancPluginRegisterIncomingHttpRequestFilter2().
+ *    - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer().
+ *    - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback().
+ *    - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback().
+ *    - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback().
+ *    - Possibly register a callback to keep/discard/modify incoming DICOM instances using OrthancPluginRegisterReceivedInstanceCallback().
+ *    - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback().
+ *    - Possibly register a callback to discard instances received through DICOM C-STORE using OrthancPluginRegisterIncomingCStoreInstanceFilter().
+ *    - Possibly register a callback to branch a WebDAV virtual filesystem using OrthancPluginRegisterWebDavCollection().
+ *    - Possibly register a callback to authenticate HTTP requests using OrthancPluginRegisterHttpAuthentication().
+ *    - Possibly register a callback to store audit logs using OrthancPluginRegisterAuditLogHandler().
+ * -# <tt>void OrthancPluginFinalize()</tt>:
+ *    This function is invoked by Orthanc during its shutdown. The plugin
+ *    must free all its memory.
+ * -# <tt>const char* OrthancPluginGetName()</tt>:
+ *    The plugin must return a short string to identify itself.
+ * -# <tt>const char* OrthancPluginGetVersion()</tt>:
+ *    The plugin must return a string containing its version number.
+ *
+ * The name and the version of a plugin is only used to prevent it
+ * from being loaded twice. Note that, in C++, it is mandatory to
+ * declare these functions within an <tt>extern "C"</tt> section.
+ * 
+ * To ensure multi-threading safety, the various REST callbacks are
+ * guaranteed to be executed in mutual exclusion since Orthanc
+ * 0.8.5. If this feature is undesired (notably when developing
+ * high-performance plugins handling simultaneous requests), use
+ * ::OrthancPluginRegisterRestCallbackNoLock().
+ **/
+
+
+
+/**
+ * @defgroup Images Images and compression
+ * @brief Functions to deal with images and compressed buffers.
+ *
+ * @defgroup REST REST
+ * @brief Functions to answer REST requests in a callback.
+ *
+ * @defgroup Callbacks Callbacks
+ * @brief Functions to register and manage callbacks by the plugins.
+ *
+ * @defgroup DicomCallbacks DicomCallbacks
+ * @brief Functions to register and manage DICOM callbacks (worklists, C-FIND, C-MOVE, storage commitment).
+ *
+ * @defgroup Orthanc Orthanc
+ * @brief Functions to access the content of the Orthanc server.
+ *
+ * @defgroup DicomInstance DicomInstance
+ * @brief Functions to access DICOM images that are managed by the Orthanc core.
+ **/
+
+
+
+/**
+ * @defgroup Toolbox Toolbox
+ * @brief Generic functions to help with the creation of plugins.
+ **/
+
+
+
+/**
+ * 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-2025 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+
+#pragma once
+
+
+#include <stdio.h>
+#include <string.h>
+
+#ifdef _WIN32
+#  define ORTHANC_PLUGINS_API __declspec(dllexport)
+#elif __GNUC__ >= 4
+#  define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default")))
+#else
+#  define ORTHANC_PLUGINS_API
+#endif
+
+#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  9
+
+
+#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
+#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision)        \
+  (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major ||                      \
+   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&                    \
+    (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||                    \
+     (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&                  \
+      ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision))))
+#endif
+
+
+
+/********************************************************************
+ ** Check that function inlining is properly supported. The use of
+ ** inlining is required, to avoid the duplication of object code
+ ** between two compilation modules that would use the Orthanc Plugin
+ ** API.
+ ********************************************************************/
+
+/* If the auto-detection of the "inline" keyword below does not work
+   automatically and that your compiler is known to properly support
+   inlining, uncomment the following #define and adapt the definition
+   of "static inline". */
+
+/* #define ORTHANC_PLUGIN_INLINE static inline */
+
+#ifndef ORTHANC_PLUGIN_INLINE
+#  if __STDC_VERSION__ >= 199901L
+/*   This is C99 or above: http://predef.sourceforge.net/prestd.html */
+#    define ORTHANC_PLUGIN_INLINE static inline
+#  elif defined(__cplusplus)
+/*   This is C++ */
+#    define ORTHANC_PLUGIN_INLINE static inline
+#  elif defined(__GNUC__)
+/*   This is GCC running in C89 mode */
+#    define ORTHANC_PLUGIN_INLINE static __inline
+#  elif defined(_MSC_VER)
+/*   This is Visual Studio running in C89 mode */
+#    define ORTHANC_PLUGIN_INLINE static __inline
+#  else
+#    error Your compiler is not known to support the "inline" keyword
+#  endif
+#endif
+
+
+#ifndef ORTHANC_PLUGIN_DEPRECATED
+#  if defined(_MSC_VER)
+#    define ORTHANC_PLUGIN_DEPRECATED __declspec(deprecated)
+#  elif __GNUC__ >= 4
+#    define ORTHANC_PLUGIN_DEPRECATED __attribute__ ((deprecated))
+#  elif defined(__clang__)
+#    define ORTHANC_PLUGIN_DEPRECATED __attribute__ ((deprecated))
+#  else
+#    pragma message("WARNING: You need to implement ORTHANC_PLUGINS_DEPRECATED for this compiler")
+#    define ORTHANC_PLUGIN_DEPRECATED
+#  endif
+#endif
+
+
+#ifndef ORTHANC_PLUGIN_SINCE_SDK
+/**
+ * This macro is used by the code model generator that produces the
+ * "OrthancPluginCodeModel.json" file. The code model is notably used
+ * to generate the Python and Java wrappers. Primitives that are not
+ * tagged with this macro were introduced before Orthanc 1.0.0.
+ **/
+#  if defined(__clang__)
+#    define ORTHANC_PLUGIN_SINCE_SDK(version) __attribute__ ((annotate("ORTHANC_PLUGIN_SINCE_SDK " version)))
+#  else
+#    define ORTHANC_PLUGIN_SINCE_SDK(version)
+#  endif
+#endif
+
+
+
+/********************************************************************
+ ** Inclusion of standard libraries.
+ ********************************************************************/
+
+/**
+ * For Microsoft Visual Studio, a compatibility "stdint.h" can be
+ * downloaded at the following URL:
+ * https://orthanc.uclouvain.be/hg/orthanc/raw-file/default/OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h
+ **/
+#include <stdint.h>
+
+#include <stdlib.h>
+
+
+
+/********************************************************************
+ ** Definition of the Orthanc Plugin API.
+ ********************************************************************/
+
+/** @{ */
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * The various error codes that can be returned by the Orthanc core.
+   **/
+  typedef enum
+  {
+    OrthancPluginErrorCode_InternalError = -1    /*!< Internal error */,
+    OrthancPluginErrorCode_Success = 0    /*!< Success */,
+    OrthancPluginErrorCode_Plugin = 1    /*!< Error encountered within the plugin engine */,
+    OrthancPluginErrorCode_NotImplemented = 2    /*!< Not implemented yet */,
+    OrthancPluginErrorCode_ParameterOutOfRange = 3    /*!< Parameter out of range */,
+    OrthancPluginErrorCode_NotEnoughMemory = 4    /*!< The server hosting Orthanc is running out of memory */,
+    OrthancPluginErrorCode_BadParameterType = 5    /*!< Bad type for a parameter */,
+    OrthancPluginErrorCode_BadSequenceOfCalls = 6    /*!< Bad sequence of calls */,
+    OrthancPluginErrorCode_InexistentItem = 7    /*!< Accessing an inexistent item */,
+    OrthancPluginErrorCode_BadRequest = 8    /*!< Bad request */,
+    OrthancPluginErrorCode_NetworkProtocol = 9    /*!< Error in the network protocol */,
+    OrthancPluginErrorCode_SystemCommand = 10    /*!< Error while calling a system command */,
+    OrthancPluginErrorCode_Database = 11    /*!< Error with the database engine */,
+    OrthancPluginErrorCode_UriSyntax = 12    /*!< Badly formatted URI */,
+    OrthancPluginErrorCode_InexistentFile = 13    /*!< Inexistent file */,
+    OrthancPluginErrorCode_CannotWriteFile = 14    /*!< Cannot write to file */,
+    OrthancPluginErrorCode_BadFileFormat = 15    /*!< Bad file format */,
+    OrthancPluginErrorCode_Timeout = 16    /*!< Timeout */,
+    OrthancPluginErrorCode_UnknownResource = 17    /*!< Unknown resource */,
+    OrthancPluginErrorCode_IncompatibleDatabaseVersion = 18    /*!< Incompatible version of the database */,
+    OrthancPluginErrorCode_FullStorage = 19    /*!< The file storage is full */,
+    OrthancPluginErrorCode_CorruptedFile = 20    /*!< Corrupted file (e.g. inconsistent MD5 hash) */,
+    OrthancPluginErrorCode_InexistentTag = 21    /*!< Inexistent tag */,
+    OrthancPluginErrorCode_ReadOnly = 22    /*!< Cannot modify a read-only data structure */,
+    OrthancPluginErrorCode_IncompatibleImageFormat = 23    /*!< Incompatible format of the images */,
+    OrthancPluginErrorCode_IncompatibleImageSize = 24    /*!< Incompatible size of the images */,
+    OrthancPluginErrorCode_SharedLibrary = 25    /*!< Error while using a shared library (plugin) */,
+    OrthancPluginErrorCode_UnknownPluginService = 26    /*!< Plugin invoking an unknown service */,
+    OrthancPluginErrorCode_UnknownDicomTag = 27    /*!< Unknown DICOM tag */,
+    OrthancPluginErrorCode_BadJson = 28    /*!< Cannot parse a JSON document */,
+    OrthancPluginErrorCode_Unauthorized = 29    /*!< Bad credentials were provided to an HTTP request */,
+    OrthancPluginErrorCode_BadFont = 30    /*!< Badly formatted font file */,
+    OrthancPluginErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
+    OrthancPluginErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
+    OrthancPluginErrorCode_EmptyRequest = 33    /*!< The request is empty */,
+    OrthancPluginErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
+    OrthancPluginErrorCode_NullPointer = 35    /*!< Cannot handle a NULL pointer */,
+    OrthancPluginErrorCode_DatabaseUnavailable = 36    /*!< The database is currently not available (probably a transient situation) */,
+    OrthancPluginErrorCode_CanceledJob = 37    /*!< This job was canceled */,
+    OrthancPluginErrorCode_BadGeometry = 38    /*!< Geometry error encountered in Stone */,
+    OrthancPluginErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
+    OrthancPluginErrorCode_DiscontinuedAbi = 40    /*!< Calling a function that has been removed from the Orthanc Framework */,
+    OrthancPluginErrorCode_BadRange = 41    /*!< Incorrect range request */,
+    OrthancPluginErrorCode_DatabaseCannotSerialize = 42    /*!< Database could not serialize access due to concurrent update, the transaction should be retried */,
+    OrthancPluginErrorCode_Revision = 43    /*!< A bad revision number was provided, which might indicate conflict between multiple writers */,
+    OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44    /*!< A main DICOM Tag has been defined multiple times for the same resource level */,
+    OrthancPluginErrorCode_ForbiddenAccess = 45    /*!< Access to a resource is forbidden */,
+    OrthancPluginErrorCode_DuplicateResource = 46    /*!< Duplicate resource */,
+    OrthancPluginErrorCode_IncompatibleConfigurations = 47    /*!< Your configuration file contains configuration that are mutually incompatible */,
+    OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
+    OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
+    OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
+    OrthancPluginErrorCode_SQLiteStatementAlreadyUsed = 1003    /*!< SQLite: This cached statement is already being referred to */,
+    OrthancPluginErrorCode_SQLiteExecute = 1004    /*!< SQLite: Cannot execute a command */,
+    OrthancPluginErrorCode_SQLiteRollbackWithoutTransaction = 1005    /*!< SQLite: Rolling back a nonexistent transaction (have you called Begin()?) */,
+    OrthancPluginErrorCode_SQLiteCommitWithoutTransaction = 1006    /*!< SQLite: Committing a nonexistent transaction */,
+    OrthancPluginErrorCode_SQLiteRegisterFunction = 1007    /*!< SQLite: Unable to register a function */,
+    OrthancPluginErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
+    OrthancPluginErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
+    OrthancPluginErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
+    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bind a value while out of range (serious error) */,
+    OrthancPluginErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
+    OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
+    OrthancPluginErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
+    OrthancPluginErrorCode_SQLiteTransactionBegin = 1015    /*!< SQLite: Cannot start a transaction */,
+    OrthancPluginErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
+    OrthancPluginErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
+    OrthancPluginErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
+    OrthancPluginErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    OrthancPluginErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
+    OrthancPluginErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
+    OrthancPluginErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
+    OrthancPluginErrorCode_PathToExecutable = 2007    /*!< Unable to get the path to the executable */,
+    OrthancPluginErrorCode_MakeDirectory = 2008    /*!< Cannot create a directory */,
+    OrthancPluginErrorCode_BadApplicationEntityTitle = 2009    /*!< An application entity title (AET) cannot be empty or be longer than 16 characters */,
+    OrthancPluginErrorCode_NoCFindHandler = 2010    /*!< No request handler factory for DICOM C-FIND SCP */,
+    OrthancPluginErrorCode_NoCMoveHandler = 2011    /*!< No request handler factory for DICOM C-MOVE SCP */,
+    OrthancPluginErrorCode_NoCStoreHandler = 2012    /*!< No request handler factory for DICOM C-STORE SCP */,
+    OrthancPluginErrorCode_NoApplicationEntityFilter = 2013    /*!< No application entity filter */,
+    OrthancPluginErrorCode_NoSopClassOrInstance = 2014    /*!< DicomUserConnection: Unable to find the SOP class and instance */,
+    OrthancPluginErrorCode_NoPresentationContext = 2015    /*!< DicomUserConnection: No acceptable presentation context for modality */,
+    OrthancPluginErrorCode_DicomFindUnavailable = 2016    /*!< DicomUserConnection: The C-FIND command is not supported by the remote SCP */,
+    OrthancPluginErrorCode_DicomMoveUnavailable = 2017    /*!< DicomUserConnection: The C-MOVE command is not supported by the remote SCP */,
+    OrthancPluginErrorCode_CannotStoreInstance = 2018    /*!< Cannot store an instance */,
+    OrthancPluginErrorCode_CreateDicomNotString = 2019    /*!< Only string values are supported when creating DICOM instances */,
+    OrthancPluginErrorCode_CreateDicomOverrideTag = 2020    /*!< Trying to override a value inherited from a parent module */,
+    OrthancPluginErrorCode_CreateDicomUseContent = 2021    /*!< Use \"Content\" to inject an image into a new DICOM instance */,
+    OrthancPluginErrorCode_CreateDicomNoPayload = 2022    /*!< No payload is present for one instance in the series */,
+    OrthancPluginErrorCode_CreateDicomUseDataUriScheme = 2023    /*!< The payload of the DICOM instance must be specified according to Data URI scheme */,
+    OrthancPluginErrorCode_CreateDicomBadParent = 2024    /*!< Trying to attach a new DICOM instance to an inexistent resource */,
+    OrthancPluginErrorCode_CreateDicomParentIsInstance = 2025    /*!< Trying to attach a new DICOM instance to an instance (must be a series, study or patient) */,
+    OrthancPluginErrorCode_CreateDicomParentEncoding = 2026    /*!< Unable to get the encoding of the parent resource */,
+    OrthancPluginErrorCode_UnknownModality = 2027    /*!< Unknown modality */,
+    OrthancPluginErrorCode_BadJobOrdering = 2028    /*!< Bad ordering of filters in a job */,
+    OrthancPluginErrorCode_JsonToLuaTable = 2029    /*!< Cannot convert the given JSON object to a Lua table */,
+    OrthancPluginErrorCode_CannotCreateLua = 2030    /*!< Cannot create the Lua context */,
+    OrthancPluginErrorCode_CannotExecuteLua = 2031    /*!< Cannot execute a Lua command */,
+    OrthancPluginErrorCode_LuaAlreadyExecuted = 2032    /*!< Arguments cannot be pushed after the Lua function is executed */,
+    OrthancPluginErrorCode_LuaBadOutput = 2033    /*!< The Lua function does not give the expected number of outputs */,
+    OrthancPluginErrorCode_NotLuaPredicate = 2034    /*!< The Lua function is not a predicate (only true/false outputs allowed) */,
+    OrthancPluginErrorCode_LuaReturnsNoString = 2035    /*!< The Lua function does not return a string */,
+    OrthancPluginErrorCode_StorageAreaAlreadyRegistered = 2036    /*!< Another plugin has already registered a custom storage area */,
+    OrthancPluginErrorCode_DatabaseBackendAlreadyRegistered = 2037    /*!< Another plugin has already registered a custom database back-end */,
+    OrthancPluginErrorCode_DatabaseNotInitialized = 2038    /*!< Plugin trying to call the database during its initialization */,
+    OrthancPluginErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
+    OrthancPluginErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
+    OrthancPluginErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
+    OrthancPluginErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
+    OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
+    OrthancPluginErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    OrthancPluginErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
+    OrthancPluginErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
+
+    _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
+  } OrthancPluginErrorCode;
+
+
+  /**
+   * Forward declaration of one of the mandatory functions for Orthanc
+   * plugins.
+   **/
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName();
+
+
+  /**
+   * The various HTTP methods for a REST call.
+   **/
+  typedef enum
+  {
+    OrthancPluginHttpMethod_Get = 1,    /*!< GET request */
+    OrthancPluginHttpMethod_Post = 2,   /*!< POST request */
+    OrthancPluginHttpMethod_Put = 3,    /*!< PUT request */
+    OrthancPluginHttpMethod_Delete = 4, /*!< DELETE request */
+
+    _OrthancPluginHttpMethod_INTERNAL = 0x7fffffff
+  } OrthancPluginHttpMethod;
+
+
+  /**
+   * @brief The parameters of a REST request.
+   * @ingroup Callbacks
+   **/
+  typedef struct
+  {
+    /**
+     * @brief The HTTP method.
+     **/
+    OrthancPluginHttpMethod method;
+
+    /**
+     * @brief The number of groups of the regular expression.
+     **/
+    uint32_t                groupsCount;
+
+    /**
+     * @brief The matched values for the groups of the regular expression.
+     **/
+    const char* const*      groups;
+
+    /**
+     * @brief For a GET request, the number of GET parameters.
+     **/
+    uint32_t                getCount;
+
+    /**
+     * @brief For a GET request, the keys of the GET parameters.
+     **/
+    const char* const*      getKeys;
+
+    /**
+     * @brief For a GET request, the values of the GET parameters.
+     **/
+    const char* const*      getValues;
+
+    /**
+     * @brief For a PUT or POST request, the content of the body.
+     **/
+    const void*             body;
+
+    /**
+     * @brief For a PUT or POST request, the number of bytes of the body.
+     **/
+    uint32_t                bodySize;
+
+
+    /* --------------------------------------------------
+       New in version 0.8.1
+       -------------------------------------------------- */
+
+    /**
+     * @brief The number of HTTP headers.
+     **/
+    uint32_t                headersCount;
+
+    /**
+     * @brief The keys of the HTTP headers (always converted to low-case).
+     **/
+    const char* const*      headersKeys;
+
+    /**
+     * @brief The values of the HTTP headers.
+     **/
+    const char* const*      headersValues;
+
+
+    /* --------------------------------------------------
+       New in version 1.12.9
+       -------------------------------------------------- */
+
+    /**
+     * @brief If a HTTP authentication callback is registered, the
+     * content of the custom payload generated by the callback.
+     **/
+    const void*             authenticationPayload;
+
+    /**
+     * @brief The size of the custom authentication payload (0 if no
+     * authentication callback is registered).
+     **/
+    uint32_t                authenticationPayloadSize;
+
+  } OrthancPluginHttpRequest;
+
+
+  typedef enum
+  {
+    /* Generic services */
+    _OrthancPluginService_LogInfo = 1,
+    _OrthancPluginService_LogWarning = 2,
+    _OrthancPluginService_LogError = 3,
+    _OrthancPluginService_GetOrthancPath = 4,
+    _OrthancPluginService_GetOrthancDirectory = 5,
+    _OrthancPluginService_GetConfigurationPath = 6,
+    _OrthancPluginService_SetPluginProperty = 7,
+    _OrthancPluginService_GetGlobalProperty = 8,
+    _OrthancPluginService_SetGlobalProperty = 9,
+    _OrthancPluginService_GetCommandLineArgumentsCount = 10,
+    _OrthancPluginService_GetCommandLineArgument = 11,
+    _OrthancPluginService_GetExpectedDatabaseVersion = 12,
+    _OrthancPluginService_GetConfiguration = 13,
+    _OrthancPluginService_BufferCompression = 14,
+    _OrthancPluginService_ReadFile = 15,
+    _OrthancPluginService_WriteFile = 16,
+    _OrthancPluginService_GetErrorDescription = 17,
+    _OrthancPluginService_CallHttpClient = 18,
+    _OrthancPluginService_RegisterErrorCode = 19,
+    _OrthancPluginService_RegisterDictionaryTag = 20,
+    _OrthancPluginService_DicomBufferToJson = 21,
+    _OrthancPluginService_DicomInstanceToJson = 22,
+    _OrthancPluginService_CreateDicom = 23,
+    _OrthancPluginService_ComputeMd5 = 24,
+    _OrthancPluginService_ComputeSha1 = 25,
+    _OrthancPluginService_LookupDictionary = 26,
+    _OrthancPluginService_CallHttpClient2 = 27,
+    _OrthancPluginService_GenerateUuid = 28,
+    _OrthancPluginService_RegisterPrivateDictionaryTag = 29,
+    _OrthancPluginService_AutodetectMimeType = 30,
+    _OrthancPluginService_SetMetricsValue = 31,
+    _OrthancPluginService_EncodeDicomWebJson = 32,
+    _OrthancPluginService_EncodeDicomWebXml = 33,
+    _OrthancPluginService_ChunkedHttpClient = 34,    /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_GetTagName = 35,           /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_EncodeDicomWebJson2 = 36,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_EncodeDicomWebXml2 = 37,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_CreateMemoryBuffer = 38,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GenerateRestApiAuthorizationToken = 39,   /* New in Orthanc 1.8.1 */
+    _OrthancPluginService_CreateMemoryBuffer64 = 40, /* New in Orthanc 1.9.0 */
+    _OrthancPluginService_CreateDicom2 = 41,         /* New in Orthanc 1.9.0 */
+    _OrthancPluginService_GetDatabaseServerIdentifier = 42,         /* New in Orthanc 1.11.1 */
+    _OrthancPluginService_SetMetricsIntegerValue = 43,              /* New in Orthanc 1.12.1 */
+    _OrthancPluginService_SetCurrentThreadName = 44,                /* New in Orthanc 1.12.2 */
+    _OrthancPluginService_LogMessage = 45,                          /* New in Orthanc 1.12.4 */
+    _OrthancPluginService_AdoptDicomInstance = 46,                  /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_GetAttachmentCustomData = 47,             /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_SetAttachmentCustomData = 48,             /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_StoreKeyValue = 49,                       /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_DeleteKeyValue = 50,                      /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_GetKeyValue = 51,                         /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_CreateKeysValuesIterator = 52,            /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_FreeKeysValuesIterator = 53,              /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_KeysValuesIteratorNext = 54,              /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_KeysValuesIteratorGetKey = 55,            /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_KeysValuesIteratorGetValue = 56,          /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_EnqueueValue = 57,                        /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_DequeueValue = 58,                        /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_GetQueueSize = 59,                        /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_SetStableStatus = 60,                     /* New in Orthanc 1.12.9 */
+    _OrthancPluginService_EmitAuditLog = 61,                        /* New in Orthanc 1.12.9 */
+
+    /* Registration of callbacks */
+    _OrthancPluginService_RegisterRestCallback = 1000,
+    _OrthancPluginService_RegisterOnStoredInstanceCallback = 1001,
+    _OrthancPluginService_RegisterStorageArea = 1002,
+    _OrthancPluginService_RegisterOnChangeCallback = 1003,
+    _OrthancPluginService_RegisterRestCallbackNoLock = 1004,
+    _OrthancPluginService_RegisterWorklistCallback = 1005,
+    _OrthancPluginService_RegisterDecodeImageCallback = 1006,
+    _OrthancPluginService_RegisterIncomingHttpRequestFilter = 1007,
+    _OrthancPluginService_RegisterFindCallback = 1008,
+    _OrthancPluginService_RegisterMoveCallback = 1009,
+    _OrthancPluginService_RegisterIncomingHttpRequestFilter2 = 1010,
+    _OrthancPluginService_RegisterRefreshMetricsCallback = 1011,
+    _OrthancPluginService_RegisterChunkedRestCallback = 1012,  /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, /* New in Orthanc 1.6.0 */
+    _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014,  /* New in Orthanc 1.6.1 */
+    _OrthancPluginService_RegisterTranscoderCallback = 1015,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_RegisterStorageArea2 = 1016,         /* New in Orthanc 1.9.0 */
+    _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017,  /* New in Orthanc 1.10.0 */
+    _OrthancPluginService_RegisterReceivedInstanceCallback = 1018,  /* New in Orthanc 1.10.0 */
+    _OrthancPluginService_RegisterWebDavCollection = 1019,     /* New in Orthanc 1.10.1 */
+    _OrthancPluginService_RegisterStorageArea3 = 1020,         /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_RegisterHttpAuthentication = 1021,   /* New in Orthanc 1.12.9 */
+    _OrthancPluginService_RegisterAuditLogHandler = 1022,      /* New in Orthanc 1.12.9 */
+
+    /* Sending answers to REST calls */
+    _OrthancPluginService_AnswerBuffer = 2000,
+    _OrthancPluginService_CompressAndAnswerPngImage = 2001,  /* Unused as of Orthanc 0.9.4 */
+    _OrthancPluginService_Redirect = 2002,
+    _OrthancPluginService_SendHttpStatusCode = 2003,
+    _OrthancPluginService_SendUnauthorized = 2004,
+    _OrthancPluginService_SendMethodNotAllowed = 2005,
+    _OrthancPluginService_SetCookie = 2006,
+    _OrthancPluginService_SetHttpHeader = 2007,
+    _OrthancPluginService_StartMultipartAnswer = 2008,
+    _OrthancPluginService_SendMultipartItem = 2009,
+    _OrthancPluginService_SendHttpStatus = 2010,
+    _OrthancPluginService_CompressAndAnswerImage = 2011,
+    _OrthancPluginService_SendMultipartItem2 = 2012,
+    _OrthancPluginService_SetHttpErrorDetails = 2013,
+    _OrthancPluginService_StartStreamAnswer = 2014,
+    _OrthancPluginService_SendStreamChunk = 2015,
+
+    /* Access to the Orthanc database and API */
+    _OrthancPluginService_GetDicomForInstance = 3000,
+    _OrthancPluginService_RestApiGet = 3001,
+    _OrthancPluginService_RestApiPost = 3002,
+    _OrthancPluginService_RestApiDelete = 3003,
+    _OrthancPluginService_RestApiPut = 3004,
+    _OrthancPluginService_LookupPatient = 3005,
+    _OrthancPluginService_LookupStudy = 3006,
+    _OrthancPluginService_LookupSeries = 3007,
+    _OrthancPluginService_LookupInstance = 3008,
+    _OrthancPluginService_LookupStudyWithAccessionNumber = 3009,
+    _OrthancPluginService_RestApiGetAfterPlugins = 3010,
+    _OrthancPluginService_RestApiPostAfterPlugins = 3011,
+    _OrthancPluginService_RestApiDeleteAfterPlugins = 3012,
+    _OrthancPluginService_RestApiPutAfterPlugins = 3013,
+    _OrthancPluginService_ReconstructMainDicomTags = 3014,
+    _OrthancPluginService_RestApiGet2 = 3015,
+    _OrthancPluginService_CallRestApi = 3016,              /* New in Orthanc 1.9.2 */
+
+    /* Access to DICOM instances */
+    _OrthancPluginService_GetInstanceRemoteAet = 4000,
+    _OrthancPluginService_GetInstanceSize = 4001,
+    _OrthancPluginService_GetInstanceData = 4002,
+    _OrthancPluginService_GetInstanceJson = 4003,
+    _OrthancPluginService_GetInstanceSimplifiedJson = 4004,
+    _OrthancPluginService_HasInstanceMetadata = 4005,
+    _OrthancPluginService_GetInstanceMetadata = 4006,
+    _OrthancPluginService_GetInstanceOrigin = 4007,
+    _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008,
+    _OrthancPluginService_HasInstancePixelData = 4009,
+    _OrthancPluginService_CreateDicomInstance = 4010,      /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_FreeDicomInstance = 4011,        /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceFramesCount = 4012,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceRawFrame = 4013,      /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDecodedFrame = 4014,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_TranscodeDicomInstance = 4015,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_SerializeDicomInstance = 4016,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceAdvancedJson = 4017,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDicomWebJson = 4018,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDicomWebXml = 4019,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_LoadDicomInstance = 4020,        /* New in Orthanc 1.12.1 */
+    
+    /* Services for plugins implementing a database back-end */
+    _OrthancPluginService_RegisterDatabaseBackend = 5000,    /* New in Orthanc 0.8.6 */
+    _OrthancPluginService_DatabaseAnswer = 5001,
+    _OrthancPluginService_RegisterDatabaseBackendV2 = 5002,  /* New in Orthanc 0.9.4 */
+    _OrthancPluginService_StorageAreaCreate = 5003,
+    _OrthancPluginService_StorageAreaRead = 5004,
+    _OrthancPluginService_StorageAreaRemove = 5005,
+    _OrthancPluginService_RegisterDatabaseBackendV3 = 5006,  /* New in Orthanc 1.9.2 */
+    _OrthancPluginService_RegisterDatabaseBackendV4 = 5007,  /* New in Orthanc 1.12.0 */
+
+    /* Primitives for handling images */
+    _OrthancPluginService_GetImagePixelFormat = 6000,
+    _OrthancPluginService_GetImageWidth = 6001,
+    _OrthancPluginService_GetImageHeight = 6002,
+    _OrthancPluginService_GetImagePitch = 6003,
+    _OrthancPluginService_GetImageBuffer = 6004,
+    _OrthancPluginService_UncompressImage = 6005,
+    _OrthancPluginService_FreeImage = 6006,
+    _OrthancPluginService_CompressImage = 6007,
+    _OrthancPluginService_ConvertPixelFormat = 6008,
+    _OrthancPluginService_GetFontsCount = 6009,
+    _OrthancPluginService_GetFontInfo = 6010,
+    _OrthancPluginService_DrawText = 6011,
+    _OrthancPluginService_CreateImage = 6012,
+    _OrthancPluginService_CreateImageAccessor = 6013,
+    _OrthancPluginService_DecodeDicomImage = 6014,
+
+    /* Primitives for handling C-Find, C-Move and worklists */
+    _OrthancPluginService_WorklistAddAnswer = 7000,
+    _OrthancPluginService_WorklistMarkIncomplete = 7001,
+    _OrthancPluginService_WorklistIsMatch = 7002,
+    _OrthancPluginService_WorklistGetDicomQuery = 7003,
+    _OrthancPluginService_FindAddAnswer = 7004,
+    _OrthancPluginService_FindMarkIncomplete = 7005,
+    _OrthancPluginService_GetFindQuerySize = 7006,
+    _OrthancPluginService_GetFindQueryTag = 7007,
+    _OrthancPluginService_GetFindQueryTagName = 7008,
+    _OrthancPluginService_GetFindQueryValue = 7009,
+    _OrthancPluginService_CreateFindMatcher = 7010,
+    _OrthancPluginService_FreeFindMatcher = 7011,
+    _OrthancPluginService_FindMatcherIsMatch = 7012,
+
+    /* Primitives for accessing Orthanc Peers (new in 1.4.2) */
+    _OrthancPluginService_GetPeers = 8000,
+    _OrthancPluginService_FreePeers = 8001,
+    _OrthancPluginService_GetPeersCount = 8003,
+    _OrthancPluginService_GetPeerName = 8004,
+    _OrthancPluginService_GetPeerUrl = 8005,
+    _OrthancPluginService_CallPeerApi = 8006,
+    _OrthancPluginService_GetPeerUserProperty = 8007,
+
+    /* Primitives for handling jobs (new in 1.4.2) */
+    _OrthancPluginService_CreateJob = 9000,   /* Deprecated since SDK 1.11.3 */
+    _OrthancPluginService_FreeJob = 9001,
+    _OrthancPluginService_SubmitJob = 9002,
+    _OrthancPluginService_RegisterJobsUnserializer = 9003,
+    _OrthancPluginService_CreateJob2 = 9004,  /* New in SDK 1.11.3 */
+    
+    _OrthancPluginService_INTERNAL = 0x7fffffff
+  } _OrthancPluginService;
+
+
+  typedef enum
+  {
+    _OrthancPluginProperty_Description = 1,
+    _OrthancPluginProperty_RootUri = 2,
+    _OrthancPluginProperty_OrthancExplorer = 3,
+
+    _OrthancPluginProperty_INTERNAL = 0x7fffffff
+  } _OrthancPluginProperty;
+
+
+
+  /**
+   * The memory layout of the pixels of an image.
+   * @ingroup Images
+   **/
+  typedef enum
+  {
+    /**
+     * @brief Graylevel 8bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * one byte.
+     **/
+    OrthancPluginPixelFormat_Grayscale8 = 1,
+
+    /**
+     * @brief Graylevel, unsigned 16bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * two bytes.
+     **/
+    OrthancPluginPixelFormat_Grayscale16 = 2,
+
+    /**
+     * @brief Graylevel, signed 16bpp image.
+     *
+     * The image is graylevel. Each pixel is signed and stored in two
+     * bytes.
+     **/
+    OrthancPluginPixelFormat_SignedGrayscale16 = 3,
+
+    /**
+     * @brief Color image in RGB24 format.
+     *
+     * This format describes a color image. The pixels are stored in 3
+     * consecutive bytes. The memory layout is RGB.
+     **/
+    OrthancPluginPixelFormat_RGB24 = 4,
+
+    /**
+     * @brief Color image in RGBA32 format.
+     *
+     * This format describes a color image. The pixels are stored in 4
+     * consecutive bytes. The memory layout is RGBA.
+     **/
+    OrthancPluginPixelFormat_RGBA32 = 5,
+
+    OrthancPluginPixelFormat_Unknown = 6,   /*!< Unknown pixel format */
+
+    /**
+     * @brief Color image in RGB48 format.
+     *
+     * This format describes a color image. The pixels are stored in 6
+     * consecutive bytes. The memory layout is RRGGBB.
+     **/
+    OrthancPluginPixelFormat_RGB48 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 7,
+
+    /**
+     * @brief Graylevel, unsigned 32bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * four bytes.
+     **/
+    OrthancPluginPixelFormat_Grayscale32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 8,
+
+    /**
+     * @brief Graylevel, floating-point 32bpp image.
+     *
+     * The image is graylevel. Each pixel is floating-point and stored
+     * in four bytes.
+     **/
+    OrthancPluginPixelFormat_Float32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 9,
+
+    /**
+     * @brief Color image in BGRA32 format.
+     *
+     * This format describes a color image. The pixels are stored in 4
+     * consecutive bytes. The memory layout is BGRA.
+     **/
+    OrthancPluginPixelFormat_BGRA32 ORTHANC_PLUGIN_SINCE_SDK("1.3.1") = 10,
+
+    /**
+     * @brief Graylevel, unsigned 64bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * eight bytes.
+     **/
+    OrthancPluginPixelFormat_Grayscale64 ORTHANC_PLUGIN_SINCE_SDK("1.4.0") = 11,
+
+    _OrthancPluginPixelFormat_INTERNAL = 0x7fffffff
+  } OrthancPluginPixelFormat;
+
+
+
+  /**
+   * The content types that are supported by Orthanc plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginContentType_Unknown = 0,             /*!< Unknown content type */
+    OrthancPluginContentType_Dicom = 1,               /*!< DICOM */
+    OrthancPluginContentType_DicomAsJson = 2,         /*!< JSON summary of a DICOM file */
+    OrthancPluginContentType_DicomUntilPixelData ORTHANC_PLUGIN_SINCE_SDK("1.9.2") = 3, /*!< DICOM Header till pixel data */
+
+    _OrthancPluginContentType_INTERNAL = 0x7fffffff
+  } OrthancPluginContentType;
+
+
+
+  /**
+   * The supported types of DICOM resources.
+   **/
+  typedef enum
+  {
+    OrthancPluginResourceType_Patient = 0,     /*!< Patient */
+    OrthancPluginResourceType_Study = 1,       /*!< Study */
+    OrthancPluginResourceType_Series = 2,      /*!< Series */
+    OrthancPluginResourceType_Instance = 3,    /*!< Instance */
+    OrthancPluginResourceType_None = 4,        /*!< Unavailable resource type */
+
+    _OrthancPluginResourceType_INTERNAL = 0x7fffffff
+  } OrthancPluginResourceType;
+
+
+
+  /**
+   * The supported types of changes that can be signaled to the change callback.
+   * Note: This enumeration is not used to store changes in the database!
+   * @ingroup Callbacks
+   **/
+  typedef enum
+  {
+    OrthancPluginChangeType_CompletedSeries = 0,    /*!< Series is now complete */
+    OrthancPluginChangeType_Deleted = 1,            /*!< Deleted resource */
+    OrthancPluginChangeType_NewChildInstance = 2,   /*!< A new instance was added to this resource */
+    OrthancPluginChangeType_NewInstance = 3,        /*!< New instance received */
+    OrthancPluginChangeType_NewPatient = 4,         /*!< New patient created */
+    OrthancPluginChangeType_NewSeries = 5,          /*!< New series created */
+    OrthancPluginChangeType_NewStudy = 6,           /*!< New study created */
+    OrthancPluginChangeType_StablePatient = 7,      /*!< Timeout: No new instance in this patient */
+    OrthancPluginChangeType_StableSeries = 8,       /*!< Timeout: No new instance in this series */
+    OrthancPluginChangeType_StableStudy = 9,        /*!< Timeout: No new instance in this study */
+    OrthancPluginChangeType_OrthancStarted = 10,    /*!< Orthanc has started */
+    OrthancPluginChangeType_OrthancStopped = 11,    /*!< Orthanc is stopping */
+    OrthancPluginChangeType_UpdatedAttachment = 12, /*!< Some user-defined attachment has changed for this resource */
+    OrthancPluginChangeType_UpdatedMetadata = 13,   /*!< Some user-defined metadata has changed for this resource */
+    OrthancPluginChangeType_UpdatedPeers ORTHANC_PLUGIN_SINCE_SDK("1.4.2") = 14,      /*!< The list of Orthanc peers has changed */
+    OrthancPluginChangeType_UpdatedModalities ORTHANC_PLUGIN_SINCE_SDK("1.4.2") = 15, /*!< The list of DICOM modalities has changed */
+    OrthancPluginChangeType_JobSubmitted ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 16,      /*!< New Job submitted */
+    OrthancPluginChangeType_JobSuccess ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 17,        /*!< A Job has completed successfully */
+    OrthancPluginChangeType_JobFailure ORTHANC_PLUGIN_SINCE_SDK("1.7.2") = 18,        /*!< A Job has failed */
+
+    _OrthancPluginChangeType_INTERNAL = 0x7fffffff
+  } OrthancPluginChangeType;
+
+
+  /**
+   * The compression algorithms that are supported by the Orthanc core.
+   * @ingroup Images
+   **/
+  typedef enum
+  {
+    OrthancPluginCompressionType_Zlib = 0,          /*!< Standard zlib compression */
+    OrthancPluginCompressionType_ZlibWithSize = 1,  /*!< zlib, prefixed with uncompressed size (uint64_t) */
+    OrthancPluginCompressionType_Gzip = 2,          /*!< Standard gzip compression */
+    OrthancPluginCompressionType_GzipWithSize = 3,  /*!< gzip, prefixed with uncompressed size (uint64_t) */
+    OrthancPluginCompressionType_None ORTHANC_PLUGIN_SINCE_SDK("1.12.8") = 4,  /*!< No compression (new in Orthanc 1.12.8) */
+
+    _OrthancPluginCompressionType_INTERNAL = 0x7fffffff
+  } OrthancPluginCompressionType;
+
+
+  /**
+   * The image formats that are supported by the Orthanc core.
+   * @ingroup Images
+   **/
+  typedef enum
+  {
+    OrthancPluginImageFormat_Png = 0,    /*!< Image compressed using PNG */
+    OrthancPluginImageFormat_Jpeg = 1,   /*!< Image compressed using JPEG */
+    OrthancPluginImageFormat_Dicom = 2,  /*!< Image compressed using DICOM */
+
+    _OrthancPluginImageFormat_INTERNAL = 0x7fffffff
+  } OrthancPluginImageFormat;
+
+
+  /**
+   * The value representations present in the DICOM standard (version 2013).
+   * @ingroup Toolbox
+   **/
+  typedef enum
+  {
+    OrthancPluginValueRepresentation_AE = 1,   /*!< Application Entity */
+    OrthancPluginValueRepresentation_AS = 2,   /*!< Age String */
+    OrthancPluginValueRepresentation_AT = 3,   /*!< Attribute Tag */
+    OrthancPluginValueRepresentation_CS = 4,   /*!< Code String */
+    OrthancPluginValueRepresentation_DA = 5,   /*!< Date */
+    OrthancPluginValueRepresentation_DS = 6,   /*!< Decimal String */
+    OrthancPluginValueRepresentation_DT = 7,   /*!< Date Time */
+    OrthancPluginValueRepresentation_FD = 8,   /*!< Floating Point Double */
+    OrthancPluginValueRepresentation_FL = 9,   /*!< Floating Point Single */
+    OrthancPluginValueRepresentation_IS = 10,  /*!< Integer String */
+    OrthancPluginValueRepresentation_LO = 11,  /*!< Long String */
+    OrthancPluginValueRepresentation_LT = 12,  /*!< Long Text */
+    OrthancPluginValueRepresentation_OB = 13,  /*!< Other Byte String */
+    OrthancPluginValueRepresentation_OF = 14,  /*!< Other Float String */
+    OrthancPluginValueRepresentation_OW = 15,  /*!< Other Word String */
+    OrthancPluginValueRepresentation_PN = 16,  /*!< Person Name */
+    OrthancPluginValueRepresentation_SH = 17,  /*!< Short String */
+    OrthancPluginValueRepresentation_SL = 18,  /*!< Signed Long */
+    OrthancPluginValueRepresentation_SQ = 19,  /*!< Sequence of Items */
+    OrthancPluginValueRepresentation_SS = 20,  /*!< Signed Short */
+    OrthancPluginValueRepresentation_ST = 21,  /*!< Short Text */
+    OrthancPluginValueRepresentation_TM = 22,  /*!< Time */
+    OrthancPluginValueRepresentation_UI = 23,  /*!< Unique Identifier (UID) */
+    OrthancPluginValueRepresentation_UL = 24,  /*!< Unsigned Long */
+    OrthancPluginValueRepresentation_UN = 25,  /*!< Unknown */
+    OrthancPluginValueRepresentation_US = 26,  /*!< Unsigned Short */
+    OrthancPluginValueRepresentation_UT = 27,  /*!< Unlimited Text */
+
+    _OrthancPluginValueRepresentation_INTERNAL = 0x7fffffff
+  } OrthancPluginValueRepresentation;
+
+
+  /**
+   * The possible output formats for a DICOM-to-JSON conversion.
+   * @ingroup Toolbox
+   * @see OrthancPluginDicomToJson()
+   **/
+  typedef enum
+  {
+    OrthancPluginDicomToJsonFormat_Full = 1,    /*!< Full output, with most details */
+    OrthancPluginDicomToJsonFormat_Short = 2,   /*!< Tags output as hexadecimal numbers */
+    OrthancPluginDicomToJsonFormat_Human = 3,   /*!< Human-readable JSON */
+
+    _OrthancPluginDicomToJsonFormat_INTERNAL = 0x7fffffff
+  } OrthancPluginDicomToJsonFormat;
+
+
+  /**
+   * Flags to customize a DICOM-to-JSON conversion. By default, binary
+   * tags are formatted using Data URI scheme.
+   * @ingroup Toolbox
+   **/
+  typedef enum
+  {
+    OrthancPluginDicomToJsonFlags_None                  = 0,         /*!< Default formatting */
+    OrthancPluginDicomToJsonFlags_IncludeBinary         = (1 << 0),  /*!< Include the binary tags */
+    OrthancPluginDicomToJsonFlags_IncludePrivateTags    = (1 << 1),  /*!< Include the private tags */
+    OrthancPluginDicomToJsonFlags_IncludeUnknownTags    = (1 << 2),  /*!< Include the tags unknown by the dictionary */
+    OrthancPluginDicomToJsonFlags_IncludePixelData      = (1 << 3),  /*!< Include the pixel data */
+    OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),  /*!< Output binary tags as-is, dropping non-ASCII */
+    OrthancPluginDicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),  /*!< Signal binary tags as null values */
+    OrthancPluginDicomToJsonFlags_StopAfterPixelData ORTHANC_PLUGIN_SINCE_SDK("1.9.1") = (1 << 6),  /*!< Stop processing after pixel data (new in 1.9.1) */
+    OrthancPluginDicomToJsonFlags_SkipGroupLengths ORTHANC_PLUGIN_SINCE_SDK("1.9.1")   = (1 << 7),  /*!< Skip tags whose element is zero (new in 1.9.1) */
+
+    _OrthancPluginDicomToJsonFlags_INTERNAL = 0x7fffffff
+  } OrthancPluginDicomToJsonFlags;
+
+
+  /**
+   * Flags for the creation of a DICOM file.
+   * @ingroup Toolbox
+   * @see OrthancPluginCreateDicom()
+   **/
+  typedef enum
+  {
+    OrthancPluginCreateDicomFlags_None ORTHANC_PLUGIN_SINCE_SDK("1.2.0") = 0,  /*!< Default mode */
+    OrthancPluginCreateDicomFlags_DecodeDataUriScheme   = (1 << 0),  /*!< Decode fields encoded using data URI scheme */
+    OrthancPluginCreateDicomFlags_GenerateIdentifiers   = (1 << 1),  /*!< Automatically generate DICOM identifiers */
+
+    _OrthancPluginCreateDicomFlags_INTERNAL = 0x7fffffff
+  } OrthancPluginCreateDicomFlags;
+
+
+  /**
+   * The constraints on the DICOM identifiers that must be supported
+   * by the database plugins.
+   * @deprecated Plugins using OrthancPluginConstraintType will be faster
+   **/
+  typedef enum
+  {
+    OrthancPluginIdentifierConstraint_Equal = 1,           /*!< Equal */
+    OrthancPluginIdentifierConstraint_SmallerOrEqual = 2,  /*!< Less or equal */
+    OrthancPluginIdentifierConstraint_GreaterOrEqual = 3,  /*!< More or equal */
+    OrthancPluginIdentifierConstraint_Wildcard = 4,        /*!< Case-sensitive wildcard matching (with * and ?) */
+
+    _OrthancPluginIdentifierConstraint_INTERNAL = 0x7fffffff
+  } OrthancPluginIdentifierConstraint;
+
+
+  /**
+   * The constraints on the tags (main DICOM tags and identifier tags)
+   * that must be supported by the database plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginConstraintType_Equal = 1,           /*!< Equal */
+    OrthancPluginConstraintType_SmallerOrEqual = 2,  /*!< Less or equal */
+    OrthancPluginConstraintType_GreaterOrEqual = 3,  /*!< More or equal */
+    OrthancPluginConstraintType_Wildcard = 4,        /*!< Wildcard matching */
+    OrthancPluginConstraintType_List = 5,            /*!< List of values */
+
+    _OrthancPluginConstraintType_INTERNAL = 0x7fffffff
+  } OrthancPluginConstraintType;
+
+
+  /**
+   * The origin of a DICOM instance that has been received by Orthanc.
+   **/
+  typedef enum
+  {
+    OrthancPluginInstanceOrigin_Unknown = 1,        /*!< Unknown origin */
+    OrthancPluginInstanceOrigin_DicomProtocol = 2,  /*!< Instance received through DICOM protocol */
+    OrthancPluginInstanceOrigin_RestApi = 3,        /*!< Instance received through REST API of Orthanc */
+    OrthancPluginInstanceOrigin_Plugin = 4,         /*!< Instance added to Orthanc by a plugin */
+    OrthancPluginInstanceOrigin_Lua = 5,            /*!< Instance added to Orthanc by a Lua script */
+    OrthancPluginInstanceOrigin_WebDav ORTHANC_PLUGIN_SINCE_SDK("1.8.0") = 6,  /*!< Instance received through WebDAV (new in 1.8.0) */
+
+    _OrthancPluginInstanceOrigin_INTERNAL = 0x7fffffff
+  } OrthancPluginInstanceOrigin;
+
+
+  /**
+   * The possible status for one single step of a job.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  {
+    OrthancPluginJobStepStatus_Success = 1,   /*!< The job has successfully executed all its steps */
+    OrthancPluginJobStepStatus_Failure = 2,   /*!< The job has failed while executing this step */
+    OrthancPluginJobStepStatus_Continue = 3   /*!< The job has still data to process after this step */
+  } OrthancPluginJobStepStatus;
+
+
+  /**
+   * Explains why the job should stop and release the resources it has
+   * allocated. This is especially important to disambiguate between
+   * the "paused" condition and the "final" conditions (success,
+   * failure, or canceled).
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  {
+    OrthancPluginJobStopReason_Success = 1,  /*!< The job has succeeded */
+    OrthancPluginJobStopReason_Paused = 2,   /*!< The job was paused, and will be resumed later */
+    OrthancPluginJobStopReason_Failure = 3,  /*!< The job has failed, and might be resubmitted later */
+    OrthancPluginJobStopReason_Canceled = 4  /*!< The job was canceled, and might be resubmitted later */
+  } OrthancPluginJobStopReason;
+
+
+  /**
+   * The available types of metrics.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
+  {
+    OrthancPluginMetricsType_Default = 0,   /*!< Default metrics */
+
+    /**
+     * This metrics represents a time duration. Orthanc will keep the
+     * maximum value of the metrics over a sliding window of ten
+     * seconds, which is useful if the metrics is sampled frequently.
+     **/
+    OrthancPluginMetricsType_Timer = 1
+  } OrthancPluginMetricsType;
+  
+
+  /**
+   * The available modes to export a binary DICOM tag into a DICOMweb
+   * JSON or XML document.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
+  {
+    OrthancPluginDicomWebBinaryMode_Ignore = 0,        /*!< Don't include binary tags */
+    OrthancPluginDicomWebBinaryMode_InlineBinary = 1,  /*!< Inline encoding using Base64 */
+    OrthancPluginDicomWebBinaryMode_BulkDataUri = 2    /*!< Use a bulk data URI field */
+  } OrthancPluginDicomWebBinaryMode;
+
+
+  /**
+   * The available values for the Failure Reason (0008,1197) during
+   * storage commitment.
+   * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.6.0")
+  {
+    /**
+     * Success: The DICOM instance is properly stored in the SCP
+     **/
+    OrthancPluginStorageCommitmentFailureReason_Success = 0,
+
+    /**
+     * 0110H: A general failure in processing the operation was encountered
+     **/
+    OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1,
+
+    /**
+     * 0112H: One or more of the elements in the Referenced SOP
+     * Instance Sequence was not available
+     **/
+    OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2,
+
+    /**
+     * 0213H: The SCP does not currently have enough resources to
+     * store the requested SOP Instance(s)
+     **/
+    OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3,
+
+    /**
+     * 0122H: Storage Commitment has been requested for a SOP Instance
+     * with a SOP Class that is not supported by the SCP
+     **/
+    OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4,
+
+    /**
+     * 0119H: The SOP Class of an element in the Referenced SOP
+     * Instance Sequence did not correspond to the SOP class
+     * registered for this SOP Instance at the SCP
+     **/
+    OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5,
+
+    /**
+     * 0131H: The Transaction UID of the Storage Commitment Request is
+     * already in use
+     **/
+    OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6
+  } OrthancPluginStorageCommitmentFailureReason;
+
+
+  /**
+   * The action to be taken after ReceivedInstanceCallback is triggered
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.10.0")
+  {
+    OrthancPluginReceivedInstanceAction_KeepAsIs = 1, /*!< Keep the instance as is */
+    OrthancPluginReceivedInstanceAction_Modify = 2,   /*!< Modify the instance */
+    OrthancPluginReceivedInstanceAction_Discard = 3,  /*!< Discard the instance */
+
+    _OrthancPluginReceivedInstanceAction_INTERNAL = 0x7fffffff
+  } OrthancPluginReceivedInstanceAction;
+
+
+  /**
+   * Mode specifying how to load a DICOM instance.
+   * @see OrthancPluginLoadDicomInstance()
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.1")
+  {
+    /**
+     * Load the whole DICOM file, including pixel data
+     **/
+    OrthancPluginLoadDicomInstanceMode_WholeDicom = 1,
+
+    /**
+     * Load the whole DICOM file until pixel data, which speeds up the
+     * loading
+     **/
+    OrthancPluginLoadDicomInstanceMode_UntilPixelData = 2,
+
+    /**
+     * Load the whole DICOM file until pixel data, and replace pixel
+     * data by an empty tag whose VR (value representation) is the
+     * same as those of the original DICOM file
+     **/
+    OrthancPluginLoadDicomInstanceMode_EmptyPixelData = 3,
+
+    _OrthancPluginLoadDicomInstanceMode_INTERNAL = 0x7fffffff
+  } OrthancPluginLoadDicomInstanceMode;
+
+
+  /**
+   * The log levels supported by Orthanc.
+   *
+   * These values must match those of enumeration "LogLevel" in the
+   * Orthanc Core.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.4")
+  {
+    OrthancPluginLogLevel_Error = 0,    /*!< Error log level */
+    OrthancPluginLogLevel_Warning = 1,  /*!< Warning log level */
+    OrthancPluginLogLevel_Info = 2,     /*!< Info log level */
+    OrthancPluginLogLevel_Trace = 3,    /*!< Trace log level */
+
+    _OrthancPluginLogLevel_INTERNAL = 0x7fffffff
+  } OrthancPluginLogLevel;
+
+
+  /**
+   * The log categories supported by Orthanc.
+   *
+   * These values must match those of enumeration "LogCategory" in the
+   * Orthanc Core.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.4")
+  {
+    OrthancPluginLogCategory_Generic = (1 << 0),  /*!< Generic (default) category */
+    OrthancPluginLogCategory_Plugins = (1 << 1),  /*!< Plugin engine related logs (shall not be used by plugins) */
+    OrthancPluginLogCategory_Http    = (1 << 2),  /*!< HTTP related logs */
+    OrthancPluginLogCategory_Sqlite  = (1 << 3),  /*!< SQLite related logs (shall not be used by plugins) */
+    OrthancPluginLogCategory_Dicom   = (1 << 4),  /*!< DICOM related logs */
+    OrthancPluginLogCategory_Jobs    = (1 << 5),  /*!< jobs related logs */
+    OrthancPluginLogCategory_Lua     = (1 << 6),  /*!< Lua related logs (shall not be used by plugins) */
+
+    _OrthancPluginLogCategory_INTERNAL = 0x7fffffff
+  } OrthancPluginLogCategory;
+
+
+  /**
+   * The store status related to the adoption of a DICOM instance.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  {
+    OrthancPluginStoreStatus_Success = 0,         /*!< The file has been stored/adopted */
+    OrthancPluginStoreStatus_AlreadyStored = 1,   /*!< The file has already been stored/adopted (only if OverwriteInstances is set to false)*/
+    OrthancPluginStoreStatus_Failure = 2,         /*!< The file could not be stored/adopted */
+    OrthancPluginStoreStatus_FilteredOut = 3,     /*!< The file has been filtered out by a Lua script or a plugin */
+    OrthancPluginStoreStatus_StorageFull = 4,     /*!< The storage is full (only if MaximumStorageSize/MaximumPatientCount is set and MaximumStorageMode is Reject)*/
+
+    _OrthancPluginStoreStatus_INTERNAL = 0x7fffffff
+  } OrthancPluginStoreStatus;
+
+
+  /**
+   * The supported modes to remove an element from a queue.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  {
+    OrthancPluginQueueOrigin_Front = 0,    /*!< Dequeue from the front of the queue */
+    OrthancPluginQueueOrigin_Back = 1,     /*!< Dequeue from the back of the queue */
+
+    _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff
+  } OrthancPluginQueueOrigin;
+
+
+  /**
+   * The "Stable" status of a resource.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.9")
+  {
+    OrthancPluginStableStatus_Stable = 0,     /*!< The resource is stable */
+    OrthancPluginStableStatus_Unstable = 1,   /*!< The resource is unstable */
+
+    _OrthancPluginStableStatus_INTERNAL = 0x7fffffff
+  } OrthancPluginStableStatus;
+
+
+  /**
+   * Status associated with the authentication of a HTTP request.
+   **/
+  typedef enum ORTHANC_PLUGIN_SINCE_SDK("1.12.9")
+  {
+    OrthancPluginHttpAuthenticationStatus_Granted = 0,       /*!< The authentication has been granted */
+    OrthancPluginHttpAuthenticationStatus_Unauthorized = 1,  /*!< The authentication has failed (401 HTTP status) */
+    OrthancPluginHttpAuthenticationStatus_Forbidden = 2,     /*!< The authorization has failed (403 HTTP status) */
+    OrthancPluginHttpAuthenticationStatus_Redirect = 3,      /*!< Redirect to another path (307 HTTP status, e.g., for login) */
+
+    _OrthancPluginHttpAuthenticationStatus_INTERNAL = 0x7fffffff
+  } OrthancPluginHttpAuthenticationStatus;
+
+
+  /**
+   * @brief A 32-bit memory buffer allocated by the core system of Orthanc.
+   *
+   * A memory buffer allocated by the core system of Orthanc. When the
+   * content of the buffer is not useful anymore, it must be free by a
+   * call to ::OrthancPluginFreeMemoryBuffer().
+   **/
+  typedef struct
+  {
+    /**
+     * @brief The content of the buffer.
+     **/
+    void*      data;
+
+    /**
+     * @brief The number of bytes in the buffer.
+     **/
+    uint32_t   size;
+  } OrthancPluginMemoryBuffer;
+
+
+
+  /**
+   * @brief A 64-bit memory buffer allocated by the core system of Orthanc.
+   *
+   * A memory buffer allocated by the core system of Orthanc. When the
+   * content of the buffer is not useful anymore, it must be free by a
+   * call to ::OrthancPluginFreeMemoryBuffer64().
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.9.0") typedef struct
+  {
+    /**
+     * @brief The content of the buffer.
+     **/
+    void*      data;
+
+    /**
+     * @brief The number of bytes in the buffer.
+     **/
+    uint64_t   size;
+  } OrthancPluginMemoryBuffer64;
+
+
+
+
+  /**
+   * @brief Opaque structure that represents the HTTP connection to the client application.
+   * @ingroup Callbacks
+   **/
+  typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput;
+
+
+
+  /**
+   * @brief Opaque structure that represents a DICOM instance that is managed by the Orthanc core.
+   * @ingroup DicomInstance
+   **/
+  typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance;
+
+
+
+  /**
+   * @brief Opaque structure that represents an image that is uncompressed in memory.
+   * @ingroup Images
+   **/
+  typedef struct _OrthancPluginImage_t OrthancPluginImage;
+
+
+
+  /**
+   * @brief Opaque structure that represents the storage area that is actually used by Orthanc.
+   * @ingroup Images
+   **/
+  typedef struct _OrthancPluginStorageArea_t OrthancPluginStorageArea;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents a C-Find query for worklists.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginWorklistQuery_t OrthancPluginWorklistQuery;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginWorklistAnswers_t OrthancPluginWorklistAnswers;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents a C-Find query.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+    _OrthancPluginFindQuery_t OrthancPluginFindQuery;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+    _OrthancPluginFindAnswers_t OrthancPluginFindAnswers;
+
+
+
+  /**
+   * @brief Opaque structure to an object that can be used to check whether a DICOM instance matches a C-Find query.
+   * @ingroup Toolbox
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.2.0")
+    _OrthancPluginFindMatcher_t OrthancPluginFindMatcher;
+
+
+  
+  /**
+   * @brief Opaque structure to the set of remote Orthanc Peers that are known to the local Orthanc server.
+   * @ingroup Toolbox
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+    _OrthancPluginPeers_t OrthancPluginPeers;
+
+
+
+  /**
+   * @brief Opaque structure to a job to be executed by Orthanc.
+   * @ingroup Toolbox
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+    _OrthancPluginJob_t OrthancPluginJob;
+
+
+
+  /**
+   * @brief Opaque structure that represents a node in a JSON or XML
+   * document used in DICOMweb.
+   * @ingroup Toolbox
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
+    _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode;
+
+  
+
+  /**
+   * @brief Signature of a callback function that answers to a REST request.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginRestCallback) (
+    OrthancPluginRestOutput* output,
+    const char* uri,
+    const OrthancPluginHttpRequest* request);
+
+
+
+  /**
+   * @brief Signature of a callback function that is triggered when Orthanc stores a new DICOM instance.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginOnStoredInstanceCallback) (
+    const OrthancPluginDicomInstance* instance,
+    const char* instanceId);
+
+
+
+  /**
+   * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginOnChangeCallback) (
+    OrthancPluginChangeType changeType,
+    OrthancPluginResourceType resourceType,
+    const char* resourceId);
+
+
+
+  /**
+   * @brief Signature of a callback function to decode a DICOM instance as an image.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginDecodeImageCallback) (
+    OrthancPluginImage** target,
+    const void* dicom,
+    const uint32_t size,
+    uint32_t frameIndex);
+
+
+
+  /**
+   * @brief Signature of a function to free dynamic memory.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginFree) (void* buffer);
+
+
+
+  /**
+   * @brief Signature of a function to set the content of a node
+   * encoding a binary DICOM tag, into a JSON or XML document
+   * generated for DICOMweb.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebSetBinaryNode) (
+    OrthancPluginDicomWebNode*       node,
+    OrthancPluginDicomWebBinaryMode  mode,
+    const char*                      bulkDataUri);
+    
+
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param uuid The UUID of the file.
+   * @param content The content of the file.
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate) (
+    const char* uuid,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc reads a file from the storage area.
+   *
+   * @param content The content of the file (output).
+   * @param size The size of the file (output).
+   * @param uuid The UUID of the file of interest.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   *
+   * @warning The "content" buffer *must* have been allocated using
+   * the "malloc()" function of your C standard library (i.e. nor
+   * "new[]", neither a pointer to a buffer). The "free()" function of
+   * your C standard library will automatically be invoked on the
+   * "content" pointer.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRead) (
+    void** content,
+    int64_t* size,
+    const char* uuid,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading a whole file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a whole file from the storage area.
+   *
+   * @param target Memory buffer where to store the content of the file. It must be allocated by the
+   * plugin using OrthancPluginCreateMemoryBuffer64(). The core of Orthanc will free it.
+   * @param uuid The UUID of the file of interest.
+   * @param type The content type corresponding to this file. 
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadWhole) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading a range of a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a portion of a file from the storage area. Orthanc
+   * indicates the start position and the length of the range.
+   *
+   * @param target Memory buffer where to store the content of the range.
+   * The memory buffer is allocated and freed by Orthanc. The length of the range
+   * of interest corresponds to the size of this buffer.
+   * @param uuid The UUID of the file of interest.
+   * @param type The content type corresponding to this file. 
+   * @param rangeStart Start position of the requested range in the file.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    OrthancPluginContentType type,
+    uint64_t rangeStart);
+
+
+
+  /**
+   * @brief Callback for removing a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area.
+   *
+   * @param uuid The UUID of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove) (
+    const char* uuid,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param customData Custom, plugin-specific data associated with the attachment (out).
+   * It must be allocated by the plugin using OrthancPluginCreateMemoryBuffer(). The core of Orthanc will free it.
+   * If the plugin does not generate custom data, leave `customData` unchanged; it will default to an empty value.
+   * @param uuid The UUID of the file.
+   * @param content The content of the file (might be compressed data).
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file.
+   * @param compressionType The compression algorithm that was used to encode `content`
+   * (the absence of compression is indicated using `OrthancPluginCompressionType_None`).
+   * @param dicomInstance The DICOM instance being stored. Equals `NULL` if not storing a DICOM instance.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate2) (
+    OrthancPluginMemoryBuffer* customData,
+    const char* uuid,
+    const void* content,
+    uint64_t size,
+    OrthancPluginContentType type,
+    OrthancPluginCompressionType compressionType,
+    const OrthancPluginDicomInstance* dicomInstance);
+
+
+
+  /**
+   * @brief Callback for reading a range of a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * reads a portion of a file from the storage area. Orthanc
+   * indicates the start position and the length of the range.
+   *
+   * @param target Memory buffer where to store the content of the range.
+   * The memory buffer is allocated and freed by Orthanc. The length of the range
+   * of interest corresponds to the size of this buffer.
+   * @param uuid The UUID of the file of interest.
+   * @param type The content type corresponding to this file.
+   * @param rangeStart Start position of the requested range in the file.
+   * @param customData The custom data of the file of interest.
+   * @param customDataSize The size of the custom data.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageReadRange2) (
+    OrthancPluginMemoryBuffer64* target,
+    const char* uuid,
+    OrthancPluginContentType type,
+    uint64_t rangeStart,
+    const void* customData,
+    uint32_t customDataSize);
+
+
+
+  /**
+   * @brief Callback for removing a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * deletes a file from the storage area.
+   *
+   * @param uuid The UUID of the file to be removed.
+   * @param type The content type corresponding to this file.
+   * @param customData The custom data of the file to be removed.
+   * @param customDataSize The size of the custom data.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove2) (
+    const char* uuid,
+    OrthancPluginContentType type,
+    const void* customData,
+    uint32_t customDataSize);
+
+
+  /**
+   * @brief Callback to handle the C-Find SCP requests for worklists.
+   *
+   * Signature of a callback function that is triggered when Orthanc
+   * receives a C-Find SCP request against modality worklists.
+   *
+   * @param answers The target structure where answers must be stored.
+   * @param query The worklist query.
+   * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates.
+   * @param calledAet The Application Entity Title (AET) of the modality that is called by the request.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWorklistCallback) (
+    OrthancPluginWorklistAnswers*     answers,
+    const OrthancPluginWorklistQuery* query,
+    const char*                       issuerAet,
+    const char*                       calledAet);
+
+
+
+  /**
+   * @brief Callback to filter incoming HTTP requests received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives an HTTP/REST request, and that answers whether
+   * this request should be allowed. If the callback returns "0"
+   * ("false"), the server answers with HTTP status code 403
+   * (Forbidden).
+   *
+   * Pay attention to the fact that this function may be invoked
+   * concurrently by different threads of the Web server of
+   * Orthanc. You must implement proper locking if applicable.
+   *
+   * @param method The HTTP method used by the request.
+   * @param uri The URI of interest.
+   * @param ip The IP address of the HTTP client.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys The keys of the HTTP headers (always converted to low-case).
+   * @param headersValues The values of the HTTP headers.
+   * @return 0 if forbidden access, 1 if allowed access, -1 if error.
+   * @ingroup Callbacks
+   * @deprecated Please instead use OrthancPluginIncomingHttpRequestFilter2()
+   **/
+  typedef int32_t (*OrthancPluginIncomingHttpRequestFilter) (
+    OrthancPluginHttpMethod  method,
+    const char*              uri,
+    const char*              ip,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues);
+
+
+
+  /**
+   * @brief Callback to filter incoming HTTP requests received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives an HTTP/REST request, and that answers whether
+   * this request should be allowed. If the callback returns "0"
+   * ("false"), the server answers with HTTP status code 403
+   * (Forbidden).
+   *
+   * Pay attention to the fact that this function may be invoked
+   * concurrently by different threads of the Web server of
+   * Orthanc. You must implement proper locking if applicable.
+   *
+   * Note that if you are using HTTP basic authentication, you can
+   * extract the username from the "Authorization" HTTP header. The
+   * value of that header contains username:pwd encoded in base64.
+   *
+   * @param method The HTTP method used by the request.
+   * @param uri The URI of interest.
+   * @param ip The IP address of the HTTP client.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys The keys of the HTTP headers (always converted to low-case).
+   * @param headersValues The values of the HTTP headers.
+   * @param getArgumentsCount The number of GET arguments (only for the GET HTTP method).
+   * @param getArgumentsKeys The keys of the GET arguments (only for the GET HTTP method).
+   * @param getArgumentsValues The values of the GET arguments (only for the GET HTTP method).
+   * @return 0 if forbidden access, 1 if allowed access, -1 if error.
+   * @ingroup Callbacks
+   **/
+  typedef int32_t (*OrthancPluginIncomingHttpRequestFilter2) (
+    OrthancPluginHttpMethod  method,
+    const char*              uri,
+    const char*              ip,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues,
+    uint32_t                 getArgumentsCount,
+    const char* const*       getArgumentsKeys,
+    const char* const*       getArgumentsValues);
+
+
+
+  /**
+   * @brief Callback to handle incoming C-Find SCP requests.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a C-Find SCP request not concerning modality
+   * worklists.
+   *
+   * @param answers The target structure where answers must be stored.
+   * @param query The worklist query.
+   * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates.
+   * @param calledAet The Application Entity Title (AET) of the modality that is called by the request.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginFindCallback) (
+    OrthancPluginFindAnswers*     answers,
+    const OrthancPluginFindQuery* query,
+    const char*                   issuerAet,
+    const char*                   calledAet);
+
+
+
+  /**
+   * @brief Callback to handle incoming C-Move SCP requests.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a C-Move SCP request. The callback receives the
+   * type of the resource of interest (study, series, instance...)
+   * together with the DICOM tags containing its identifiers. In turn,
+   * the plugin must create a driver object that will be responsible
+   * for driving the successive move suboperations.
+   *
+   * @param resourceType The type of the resource of interest. Note
+   * that this might be set to ResourceType_None if the
+   * QueryRetrieveLevel (0008,0052) tag was not provided by the
+   * issuer (i.e. the originator modality).
+   * @param patientId Content of the PatientID (0x0010, 0x0020) tag of the resource of interest. Might be NULL.
+   * @param accessionNumber Content of the AccessionNumber (0x0008, 0x0050) tag. Might be NULL.
+   * @param studyInstanceUid Content of the StudyInstanceUID (0x0020, 0x000d) tag. Might be NULL.
+   * @param seriesInstanceUid Content of the SeriesInstanceUID (0x0020, 0x000e) tag. Might be NULL.
+   * @param sopInstanceUid Content of the SOPInstanceUID (0x0008, 0x0018) tag. Might be NULL.
+   * @param originatorAet The Application Entity Title (AET) of the
+   * modality from which the request originates.
+   * @param sourceAet The Application Entity Title (AET) of the
+   * modality that should send its DICOM files to another modality.
+   * @param targetAet The Application Entity Title (AET) of the
+   * modality that should receive the DICOM files.
+   * @param originatorId The Message ID issued by the originator modality,
+   * as found in tag (0000,0110) of the DICOM query emitted by the issuer.
+   *
+   * @return The NULL value if the plugin cannot deal with this query,
+   * or a pointer to the driver object that is responsible for
+   * handling the successive move suboperations.
+   * 
+   * @note If targetAet equals sourceAet, this is actually a query/retrieve operation.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void* (*OrthancPluginMoveCallback) (
+    OrthancPluginResourceType  resourceType,
+    const char*                patientId,
+    const char*                accessionNumber,
+    const char*                studyInstanceUid,
+    const char*                seriesInstanceUid,
+    const char*                sopInstanceUid,
+    const char*                originatorAet,
+    const char*                sourceAet,
+    const char*                targetAet,
+    uint16_t                   originatorId);
+    
+
+  /**
+   * @brief Callback to read the size of a C-Move driver.
+   * 
+   * Signature of a callback function that returns the number of
+   * C-Move suboperations that are to be achieved by the given C-Move
+   * driver. This driver is the return value of a previous call to the
+   * OrthancPluginMoveCallback() callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @return The number of suboperations. 
+   * @ingroup DicomCallbacks
+   **/
+  typedef uint32_t (*OrthancPluginGetMoveSize) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to apply one C-Move suboperation.
+   * 
+   * Signature of a callback function that applies the next C-Move
+   * suboperation that os to be achieved by the given C-Move
+   * driver. This driver is the return value of a previous call to the
+   * OrthancPluginMoveCallback() callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginApplyMove) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to free one C-Move driver.
+   * 
+   * Signature of a callback function that releases the resources
+   * allocated by the given C-Move driver. This driver is the return
+   * value of a previous call to the OrthancPluginMoveCallback()
+   * callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void (*OrthancPluginFreeMove) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to finalize one custom job.
+   * 
+   * Signature of a callback function that releases all the resources
+   * allocated by the given job. This job is the argument provided to
+   * OrthancPluginCreateJob().
+   *
+   * @param job The job of interest.
+   * @ingroup Toolbox
+   **/  
+  typedef void (*OrthancPluginJobFinalize) (void* job);
+
+
+  /**
+   * @brief Callback to check the progress of one custom job.
+   * 
+   * Signature of a callback function that returns the progress of the
+   * job.
+   *
+   * @param job The job of interest.
+   * @return The progress, as a floating-point number ranging from 0 to 1.
+   * @ingroup Toolbox
+   **/  
+  typedef float (*OrthancPluginJobGetProgress) (void* job);
+
+  
+  /**
+   * @brief Callback to retrieve the content of one custom job.
+   * 
+   * Signature of a callback function that returns human-readable
+   * statistics about the job. This statistics must be formatted as a
+   * JSON object. This information is notably displayed in the "Jobs"
+   * tab of "Orthanc Explorer".
+   *
+   * @param job The job of interest.
+   * @return The statistics, as a JSON object encoded as a string.
+   * @ingroup Toolbox
+   * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3.
+   **/  
+  typedef const char* (*OrthancPluginJobGetContent) (void* job);
+
+
+  /**
+   * @brief Callback to retrieve the content of one custom job.
+   *
+   * Signature of a callback function that returns human-readable
+   * statistics about the job. This statistics must be formatted as a
+   * JSON object. This information is notably displayed in the "Jobs"
+   * tab of "Orthanc Explorer".
+   *
+   * @param target The target memory buffer where to store the JSON string.
+   * This buffer must be allocated using OrthancPluginCreateMemoryBuffer()
+   * and will be freed by the Orthanc core.
+   * @param job The job of interest.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginJobGetContent2) (OrthancPluginMemoryBuffer* target,
+                                                                 void* job);
+
+
+  /**
+   * @brief Callback to serialize one custom job.
+   * 
+   * Signature of a callback function that returns a serialized
+   * version of the job, formatted as a JSON object. This
+   * serialization is stored in the Orthanc database, and is used to
+   * reload the job on the restart of Orthanc. The "unserialization"
+   * callback (with OrthancPluginJobsUnserializer signature) will
+   * receive this serialized object.
+   *
+   * @param job The job of interest.
+   * @return The serialized job, as a JSON object encoded as a string.
+   * @see OrthancPluginRegisterJobsUnserializer()
+   * @ingroup Toolbox
+   * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3.
+   **/  
+  typedef const char* (*OrthancPluginJobGetSerialized) (void* job);
+
+
+  /**
+   * @brief Callback to serialize one custom job.
+   *
+   * Signature of a callback function that returns a serialized
+   * version of the job, formatted as a JSON object. This
+   * serialization is stored in the Orthanc database, and is used to
+   * reload the job on the restart of Orthanc. The "unserialization"
+   * callback (with OrthancPluginJobsUnserializer signature) will
+   * receive this serialized object.
+   *
+   * @param target The target memory buffer where to store the JSON string.
+   * This buffer must be allocated using OrthancPluginCreateMemoryBuffer()
+   * and will be freed by the Orthanc core.
+   * @param job The job of interest.
+   * @return 1 if the serialization has succeeded, 0 if serialization is
+   * not implemented for this type of job, or -1 in case of error.
+   **/
+  typedef int32_t (*OrthancPluginJobGetSerialized2) (OrthancPluginMemoryBuffer* target,
+                                                     void* job);
+
+
+  /**
+   * @brief Callback to execute one step of a custom job.
+   * 
+   * Signature of a callback function that executes one step in the
+   * job. The jobs engine of Orthanc will make successive calls to
+   * this method, as long as it returns
+   * OrthancPluginJobStepStatus_Continue.
+   *
+   * @param job The job of interest.
+   * @return The status of execution.
+   * @ingroup Toolbox
+   **/  
+  typedef OrthancPluginJobStepStatus (*OrthancPluginJobStep) (void* job);
+
+
+  /**
+   * @brief Callback executed once one custom job leaves the "running" state.
+   * 
+   * Signature of a callback function that is invoked once a job
+   * leaves the "running" state. This can happen if the previous call
+   * to OrthancPluginJobStep has failed/succeeded, if the host Orthanc
+   * server is being stopped, or if the user manually tags the job as
+   * paused/canceled. This callback allows the plugin to free
+   * resources allocated for running this custom job (e.g. to stop
+   * threads, or to remove temporary files).
+   * 
+   * Note that handling pauses might involves a specific treatment
+   * (such a stopping threads, but keeping temporary files on the
+   * disk). This "paused" situation can be checked by looking at the
+   * "reason" parameter.
+   *
+   * @param job The job of interest.
+   * @param reason The reason for leaving the "running" state. 
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/    
+  typedef OrthancPluginErrorCode (*OrthancPluginJobStop) (void* job, 
+                                                          OrthancPluginJobStopReason reason);
+
+
+  /**
+   * @brief Callback executed once one stopped custom job is started again.
+   * 
+   * Signature of a callback function that is invoked once a job
+   * leaves the "failure/canceled" state, to be started again. This
+   * function will typically reset the progress to zero. Note that
+   * before being actually executed, the job would first be tagged as
+   * "pending" in the Orthanc jobs engine.
+   *
+   * @param job The job of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/    
+  typedef OrthancPluginErrorCode (*OrthancPluginJobReset) (void* job);
+
+
+  /**
+   * @brief Callback executed to unserialize a custom job.
+   * 
+   * Signature of a callback function that unserializes a job that was
+   * saved in the Orthanc database.
+   *
+   * @param jobType The type of the job, as provided to OrthancPluginCreateJob().
+   * @param serialized The serialization of the job, as provided by OrthancPluginJobGetSerialized.
+   * @return The unserialized job (as created by OrthancPluginCreateJob()), or NULL
+   * if this unserializer cannot handle this job type.
+   * @see OrthancPluginRegisterJobsUnserializer()
+   * @ingroup Callbacks
+   **/    
+  typedef OrthancPluginJob* (*OrthancPluginJobsUnserializer) (const char* jobType,
+                                                              const char* serialized);
+  
+
+
+  /**
+   * @brief Callback executed to update the metrics of the plugin.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a monitoring tool (such as Prometheus) asks the current
+   * values of the metrics. This callback gives the plugin a chance to
+   * update its metrics, by calling OrthancPluginSetMetricsValue() or
+   * OrthancPluginSetMetricsIntegerValue().
+   * This is typically useful for metrics that are expensive to
+   * acquire.
+   * 
+   * @see OrthancPluginRegisterRefreshMetrics()
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginRefreshMetricsCallback) ();
+
+  
+
+  /**
+   * @brief Callback executed to encode a binary tag in DICOMweb.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a DICOM tag that contains a binary value must be written
+   * to a JSON or XML node, while a DICOMweb document is being
+   * generated. The value representation (VR) of the DICOM tag can be
+   * OB, OD, OF, OL, OW, or UN.
+   * 
+   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
+   * @param node The node being generated, as provided by Orthanc.
+   * @param setter The setter to be used to encode the content of the node. If
+   * the setter is not called, the binary tag is not written to the output document.
+   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
+   * This parameter gives the number of elements in the "levelTagGroup", 
+   * "levelTagElement", and "levelIndex" arrays.
+   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
+   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
+   * @param levelIndex The index of the node in the parent sequences of the hierarchy.
+   * @param tagGroup The group of the DICOM tag of interest.
+   * @param tagElement The element of the DICOM tag of interest.
+   * @param vr The value representation of the binary DICOM node.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebBinaryCallback) (
+    OrthancPluginDicomWebNode*          node,
+    OrthancPluginDicomWebSetBinaryNode  setter,
+    uint32_t                            levelDepth,
+    const uint16_t*                     levelTagGroup,
+    const uint16_t*                     levelTagElement,
+    const uint32_t*                     levelIndex,
+    uint16_t                            tagGroup,
+    uint16_t                            tagElement,
+    OrthancPluginValueRepresentation    vr);
+
+
+
+  /**
+   * @brief Callback executed to encode a binary tag in DICOMweb.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a DICOM tag that contains a binary value must be written
+   * to a JSON or XML node, while a DICOMweb document is being
+   * generated. The value representation (VR) of the DICOM tag can be
+   * OB, OD, OF, OL, OW, or UN.
+   * 
+   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
+   * @param node The node being generated, as provided by Orthanc.
+   * @param setter The setter to be used to encode the content of the node. If
+   * the setter is not called, the binary tag is not written to the output document.
+   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
+   * This parameter gives the number of elements in the "levelTagGroup", 
+   * "levelTagElement", and "levelIndex" arrays.
+   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
+   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
+   * @param levelIndex The index of the node in the parent sequences of the hierarchy.
+   * @param tagGroup The group of the DICOM tag of interest.
+   * @param tagElement The element of the DICOM tag of interest.
+   * @param vr The value representation of the binary DICOM node.
+   * @param payload The user payload.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebBinaryCallback2) (
+    OrthancPluginDicomWebNode*          node,
+    OrthancPluginDicomWebSetBinaryNode  setter,
+    uint32_t                            levelDepth,
+    const uint16_t*                     levelTagGroup,
+    const uint16_t*                     levelTagElement,
+    const uint32_t*                     levelIndex,
+    uint16_t                            tagGroup,
+    uint16_t                            tagElement,
+    OrthancPluginValueRepresentation    vr,
+    void*                               payload);
+
+
+
+  /**
+   * @brief Data structure that contains information about the Orthanc core.
+   **/
+  typedef struct _OrthancPluginContext_t
+  {
+    void*                     pluginsManager;
+    const char*               orthancVersion;
+    OrthancPluginFree         Free;
+    OrthancPluginErrorCode  (*InvokeService) (struct _OrthancPluginContext_t* context,
+                                              _OrthancPluginService service,
+                                              const void* params);
+  } OrthancPluginContext;
+
+
+  
+  /**
+   * @brief An entry in the dictionary of DICOM tags.
+   **/
+  typedef struct
+  {
+    uint16_t                          group;            /*!< The group of the tag */
+    uint16_t                          element;          /*!< The element of the tag */
+    OrthancPluginValueRepresentation  vr;               /*!< The value representation of the tag */
+    uint32_t                          minMultiplicity;  /*!< The minimum multiplicity of the tag */
+    uint32_t                          maxMultiplicity;  /*!< The maximum multiplicity of the tag (0 means arbitrary) */
+  } OrthancPluginDictionaryEntry;
+
+
+
+  /**
+   * @brief Free a string.
+   * 
+   * Free a string that was allocated by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param str The string to be freed.
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeString(
+    OrthancPluginContext* context, 
+    char* str)
+  {
+    if (str != NULL)
+    {
+      context->Free(str);
+    }
+  }
+
+
+  /**
+   * @brief Check that the version of the hosting Orthanc is above a given version.
+   * 
+   * This function checks whether the version of the Orthanc server
+   * running this plugin, is above the given version. Contrarily to
+   * OrthancPluginCheckVersion(), it is up to the developer of the
+   * plugin to make sure that all the Orthanc SDK services called by
+   * the plugin are actually implemented in the given version of
+   * Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param expectedMajor Expected major version.
+   * @param expectedMinor Expected minor version.
+   * @param expectedRevision Expected revision.
+   * @return 1 if and only if the versions are compatible. If the
+   * result is 0, the initialization of the plugin should fail.
+   * @see OrthancPluginCheckVersion()
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.0")
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginCheckVersionAdvanced(
+    OrthancPluginContext* context,
+    int32_t expectedMajor,
+    int32_t expectedMinor,
+    int32_t expectedRevision)
+  {
+    int32_t major, minor, revision;
+
+    if (sizeof(int) != sizeof(int32_t) || /* Ensure binary compatibility with Orthanc SDK <= 1.12.1 */
+        sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
+        sizeof(int32_t) != sizeof(_OrthancPluginService) ||
+        sizeof(int32_t) != sizeof(_OrthancPluginProperty) ||
+        sizeof(int32_t) != sizeof(OrthancPluginPixelFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginContentType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginResourceType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginChangeType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginCompressionType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginImageFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
+        sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
+        sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
+        sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
+        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginJobStopReason) ||
+        sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) ||
+        sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLogLevel) ||
+        sizeof(int32_t) != sizeof(OrthancPluginLogCategory) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStoreStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginQueueOrigin) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStableStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginHttpAuthenticationStatus))
+    {
+      /* Mismatch in the size of the enumerations */
+      return 0;
+    }
+
+    /* Assume compatibility with the mainline */
+    if (!strcmp(context->orthancVersion, "mainline"))
+    {
+      return 1;
+    }
+
+    /* Parse the version of the Orthanc core */
+    if ( 
+#ifdef _MSC_VER
+      sscanf_s
+#else
+      sscanf
+#endif
+      (context->orthancVersion, "%4d.%4d.%4d", &major, &minor, &revision) != 3)
+    {
+      return 0;
+    }
+
+    /* Check the major number of the version */
+
+    if (major > expectedMajor)
+    {
+      return 1;
+    }
+
+    if (major < expectedMajor)
+    {
+      return 0;
+    }
+
+    /* Check the minor number of the version */
+
+    if (minor > expectedMinor)
+    {
+      return 1;
+    }
+
+    if (minor < expectedMinor)
+    {
+      return 0;
+    }
+
+    /* Check the revision number of the version */
+
+    if (revision >= expectedRevision)
+    {
+      return 1;
+    }
+    else
+    {
+      return 0;
+    }
+  }
+
+
+  /**
+   * @brief Check the compatibility of the plugin wrt. the version of its hosting Orthanc.
+   * 
+   * This function checks whether the version of the Orthanc server
+   * running this plugin, is above the version of the current Orthanc
+   * SDK header. This guarantees that the plugin is compatible with
+   * the hosting Orthanc (i.e. it will not call unavailable services).
+   * The result of this function should always be checked in the
+   * OrthancPluginInitialize() entry point of the plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return 1 if and only if the versions are compatible. If the
+   * result is 0, the initialization of the plugin should fail.
+   * @see OrthancPluginCheckVersionAdvanced()
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginCheckVersion(
+    OrthancPluginContext* context)
+  {
+    return OrthancPluginCheckVersionAdvanced(
+      context,
+      ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+      ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+      ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+  }
+
+
+  /**
+   * @brief Free a memory buffer.
+   * 
+   * Free a memory buffer that was allocated by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer to release.
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeMemoryBuffer(
+    OrthancPluginContext* context, 
+    OrthancPluginMemoryBuffer* buffer)
+  {
+    context->Free(buffer->data);
+  }
+
+
+  /**
+   * @brief Free a memory buffer.
+   * 
+   * Free a memory buffer that was allocated by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer to release.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.9.0")
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeMemoryBuffer64(
+    OrthancPluginContext* context, 
+    OrthancPluginMemoryBuffer64* buffer)
+  {
+    context->Free(buffer->data);
+  }
+
+
+  /**
+   * @brief Log an error.
+   *
+   * Log an error message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogError(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogError, message);
+  }
+
+
+  /**
+   * @brief Log a warning.
+   *
+   * Log a warning message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogWarning(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogWarning, message);
+  }
+
+
+  /**
+   * @brief Log an information.
+   *
+   * Log an information message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogInfo(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogInfo, message);
+  }
+
+
+
+  typedef struct
+  {
+    const char* pathRegularExpression;
+    OrthancPluginRestCallback callback;
+  } _OrthancPluginRestCallback;
+
+  /**
+   * @brief Register a REST callback.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   *
+   * Each REST callback is guaranteed to run in mutual exclusion.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
+   * @param callback The callback function to handle the REST call.
+   * @see OrthancPluginRegisterRestCallbackNoLock()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback(
+    OrthancPluginContext*     context,
+    const char*               pathRegularExpression,
+    OrthancPluginRestCallback callback)
+  {
+    _OrthancPluginRestCallback params;
+    params.pathRegularExpression = pathRegularExpression;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRestCallback, &params);
+  }
+
+
+
+  /**
+   * @brief Register a REST callback, without locking.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   *
+   * Contrarily to OrthancPluginRegisterRestCallback(), the callback
+   * will NOT be invoked in mutual exclusion. This can be useful for
+   * high-performance plugins that must handle concurrent requests
+   * (Orthanc uses a pool of threads, one thread being assigned to
+   * each incoming HTTP request). Of course, if using this function,
+   * it is up to the plugin to implement the required locking
+   * mechanisms.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param pathRegularExpression Regular expression for the URI. May contain groups.
+   * @param callback The callback function to handle the REST call.
+   * @see OrthancPluginRegisterRestCallback()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallbackNoLock(
+    OrthancPluginContext*     context,
+    const char*               pathRegularExpression,
+    OrthancPluginRestCallback callback)
+  {
+    _OrthancPluginRestCallback params;
+    params.pathRegularExpression = pathRegularExpression;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRestCallbackNoLock, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginOnStoredInstanceCallback callback;
+  } _OrthancPluginOnStoredInstanceCallback;
+
+  /**
+   * @brief Register a callback for received instances.
+   *
+   * This function registers a callback function that is called
+   * whenever a new DICOM instance is stored into the Orthanc core.
+   *
+   * @warning Your callback function will be called synchronously with
+   * the core of Orthanc. This implies that deadlocks might emerge if
+   * you call other core primitives of Orthanc in your callback (such
+   * deadlocks are particularly visible in the presence of other plugins
+   * or Lua scripts). It is thus strongly advised to avoid any call to
+   * the REST API of Orthanc in the callback. If you have to call
+   * other primitives of Orthanc, you should make these calls in a
+   * separate thread, passing the pending events to be processed
+   * through a message queue.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnStoredInstanceCallback(
+    OrthancPluginContext*                  context,
+    OrthancPluginOnStoredInstanceCallback  callback)
+  {
+    _OrthancPluginOnStoredInstanceCallback params;
+    params.callback = callback;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterOnStoredInstanceCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const void*              answer;
+    uint32_t                 answerSize;
+    const char*              mimeType;
+  } _OrthancPluginAnswerBuffer;
+
+  /**
+   * @brief Answer to a REST request.
+   *
+   * This function answers to a REST request with the content of a memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the answer.
+   * @param answerSize Number of bytes of the answer.
+   * @param mimeType The MIME type of the answer.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginAnswerBuffer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const void*              answer,
+    uint32_t                 answerSize,
+    const char*              mimeType)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = mimeType;
+    context->InvokeService(context, _OrthancPluginService_AnswerBuffer, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput*  output;
+    OrthancPluginPixelFormat  format;
+    uint32_t                  width;
+    uint32_t                  height;
+    uint32_t                  pitch;
+    const void*               buffer;
+  } _OrthancPluginCompressAndAnswerPngImage;
+
+  typedef struct
+  {
+    OrthancPluginRestOutput*  output;
+    OrthancPluginImageFormat  imageFormat;
+    OrthancPluginPixelFormat  pixelFormat;
+    uint32_t                  width;
+    uint32_t                  height;
+    uint32_t                  pitch;
+    const void*               buffer;
+    uint8_t                   quality;
+  } _OrthancPluginCompressAndAnswerImage;
+
+
+  /**
+   * @brief Answer to a REST request with a PNG image.
+   *
+   * This function answers to a REST request with a PNG image. The
+   * parameters of this function describe a memory buffer that
+   * contains an uncompressed image. The image will be automatically compressed
+   * as a PNG image by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param format The memory layout of the uncompressed image.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @param pitch The pitch of the image (i.e. the number of bytes
+   * between 2 successive lines of the image in the memory buffer).
+   * @param buffer The memory buffer containing the uncompressed image.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerPngImage(
+    OrthancPluginContext*     context,
+    OrthancPluginRestOutput*  output,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height,
+    uint32_t                  pitch,
+    const void*               buffer)
+  {
+    _OrthancPluginCompressAndAnswerImage params;
+    params.output = output;
+    params.imageFormat = OrthancPluginImageFormat_Png;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = 0;  /* No quality for PNG */
+    context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 instanceId;
+  } _OrthancPluginGetDicomForInstance;
+
+  /**
+   * @brief Retrieve a DICOM instance using its Orthanc identifier.
+   * 
+   * Retrieve a DICOM instance using its Orthanc identifier. The DICOM
+   * file is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param instanceId The Orthanc identifier of the DICOM instance of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetDicomForInstance(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 instanceId)
+  {
+    _OrthancPluginGetDicomForInstance params;
+    params.target = target;
+    params.instanceId = instanceId;
+    return context->InvokeService(context, _OrthancPluginService_GetDicomForInstance, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+  } _OrthancPluginRestApiGet;
+
+  /**
+   * @brief Make a GET call to the built-in Orthanc REST API.
+   * 
+   * Make a GET call to the built-in Orthanc REST API. The result to
+   * the query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiGetAfterPlugins()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGet(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri)
+  {
+    _OrthancPluginRestApiGet params;
+    params.target = target;
+    params.uri = uri;
+    return context->InvokeService(context, _OrthancPluginService_RestApiGet, &params);
+  }
+
+
+
+  /**
+   * @brief Make a GET call to the REST API, as tainted by the plugins.
+   * 
+   * Make a GET call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. The result to the
+   * query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiGet()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGetAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri)
+  {
+    _OrthancPluginRestApiGet params;
+    params.target = target;
+    params.uri = uri;
+    return context->InvokeService(context, _OrthancPluginService_RestApiGetAfterPlugins, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+    const void*                 body;
+    uint32_t                    bodySize;
+  } _OrthancPluginRestApiPostPut;
+
+  /**
+   * @brief Make a POST call to the built-in Orthanc REST API.
+   * 
+   * Make a POST call to the built-in Orthanc REST API. The result to
+   * the query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the POST request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiPostAfterPlugins()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPost(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPost, &params);
+  }
+
+
+  /**
+   * @brief Make a POST call to the REST API, as tainted by the plugins.
+   * 
+   * Make a POST call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. The result to the
+   * query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the POST request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiPost()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPostAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPostAfterPlugins, &params);
+  }
+
+
+
+  /**
+   * @brief Make a DELETE call to the built-in Orthanc REST API.
+   * 
+   * Make a DELETE call to the built-in Orthanc REST API.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The URI to delete in the built-in Orthanc API.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiDeleteAfterPlugins()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDelete(
+    OrthancPluginContext*       context,
+    const char*                 uri)
+  {
+    return context->InvokeService(context, _OrthancPluginService_RestApiDelete, uri);
+  }
+
+
+  /**
+   * @brief Make a DELETE call to the REST API, as tainted by the plugins.
+   * 
+   * Make a DELETE call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. 
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The URI to delete in the built-in Orthanc API.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiDelete()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDeleteAfterPlugins(
+    OrthancPluginContext*       context,
+    const char*                 uri)
+  {
+    return context->InvokeService(context, _OrthancPluginService_RestApiDeleteAfterPlugins, uri);
+  }
+
+
+
+  /**
+   * @brief Make a PUT call to the built-in Orthanc REST API.
+   * 
+   * Make a PUT call to the built-in Orthanc REST API. The result to
+   * the query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the PUT request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiPutAfterPlugins()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPut(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPut, &params);
+  }
+
+
+
+  /**
+   * @brief Make a PUT call to the REST API, as tainted by the plugins.
+   * 
+   * Make a PUT call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. The result to the
+   * query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the PUT request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
+   * @see OrthancPluginRestApiPut()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPutAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPutAfterPlugins, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              argument;
+  } _OrthancPluginOutputPlusArgument;
+
+  /**
+   * @brief Redirect a REST request.
+   *
+   * This function answers to a REST request by redirecting the user
+   * to another URI using HTTP status 301.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param redirection Where to redirect.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRedirect(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              redirection)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = redirection;
+    context->InvokeService(context, _OrthancPluginService_Redirect, &params);
+  }
+
+
+
+  typedef struct
+  {
+    char**       result;
+    const char*  argument;
+  } _OrthancPluginRetrieveDynamicString;
+
+  /**
+   * @brief Look for a patient.
+   *
+   * Look for a patient stored in Orthanc, using its Patient ID tag (0x0010, 0x0020).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored patients).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param patientID The Patient ID of interest.
+   * @return The NULL value if the patient is non-existent, or a string containing the 
+   * Orthanc ID of the patient. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupPatient(
+    OrthancPluginContext*  context,
+    const char*            patientID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = patientID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupPatient, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a study.
+   *
+   * Look for a study stored in Orthanc, using its Study Instance UID tag (0x0020, 0x000d).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored studies).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param studyUID The Study Instance UID of interest.
+   * @return The NULL value if the study is non-existent, or a string containing the 
+   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudy(
+    OrthancPluginContext*  context,
+    const char*            studyUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = studyUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupStudy, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a study, using the accession number.
+   *
+   * Look for a study stored in Orthanc, using its Accession Number tag (0x0008, 0x0050).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored studies).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param accessionNumber The Accession Number of interest.
+   * @return The NULL value if the study is non-existent, or a string containing the 
+   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudyWithAccessionNumber(
+    OrthancPluginContext*  context,
+    const char*            accessionNumber)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = accessionNumber;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupStudyWithAccessionNumber, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a series.
+   *
+   * Look for a series stored in Orthanc, using its Series Instance UID tag (0x0020, 0x000e).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored series).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param seriesUID The Series Instance UID of interest.
+   * @return The NULL value if the series is non-existent, or a string containing the 
+   * Orthanc ID of the series. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupSeries(
+    OrthancPluginContext*  context,
+    const char*            seriesUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = seriesUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupSeries, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for an instance.
+   *
+   * Look for an instance stored in Orthanc, using its SOP Instance UID tag (0x0008, 0x0018).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored instances).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param sopInstanceUID The SOP Instance UID of interest.
+   * @return The NULL value if the instance is non-existent, or a string containing the 
+   * Orthanc ID of the instance. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupInstance(
+    OrthancPluginContext*  context,
+    const char*            sopInstanceUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = sopInstanceUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    uint16_t                 status;
+  } _OrthancPluginSendHttpStatusCode;
+
+  /**
+   * @brief Send a HTTP status code.
+   *
+   * This function answers to a REST request by sending a HTTP status
+   * code (such as "400 - Bad Request"). Note that:
+   * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer().
+   * - Redirections (status 301) must use ::OrthancPluginRedirect().
+   * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized().
+   * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param status The HTTP status code to be sent.
+   * @ingroup REST
+   * @see OrthancPluginSendHttpStatus()
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatusCode(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    uint16_t                 status)
+  {
+    _OrthancPluginSendHttpStatusCode params;
+    params.output = output;
+    params.status = status;
+    context->InvokeService(context, _OrthancPluginService_SendHttpStatusCode, &params);
+  }
+
+
+  /**
+   * @brief Signal that a REST request is not authorized.
+   *
+   * This function answers to a REST request by signaling that it is
+   * not authorized.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param realm The realm for the authorization process.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendUnauthorized(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              realm)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = realm;
+    context->InvokeService(context, _OrthancPluginService_SendUnauthorized, &params);
+  }
+
+
+  /**
+   * @brief Signal that this URI does not support this HTTP method.
+   *
+   * This function answers to a REST request by signaling that the
+   * queried URI does not support this method.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param allowedMethods The allowed methods for this URI (e.g. "GET,POST" after a PUT or a POST request).
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendMethodNotAllowed(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              allowedMethods)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = allowedMethods;
+    context->InvokeService(context, _OrthancPluginService_SendMethodNotAllowed, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              key;
+    const char*              value;
+  } _OrthancPluginSetHttpHeader;
+
+  /**
+   * @brief Set a cookie.
+   *
+   * This function sets a cookie in the HTTP client.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param cookie The cookie to be set.
+   * @param value The value of the cookie.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetCookie(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              cookie,
+    const char*              value)
+  {
+    _OrthancPluginSetHttpHeader params;
+    params.output = output;
+    params.key = cookie;
+    params.value = value;
+    context->InvokeService(context, _OrthancPluginService_SetCookie, &params);
+  }
+
+
+  /**
+   * @brief Set some HTTP header.
+   *
+   * This function sets a HTTP header in the HTTP answer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param key The HTTP header to be set.
+   * @param value The value of the HTTP header.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpHeader(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              key,
+    const char*              value)
+  {
+    _OrthancPluginSetHttpHeader params;
+    params.output = output;
+    params.key = key;
+    params.value = value;
+    context->InvokeService(context, _OrthancPluginService_SetHttpHeader, &params);
+  }
+
+
+  typedef struct
+  {
+    char**                             resultStringToFree;
+    const char**                       resultString;
+    int64_t*                           resultInt64;
+    const char*                        key;
+    const OrthancPluginDicomInstance*  instance;
+    OrthancPluginInstanceOrigin*       resultOrigin;   /* New in Orthanc 0.9.5 SDK */
+  } _OrthancPluginAccessDicomInstance;
+
+
+  /**
+   * @brief Get the AET of a DICOM instance.
+   *
+   * This function returns the Application Entity Title (AET) of the
+   * DICOM modality from which a DICOM instance originates.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The AET if success, NULL if error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceRemoteAet, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the size of a DICOM file.
+   *
+   * This function returns the number of bytes of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The size of the file, -1 in case of error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    int64_t size;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &size;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSize, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return size;
+    }
+  }
+
+
+  /**
+   * @brief Get the data of a DICOM file.
+   *
+   * This function returns a pointer to the content of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The pointer to the DICOM data, NULL in case of error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceData, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the DICOM tag hierarchy as a JSON file.
+   *
+   * This function returns a pointer to a newly created string
+   * containing a JSON file. This JSON file encodes the tag hierarchy
+   * of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the JSON file.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the DICOM tag hierarchy as a JSON file (with simplification).
+   *
+   * This function returns a pointer to a newly created string
+   * containing a JSON file. This JSON file encodes the tag hierarchy
+   * of the given DICOM instance. In contrast with
+   * ::OrthancPluginGetInstanceJson(), the returned JSON file is in
+   * its simplified version.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the JSON file.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSimplifiedJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Check whether a DICOM instance is associated with some metadata.
+   *
+   * This function checks whether the DICOM instance of interest is
+   * associated with some metadata. As of Orthanc 0.8.1, in the
+   * callbacks registered by
+   * ::OrthancPluginRegisterOnStoredInstanceCallback(), the only
+   * possibly available metadata are "ReceptionDate", "RemoteAET" and
+   * "IndexInSeries".
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param metadata The metadata of interest.
+   * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginHasInstanceMetadata(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    const char*                        metadata)
+  {
+    int64_t result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &result;
+    params.instance = instance;
+    params.key = metadata;
+
+    if (context->InvokeService(context, _OrthancPluginService_HasInstanceMetadata, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return (result != 0);
+    }
+  }
+
+
+  /**
+   * @brief Get the value of some metadata associated with a given DICOM instance.
+   *
+   * This functions returns the value of some metadata that is associated with the DICOM instance of interest.
+   * Before calling this function, the existence of the metadata must have been checked with
+   * ::OrthancPluginHasInstanceMetadata().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param metadata The metadata of interest.
+   * @return The metadata value if success, NULL if error. Please note that the 
+   *         returned string belongs to the instance object and must NOT be 
+   *         deallocated. Please make a copy of the string if you wish to access 
+   *         it later.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    const char*                        metadata)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+    params.key = metadata;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceMetadata, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginStorageCreate  create;
+    OrthancPluginStorageRead    read;
+    OrthancPluginStorageRemove  remove;
+    OrthancPluginFree           free;
+  } _OrthancPluginRegisterStorageArea;
+
+  /**
+   * @brief Register a custom storage area.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param read The callback function to read a file from the custom storage area.
+   * @param remove The callback function to remove a file from the custom storage area.
+   * @ingroup Callbacks
+   * @deprecated New plugins should use OrthancPluginRegisterStorageArea3()
+   **/
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea(
+    OrthancPluginContext*       context,
+    OrthancPluginStorageCreate  create,
+    OrthancPluginStorageRead    read,
+    OrthancPluginStorageRemove  remove)
+  {
+    _OrthancPluginRegisterStorageArea params;
+    params.create = create;
+    params.read = read;
+    params.remove = remove;
+
+#ifdef  __cplusplus
+    params.free = ::free;
+#else
+    params.free = free;
+#endif
+
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea, &params);
+  }
+
+
+
+  /**
+   * @brief Return the path to the Orthanc executable.
+   *
+   * This function returns the path to the Orthanc executable.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancPath(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetOrthancPath, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the directory containing the Orthanc.
+   *
+   * This function returns the path to the directory containing the Orthanc executable.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancDirectory(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetOrthancDirectory, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the path to the configuration file(s).
+   *
+   * This function returns the path to the configuration file(s) that
+   * was specified when starting Orthanc. Since version 0.9.1, this
+   * path can refer to a folder that stores a set of configuration
+   * files. This function is deprecated in favor of
+   * OrthancPluginGetConfiguration().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   * @see OrthancPluginGetConfiguration()
+   **/
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetConfigurationPath, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginOnChangeCallback callback;
+  } _OrthancPluginOnChangeCallback;
+
+  /**
+   * @brief Register a callback to monitor changes.
+   *
+   * This function registers a callback function that is called
+   * whenever a change happens to some DICOM resource.
+   *
+   * @warning Your callback function will be called synchronously with
+   * the core of Orthanc. This implies that deadlocks might emerge if
+   * you call other core primitives of Orthanc in your callback (such
+   * deadlocks are particularly visible in the presence of other plugins
+   * or Lua scripts). It is thus strongly advised to avoid any call to
+   * the REST API of Orthanc in the callback. If you have to call
+   * other primitives of Orthanc, you should make these calls in a
+   * separate thread, passing the pending events to be processed
+   * through a message queue.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnChangeCallback(
+    OrthancPluginContext*          context,
+    OrthancPluginOnChangeCallback  callback)
+  {
+    _OrthancPluginOnChangeCallback params;
+    params.callback = callback;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterOnChangeCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char* plugin;
+    _OrthancPluginProperty property;
+    const char* value;
+  } _OrthancPluginSetPluginProperty;
+
+
+  /**
+   * @brief Set the URI where the plugin provides its Web interface.
+   *
+   * For plugins that come with a Web interface, this function
+   * declares the entry path where to find this interface. This
+   * information is notably used in the "Plugins" page of Orthanc
+   * Explorer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The root URI for this plugin.
+   *
+   * @deprecated This function should not be used anymore because the
+   * result of the call to "OrthancPluginGetName()" depends on the
+   * system. Use "OrthancPluginSetRootUri2()" instead.
+   **/ 
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri(
+    OrthancPluginContext*  context,
+    const char*            uri)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_RootUri;
+    params.value = uri;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Set the URI where the plugin provides its Web interface.
+   *
+   * For plugins that come with a Web interface, this function
+   * declares the entry path where to find this interface. This
+   * information is notably used in the "Plugins" page of Orthanc
+   * Explorer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()").
+   * @param uri The root URI for this plugin.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri2(
+    OrthancPluginContext*  context,
+    const char*            plugin,
+    const char*            uri)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = plugin;
+    params.property = _OrthancPluginProperty_RootUri;
+    params.value = uri;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Set a description for this plugin.
+   *
+   * Set a description for this plugin. It is displayed in the
+   * "Plugins" page of Orthanc Explorer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param description The description.
+   *
+   * @deprecated This function should not be used anymore because the
+   * result of the call to "OrthancPluginGetName()" depends on the
+   * system. Use "OrthancPluginSetDescription2()" instead.
+   **/ 
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription(
+    OrthancPluginContext*  context,
+    const char*            description)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_Description;
+    params.value = description;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Set a description for this plugin.
+   *
+   * Set a description for this plugin. It is displayed in the
+   * "Plugins" page of Orthanc Explorer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()").
+   * @param description The description.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription2(
+    OrthancPluginContext*  context,
+    const char*            plugin,
+    const char*            description)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = plugin;
+    params.property = _OrthancPluginProperty_Description;
+    params.value = description;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Extend the JavaScript code of Orthanc Explorer.
+   *
+   * Add JavaScript code to customize the default behavior of Orthanc
+   * Explorer. This can for instance be used to add new buttons.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param javascript The custom JavaScript code.
+   *
+   * @deprecated This function should not be used anymore because the
+   * result of the call to "OrthancPluginGetName()" depends on the
+   * system. Use "OrthancPluginExtendOrthancExplorer2()" instead.
+   **/ 
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer(
+    OrthancPluginContext*  context,
+    const char*            javascript)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_OrthancExplorer;
+    params.value = javascript;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Extend the JavaScript code of Orthanc Explorer.
+   *
+   * Add JavaScript code to customize the default behavior of Orthanc
+   * Explorer. This can for instance be used to add new buttons.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param plugin Identifier of your plugin (it must match "OrthancPluginGetName()").
+   * @param javascript The custom JavaScript code.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer2(
+    OrthancPluginContext*  context,
+    const char*            plugin,
+    const char*            javascript)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = plugin;
+    params.property = _OrthancPluginProperty_OrthancExplorer;
+    params.value = javascript;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  typedef struct
+  {
+    char**       result;
+    int32_t      property;
+    const char*  value;
+  } _OrthancPluginGlobalProperty;
+
+
+  /**
+   * @brief Get the value of a global property.
+   *
+   * Get the value of a global property that is stored in the Orthanc database. Global
+   * properties whose index is below 1024 are reserved by Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param property The global property of interest.
+   * @param defaultValue The value to return, if the global property is unset.
+   * @return The value of the global property, or NULL in the case of an error. This
+   * string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetGlobalProperty(
+    OrthancPluginContext*  context,
+    int32_t                property,
+    const char*            defaultValue)
+  {
+    char* result;
+
+    _OrthancPluginGlobalProperty params;
+    params.result = &result;
+    params.property = property;
+    params.value = defaultValue;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetGlobalProperty, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Set the value of a global property.
+   *
+   * Set the value of a global property into the Orthanc
+   * database. Setting a global property can be used by plugins to
+   * save their internal parameters. Plugins are only allowed to set
+   * properties whose index are above or equal to 1024 (properties
+   * below 1024 are read-only and reserved by Orthanc).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param property The global property of interest.
+   * @param value The value to be set in the global property.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetGlobalProperty(
+    OrthancPluginContext*  context,
+    int32_t                property,
+    const char*            value)
+  {
+    _OrthancPluginGlobalProperty params;
+    params.result = NULL;
+    params.property = property;
+    params.value = value;
+
+    return context->InvokeService(context, _OrthancPluginService_SetGlobalProperty, &params);
+  }
+
+
+
+  typedef struct
+  {
+    int32_t   *resultInt32;
+    uint32_t  *resultUint32;
+    int64_t   *resultInt64;
+    uint64_t  *resultUint64;
+  } _OrthancPluginReturnSingleValue;
+
+  /**
+   * @brief Get the number of command-line arguments.
+   *
+   * Retrieve the number of command-line arguments that were used to launch Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The number of arguments.
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetCommandLineArgumentsCount(
+    OrthancPluginContext*  context)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginReturnSingleValue params;
+    memset(&params, 0, sizeof(params));
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgumentsCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+
+  /**
+   * @brief Get the value of a command-line argument.
+   *
+   * Get the value of one of the command-line arguments that were used
+   * to launch Orthanc. The number of available arguments can be
+   * retrieved by OrthancPluginGetCommandLineArgumentsCount().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param argument The index of the argument.
+   * @return The value of the argument, or NULL in the case of an error. This
+   * string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetCommandLineArgument(
+    OrthancPluginContext*  context,
+    uint32_t               argument)
+  {
+    char* result;
+
+    _OrthancPluginGlobalProperty params;
+    params.result = &result;
+    params.property = (int32_t) argument;
+    params.value = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgument, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the expected version of the database schema.
+   *
+   * Retrieve the expected version of the database schema.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The version.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetExpectedDatabaseVersion(
+    OrthancPluginContext*  context)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginReturnSingleValue params;
+    memset(&params, 0, sizeof(params));
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetExpectedDatabaseVersion, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the content of the configuration file(s).
+   *
+   * This function returns the content of the configuration that is
+   * used by Orthanc, formatted as a JSON string.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the configuration. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfiguration(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetConfiguration, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              subType;
+    const char*              contentType;
+  } _OrthancPluginStartMultipartAnswer;
+
+  /**
+   * @brief Start an HTTP multipart answer.
+   *
+   * Initiates a HTTP multipart answer, as the result of a REST request.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param subType The sub-type of the multipart answer ("mixed" or "related").
+   * @param contentType The MIME type of the items in the multipart answer.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginSendMultipartItem(), OrthancPluginSendMultipartItem2()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartMultipartAnswer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              subType,
+    const char*              contentType)
+  {
+    _OrthancPluginStartMultipartAnswer params;
+    params.output = output;
+    params.subType = subType;
+    params.contentType = contentType;
+    return context->InvokeService(context, _OrthancPluginService_StartMultipartAnswer, &params);
+  }
+
+
+  /**
+   * @brief Send an item as a part of some HTTP multipart answer.
+   *
+   * This function sends an item as a part of some HTTP multipart
+   * answer that was initiated by OrthancPluginStartMultipartAnswer().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the item.
+   * @param answerSize Number of bytes of the item.
+   * @return 0 if success, or the error code if failure (this notably happens
+   * if the connection is closed by the client).
+   * @see OrthancPluginSendMultipartItem2()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const void*              answer,
+    uint32_t                 answerSize)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = NULL;
+    return context->InvokeService(context, _OrthancPluginService_SendMultipartItem, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*    target;
+    const void*                   source;
+    uint32_t                      size;
+    OrthancPluginCompressionType  compression;
+    uint8_t                       uncompress;
+  } _OrthancPluginBufferCompression;
+
+
+  /**
+   * @brief Compress or decompress a buffer.
+   *
+   * This function compresses or decompresses a buffer, using the
+   * version of the zlib library that is used by the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param source The source buffer.
+   * @param size The size in bytes of the source buffer.
+   * @param compression The compression algorithm.
+   * @param uncompress If set to "0", the buffer must be compressed. 
+   * If set to "1", the buffer must be uncompressed.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginBufferCompression(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    target,
+    const void*                   source,
+    uint32_t                      size,
+    OrthancPluginCompressionType  compression,
+    uint8_t                       uncompress)
+  {
+    _OrthancPluginBufferCompression params;
+    params.target = target;
+    params.source = source;
+    params.size = size;
+    params.compression = compression;
+    params.uncompress = uncompress;
+
+    return context->InvokeService(context, _OrthancPluginService_BufferCompression, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 path;
+  } _OrthancPluginReadFile;
+
+  /**
+   * @brief Read a file.
+   * 
+   * Read the content of a file on the filesystem, and returns it into
+   * a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param path The path of the file to be read.
+   * @return 0 if success, or the error code if failure.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginReadFile(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 path)
+  {
+    _OrthancPluginReadFile params;
+    params.target = target;
+    params.path = path;
+    return context->InvokeService(context, _OrthancPluginService_ReadFile, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char*  path;
+    const void*  data;
+    uint32_t     size;
+  } _OrthancPluginWriteFile;
+
+  /**
+   * @brief Write a file.
+   * 
+   * Write the content of a memory buffer to the filesystem.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param path The path of the file to be written.
+   * @param data The content of the memory buffer.
+   * @param size The size of the memory buffer.
+   * @return 0 if success, or the error code if failure.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWriteFile(
+    OrthancPluginContext*  context,
+    const char*            path,
+    const void*            data,
+    uint32_t               size)
+  {
+    _OrthancPluginWriteFile params;
+    params.path = path;
+    params.data = data;
+    params.size = size;
+    return context->InvokeService(context, _OrthancPluginService_WriteFile, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char**            target;
+    OrthancPluginErrorCode  error;
+  } _OrthancPluginGetErrorDescription;
+
+  /**
+   * @brief Get the description of a given error code.
+   *
+   * This function returns the description of a given error code.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param error The error code of interest.
+   * @return The error description. This is a statically-allocated
+   * string, do not free it.
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetErrorDescription(
+    OrthancPluginContext*    context,
+    OrthancPluginErrorCode   error)
+  {
+    const char* result = NULL;
+
+    _OrthancPluginGetErrorDescription params;
+    params.target = &result;
+    params.error = error;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetErrorDescription, &params) != OrthancPluginErrorCode_Success ||
+        result == NULL)
+    {
+      return "Unknown error code";
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    uint16_t                 status;
+    const void*              body;
+    uint32_t                 bodySize;
+  } _OrthancPluginSendHttpStatus;
+
+  /**
+   * @brief Send a HTTP status, with a custom body.
+   *
+   * This function answers to a HTTP request by sending a HTTP status
+   * code (such as "400 - Bad Request"), together with a body
+   * describing the error. The body will only be returned if the
+   * configuration option "HttpDescribeErrors" of Orthanc is set to "true".
+   * 
+   * Note that:
+   * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer().
+   * - Redirections (status 301) must use ::OrthancPluginRedirect().
+   * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized().
+   * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param status The HTTP status code to be sent.
+   * @param body The body of the answer.
+   * @param bodySize The size of the body.
+   * @see OrthancPluginSendHttpStatusCode()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatus(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    uint16_t                 status,
+    const void*              body,
+    uint32_t                 bodySize)
+  {
+    _OrthancPluginSendHttpStatus params;
+    params.output = output;
+    params.status = status;
+    params.body = body;
+    params.bodySize = bodySize;
+    context->InvokeService(context, _OrthancPluginService_SendHttpStatus, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const OrthancPluginImage*  image;
+    uint32_t*                  resultUint32;
+    OrthancPluginPixelFormat*  resultPixelFormat;
+    void**                     resultBuffer;
+  } _OrthancPluginGetImageInfo;
+
+
+  /**
+   * @brief Return the pixel format of an image.
+   *
+   * This function returns the type of memory layout for the pixels of the given image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The pixel format.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginPixelFormat  OrthancPluginGetImagePixelFormat(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    OrthancPluginPixelFormat target;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultPixelFormat = &target;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImagePixelFormat, &params) != OrthancPluginErrorCode_Success)
+    {
+      return OrthancPluginPixelFormat_Unknown;
+    }
+    else
+    {
+      return (OrthancPluginPixelFormat) target;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the width of an image.
+   *
+   * This function returns the width of the given image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The width.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImageWidth(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    uint32_t width;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultUint32 = &width;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImageWidth, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return width;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the height of an image.
+   *
+   * This function returns the height of the given image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The height.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImageHeight(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    uint32_t height;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultUint32 = &height;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImageHeight, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return height;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the pitch of an image.
+   *
+   * This function returns the pitch of the given image. The pitch is
+   * defined as the number of bytes between 2 successive lines of the
+   * image in the memory buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The pitch.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImagePitch(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    uint32_t pitch;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultUint32 = &pitch;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImagePitch, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return pitch;
+    }
+  }
+
+
+
+  /**
+   * @brief Return a pointer to the content of an image.
+   *
+   * This function returns a pointer to the memory buffer that
+   * contains the pixels of the image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The pointer.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE void*  OrthancPluginGetImageBuffer(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    void* target = NULL;
+
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.resultBuffer = &target;
+    params.image = image;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImageBuffer, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginImage**       target;
+    const void*                data;
+    uint32_t                   size;
+    OrthancPluginImageFormat   format;
+  } _OrthancPluginUncompressImage;
+
+
+  /**
+   * @brief Decode a compressed image.
+   *
+   * This function decodes a compressed image from a memory buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param data Pointer to a memory buffer containing the compressed image.
+   * @param size Size of the memory buffer containing the compressed image.
+   * @param format The file format of the compressed image.
+   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginUncompressImage(
+    OrthancPluginContext*      context,
+    const void*                data,
+    uint32_t                   size,
+    OrthancPluginImageFormat   format)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginUncompressImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.data = data;
+    params.size = size;
+    params.format = format;
+
+    if (context->InvokeService(context, _OrthancPluginService_UncompressImage, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginImage*   image;
+  } _OrthancPluginFreeImage;
+
+  /**
+   * @brief Free an image.
+   *
+   * This function frees an image that was decoded with OrthancPluginUncompressImage().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeImage(
+    OrthancPluginContext* context, 
+    OrthancPluginImage*   image)
+  {
+    _OrthancPluginFreeImage params;
+    params.image = image;
+
+    context->InvokeService(context, _OrthancPluginService_FreeImage, &params);
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer* target;
+    OrthancPluginImageFormat   imageFormat;
+    OrthancPluginPixelFormat   pixelFormat;
+    uint32_t                   width;
+    uint32_t                   height;
+    uint32_t                   pitch;
+    const void*                buffer;
+    uint8_t                    quality;
+  } _OrthancPluginCompressImage;
+
+
+  /**
+   * @brief Encode a PNG image.
+   *
+   * This function compresses the given memory buffer containing an
+   * image using the PNG specification, and stores the result of the
+   * compression into a newly allocated memory buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param format The memory layout of the uncompressed image.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @param pitch The pitch of the image (i.e. the number of bytes
+   * between 2 successive lines of the image in the memory buffer).
+   * @param buffer The memory buffer containing the uncompressed image.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginCompressAndAnswerPngImage()
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressPngImage(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    target,
+    OrthancPluginPixelFormat      format,
+    uint32_t                      width,
+    uint32_t                      height,
+    uint32_t                      pitch,
+    const void*                   buffer)
+  {
+    _OrthancPluginCompressImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = target;
+    params.imageFormat = OrthancPluginImageFormat_Png;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = 0;  /* Unused for PNG */
+
+    return context->InvokeService(context, _OrthancPluginService_CompressImage, &params);
+  }
+
+
+  /**
+   * @brief Encode a JPEG image.
+   *
+   * This function compresses the given memory buffer containing an
+   * image using the JPEG specification, and stores the result of the
+   * compression into a newly allocated memory buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param format The memory layout of the uncompressed image.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @param pitch The pitch of the image (i.e. the number of bytes
+   * between 2 successive lines of the image in the memory buffer).
+   * @param buffer The memory buffer containing the uncompressed image.
+   * @param quality The quality of the JPEG encoding, between 1 (worst
+   * quality, best compression) and 100 (best quality, worst
+   * compression).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressJpegImage(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    target,
+    OrthancPluginPixelFormat      format,
+    uint32_t                      width,
+    uint32_t                      height,
+    uint32_t                      pitch,
+    const void*                   buffer,
+    uint8_t                       quality)
+  {
+    _OrthancPluginCompressImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = target;
+    params.imageFormat = OrthancPluginImageFormat_Jpeg;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = quality;
+
+    return context->InvokeService(context, _OrthancPluginService_CompressImage, &params);
+  }
+
+
+
+  /**
+   * @brief Answer to a REST request with a JPEG image.
+   *
+   * This function answers to a REST request with a JPEG image. The
+   * parameters of this function describe a memory buffer that
+   * contains an uncompressed image. The image will be automatically compressed
+   * as a JPEG image by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param format The memory layout of the uncompressed image.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @param pitch The pitch of the image (i.e. the number of bytes
+   * between 2 successive lines of the image in the memory buffer).
+   * @param buffer The memory buffer containing the uncompressed image.
+   * @param quality The quality of the JPEG encoding, between 1 (worst
+   * quality, best compression) and 100 (best quality, worst
+   * compression).
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerJpegImage(
+    OrthancPluginContext*     context,
+    OrthancPluginRestOutput*  output,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height,
+    uint32_t                  pitch,
+    const void*               buffer,
+    uint8_t                   quality)
+  {
+    _OrthancPluginCompressAndAnswerImage params;
+    params.output = output;
+    params.imageFormat = OrthancPluginImageFormat_Jpeg;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = quality;
+    context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, &params);
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    OrthancPluginHttpMethod     method;
+    const char*                 url;
+    const char*                 username;
+    const char*                 password;
+    const void*                 body;
+    uint32_t                    bodySize;
+  } _OrthancPluginCallHttpClient;
+
+
+  /**
+   * @brief Issue a HTTP GET call.
+   * 
+   * Make a HTTP GET call to the given URL. The result to the query is
+   * stored into a newly allocated memory buffer. Favor
+   * OrthancPluginRestApiGet() if calling the built-in REST API of the
+   * Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param url The URL of interest.
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpGet(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 url,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = target;
+    params.method = OrthancPluginHttpMethod_Get;
+    params.url = url;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+  /**
+   * @brief Issue a HTTP POST call.
+   * 
+   * Make a HTTP POST call to the given URL. The result to the query
+   * is stored into a newly allocated memory buffer. Favor
+   * OrthancPluginRestApiPost() if calling the built-in REST API of
+   * the Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param url The URL of interest.
+   * @param body The content of the body of the request.
+   * @param bodySize The size of the body of the request.
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPost(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 url,
+    const void*                 body,
+    uint32_t                    bodySize,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = target;
+    params.method = OrthancPluginHttpMethod_Post;
+    params.url = url;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+  /**
+   * @brief Issue a HTTP PUT call.
+   * 
+   * Make a HTTP PUT call to the given URL. The result to the query is
+   * stored into a newly allocated memory buffer. Favor
+   * OrthancPluginRestApiPut() if calling the built-in REST API of the
+   * Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param url The URL of interest.
+   * @param body The content of the body of the request.
+   * @param bodySize The size of the body of the request.
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPut(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 url,
+    const void*                 body,
+    uint32_t                    bodySize,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = target;
+    params.method = OrthancPluginHttpMethod_Put;
+    params.url = url;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+  /**
+   * @brief Issue a HTTP DELETE call.
+   * 
+   * Make a HTTP DELETE call to the given URL. Favor
+   * OrthancPluginRestApiDelete() if calling the built-in REST API of
+   * the Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param url The URL of interest.
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpDelete(
+    OrthancPluginContext*       context,
+    const char*                 url,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.method = OrthancPluginHttpMethod_Delete;
+    params.url = url;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginImage**       target;
+    const OrthancPluginImage*  source;
+    OrthancPluginPixelFormat   targetFormat;
+  } _OrthancPluginConvertPixelFormat;
+
+
+  /**
+   * @brief Change the pixel format of an image.
+   *
+   * This function creates a new image, changing the memory layout of the pixels.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param source The source image.
+   * @param targetFormat The target pixel format.
+   * @return The resulting image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginConvertPixelFormat(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  source,
+    OrthancPluginPixelFormat   targetFormat)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginConvertPixelFormat params;
+    params.target = &target;
+    params.source = source;
+    params.targetFormat = targetFormat;
+
+    if (context->InvokeService(context, _OrthancPluginService_ConvertPixelFormat, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the number of available fonts.
+   *
+   * This function returns the number of fonts that are built in the
+   * Orthanc core. These fonts can be used to draw texts on images
+   * through OrthancPluginDrawText().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The number of fonts.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontsCount(
+    OrthancPluginContext*  context)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginReturnSingleValue params;
+    memset(&params, 0, sizeof(params));
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFontsCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+
+
+  typedef struct
+  {
+    uint32_t      fontIndex; /* in */
+    const char**  name; /* out */
+    uint32_t*     size; /* out */
+  } _OrthancPluginGetFontInfo;
+
+  /**
+   * @brief Return the name of a font.
+   *
+   * This function returns the name of a font that is built in the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
+   * @return The font name. This is a statically-allocated string, do not free it.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetFontName(
+    OrthancPluginContext*  context,
+    uint32_t               fontIndex)
+  {
+    const char* result = NULL;
+
+    _OrthancPluginGetFontInfo params;
+    memset(&params, 0, sizeof(params));
+    params.name = &result;
+    params.fontIndex = fontIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the size of a font.
+   *
+   * This function returns the size of a font that is built in the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
+   * @return The font size.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontSize(
+    OrthancPluginContext*  context,
+    uint32_t               fontIndex)
+  {
+    uint32_t result;
+
+    _OrthancPluginGetFontInfo params;
+    memset(&params, 0, sizeof(params));
+    params.size = &result;
+    params.fontIndex = fontIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginImage*   image;
+    uint32_t              fontIndex;
+    const char*           utf8Text;
+    int32_t               x;
+    int32_t               y;
+    uint8_t               r;
+    uint8_t               g;
+    uint8_t               b;
+  } _OrthancPluginDrawText;
+
+
+  /**
+   * @brief Draw text on an image.
+   *
+   * This function draws some text on some image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image upon which to draw the text.
+   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
+   * @param utf8Text The text to be drawn, encoded as an UTF-8 zero-terminated string.
+   * @param x The X position of the text over the image.
+   * @param y The Y position of the text over the image.
+   * @param r The value of the red color channel of the text.
+   * @param g The value of the green color channel of the text.
+   * @param b The value of the blue color channel of the text.
+   * @return 0 if success, other value if error.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginDrawText(
+    OrthancPluginContext*  context,
+    OrthancPluginImage*    image,
+    uint32_t               fontIndex,
+    const char*            utf8Text,
+    int32_t                x,
+    int32_t                y,
+    uint8_t                r,
+    uint8_t                g,
+    uint8_t                b)
+  {
+    _OrthancPluginDrawText params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.fontIndex = fontIndex;
+    params.utf8Text = utf8Text;
+    params.x = x;
+    params.y = y;
+    params.r = r;
+    params.g = g;
+    params.b = b;
+
+    return context->InvokeService(context, _OrthancPluginService_DrawText, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginStorageArea*   storageArea;
+    const char*                 uuid;
+    const void*                 content;
+    uint64_t                    size;
+    OrthancPluginContentType    type;
+  } _OrthancPluginStorageAreaCreate;
+
+
+  /**
+   * @brief Create a file inside the storage area.
+   *
+   * This function creates a new file inside the storage area that is
+   * currently used by Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storageArea The storage area.
+   * @param uuid The identifier of the file to be created.
+   * @param content The content to store in the newly created file.
+   * @param size The size of the content.
+   * @param type The type of the file content.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiPut()" on
+   * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
+   * @warning This function will result in a "not implemented" error on versions of the
+   * Orthanc core above 1.12.6.
+   **/
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaCreate(
+    OrthancPluginContext*       context,
+    OrthancPluginStorageArea*   storageArea,
+    const char*                 uuid,
+    const void*                 content,
+    uint64_t                    size,
+    OrthancPluginContentType    type)
+  {
+    _OrthancPluginStorageAreaCreate params;
+    params.storageArea = storageArea;
+    params.uuid = uuid;
+    params.content = content;
+    params.size = size;
+    params.type = type;
+
+    return context->InvokeService(context, _OrthancPluginService_StorageAreaCreate, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    OrthancPluginStorageArea*   storageArea;
+    const char*                 uuid;
+    OrthancPluginContentType    type;
+  } _OrthancPluginStorageAreaRead;
+
+
+  /**
+   * @brief Read a file from the storage area.
+   *
+   * This function reads the content of a given file from the storage
+   * area that is currently used by Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param storageArea The storage area.
+   * @param uuid The identifier of the file to be read.
+   * @param type The type of the file content.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiGet()" on
+   * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
+   * @warning This function will result in a "not implemented" error on versions of the
+   * Orthanc core above 1.12.6.
+   **/
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRead(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    OrthancPluginStorageArea*   storageArea,
+    const char*                 uuid,
+    OrthancPluginContentType    type)
+  {
+    _OrthancPluginStorageAreaRead params;
+    params.target = target;
+    params.storageArea = storageArea;
+    params.uuid = uuid;
+    params.type = type;
+
+    return context->InvokeService(context, _OrthancPluginService_StorageAreaRead, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginStorageArea*   storageArea;
+    const char*                 uuid;
+    OrthancPluginContentType    type;
+  } _OrthancPluginStorageAreaRemove;
+
+  /**
+   * @brief Remove a file from the storage area.
+   *
+   * This function removes a given file from the storage area that is
+   * currently used by Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storageArea The storage area.
+   * @param uuid The identifier of the file to be removed.
+   * @param type The type of the file content.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   * @deprecated This function should not be used anymore. Use "OrthancPluginRestApiDelete()" on
+   * "/{patients|studies|series|instances}/{id}/attachments/{name}" instead.
+   * @warning This function will result in a "not implemented" error on versions of the
+   * Orthanc core above 1.12.6.
+   **/
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRemove(
+    OrthancPluginContext*       context,
+    OrthancPluginStorageArea*   storageArea,
+    const char*                 uuid,
+    OrthancPluginContentType    type)
+  {
+    _OrthancPluginStorageAreaRemove params;
+    params.storageArea = storageArea;
+    params.uuid = uuid;
+    params.type = type;
+
+    return context->InvokeService(context, _OrthancPluginService_StorageAreaRemove, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginErrorCode*  target;
+    int32_t                  code;
+    uint16_t                 httpStatus;
+    const char*              message;
+  } _OrthancPluginRegisterErrorCode;
+  
+  /**
+   * @brief Declare a custom error code for this plugin.
+   *
+   * This function declares a custom error code that can be generated
+   * by this plugin. This declaration is used to enrich the body of
+   * the HTTP answer in the case of an error, and to set the proper
+   * HTTP status code.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param code The error code that is internal to this plugin.
+   * @param httpStatus The HTTP status corresponding to this error.
+   * @param message The description of the error.
+   * @return The error code that has been assigned inside the Orthanc core.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterErrorCode(
+    OrthancPluginContext*    context,
+    int32_t                  code,
+    uint16_t                 httpStatus,
+    const char*              message)
+  {
+    OrthancPluginErrorCode target;
+
+    _OrthancPluginRegisterErrorCode params;
+    params.target = &target;
+    params.code = code;
+    params.httpStatus = httpStatus;
+    params.message = message;
+
+    if (context->InvokeService(context, _OrthancPluginService_RegisterErrorCode, &params) == OrthancPluginErrorCode_Success)
+    {
+      return target;
+    }
+    else
+    {
+      /* There was an error while assigned the error. Use a generic code. */
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    uint16_t                          group;
+    uint16_t                          element;
+    OrthancPluginValueRepresentation  vr;
+    const char*                       name;
+    uint32_t                          minMultiplicity;
+    uint32_t                          maxMultiplicity;
+  } _OrthancPluginRegisterDictionaryTag;
+  
+  /**
+   * @brief Register a new tag into the DICOM dictionary.
+   *
+   * This function declares a new public tag in the dictionary of
+   * DICOM tags that are known to Orthanc. This function should be
+   * used in the OrthancPluginInitialize() callback.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag.
+   * @param element The element of the tag.
+   * @param vr The value representation of the tag.
+   * @param name The nickname of the tag.
+   * @param minMultiplicity The minimum multiplicity of the tag (must be above 0).
+   * @param maxMultiplicity The maximum multiplicity of the tag. A value of 0 means
+   * an arbitrary multiplicity ("<tt>n</tt>").
+   * @return 0 if success, other value if error.
+   * @see OrthancPluginRegisterPrivateDictionaryTag()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterDictionaryTag(
+    OrthancPluginContext*             context,
+    uint16_t                          group,
+    uint16_t                          element,
+    OrthancPluginValueRepresentation  vr,
+    const char*                       name,
+    uint32_t                          minMultiplicity,
+    uint32_t                          maxMultiplicity)
+  {
+    _OrthancPluginRegisterDictionaryTag params;
+    params.group = group;
+    params.element = element;
+    params.vr = vr;
+    params.name = name;
+    params.minMultiplicity = minMultiplicity;
+    params.maxMultiplicity = maxMultiplicity;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDictionaryTag, &params);
+  }
+
+
+
+  typedef struct
+  {
+    uint16_t                          group;
+    uint16_t                          element;
+    OrthancPluginValueRepresentation  vr;
+    const char*                       name;
+    uint32_t                          minMultiplicity;
+    uint32_t                          maxMultiplicity;
+    const char*                       privateCreator;
+  } _OrthancPluginRegisterPrivateDictionaryTag;
+  
+  /**
+   * @brief Register a new private tag into the DICOM dictionary.
+   *
+   * This function declares a new private tag in the dictionary of
+   * DICOM tags that are known to Orthanc. This function should be
+   * used in the OrthancPluginInitialize() callback.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag.
+   * @param element The element of the tag.
+   * @param vr The value representation of the tag.
+   * @param name The nickname of the tag.
+   * @param minMultiplicity The minimum multiplicity of the tag (must be above 0).
+   * @param maxMultiplicity The maximum multiplicity of the tag. A value of 0 means
+   * an arbitrary multiplicity ("<tt>n</tt>").
+   * @param privateCreator The private creator of this private tag.
+   * @return 0 if success, other value if error.
+   * @see OrthancPluginRegisterDictionaryTag()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.2.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterPrivateDictionaryTag(
+    OrthancPluginContext*             context,
+    uint16_t                          group,
+    uint16_t                          element,
+    OrthancPluginValueRepresentation  vr,
+    const char*                       name,
+    uint32_t                          minMultiplicity,
+    uint32_t                          maxMultiplicity,
+    const char*                       privateCreator)
+  {
+    _OrthancPluginRegisterPrivateDictionaryTag params;
+    params.group = group;
+    params.element = element;
+    params.vr = vr;
+    params.name = name;
+    params.minMultiplicity = minMultiplicity;
+    params.maxMultiplicity = maxMultiplicity;
+    params.privateCreator = privateCreator;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterPrivateDictionaryTag, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginStorageArea*  storageArea;
+    OrthancPluginResourceType  level;
+  } _OrthancPluginReconstructMainDicomTags;
+
+  /**
+   * @brief Reconstruct the main DICOM tags.
+   *
+   * This function requests the Orthanc core to reconstruct the main
+   * DICOM tags of all the resources of the given type. This function
+   * can only be used as a part of the upgrade of a custom database
+   * back-end. A database transaction will be automatically setup.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storageArea The storage area.
+   * @param level The type of the resources of interest.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginReconstructMainDicomTags(
+    OrthancPluginContext*      context,
+    OrthancPluginStorageArea*  storageArea,
+    OrthancPluginResourceType  level)
+  {
+    _OrthancPluginReconstructMainDicomTags params;
+    params.level = level;
+    params.storageArea = storageArea;
+
+    return context->InvokeService(context, _OrthancPluginService_ReconstructMainDicomTags, &params);
+  }
+
+
+  typedef struct
+  {
+    char**                          result;
+    const char*                     instanceId;
+    const void*                     buffer;
+    uint32_t                        size;
+    OrthancPluginDicomToJsonFormat  format;
+    OrthancPluginDicomToJsonFlags   flags;
+    uint32_t                        maxStringLength;
+  } _OrthancPluginDicomToJson;
+
+
+  /**
+   * @brief Format a DICOM memory buffer as a JSON string.
+   *
+   * This function takes as input a memory buffer containing a DICOM
+   * file, and outputs a JSON string representing the tags of this
+   * DICOM file.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM file.
+   * @param size The size of the memory buffer.
+   * @param format The output format.
+   * @param flags Flags governing the output.
+   * @param maxStringLength The maximum length of a field. Too long fields will
+   * be output as "null". The 0 value means no maximum length.
+   * @return The NULL value if the case of an error, or the JSON
+   * string. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   * @see OrthancPluginDicomInstanceToJson()
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson(
+    OrthancPluginContext*           context,
+    const void*                     buffer,
+    uint32_t                        size,
+    OrthancPluginDicomToJsonFormat  format,
+    OrthancPluginDicomToJsonFlags   flags, 
+    uint32_t                        maxStringLength)
+  {
+    char* result;
+
+    _OrthancPluginDicomToJson params;
+    memset(&params, 0, sizeof(params));
+    params.result = &result;
+    params.buffer = buffer;
+    params.size = size;
+    params.format = format;
+    params.flags = flags;
+    params.maxStringLength = maxStringLength;
+
+    if (context->InvokeService(context, _OrthancPluginService_DicomBufferToJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Format a DICOM instance as a JSON string.
+   *
+   * This function formats a DICOM instance that is stored in Orthanc,
+   * and outputs a JSON string representing the tags of this DICOM
+   * instance.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instanceId The Orthanc identifier of the instance.
+   * @param format The output format.
+   * @param flags Flags governing the output.
+   * @param maxStringLength The maximum length of a field. Too long fields will
+   * be output as "null". The 0 value means no maximum length.
+   * @return The NULL value if the case of an error, or the JSON
+   * string. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   * @see OrthancPluginDicomInstanceToJson()
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomInstanceToJson(
+    OrthancPluginContext*           context,
+    const char*                     instanceId,
+    OrthancPluginDicomToJsonFormat  format,
+    OrthancPluginDicomToJsonFlags   flags, 
+    uint32_t                        maxStringLength)
+  {
+    char* result;
+
+    _OrthancPluginDicomToJson params;
+    memset(&params, 0, sizeof(params));
+    params.result = &result;
+    params.instanceId = instanceId;
+    params.format = format;
+    params.flags = flags;
+    params.maxStringLength = maxStringLength;
+
+    if (context->InvokeService(context, _OrthancPluginService_DicomInstanceToJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+    uint32_t                    headersCount;
+    const char* const*          headersKeys;
+    const char* const*          headersValues;
+    int32_t                     afterPlugins;
+  } _OrthancPluginRestApiGet2;
+
+  /**
+   * @brief Make a GET call to the Orthanc REST API, with custom HTTP headers.
+   * 
+   * Make a GET call to the Orthanc REST API with extended
+   * parameters. The result to the query is stored into a newly
+   * allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param uri The URI in the built-in Orthanc API.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param afterPlugins If 0, the built-in API of Orthanc is used.
+   * If 1, the API is tainted by the plugins.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginRestApiGet(), OrthancPluginRestApiGetAfterPlugins()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGet2(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    uint32_t                    headersCount,
+    const char* const*          headersKeys,
+    const char* const*          headersValues,
+    int32_t                     afterPlugins)
+  {
+    _OrthancPluginRestApiGet2 params;
+    params.target = target;
+    params.uri = uri;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.afterPlugins = afterPlugins;
+
+    return context->InvokeService(context, _OrthancPluginService_RestApiGet2, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginWorklistCallback callback;
+  } _OrthancPluginWorklistCallback;
+
+  /**
+   * @brief Register a callback to handle modality worklists requests.
+   *
+   * This function registers a callback to handle C-Find SCP requests
+   * on modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWorklistCallback(
+    OrthancPluginContext*          context,
+    OrthancPluginWorklistCallback  callback)
+  {
+    _OrthancPluginWorklistCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterWorklistCallback, &params);
+  }
+
+
+  
+  typedef struct
+  {
+    OrthancPluginWorklistAnswers*      answers;
+    const OrthancPluginWorklistQuery*  query;
+    const void*                        dicom;
+    uint32_t                           size;
+  } _OrthancPluginWorklistAnswersOperation;
+
+  /**
+   * @brief Add one answer to some modality worklist request.
+   *
+   * This function adds one worklist (encoded as a DICOM file) to the
+   * set of answers corresponding to some C-Find SCP request against
+   * modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @param query The worklist query, as received by the callback.
+   * @param dicom The worklist to answer, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   * @see OrthancPluginCreateDicom()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistAddAnswer(
+    OrthancPluginContext*             context,
+    OrthancPluginWorklistAnswers*     answers,
+    const OrthancPluginWorklistQuery* query,
+    const void*                       dicom,
+    uint32_t                          size)
+  {
+    _OrthancPluginWorklistAnswersOperation params;
+    params.answers = answers;
+    params.query = query;
+    params.dicom = dicom;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_WorklistAddAnswer, &params);
+  }
+
+
+  /**
+   * @brief Mark the set of worklist answers as incomplete.
+   *
+   * This function marks as incomplete the set of answers
+   * corresponding to some C-Find SCP request against modality
+   * worklists. This must be used if canceling the handling of a
+   * request when too many answers are to be returned.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistMarkIncomplete(
+    OrthancPluginContext*          context,
+    OrthancPluginWorklistAnswers*  answers)
+  {
+    _OrthancPluginWorklistAnswersOperation params;
+    params.answers = answers;
+    params.query = NULL;
+    params.dicom = NULL;
+    params.size = 0;
+
+    return context->InvokeService(context, _OrthancPluginService_WorklistMarkIncomplete, &params);
+  }
+
+
+  typedef struct
+  {
+    const OrthancPluginWorklistQuery*  query;
+    const void*                        dicom;
+    uint32_t                           size;
+    int32_t*                           isMatch;
+    OrthancPluginMemoryBuffer*         target;
+  } _OrthancPluginWorklistQueryOperation;
+
+  /**
+   * @brief Test whether a worklist matches the query.
+   *
+   * This function checks whether one worklist (encoded as a DICOM
+   * file) matches the C-Find SCP query against modality
+   * worklists. This function must be called before adding the
+   * worklist as an answer through OrthancPluginWorklistAddAnswer().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The worklist query, as received by the callback.
+   * @param dicom The worklist to answer, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 1 if the worklist matches the query, 0 otherwise.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginWorklistIsMatch(
+    OrthancPluginContext*              context,
+    const OrthancPluginWorklistQuery*  query,
+    const void*                        dicom,
+    uint32_t                           size)
+  {
+    int32_t isMatch = 0;
+
+    _OrthancPluginWorklistQueryOperation params;
+    params.query = query;
+    params.dicom = dicom;
+    params.size = size;
+    params.isMatch = &isMatch;
+    params.target = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_WorklistIsMatch, &params) == OrthancPluginErrorCode_Success)
+    {
+      return isMatch;
+    }
+    else
+    {
+      /* Error: Assume non-match */
+      return 0;
+    }
+  }
+
+
+  /**
+   * @brief Retrieve the worklist query as a DICOM file.
+   *
+   * This function retrieves the DICOM file that underlies a C-Find
+   * SCP query against modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param query The worklist query, as received by the callback.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistGetDicomQuery(
+    OrthancPluginContext*              context,
+    OrthancPluginMemoryBuffer*         target,
+    const OrthancPluginWorklistQuery*  query)
+  {
+    _OrthancPluginWorklistQueryOperation params;
+    params.query = query;
+    params.dicom = NULL;
+    params.size = 0;
+    params.isMatch = NULL;
+    params.target = target;
+
+    return context->InvokeService(context, _OrthancPluginService_WorklistGetDicomQuery, &params);
+  }
+
+
+  /**
+   * @brief Get the origin of a DICOM file.
+   *
+   * This function returns the origin of a DICOM instance that has been received by Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The origin of the instance.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    OrthancPluginInstanceOrigin origin;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultOrigin = &origin;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceOrigin, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return OrthancPluginInstanceOrigin_Unknown;
+    }
+    else
+    {
+      return origin;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*     target;
+    const char*                    json;
+    const OrthancPluginImage*      pixelData;
+    OrthancPluginCreateDicomFlags  flags;
+  } _OrthancPluginCreateDicom;
+
+  /**
+   * @brief Create a DICOM instance from a JSON string and an image.
+   *
+   * This function takes as input a string containing a JSON file
+   * describing the content of a DICOM instance. As an output, it
+   * writes the corresponding DICOM instance to a newly allocated
+   * memory buffer. Additionally, an image to be encoded within the
+   * DICOM instance can also be provided.
+   *
+   * Private tags will be associated with the private creator whose
+   * value is specified in the "DefaultPrivateCreator" configuration
+   * option of Orthanc. The function OrthancPluginCreateDicom2() can
+   * be used if another private creator must be used to create this
+   * instance.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param json The input JSON file.
+   * @param pixelData The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme.
+   * @param flags Flags governing the output.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   * @see OrthancPluginCreateDicom2()
+   * @see OrthancPluginDicomBufferToJson()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom(
+    OrthancPluginContext*          context,
+    OrthancPluginMemoryBuffer*     target,
+    const char*                    json,
+    const OrthancPluginImage*      pixelData,
+    OrthancPluginCreateDicomFlags  flags)
+  {
+    _OrthancPluginCreateDicom params;
+    params.target = target;
+    params.json = json;
+    params.pixelData = pixelData;
+    params.flags = flags;
+
+    return context->InvokeService(context, _OrthancPluginService_CreateDicom, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginDecodeImageCallback callback;
+  } _OrthancPluginDecodeImageCallback;
+
+  /**
+   * @brief Register a callback to handle the decoding of DICOM images.
+   *
+   * This function registers a custom callback to decode DICOM images,
+   * extending the built-in decoder of Orthanc that uses
+   * DCMTK. Starting with Orthanc 1.7.0, the exact behavior is
+   * affected by the configuration option
+   * "BuiltinDecoderTranscoderOrder" of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDecodeImageCallback(
+    OrthancPluginContext*             context,
+    OrthancPluginDecodeImageCallback  callback)
+  {
+    _OrthancPluginDecodeImageCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDecodeImageCallback, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginImage**       target;
+    OrthancPluginPixelFormat   format;
+    uint32_t                   width;
+    uint32_t                   height;
+    uint32_t                   pitch;
+    void*                      buffer;
+    const void*                constBuffer;
+    uint32_t                   bufferSize;
+    uint32_t                   frameIndex;
+  } _OrthancPluginCreateImage;
+
+
+  /**
+   * @brief Create an image.
+   *
+   * This function creates an image of given size and format.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param format The format of the pixels.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @return The newly allocated image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImage(
+    OrthancPluginContext*     context,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginCreateImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.format = format;
+    params.width = width;
+    params.height = height;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateImage, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Create an image pointing to a memory buffer.
+   *
+   * This function creates an image whose content points to a memory
+   * buffer managed by the plugin. Note that the buffer is directly
+   * accessed, no memory is allocated and no data is copied.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param format The format of the pixels.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @param pitch The pitch of the image (i.e. the number of bytes
+   * between 2 successive lines of the image in the memory buffer).
+   * @param buffer The memory buffer.
+   * @return The newly allocated image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImageAccessor(
+    OrthancPluginContext*     context,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height,
+    uint32_t                  pitch,
+    void*                     buffer)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginCreateImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.format = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateImageAccessor, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Decode one frame from a DICOM instance.
+   *
+   * This function decodes one frame of a DICOM image that is stored
+   * in a memory buffer. This function will give the same result as
+   * OrthancPluginUncompressImage() for single-frame DICOM images.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer Pointer to a memory buffer containing the DICOM image.
+   * @param bufferSize Size of the memory buffer containing the DICOM image.
+   * @param frameIndex The index of the frame of interest in a multi-frame image.
+   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   * @see OrthancPluginGetInstanceDecodedFrame()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginDecodeDicomImage(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               bufferSize,
+    uint32_t               frameIndex)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginCreateImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.constBuffer = buffer;
+    params.bufferSize = bufferSize;
+    params.frameIndex = frameIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_DecodeDicomImage, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    char**       result;
+    const void*  buffer;
+    uint32_t     size;
+  } _OrthancPluginComputeHash;
+
+  /**
+   * @brief Compute an MD5 hash.
+   *
+   * This functions computes the MD5 cryptographic hash of the given memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The source memory buffer.
+   * @param size The size in bytes of the source buffer.
+   * @return The NULL value in case of error, or a string containing the cryptographic hash.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeMd5(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size)
+  {
+    char* result;
+
+    _OrthancPluginComputeHash params;
+    params.result = &result;
+    params.buffer = buffer;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_ComputeMd5, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Compute a SHA-1 hash.
+   *
+   * This functions computes the SHA-1 cryptographic hash of the given memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The source memory buffer.
+   * @param size The size in bytes of the source buffer.
+   * @return The NULL value in case of error, or a string containing the cryptographic hash.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeSha1(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size)
+  {
+    char* result;
+
+    _OrthancPluginComputeHash params;
+    params.result = &result;
+    params.buffer = buffer;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_ComputeSha1, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginDictionaryEntry* target;
+    const char*                   name;
+  } _OrthancPluginLookupDictionary;
+
+  /**
+   * @brief Get information about the given DICOM tag.
+   *
+   * This functions makes a lookup in the dictionary of DICOM tags
+   * that are known to Orthanc, and returns information about this
+   * tag. The tag can be specified using its human-readable name
+   * (e.g. "PatientName") or a set of two hexadecimal numbers
+   * (e.g. "0010-0020").
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target Where to store the information about the tag.
+   * @param name The name of the DICOM tag.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginLookupDictionary(
+    OrthancPluginContext*          context,
+    OrthancPluginDictionaryEntry*  target,
+    const char*                    name)
+  {
+    _OrthancPluginLookupDictionary params;
+    params.target = target;
+    params.name = name;
+    return context->InvokeService(context, _OrthancPluginService_LookupDictionary, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const void*              answer;
+    uint32_t                 answerSize;
+    uint32_t                 headersCount;
+    const char* const*       headersKeys;
+    const char* const*       headersValues;
+  } _OrthancPluginSendMultipartItem2;
+
+  /**
+   * @brief Send an item as a part of some HTTP multipart answer, with custom headers.
+   *
+   * This function sends an item as a part of some HTTP multipart
+   * answer that was initiated by OrthancPluginStartMultipartAnswer(). In addition to
+   * OrthancPluginSendMultipartItem(), this function will set HTTP header associated
+   * with the item.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the item.
+   * @param answerSize Number of bytes of the item.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers.
+   * @param headersValues Array containing the values of the HTTP headers.
+   * @return 0 if success, or the error code if failure (this notably happens
+   * if the connection is closed by the client).
+   * @see OrthancPluginSendMultipartItem()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.0.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem2(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const void*              answer,
+    uint32_t                 answerSize,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues)
+  {
+    _OrthancPluginSendMultipartItem2 params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;    
+
+    return context->InvokeService(context, _OrthancPluginService_SendMultipartItem2, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingHttpRequestFilter callback;
+  } _OrthancPluginIncomingHttpRequestFilter;
+
+  /**
+   * @brief Register a callback to filter incoming HTTP requests.
+   *
+   * This function registers a custom callback to filter incoming HTTP/REST
+   * requests received by the HTTP server of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   * @deprecated Please instead use OrthancPluginRegisterIncomingHttpRequestFilter2()
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter(
+    OrthancPluginContext*                   context,
+    OrthancPluginIncomingHttpRequestFilter  callback)
+  {
+    _OrthancPluginIncomingHttpRequestFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  answerBody;
+    OrthancPluginMemoryBuffer*  answerHeaders;
+    uint16_t*                   httpStatus;
+    OrthancPluginHttpMethod     method;
+    const char*                 url;
+    uint32_t                    headersCount;
+    const char* const*          headersKeys;
+    const char* const*          headersValues;
+    const void*                 body;
+    uint32_t                    bodySize;
+    const char*                 username;
+    const char*                 password;
+    uint32_t                    timeout;
+    const char*                 certificateFile;
+    const char*                 certificateKeyFile;
+    const char*                 certificateKeyPassword;
+    uint8_t                     pkcs11;
+  } _OrthancPluginCallHttpClient2;
+
+
+
+  /**
+   * @brief Issue a HTTP call with full flexibility.
+   * 
+   * Make a HTTP call to the given URL. The result to the query is
+   * stored into a newly allocated memory buffer. The HTTP request
+   * will be done accordingly to the global configuration of Orthanc
+   * (in particular, the options "HttpProxy", "HttpTimeout",
+   * "HttpsVerifyPeers", "HttpsCACertificates", and "Pkcs11" will be
+   * taken into account).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answerBody The target memory buffer (out argument).
+   *        It must be freed with OrthancPluginFreeMemoryBuffer().
+   *        The value of this argument is ignored if the HTTP method is DELETE.
+   * @param answerHeaders The target memory buffer for the HTTP headers in the answers (out argument). 
+   *        The answer headers are formatted as a JSON object (associative array).
+   *        The buffer must be freed with OrthancPluginFreeMemoryBuffer().
+   *        This argument can be set to NULL if the plugin has no interest in the HTTP headers.
+   * @param httpStatus The HTTP status after the execution of the request (out argument).
+   * @param method HTTP method to be used.
+   * @param url The URL of interest.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @param body The HTTP body for a POST or PUT request.
+   * @param bodySize The size of the body.
+   * @param timeout Timeout in seconds (0 for default timeout).
+   * @param certificateFile Path to the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyPassword Password to unlock the key of the client certificate 
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginCallPeerApi()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpClient(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  answerBody,
+    OrthancPluginMemoryBuffer*  answerHeaders,
+    uint16_t*                   httpStatus,
+    OrthancPluginHttpMethod     method,
+    const char*                 url,
+    uint32_t                    headersCount,
+    const char* const*          headersKeys,
+    const char* const*          headersValues,
+    const void*                 body,
+    uint32_t                    bodySize,
+    const char*                 username,
+    const char*                 password,
+    uint32_t                    timeout,
+    const char*                 certificateFile,
+    const char*                 certificateKeyFile,
+    const char*                 certificateKeyPassword,
+    uint8_t                     pkcs11)
+  {
+    _OrthancPluginCallHttpClient2 params;
+    memset(&params, 0, sizeof(params));
+
+    params.answerBody = answerBody;
+    params.answerHeaders = answerHeaders;
+    params.httpStatus = httpStatus;
+    params.method = method;
+    params.url = url;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.username = username;
+    params.password = password;
+    params.timeout = timeout;
+    params.certificateFile = certificateFile;
+    params.certificateKeyFile = certificateKeyFile;
+    params.certificateKeyPassword = certificateKeyPassword;
+    params.pkcs11 = pkcs11;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient2, &params);
+  }
+
+
+  /**
+   * @brief Generate an UUID.
+   *
+   * Generate a random GUID/UUID (globally unique identifier).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the UUID. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateUuid(
+    OrthancPluginContext*  context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GenerateUuid, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginFindCallback callback;
+  } _OrthancPluginFindCallback;
+
+  /**
+   * @brief Register a callback to handle C-Find requests.
+   *
+   * This function registers a callback to handle C-Find SCP requests
+   * that are not related to modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterFindCallback(
+    OrthancPluginContext*      context,
+    OrthancPluginFindCallback  callback)
+  {
+    _OrthancPluginFindCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterFindCallback, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginFindAnswers      *answers;
+    const OrthancPluginFindQuery  *query;
+    const void                    *dicom;
+    uint32_t                       size;
+    uint32_t                       index;
+    uint32_t                      *resultUint32;
+    uint16_t                      *resultGroup;
+    uint16_t                      *resultElement;
+    char                         **resultString;
+  } _OrthancPluginFindOperation;
+
+  /**
+   * @brief Add one answer to some C-Find request.
+   *
+   * This function adds one answer (encoded as a DICOM file) to the
+   * set of answers corresponding to some C-Find SCP request that is
+   * not related to modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @param dicom The answer to be added, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   * @see OrthancPluginCreateDicom()
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginFindAddAnswer(
+    OrthancPluginContext*      context,
+    OrthancPluginFindAnswers*  answers,
+    const void*                dicom,
+    uint32_t                   size)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.answers = answers;
+    params.dicom = dicom;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_FindAddAnswer, &params);
+  }
+
+
+  /**
+   * @brief Mark the set of C-Find answers as incomplete.
+   *
+   * This function marks as incomplete the set of answers
+   * corresponding to some C-Find SCP request that is not related to
+   * modality worklists. This must be used if canceling the handling
+   * of a request when too many answers are to be returned.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginFindMarkIncomplete(
+    OrthancPluginContext*      context,
+    OrthancPluginFindAnswers*  answers)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.answers = answers;
+
+    return context->InvokeService(context, _OrthancPluginService_FindMarkIncomplete, &params);
+  }
+
+
+
+  /**
+   * @brief Get the number of tags in a C-Find query.
+   *
+   * This function returns the number of tags that are contained in
+   * the given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @return The number of tags.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetFindQuerySize(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQuerySize, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+  /**
+   * @brief Get one tag in a C-Find query.
+   *
+   * This function returns the group and the element of one DICOM tag
+   * in the given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag (output).
+   * @param element The element of the tag (output).
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetFindQueryTag(
+    OrthancPluginContext*          context,
+    uint16_t*                      group,
+    uint16_t*                      element,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultGroup = group;
+    params.resultElement = element;
+
+    return context->InvokeService(context, _OrthancPluginService_GetFindQueryTag, &params);
+  }
+
+
+  /**
+   * @brief Get the symbolic name of one tag in a C-Find query.
+   *
+   * This function returns the symbolic name of one DICOM tag in the
+   * given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return The NULL value in case of error, or a string containing the name of the tag.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE char*  OrthancPluginGetFindQueryTagName(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    char* result;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultString = &result;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQueryTagName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the value associated with one tag in a C-Find query.
+   *
+   * This function returns the value associated with one tag in the
+   * given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return The NULL value in case of error, or a string containing the value of the tag.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE char*  OrthancPluginGetFindQueryValue(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    char* result;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultString = &result;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQueryValue, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+ 
+
+
+
+  typedef struct
+  {
+    OrthancPluginMoveCallback   callback;
+    OrthancPluginGetMoveSize    getMoveSize;
+    OrthancPluginApplyMove      applyMove;
+    OrthancPluginFreeMove       freeMove;
+  } _OrthancPluginMoveCallback;
+
+  /**
+   * @brief Register a callback to handle C-Move requests.
+   *
+   * This function registers a callback to handle C-Move SCP requests.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The main callback.
+   * @param getMoveSize Callback to read the number of C-Move suboperations.
+   * @param applyMove Callback to apply one C-Move suboperation.
+   * @param freeMove Callback to free the C-Move driver.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.1.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterMoveCallback(
+    OrthancPluginContext*       context,
+    OrthancPluginMoveCallback   callback,
+    OrthancPluginGetMoveSize    getMoveSize,
+    OrthancPluginApplyMove      applyMove,
+    OrthancPluginFreeMove       freeMove)
+  {
+    _OrthancPluginMoveCallback params;
+    params.callback = callback;
+    params.getMoveSize = getMoveSize;
+    params.applyMove = applyMove;
+    params.freeMove = freeMove;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterMoveCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginFindMatcher** target;
+    const void*                query;
+    uint32_t                   size;
+  } _OrthancPluginCreateFindMatcher;
+
+
+  /**
+   * @brief Create a C-Find matcher.
+   *
+   * This function creates a "matcher" object that can be used to
+   * check whether a DICOM instance matches a C-Find query. The C-Find
+   * query must be expressed as a DICOM buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find DICOM query.
+   * @param size The size of the DICOM query.
+   * @return The newly allocated matcher. It must be freed with OrthancPluginFreeFindMatcher().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.2.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginFindMatcher* OrthancPluginCreateFindMatcher(
+    OrthancPluginContext*  context,
+    const void*            query,
+    uint32_t               size)
+  {
+    OrthancPluginFindMatcher* target = NULL;
+
+    _OrthancPluginCreateFindMatcher params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.query = query;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateFindMatcher, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginFindMatcher*   matcher;
+  } _OrthancPluginFreeFindMatcher;
+
+  /**
+   * @brief Free a C-Find matcher.
+   *
+   * This function frees a matcher that was created using OrthancPluginCreateFindMatcher().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param matcher The matcher of interest.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.2.0")
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeFindMatcher(
+    OrthancPluginContext*     context, 
+    OrthancPluginFindMatcher* matcher)
+  {
+    _OrthancPluginFreeFindMatcher params;
+    params.matcher = matcher;
+
+    context->InvokeService(context, _OrthancPluginService_FreeFindMatcher, &params);
+  }
+
+
+  typedef struct
+  {
+    const OrthancPluginFindMatcher*  matcher;
+    const void*                      dicom;
+    uint32_t                         size;
+    int32_t*                         isMatch;
+  } _OrthancPluginFindMatcherIsMatch;
+
+  /**
+   * @brief Test whether a DICOM instance matches a C-Find query.
+   *
+   * This function checks whether one DICOM instance matches C-Find
+   * matcher that was previously allocated using
+   * OrthancPluginCreateFindMatcher().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param matcher The matcher of interest.
+   * @param dicom The DICOM instance to be matched.
+   * @param size The size of the DICOM instance.
+   * @return 1 if the DICOM instance matches the query, 0 otherwise.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.2.0")
+  ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginFindMatcherIsMatch(
+    OrthancPluginContext*            context,
+    const OrthancPluginFindMatcher*  matcher,
+    const void*                      dicom,
+    uint32_t                         size)
+  {
+    int32_t isMatch = 0;
+
+    _OrthancPluginFindMatcherIsMatch params;
+    params.matcher = matcher;
+    params.dicom = dicom;
+    params.size = size;
+    params.isMatch = &isMatch;
+
+    if (context->InvokeService(context, _OrthancPluginService_FindMatcherIsMatch, &params) == OrthancPluginErrorCode_Success)
+    {
+      return isMatch;
+    }
+    else
+    {
+      /* Error: Assume non-match */
+      return 0;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingHttpRequestFilter2 callback;
+  } _OrthancPluginIncomingHttpRequestFilter2;
+
+  /**
+   * @brief Register a callback to filter incoming HTTP requests.
+   *
+   * This function registers a custom callback to filter incoming HTTP/REST
+   * requests received by the HTTP server of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.3.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter2(
+    OrthancPluginContext*                   context,
+    OrthancPluginIncomingHttpRequestFilter2 callback)
+  {
+    _OrthancPluginIncomingHttpRequestFilter2 params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter2, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginPeers**  peers;
+  } _OrthancPluginGetPeers;
+
+  /**
+   * @brief Return the list of available Orthanc peers.
+   *
+   * This function returns the parameters of the Orthanc peers that are known to
+   * the Orthanc server hosting the plugin.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL if error, or a newly allocated opaque data structure containing the peers.
+   * This structure must be freed with OrthancPluginFreePeers().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE OrthancPluginPeers* OrthancPluginGetPeers(
+    OrthancPluginContext*  context)
+  {
+    OrthancPluginPeers* peers = NULL;
+
+    _OrthancPluginGetPeers params;
+    memset(&params, 0, sizeof(params));
+    params.peers = &peers;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeers, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return peers;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginPeers*   peers;
+  } _OrthancPluginFreePeers;
+
+  /**
+   * @brief Free the list of available Orthanc peers.
+   *
+   * This function frees the data structure returned by OrthancPluginGetPeers().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreePeers(
+    OrthancPluginContext*     context, 
+    OrthancPluginPeers* peers)
+  {
+    _OrthancPluginFreePeers params;
+    params.peers = peers;
+
+    context->InvokeService(context, _OrthancPluginService_FreePeers, &params);
+  }
+
+
+  typedef struct
+  {
+    uint32_t*                  target;
+    const OrthancPluginPeers*  peers;
+  } _OrthancPluginGetPeersCount;
+
+  /**
+   * @brief Get the number of Orthanc peers.
+   *
+   * This function returns the number of Orthanc peers.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @result The number of peers. 
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetPeersCount(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers)
+  {
+    uint32_t target = 0;
+
+    _OrthancPluginGetPeersCount params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeersCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    const char**               target;
+    const OrthancPluginPeers*  peers;
+    uint32_t                   peerIndex;
+    const char*                userProperty;
+  } _OrthancPluginGetPeerProperty;
+
+  /**
+   * @brief Get the symbolic name of an Orthanc peer.
+   *
+   * This function returns the symbolic name of the Orthanc peer,
+   * which corresponds to the key of the "OrthancPeers" configuration
+   * option of Orthanc.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @result The symbolic name, or NULL in the case of an error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerName(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.userProperty = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Get the base URL of an Orthanc peer.
+   *
+   * This function returns the base URL to the REST API of some Orthanc peer.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @result The URL, or NULL in the case of an error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUrl(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.userProperty = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerUrl, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Get some user-defined property of an Orthanc peer.
+   *
+   * This function returns some user-defined property of some Orthanc
+   * peer. An user-defined property is a property that is associated
+   * with the peer in the Orthanc configuration file, but that is not
+   * recognized by the Orthanc core.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @param userProperty The user property of interest.
+   * @result The value of the user property, or NULL if it is not defined.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUserProperty(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex,
+    const char*                userProperty)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.userProperty = userProperty;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerUserProperty, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* No such user property */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  answerBody;
+    OrthancPluginMemoryBuffer*  answerHeaders;
+    uint16_t*                   httpStatus;
+    const OrthancPluginPeers*   peers;
+    uint32_t                    peerIndex;
+    OrthancPluginHttpMethod     method;
+    const char*                 uri;
+    uint32_t                    additionalHeadersCount;
+    const char* const*          additionalHeadersKeys;
+    const char* const*          additionalHeadersValues;
+    const void*                 body;
+    uint32_t                    bodySize;
+    uint32_t                    timeout;
+  } _OrthancPluginCallPeerApi;
+
+  /**
+   * @brief Call the REST API of an Orthanc peer.
+   * 
+   * Make a REST call to the given URI in the REST API of a remote
+   * Orthanc peer. The result to the query is stored into a newly
+   * allocated memory buffer. The HTTP request will be done according
+   * to the "OrthancPeers" configuration option of Orthanc.
+   *
+   * This function is thread-safe: Several threads sharing the same
+   * OrthancPluginPeers object can simultaneously call this function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answerBody The target memory buffer (out argument).
+   *        It must be freed with OrthancPluginFreeMemoryBuffer().
+   *        The value of this argument is ignored if the HTTP method is DELETE.
+   * @param answerHeaders The target memory buffer for the HTTP headers in the answers (out argument). 
+   *        The answer headers are formatted as a JSON object (associative array).
+   *        The buffer must be freed with OrthancPluginFreeMemoryBuffer().
+   *        This argument can be set to NULL if the plugin has no interest in the HTTP headers.
+   * @param httpStatus The HTTP status after the execution of the request (out argument).
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @param method HTTP method to be used.
+   * @param uri The URI of interest in the REST API.
+   * @param additionalHeadersCount The number of HTTP headers to be added to the
+   * HTTP headers provided in the global configuration of Orthanc.
+   * @param additionalHeadersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param additionalHeadersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param body The HTTP body for a POST or PUT request.
+   * @param bodySize The size of the body.
+   * @param timeout Timeout in seconds (0 for default timeout).
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginHttpClient()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginCallPeerApi(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  answerBody,
+    OrthancPluginMemoryBuffer*  answerHeaders,
+    uint16_t*                   httpStatus,
+    const OrthancPluginPeers*   peers,
+    uint32_t                    peerIndex,
+    OrthancPluginHttpMethod     method,
+    const char*                 uri,
+    uint32_t                    additionalHeadersCount,
+    const char* const*          additionalHeadersKeys,
+    const char* const*          additionalHeadersValues,
+    const void*                 body,
+    uint32_t                    bodySize,
+    uint32_t                    timeout)
+  {
+    _OrthancPluginCallPeerApi params;
+    memset(&params, 0, sizeof(params));
+
+    params.answerBody = answerBody;
+    params.answerHeaders = answerHeaders;
+    params.httpStatus = httpStatus;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.method = method;
+    params.uri = uri;
+    params.additionalHeadersCount = additionalHeadersCount;
+    params.additionalHeadersKeys = additionalHeadersKeys;
+    params.additionalHeadersValues = additionalHeadersValues;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.timeout = timeout;
+
+    return context->InvokeService(context, _OrthancPluginService_CallPeerApi, &params);
+  }
+
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginJob**              target;
+    void                           *job;
+    OrthancPluginJobFinalize        finalize;
+    const char                     *type;
+    OrthancPluginJobGetProgress     getProgress;
+    OrthancPluginJobGetContent      getContent;
+    OrthancPluginJobGetSerialized   getSerialized;
+    OrthancPluginJobStep            step;
+    OrthancPluginJobStop            stop;
+    OrthancPluginJobReset           reset;
+  } _OrthancPluginCreateJob;
+
+  /**
+   * @brief Create a custom job.
+   *
+   * This function creates a custom job to be run by the jobs engine
+   * of Orthanc.
+   * 
+   * Orthanc starts one dedicated thread per custom job that is
+   * running. It is guaranteed that all the callbacks will only be
+   * called from this single dedicated thread, in mutual exclusion: As
+   * a consequence, it is *not* mandatory to protect the various
+   * callbacks by mutexes.
+   * 
+   * The custom job can nonetheless launch its own processing threads
+   * on the first call to the "step()" callback, and stop them once
+   * the "stop()" callback is called.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job to be executed.
+   * @param finalize The finalization callback.
+   * @param type The type of the job, provided to the job unserializer. 
+   * See OrthancPluginRegisterJobsUnserializer().
+   * @param getProgress The progress callback.
+   * @param getContent The content callback.
+   * @param getSerialized The serialization callback.
+   * @param step The callback to execute the individual steps of the job.
+   * @param stop The callback that is invoked once the job leaves the "running" state.
+   * @param reset The callback that is invoked if a stopped job is started again.
+   * @return The newly allocated job. It must be freed with OrthancPluginFreeJob(),
+   * as long as it is not submitted with OrthancPluginSubmitJob().
+   * @ingroup Toolbox
+   * @deprecated This signature should not be used anymore since Orthanc SDK 1.11.3.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob(
+    OrthancPluginContext           *context,
+    void                           *job,
+    OrthancPluginJobFinalize        finalize,
+    const char                     *type,
+    OrthancPluginJobGetProgress     getProgress,
+    OrthancPluginJobGetContent      getContent,
+    OrthancPluginJobGetSerialized   getSerialized,
+    OrthancPluginJobStep            step,
+    OrthancPluginJobStop            stop,
+    OrthancPluginJobReset           reset)
+  {
+    OrthancPluginJob* target = NULL;
+
+    _OrthancPluginCreateJob params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = &target;
+    params.job = job;
+    params.finalize = finalize;
+    params.type = type;
+    params.getProgress = getProgress;
+    params.getContent = getContent;
+    params.getSerialized = getSerialized;
+    params.step = step;
+    params.stop = stop;
+    params.reset = reset;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateJob, &params) != OrthancPluginErrorCode_Success ||
+        target == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginJob**              target;
+    void                           *job;
+    OrthancPluginJobFinalize        finalize;
+    const char                     *type;
+    OrthancPluginJobGetProgress     getProgress;
+    OrthancPluginJobGetContent2     getContent;
+    OrthancPluginJobGetSerialized2  getSerialized;
+    OrthancPluginJobStep            step;
+    OrthancPluginJobStop            stop;
+    OrthancPluginJobReset           reset;
+  } _OrthancPluginCreateJob2;
+
+  /**
+   * @brief Create a custom job.
+   *
+   * This function creates a custom job to be run by the jobs engine
+   * of Orthanc.
+   * 
+   * Orthanc starts one dedicated thread per custom job that is
+   * running. It is guaranteed that all the callbacks will only be
+   * called from this single dedicated thread, in mutual exclusion: As
+   * a consequence, it is *not* mandatory to protect the various
+   * callbacks by mutexes.
+   * 
+   * The custom job can nonetheless launch its own processing threads
+   * on the first call to the "step()" callback, and stop them once
+   * the "stop()" callback is called.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job to be executed.
+   * @param finalize The finalization callback.
+   * @param type The type of the job, provided to the job unserializer. 
+   * See OrthancPluginRegisterJobsUnserializer().
+   * @param getProgress The progress callback.
+   * @param getContent The content callback.
+   * @param getSerialized The serialization callback.
+   * @param step The callback to execute the individual steps of the job.
+   * @param stop The callback that is invoked once the job leaves the "running" state.
+   * @param reset The callback that is invoked if a stopped job is started again.
+   * @return The newly allocated job. It must be freed with OrthancPluginFreeJob(),
+   * as long as it is not submitted with OrthancPluginSubmitJob().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.11.3")
+  ORTHANC_PLUGIN_INLINE OrthancPluginJob *OrthancPluginCreateJob2(
+    OrthancPluginContext           *context,
+    void                           *job,
+    OrthancPluginJobFinalize        finalize,
+    const char                     *type,
+    OrthancPluginJobGetProgress     getProgress,
+    OrthancPluginJobGetContent2     getContent,
+    OrthancPluginJobGetSerialized2  getSerialized,
+    OrthancPluginJobStep            step,
+    OrthancPluginJobStop            stop,
+    OrthancPluginJobReset           reset)
+  {
+    OrthancPluginJob* target = NULL;
+
+    _OrthancPluginCreateJob2 params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = &target;
+    params.job = job;
+    params.finalize = finalize;
+    params.type = type;
+    params.getProgress = getProgress;
+    params.getContent = getContent;
+    params.getSerialized = getSerialized;
+    params.step = step;
+    params.stop = stop;
+    params.reset = reset;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateJob2, &params) != OrthancPluginErrorCode_Success ||
+        target == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginJob*   job;
+  } _OrthancPluginFreeJob;
+
+  /**
+   * @brief Free a custom job.
+   *
+   * This function frees an image that was created with OrthancPluginCreateJob().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeJob(
+    OrthancPluginContext* context, 
+    OrthancPluginJob*     job)
+  {
+    _OrthancPluginFreeJob params;
+    params.job = job;
+
+    context->InvokeService(context, _OrthancPluginService_FreeJob, &params);
+  }
+
+
+  
+  typedef struct
+  {
+    char**             resultId;
+    OrthancPluginJob  *job;
+    int32_t            priority;
+  } _OrthancPluginSubmitJob;
+
+  /**
+   * @brief Submit a new job to the jobs engine of Orthanc.
+   *
+   * This function adds the given job to the pending jobs of
+   * Orthanc. Orthanc will take take of freeing it by invoking the
+   * finalization callback provided to OrthancPluginCreateJob().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param job The job, as received by OrthancPluginCreateJob().
+   * @param priority The priority of the job.
+   * @return ID of the newly-submitted job. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginSubmitJob(
+    OrthancPluginContext   *context,
+    OrthancPluginJob       *job,
+    int32_t                 priority)
+  {
+    char* resultId = NULL;
+
+    _OrthancPluginSubmitJob params;
+    memset(&params, 0, sizeof(params));
+
+    params.resultId = &resultId;
+    params.job = job;
+    params.priority = priority;
+
+    if (context->InvokeService(context, _OrthancPluginService_SubmitJob, &params) != OrthancPluginErrorCode_Success ||
+        resultId == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return resultId;
+    }
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginJobsUnserializer unserializer;
+  } _OrthancPluginJobsUnserializer;
+
+  /**
+   * @brief Register an unserializer for custom jobs.
+   *
+   * This function registers an unserializer that decodes custom jobs
+   * from a JSON string. This callback is invoked when the jobs engine
+   * of Orthanc is started (on Orthanc initialization), for each job
+   * that is stored in the Orthanc database.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param unserializer The job unserializer.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.4.2")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterJobsUnserializer(
+    OrthancPluginContext*          context,
+    OrthancPluginJobsUnserializer  unserializer)
+  {
+    _OrthancPluginJobsUnserializer params;
+    params.unserializer = unserializer;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterJobsUnserializer, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              details;
+    uint8_t                  log;
+  } _OrthancPluginSetHttpErrorDetails;
+
+  /**
+   * @brief Provide a detailed description for an HTTP error.
+   *
+   * This function sets the detailed description associated with an
+   * HTTP error. This description will be displayed in the "Details"
+   * field of the JSON body of the HTTP answer. It is only taken into
+   * consideration if the REST callback returns an error code that is
+   * different from "OrthancPluginErrorCode_Success", and if the
+   * "HttpDescribeErrors" configuration option of Orthanc is set to
+   * "true".
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param details The details of the error message.
+   * @param log Whether to also write the detailed error to the Orthanc logs.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.0")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpErrorDetails(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              details,
+    uint8_t                  log)
+  {
+    _OrthancPluginSetHttpErrorDetails params;
+    params.output = output;
+    params.details = details;
+    params.log = log;
+    context->InvokeService(context, _OrthancPluginService_SetHttpErrorDetails, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char** result;
+    const char*  argument;
+  } _OrthancPluginRetrieveStaticString;
+
+  /**
+   * @brief Detect the MIME type of a file.
+   *
+   * This function returns the MIME type of a file by inspecting its extension.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param path Path to the file.
+   * @return The MIME type. This is a statically-allocated
+   * string, do not free it.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.0")
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginAutodetectMimeType(
+    OrthancPluginContext*  context,
+    const char*            path)
+  {
+    const char* result = NULL;
+
+    _OrthancPluginRetrieveStaticString params;
+    params.result = &result;
+    params.argument = path;
+
+    if (context->InvokeService(context, _OrthancPluginService_AutodetectMimeType, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    const char*               name;
+    float                     value;
+    OrthancPluginMetricsType  type;
+  } _OrthancPluginSetMetricsValue;
+
+  /**
+   * @brief Set the value of a floating-point metrics.
+   *
+   * This function sets the value of a floating-point metrics to
+   * monitor the behavior of the plugin through tools such as
+   * Prometheus. The values of all the metrics are stored within the
+   * Orthanc context.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param name The name of the metrics to be set.
+   * @param value The value of the metrics.
+   * @param type The type of the metrics. This parameter is only taken into consideration
+   * the first time this metrics is set.
+   * @ingroup Toolbox
+   * @see OrthancPluginSetMetricsIntegerValue()
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue(
+    OrthancPluginContext*     context,
+    const char*               name,
+    float                     value,
+    OrthancPluginMetricsType  type)
+  {
+    _OrthancPluginSetMetricsValue params;
+    params.name = name;
+    params.value = value;
+    params.type = type;
+    context->InvokeService(context, _OrthancPluginService_SetMetricsValue, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRefreshMetricsCallback  callback;
+  } _OrthancPluginRegisterRefreshMetricsCallback;
+
+  /**
+   * @brief Register a callback to refresh the metrics.
+   *
+   * This function registers a callback to refresh the metrics. The
+   * callback must make calls to OrthancPluginSetMetricsValue() or
+   * OrthancPluginSetMetricsIntegerValue().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function to handle the refresh.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRefreshMetricsCallback(
+    OrthancPluginContext*               context,
+    OrthancPluginRefreshMetricsCallback callback)
+  {
+    _OrthancPluginRegisterRefreshMetricsCallback params;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRefreshMetricsCallback, &params);
+  }
+
+
+
+
+  typedef struct
+  {
+    char**                               target;
+    const void*                          dicom;
+    uint32_t                             dicomSize;
+    OrthancPluginDicomWebBinaryCallback  callback;
+  } _OrthancPluginEncodeDicomWeb;
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @see OrthancPluginCreateDicom()
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @deprecated OrthancPluginEncodeDicomWebJson2()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson(
+    OrthancPluginContext*                context,
+    const void*                          dicom,
+    uint32_t                             dicomSize,
+    OrthancPluginDicomWebBinaryCallback  callback)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @return The NULL value in case of error, or the XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @deprecated OrthancPluginEncodeDicomWebXml2()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.4")
+  ORTHANC_PLUGIN_DEPRECATED ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml(
+    OrthancPluginContext*                context,
+    const void*                          dicom,
+    uint32_t                             dicomSize,
+    OrthancPluginDicomWebBinaryCallback  callback)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+
+  typedef struct
+  {
+    char**                                target;
+    const void*                           dicom;
+    uint32_t                              dicomSize;
+    OrthancPluginDicomWebBinaryCallback2  callback;
+    void*                                 payload;
+  } _OrthancPluginEncodeDicomWeb2;
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson2(
+    OrthancPluginContext*                 context,
+    const void*                           dicom,
+    uint32_t                              dicomSize,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb2 params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson2, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2(
+    OrthancPluginContext*                 context,
+    const void*                           dicom,
+    uint32_t                              dicomSize,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb2 params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml2, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+
+  /**
+   * @brief Callback executed when a HTTP header is received during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, as soon as it
+   * receives one HTTP header from the answer of the remote HTTP
+   * server.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param answer The user payload, as provided by the calling plugin.
+   * @param key The key of the HTTP header.
+   * @param value The value of the HTTP header.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) (
+    void* answer,
+    const char* key,
+    const char* value);
+
+
+  /**
+   * @brief Callback executed when an answer chunk is received during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, as soon as it
+   * receives one data chunk from the answer of the remote HTTP
+   * server.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param answer The user payload, as provided by the calling plugin.
+   * @param data The content of the data chunk.
+   * @param size The size of the data chunk.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) (
+    void* answer,
+    const void* data,
+    uint32_t size);
+  
+
+  /**
+   * @brief Callback to know whether the request body is entirely read during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must answer "1" as
+   * soon as the body is entirely read: The "request" data structure
+   * must act as an iterator.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return "1" if the body is over, or "0" if there is still data to be read.
+   * @ingroup Toolbox
+   **/
+  typedef uint8_t (*OrthancPluginChunkedClientRequestIsDone) (void* request);
+
+
+  /**
+   * @brief Callback to advance in the request body during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. This function asks the plugin
+   * to advance to the next chunk of data of the request body: The
+   * "request" data structure must act as an iterator.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientRequestNext) (void* request);
+
+
+  /**
+   * @brief Callback to read the current chunk of the request body during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must provide the
+   * content of the current chunk of data of the request body.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return The content of the current request chunk.
+   * @ingroup Toolbox
+   **/
+  typedef const void* (*OrthancPluginChunkedClientRequestGetChunkData) (void* request);
+
+
+  /**
+   * @brief Callback to read the size of the current request chunk during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must provide the
+   * size of the current chunk of data of the request body.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return The size of the current request chunk.
+   * @ingroup Toolbox
+   **/
+  typedef uint32_t (*OrthancPluginChunkedClientRequestGetChunkSize) (void* request);
+
+  
+  typedef struct
+  {
+    void*                                          answer;
+    OrthancPluginChunkedClientAnswerAddChunk       answerAddChunk;
+    OrthancPluginChunkedClientAnswerAddHeader      answerAddHeader;
+    uint16_t*                                      httpStatus;
+    OrthancPluginHttpMethod                        method;
+    const char*                                    url;
+    uint32_t                                       headersCount;
+    const char* const*                             headersKeys;
+    const char* const*                             headersValues;
+    void*                                          request;
+    OrthancPluginChunkedClientRequestIsDone        requestIsDone;
+    OrthancPluginChunkedClientRequestGetChunkData  requestChunkData;
+    OrthancPluginChunkedClientRequestGetChunkSize  requestChunkSize;
+    OrthancPluginChunkedClientRequestNext          requestNext;
+    const char*                                    username;
+    const char*                                    password;
+    uint32_t                                       timeout;
+    const char*                                    certificateFile;
+    const char*                                    certificateKeyFile;
+    const char*                                    certificateKeyPassword;
+    uint8_t                                        pkcs11;
+  } _OrthancPluginChunkedHttpClient;
+
+  
+  /**
+   * @brief Issue a HTTP call, using chunked HTTP transfers.
+   * 
+   * Make a HTTP call to the given URL using chunked HTTP
+   * transfers. The request body is provided as an iterator over data
+   * chunks. The answer is provided as a sequence of function calls
+   * with the individual HTTP headers and answer chunks.
+   * 
+   * Contrarily to OrthancPluginHttpClient() that entirely stores the
+   * request body and the answer body in memory buffers, this function
+   * uses chunked HTTP transfers. This results in a lower memory
+   * consumption. Pay attention to the fact that Orthanc servers with
+   * version <= 1.5.6 do not support chunked transfers: You must use
+   * OrthancPluginHttpClient() if contacting such older servers.
+   *
+   * The HTTP request will be done accordingly to the global
+   * configuration of Orthanc (in particular, the options "HttpProxy",
+   * "HttpTimeout", "HttpsVerifyPeers", "HttpsCACertificates", and
+   * "Pkcs11" will be taken into account).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answer The user payload for the answer body. It will be provided to the callbacks for the answer.
+   * @param answerAddChunk Callback function to report a data chunk from the answer body.
+   * @param answerAddHeader Callback function to report an HTTP header sent by the remote server.
+   * @param httpStatus The HTTP status after the execution of the request (out argument).
+   * @param method HTTP method to be used.
+   * @param url The URL of interest.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param request The user payload containing the request body, and acting as an iterator.
+   * It will be provided to the callbacks for the request.
+   * @param requestIsDone Callback function to tell whether the request body is entirely read.
+   * @param requestChunkData Callback function to get the content of the current data chunk of the request body.
+   * @param requestChunkSize Callback function to get the size of the current data chunk of the request body.
+   * @param requestNext Callback function to advance to the next data chunk of the request body.
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @param timeout Timeout in seconds (0 for default timeout).
+   * @param certificateFile Path to the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyPassword Password to unlock the key of the client certificate 
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginHttpClient()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.7")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginChunkedHttpClient(
+    OrthancPluginContext*                          context,
+    void*                                          answer,
+    OrthancPluginChunkedClientAnswerAddChunk       answerAddChunk,
+    OrthancPluginChunkedClientAnswerAddHeader      answerAddHeader,
+    uint16_t*                                      httpStatus,
+    OrthancPluginHttpMethod                        method,
+    const char*                                    url,
+    uint32_t                                       headersCount,
+    const char* const*                             headersKeys,
+    const char* const*                             headersValues,
+    void*                                          request,
+    OrthancPluginChunkedClientRequestIsDone        requestIsDone,
+    OrthancPluginChunkedClientRequestGetChunkData  requestChunkData,
+    OrthancPluginChunkedClientRequestGetChunkSize  requestChunkSize,
+    OrthancPluginChunkedClientRequestNext          requestNext,
+    const char*                                    username,
+    const char*                                    password,
+    uint32_t                                       timeout,
+    const char*                                    certificateFile,
+    const char*                                    certificateKeyFile,
+    const char*                                    certificateKeyPassword,
+    uint8_t                                        pkcs11)
+  {
+    _OrthancPluginChunkedHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    /* In common with OrthancPluginHttpClient() */
+    params.httpStatus = httpStatus;
+    params.method = method;
+    params.url = url;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.username = username;
+    params.password = password;
+    params.timeout = timeout;
+    params.certificateFile = certificateFile;
+    params.certificateKeyFile = certificateKeyFile;
+    params.certificateKeyPassword = certificateKeyPassword;
+    params.pkcs11 = pkcs11;
+
+    /* For chunked body/answer */
+    params.answer = answer;
+    params.answerAddChunk = answerAddChunk;
+    params.answerAddHeader = answerAddHeader;
+    params.request = request;
+    params.requestIsDone = requestIsDone;
+    params.requestChunkData = requestChunkData;
+    params.requestChunkSize = requestChunkSize;
+    params.requestNext = requestNext;
+
+    return context->InvokeService(context, _OrthancPluginService_ChunkedHttpClient, &params);
+  }
+
+
+
+  /**
+   * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer.
+   * @ingroup Callbacks
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.5.7")
+    _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader;
+
+
+
+  /**
+   * @brief Callback to create a reader to handle incoming chunked HTTP transfers.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is only invoked if the HTTP method is POST or PUT. The
+   * callback must create an user-specific "reader" object that will
+   * be fed with the body of the incoming body.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader Memory location that must be filled with the newly-created reader.
+   * @param uri The URI that is accessed.
+   * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used.
+   * @return 0 if success, or the error code if failure.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) (
+    OrthancPluginServerChunkedRequestReader**  reader,
+    const char*                                uri,
+    const OrthancPluginHttpRequest*            request);
+
+  
+  /**
+   * @brief Callback invoked whenever a new data chunk is available during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This callback
+   * is invoked as soon as a new data chunk is available for the request body.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   * @param data The content of the data chunk.
+   * @param size The size of the data chunk.
+   * @return 0 if success, or the error code if failure.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderAddChunk) (
+    OrthancPluginServerChunkedRequestReader* reader,
+    const void*                              data,
+    uint32_t                                 size);
+    
+
+  /**
+   * @brief Callback invoked whenever the request body is entirely received.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is invoked as soon as the full body of the HTTP request
+   * is available. The plugin can then send its answer thanks to the
+   * provided "output" object.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   * @param output The HTTP connection to the client application.
+   * @return 0 if success, or the error code if failure.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderExecute) (
+    OrthancPluginServerChunkedRequestReader* reader,
+    OrthancPluginRestOutput*                 output);
+    
+
+  /**
+   * @brief Callback invoked to release the resources associated with an incoming HTTP chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is invoked to release all the resources allocated by the
+   * given reader. Note that this function might be invoked even if
+   * the entire body was not read, to deal with client error or
+   * disconnection.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   **/
+  typedef void (*OrthancPluginServerChunkedRequestReaderFinalize) (
+    OrthancPluginServerChunkedRequestReader* reader);
+  
+  typedef struct
+  {
+    const char*                                      pathRegularExpression;
+    OrthancPluginRestCallback                        getHandler;
+    OrthancPluginServerChunkedRequestReaderFactory   postHandler;
+    OrthancPluginRestCallback                        deleteHandler;
+    OrthancPluginServerChunkedRequestReaderFactory   putHandler;
+    OrthancPluginServerChunkedRequestReaderAddChunk  addChunk;
+    OrthancPluginServerChunkedRequestReaderExecute   execute;
+    OrthancPluginServerChunkedRequestReaderFinalize  finalize;
+  } _OrthancPluginChunkedRestCallback;
+
+
+  /**
+   * @brief Register a REST callback to handle chunked HTTP transfers.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   *
+   * Contrarily to OrthancPluginRegisterRestCallback(), the callbacks
+   * will NOT be invoked in mutual exclusion, so it is up to the
+   * plugin to implement the required locking mechanisms.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
+   * @param getHandler The callback function to handle REST calls using the GET HTTP method.
+   * @param postHandler The callback function to handle REST calls using the POST HTTP method.
+   * @param deleteHandler The callback function to handle REST calls using the DELETE HTTP method.
+   * @param putHandler The callback function to handle REST calls using the PUT HTTP method.
+   * @param addChunk The callback invoked when a new chunk is available for the request body of a POST or PUT call.
+   * @param execute The callback invoked once the entire body of a POST or PUT call is read.
+   * @param finalize The callback invoked to release the resources associated with a POST or PUT call.
+   * @see OrthancPluginRegisterRestCallbackNoLock()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.7")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterChunkedRestCallback(
+    OrthancPluginContext*                            context,
+    const char*                                      pathRegularExpression,
+    OrthancPluginRestCallback                        getHandler,
+    OrthancPluginServerChunkedRequestReaderFactory   postHandler,
+    OrthancPluginRestCallback                        deleteHandler,
+    OrthancPluginServerChunkedRequestReaderFactory   putHandler,
+    OrthancPluginServerChunkedRequestReaderAddChunk  addChunk,
+    OrthancPluginServerChunkedRequestReaderExecute   execute,
+    OrthancPluginServerChunkedRequestReaderFinalize  finalize)
+  {
+    _OrthancPluginChunkedRestCallback params;
+    params.pathRegularExpression = pathRegularExpression;
+    params.getHandler = getHandler;
+    params.postHandler = postHandler;
+    params.deleteHandler = deleteHandler;
+    params.putHandler = putHandler;
+    params.addChunk = addChunk;
+    params.execute = execute;
+    params.finalize = finalize;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterChunkedRestCallback, &params);
+  }
+
+
+
+
+
+  typedef struct
+  {
+    char**       result;
+    uint16_t     group;
+    uint16_t     element;
+    const char*  privateCreator;
+  } _OrthancPluginGetTagName;
+
+  /**
+   * @brief Returns the symbolic name of a DICOM tag.
+   *
+   * This function makes a lookup to the dictionary of DICOM tags that
+   * are known to Orthanc, and returns the symbolic name of a DICOM tag.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag.
+   * @param element The element of the tag.
+   * @param privateCreator For private tags, the name of the private creator (can be NULL).
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.5.7")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetTagName(
+    OrthancPluginContext*  context,
+    uint16_t               group,
+    uint16_t               element,
+    const char*            privateCreator)
+  {
+    char* result;
+
+    _OrthancPluginGetTagName params;
+    params.result = &result;
+    params.group = group;
+    params.element = element;
+    params.privateCreator = privateCreator;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetTagName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  /**
+   * @brief Callback executed by the storage commitment SCP.
+   *
+   * Signature of a factory function that creates an object to handle
+   * one incoming storage commitment request.
+   *
+   * @remark The factory receives the list of the SOP class/instance
+   * UIDs of interest to the remote storage commitment SCU. This gives
+   * the factory the possibility to start some prefetch process
+   * upfront in the background, before the handler object is actually
+   * queried about the status of these DICOM instances.
+   *
+   * @param handler Output variable where the factory puts the handler object it created.
+   * @param jobId ID of the Orthanc job that is responsible for handling 
+   * the storage commitment request. This job will successively look for the
+   * status of all the individual queried DICOM instances.
+   * @param transactionUid UID of the storage commitment transaction
+   * provided by the storage commitment SCU. It contains the value of the
+   * (0008,1195) DICOM tag.
+   * @param sopClassUids Array of the SOP class UIDs (0008,0016) that are queried by the SCU.
+   * @param sopInstanceUids Array of the SOP instance UIDs (0008,0018) that are queried by the SCU.
+   * @param countInstances Number of DICOM instances that are queried. This is the size
+   * of the `sopClassUids` and `sopInstanceUids` arrays.
+   * @param remoteAet The AET of the storage commitment SCU.
+   * @param calledAet The AET used by the SCU to contact the storage commitment SCP (i.e. Orthanc).
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentFactory) (
+    void**              handler /* out */,
+    const char*         jobId,
+    const char*         transactionUid,
+    const char* const*  sopClassUids,
+    const char* const*  sopInstanceUids,
+    uint32_t            countInstances,
+    const char*         remoteAet,
+    const char*         calledAet);
+
+  
+  /**
+   * @brief Callback to free one storage commitment SCP handler.
+   * 
+   * Signature of a callback function that releases the resources
+   * allocated by the factory of the storage commitment SCP. The
+   * handler is the return value of a previous call to the
+   * OrthancPluginStorageCommitmentFactory() callback.
+   *
+   * @param handler The handler object to be destructed.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler);
+
+
+  /**
+   * @brief Callback to get the status of one DICOM instance in the
+   * storage commitment SCP.
+   *
+   * Signature of a callback function that is successively invoked for
+   * each DICOM instance that is queried by the remote storage
+   * commitment SCU.  The function must be tought of as a method of
+   * the handler object that was created by a previous call to the
+   * OrthancPluginStorageCommitmentFactory() callback. After each call
+   * to this method, the progress of the associated Orthanc job is
+   * updated.
+   * 
+   * @param target Output variable where to put the status for the queried instance.
+   * @param handler The handler object associated with this storage commitment request.
+   * @param sopClassUid The SOP class UID (0008,0016) of interest.
+   * @param sopInstanceUid The SOP instance UID (0008,0018) of interest.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) (
+    OrthancPluginStorageCommitmentFailureReason* target,
+    void* handler,
+    const char* sopClassUid,
+    const char* sopInstanceUid);
+    
+    
+  typedef struct
+  {
+    OrthancPluginStorageCommitmentFactory     factory;
+    OrthancPluginStorageCommitmentDestructor  destructor;
+    OrthancPluginStorageCommitmentLookup      lookup;
+  } _OrthancPluginRegisterStorageCommitmentScpCallback;
+
+  /**
+   * @brief Register a callback to handle incoming requests to the storage commitment SCP.
+   *
+   * This function registers a callback to handle storage commitment SCP requests.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param factory Factory function that creates the handler object
+   * for incoming storage commitment requests.
+   * @param destructor Destructor function to destroy the handler object.
+   * @param lookup Callback function to get the status of one DICOM instance.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.6.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterStorageCommitmentScpCallback(
+    OrthancPluginContext*                     context,
+    OrthancPluginStorageCommitmentFactory     factory,
+    OrthancPluginStorageCommitmentDestructor  destructor,
+    OrthancPluginStorageCommitmentLookup      lookup)
+  {
+    _OrthancPluginRegisterStorageCommitmentScpCallback params;
+    params.factory = factory;
+    params.destructor = destructor;
+    params.lookup = lookup;
+    return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, &params);
+  }
+  
+
+
+  /**
+   * @brief Callback to filter incoming DICOM instances received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a new DICOM instance (e.g. through REST API or
+   * DICOM protocol), and that answers whether this DICOM instance
+   * should be accepted or discarded by Orthanc.
+   *
+   * Note that the metadata information is not available
+   * (i.e. GetInstanceMetadata() should not be used on "instance").
+   *
+   * @warning Your callback function will be called synchronously with
+   * the core of Orthanc. This implies that deadlocks might emerge if
+   * you call other core primitives of Orthanc in your callback (such
+   * deadlocks are particularly visible in the presence of other plugins
+   * or Lua scripts). It is thus strongly advised to avoid any call to
+   * the REST API of Orthanc in the callback. If you have to call
+   * other primitives of Orthanc, you should make these calls in a
+   * separate thread, passing the pending events to be processed
+   * through a message queue.
+   * 
+   * @param instance The received DICOM instance.
+   * @return 0 to discard the instance, 1 to store the instance, -1 if error.
+   * @ingroup Callbacks
+   **/
+  typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) (
+    const OrthancPluginDicomInstance* instance);
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingDicomInstanceFilter callback;
+  } _OrthancPluginIncomingDicomInstanceFilter;
+
+  /**
+   * @brief Register a callback to filter incoming DICOM instances.
+   *
+   * This function registers a custom callback to filter incoming
+   * DICOM instances received by Orthanc (either through the REST API
+   * or through the DICOM protocol).
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.6.1")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingDicomInstanceFilter(
+    OrthancPluginContext*                     context,
+    OrthancPluginIncomingDicomInstanceFilter  callback)
+  {
+    _OrthancPluginIncomingDicomInstanceFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingDicomInstanceFilter, &params);
+  }
+
+
+  /**
+   * @brief Callback to filter incoming DICOM instances received by 
+   * Orthanc through C-STORE.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a new DICOM instance using DICOM C-STORE, and
+   * that answers whether this DICOM instance should be accepted or
+   * discarded by Orthanc. If the instance is discarded, the callback
+   * can specify the DIMSE error code answered by the Orthanc C-STORE
+   * SCP.
+   *
+   * Note that the metadata information is not available
+   * (i.e. GetInstanceMetadata() should not be used on "instance").
+   *
+   * @warning Your callback function will be called synchronously with
+   * the core of Orthanc. This implies that deadlocks might emerge if
+   * you call other core primitives of Orthanc in your callback (such
+   * deadlocks are particularly visible in the presence of other plugins
+   * or Lua scripts). It is thus strongly advised to avoid any call to
+   * the REST API of Orthanc in the callback. If you have to call
+   * other primitives of Orthanc, you should make these calls in a
+   * separate thread, passing the pending events to be processed
+   * through a message queue.
+   *
+   * @param dimseStatus If the DICOM instance is discarded, 
+   * DIMSE status to be sent by the C-STORE SCP of Orthanc
+   * @param instance The received DICOM instance.
+   * @return 0 to discard the instance, 1 to store the instance, -1 if error.
+   * @ingroup Callbacks
+   **/
+  typedef int32_t (*OrthancPluginIncomingCStoreInstanceFilter) (
+    uint16_t* dimseStatus /* out */,
+    const OrthancPluginDicomInstance* instance);
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingCStoreInstanceFilter callback;
+  } _OrthancPluginIncomingCStoreInstanceFilter;
+
+  /**
+   * @brief Register a callback to filter incoming DICOM instances
+   * received by Orthanc through C-STORE.
+   *
+   * This function registers a custom callback to filter incoming
+   * DICOM instances received by Orthanc through the DICOM protocol.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.10.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingCStoreInstanceFilter(
+    OrthancPluginContext*                      context,
+    OrthancPluginIncomingCStoreInstanceFilter  callback)
+  {
+    _OrthancPluginIncomingCStoreInstanceFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingCStoreInstanceFilter, &params);
+  }
+
+  /**
+   * @brief Callback to keep/discard/modify a DICOM instance received
+   * by Orthanc from any source (C-STORE or REST API)
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a new DICOM instance (through DICOM protocol or
+   * REST API), and that specifies an action to be applied to this
+   * newly received instance. The instance can be kept as it is, can
+   * be modified by the plugin, or can be discarded.
+   *
+   * This callback is invoked immediately after reception, i.e. before
+   * transcoding and before filtering
+   * (cf. OrthancPluginRegisterIncomingDicomInstanceFilter()).
+   *
+   * @warning Your callback function will be called synchronously with
+   * the core of Orthanc. This implies that deadlocks might emerge if
+   * you call other core primitives of Orthanc in your callback (such
+   * deadlocks are particularly visible in the presence of other plugins
+   * or Lua scripts). It is thus strongly advised to avoid any call to
+   * the REST API of Orthanc in the callback. If you have to call
+   * other primitives of Orthanc, you should make these calls in a
+   * separate thread, passing the pending events to be processed
+   * through a message queue.
+   *
+   * @param modifiedDicomBuffer A buffer containing the modified DICOM (output).
+   * This buffer must be allocated using OrthancPluginCreateMemoryBuffer64()
+   * and will be freed by the Orthanc core.
+   * @param receivedDicomBuffer A buffer containing the received DICOM (input).
+   * @param receivedDicomBufferSize The size of the received DICOM (input).
+   * @param origin The origin of the DICOM instance (input).
+   * @return `OrthancPluginReceivedInstanceAction_KeepAsIs` to accept the instance as is,<br/>
+   *         `OrthancPluginReceivedInstanceAction_Modify` to store the modified DICOM contained in `modifiedDicomBuffer`,<br/>
+   *         `OrthancPluginReceivedInstanceAction_Discard` to tell Orthanc to discard the instance.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginReceivedInstanceAction (*OrthancPluginReceivedInstanceCallback) (
+    OrthancPluginMemoryBuffer64* modifiedDicomBuffer,
+    const void* receivedDicomBuffer,
+    uint64_t receivedDicomBufferSize,
+    OrthancPluginInstanceOrigin origin);
+
+
+  typedef struct
+  {
+    OrthancPluginReceivedInstanceCallback callback;
+  } _OrthancPluginReceivedInstanceCallback;
+
+  /**
+   * @brief Register a callback to keep/discard/modify a DICOM instance received
+   * by Orthanc from any source (C-STORE or REST API)
+   *
+   * This function registers a custom callback to keep/discard/modify
+   * incoming DICOM instances received by Orthanc from any source
+   * (C-STORE or REST API).
+   * 
+   * @warning Contrarily to
+   * OrthancPluginRegisterIncomingCStoreInstanceFilter() and
+   * OrthancPluginRegisterIncomingDicomInstanceFilter() that can be
+   * called by multiple plugins,
+   * OrthancPluginRegisterReceivedInstanceCallback() can only be used
+   * by one single plugin.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.10.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterReceivedInstanceCallback(
+    OrthancPluginContext*                     context,
+    OrthancPluginReceivedInstanceCallback     callback)
+  {
+    _OrthancPluginReceivedInstanceCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterReceivedInstanceCallback, &params);
+  }
+
+  /**
+   * @brief Get the transfer syntax of a DICOM file.
+   *
+   * This function returns a pointer to a newly created string that
+   * contains the transfer syntax UID of the DICOM instance. The empty
+   * string might be returned if this information is unknown.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the
+   * transfer syntax UID. This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.6.1")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceTransferSyntaxUid, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Check whether the DICOM file has pixel data.
+   *
+   * This function returns a Boolean value indicating whether the
+   * DICOM instance contains the pixel data (7FE0,0010) tag.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return "1" if the DICOM instance contains pixel data, or "0" if
+   * the tag is missing, or "-1" in the case of an error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.6.1")
+  ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    int64_t hasPixelData;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &hasPixelData;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_HasInstancePixelData, &params) != OrthancPluginErrorCode_Success ||
+        hasPixelData < 0 ||
+        hasPixelData > 1)
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return (hasPixelData != 0);
+    }
+  }
+
+
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginDicomInstance**  target;
+    const void*                   buffer;
+    uint32_t                      size;
+    const char*                   transferSyntax;
+  } _OrthancPluginCreateDicomInstance;
+
+  /**
+   * @brief Parse a DICOM instance.
+   *
+   * This function parses a memory buffer that contains a DICOM
+   * file. The function returns a new pointer to a data structure that
+   * is managed by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM instance.
+   * @param size The size of the memory buffer.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size)
+  {
+    OrthancPluginDicomInstance* target = NULL;
+
+    _OrthancPluginCreateDicomInstance params;
+    params.target = &target;
+    params.buffer = buffer;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateDicomInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+  typedef struct
+  {
+    OrthancPluginDicomInstance*   dicom;
+  } _OrthancPluginFreeDicomInstance;
+
+  /**
+   * @brief Free a DICOM instance.
+   *
+   * This function frees a DICOM instance that was parsed using
+   * OrthancPluginCreateDicomInstance().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom The DICOM instance.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeDicomInstance(
+    OrthancPluginContext*        context, 
+    OrthancPluginDicomInstance*  dicom)
+  {
+    _OrthancPluginFreeDicomInstance params;
+    params.dicom = dicom;
+
+    context->InvokeService(context, _OrthancPluginService_FreeDicomInstance, &params);
+  }
+
+
+  typedef struct
+  {
+    uint32_t*                             targetUint32;
+    OrthancPluginMemoryBuffer*            targetBuffer;
+    OrthancPluginImage**                  targetImage;
+    char**                                targetStringToFree;
+    const OrthancPluginDicomInstance*     instance;
+    uint32_t                              frameIndex;
+    OrthancPluginDicomToJsonFormat        format;
+    OrthancPluginDicomToJsonFlags         flags;
+    uint32_t                              maxStringLength;
+    OrthancPluginDicomWebBinaryCallback2  dicomWebCallback;
+    void*                                 dicomWebPayload;
+  } _OrthancPluginAccessDicomInstance2;
+
+  /**
+   * @brief Get the number of frames in a DICOM instance.
+   *
+   * This function returns the number of frames that are part of a
+   * DICOM image managed by the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The number of frames (will be zero in the case of an error).
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    uint32_t count;
+
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetUint32 = &count;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceFramesCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+  /**
+   * @brief Get the raw content of a frame in a DICOM instance.
+   *
+   * This function returns a memory buffer containing the raw content
+   * of a frame in a DICOM instance that is managed by the Orthanc
+   * core. This is notably useful for compressed transfer syntaxes, as
+   * it gives access to the embedded files (such as JPEG, JPEG-LS or
+   * JPEG2k). The Orthanc core transparently reassembles the fragments
+   * to extract the raw frame.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param instance The instance of interest.
+   * @param frameIndex The index of the frame of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame(
+    OrthancPluginContext*             context,
+    OrthancPluginMemoryBuffer*        target,
+    const OrthancPluginDicomInstance* instance,
+    uint32_t                          frameIndex)
+  {
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetBuffer = target;
+    params.instance = instance;
+    params.frameIndex = frameIndex;
+
+    return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, &params);
+  }
+
+
+  /**
+   * @brief Decode one frame from a DICOM instance.
+   *
+   * This function decodes one frame of a DICOM image that is managed
+   * by the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param frameIndex The index of the frame of interest.
+   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance,
+    uint32_t                          frameIndex)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetImage = &target;
+    params.instance = instance;
+    params.frameIndex = frameIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDecodedFrame, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+  
+  /**
+   * @brief Parse and transcode a DICOM instance.
+   *
+   * This function parses a memory buffer that contains a DICOM file,
+   * then transcodes it to the given transfer syntax. The function
+   * returns a new pointer to a data structure that is managed by the
+   * Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM instance.
+   * @param size The size of the memory buffer.
+   * @param transferSyntax The transfer syntax UID for the transcoding.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size,
+    const char*            transferSyntax)
+  {
+    OrthancPluginDicomInstance* target = NULL;
+
+    _OrthancPluginCreateDicomInstance params;
+    params.target = &target;
+    params.buffer = buffer;
+    params.size = size;
+    params.transferSyntax = transferSyntax;
+
+    if (context->InvokeService(context, _OrthancPluginService_TranscodeDicomInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+  /**
+   * @brief Writes a DICOM instance to a memory buffer.
+   *
+   * This function returns a memory buffer containing the
+   * serialization of a DICOM instance that is managed by the Orthanc
+   * core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param instance The instance of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance(
+    OrthancPluginContext*             context,
+    OrthancPluginMemoryBuffer*        target,
+    const OrthancPluginDicomInstance* instance)
+  {
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetBuffer = target;
+    params.instance = instance;
+
+    return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, &params);
+  }
+  
+
+  /**
+   * @brief Format a DICOM memory buffer as a JSON string.
+   *
+   * This function takes as DICOM instance managed by the Orthanc
+   * core, and outputs a JSON string representing the tags of this
+   * DICOM file.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @param format The output format.
+   * @param flags Flags governing the output.
+   * @param maxStringLength The maximum length of a field. Too long fields will
+   * be output as "null". The 0 value means no maximum length.
+   * @return The NULL value if the case of an error, or the JSON
+   * string. This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   * @see OrthancPluginDicomBufferToJson()
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    OrthancPluginDicomToJsonFormat     format,
+    OrthancPluginDicomToJsonFlags      flags, 
+    uint32_t                           maxStringLength)
+  {
+    char* result = NULL;
+
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetStringToFree = &result;
+    params.instance = instance;
+    params.format = format;
+    params.flags = flags;
+    params.maxStringLength = maxStringLength;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceAdvancedJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a DICOM instance that is managed by the
+   * Orthanc core, into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson(
+    OrthancPluginContext*                 context,
+    const OrthancPluginDicomInstance*     instance,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginAccessDicomInstance2 params;
+    params.targetStringToFree = &target;
+    params.instance = instance;
+    params.dicomWebCallback = callback;
+    params.dicomWebPayload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a DICOM instance that is managed by the
+   * Orthanc core, into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml(
+    OrthancPluginContext*                 context,
+    const OrthancPluginDicomInstance*     instance,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginAccessDicomInstance2 params;
+    params.targetStringToFree = &target;
+    params.instance = instance;
+    params.dicomWebCallback = callback;
+    params.dicomWebPayload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebXml, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Signature of a callback function to transcode a DICOM instance.
+   * @param transcoded Target memory buffer. It must be allocated by the
+   * plugin using OrthancPluginCreateMemoryBuffer().
+   * @param buffer Memory buffer containing the source DICOM instance.
+   * @param size Size of the source memory buffer.
+   * @param allowedSyntaxes A C array of possible transfer syntaxes UIDs for the
+   * result of the transcoding. The plugin must choose by itself the 
+   * transfer syntax that will be used for the resulting DICOM image.
+   * @param countSyntaxes The number of transfer syntaxes that are contained
+   * in the "allowedSyntaxes" array.
+   * @param allowNewSopInstanceUid Whether the transcoding plugin can select
+   * a transfer syntax that will change the SOP instance UID (or, in other 
+   * terms, whether the plugin can transcode using lossy compression).
+   * @return 0 if success (i.e. image successfully transcoded and stored into
+   * "transcoded"), or the error code if failure.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginTranscoderCallback) (
+    OrthancPluginMemoryBuffer* transcoded /* out */,
+    const void*                buffer,
+    uint64_t                   size,
+    const char* const*         allowedSyntaxes,
+    uint32_t                   countSyntaxes,
+    uint8_t                    allowNewSopInstanceUid);
+
+
+  typedef struct
+  {
+    OrthancPluginTranscoderCallback callback;
+  } _OrthancPluginTranscoderCallback;
+
+  /**
+   * @brief Register a callback to handle the transcoding of DICOM images.
+   *
+   * This function registers a custom callback to transcode DICOM
+   * images, extending the built-in transcoder of Orthanc that uses
+   * DCMTK. The exact behavior is affected by the configuration option
+   * "BuiltinDecoderTranscoderOrder" of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterTranscoderCallback(
+    OrthancPluginContext*            context,
+    OrthancPluginTranscoderCallback  callback)
+  {
+    _OrthancPluginTranscoderCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterTranscoderCallback, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    uint32_t                    size;
+  } _OrthancPluginCreateMemoryBuffer;
+
+  /**
+   * @brief Create a 32-bit memory buffer.
+   *
+   * This function creates a memory buffer that is managed by the
+   * Orthanc core. The main use case of this function is for plugins
+   * that act as DICOM transcoders.
+   * 
+   * Your plugin should never call "free()" on the resulting memory
+   * buffer, as the C library that is used by the plugin is in general
+   * not the same as the one used by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param size Size of the memory buffer to be created.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.7.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    uint32_t                    size)
+  {
+    _OrthancPluginCreateMemoryBuffer params;
+    params.target = target;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer, &params);
+  }
+  
+
+  /**
+   * @brief Generate a token to grant full access to the REST API of Orthanc.
+   *
+   * This function generates a token that can be set in the HTTP
+   * header "Authorization" so as to grant full access to the REST API
+   * of Orthanc using an external HTTP client. Using this function
+   * avoids the need of adding a separate user in the
+   * "RegisteredUsers" configuration of Orthanc, which eases
+   * deployments.
+   *
+   * This feature is notably useful in multiprocess scenarios, where a
+   * subprocess created by a plugin has no access to the
+   * "OrthancPluginContext", and thus cannot call
+   * "OrthancPluginRestApi[Get|Post|Put|Delete]()".
+   *
+   * This situation is frequently encountered in Python plugins, where
+   * the "multiprocessing" package can be used to bypass the Global
+   * Interpreter Lock (GIL) and thus to improve performance and
+   * concurrency.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The authorization token, or NULL value in the case of an error.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.8.1")
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateRestApiAuthorizationToken(
+    OrthancPluginContext*  context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GenerateRestApiAuthorizationToken,
+                               &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer64*  target;
+    uint64_t                      size;
+  } _OrthancPluginCreateMemoryBuffer64;
+
+  /**
+   * @brief Create a 64-bit memory buffer.
+   *
+   * This function creates a 64-bit memory buffer that is managed by
+   * the Orthanc core. The main use case of this function is for
+   * plugins that read files from the storage area.
+   * 
+   * Your plugin should never call "free()" on the resulting memory
+   * buffer, as the C library that is used by the plugin is in general
+   * not the same as the one used by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param size Size of the memory buffer to be created.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.9.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer64(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer64*  target,
+    uint64_t                      size)
+  {
+    _OrthancPluginCreateMemoryBuffer64 params;
+    params.target = target;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer64, &params);
+  }
+  
+
+  typedef struct
+  {
+    OrthancPluginStorageCreate    create;
+    OrthancPluginStorageReadWhole readWhole;
+    OrthancPluginStorageReadRange readRange;
+    OrthancPluginStorageRemove    remove;
+  } _OrthancPluginRegisterStorageArea2;
+
+  /**
+   * @brief Register a custom storage area, with support for range request.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param readWhole The callback function to read a whole file from the custom storage area.
+   * @param readRange The callback function to read some range of a file from the custom storage area.
+   * If this feature is not supported by the plugin, this value can be set to NULL.
+   * @param remove The callback function to remove a file from the custom storage area.
+   * @ingroup Callbacks
+   * @deprecated New plugins should use OrthancPluginRegisterStorageArea3()
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.9.0")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea2(
+    OrthancPluginContext*          context,
+    OrthancPluginStorageCreate     create,
+    OrthancPluginStorageReadWhole  readWhole,
+    OrthancPluginStorageReadRange  readRange,
+    OrthancPluginStorageRemove     remove)
+  {
+    _OrthancPluginRegisterStorageArea2 params;
+    params.create = create;
+    params.readWhole = readWhole;
+    params.readRange = readRange;
+    params.remove = remove;
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea2, &params);
+  }
+
+
+
+  typedef struct
+  {
+    _OrthancPluginCreateDicom  createDicom;
+    const char*                privateCreator;
+  } _OrthancPluginCreateDicom2;
+
+  /**
+   * @brief Create a DICOM instance from a JSON string and an image, with a private creator.
+   *
+   * This function takes as input a string containing a JSON file
+   * describing the content of a DICOM instance. As an output, it
+   * writes the corresponding DICOM instance to a newly allocated
+   * memory buffer. Additionally, an image to be encoded within the
+   * DICOM instance can also be provided.
+   *
+   * Contrarily to the function OrthancPluginCreateDicom(), this
+   * function can be explicitly provided with a private creator.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param json The input JSON file.
+   * @param pixelData The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme.
+   * @param flags Flags governing the output.
+   * @param privateCreator The private creator to be used for the private DICOM tags.
+   * Check out the global configuration option "Dictionary" of Orthanc.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   * @see OrthancPluginCreateDicom()
+   * @see OrthancPluginDicomBufferToJson()
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.9.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom2(
+    OrthancPluginContext*          context,
+    OrthancPluginMemoryBuffer*     target,
+    const char*                    json,
+    const OrthancPluginImage*      pixelData,
+    OrthancPluginCreateDicomFlags  flags,
+    const char*                    privateCreator)
+  {
+    _OrthancPluginCreateDicom2 params;
+    params.createDicom.target = target;
+    params.createDicom.json = json;
+    params.createDicom.pixelData = pixelData;
+    params.createDicom.flags = flags;
+    params.privateCreator = privateCreator;
+
+    return context->InvokeService(context, _OrthancPluginService_CreateDicom2, &params);
+  }
+
+
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  answerBody;
+    OrthancPluginMemoryBuffer*  answerHeaders;
+    uint16_t*                   httpStatus;
+    OrthancPluginHttpMethod     method;
+    const char*                 uri;
+    uint32_t                    headersCount;
+    const char* const*          headersKeys;
+    const char* const*          headersValues;
+    const void*                 body;
+    uint32_t                    bodySize;
+    uint8_t                     afterPlugins;
+  } _OrthancPluginCallRestApi;
+
+  /**
+   * @brief Call the REST API of Orthanc with full flexibility.
+   * 
+   * Make a call to the given URI in the REST API of Orthanc. The
+   * result to the query is stored into a newly allocated memory
+   * buffer. This function is always granted full access to the REST
+   * API (no credentials, nor security token is needed).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answerBody The target memory buffer (out argument).
+   *        It must be freed with OrthancPluginFreeMemoryBuffer().
+   *        The value of this argument is ignored if the HTTP method is DELETE.
+   * @param answerHeaders The target memory buffer for the HTTP headers in the answer (out argument). 
+   *        The answer headers are formatted as a JSON object (associative array).
+   *        The buffer must be freed with OrthancPluginFreeMemoryBuffer().
+   *        This argument can be set to NULL if the plugin has no interest in the answer HTTP headers.
+   * @param httpStatus The HTTP status after the execution of the request (out argument).
+   * @param method HTTP method to be used.
+   * @param uri The URI of interest.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param body The HTTP body for a POST or PUT request.
+   * @param bodySize The size of the body.
+   * @param afterPlugins If 0, the built-in API of Orthanc is used.
+   * If 1, the API is tainted by the plugins.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginRestApiGet2(), OrthancPluginRestApiPost(), OrthancPluginRestApiPut(), OrthancPluginRestApiDelete()
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.9.2")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginCallRestApi(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  answerBody,
+    OrthancPluginMemoryBuffer*  answerHeaders,
+    uint16_t*                   httpStatus,
+    OrthancPluginHttpMethod     method,
+    const char*                 uri,
+    uint32_t                    headersCount,
+    const char* const*          headersKeys,
+    const char* const*          headersValues,
+    const void*                 body,
+    uint32_t                    bodySize,
+    uint8_t                     afterPlugins)
+  {
+    _OrthancPluginCallRestApi params;
+    memset(&params, 0, sizeof(params));
+
+    params.answerBody = answerBody;
+    params.answerHeaders = answerHeaders;
+    params.httpStatus = httpStatus;
+    params.method = method;
+    params.uri = uri;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.afterPlugins = afterPlugins;
+
+    return context->InvokeService(context, _OrthancPluginService_CallRestApi, &params);
+  }
+
+
+
+  /**
+   * @brief Opaque structure that represents a WebDAV collection.
+   * @ingroup Callbacks
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.10.1")
+    _OrthancPluginWebDavCollection_t OrthancPluginWebDavCollection;
+
+
+  /**
+   * @brief Declare a file while returning the content of a folder.
+   *
+   * This function declares a file while returning the content of a
+   * WebDAV folder.
+   *
+   * @param collection Context of the collection.
+   * @param name Base name of the file.
+   * @param dateTime The date and time of creation of the file.
+   * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information.
+   * @param size Size of the file.
+   * @param mimeType The MIME type of the file. If empty or set to `NULL`,
+   * Orthanc will do a best guess depending on the file extension.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFile) (
+    OrthancPluginWebDavCollection*  collection,
+    const char*                     name,
+    uint64_t                        size,
+    const char*                     mimeType,
+    const char*                     dateTime);
+
+  
+  /**
+   * @brief Declare a subfolder while returning the content of a folder.
+   *
+   * This function declares a subfolder while returning the content of a
+   * WebDAV folder.
+   *
+   * @param collection Context of the collection.
+   * @param name Base name of the subfolder.
+   * @param dateTime The date and time of creation of the subfolder.
+   * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFolder) (
+    OrthancPluginWebDavCollection*  collection,
+    const char*                     name,
+    const char*                     dateTime);
+
+
+  /**
+   * @brief Retrieve the content of a file.
+   *
+   * This function is used to forward the content of a file from a
+   * WebDAV collection, to the core of Orthanc.
+   *
+   * @param collection Context of the collection.
+   * @param data Content of the file.
+   * @param size Size of the file.
+   * @param mimeType The MIME type of the file. If empty or set to `NULL`,
+   * Orthanc will do a best guess depending on the file extension.
+   * @param dateTime The date and time of creation of the file.
+   * It must be formatted as an ISO string of form
+   * `YYYYMMDDTHHMMSS,fffffffff` where T is the date-time
+   * separator. It must be expressed in UTC (it is the responsibility
+   * of the plugin to do the possible timezone
+   * conversions). Internally, this string will be parsed using
+   * `boost::posix_time::from_iso_string()`.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFile) (
+    OrthancPluginWebDavCollection*  collection,
+    const void*                     data,
+    uint64_t                        size,
+    const char*                     mimeType,
+    const char*                     dateTime);
+
+  
+  /**
+   * @brief Callback for testing the existence of a folder.
+   *
+   * Signature of a callback function that tests whether the given
+   * path in the WebDAV collection exists and corresponds to a folder.
+   *
+   * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavIsExistingFolderCallback) (
+    uint8_t*                        isExisting, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback for listing the content of a folder.
+   *
+   * Signature of a callback function that lists the content of a
+   * folder in the WebDAV collection. The callback must call the
+   * provided `addFile()` and `addFolder()` functions to emit the
+   * content of the folder.
+   *
+   * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise.
+   * @param collection Context to be provided to `addFile()` and `addFolder()` functions.
+   * @param addFile Function to add a file to the list.
+   * @param addFolder Function to add a folder to the list.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavListFolderCallback) (
+    uint8_t*                        isExisting, /* out */
+    OrthancPluginWebDavCollection*  collection,
+    OrthancPluginWebDavAddFile      addFile,
+    OrthancPluginWebDavAddFolder    addFolder,
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback for retrieving the content of a file.
+   *
+   * Signature of a callback function that retrieves the content of a
+   * file in the WebDAV collection. The callback must call the
+   * provided `retrieveFile()` function to emit the actual content of
+   * the file.
+   *
+   * @param collection Context to be provided to `retrieveFile()` function.
+   * @param retrieveFile Function to return the content of the file.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFileCallback) (
+    OrthancPluginWebDavCollection*  collection,
+    OrthancPluginWebDavRetrieveFile retrieveFile,
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback to store a file.
+   *
+   * Signature of a callback function that stores a file into the
+   * WebDAV collection.
+   *
+   * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param data Content of the file to be stored.
+   * @param size Size of the file to be stored.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavStoreFileCallback) (
+    uint8_t*                        isReadOnly, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    const void*                     data,
+    uint64_t                        size,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback to create a folder.
+   *
+   * Signature of a callback function that creates a folder in the
+   * WebDAV collection.
+   *
+   * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavCreateFolderCallback) (
+    uint8_t*                        isReadOnly, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback to remove a file or a folder.
+   *
+   * Signature of a callback function that removes a file or a folder
+   * from the WebDAV collection.
+   *
+   * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavDeleteItemCallback) (
+    uint8_t*                        isReadOnly, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  typedef struct
+  {
+    const char*                                  uri;
+    OrthancPluginWebDavIsExistingFolderCallback  isExistingFolder;
+    OrthancPluginWebDavListFolderCallback        listFolder;
+    OrthancPluginWebDavRetrieveFileCallback      retrieveFile;
+    OrthancPluginWebDavStoreFileCallback         storeFile;
+    OrthancPluginWebDavCreateFolderCallback      createFolder;
+    OrthancPluginWebDavDeleteItemCallback        deleteItem;
+    void*                                        payload;
+  } _OrthancPluginRegisterWebDavCollection;
+
+  /**
+   * @brief Register a WebDAV virtual filesystem.
+   *
+   * This function maps a WebDAV collection onto the given URI in the
+   * REST API of Orthanc. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri URI where to map the WebDAV collection (must start with a `/` character).
+   * @param isExistingFolder Callback method to test for the existence of a folder.
+   * @param listFolder Callback method to list the content of a folder.
+   * @param retrieveFile Callback method to retrieve the content of a file.
+   * @param storeFile Callback method to store a file into the WebDAV collection.
+   * @param createFolder Callback method to create a folder.
+   * @param deleteItem Callback method to delete a file or a folder.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.10.1")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWebDavCollection(
+    OrthancPluginContext*                        context,
+    const char*                                  uri,
+    OrthancPluginWebDavIsExistingFolderCallback  isExistingFolder,
+    OrthancPluginWebDavListFolderCallback        listFolder,
+    OrthancPluginWebDavRetrieveFileCallback      retrieveFile,
+    OrthancPluginWebDavStoreFileCallback         storeFile,
+    OrthancPluginWebDavCreateFolderCallback      createFolder,
+    OrthancPluginWebDavDeleteItemCallback        deleteItem,
+    void*                                        payload)
+  {
+    _OrthancPluginRegisterWebDavCollection params;
+    params.uri = uri;
+    params.isExistingFolder = isExistingFolder;
+    params.listFolder = listFolder;
+    params.retrieveFile = retrieveFile;
+    params.storeFile = storeFile;
+    params.createFolder = createFolder;
+    params.deleteItem = deleteItem;
+    params.payload = payload;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterWebDavCollection, &params);
+  }
+
+
+  /**
+   * @brief Gets the DatabaseServerIdentifier.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return the database server identifier.  This is a statically-allocated
+   * string, do not free it.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.11.1")
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetDatabaseServerIdentifier(
+    OrthancPluginContext*  context)
+  {
+    const char* result;
+
+    _OrthancPluginRetrieveStaticString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetDatabaseServerIdentifier, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginStorageCreate2     create;
+    OrthancPluginStorageReadRange2  readRange;
+    OrthancPluginStorageRemove2     remove;
+  } _OrthancPluginRegisterStorageArea3;
+
+  /**
+   * @brief Register a custom storage area, with support for custom data.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param readRange The callback function to read some range of a file from the custom storage area.
+   * @param remove The callback function to remove a file from the custom storage area.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea3(
+    OrthancPluginContext*           context,
+    OrthancPluginStorageCreate2     create,
+    OrthancPluginStorageReadRange2  readRange,
+    OrthancPluginStorageRemove2     remove)
+  {
+    _OrthancPluginRegisterStorageArea3 params;
+    params.create = create;
+    params.readRange = readRange;
+    params.remove = remove;
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea3, &params);
+  }
+
+  /**
+   * @brief Signature of a callback function that is triggered when
+   * the Orthanc core requests an operation from the database plugin.
+   * Both request and response are encoded as protobuf buffers.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginCallDatabaseBackendV4) (
+    OrthancPluginMemoryBuffer64* response,
+    void* backend,
+    const void* request,
+    uint64_t requestSize);
+
+  /**
+   * @brief Signature of a callback function that is triggered when
+   * the database plugin must be finalized.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginFinalizeDatabaseBackendV4) (void* backend);
+
+  typedef struct
+  {
+    void*                                   backend;
+    uint32_t                                maxDatabaseRetries;
+    OrthancPluginCallDatabaseBackendV4      operations;
+    OrthancPluginFinalizeDatabaseBackendV4  finalize;
+  } _OrthancPluginRegisterDatabaseBackendV4;
+
+  /**
+   * @brief Register a custom database back-end.
+   *
+   * This function was added in Orthanc SDK 1.12.0. It uses Google
+   * Protocol Buffers for the communications between the Orthanc core
+   * and database plugins. Check out "OrthancDatabasePlugin.proto" for
+   * the definition of the protobuf messages.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param backend Pointer to the custom database backend.
+   * @param maxDatabaseRetries Maximum number of retries if transaction doesn't succeed.
+   * If no retry is successful, OrthancPluginErrorCode_DatabaseCannotSerialize is generated.
+   * @param operations Access to the operations of the custom database backend.
+   * @param finalize Callback to deallocate the custom database backend.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.0")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterDatabaseBackendV4(
+    OrthancPluginContext*                   context,
+    void*                                   backend,
+    uint32_t                                maxDatabaseRetries,
+    OrthancPluginCallDatabaseBackendV4      operations,
+    OrthancPluginFinalizeDatabaseBackendV4  finalize)
+  {
+    _OrthancPluginRegisterDatabaseBackendV4 params;
+    params.backend = backend;
+    params.maxDatabaseRetries = maxDatabaseRetries;
+    params.operations = operations;
+    params.finalize = finalize;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginDicomInstance**        target;
+    const char*                         instanceId;
+    OrthancPluginLoadDicomInstanceMode  mode;
+  } _OrthancPluginLoadDicomInstance;
+
+  /**
+   * @brief Load a DICOM instance from the Orthanc server.
+   *
+   * This function loads a DICOM instance from the content of the
+   * Orthanc database. The function returns a new pointer to a data
+   * structure that is managed by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instanceId The Orthanc identifier of the DICOM instance of interest.
+   * @param mode Flag specifying how to deal with pixel data.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.1")
+  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginLoadDicomInstance(
+    OrthancPluginContext*               context,
+    const char*                         instanceId,
+    OrthancPluginLoadDicomInstanceMode  mode)
+  {
+    OrthancPluginDicomInstance* target = NULL;
+
+    _OrthancPluginLoadDicomInstance params;
+    params.target = &target;
+    params.instanceId = instanceId;
+    params.mode = mode;
+
+    if (context->InvokeService(context, _OrthancPluginService_LoadDicomInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    const char*               name;
+    int64_t                   value;
+    OrthancPluginMetricsType  type;
+  } _OrthancPluginSetMetricsIntegerValue;
+
+  /**
+   * @brief Set the value of an integer metrics.
+   *
+   * This function sets the value of an integer metrics to monitor the
+   * behavior of the plugin through tools such as Prometheus. The
+   * values of all the metrics are stored within the Orthanc context.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param name The name of the metrics to be set.
+   * @param value The value of the metrics.
+   * @param type The type of the metrics. This parameter is only taken into consideration
+   * the first time this metrics is set.
+   * @ingroup Toolbox
+   * @see OrthancPluginSetMetricsValue()
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.1")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsIntegerValue(
+    OrthancPluginContext*     context,
+    const char*               name,
+    int64_t                   value,
+    OrthancPluginMetricsType  type)
+  {
+    _OrthancPluginSetMetricsIntegerValue params;
+    params.name = name;
+    params.value = value;
+    params.type = type;
+    context->InvokeService(context, _OrthancPluginService_SetMetricsIntegerValue, &params);
+  }
+
+
+  /**
+   * @brief Set the name of the current thread.
+   *
+   * This function gives a name to the thread that is calling this
+   * function. This name is used in the Orthanc logs. This function
+   * must only be called from threads that the plugin has created
+   * itself.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param threadName The name of the current thread. A thread name cannot be longer than 16 characters.
+   * @return 0 if success, other value if error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.2")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetCurrentThreadName(
+    OrthancPluginContext*  context,
+    const char*            threadName)
+  {
+    return context->InvokeService(context, _OrthancPluginService_SetCurrentThreadName, threadName);
+  }
+
+
+  typedef struct
+  {
+    /* Note: This structure is also defined in Logging.h and it must be binary compatible */
+    const char*               message;
+    const char*               plugin;
+    const char*               file;
+    uint32_t                  line;
+    OrthancPluginLogCategory  category;
+    OrthancPluginLogLevel     level;
+  } _OrthancPluginLogMessage;
+
+
+  /**
+   * @brief Log a message.
+   *
+   * Log a message using the Orthanc logging system.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   * @param plugin The plugin name.
+   * @param file The filename in the plugin code.
+   * @param line The file line in the plugin code.
+   * @param category The category.
+   * @param level The level of the message.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.4")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogMessage(
+    OrthancPluginContext* context,
+    const char* message,
+    const char* plugin,
+    const char* file,
+    uint32_t line,
+    OrthancPluginLogCategory category,
+    OrthancPluginLogLevel level)
+  {
+    _OrthancPluginLogMessage m;
+    m.message = message;
+    m.plugin = plugin;
+    m.file = file;
+    m.line = line;
+    m.category = category;
+    m.level = level;
+    context->InvokeService(context, _OrthancPluginService_LogMessage, &m);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              contentType;
+  } _OrthancPluginStartStreamAnswer;
+
+  /**
+   * @brief Start an HTTP stream answer.
+   *
+   * Initiates an HTTP stream answer, as the result of a REST request.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param contentType The MIME type of the items in the stream answer.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginSendStreamChunk()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.6")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartStreamAnswer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              contentType)
+  {
+    _OrthancPluginStartStreamAnswer params;
+    params.output = output;
+    params.contentType = contentType;
+    return context->InvokeService(context, _OrthancPluginService_StartStreamAnswer, &params);
+  }
+
+
+  /**
+   * @brief Send a chunk as a part of an HTTP stream answer.
+   *
+   * This function sends a chunk as part of an HTTP stream
+   * answer that was initiated by OrthancPluginStartStreamAnswer().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the item.
+   * @param answerSize Number of bytes of the item.
+   * @return 0 if success, or the error code if failure (this notably happens
+   * if the connection is closed by the client).
+   * @see OrthancPluginStartStreamAnswer()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.6")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendStreamChunk(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const void*              answer,
+    uint32_t                 answerSize)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = NULL;
+    return context->InvokeService(context, _OrthancPluginService_SendStreamChunk, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*    instanceId;
+    OrthancPluginMemoryBuffer*    attachmentUuid;
+    OrthancPluginStoreStatus*     storeStatus;
+    const void*                   dicom;
+    uint64_t                      dicomSize;
+    const void*                   customData;
+    uint32_t                      customDataSize;
+  } _OrthancPluginAdoptDicomInstance;
+
+  /**
+   * @brief Adopt a DICOM instance read from the filesystem.
+   *
+   * This function requests Orthanc to create a DICOM resource at the
+   * "Instance" level in its database, using the content of a DICOM
+   * instance read from the filesystem. The newly created DICOM
+   * resource is associated with an attachment whose content type is
+   * "OrthancPluginContentType_Dicom". The attachment is associated
+   * with the provided custom data.
+   *
+   * This function should only be used in combination with a custom
+   * storage area featuring support for custom data (i.e., installed
+   * using OrthancPluginRegisterStorageArea3()). The custom storage
+   * area is responsible for *not* duplicating the DICOM file into the
+   * storage area of Orthanc, hence the name "Adopt". The support for
+   * custom data is necessary for the custom storage area to
+   * distinguish between adopted and non-adopted DICOM instances.
+   *
+   * Check out the "AdoptDicomInstance" plugin in the source
+   * distribution of Orthanc for a working sample:
+   * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Plugins/Samples/AdoptDicomInstance/
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instanceId The target memory buffer that will be filled by
+   * the Orthanc core with the public identifier of the newly created
+   * instance. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param attachmentUuid The target memory buffer that will be
+   * filled by the Orthanc core with the UUID of the newly created
+   * attachment corresponding to the adopted DICOM instance. It must
+   * be freed with OrthancPluginFreeMemoryBuffer().
+   * @param storeStatus Variable that will be filled by the Orthanc core
+   * with the status of store operation.
+   * @param dicom Pointer to the DICOM instance read from the filesystem.
+   * @param dicomSize Size of the DICOM instance.
+   * @param customData The custom data to associated with the attachment.
+   * @param customDataSize The size of the custom data.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginAdoptDicomInstance(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    instanceId,        /* out */
+    OrthancPluginMemoryBuffer*    attachmentUuid,    /* out */
+    OrthancPluginStoreStatus*     storeStatus,       /* out */
+    const void*                   dicom,
+    uint64_t                      dicomSize,
+    const void*                   customData,
+    uint32_t                      customDataSize)
+  {
+    _OrthancPluginAdoptDicomInstance params;
+    params.instanceId = instanceId;
+    params.attachmentUuid = attachmentUuid;
+    params.storeStatus = storeStatus;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.customData = customData;
+    params.customDataSize = customDataSize;
+
+    return context->InvokeService(context, _OrthancPluginService_AdoptDicomInstance, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*    customData;
+    const char*                   attachmentUuid;
+  } _OrthancPluginGetAttachmentCustomData;
+
+  /**
+   * @brief Retrieve the custom data associated with an attachment in the Orthanc database.
+   *
+   * If no custom data is associated with the attachment of interest,
+   * the target memory buffer is filled with the NULL value and a zero size.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param customData Memory buffer where to store the retrieved value. It must be freed
+   * by the plugin by calling OrthancPluginFreeMemoryBuffer().
+   * @param attachmentUuid The UUID of the attachment of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetAttachmentCustomData(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    customData,     /* out */
+    const char*                   attachmentUuid  /* in */)
+  {
+    _OrthancPluginGetAttachmentCustomData params;
+    params.customData = customData;
+    params.attachmentUuid = attachmentUuid;
+
+    return context->InvokeService(context, _OrthancPluginService_GetAttachmentCustomData, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*  attachmentUuid;
+    const void*  customData;
+    uint32_t     customDataSize;
+  } _OrthancPluginSetAttachmentCustomData;
+
+  /**
+   * @brief Update the custom data associated with an attachment in the Orthanc database.
+   *
+   * This function is notably used in the "orthanc-advanced-storage"
+   * when the plugin moves an attachment.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param attachmentUuid The UUID of the attachment of interest.
+   * @param customData The value to store.
+   * @param customDataSize The size of the value to store.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetAttachmentCustomData(
+    OrthancPluginContext*         context,
+    const char*                   attachmentUuid, /* in */
+    const void*                   customData,     /* in */
+    uint32_t                      customDataSize  /* in */)
+  {
+    _OrthancPluginSetAttachmentCustomData params;
+    params.attachmentUuid = attachmentUuid;
+    params.customData = customData;
+    params.customDataSize = customDataSize;
+
+    return context->InvokeService(context, _OrthancPluginService_SetAttachmentCustomData, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   storeId;
+    const char*                   key;
+    const void*                   value;
+    uint32_t                      valueSize;
+  } _OrthancPluginStoreKeyValue;
+
+  /**
+   * @brief Store a key-value pair in the Orthanc database.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the key-value store.
+   * @param key The key of the value to store (note: storeId + key must be unique).
+   * @param value The value to store.
+   * @param valueSize The length of the value to store.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStoreKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   storeId,  /* in */
+    const char*                   key,      /* in */
+    const void*                   value,    /* in */
+    uint32_t                      valueSize /* in */)
+  {
+    _OrthancPluginStoreKeyValue params;
+    params.storeId = storeId;
+    params.key = key;
+    params.value = value;
+    params.valueSize = valueSize;
+
+    return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   storeId;
+    const char*                   key;
+  } _OrthancPluginDeleteKeyValue;
+
+  /**
+   * @brief Delete a key-value pair from the Orthanc database.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the key-value store.
+   * @param key The key of the value to store (note: storeId + key must be unique).
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDeleteKeyValue(
+    OrthancPluginContext*         context,
+    const char*                   storeId, /* in */
+    const char*                   key      /* in */)
+  {
+    _OrthancPluginDeleteKeyValue params;
+    params.storeId = storeId;
+    params.key = key;
+
+    return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, &params);
+  }
+
+
+  typedef struct
+  {
+    uint8_t*                      found;
+    OrthancPluginMemoryBuffer*    target;
+    const char*                   storeId;
+    const char*                   key;
+  } _OrthancPluginGetKeyValue;
+
+  /**
+   * @brief Get the value associated with a key in the Orthanc key-value store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param found Pointer to a Boolean that is set to "true" iff. the key exists in the key-value store.
+   * @param target Memory buffer where to store the retrieved value. It must be freed
+   * by the plugin by calling OrthancPluginFreeMemoryBuffer().
+   * @param storeId A unique identifier identifying both the plugin and the key-value store.
+   * @param key The key of the value to retrieve from the store (note: storeId + key must be unique).
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetKeyValue(
+    OrthancPluginContext*         context,
+    uint8_t*                      found,   /* out */
+    OrthancPluginMemoryBuffer*    target,  /* out */
+    const char*                   storeId, /* in */
+    const char*                   key      /* in */)
+  {
+    _OrthancPluginGetKeyValue params;
+    params.found = found;
+    params.target = target;
+    params.storeId = storeId;
+    params.key = key;
+
+    return context->InvokeService(context, _OrthancPluginService_GetKeyValue, &params);
+  }
+
+
+  /**
+   * @brief Opaque structure that represents an iterator over the keys and values of
+   * a key-value store.
+   * @ingroup Callbacks
+   **/
+  typedef struct ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+    _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator;
+
+
+  typedef struct
+  {
+    OrthancPluginKeysValuesIterator**  target;
+    const char*                        storeId;
+  } _OrthancPluginCreateKeysValuesIterator;
+
+
+  /**
+   * @brief Create an iterator over the key-value pairs of a key-value store in the Orthanc database.
+   *
+   * The iterator loops over the keys according to the lexicographical order.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storeId A unique identifier identifying both the plugin and the key-value store.
+   * @return The newly allocated iterator, or NULL in the case of an error.
+   * The iterator must be freed by calling OrthancPluginFreeKeysValuesIterator().
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginKeysValuesIterator* OrthancPluginCreateKeysValuesIterator(
+    OrthancPluginContext*  context,
+    const char*            storeId)
+  {
+    OrthancPluginKeysValuesIterator* target = NULL;
+
+    _OrthancPluginCreateKeysValuesIterator params;
+    params.target = &target;
+    params.storeId = storeId;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateKeysValuesIterator, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginKeysValuesIterator*   iterator;
+  } _OrthancPluginFreeKeysValuesIterator;
+
+  /**
+   * @brief Free an iterator over a key-value store.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param iterator The iterator of interest.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeKeysValuesIterator(
+    OrthancPluginContext*             context,
+    OrthancPluginKeysValuesIterator*  iterator)
+  {
+    _OrthancPluginFreeKeysValuesIterator params;
+    params.iterator = iterator;
+
+    context->InvokeService(context, _OrthancPluginService_FreeKeysValuesIterator, &params);
+  }
+
+
+  typedef struct
+  {
+    uint8_t*                          done;
+    OrthancPluginKeysValuesIterator*  iterator;
+  } _OrthancPluginKeysValuesIteratorNext;
+
+  /**
+   * @brief Read the next element of an iterator over a key-value store.
+  *
+   * The iterator loops over the keys according to the lexicographical order.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param done Pointer to a Boolean that is set to "true" iff. the iterator has reached the end of the store.
+   * @param iterator The iterator of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorNext(
+    OrthancPluginContext*             context,
+    uint8_t*                          done,     /* out */
+    OrthancPluginKeysValuesIterator*  iterator  /* in */)
+  {
+    _OrthancPluginKeysValuesIteratorNext params;
+    params.done = done;
+    params.iterator = iterator;
+
+    return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorNext, &params);
+  }
+
+
+  typedef struct
+  {
+    const char**                      target;
+    OrthancPluginKeysValuesIterator*  iterator;
+  } _OrthancPluginKeysValuesIteratorGetKey;
+
+  /**
+   * @brief Get the current key of an iterator over a key-value store.
+   *
+   * Before using this function, the function OrthancPluginKeysValuesIteratorNext()
+   * must have been called at least once.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param iterator The iterator of interest.
+   * @return The current key, or NULL in the case of an error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginKeysValuesIteratorGetKey(
+    OrthancPluginContext*             context,
+    OrthancPluginKeysValuesIterator*  iterator)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginKeysValuesIteratorGetKey params;
+    params.target = &target;
+    params.iterator = iterator;
+
+    if (context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetKey, &params) == OrthancPluginErrorCode_Success)
+    {
+      return target;
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*        target;
+    OrthancPluginKeysValuesIterator*  iterator;
+  } _OrthancPluginKeysValuesIteratorGetValue;
+
+  /**
+   * @brief Get the current value of an iterator over a key-value store.
+   *
+   * Before using this function, the function OrthancPluginKeysValuesIteratorNext()
+   * must have been called at least once.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target Memory buffer where to store the value that has been retrieved from the key-value store.
+   * It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param iterator The iterator of interest.
+   * @return The current value, or NULL in the case of an error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginKeysValuesIteratorGetValue(
+    OrthancPluginContext*             context,
+    OrthancPluginMemoryBuffer*        target   /* out */,
+    OrthancPluginKeysValuesIterator*  iterator /* in */)
+  {
+    _OrthancPluginKeysValuesIteratorGetValue params;
+    params.target = target;
+    params.iterator = iterator;
+
+    return context->InvokeService(context, _OrthancPluginService_KeysValuesIteratorGetValue, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   queueId;
+    const void*                   value;
+    uint32_t                      valueSize;
+  } _OrthancPluginEnqueueValue;
+
+  /**
+   * @brief Append a value to the back of a queue.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param queueId A unique identifier identifying both the plugin and the queue.
+   * @param value The value to store.
+   * @param valueSize The size of the value to store.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginEnqueueValue(
+    OrthancPluginContext*         context,
+    const char*                   queueId,  /* in */
+    const void*                   value,    /* in */
+    uint32_t                      valueSize /* in */)
+  {
+    _OrthancPluginEnqueueValue params;
+    params.queueId = queueId;
+    params.value = value;
+    params.valueSize = valueSize;
+
+    return context->InvokeService(context, _OrthancPluginService_EnqueueValue, &params);
+  }
+
+
+  typedef struct
+  {
+    uint8_t*                      found;
+    OrthancPluginMemoryBuffer*    target;
+    const char*                   queueId;
+    OrthancPluginQueueOrigin      origin;
+  } _OrthancPluginDequeueValue;
+
+  /**
+   * @brief Dequeue a value from a queue.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param found Pointer to a Boolean that is set to "true" iff. a value has been dequeued.
+   * @param target Memory buffer where to store the value that has been retrieved from the queue.
+   * It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param queueId A unique identifier identifying both the plugin and the queue.
+   * @param origin The position from where the value is dequeued (back for LIFO, front for FIFO).
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginDequeueValue(
+    OrthancPluginContext*         context,
+    uint8_t*                      found,    /* out */
+    OrthancPluginMemoryBuffer*    target,   /* out */
+    const char*                   queueId,  /* in */
+    OrthancPluginQueueOrigin      origin    /* in */)
+  {
+    _OrthancPluginDequeueValue params;
+    params.found = found;
+    params.target = target;
+    params.queueId = queueId;
+    params.origin = origin;
+
+    return context->InvokeService(context, _OrthancPluginService_DequeueValue, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   queueId;
+    uint64_t*                     size;
+  } _OrthancPluginGetQueueSize;
+
+  /**
+   * @brief Get the number of elements in a queue.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param queueId A unique identifier identifying both the plugin and the queue.
+   * @param size The number of elements in the queue.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.8")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetQueueSize(
+    OrthancPluginContext*         context,
+    const char*                   queueId, /* in */
+    uint64_t*                     size /* out */)
+  {
+    _OrthancPluginGetQueueSize params;
+    params.queueId = queueId;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_GetQueueSize, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*                   resourceId;
+    OrthancPluginStableStatus     stableStatus;
+    uint8_t*                      statusHasChanged;
+  } _OrthancPluginSetStableStatus;
+
+  /**
+   * @brief Change the "Stable" status of a resource.
+   *
+   * Forcing a resource to "Stable" if it is currently "Unstable" will
+   * change its "Stable" status AND trigger a new "Stable" change,
+   * which will also trigger listener callbacks.
+   *
+   * Forcing a resource to "Stable" if it is already "Stable" has no
+   * effect (no-op).
+   *
+   * Forcing a resource to "Unstable" will change its "Stable" status
+   * to "Unstable" AND reset its stabilization period, no matter its
+   * initial state.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param statusHasChanged Whether the status has changed (1) or not (0) during the execution of this command.
+   * @param resourceId The Orthanc identifier of the DICOM resource of interest.
+   * @param stableStatus The new stable status of the resource of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.9")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetStableStatus(
+    OrthancPluginContext*         context,
+    uint8_t*                      statusHasChanged,  /* out */
+    const char*                   resourceId,  /* in */
+    OrthancPluginStableStatus     stableStatus /* in */)
+  {
+    _OrthancPluginSetStableStatus params;
+    params.resourceId = resourceId;
+    params.stableStatus= stableStatus;
+    params.statusHasChanged = statusHasChanged;
+
+    return context->InvokeService(context, _OrthancPluginService_SetStableStatus, &params);
+  }
+
+
+  /**
+   * @brief Callback to authenticate a HTTP request.
+   *
+   * Signature of a callback function that authenticates every incoming HTTP request.
+   *
+   * @param status The output status of the authentication.
+   * @param customPayload If status is `OrthancPluginHttpAuthenticationStatus_Granted`,
+   * a custom payload that will be provided to the HTTP handler callback.
+   * @param redirection If status is `OrthancPluginHttpAuthenticationStatus_Redirect`,
+   * a buffer filled with the path where to redirect the user (typically, a login page).
+   * The path is relative to the root of the Web server of Orthanc.
+   * @param uri The URI of interest (without the possible GET arguments).
+   * @param ip The IP address of the HTTP client.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys The keys of the HTTP headers (always converted to low-case).
+   * @param headersValues The values of the HTTP headers.
+   * @param getCount For a GET request, the number of GET parameters.
+   * @param getKeys For a GET request, the keys of the GET parameters.
+   * @param getValues For a GET request, the values of the GET parameters.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginHttpAuthentication) (
+    OrthancPluginHttpAuthenticationStatus*  status,         /* out */
+    OrthancPluginMemoryBuffer*              customPayload,  /* out */
+    OrthancPluginMemoryBuffer*              redirection,    /* out */
+    const char*                             uri,
+    const char*                             ip,
+    uint32_t                                headersCount,
+    const char* const*                      headersKeys,
+    const char* const*                      headersValues,
+    uint32_t                                getCount,
+    const char* const*                      getKeys,
+    const char* const*                      getValues);
+
+
+  typedef struct
+  {
+    OrthancPluginHttpAuthentication  callback;
+  } _OrthancPluginHttpAuthentication;
+
+  /**
+   * @brief Register a callback to handle HTTP authentication (and
+   * possibly HTTP authorization).
+   *
+   * This function installs a callback that is executed for each
+   * incoming HTTP request to handle HTTP authentication. At most one
+   * plugin can register such a callback. This gives the opportunity
+   * to the plugin to validate access tokens (such as a JWT), possibly
+   * redirecting the user to a login page. The authentication callback
+   * can generate a custom payload that will be provided to the
+   * subsequent REST handling callback (cf. `authenticationPayload` in
+   * `OrthancPluginHttpRequest`).
+   *
+   * If one plugin installs a HTTP authentication callback, the
+   * built-in HTTP authentication of Orthanc is disabled. This means
+   * that the "RegisteredUsers" and "AuthenticationEnabled"
+   * configuration options of Orthanc are totally ignored. In
+   * addition, tokens generated by
+   * OrthancPluginGenerateRestApiAuthorizationToken() become
+   * ineffective.
+   *
+   * This HTTP authentication callback can notably be used if some
+   * resource in the REST API must be available for public access, if
+   * the "RemoteAccessAllowed" configuration option is set to "true"
+   * (which necessitates bypassing the built-in HTTP authentication of
+   * Orthanc).
+   *
+   * In addition, the callback can handle HTTP authorization
+   * simultaneously with HTTP authentication, by reporting the
+   * "OrthancPluginHttpAuthenticationStatus_Forbidden" status. This
+   * corresponds to the behavior of callbacks installed using
+   * OrthancPluginRegisterIncomingHttpRequestFilter2(), but the latter
+   * callbacks do not provide access to the authentication payload.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The HTTP authentication callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.9")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterHttpAuthentication(
+    OrthancPluginContext*            context,
+    OrthancPluginHttpAuthentication  callback)
+  {
+    _OrthancPluginHttpAuthentication params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterHttpAuthentication, &params);
+  }
+
+
+  typedef struct
+  {
+    const char*               sourcePlugin;
+    const char*               userId;
+    OrthancPluginResourceType resourceType;
+    const char*               resourceId;
+    const char*               action;
+    const void*               logData;
+    uint32_t                  logDataSize;
+  } _OrthancPluginEmitAuditLog;
+
+
+  /**
+   * @brief Generate an audit log to signal security-related events.
+   *
+   * Generate an audit log that will be broadcasted to all the plugins
+   * that have registered a callback handler using
+   * OrthancPluginRegisterAuditLogHandler(). If no plugin has
+   * registered such a callback, the audit log is ignored.
+   *
+   * A typical handler would record the audit log in a database and/or
+   * relay the audit log to a message broker.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param sourcePlugin The name of the source plugin, to properly interpret the
+   * content of "action" and "logData".
+   * @param userId A string that uniquely identifies the user or
+   * entity that is executing the action on the resource.
+   * @param resourceType The type of the resource this audit log relates to.
+   * @param resourceId The resource this audit log relates to.
+   * @param action The action that was performed on the resource.
+   * @param logData A pointer to custom log data.
+   * @param logDataSize The size of the custom log data.
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.9")
+  ORTHANC_PLUGIN_INLINE void OrthancPluginEmitAuditLog(
+    OrthancPluginContext*     context,
+    const char*               sourcePlugin,
+    const char*               userId,
+    OrthancPluginResourceType resourceType,
+    const char*               resourceId,
+    const char*               action,
+    const void*               logData,
+    uint32_t                  logDataSize)
+  {
+    _OrthancPluginEmitAuditLog m;
+    m.sourcePlugin = sourcePlugin;
+    m.userId = userId;
+    m.resourceType = resourceType;
+    m.resourceId = resourceId;
+    m.action = action;
+    m.logData = logData;
+    m.logDataSize = logDataSize;
+    context->InvokeService(context, _OrthancPluginService_EmitAuditLog, &m);
+  }
+
+
+  /**
+   * @brief Callback to handle an audit log.
+   *
+   * Signature of a callback function that handles an audit log
+   * emitted by a source plugin.
+   *
+   * @param sourcePlugin The name of the source plugin. This information can
+   * be used to properly interpret the content of the "action" and
+   * "logData" arguments.
+   * @param userId A string uniquely identifying the user or entity that is executing the action on the resource.
+   * @param resourceType The type of the resource this log relates to.
+   * @param resourceId The resource this log relates to.
+   * @param action The action that is performed on the resource.
+   * @param logData A pointer to custom log data.
+   * @param logDataSize The size of the custom log data.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginAuditLogHandler) (
+    const char*               sourcePlugin,
+    const char*               userId,
+    OrthancPluginResourceType resourceType,
+    const char*               resourceId,
+    const char*               action,
+    const void*               logData,
+    uint32_t                  logDataSize);
+
+  typedef struct
+  {
+    OrthancPluginAuditLogHandler  handler;
+  } _OrthancPluginAuditLogHandler;
+
+  /**
+   * @brief Register a callback to handle audit logs.
+   *
+   * This function installs a callback to listen to each audit log
+   * that is generated by some other plugin.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param handler The audit log handler.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_SINCE_SDK("1.12.9")
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterAuditLogHandler(
+    OrthancPluginContext*            context,
+    OrthancPluginAuditLogHandler     handler)
+  {
+    _OrthancPluginAuditLogHandler params;
+    params.handler = handler;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterAuditLogHandler, &params);
+  }
+
+
+#ifdef  __cplusplus
+}
+#endif
+
+
+/** @} */
--- a/Resources/SyncOrthancFolder.py	Mon Sep 07 15:12:02 2026 +0200
+++ b/Resources/SyncOrthancFolder.py	Mon Sep 07 20:58:51 2026 +0200
@@ -11,7 +11,10 @@
 import urllib.request
 
 TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc')
-PLUGIN_SDK_VERSION = '1.7.0'
+PLUGIN_SDK_VERSIONS = [
+    [ '1.7.0', 'Plugins/Include' ],
+    [ '1.12.9', 'OrthancServer/Plugins/Include' ],
+]
 REPOSITORY = 'https://orthanc.uclouvain.be/hg/%s/raw-file'
 
 FILES = [
@@ -81,13 +84,14 @@
                       f[1],
                       os.path.join(f[2], os.path.basename(f[1])) ])
 
-for f in SDK:
-    commands.append([
-        'orthanc',
-        'Orthanc-%s' % PLUGIN_SDK_VERSION, 
-        'Plugins/Include/%s' % f,
-        'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f) 
-    ])
+for (version, folder) in PLUGIN_SDK_VERSIONS:
+    for f in SDK:
+        commands.append([
+            'orthanc',
+            'Orthanc-%s' % version,
+            '%s/%s' % (folder, f),
+            'Sdk-%s/%s' % (version, f)
+        ])
 
 
 pool = multiprocessing.Pool(10)  # simultaneous downloads
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/AnnotationsRestApi.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,595 @@
+/**
+ * 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 "AnnotationsRestApi.h"
+
+#include "../ViewerConfiguration.h"
+#include "../ViewerToolbox.h"
+#include "CachedAnnotationsWorkspace.h"
+#include "CachedUserFeatures.h"
+#include "IAuthenticatedUser.h"
+
+#include <SerializationToolbox.h>
+#include <Toolbox.h>
+
+#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+
+static const char* const KEY_FEATURES = "features";
+static const char* const KEY_LAYER_ID = "layer-id";
+
+
+namespace OrthancWSI
+{
+  class AnnotationsCommandContext : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr<IAuthenticatedUser>          user_;
+    Json::Value                                  body_;
+    std::unique_ptr<CachedAnnotationsWorkspace>  workspace_;
+    ProjectRole                                  role_;
+
+  public:
+    explicit AnnotationsCommandContext(const OrthancPluginHttpRequest* request)
+    {
+      if (request->method != OrthancPluginHttpMethod_Post)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+      }
+
+      user_.reset(IAuthenticatedUser::FromHttpRequest(request));
+
+      if (!Orthanc::Toolbox::ReadJson(body_, request->body, request->bodySize) ||
+          !body_.isObject())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+
+      const std::string projectId = Orthanc::SerializationToolbox::ReadString(body_, "project", "" /* default project */);
+      const std::string levelString = Orthanc::SerializationToolbox::ReadString(body_, "level");
+      const std::string resourceId = Orthanc::SerializationToolbox::ReadString(body_, "resource");
+      unsigned int frameNumber = Orthanc::SerializationToolbox::ReadUnsignedInteger(body_, "frame", 0 /* default frame */);
+
+      role_ = user_->GetRoleInProject(projectId);
+
+      if (role_ != ProjectRole_Instructor &&
+          role_ != ProjectRole_Learner)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "User \"" + user_->Format() +
+                                        "\" is not instructor or learner of project \"" + projectId + "\"");
+      }
+
+      Orthanc::ResourceType level = Orthanc::StringToResourceType(levelString.c_str());
+      AnnotationsWorkspaceId workspaceId(projectId, level, resourceId, frameNumber);
+
+      workspace_.reset(new CachedAnnotationsWorkspace(workspaceId));
+    }
+
+    const IAuthenticatedUser& GetUser() const
+    {
+      assert(user_.get() != NULL);
+      return *user_;
+    }
+
+    ProjectRole GetRole() const
+    {
+      return role_;
+    }
+
+    AnnotationsWorkspace& GetWorkspace()
+    {
+      assert(workspace_.get() != NULL);
+      return workspace_->GetContent();
+    }
+
+    std::string GetBodyString(const char* field) const
+    {
+      return Orthanc::SerializationToolbox::ReadString(body_, field);
+    }
+
+    const Json::Value& GetBodyField(const char* field) const
+    {
+      if (!body_.isMember(field))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      }
+      else
+      {
+        return body_[field];
+      }
+    }
+
+    AnnotationsWorkspace::UserReader* CreateUserReader() const
+    {
+      return new AnnotationsWorkspace::UserReader(workspace_->GetContent(), GetUser().GetAnnotatingId(), role_);
+    }
+
+    AnnotationsWorkspace::UserWriter* CreateUserWriter()
+    {
+      return new AnnotationsWorkspace::UserWriter(workspace_->GetContent(), GetUser().GetAnnotatingId(), role_);
+    }
+  };
+
+
+  static bool ProtectPostRequest(OrthancPluginRestOutput* output,
+                                 const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+      return false;
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+
+  void GetWorkspaceInfo(OrthancPluginRestOutput* output,
+                        const char* url,
+                        const OrthancPluginHttpRequest* request)
+  {
+    static const char* const KEY_IS_LEARNER = "is_learner";
+    static const char* const KEY_IS_INSTRUCTOR = "is_instructor";
+
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      Json::Value answer;
+
+      answer["name"] = context.GetWorkspace().GetProjectName();
+      answer["description"] = context.GetWorkspace().GetProjectDescription();
+      answer["project"] = context.GetWorkspace().GetId().GetProjectId();
+      answer["enabled"] = ViewerConfiguration::GetInstance().AreAnnotationsEnabled();
+      answer["sharing"] = (ViewerConfiguration::GetInstance().AreAnnotationsEnabled() &&
+                           ViewerConfiguration::GetInstance().IsAnnotationsSharingEnabled());
+      answer["learner_to_learner_sharing"] = ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled();
+      answer["user"] = context.GetUser().Format();
+
+      std::string role;
+      switch (context.GetRole())
+      {
+        case ProjectRole_Learner:
+          answer[KEY_IS_LEARNER] = true;
+          answer[KEY_IS_INSTRUCTOR] = false;
+          break;
+
+        case ProjectRole_Instructor:
+          answer[KEY_IS_LEARNER] = false;
+          answer[KEY_IS_INSTRUCTOR] = true;
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      answer["role"] = role;
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+      answer["persistent"] = true;
+#else
+      answer["persistent"] = false;
+#endif
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void ListUserLayers(OrthancPluginRestOutput* output,
+                      const char* url,
+                      const OrthancPluginHttpRequest* request)
+  {
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      Json::Value answer;
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->ListLayers(answer);
+      }
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void CreateUserLayer(OrthancPluginRestOutput* output,
+                       const char* url,
+                       const OrthancPluginHttpRequest* request)
+  {
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      Json::Value answer;
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->CreateUserLayer(answer);
+      }
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void SaveUserLayer(OrthancPluginRestOutput* output,
+                     const char* url,
+                     const OrthancPluginHttpRequest* request)
+  {
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      UserLayer updated(context.GetBodyField("layer"));
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->UpdateUserLayer(updated);
+      }
+
+      ViewerToolbox::AnswerEmpty(output);
+    }
+  }
+
+
+  void DeleteUserLayer(OrthancPluginRestOutput* output,
+                       const char* url,
+                       const OrthancPluginHttpRequest* request)
+  {
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      const std::string layerId = context.GetBodyString(KEY_LAYER_ID);
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->DeleteUserLayer(layerId);
+      }
+
+      ViewerToolbox::AnswerEmpty(output);
+    }
+  }
+
+
+  void LoadUserFeatures(OrthancPluginRestOutput* output,
+                        const char* url,
+                        const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+    }
+    else
+    {
+      AnnotationsCommandContext context(request);
+
+      Json::Value answer;
+
+      {
+        CachedUserFeatures cached(context.GetWorkspace().GetId(), context.GetUser().GetAnnotatingId());
+        cached.GetFeatures().GetContent(answer[KEY_FEATURES]);
+      }
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void SaveUserFeatures(OrthancPluginRestOutput* output,
+                        const char* url,
+                        const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+    }
+    else
+    {
+      AnnotationsCommandContext context(request);
+
+      {
+        CachedUserFeatures cached(context.GetWorkspace().GetId(), context.GetUser().GetAnnotatingId());
+        cached.GetFeatures().SetContent(context.GetBodyField(KEY_FEATURES));
+      }
+
+      ViewerToolbox::AnswerEmpty(output);
+    }
+  }
+
+
+  void SearchActiveUsers(OrthancPluginRestOutput* output,
+                         const char* url,
+                         const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+    }
+    else
+    {
+      AnnotationsCommandContext context(request);
+      const UserId self = context.GetUser().GetAnnotatingId();
+
+      const std::string query = context.GetBodyString("query");
+
+      std::set<UserId> users;
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->SearchActiveUsers(users, query);
+      }
+
+      Json::Value answer = Json::arrayValue;
+
+      for (std::set<UserId>::const_iterator it = users.begin(); it != users.end(); ++it)
+      {
+        assert(it->GetType() == UserId::Type_Root ||
+               it->GetType() == UserId::Type_Standard);
+
+        if (answer.size() >= 20)
+        {
+          break;  // Don't load too many users
+        }
+        else if (!self.Equals(*it)) // Don't add self
+        {
+          Json::Value item;
+          it->Serialize(item);
+          answer.append(item);
+        }
+      }
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void ListUsersSharingLayersWithMe(OrthancPluginRestOutput* output,
+                                    const char* url,
+                                    const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+    }
+    else
+    {
+      AnnotationsCommandContext context(request);
+
+      std::set<UserId> users;
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->ListUsersSharingLayersWithMe(users);
+      }
+
+      Json::Value answer = Json::arrayValue;
+
+      for (std::set<UserId>::const_iterator it = users.begin(); it != users.end(); ++it)
+      {
+        Json::Value item;
+        it->Serialize(item);
+        answer.append(item);
+      }
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void ListLayersSharedWithMe(OrthancPluginRestOutput* output,
+                              const char* url,
+                              const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+    }
+    else
+    {
+      AnnotationsCommandContext context(request);
+
+      const UserId author(context.GetBodyField("author"));
+      Json::Value answer;
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->ListLayersSharedWithMe(answer, author);
+      }
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void ImportLayer(OrthancPluginRestOutput* output,
+                   const char* url,
+                   const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+    }
+    else
+    {
+      AnnotationsCommandContext context(request);
+
+      const UserId author(context.GetBodyField("author"));
+      const std::string layerId = context.GetBodyString("layer");
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->ImportLayer(author, layerId);
+      }
+
+      ViewerToolbox::AnswerEmpty(output);
+    }
+  }
+
+
+  void RemoveImportedLayer(OrthancPluginRestOutput* output,
+                           const char* url,
+                           const OrthancPluginHttpRequest* request)
+  {
+    if (request->method != OrthancPluginHttpMethod_Post)
+    {
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
+    }
+    else
+    {
+      AnnotationsCommandContext context(request);
+
+      const std::string layerId = context.GetBodyString("layer");
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->RemoveImportedLayer(layerId);
+      }
+
+      ViewerToolbox::AnswerEmpty(output);
+    }
+  }
+
+
+  void SaveImportedLayer(OrthancPluginRestOutput* output,
+                       const char* url,
+                       const OrthancPluginHttpRequest* request)
+  {
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      ImportedLayer updated(context.GetBodyField("layer"));
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->UpdateImportedLayer(updated);
+      }
+
+      ViewerToolbox::AnswerEmpty(output);
+    }
+  }
+
+
+  void CreateStandardUser(OrthancPluginRestOutput* output,
+                          const char* url,
+                          const OrthancPluginHttpRequest* request)
+  {
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      UserId user(UserId::Type_Standard, context.GetBodyString("name"));
+
+      Json::Value answer;
+      user.Serialize(answer);
+
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+
+
+  void LoadImportedFeatures(OrthancPluginRestOutput* output,
+                            const char* url,
+                            const OrthancPluginHttpRequest* request)
+  {
+    if (ProtectPostRequest(output, request))
+    {
+      AnnotationsCommandContext context(request);
+
+      std::set<UserId> authors;
+      std::set<std::string> layerIds;
+
+      {
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->ListImportedLayers(authors, layerIds);
+      }
+
+      Json::Value importedFeatures = Json::arrayValue;
+
+      // Loop over the imported authors
+      for (std::set<UserId>::const_iterator it = authors.begin(); it != authors.end(); ++it)
+      {
+        Json::Value authorFeatures;
+
+        {
+          CachedUserFeatures author(context.GetWorkspace().GetId(), *it);
+          author.GetFeatures().GetContent(authorFeatures);
+        }
+
+        assert(authorFeatures.isArray());
+
+        for (Json::Value::ArrayIndex i = 0; i < authorFeatures.size(); i++)
+        {
+          std::string layerId = Orthanc::SerializationToolbox::ReadString(authorFeatures[i], KEY_LAYER_ID);
+          if (layerIds.find(layerId) != layerIds.end())
+          {
+            importedFeatures.append(authorFeatures[i]);
+          }
+        }
+      }
+
+      Json::Value answer;
+      answer[KEY_FEATURES] = importedFeatures;
+      ViewerToolbox::AnswerJson(output, answer);
+    }
+  }
+}
+
+
+void RegisterAnnotationsRestApi()
+{
+  OrthancPlugins::RegisterRestCallback<OrthancWSI::GetWorkspaceInfo>("/wsi/api/workspace-info", true);
+
+  if (OrthancWSI::ViewerConfiguration::GetInstance().AreAnnotationsEnabled())
+  {
+    OrthancPlugins::RegisterRestCallback<OrthancWSI::CreateUserLayer>("/wsi/api/create-user-layer", true);
+    OrthancPlugins::RegisterRestCallback<OrthancWSI::DeleteUserLayer>("/wsi/api/delete-user-layer", true);
+    OrthancPlugins::RegisterRestCallback<OrthancWSI::ListUserLayers>("/wsi/api/list-user-layers", true);
+    OrthancPlugins::RegisterRestCallback<OrthancWSI::SaveUserLayer>("/wsi/api/save-user-layer", true);
+
+    OrthancPlugins::RegisterRestCallback<OrthancWSI::LoadUserFeatures>("/wsi/api/load-user-features", true);
+    OrthancPlugins::RegisterRestCallback<OrthancWSI::SaveUserFeatures>("/wsi/api/save-user-features", true);
+
+    if (OrthancWSI::ViewerConfiguration::GetInstance().IsAnnotationsSharingEnabled())
+    {
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::CreateStandardUser>("/wsi/api/create-standard-user", true);
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::SearchActiveUsers>("/wsi/api/search-active-users", true);
+
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::ImportLayer>("/wsi/api/import-layer", true);
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::ListLayersSharedWithMe>("/wsi/api/list-shared-layers", true);
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::ListUsersSharingLayersWithMe>("/wsi/api/list-sharing-users", true);
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::RemoveImportedLayer>("/wsi/api/remove-imported-layer", true);
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::SaveImportedLayer>("/wsi/api/save-imported-layer", true);
+
+      OrthancPlugins::RegisterRestCallback<OrthancWSI::LoadImportedFeatures>("/wsi/api/load-imported-features", true);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/AnnotationsRestApi.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,26 @@
+/**
+ * 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
+
+void RegisterAnnotationsRestApi();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,649 @@
+/**
+ * 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 "AnnotationsWorkspace.h"
+
+#include "../ViewerConfiguration.h"
+#include "../ViewerToolbox.h"
+
+#include <OrthancException.h>
+
+#include <boost/regex.hpp>
+
+
+static const char* const KEY_ACTIVE_INSTRUCTORS = "active-instructors";
+static const char* const KEY_ACTIVE_LEARNERS = "active-learners";
+
+
+namespace OrthancWSI
+{
+  class AnnotationsWorkspace::PersistentInfo : public ISerializable
+  {
+  private:
+    // An active user is always of type "standard"
+    std::set<UserId>   activeInstructors_;
+    std::set<UserId>   activeLearners_;
+
+
+    static void ParseActiveUsers(std::set<UserId>& target,
+                                 const Json::Value& serialized)
+    {
+      if (!serialized.isArray())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      target.clear();
+
+      for (Json::Value::ArrayIndex i = 0; i < serialized.size(); i++)
+      {
+        target.insert(UserId(serialized[i]));
+      }
+    }
+
+
+    static void SerializeActiveUsers(Json::Value& serialized,
+                                     const std::set<UserId>& source)
+    {
+      serialized = Json::arrayValue;
+
+      for (std::set<UserId>::const_iterator it = source.begin(); it != source.end(); ++it)
+      {
+        Json::Value user;
+        it->Serialize(user);
+        serialized.append(user);
+      }
+    }
+
+    void SanityCheck() const
+    {
+#if !defined(NDEBUG)
+      for (std::set<UserId>::const_iterator it = activeInstructors_.begin(); it != activeInstructors_.end(); ++it)
+      {
+        assert(activeLearners_.find(*it) == activeLearners_.end());
+      }
+
+      for (std::set<UserId>::const_iterator it = activeLearners_.begin(); it != activeLearners_.end(); ++it)
+      {
+        assert(activeInstructors_.find(*it) == activeInstructors_.end());
+      }
+#endif
+    }
+
+  public:
+    PersistentInfo()
+    {
+    }
+
+
+    explicit PersistentInfo(const Json::Value& serialized)
+    {
+      if (!serialized.isObject() ||
+          !serialized.isMember(KEY_ACTIVE_INSTRUCTORS) ||
+          !serialized.isMember(KEY_ACTIVE_LEARNERS))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      ParseActiveUsers(activeInstructors_, serialized[KEY_ACTIVE_INSTRUCTORS]);
+      ParseActiveUsers(activeLearners_, serialized[KEY_ACTIVE_LEARNERS]);
+
+      SanityCheck();
+    }
+
+
+    // Return "true" iff. the user was not already tagged as active or
+    // if the user has changed their role in the project (from learner
+    // to instructor, or from instructor to learner)
+    bool AddActiveUser(const UserId& user,
+                       ProjectRole role)
+    {
+      SanityCheck();
+
+      switch (role)
+      {
+        case ProjectRole_Instructor:
+          if (activeInstructors_.find(user) == activeInstructors_.end())
+          {
+            activeLearners_.erase(user);  // Accomodate with change in the role
+            activeInstructors_.insert(user);
+            SanityCheck();
+            return true;
+          }
+          else
+          {
+            return false;
+          }
+
+        case ProjectRole_Learner:
+          if (activeLearners_.find(user) == activeLearners_.end())
+          {
+            activeInstructors_.erase(user);  // Accomodate with change in the role
+            activeLearners_.insert(user);
+            SanityCheck();
+            return true;
+          }
+          else
+          {
+            return false;
+          }
+
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    bool LookupUserRole(ProjectRole& role,
+                        const UserId& user) const
+    {
+      if (activeInstructors_.find(user) != activeInstructors_.end())
+      {
+        assert(activeLearners_.find(user) == activeLearners_.end());
+        role = ProjectRole_Instructor;
+        return true;
+      }
+      else if (activeLearners_.find(user) != activeLearners_.end())
+      {
+        assert(activeInstructors_.find(user) == activeInstructors_.end());
+        role = ProjectRole_Learner;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+
+    virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE
+    {
+      serialized = Json::objectValue;
+      SerializeActiveUsers(serialized[KEY_ACTIVE_INSTRUCTORS], activeInstructors_);
+      SerializeActiveUsers(serialized[KEY_ACTIVE_LEARNERS], activeLearners_);
+    }
+
+
+    class ActiveUsersIterator : public boost::noncopyable
+    {
+    private:
+      std::set<UserId>::const_iterator  instructorsIterator_;
+      std::set<UserId>::const_iterator  instructorsEnd_;
+      std::set<UserId>::const_iterator  learnersIterator_;
+      std::set<UserId>::const_iterator  learnersEnd_;
+
+    public:
+      explicit ActiveUsersIterator(const PersistentInfo& that) :
+        instructorsIterator_(that.activeInstructors_.begin()),
+        instructorsEnd_(that.activeInstructors_.end()),
+        learnersIterator_(that.activeLearners_.begin()),
+        learnersEnd_(that.activeLearners_.end())
+      {
+        that.SanityCheck();
+      }
+
+      bool IsDone() const
+      {
+        return (instructorsIterator_ == instructorsEnd_ &&
+                learnersIterator_ == learnersEnd_);
+      }
+
+      const UserId& GetUser() const
+      {
+        if (instructorsIterator_ != instructorsEnd_)
+        {
+          return *instructorsIterator_;
+        }
+        else if (learnersIterator_ != learnersEnd_)
+        {
+          return *learnersIterator_;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+
+      ProjectRole GetRole() const
+      {
+        if (instructorsIterator_ != instructorsEnd_)
+        {
+          return ProjectRole_Instructor;
+        }
+        else if (learnersIterator_ != learnersEnd_)
+        {
+          return ProjectRole_Learner;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+
+      void Next()
+      {
+        if (instructorsIterator_ != instructorsEnd_)
+        {
+          ++instructorsIterator_;
+        }
+        else if (learnersIterator_ != learnersEnd_)
+        {
+          ++learnersIterator_;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+    };
+  };
+
+
+
+  void AnnotationsWorkspace::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();
+      }
+    }
+  }
+
+
+  bool AnnotationsWorkspace::IsSharedWith(const UserLayer& layer,
+                                          const UserId& layerAuthorId,
+                                          const UserId& viewerId,
+                                          ProjectRole viewerRole) const
+  {
+    assert(persistentInfo_.get() != NULL);
+
+    ProjectRole authorRole;
+    if (persistentInfo_->LookupUserRole(authorRole, layerAuthorId))
+    {
+      return layer.IsSharedWith(authorRole, viewerId, viewerRole);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  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));
+
+      PersistentInfo::ActiveUsersIterator iterator(*persistentInfo_);
+
+      while (!iterator.IsDone())
+      {
+        Load(iterator.GetUser());
+        iterator.Next();
+      }
+    }
+    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;
+    }
+  }
+
+
+  AnnotationsWorkspace::UserReader::UserReader(AnnotationsWorkspace& that,
+                                               const UserId& userId,
+                                               ProjectRole userRole) :
+    lock_(that.mutex_),
+    that_(that),
+    userId_(userId),
+    userRole_(userRole)
+  {
+    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::ListUsersSharingLayersWithMe(std::set<UserId>& target) const
+  {
+    target.clear();
+
+    // Loop over all the users (i.e., all the possible authors) in this workspace
+    for (Content::const_iterator it = that_.content_.begin(); it != that_.content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!userId_.Equals(it->first))  // Don't add self
+      {
+        LayersCollection::Iterator iterator(it->second->GetUserLayers());
+
+        // Loop over all the layers of this user in this workspace
+        while (!iterator.IsDone())
+        {
+          const UserLayer& layer = dynamic_cast<const UserLayer&>(iterator.GetLayer());
+
+          if (that_.IsSharedWith(layer, it->first, userId_, userRole_))
+          {
+            target.insert(it->first);
+            break;
+          }
+          else
+          {
+            iterator.Next();
+          }
+        }
+      }
+    }
+  }
+
+
+  void AnnotationsWorkspace::UserReader::ListLayersSharedWithMe(Json::Value& target,
+                                                                const UserId& author) const
+  {
+    target = Json::arrayValue;
+
+    if (IsValid())
+    {
+      Content::const_iterator found = that_.content_.find(author);
+
+      if (found != that_.content_.end())
+      {
+        assert(found->second != NULL);
+        LayersCollection::Iterator iterator(found->second->GetUserLayers());
+
+        while (!iterator.IsDone())
+        {
+          const UserLayer& layer = dynamic_cast<const UserLayer&>(iterator.GetLayer());
+
+          if (that_.IsSharedWith(layer, author, userId_, userRole_))
+          {
+            Json::Value item;
+            layer.Serialize(item);
+            target.append(item);
+          }
+
+          iterator.Next();
+        }
+      }
+    }
+  }
+
+
+  void AnnotationsWorkspace::UserReader::SearchActiveUsers(std::set<UserId>& target,
+                                                           const std::string& query) const
+  {
+    target.clear();
+
+    const boost::regex re(query);
+
+    PersistentInfo::ActiveUsersIterator iterator(*that_.persistentInfo_);
+
+    while (!iterator.IsDone())
+    {
+      if (boost::regex_search(iterator.GetUser().GetName(), re))
+      {
+        bool add = false;
+
+        switch (userRole_)
+        {
+          case ProjectRole_Instructor:
+            add = true;
+            break;
+
+          case ProjectRole_Learner:
+            switch (iterator.GetRole())  // Consider the role of the other user
+            {
+              case ProjectRole_Instructor:
+                // Learners can always share with instructors
+                add = true;
+                break;
+
+              case ProjectRole_Learner:
+                add = ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled();
+                break;
+
+              default:
+                throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+            }
+            break;
+
+          case ProjectRole_Guest:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        if (add)
+        {
+          target.insert(iterator.GetUser());
+        }
+      }
+
+      iterator.Next();
+    }
+  }
+
+
+  void AnnotationsWorkspace::UserReader::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 (that_.IsSharedWith(authorLayer, layer.GetAuthor(), userId_, userRole_))
+            {
+              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,
+                                               ProjectRole userRole) :
+    lock_(that.mutex_),
+    that_(that),
+    userId_(userId),
+    userRole_(userRole)
+  {
+    if (that.persistentInfo_->AddActiveUser(userId_, userRole))
+    {
+      // Only update the key-value store if this is the first time we
+      // meet this user or if their role has changed in the project
+      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 AnnotationsWorkspace::UserWriter::CreateUserLayer(Json::Value& answer)
+  {
+    assert(userSettings_ != NULL);
+
+    const std::string layerId = userSettings_->CreateUserLayer();
+    Commit();
+
+    const 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;
+
+    const UserLayer& layer = authorData.GetUserLayer(layerId);
+    if (!that_.IsSharedWith(layer, author, userId_, userRole_))
+    {
+      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();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,141 @@
+/**
+ * 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 "AnnotationsWorkspaceId.h"
+#include "ProjectInformation.h"
+#include "UserAnnotationsSettings.h"
+
+#include <IDynamicObject.h>
+#include <MultiThreading/ReaderWriterLock.h>
+
+
+namespace OrthancWSI
+{
+  class AnnotationsWorkspace : public Orthanc::IDynamicObject
+  {
+  private:
+    class PersistentInfo;
+
+    typedef std::map<UserId, UserAnnotationsSettings*>   Content;
+
+    Orthanc::ReaderWriterLock        mutex_;
+    AnnotationsWorkspaceId           id_;
+    std::unique_ptr<PersistentInfo>  persistentInfo_;
+    Content                          content_;
+    ProjectInformation               projectInformation_;
+
+    void Load(const UserId& user);
+
+    bool IsSharedWith(const UserLayer& layer,
+                      const UserId& layerAuthorId,
+                      const UserId& viewerId,
+                      ProjectRole viewerRole) const;
+
+  public:
+    explicit AnnotationsWorkspace(const AnnotationsWorkspaceId& id);
+
+    virtual ~AnnotationsWorkspace() ORTHANC_OVERRIDE;
+
+    const AnnotationsWorkspaceId& GetId() const
+    {
+      return id_;
+    }
+
+    std::string GetProjectName()
+    {
+      return projectInformation_.GetName();
+    }
+
+    std::string GetProjectDescription()
+    {
+      return projectInformation_.GetDescription();
+    }
+
+
+    class UserReader : public boost::noncopyable
+    {
+    private:
+      Orthanc::ReaderWriterLock::ReadLock lock_;
+      AnnotationsWorkspace&               that_;
+      UserId                              userId_;
+      const UserAnnotationsSettings*      userSettings_;
+      ProjectRole                         userRole_;
+
+    public:
+      UserReader(AnnotationsWorkspace& that,
+                 const UserId& userId,
+                 ProjectRole userRole);
+
+      bool IsValid() const
+      {
+        return userSettings_ != NULL;
+      }
+
+      void ListLayers(Json::Value& serialized) const;
+
+      void ListUsersSharingLayersWithMe(std::set<UserId>& target) const;
+
+      void ListLayersSharedWithMe(Json::Value& target,
+                                  const UserId& author) const;
+
+      void ListImportedLayers(std::set<UserId>& authors,
+                              std::set<std::string>& layerIds) const;
+
+      void SearchActiveUsers(std::set<UserId>& target,
+                             const std::string& query) const;
+    };
+
+
+    class UserWriter : public boost::noncopyable
+    {
+    private:
+      Orthanc::ReaderWriterLock::WriteLock  lock_;
+      AnnotationsWorkspace&                 that_;
+      UserId                                userId_;
+      UserAnnotationsSettings*              userSettings_;
+      ProjectRole                           userRole_;
+
+      void Commit();
+
+    public:
+      UserWriter(AnnotationsWorkspace& that,
+                 const UserId& userId,
+                 ProjectRole userRole);
+
+      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);
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/AnnotationsWorkspaceId.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,112 @@
+/**
+ * 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 "AnnotationsWorkspaceId.h"
+
+#include <OrthancException.h>
+#include <Toolbox.h>
+
+#include <boost/lexical_cast.hpp>
+
+
+namespace OrthancWSI
+{
+  std::string AnnotationsWorkspaceId::GetKeyPrefix() const
+  {
+    switch (level_)
+    {
+      case Orthanc::ResourceType_Series:
+        return projectId_ + "|series|" + resourceId_;
+
+      case Orthanc::ResourceType_Instance:
+        return projectId_ + "|instance|" + boost::lexical_cast<std::string>(frameNumber_) + "|" + resourceId_;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  AnnotationsWorkspaceId::AnnotationsWorkspaceId(const std::string& projectId,
+                                                 Orthanc::ResourceType level,
+                                                 const std::string& resourceId,
+                                                 unsigned int frameNumber) :
+    projectId_(projectId),
+    level_(level),
+    resourceId_(resourceId),
+    frameNumber_(frameNumber)
+  {
+    if (level_ != Orthanc::ResourceType_Series &&
+        level_ != Orthanc::ResourceType_Instance)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+
+    // The pipe symbol is disallowed as it is used to build the key, cf. GetKey()
+    if (!Orthanc::Toolbox::IsAsciiString(projectId_) ||
+        projectId_.find('|') != std::string::npos)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "Project name containing non-ASCII characters or the pipe symbol: " + projectId_);
+    }
+
+    if (!Orthanc::Toolbox::IsAsciiString(resourceId_) ||
+        resourceId_.find('|') != std::string::npos)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                      "Resource ID containing non-ASCII characters or the pipe symbol: " + resourceId_);
+    }
+  }
+
+
+  unsigned int AnnotationsWorkspaceId::GetFrameNumber() const
+  {
+    if (level_ == Orthanc::ResourceType_Instance)
+    {
+      return frameNumber_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  std::string AnnotationsWorkspaceId::GetInfoKey() const
+  {
+    return GetKeyPrefix() + "|info";
+  }
+
+
+  std::string AnnotationsWorkspaceId::GetSettingsKey(const UserId& user) const
+  {
+    return GetKeyPrefix() + "|settings|" + user.GetKey();
+  }
+
+
+  std::string AnnotationsWorkspaceId::GetFeaturesKey(const UserId& user) const
+  {
+    return GetKeyPrefix() + "|features|" + user.GetKey();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/AnnotationsWorkspaceId.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,72 @@
+/**
+ * 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 "UserId.h"
+
+#include <Enumerations.h>
+
+
+namespace OrthancWSI
+{
+  class AnnotationsWorkspaceId
+  {
+  private:
+    std::string            projectId_;
+    Orthanc::ResourceType  level_;
+    std::string            resourceId_;
+    unsigned int           frameNumber_;
+
+    std::string GetKeyPrefix() const;
+
+  public:
+    AnnotationsWorkspaceId(const std::string& projectId,
+                           Orthanc::ResourceType level,
+                           const std::string& resourceId,
+                           unsigned int frameNumber);
+
+    const std::string& GetProjectId() const
+    {
+      return projectId_;
+    }
+
+    Orthanc::ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const std::string& GetResourceId() const
+    {
+      return resourceId_;
+    }
+
+    unsigned int GetFrameNumber() const;
+
+    std::string GetInfoKey() const;
+
+    std::string GetSettingsKey(const UserId& user) const;
+
+    std::string GetFeaturesKey(const UserId& user) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/CachedAnnotationsWorkspace.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,63 @@
+/**
+ * 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 "CachedAnnotationsWorkspace.h"
+
+#include "../ViewerConfiguration.h"
+
+
+namespace OrthancWSI
+{
+  Orthanc::SharedObjectCache& CachedAnnotationsWorkspace::GetCache()
+  {
+    static boost::mutex  mutex;
+    static std::unique_ptr<Orthanc::SharedObjectCache>  cache;
+
+    {
+      boost::mutex::scoped_lock lock(mutex);
+
+      if (cache.get() == NULL)
+      {
+        const unsigned int cacheSize = ViewerConfiguration::GetInstance().GetAnnotationsCacheSize();
+        cache.reset(new Orthanc::SharedObjectCache(cacheSize));
+      }
+
+      return *cache;
+    }
+  }
+
+
+  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);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/CachedAnnotationsWorkspace.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,50 @@
+/**
+ * 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 "AnnotationsWorkspace.h"
+
+#include <Cache/SharedObjectCache.h>
+
+#include <boost/shared_ptr.hpp>
+
+
+namespace OrthancWSI
+{
+  class CachedAnnotationsWorkspace : public boost::noncopyable
+  {
+  private:
+    static Orthanc::SharedObjectCache& GetCache();
+
+    boost::shared_ptr<Orthanc::IDynamicObject>  cached_;
+
+  public:
+    explicit CachedAnnotationsWorkspace(const AnnotationsWorkspaceId& id);
+
+    AnnotationsWorkspace& GetContent() const
+    {
+      return dynamic_cast<AnnotationsWorkspace&>(*cached_);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/CachedUserFeatures.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,64 @@
+/**
+ * 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 "CachedUserFeatures.h"
+
+#include "../ViewerConfiguration.h"
+
+
+namespace OrthancWSI
+{
+  Orthanc::SharedObjectCache& CachedUserFeatures::GetCache()
+  {
+    static boost::mutex  mutex;
+    static std::unique_ptr<Orthanc::SharedObjectCache>  cache;
+
+    {
+      boost::mutex::scoped_lock lock(mutex);
+
+      if (cache.get() == NULL)
+      {
+        const unsigned int cacheSize = ViewerConfiguration::GetInstance().GetFeaturesCacheSize();
+        cache.reset(new Orthanc::SharedObjectCache(cacheSize));
+      }
+
+      return *cache;
+    }
+  }
+
+
+  CachedUserFeatures::CachedUserFeatures(const AnnotationsWorkspaceId& id,
+                                         const UserId& user)
+  {
+    const std::string key = id.GetFeaturesKey(user);
+
+    cached_ = GetCache().GetCachedValue(key);
+
+    if (cached_.get() == NULL)
+    {
+      cached_.reset(new UserFeatures(key));
+      GetCache().Store(key, cached_, 1);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/CachedUserFeatures.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,52 @@
+/**
+ * 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 "AnnotationsWorkspaceId.h"
+#include "UserFeatures.h"
+
+#include <Cache/SharedObjectCache.h>
+
+#include <boost/shared_ptr.hpp>
+
+
+namespace OrthancWSI
+{
+  class CachedUserFeatures : public boost::noncopyable
+  {
+  private:
+    static Orthanc::SharedObjectCache& GetCache();
+
+    boost::shared_ptr<Orthanc::IDynamicObject>  cached_;
+
+  public:
+    CachedUserFeatures(const AnnotationsWorkspaceId& id,
+                       const UserId& user);
+
+    UserFeatures& GetFeatures() const
+    {
+      return dynamic_cast<UserFeatures&>(*cached_);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/IAuthenticatedUser.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,343 @@
+/**
+ * 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 "IAuthenticatedUser.h"
+
+#include "../ViewerConfiguration.h"
+
+#include <Compatibility.h>
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+#include <Toolbox.h>
+
+#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+#include <cassert>
+#include <json/reader.h>
+
+
+namespace OrthancWSI
+{
+  namespace
+  {
+    // If Orthanc runs without an authentication plugin
+    class RootUser : public IAuthenticatedUser
+    {
+    public:
+      virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE
+      {
+        return UserId(UserId::Type_Root);
+      }
+
+      virtual std::string Format() const ORTHANC_OVERRIDE
+      {
+        return "(root)";
+      }
+
+      virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE
+      {
+        return ProjectRole_Instructor;
+      }
+    };
+
+
+    class GuestUser : public IAuthenticatedUser
+    {
+    public:
+      virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE
+      {
+        // Anonymous users cannot save annotations
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
+      }
+
+      virtual std::string Format() const ORTHANC_OVERRIDE
+      {
+        return "(guest)";
+      }
+
+      virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE
+      {
+        return ProjectRole_Guest;
+      }
+    };
+
+
+    class EducationPluginUser : public IAuthenticatedUser
+    {
+    public:
+      enum EducationRole
+      {
+        EducationRole_Administrator,
+        EducationRole_Standard,
+        EducationRole_Guest
+      };
+
+    private:
+      std::string            id_;
+      EducationRole          role_;
+      std::set<std::string>  instructorOfProjects_;
+      std::set<std::string>  learnerOfProjects_;
+
+    public:
+      explicit EducationPluginUser(const Json::Value& authentication)
+      {
+        assert(Orthanc::SerializationToolbox::ReadString(authentication, "source") == "orthanc-education");
+
+        id_ = Orthanc::SerializationToolbox::ReadString(authentication, "id");
+
+        const std::string role = Orthanc::SerializationToolbox::ReadString(authentication, "role");
+
+        if (role == "admin")
+        {
+          role_ = EducationRole_Administrator;
+        }
+        else if (role == "standard")
+        {
+          role_ = EducationRole_Standard;
+        }
+        else if (role == "guest")
+        {
+          role_ = EducationRole_Guest;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        }
+
+        Orthanc::SerializationToolbox::ReadSetOfStrings(instructorOfProjects_, authentication, "instructor_of");
+        Orthanc::SerializationToolbox::ReadSetOfStrings(learnerOfProjects_, authentication, "learner_of");
+      }
+
+      virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE
+      {
+        switch (role_)
+        {
+          case EducationRole_Administrator:
+            return UserId(UserId::Type_Root);
+
+          case EducationRole_Standard:
+            return UserId(UserId::Type_Standard, id_);
+
+          case EducationRole_Guest:
+            // Anonymous users cannot save annotations
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+
+      virtual std::string Format() const ORTHANC_OVERRIDE
+      {
+        return id_;
+      }
+
+      virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE
+      {
+        switch (role_)
+        {
+          case EducationRole_Administrator:
+            return ProjectRole_Instructor;
+
+          case EducationRole_Standard:
+            if (instructorOfProjects_.find(projectId) != instructorOfProjects_.end())
+            {
+              return ProjectRole_Instructor;
+            }
+            else if (learnerOfProjects_.find(projectId) != learnerOfProjects_.end())
+            {
+              return ProjectRole_Learner;
+            }
+            else
+            {
+              return ProjectRole_Guest;
+            }
+
+          case EducationRole_Guest:
+            return ProjectRole_Guest;
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+      }
+    };
+
+
+    class GenericStandardUser : public IAuthenticatedUser
+    {
+    private:
+      ProjectRole  role_;
+      std::string  username_;
+
+    public:
+      GenericStandardUser(ProjectRole role,
+                          const std::string& username) :
+        role_(role),
+        username_(username)
+      {
+      }
+
+      virtual UserId GetAnnotatingId() const ORTHANC_OVERRIDE
+      {
+        return UserId(UserId::Type_Standard, username_);
+      }
+
+      virtual std::string Format() const ORTHANC_OVERRIDE
+      {
+        return username_;
+      }
+
+      virtual ProjectRole GetRoleInProject(const std::string& projectId) const ORTHANC_OVERRIDE
+      {
+        return role_;
+      }
+    };
+  }
+
+
+  static IAuthenticatedUser* FromRegisteredUsers(const OrthancPluginHttpRequest* request)
+  {
+    for (uint32_t i = 0; i < request->headersCount; i++)
+    {
+      if (std::string(request->headersKeys[i]) == "authorization")
+      {
+        const std::string value(request->headersValues[i]);
+
+        std::vector<std::string> tokens;
+        Orthanc::Toolbox::TokenizeString(tokens, value, ' ');
+
+        if (tokens.size() == 2 &&
+            tokens[0] == "Basic")
+        {
+          std::string decoded;
+          Orthanc::Toolbox::DecodeBase64(decoded, tokens[1]);
+
+          Orthanc::Toolbox::TokenizeString(tokens, decoded, ':');
+          if (!tokens.empty() &&
+              !tokens[0].empty())
+          {
+            return new GenericStandardUser(ProjectRole_Instructor, tokens[0]);
+          }
+        }
+      }
+    }
+
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess,
+                                    "Forbidden access, HTTP basic authentication is missing");
+  }
+
+
+  static IAuthenticatedUser* FromHttpHeader(const OrthancPluginHttpRequest* request)
+  {
+    const std::string& header = ViewerConfiguration::GetInstance().GetAuthenticationHttpHeader();
+
+    for (uint32_t i = 0; i < request->headersCount; i++)
+    {
+      if (std::string(request->headersKeys[i]) == header)
+      {
+        const std::string user(request->headersValues[i]);
+
+        if (user.empty())
+        {
+          return new GuestUser;
+        }
+        else if (ViewerConfiguration::GetInstance().IsInstructor(user))
+        {
+          return new GenericStandardUser(ProjectRole_Instructor, user);
+        }
+        else
+        {
+          return new GenericStandardUser(ProjectRole_Learner, user);
+        }
+      }
+    }
+
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess,
+                                    "Forbidden access, as HTTP header \"" + header + "\" is not set by your proxy");
+  }
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9)
+  static IAuthenticatedUser* FromPlugin(const OrthancPluginHttpRequest* request)
+  {
+    if (request->authenticationPayloadSize == 0)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "No authentication plugin is properly installed");
+    }
+    else
+    {
+      const char* payload = reinterpret_cast<const char*>(request->authenticationPayload);
+
+      // We use "Json::Reader" as "Orthanc::ReadJson()" would write an
+      // error log if the authentication payload is not a JSON string
+      Json::Reader reader;
+
+      Json::Value authentication;
+      if (reader.parse(payload, payload + request->authenticationPayloadSize, authentication, false))
+      {
+        const std::string source = Orthanc::SerializationToolbox::ReadString(authentication, "source", "(none)");
+
+        if (source == "orthanc-education")
+        {
+          return new EducationPluginUser(authentication);
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Unknown authentication plugin: " + source);
+        }
+      }
+
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Unknown authentication plugin");
+    }
+  }
+#endif
+
+
+
+  IAuthenticatedUser* IAuthenticatedUser::FromHttpRequest(const OrthancPluginHttpRequest* request)
+  {
+    switch (ViewerConfiguration::GetInstance().GetAuthenticationSource())
+    {
+      case AuthenticationSource_None:
+        // No authentication is available, use the root user of Orthanc
+        return new RootUser;
+
+      case AuthenticationSource_RegisteredUsers:
+        return FromRegisteredUsers(request);
+
+      case AuthenticationSource_HttpHeader:
+        return FromHttpHeader(request);
+
+      case AuthenticationSource_Plugin:
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9)
+        return FromPlugin(request);
+#else
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+#endif
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/IAuthenticatedUser.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,51 @@
+/**
+ * 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 "../../Framework/FrameworkEnumerations.h"
+#include "UserId.h"
+
+#include <orthanc/OrthancCPlugin.h>
+
+#include <boost/noncopyable.hpp>
+
+
+namespace OrthancWSI
+{
+  class IAuthenticatedUser : public boost::noncopyable
+  {
+  public:
+    virtual ~IAuthenticatedUser()
+    {
+    }
+
+    virtual UserId GetAnnotatingId() const = 0;
+
+    virtual std::string Format() const = 0;
+
+    virtual ProjectRole GetRoleInProject(const std::string& projectId) const = 0;
+
+    static IAuthenticatedUser* FromHttpRequest(const OrthancPluginHttpRequest* request);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/ILayer.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,36 @@
+/**
+ * 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 "ISerializable.h"
+
+
+namespace OrthancWSI
+{
+  class ILayer : public ISerializable
+  {
+  public:
+    virtual std::string GetId() const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/ISerializable.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,50 @@
+/**
+ * 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 "ISerializable.h"
+
+#include "../ViewerToolbox.h"
+
+#include <Toolbox.h>
+
+
+namespace OrthancWSI
+{
+  void ISerializable::Serialize(std::string& serialized,
+                                const ISerializable& obj)
+  {
+    Json::Value value;
+    obj.Serialize(value);
+    Orthanc::Toolbox::WriteFastJson(serialized, value);
+  }
+
+
+  void ISerializable::SetKeyValueStore(const std::string& key,
+                                       const ISerializable& obj)
+  {
+    std::string s;
+    ISerializable::Serialize(s, obj);
+    ViewerToolbox::SetKeyValueStore(key, s);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/ISerializable.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,48 @@
+/**
+ * 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 <boost/noncopyable.hpp>
+#include <json/value.h>
+#include <string>
+
+
+namespace OrthancWSI
+{
+  class ISerializable : public boost::noncopyable
+  {
+  public:
+    virtual ~ISerializable()
+    {
+    }
+
+    virtual void Serialize(Json::Value& serialized) const = 0;
+
+    static void Serialize(std::string& serialized,
+                          const ISerializable& obj);
+
+    static void SetKeyValueStore(const std::string& key,
+                                 const ISerializable& obj);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/ImportedLayer.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,95 @@
+/**
+ * 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 "ImportedLayer.h"
+
+#include "UserLayer.h"
+
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+
+
+static const char* const KEY_VISIBLE = "visible";
+static const char* const KEY_AUTHOR = "author";
+static const char* const KEY_COLOR = "color";
+static const char* const KEY_ID = "id";
+static const char* const KEY_NAME = "name";
+
+
+namespace OrthancWSI
+{
+  ImportedLayer::ImportedLayer(const UserId& author,
+                               const UserLayer& layer) :
+    isVisible_(true),
+    color_(layer.GetColor()),
+    author_(author),
+    id_(layer.GetId()),
+    name_(layer.GetName())
+  {
+  }
+
+
+  ImportedLayer::ImportedLayer(const Json::Value& serialized)
+  {
+    if (!serialized.isObject() ||
+        !serialized.isMember(KEY_AUTHOR))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    isVisible_ = Orthanc::SerializationToolbox::ReadBoolean(serialized, KEY_VISIBLE);
+    author_ = UserId(serialized[KEY_AUTHOR]);
+    id_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_ID);
+    name_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_NAME);
+    color_ = BackgroundColor::FromHexadecimalString(Orthanc::SerializationToolbox::ReadString(serialized, KEY_COLOR));
+  }
+
+
+  void ImportedLayer::Assign(const ImportedLayer& other)
+  {
+    if (other.GetId() != id_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isVisible_ = other.isVisible_;
+      color_ = other.color_;
+      author_ = other.author_;
+      name_ = other.name_;
+    }
+  }
+
+
+  void ImportedLayer::Serialize(Json::Value& serialized) const
+  {
+    serialized = Json::objectValue;
+    serialized[KEY_VISIBLE] = isVisible_;
+    serialized[KEY_COLOR] = color_.ToHexadecimalString();
+    serialized[KEY_ID] = id_;
+    serialized[KEY_NAME] = name_;
+
+    author_.Serialize(serialized[KEY_AUTHOR]);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/ImportedLayer.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,81 @@
+/**
+ * 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 "../../Framework/BackgroundColor.h"
+#include "ILayer.h"
+#include "UserId.h"
+
+#include <Compatibility.h>
+
+
+namespace OrthancWSI
+{
+  class UserLayer;
+
+  class ImportedLayer : public ILayer
+  {
+  private:
+    bool             isVisible_;
+    BackgroundColor  color_;
+    UserId           author_;
+    std::string      id_;
+    std::string      name_;
+
+  public:
+    ImportedLayer(const UserId& author,
+                  const UserLayer& layer);
+
+    explicit ImportedLayer(const Json::Value& serialized);
+
+    void Assign(const ImportedLayer& other);
+
+    virtual std::string GetId() const ORTHANC_OVERRIDE
+    {
+      return id_;
+    }
+
+    bool IsVisible() const
+    {
+      return isVisible_;
+    }
+
+    const BackgroundColor& GetColor() const
+    {
+      return color_;
+    }
+
+    const UserId& GetAuthor() const
+    {
+      return author_;
+    }
+
+    const std::string& GetName() const
+    {
+      return name_;
+    }
+
+    virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/LayersCollection.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,170 @@
+/**
+ * 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 "LayersCollection.h"
+
+#include <OrthancException.h>
+
+#include <cassert>
+
+
+namespace OrthancWSI
+{
+  LayersCollection::~LayersCollection()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+
+
+  size_t LayersCollection::GetSize() const
+  {
+    assert(content_.size() == index_.size());
+    return content_.size();
+  }
+
+
+  void LayersCollection::AddLayer(ILayer* layer)
+  {
+    std::unique_ptr<ILayer> protection(layer);
+
+    if (layer == NULL)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
+    }
+
+    const std::string id = protection->GetId();
+
+    if (index_.find(id) != index_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls, "Duplicate layer ID");
+    }
+    else
+    {
+      content_.push_back(protection.release());
+
+      Content::iterator it = content_.end();
+      --it;  // Points to the element we just inserted
+      index_[id] = it;
+    }
+  }
+
+
+  bool LayersCollection::HasLayer(const std::string& id) const
+  {
+    return (index_.find(id) != index_.end());
+  }
+
+
+  ILayer& LayersCollection::GetLayer(const std::string& id) const
+  {
+    Index::const_iterator found = index_.find(id);
+
+    if (found == index_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+    else
+    {
+      assert(*(found->second) != NULL);
+      return **(found->second);
+    }
+  }
+
+
+  void LayersCollection::DeleteLayer(const std::string& id)
+  {
+    Index::iterator found = index_.find(id);
+
+    if (found == index_.end())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+    else
+    {
+      assert(*(found->second) != NULL);
+      delete *(found->second);
+      content_.erase(found->second);
+      index_.erase(found);
+    }
+  }
+
+
+  void LayersCollection::Serialize(Json::Value& serialized) const
+  {
+    serialized = Json::arrayValue;
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(*it != NULL);
+
+      Json::Value item;
+      (*it)->Serialize(item);
+      serialized.append(item);
+    }
+  }
+
+
+  LayersCollection::Iterator::Iterator(const LayersCollection& that) :
+    it_(that.content_.begin()),
+    end_(that.content_.end())
+  {
+  }
+
+
+  bool LayersCollection::Iterator::IsDone() const
+  {
+    return it_ == end_;
+  }
+
+
+  const ILayer& LayersCollection::Iterator::GetLayer() const
+  {
+    if (IsDone())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      assert(*it_ != NULL);
+      return **it_;
+    }
+  }
+
+
+  void LayersCollection::Iterator::Next()
+  {
+    if (IsDone())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      ++it_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/LayersCollection.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,76 @@
+/**
+ * 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 "ILayer.h"
+
+#include <Compatibility.h>
+
+#include <list>
+#include <map>
+
+
+namespace OrthancWSI
+{
+  class LayersCollection : public ISerializable
+  {
+  private:
+    typedef std::list<ILayer*>                        Content;
+    typedef std::map<std::string, Content::iterator>  Index;
+
+    Content  content_;
+    Index    index_;
+
+  public:
+    virtual ~LayersCollection() ORTHANC_OVERRIDE;
+
+    size_t GetSize() const;
+
+    void AddLayer(ILayer* layer /* takes ownership */);
+
+    bool HasLayer(const std::string& id) const;
+
+    ILayer& GetLayer(const std::string& id) const;
+
+    void DeleteLayer(const std::string& id);
+
+    virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE;
+
+    class Iterator : public boost::noncopyable
+    {
+    private:
+      Content::const_iterator  it_;
+      Content::const_iterator  end_;
+
+    public:
+      explicit Iterator(const LayersCollection& that);
+
+      bool IsDone() const;
+
+      const ILayer& GetLayer() const;
+
+      void Next();
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/ProjectInformation.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,91 @@
+/**
+ * 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 "ProjectInformation.h"
+
+#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+#include <SerializationToolbox.h>
+
+
+static boost::posix_time::ptime GetNow()
+{
+  return boost::posix_time::second_clock::universal_time();
+}
+
+
+namespace OrthancWSI
+{
+  void ProjectInformation::Load()
+  {
+    Json::Value info;
+
+    if (OrthancPlugins::RestApiGet(info, "/education/api-plugins/project?id=" + projectId_, true) &&
+        info.isObject())
+    {
+      // The "orthanc-education" plugin is available
+      name_ = Orthanc::SerializationToolbox::ReadString(info, "name", "");
+      description_ = Orthanc::SerializationToolbox::ReadString(info, "description", "");
+    }
+    else
+    {
+      name_.clear();
+      description_.clear();
+    }
+
+    lastUpdate_ = GetNow();
+  }
+
+
+  void ProjectInformation::Refresh()
+  {
+    if (GetNow() - lastUpdate_ >= boost::posix_time::seconds(10))
+    {
+      Load();
+    }
+  }
+
+
+  ProjectInformation::ProjectInformation(const std::string& projectId) :
+    projectId_(projectId)
+  {
+    Load();
+  }
+
+
+  std::string ProjectInformation::GetName()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Refresh();
+    return name_;
+  }
+
+
+  std::string ProjectInformation::GetDescription()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Refresh();
+    return description_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/ProjectInformation.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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 <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/thread/mutex.hpp>
+#include <string>
+
+
+namespace OrthancWSI
+{
+  class ProjectInformation : public boost::noncopyable
+  {
+  private:
+    boost::mutex              mutex_;
+    std::string               projectId_;
+    boost::posix_time::ptime  lastUpdate_;
+    std::string               name_;
+    std::string               description_;
+
+    void Load();
+
+    // The mutex must be locked
+    void Refresh();
+
+  public:
+    explicit ProjectInformation(const std::string& projectId);
+
+    std::string GetName();
+
+    std::string GetDescription();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserAnnotationsSettings.cpp	Mon Sep 07 20:58:51 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	Mon Sep 07 20:58:51 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()
+    {
+    }
+
+    explicit 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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserFeatures.cpp	Mon Sep 07 20:58:51 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 "UserFeatures.h"
+
+#include "../ViewerToolbox.h"
+
+#include <Compression/GzipCompressor.h>
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+#include <Toolbox.h>
+
+#include <boost/lexical_cast.hpp>
+
+
+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<std::string>(version));
+        }
+      }
+    }
+  }
+
+
+  void UserFeatures::Save() const
+  {
+    assert(content_.isArray());
+
+    Json::Value unserialized;
+    unserialized[KEY_VERSION] = static_cast<unsigned int>(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();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserFeatures.h	Mon Sep 07 20:58:51 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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <IDynamicObject.h>
+#include <MultiThreading/ReaderWriterLock.h>
+
+#include <json/value.h>
+#include <string>
+
+
+namespace OrthancWSI
+{
+  class UserFeatures : public Orthanc::IDynamicObject
+  {
+  private:
+    Orthanc::ReaderWriterLock  mutex_;
+    std::string                key_;
+    Json::Value                content_;
+
+    void Load();
+
+    void Save() const;
+
+  public:
+    explicit UserFeatures(const std::string& key);
+
+    void GetContent(Json::Value& target);
+
+    void SetContent(const Json::Value& content);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserId.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,145 @@
+/**
+ * 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 "UserId.h"
+
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+#include <Toolbox.h>
+
+
+static const char* const KEY_TYPE = "type";
+static const char* const KEY_NAME = "name";
+
+
+namespace OrthancWSI
+{
+  void UserId::Setup(Type type,
+                     const std::string& name)
+  {
+    type_ = type;
+    name_ = name;
+
+    switch (type_)
+    {
+      case Type_Invalid:
+      case Type_Root:
+        if (!name.empty())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        break;
+
+      case Type_Standard:
+        if (name.empty())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        }
+        break;
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  UserId::UserId(const Json::Value& serialized)
+  {
+    Setup(static_cast<Type>(Orthanc::SerializationToolbox::ReadInteger(serialized, KEY_TYPE)),
+          Orthanc::SerializationToolbox::ReadString(serialized, KEY_NAME));
+  }
+
+
+  bool UserId::Equals(const UserId& other) const
+  {
+    if (type_ != other.type_)
+    {
+      return false;
+    }
+    else if (type_ == Type_Standard)
+    {
+      return name_ == other.name_;
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+
+  bool UserId::operator<(const UserId& other) const
+  {
+    if (type_ < other.type_)
+    {
+      return true;
+    }
+    else if (type_ > other.type_)
+    {
+      return false;
+    }
+    else
+    {
+      return name_ < other.name_;
+    }
+  }
+
+
+  std::string UserId::GetKey() const
+  {
+    switch (type_)
+    {
+      case Type_Root:
+        return "root";
+
+      case Type_Standard:
+      {
+        // The pipe character "|" is not part of Base64, so we can safely use it to separate components
+        std::string s;
+        Orthanc::Toolbox::EncodeBase64(s, name_);
+        return "user_" + s;
+      }
+
+      case Type_Invalid:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  void UserId::Serialize(Json::Value& target) const
+  {
+    if (type_ == Type_Invalid)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      target = Json::objectValue;
+      target[KEY_TYPE] = static_cast<int>(type_);
+      target[KEY_NAME] = name_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserId.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,86 @@
+/**
+ * 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 <json/value.h>
+#include <string>
+
+
+namespace OrthancWSI
+{
+  class UserId
+  {
+  public:
+    enum Type
+    {
+      Type_Root,
+      Type_Standard,
+      Type_Invalid
+    };
+
+  private:
+    Type         type_;
+    std::string  name_;
+
+    void Setup(Type type,
+               const std::string& name);
+
+  public:
+    explicit UserId()
+    {
+      Setup(Type_Invalid, "");
+    }
+
+    explicit UserId(Type type)
+    {
+      Setup(type, "");
+    }
+
+    UserId(Type type,
+           const std::string& name)
+    {
+      Setup(type, name);
+    }
+
+    explicit UserId(const Json::Value& serialized);
+
+    Type GetType() const
+    {
+      return type_;
+    }
+
+    const std::string& GetName() const
+    {
+      return name_;
+    }
+
+    bool Equals(const UserId& other) const;
+
+    bool operator<(const UserId& other) const;
+
+    std::string GetKey() const;
+
+    void Serialize(Json::Value& target) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserLayer.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,199 @@
+/**
+ * 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 "UserLayer.h"
+
+#include "../ViewerConfiguration.h"
+
+#include <OrthancException.h>
+#include <SerializationToolbox.h>
+#include <Toolbox.h>
+
+#include <cassert>
+
+
+static const char* const KEY_COLOR = "color";
+static const char* const KEY_ID = "id";
+static const char* const KEY_NAME = "name";
+static const char* const KEY_PUBLIC = "public";
+static const char* const KEY_SHARED_WITH = "shared_with";
+static const char* const KEY_VISIBLE = "visible";
+
+
+namespace OrthancWSI
+{
+  UserLayer::UserLayer(const BackgroundColor& color,
+                       const std::string& name) :
+    isVisible_(true),
+    color_(color),
+    id_(Orthanc::Toolbox::GenerateUuid()),
+    name_(name),
+    isPublic_(false)
+  {
+  }
+
+
+  UserLayer::UserLayer(const Json::Value& serialized)
+  {
+    if (!serialized.isObject() ||
+        !serialized.isMember(KEY_SHARED_WITH) ||
+        !serialized[KEY_SHARED_WITH].isArray())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    isVisible_ = Orthanc::SerializationToolbox::ReadBoolean(serialized, KEY_VISIBLE);
+    color_ = BackgroundColor::FromHexadecimalString(Orthanc::SerializationToolbox::ReadString(serialized, KEY_COLOR));
+    id_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_ID);
+    name_ = Orthanc::SerializationToolbox::ReadString(serialized, KEY_NAME);
+    isPublic_ = Orthanc::SerializationToolbox::ReadBoolean(serialized, KEY_PUBLIC);
+
+    const Json::Value& v = serialized[KEY_SHARED_WITH];
+    for (Json::Value::ArrayIndex i = 0; i < v.size(); i++)
+    {
+      sharedWith_.insert(UserId(v[i]));
+    }
+  }
+
+
+  bool UserLayer::IsSharedWith(ProjectRole authorRole,
+                               const UserId& viewerId,
+                               ProjectRole viewerRole) const
+  {
+    /**
+
+       Instructors can see:
+
+       - All layers tagged "publicly shared with instructors"
+         (i.e. public), created by anyone (instructors or learners).
+
+       - Any layer explicitly shared with them, by anyone.
+
+       Learners can see:
+
+       - All layers tagged public that were created by instructors
+         (this is true "class-wide public" for instructor content).
+
+       - Any instructor layer explicitly shared with them.
+
+       - Any learner layer explicitly shared with them by name, only
+         if learner-to-learner sharing is enabled (cf. configuration
+         option "EnableLearnerToLearnerSharing").
+
+       Note 1: Learner layers tagged "public" are visible only to
+       instructors, never to other learners, regardless of the
+       learner-to-learner sharing configuration. This is a deliberate
+       asymmetry: for a learner, "public" means "submitted/visible to
+       instructors," not "visible to the class." This prevents one
+       learner's work from becoming broadcast to the whole cohort,
+       while still allowing small, named-group collaboration (e.g.,
+       project teams) through explicit sharing.
+
+       Note 2: Learner-to-learner sharing (configuration option)
+       governs only the explicit-share-list channel between
+       learners. It has no effect on instructor visibility and no
+       effect on the behavior of the "public" tag (public learner
+       layers are never learner-visible whether this option is "true"
+       or "false").
+
+     **/
+
+    if (viewerId.GetType() != UserId::Type_Standard)
+    {
+      return false;
+    }
+
+    const bool explicitlyShared = sharedWith_.find(viewerId) != sharedWith_.end();
+
+    switch (authorRole)
+    {
+      case ProjectRole_Instructor:
+        // Instructor layers: "public" truly means public to everyone,
+        // and explicit sharing is unconditional
+        return isPublic_ || explicitlyShared;
+
+      case ProjectRole_Learner:
+        switch (viewerRole)
+        {
+          case ProjectRole_Instructor:
+            // Instructors see public learner layers, and anything shared with them
+            return isPublic_ || explicitlyShared;
+
+          case ProjectRole_Learner:
+            // Learner viewing another learner's layer: "public" never applies,
+            // explicit sharing is gated by the configuration switch.
+            return explicitlyShared && ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled();
+
+          case ProjectRole_Guest:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
+
+          default:
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+      case ProjectRole_Guest:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess);
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+  }
+
+
+  void UserLayer::Assign(const UserLayer& other)
+  {
+    if (other.GetId() != id_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isVisible_ = other.isVisible_;
+      color_ = other.color_;
+      name_ = other.name_;
+      sharedWith_ = other.sharedWith_;
+      isPublic_ = other.isPublic_;
+    }
+  }
+
+
+  void UserLayer::Serialize(Json::Value& serialized) const
+  {
+    Json::Value sharedWith = Json::arrayValue;
+    for (std::set<UserId>::const_iterator it = sharedWith_.begin(); it != sharedWith_.end(); ++it)
+    {
+      Json::Value item;
+      it->Serialize(item);
+      sharedWith.append(item);
+    }
+
+    serialized = Json::objectValue;
+    serialized[KEY_VISIBLE] = isVisible_;
+    serialized[KEY_COLOR] = color_.ToHexadecimalString();
+    serialized[KEY_ID] = id_;
+    serialized[KEY_NAME] = name_;
+    serialized[KEY_PUBLIC] = isPublic_;
+    serialized[KEY_SHARED_WITH] = sharedWith;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/Annotations/UserLayer.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,82 @@
+/**
+ * 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 "../../Framework/BackgroundColor.h"
+#include "../../Framework/FrameworkEnumerations.h"
+#include "ILayer.h"
+#include "UserId.h"
+
+#include <Compatibility.h>
+
+#include <set>
+
+
+namespace OrthancWSI
+{
+  class UserLayer : public ILayer
+  {
+  private:
+    bool              isVisible_;
+    BackgroundColor   color_;
+    std::string       id_;
+    std::string       name_;
+    std::set<UserId>  sharedWith_;
+    bool              isPublic_;
+
+  public:
+    UserLayer(const BackgroundColor& color,
+              const std::string& name);
+
+    explicit UserLayer(const Json::Value& serialized);
+
+    virtual std::string GetId() const ORTHANC_OVERRIDE
+    {
+      return id_;
+    }
+
+    bool IsVisible() const
+    {
+      return isVisible_;
+    }
+
+    const BackgroundColor& GetColor() const
+    {
+      return color_;
+    }
+
+    const std::string& GetName() const
+    {
+      return name_;
+    }
+
+    bool IsSharedWith(ProjectRole authorRole,
+                      const UserId& viewerId,
+                      ProjectRole viewerRole) const;
+
+    void Assign(const UserLayer& other);
+
+    virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE;
+  };
+}
--- a/ViewerPlugin/CMakeLists.txt	Mon Sep 07 15:12:02 2026 +0200
+++ b/ViewerPlugin/CMakeLists.txt	Mon Sep 07 20:58:51 2026 +0200
@@ -37,7 +37,7 @@
 # Advanced parameters to fine-tune linking against system libraries
 SET(USE_SYSTEM_OPENJPEG ON CACHE BOOL "Use the system version of OpenJpeg")
 SET(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK")
-set(ORTHANC_SDK_VERSION "1.7.0" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"framework\" or \"1.7.0\")")
+SET(ORTHANC_SDK_VERSION "1.12.9" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"framework\", \"1.7.0\", or \"1.12.9\")")
 
 
 #####################################################################
@@ -99,6 +99,8 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
   if (ORTHANC_SDK_VERSION STREQUAL "1.7.0")
     include_directories(${CMAKE_SOURCE_DIR}/../Resources/Orthanc/Sdk-1.7.0)
+  elseif (ORTHANC_SDK_VERSION STREQUAL "1.12.9")
+    include_directories(${CMAKE_SOURCE_DIR}/../Resources/Orthanc/Sdk-1.12.9)
   elseif (ORTHANC_SDK_VERSION STREQUAL "framework")
     include_directories(${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Include/)
   else()
@@ -183,17 +185,33 @@
 #####################################################################
 
 set(ORTHANC_WSI_SOURCES
+  Annotations/AnnotationsRestApi.cpp
+  Annotations/AnnotationsWorkspace.cpp
+  Annotations/AnnotationsWorkspaceId.cpp
+  Annotations/CachedAnnotationsWorkspace.cpp
+  Annotations/CachedUserFeatures.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
   IIIF.cpp
   OrthancPluginConnection.cpp
   OrthancPyramidFrameFetcher.cpp
   Plugin.cpp
   RawTile.cpp
+  ViewerConfiguration.cpp
+  ViewerToolbox.cpp
 
   ${ORTHANC_WSI_DIR}/Framework/BackgroundColor.cpp
   ${ORTHANC_WSI_DIR}/Framework/ColorSpaces.cpp
   ${ORTHANC_WSI_DIR}/Framework/DicomToolbox.cpp
-  ${ORTHANC_WSI_DIR}/Framework/Enumerations.cpp
+  ${ORTHANC_WSI_DIR}/Framework/FrameworkEnumerations.cpp
   ${ORTHANC_WSI_DIR}/Framework/ImageToolbox.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedPyramidCache.cpp
   ${ORTHANC_WSI_DIR}/Framework/Inputs/DecodedTiledPyramid.cpp
--- a/ViewerPlugin/DicomPyramidCache.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/ViewerPlugin/DicomPyramidCache.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -139,7 +139,15 @@
     while (!cache_.IsEmpty())
     {
       DicomPyramid* pyramid = NULL;
-      std::string seriesId = cache_.RemoveOldest(pyramid);
+
+      try
+      {
+        /* std::string seriesId = */ cache_.RemoveOldest(pyramid);
+      }
+      catch (Orthanc::OrthancException&)
+      {
+        // Should never happen, don't throw exceptions in destructor
+      }
 
       if (pyramid != NULL)
       {
@@ -154,7 +162,7 @@
   {
     if (singleton_.get() == NULL)
     {
-      singleton_.reset(new DicomPyramidCache(new OrthancWSI::OrthancPluginConnection, maxSize, useMetadataCache));
+      singleton_.reset(new DicomPyramidCache(new OrthancPluginConnection, maxSize, useMetadataCache));
     }
     else
     {
--- a/ViewerPlugin/OrthancPyramidFrameFetcher.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/ViewerPlugin/OrthancPyramidFrameFetcher.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -91,11 +91,11 @@
   }
 
 
-  DecodedTiledPyramid* OrthancPyramidFrameFetcher::Fetch(const std::string &instanceId,
+  DecodedTiledPyramid* OrthancPyramidFrameFetcher::Fetch(const std::string& instanceId,
                                                          unsigned frameNumber)
   {
     OrthancPlugins::MemoryBuffer buffer;
-    buffer.GetDicomInstance(instanceId.c_str());
+    buffer.GetDicomInstance(instanceId);
 
     OrthancPlugins::DicomInstance dicom(buffer.GetData(), buffer.GetSize());
 
@@ -129,7 +129,7 @@
 
     if (paddingX_ >= 2)
     {
-      paddedWidth = OrthancWSI::CeilingDivision(frame->GetWidth(), paddingX_) * paddingX_;
+      paddedWidth = CeilingDivision(frame->GetWidth(), paddingX_) * paddingX_;
     }
     else
     {
@@ -138,7 +138,7 @@
 
     if (paddingY_ >= 2)
     {
-      paddedHeight = OrthancWSI::CeilingDivision(frame->GetHeight(), paddingY_) * paddingY_;
+      paddedHeight = CeilingDivision(frame->GetHeight(), paddingY_) * paddingY_;
     }
     else
     {
--- a/ViewerPlugin/Plugin.cpp	Mon Sep 07 15:12:02 2026 +0200
+++ b/ViewerPlugin/Plugin.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -23,21 +23,18 @@
 
 #include "../Framework/PrecompiledHeadersWSI.h"
 
-#include "OrthancPyramidFrameFetcher.h"
+#include "../Framework/ColorSpaces.h"
+#include "../Framework/ImageToolbox.h"
+#include "Annotations/AnnotationsRestApi.h"
 #include "DicomPyramidCache.h"
 #include "IIIF.h"
+#include "OrthancPluginConnection.h"
+#include "OrthancPyramidFrameFetcher.h"
 #include "RawTile.h"
-#include "../Framework/ColorSpaces.h"
-#include "../Framework/Inputs/DecodedTiledPyramid.h"
-#include "../Framework/Inputs/OnTheFlyPyramid.h"
-#include "../Framework/Inputs/DecodedPyramidCache.h"
-#include "../Framework/ImageToolbox.h"
+#include "ViewerConfiguration.h"
+#include "ViewerToolbox.h"
 
-#include <Compatibility.h>  // For std::unique_ptr
-#include <Images/Image.h>
-#include <Images/ImageProcessing.h>
 #include <Logging.h>
-#include <OrthancException.h>
 #include <SystemToolbox.h>
 
 #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
@@ -45,9 +42,6 @@
 #include <EmbeddedResources.h>
 
 #include <cassert>
-#include <Images/PngReader.h>
-
-#include "OrthancPluginConnection.h"
 
 
 #define ORTHANC_PLUGIN_NAME "wsi"
@@ -140,8 +134,7 @@
     }
   }
 
-  std::string s = answer.toStyledString();
-  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
+  OrthancWSI::ViewerToolbox::AnswerJson(output, answer);
 }
 
 
@@ -168,8 +161,7 @@
     DescribePyramid(answer, accessor.GetPyramid());
   }
 
-  std::string s = answer.toStyledString();
-  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
+  OrthancWSI::ViewerToolbox::AnswerJson(output, answer);
 }
 
 
@@ -462,6 +454,7 @@
 #endif
 
 
+
 extern "C"
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
@@ -496,6 +489,14 @@
     Orthanc::Logging::Initialize(context);
 #endif
 
+#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+    LOG(WARNING) << "The whole-slide imaging viewer was compiled against an old "
+                 << "version of the Orthanc SDK, annotations will not be persistent";
+#elif !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9)
+    LOG(WARNING) << "The whole-slide imaging viewer was compiled against an old "
+                 << "version of the Orthanc SDK, per-user annotations are not supported";
+#endif
+
     try
     {
       /**
@@ -514,7 +515,7 @@
          an a* or b* of -128.0, 0x8080 corresponds to an a* or b* of
          0.0 and 0xFFFF corresponds to an a* or b* of 127.0
 
-       **/
+      **/
 
       OrthancWSI::LABColor lab;
       if (!OrthancWSI::LABColor::DecodeDicomRecommendedAbsentPixelCIELab(lab, "65535\\0\\0") ||
@@ -539,122 +540,114 @@
       return -1;
     }
 
-    // Limit the number of PNG transcoders to the number of available
-    // hardware threads (e.g. number of CPUs or cores or
-    // hyperthreading units)
-    unsigned int threads = Orthanc::SystemToolbox::GetHardwareConcurrency();
-    OrthancWSI::RawTile::InitializeTranscoderSemaphore(threads);
-
-    LOG(WARNING) << "The whole-slide imaging plugin will use at most " << threads << " threads to transcode the tiles";
-
-    OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Provides a Web viewer of whole-slide microscopic images within Orthanc.");
-
-    OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */,
-                                                      true /* Use the metadata cache - Should be "false" only during development */);
-
+    try
     {
-      std::unique_ptr<OrthancWSI::OrthancPyramidFrameFetcher> fetcher(
-        new OrthancWSI::OrthancPyramidFrameFetcher(new OrthancWSI::OrthancPluginConnection(), false /* smooth - TODO PARAMETER */));
-      fetcher->SetPaddingX(64);  // TODO PARAMETER
-      fetcher->SetPaddingY(64);  // TODO PARAMETER
-      fetcher->SetDefaultBackgroundColor(255, 255, 255);  // TODO PARAMETER
+      // Limit the number of PNG transcoders to the number of available
+      // hardware threads (e.g. number of CPUs or cores or
+      // hyperthreading units)
+      unsigned int threads = Orthanc::SystemToolbox::GetHardwareConcurrency();
+      OrthancWSI::RawTile::InitializeTranscoderSemaphore(threads);
+
+      LOG(WARNING) << "The whole-slide imaging plugin will use at most " << threads << " threads to transcode the tiles";
+
+      OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Provides a Web viewer of whole-slide microscopic images within Orthanc.");
+
+      OrthancWSI::DicomPyramidCache::InitializeInstance(10 /* Number of pyramids to be cached - TODO parameter */,
+                                                        true /* Use the metadata cache - Should be "false" only during development */);
 
-      OrthancWSI::DecodedPyramidCache::InitializeInstance(fetcher.release(),
-                                                          10 /* TODO - PARAMETER */,
-                                                          256 * 1024 * 1024 /* TODO - PARAMETER */);
-    }
+      {
+        std::unique_ptr<OrthancWSI::OrthancPyramidFrameFetcher> fetcher(
+          new OrthancWSI::OrthancPyramidFrameFetcher(new OrthancWSI::OrthancPluginConnection(), false /* smooth - TODO PARAMETER */));
+        fetcher->SetPaddingX(64);  // TODO PARAMETER
+        fetcher->SetPaddingY(64);  // TODO PARAMETER
+        fetcher->SetDefaultBackgroundColor(255, 255, 255);  // TODO PARAMETER
 
-    OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback);
+        OrthancWSI::DecodedPyramidCache::InitializeInstance(fetcher.release(),
+                                                            10 /* TODO - PARAMETER */,
+                                                            256 * 1024 * 1024 /* TODO - PARAMETER */);
+      }
 
-    OrthancPlugins::RegisterRestCallback<ServeJavaScriptLibraries>("/wsi/libs/(.*)", true);
+      OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback);
+
+      OrthancPlugins::RegisterRestCallback<ServeJavaScriptLibraries>("/wsi/libs/(.*)", true);
 
 #if ORTHANC_STANDALONE == 1
-    OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.html)", true);
-    OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.js)", true);
+      OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.html)", true);
+      OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(viewer.js)", true);
 #else
-    OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.html)", true);
-    OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.js)", true);
+      OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.html)", true);
+      OrthancPlugins::RegisterRestCallback<ServeSourceFile>("/wsi/app/(viewer.js)", true);
 #endif
 
-    OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true);
-    OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
-    OrthancPlugins::RegisterRestCallback<ServeFramePyramid>("/wsi/frames-pyramids/([0-9a-f-]+)/([0-9-]+)", true);
-    OrthancPlugins::RegisterRestCallback<ServeFrameTile>("/wsi/frames-tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
+      OrthancPlugins::RegisterRestCallback<ServePyramid>("/wsi/pyramids/([0-9a-f-]+)", true);
+      OrthancPlugins::RegisterRestCallback<ServeTile>("/wsi/tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
+      OrthancPlugins::RegisterRestCallback<ServeFramePyramid>("/wsi/frames-pyramids/([0-9a-f-]+)/([0-9-]+)", true);
+      OrthancPlugins::RegisterRestCallback<ServeFrameTile>("/wsi/frames-tiles/([0-9a-f-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)/([0-9-]+)", true);
 
-    OrthancPlugins::OrthancConfiguration mainConfiguration;
+      const bool enableIIIF = OrthancWSI::ViewerConfiguration::GetInstance().IsIIIFEnabled();
+      bool serveMirador = false;
+      bool serveOpenSeadragon = false;
 
-    OrthancPlugins::OrthancConfiguration wsiConfiguration;
-    mainConfiguration.GetSection(wsiConfiguration, "WholeSlideImaging");
+      if (enableIIIF)
+      {
+        std::string iiifPublicUrl;
+        InitializeIIIF(iiifPublicUrl);
+
+        serveMirador = OrthancWSI::ViewerConfiguration::GetInstance().IsServeMirador();
+        serveOpenSeadragon = OrthancWSI::ViewerConfiguration::GetInstance().IsServeOpenSeadragon();
 
-    const bool enableIIIF = wsiConfiguration.GetBooleanValue("EnableIIIF", true);
-    bool serveMirador = false;
-    bool serveOpenSeadragon = false;
-    std::string iiifPublicUrl;
-
-    if (enableIIIF)
-    {
-      if (!wsiConfiguration.LookupStringValue(iiifPublicUrl, "OrthancPublicURL"))
-      {
-        unsigned int port = mainConfiguration.GetUnsignedIntegerValue("HttpPort", 8042);
-        iiifPublicUrl = "http://localhost:" + boost::lexical_cast<std::string>(port) + "/";
-      }
-
-      if (iiifPublicUrl.empty() ||
-          iiifPublicUrl[iiifPublicUrl.size() - 1] != '/')
-      {
-        iiifPublicUrl += "/";
+        bool value;
+        if (OrthancWSI::ViewerConfiguration::GetInstance().LookupForcePowersOfTwoScaleFactors(value))
+        {
+          SetIIIFForcePowersOfTwoScaleFactors(value);
+        }
+        else
+        {
+          /**
+           * By default, compatibility mode is disabled. However, if
+           * Mirador or OSD are enabled, compatibility mode is
+           * automatically enabled to enhance user experience, at least
+           * until issue 2379 of OSD is solved:
+           * https://github.com/openseadragon/openseadragon/issues/2379
+           **/
+          SetIIIFForcePowersOfTwoScaleFactors(serveMirador || serveOpenSeadragon);
+        }
       }
 
-      iiifPublicUrl += "wsi/iiif/";
-
-      InitializeIIIF(iiifPublicUrl);
+      LOG(WARNING) << "Support of IIIF is " << (enableIIIF ? "enabled" : "disabled") << " in the whole-slide imaging plugin";
 
-      serveMirador = wsiConfiguration.GetBooleanValue("ServeMirador", false);
-      serveOpenSeadragon = wsiConfiguration.GetBooleanValue("ServeOpenSeadragon", false);
-
-      bool value;
-      if (wsiConfiguration.LookupBooleanValue(value, "ForcePowersOfTwoScaleFactors"))
+      if (serveMirador)
       {
-        SetIIIFForcePowersOfTwoScaleFactors(value);
+        OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(mirador.html)", true);
       }
-      else
+
+      if (serveOpenSeadragon)
       {
-        /**
-         * By default, compatibility mode is disabled. However, if
-         * Mirador or OSD are enabled, compatibility mode is
-         * automatically enabled to enhance user experience, at least
-         * until issue 2379 of OSD is solved:
-         * https://github.com/openseadragon/openseadragon/issues/2379
-         **/
-        SetIIIFForcePowersOfTwoScaleFactors(serveMirador || serveOpenSeadragon);
+        OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(openseadragon.html)", true);
       }
-    }
+
+      {
+        // Extend the default Orthanc Explorer with custom JavaScript for WSI
 
-    LOG(WARNING) << "Support of IIIF is " << (enableIIIF ? "enabled" : "disabled") << " in the whole-slide imaging plugin";
+        std::string explorer;
+        Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
 
-    if (serveMirador)
-    {
-      OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(mirador.html)", true);
+        std::map<std::string, std::string> dictionary;
+        dictionary["ENABLE_IIIF"] = (enableIIIF ? "true" : "false");
+        dictionary["SERVE_MIRADOR"] = (serveMirador ? "true" : "false");
+        dictionary["SERVE_OPEN_SEADRAGON"] = (serveOpenSeadragon ? "true" : "false");
+        explorer = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary);
+
+        OrthancPlugins::ExtendOrthancExplorer(ORTHANC_PLUGIN_NAME, explorer);
+      }
+
+      // New in WSI 4.0
+      RegisterAnnotationsRestApi();
     }
-
-    if (serveOpenSeadragon)
-    {
-      OrthancPlugins::RegisterRestCallback<ServeEmbeddedFile>("/wsi/app/(openseadragon.html)", true);
-    }
-
+    catch (Orthanc::OrthancException& e)
     {
-      // Extend the default Orthanc Explorer with custom JavaScript for WSI
-
-      std::string explorer;
-      Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
-
-      std::map<std::string, std::string> dictionary;
-      dictionary["ENABLE_IIIF"] = (enableIIIF ? "true" : "false");
-      dictionary["SERVE_MIRADOR"] = (serveMirador ? "true" : "false");
-      dictionary["SERVE_OPEN_SEADRAGON"] = (serveOpenSeadragon ? "true" : "false");
-      explorer = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary);
-
-      OrthancPlugins::ExtendOrthancExplorer(ORTHANC_PLUGIN_NAME, explorer);
+      LOG(ERROR) << "Exception while starting whole-slide imaging viewer: " << e.What();
+      return -1;
     }
 
     return 0;
--- a/ViewerPlugin/RawTile.h	Mon Sep 07 15:12:02 2026 +0200
+++ b/ViewerPlugin/RawTile.h	Mon Sep 07 20:58:51 2026 +0200
@@ -23,7 +23,7 @@
 
 #pragma once
 
-#include "../Framework/Enumerations.h"
+#include "../Framework/FrameworkEnumerations.h"
 #include "../Framework/Inputs/ITiledPyramid.h"
 
 #include <orthanc/OrthancCPlugin.h>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/ViewerConfiguration.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,239 @@
+/**
+ * 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 "ViewerConfiguration.h"
+
+#include <SerializationToolbox.h>
+#include <Toolbox.h>
+
+
+static const char* const KEY_AUTHENTICATION_SOURCE = "AuthenticationSource";
+static const char* const KEY_AUTHENTICATION_HTTP_HEADER = "AuthenticationHttpHeader";
+static const char* const KEY_AUTHENTICATION_ENABLED = "AuthenticationEnabled";
+
+
+namespace OrthancWSI
+{
+  ViewerConfiguration::ViewerConfiguration()
+  {
+    mainConfiguration_.GetSection(wsiConfiguration_, "WholeSlideImaging");
+
+    std::string value;
+    if (wsiConfiguration_.LookupStringValue(value, KEY_AUTHENTICATION_SOURCE))
+    {
+      if (value == "None")
+      {
+        authenticationSource_ = AuthenticationSource_None;
+      }
+      else if (value == "HttpHeader")
+      {
+        authenticationSource_ = AuthenticationSource_HttpHeader;
+
+        if (!wsiConfiguration_.LookupStringValue(authenticationHttpHeader_, KEY_AUTHENTICATION_HTTP_HEADER) ||
+            authenticationHttpHeader_.empty())
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Configuration option \"" +
+                                          std::string(KEY_AUTHENTICATION_HTTP_HEADER) + "\" must be defined and non-empty");
+        }
+
+        Orthanc::Toolbox::ToLowerCase(authenticationHttpHeader_);
+
+        if (!wsiConfiguration_.LookupSetOfStrings(instructors_, "Instructors", false))
+        {
+          instructors_.clear();
+        }
+      }
+      else if (value == "RegisteredUsers")
+      {
+        if (mainConfiguration_.GetBooleanValue(KEY_AUTHENTICATION_ENABLED, false))
+        {
+          authenticationSource_ = AuthenticationSource_RegisteredUsers;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Configuration option \"" +
+                                          std::string(KEY_AUTHENTICATION_ENABLED) + "\" must be set to \"true\" " +
+                                          "to use the registered users as the authentication source");
+        }
+      }
+      else if (value == "Plugin")
+      {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 9)
+        authenticationSource_ = AuthenticationSource_Plugin;
+#else
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, "Your Orthanc SDK is too old to support "
+                                        "authentication from plugins, check configuration option \"" +
+                                        std::string(KEY_AUTHENTICATION_SOURCE) + "\"");
+#endif
+      }
+      else
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Unknown source of authentication: " + value);
+      }
+    }
+    else
+    {
+      authenticationSource_ = AuthenticationSource_None;
+    }
+  }
+
+
+  const ViewerConfiguration& ViewerConfiguration::GetInstance()
+  {
+    static ViewerConfiguration configuration;
+    return configuration;
+  }
+
+
+  bool ViewerConfiguration::IsIIIFEnabled() const
+  {
+    return wsiConfiguration_.GetBooleanValue("EnableIIIF", true);
+  }
+
+
+  std::string ViewerConfiguration::GetOrthancPublicUrl() const
+  {
+    std::string base;
+
+    if (wsiConfiguration_.LookupStringValue(base, "OrthancPublicURL"))
+    {
+      return base;
+    }
+    else
+    {
+      unsigned int port = mainConfiguration_.GetUnsignedIntegerValue("HttpPort", 8042);
+      return "http://localhost:" + boost::lexical_cast<std::string>(port);
+    }
+  }
+
+
+  std::string ViewerConfiguration::GetIIIFPublicUrl() const
+  {
+    if (IsIIIFEnabled())
+    {
+      return Orthanc::Toolbox::JoinUri(GetOrthancPublicUrl(), "wsi/iiif/");
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool ViewerConfiguration::IsServeMirador() const
+  {
+    if (IsIIIFEnabled())
+    {
+      return wsiConfiguration_.GetBooleanValue("ServeMirador", false);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool ViewerConfiguration::IsServeOpenSeadragon() const
+  {
+    if (IsIIIFEnabled())
+    {
+      return wsiConfiguration_.GetBooleanValue("ServeOpenSeadragon", false);
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool ViewerConfiguration::LookupForcePowersOfTwoScaleFactors(bool& value) const
+  {
+    if (IsIIIFEnabled())
+    {
+      return wsiConfiguration_.LookupBooleanValue(value, "ForcePowersOfTwoScaleFactors");
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool ViewerConfiguration::AreAnnotationsEnabled() const
+  {
+    return wsiConfiguration_.GetBooleanValue("EnableAnnotations", true);
+  }
+
+
+  const std::string& ViewerConfiguration::GetAuthenticationHttpHeader() const
+  {
+    if (authenticationSource_ != AuthenticationSource_HttpHeader)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      assert(!authenticationHttpHeader_.empty());
+      return authenticationHttpHeader_;
+    }
+  }
+
+
+  unsigned int ViewerConfiguration::GetAnnotationsCacheSize() const
+  {
+    return 100;   // TODO - Configuration option?
+  }
+
+
+  unsigned int ViewerConfiguration::GetFeaturesCacheSize() const
+  {
+    return 100;   // TODO - Configuration option?
+  }
+
+
+  bool ViewerConfiguration::IsAnnotationsSharingEnabled() const
+  {
+    return wsiConfiguration_.GetBooleanValue("EnableAnnotationsSharing", false);
+  }
+
+
+  bool ViewerConfiguration::IsInstructor(const std::string& username) const
+  {
+    if (username.empty())
+    {
+      return false;
+    }
+    else
+    {
+      return instructors_.find(username) != instructors_.end();
+    }
+  }
+
+
+  bool ViewerConfiguration::IsLearnerToLearnerSharingEnabled() const
+  {
+    return wsiConfiguration_.GetBooleanValue("EnableLearnerToLearnerSharing", false);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/ViewerConfiguration.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,78 @@
+/**
+ * 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 "../Framework/FrameworkEnumerations.h"
+
+#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+
+namespace OrthancWSI
+{
+  class ViewerConfiguration
+  {
+  private:
+    OrthancPlugins::OrthancConfiguration mainConfiguration_;
+    OrthancPlugins::OrthancConfiguration wsiConfiguration_;
+    AuthenticationSource                 authenticationSource_;
+    std::string                          authenticationHttpHeader_;
+    std::set<std::string>                instructors_;
+
+    ViewerConfiguration();
+
+  public:
+    static const ViewerConfiguration& GetInstance();
+
+    bool IsIIIFEnabled() const;
+
+    std::string GetOrthancPublicUrl() const;
+
+    std::string GetIIIFPublicUrl() const;
+
+    bool IsServeMirador() const;
+
+    bool IsServeOpenSeadragon() const;
+
+    bool LookupForcePowersOfTwoScaleFactors(bool& value) const;
+
+    bool AreAnnotationsEnabled() const;
+
+    AuthenticationSource GetAuthenticationSource() const
+    {
+      return authenticationSource_;
+    }
+
+    const std::string& GetAuthenticationHttpHeader() const;
+
+    unsigned int GetAnnotationsCacheSize() const;
+
+    unsigned int GetFeaturesCacheSize() const;
+
+    bool IsAnnotationsSharingEnabled() const;
+
+    bool IsInstructor(const std::string& username) const;
+
+    bool IsLearnerToLearnerSharingEnabled() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/ViewerToolbox.cpp	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,112 @@
+/**
+ * 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 "ViewerToolbox.h"
+
+#include <Logging.h>
+#include <Toolbox.h>
+
+#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+
+static const char* const KEY_VALUE_STORE = "wsi";
+
+
+namespace OrthancWSI
+{
+  namespace ViewerToolbox
+  {
+    void AnswerJson(OrthancPluginRestOutput* output,
+                    const Json::Value& value)
+    {
+      std::string s;
+      Orthanc::Toolbox::WriteFastJson(s, value);
+      OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), "application/json");
+    }
+
+
+    void AnswerEmpty(OrthancPluginRestOutput* output)
+    {
+      Json::Value answer = Json::objectValue;
+      AnswerJson(output, answer);
+    }
+
+
+    void SetKeyValueStore(const std::string& key,
+                          const std::string& value)
+    {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+      OrthancPlugins::KeyValueStore store(KEY_VALUE_STORE);
+      store.Store(key, value);
+#else
+      LOG(WARNING) << "Your Orthanc SDK is too old to save annotations";
+#endif
+    }
+
+
+    void SetKeyValueStore(const std::string& key,
+                          const Json::Value& value)
+    {
+      std::string s;
+      Orthanc::Toolbox::WriteFastJson(s, value);
+      SetKeyValueStore(key, s);
+    }
+
+
+    bool LookupKeyValueStore(std::string& value,
+                             const std::string& key)
+    {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+      OrthancPlugins::KeyValueStore store(KEY_VALUE_STORE);
+      return store.GetValue(value, key);
+#else
+      LOG(WARNING) << "Your Orthanc SDK is too old to load annotations";
+      return false;
+#endif
+    }
+
+
+    bool LookupKeyValueStore(Json::Value& value,
+                             const std::string& key)
+    {
+      std::string s;
+      if (LookupKeyValueStore(s, key))
+      {
+        if (Orthanc::Toolbox::ReadJson(value, s))
+        {
+          return true;
+        }
+        else
+        {
+          LOG(WARNING) << "Discarding incorrect JSON in the key-value store: " << key;
+          return false;
+        }
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ViewerPlugin/ViewerToolbox.h	Mon Sep 07 20:58:51 2026 +0200
@@ -0,0 +1,51 @@
+/**
+ * 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 <json/value.h>
+#include <orthanc/OrthancCPlugin.h>
+
+
+namespace OrthancWSI
+{
+  namespace ViewerToolbox
+  {
+    void AnswerJson(OrthancPluginRestOutput* output,
+                    const Json::Value& value);
+
+    void AnswerEmpty(OrthancPluginRestOutput* output);
+
+    void SetKeyValueStore(const std::string& key,
+                          const std::string& value);
+
+    void SetKeyValueStore(const std::string& key,
+                          const Json::Value& value);
+
+    bool LookupKeyValueStore(std::string& value,
+                             const std::string& key);
+
+    bool LookupKeyValueStore(Json::Value& value,
+                             const std::string& key);
+  }
+}
--- a/ViewerPlugin/WebApplication/viewer.html	Mon Sep 07 15:12:02 2026 +0200
+++ b/ViewerPlugin/WebApplication/viewer.html	Mon Sep 07 20:58:51 2026 +0200
@@ -3,7 +3,7 @@
 <html lang="en">
   <head>
     <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
     <title>Orthanc for Whole-Slide Imaging</title>
 
     <link rel="stylesheet" href="../libs/css/bootstrap.min.css" type="text/css">
@@ -14,6 +14,7 @@
       body {
           margin: 0;
           height: 100%;
+          overflow: hidden;
       }
 
       #map {
@@ -23,41 +24,682 @@
           width: 100%;
       }
 
-      .ol-rotate {
-          top: 4em;
-          left: .5em;
-          right: initial;
+      #toolbar-left { /* the top/left coordinates are adjusted in JavaScript */
+          position: absolute;
+          z-index: 20;
+      }
+
+      #toolbar-left .ol-rotate {
+          right: auto;
+      }
+
+      #toolbar-top { /* the top/left coordinates are adjusted in JavaScript */
+          padding-left: 1em;
+          position: absolute;
+          z-index: 20;
+      }
+
+      .icon-btn { /* the width and height can be adjusted in JavaScript */
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+          padding: 3px;
+      }
+
+      .icon-btn:disabled {
+          opacity: 100%;
+          background-color: white;
+      }
+
+      .btn-outline-secondary {
+          background-color: white;  /* make the toolbar buttons opaque */
+      }
+
+      .offcanvas {
+          background-color: #E5E5E5;
+      }
+
+      th {
+          background-color: transparent !important;
+      }
+
+      td {
+          background-color: transparent !important;
+      }
+
+      #right-panel {
+          --bs-offcanvas-width: 300px;
+          --bs-offcanvas-border-width: 0;
+          /*transition: 0.1s ease-in-out;*/   /* this gives a fast animation */
+          /*transition: none;*/   /* this removes the animation */
       }
+
+      #right-panel-toggle {
+          position: fixed;
+          top: 0;
+          bottom: 0;
+          z-index: 20;
+          border-radius: 4px 0 0 4px;
+          border-right: none;
+          opacity: 0.75;
+          padding: 0px;
+      }
+
+      .ol-attribution {
+          right: 20px !important;  /* keep the description out of the vertical toggle button */
+      }
+
+      /* Bootstrap Icons */
+      .bi {
+          display: inline-block;
+          width: 1em;
+          height: 1em;
+          background-color: currentColor;
+          -webkit-mask-size: 100% 100%;
+          mask-size: 100% 100%;
+          -webkit-mask-repeat: no-repeat;
+          mask-repeat: no-repeat;
+      }
+
+      /* The used icons must be added to ../Resources/CMake/JavaScriptLibraries.cmake */
+      .bi-arrow-clockwise       { -webkit-mask-image: url("../libs/svg/arrow-clockwise.svg"); mask-image: url("../libs/svg/arrow-clockwise.svg"); }
+      .bi-arrow-up-right        { -webkit-mask-image: url("../libs/svg/arrow-up-right.svg"); mask-image: url("../libs/svg/arrow-up-right.svg"); }
+      .bi-arrows                { -webkit-mask-image: url("../libs/svg/arrows.svg"); mask-image: url("../libs/svg/arrows.svg"); }
+      .bi-arrows-fullscreen     { -webkit-mask-image: url("../libs/svg/arrows-fullscreen.svg"); mask-image: url("../libs/svg/arrows-fullscreen.svg"); }
+      .bi-arrows-move           { -webkit-mask-image: url("../libs/svg/arrows-move.svg"); mask-image: url("../libs/svg/arrows-move.svg"); }
+      .bi-brightness-high       { -webkit-mask-image: url("../libs/svg/brightness-high.svg"); mask-image: url("../libs/svg/brightness-high.svg"); }
+      .bi-camera                { -webkit-mask-image: url("../libs/svg/camera.svg"); mask-image: url("../libs/svg/camera.svg"); }
+      .bi-chevron-compact-left  { -webkit-mask-image: url("../libs/svg/chevron-compact-left.svg"); mask-image: url("../libs/svg/chevron-compact-left.svg"); }
+      .bi-chevron-compact-right { -webkit-mask-image: url("../libs/svg/chevron-compact-right.svg"); mask-image: url("../libs/svg/chevron-compact-right.svg"); }
+      .bi-circle                { -webkit-mask-image: url("../libs/svg/circle.svg"); mask-image: url("../libs/svg/circle.svg"); }
+      .bi-cloud-download        { -webkit-mask-image: url("../libs/svg/cloud-download.svg"); mask-image: url("../libs/svg/cloud-download.svg"); }
+      .bi-eye                   { -webkit-mask-image: url("../libs/svg/eye.svg"); mask-image: url("../libs/svg/eye.svg"); }
+      .bi-eye-slash             { -webkit-mask-image: url("../libs/svg/eye-slash.svg"); mask-image: url("../libs/svg/eye-slash.svg"); }
+      .bi-file-earmark-plus     { -webkit-mask-image: url("../libs/svg/file-earmark-plus.svg"); mask-image: url("../libs/svg/file-earmark-plus.svg"); }
+      .bi-freehand-closed       { -webkit-mask-image: url("../libs/svg/freehand-area-svgrepo-com.svg"); mask-image: url("../libs/svg/freehand-area-svgrepo-com.svg"); }
+      .bi-freehand-line         { -webkit-mask-image: url("../libs/svg/freehand-svgrepo-com.svg"); mask-image: url("../libs/svg/freehand-svgrepo-com.svg"); }
+      .bi-geo-alt               { -webkit-mask-image: url("../libs/svg/geo-alt.svg"); mask-image: url("../libs/svg/geo-alt.svg"); }
+      .bi-hand-index            { -webkit-mask-image: url("../libs/svg/hand-index.svg"); mask-image: url("../libs/svg/hand-index.svg"); }
+      .bi-pen                   { -webkit-mask-image: url("../libs/svg/pen.svg"); mask-image: url("../libs/svg/pen.svg"); }
+      .bi-pentagon              { -webkit-mask-image: url("../libs/svg/pentagon.svg"); mask-image: url("../libs/svg/pentagon.svg"); }
+      .bi-share                 { -webkit-mask-image: url("../libs/svg/share.svg"); mask-image: url("../libs/svg/share.svg"); }
+      .bi-square                { -webkit-mask-image: url("../libs/svg/square.svg"); mask-image: url("../libs/svg/square.svg"); }
+      .bi-trash                 { -webkit-mask-image: url("../libs/svg/trash.svg"); mask-image: url("../libs/svg/trash.svg"); }
+
+
+      .ol-scale-magnification {
+          color: #000;
+          font-size: 12px;
+          text-align: center;
+          margin-top: 2px;
+      }
+
     </style>
 
   </head>
   <body>
-    <div id="map">
-    </div>
+    <div id="app">
+      <div id="map" :style="{ background: mapBackground }">
+        <div id="toolbar-top" style="display:none" v-show="toolbarsVisible">
+          <div class="btn-group" role="group" v-if="workspaceInfo.enabled">
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'select' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Select annotation"
+                    v-on:click="ToggleSelectTool()">
+              <i class="bi bi-hand-index"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'line' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw line"
+                    v-on:click="ToggleDrawTool('line')">
+              <i class="bi bi-arrows"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'point' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw point"
+                    v-on:click="ToggleDrawTool('point')">
+              <i class="bi bi-geo-alt"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'circle' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw circle"
+                    v-on:click="ToggleDrawTool('circle')">
+              <i class="bi bi-circle"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'rectangle' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw rectangle"
+                    v-on:click="ToggleDrawTool('rectangle')">
+              <i class="bi bi-square"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'closed-polygon' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw closed polygon"
+                    v-on:click="ToggleDrawTool('closed-polygon')">
+              <i class="bi bi-pentagon"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'freehand-line' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw freehand line"
+                    v-on:click="ToggleDrawTool('freehand-line')">
+              <i class="bi bi-freehand-line"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'freehand' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw freehand closed polygon"
+                    v-on:click="ToggleDrawTool('freehand')">
+              <i class="bi bi-freehand-closed"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'arrow' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Draw arrow"
+                    v-on:click="ToggleDrawTool('arrow')">
+              <i class="bi bi-arrow-up-right"></i>
+            </button>
+          </div>
 
-    <div style="display:none">
-      <div id="popover-content">
-        <div class="container">
-          <div class="row mb-2">
-            <div class="btn-group btn-group-sm" role="group">
-              <button type="button" class="btn btn-outline-dark" id="rotation-reset">Reset</button>
-              <button type="button" class="btn btn-outline-dark" id="rotation-plus90">+90&deg;</button>
-              <button type="button" class="btn btn-outline-dark" id="rotation-minus90">-90&deg;</button>
+          <div class="btn-group" role="group" v-if="workspaceInfo.enabled">
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'move' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Move annotation"
+                    v-on:click="ToggleDrawTool('move')">
+              <i class="bi bi-arrows-move"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    :class="{ active: activeDrawTool === 'modify' }"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Modify points"
+                    v-on:click="ToggleDrawTool('modify')">
+              <i class="bi bi-pen"></i>
+            </button>
+
+            <button type="button" class="btn btn-outline-secondary icon-btn"
+                    data-bs-toggle="tooltip" data-bs-placement="bottom" title="Delete selected annotation"
+                    v-bind:disabled="selectedFeature === null || selectedFeature.get('layer-id') !== activeUserLayerId"
+                    v-on:click="DeleteSelectedAnnotation()">
+              <i class="bi bi-trash"></i>
+            </button>
+          </div>
+
+          <div class="alert alert-danger alert-dismissible d-inline-block py-0" role="alert"
+               v-if="workspaceInfo.enabled && !workspaceInfo.persistent">
+            Your annotations will not be saved
+            <button type="button" class="btn-close top-50 translate-middle-y" data-bs-dismiss="alert" aria-label="Close"></button>
+          </div>
+        </div>
+
+        <div id="toolbar-left" style="display:none" v-show="toolbarsVisible">
+          <div id="toolbar-left-content" style="position:absolute; display:flex; flex-direction:column; align-items:center;">
+            <div class="btn-group-vertical" role="group" style="padding-top: 1em">
+              <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons"
+                      data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 1x"
+                      v-on:click="SetMagnification(map, referenceMagnification, 1)">
+                <small>1x</small>
+              </button>
+
+              <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons"
+                      data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 4x"
+                      v-on:click="SetMagnification(map, referenceMagnification, 4)">
+                <small>4x</small>
+              </button>
+
+              <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons"
+                      data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 10x"
+                      v-on:click="SetMagnification(map, referenceMagnification, 10)">
+                <small>10x</small>
+              </button>
+
+              <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons"
+                      data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 20x"
+                      v-on:click="SetMagnification(map, referenceMagnification, 20)">
+                <small>20x</small>
+              </button>
+
+              <button type="button" class="btn btn-outline-secondary p-0" v-if="showMagnificationButtons"
+                      data-bs-toggle="tooltip" data-bs-placement="right" title="Set magnification level to 40x"
+                      v-on:click="SetMagnification(map, referenceMagnification, 40)">
+                <small>40x</small>
+              </button>
+
+              <button type="button" class="btn btn-outline-secondary d-inline-flex" id="button-adjustments">
+                <i class="bi bi-brightness-high"></i>
+              </button>
+
+              <button type="button" class="btn btn-outline-secondary d-inline-flex"
+                      data-bs-toggle="tooltip" data-bs-placement="right" title="Take a screenshot"
+                      v-on:click="TakeScreenshot()">
+                <i class="bi bi-camera"></i>
+              </button>
+            </div>
+            <div id="toolbar-spinner" v-show="showSpinner" style="padding-top:1em;">
+              <div class="spinner-border spinner-border-sm text-secondary" role="status">
+                <span class="visually-hidden">Saving...</span>
+              </div>
             </div>
           </div>
-          <div class="row">
-            <input type="range" class="form-range" min="-180" max="180" id="rotation-slider">
+        </div>
+      </div>
+
+      <div class="offcanvas offcanvas-end" data-bs-backdrop="false" data-bs-scroll="true"
+           tabindex="-1" id="right-panel" v-show="workspaceInfo.enabled">
+        <div class="offcanvas-header">
+          <h5 class="offcanvas-title">Annotations</h5>
+          <button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
+        </div>
+        <div class="offcanvas-body p-2">
+          <h5 v-if="workspaceInfo.name !== ''">{{ workspaceInfo.name }}</h5>
+
+          <p v-if="workspaceInfo.description !== ''">
+            <small>{{ workspaceInfo.description }}</small>
+          </p>
+
+          <p v-if="workspaceInfo.user !== ''">
+            <small>Logged as:</small>
+            <span class="badge bg-secondary">{{ workspaceInfo.user }}</span>
+          </p>
+
+          <div class="d-flex align-items-center mb-1">
+            <h5>User layers</h5>
+            <button type="button" class="btn btn-primary btn-sm ms-auto"
+                    v-on:click="CreateUserLayer()">
+              <i class="bi bi-file-earmark-plus"></i>
+              Create
+            </button>
+          </div>
+          <table class="table table-sm table-borderless mb-0 align-middle">
+            <thead class="small text-muted">
+              <tr>
+                <th class="p-0 text-center fw-normal" title="Editing layer">E</th>
+                <th class="p-0 text-center fw-normal" title="Visible">V</th>
+                <th class="p-0 text-center fw-normal" title="Color">C</th>
+                <th class="px-1 py-0 fw-normal">Name</th>
+                <th class="p-0 text-center fw-normal" title="Share layer with other users"
+                    v-if="workspaceInfo.sharing">S</th>
+                <th class="p-0"></th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="layer in userLayers">
+                <td class="text-center align-middle p-0">
+                  <input type="radio" class="form-check-input m-0"
+                         v-bind:checked="activeUserLayerId === layer.id"
+                         v-on:change="activeUserLayerId = layer.id">
+                </td>
+                <td class="text-center align-middle p-0">
+                  <button type="button" class="btn btn-link p-1" style="color:inherit">
+                    <i class="bi"
+                       :class="layer.visible ? 'bi-eye' : 'bi-eye-slash'"
+                       v-on:click="layer.visible = !layer.visible; drawLayer.changed(); SaveUserLayer(layer)"
+                       ></i>
+                  </button>
+                </td>
+                <td class="text-center align-middle p-0">
+                  <input type="color" style="width:22px;height:22px;padding:1px;border:none;cursor:pointer;border-radius:3px"
+                         v-model:value="layer.color"
+                         v-on:input="drawLayer.changed()"
+                         v-on:change="SaveUserLayer(layer)">
+                </td>
+                <td class="align-middle px-1 py-0">
+                  <input type="text" class="form-control form-control-sm py-0"
+                         v-model:value="layer.name"
+                         v-on:change="SaveUserLayer(layer)">
+                </td>
+                <td class="text-center align-middle p-0" v-if="workspaceInfo.sharing">
+                  <button type="button" class="btn btn-link p-1"
+                          v-on:click="ShowShareUserLayerModal(layer)">
+                    <i class="bi bi-share"></i>
+                  </button>
+                </td>
+                <td class="text-center align-middle p-0">
+                  <button type="button" class="btn btn-link p-0"
+                          :class="userLayers.length <= 1 ? 'text-muted' : 'text-danger'"
+                          v-bind:disabled="userLayers.length <= 1"
+                          v-on:click="DeleteUserLayer(layer.id)">
+                    <i class="bi bi-trash"></i>
+                  </button>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+
+          <hr class="my-4">
+
+          <div class="d-flex align-items-center mb-1" v-if="workspaceInfo.sharing">
+            <h5>Imported layers</h5>
+            <button type="button" class="btn btn-primary btn-sm ms-auto"
+                    v-on:click="ShowImportLayerModal()">
+              <i class="bi bi-cloud-download"></i>
+              Import
+            </button>
+          </div>
+
+          <table class="table table-sm table-borderless mb-3 align-middle" v-if="workspaceInfo.sharing">
+            <thead class="small text-muted">
+              <tr>
+                <th class="p-0 text-center fw-normal" title="Visible">V</th>
+                <th class="p-0 text-center fw-normal" title="Color">C</th>
+                <th class="px-1 py-0 fw-normal" style="width:100%">Name</th>
+                <th class="p-0"></th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="layer in importedLayers">
+                <td class="text-center align-middle p-0">
+                  <button type="button" class="btn btn-link p-1" style="color:inherit">
+                    <i class="bi"
+                       :class="layer.visible ? 'bi-eye' : 'bi-eye-slash'"
+                       v-on:click="layer.visible = !layer.visible; drawImportedLayer.changed(); SaveImportedLayer(layer)"
+                       ></i>
+                  </button>
+                </td>
+                <td class="text-center align-middle p-0">
+                  <input type="color" style="width:22px;height:22px;padding:1px;border:none;cursor:pointer;border-radius:3px"
+                         v-model:value="layer.color"
+                         v-on:input="drawImportedLayer.changed()"
+                         v-on:change="SaveImportedLayer(layer)">
+                </td>
+                <td class="align-middle px-1 py-0">
+                  <span class="small d-block">{{layer.name}}</span>
+                  <span class="badge bg-secondary">{{ layer.author.name }}</span>
+                </td>
+                <td class="text-center align-middle p-0">
+                  <button type="button" class="btn btn-link p-0"
+                          v-on:click="DeleteImportedLayer(layer.id)">
+                    <i class="bi bi-trash"></i>
+                  </button>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+
+          <button class="btn btn-sm btn-primary w-100 mb-2" v-on:click="ReloadImportedFeatures()"
+                  v-if="workspaceInfo.sharing && importedLayers.length > 0">
+            <i class="bi bi-arrow-clockwise me-2 align-middle"></i> Reload imported content
+          </button>
+
+          <hr class="my-4" v-if="workspaceInfo.sharing">
+
+          <div v-show="selectedFeature !== null" class="mb-3">
+            <h5>Current selection</h5>
+            <button class="btn btn-sm btn-primary w-100 mb-2" v-on:click="FocusAnnotation()">
+              <i class="bi bi-arrows-fullscreen me-2 align-middle"></i> Focus on annotation
+            </button>
+            <div>
+              <template v-for="prop in annotationProperties">
+                <div v-if="prop.type === 'readonly'" class="d-flex align-items-baseline border-bottom py-1 gap-2">
+                  <span class="text-muted small flex-shrink-0" style="width:5em">{{prop.label}}</span>
+                  <span class="text-break small">{{prop.value}}</span>
+                </div>
+                <div v-else-if="prop.type === 'readonly-textarea'" class="d-flex align-items-start border-bottom py-1 gap-2">
+                  <label class="text-muted small flex-shrink-0 mt-1 mb-0" style="width:5em">{{prop.label}}</label>
+                  <textarea class="form-control form-control-sm small flex-grow-1"
+                            rows="3"
+                            readonly
+                            style="background-color: transparent;"
+                            v-model="prop.value"></textarea>
+                </div>
+                <div v-else-if="prop.type === 'editable'" class="d-flex align-items-center border-bottom py-1 gap-2">
+                  <label class="text-muted small flex-shrink-0 mb-0" style="width:5em">{{prop.label}}</label>
+                  <input type="text" class="form-control form-control-sm small flex-grow-1"
+                         v-model="prop.value"
+                         v-on:change="UpdateAnnotationProperty(prop)">
+                </div>
+                <div v-else-if="prop.type === 'editable-textarea'" class="d-flex align-items-start border-bottom py-1 gap-2">
+                  <label class="text-muted small flex-shrink-0 mt-1 mb-0" style="width:5em">{{prop.label}}</label>
+                  <textarea class="form-control form-control-sm small flex-grow-1"
+                            rows="3"
+                            v-model="prop.value"
+                            v-on:change="UpdateAnnotationProperty(prop)"></textarea>
+                </div>
+                <div v-else-if="prop.type === 'dropdown'" class="d-flex align-items-center border-bottom py-1 gap-2">
+                  <label class="text-muted small flex-shrink-0 mb-0" style="width:5em">{{prop.label}}</label>
+                  <select class="form-select form-select-sm small flex-grow-1"
+                          v-model="prop.value"
+                          v-on:change="UpdateAnnotationProperty(prop)">
+                    <option v-for="opt in prop.options" :value="opt.value">{{opt.label}}</option>
+                  </select>
+                </div>
+              </template>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <button id="right-panel-toggle" type="button" class="btn btn-secondary"
+              style="display:none" v-show="workspaceInfo.enabled && toolbarsVisible"
+              data-bs-toggle="offcanvas" data-bs-target="#right-panel">
+        <i class="bi" :class="panelOpen ? 'bi-chevron-compact-right' : 'bi-chevron-compact-left'"></i>
+      </button>
+
+
+      <!-- Confirmation modal for annotation deletion -->
+      <div class="modal fade" id="modal-delete-annotation" tabindex="-1">
+        <div class="modal-dialog modal-sm">
+          <div class="modal-content">
+            <div class="modal-body">
+              Delete this annotation?
+            </div>
+            <div class="modal-footer py-1">
+              <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
+              <button type="button" class="btn btn-sm btn-danger" v-on:click="ConfirmDeleteAnnotation()">Delete</button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+
+      <!-- Confirmation modal for user layer deletion -->
+      <div class="modal fade" id="modal-delete-user-layer" tabindex="-1">
+        <div class="modal-dialog modal-sm">
+          <div class="modal-content">
+            <div class="modal-body">
+              Delete this layer and all its annotations?
+            </div>
+            <div class="modal-footer py-1">
+              <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
+              <button type="button" class="btn btn-sm btn-danger" v-on:click="UserLayerDeleteConfirmed()">Delete</button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+
+      <!-- Confirmation modal for imported layer deletion -->
+      <div class="modal fade" id="modal-delete-imported-layer" tabindex="-1">
+        <div class="modal-dialog modal-sm">
+          <div class="modal-content">
+            <div class="modal-body">
+              Remove this imported layer?
+            </div>
+            <div class="modal-footer py-1">
+              <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
+              <button type="button" class="btn btn-sm btn-danger" v-on:click="ImportedLayerDeleteConfirmed()">Delete</button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+
+      <!-- Modal to configure the sharing of a user layer -->
+      <div class="modal fade" id="modal-share-user-layer" tabindex="-1">
+        <div class="modal-dialog modal-xl">
+          <div class="modal-content">
+            <div class="modal-header py-2">
+              <h5 class="modal-title fs-6">Sharing parameters of layer "{{shareLayerTarget.name}}"</h5>
+              <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+            </div>
+            <div class="modal-body">
+
+              <div class="form-check mb-3">
+                <input class="form-check-input" type="checkbox" id="share-layer-public"
+                       v-model="shareLayerPublic">
+                <label class="form-check-label" for="share-layer-public">
+                  <span v-if="workspaceInfo.is_instructor">Make this layer public (accessible to any instructor or learner)</span>
+                  <span v-if="workspaceInfo.is_learner">Make this layer visible to instructors</span>
+                </label>
+              </div>
+
+              <label class="form-label small mb-1">
+                <span v-if="shareLayerCanAddLearner">Shared with specific learners or instructors:</span>
+                <span v-if="!shareLayerCanAddLearner">Shared with specific instructors (learners in this list are ignored):</span>
+              </label>
+              <div class="mb-3 border rounded p-2" style="min-height:2.5em; max-height:8em; overflow-y:auto">
+                <span v-if="shareLayerUsers.length === 0" class="text-muted small">No users added</span>
+                <span v-for="(user, index) in shareLayerUsers" :key="user.name"
+                      class="badge bg-secondary me-1 mb-1"
+                      style="cursor:pointer"
+                      v-on:click="shareLayerUsers.splice(index, 1); shareLayerSearchQuery = ''; shareLayerSearchResults = [];"
+                      :title="'Remove ' + user.name">
+                  {{ user.name }} &times;
+                </span>
+              </div>
+
+              <label class="form-label small mb-1">
+                <span v-if="shareLayerCanAddLearner">Add learner or instructor:</span>
+                <span v-if="!shareLayerCanAddLearner">Add instructor:</span>
+              </label>
+              <div class="input-group input-group-sm mb-1">
+                <input type="text" class="form-control" placeholder="Type to search or enter a user ID..."
+                       v-model="shareLayerSearchQuery"
+                       v-on:input="ShareLayerSearchUsers()"
+                       v-on:keydown.enter="ShareLayerAddStandardUser(shareLayerSearchQuery)">
+                <button class="btn btn-outline-secondary" type="button"
+                        v-on:click="ShareLayerAddStandardUser(shareLayerSearchQuery)">Add</button>
+              </div>
+              <div v-if="shareLayerSearchResults.length > 0"
+                   class="list-group list-group-flush mt-1 border rounded"
+                   style="max-height:8em; overflow-y:auto">
+                <button type="button"
+                        class="list-group-item list-group-item-action py-1 small"
+                        v-for="user in shareLayerSearchResults"
+                        :key="user.name"
+                        v-on:click="ShareLayerAddUser(user)">
+                  {{ user.name }}
+                </button>
+              </div>
+
+            </div>
+            <div class="modal-footer py-1">
+              <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
+              <button type="button" class="btn btn-sm btn-primary" v-on:click="ShareLayerSave()">Save</button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+
+      <!-- Modal to import a layer from another user -->
+      <div class="modal fade" id="modal-import-layer" tabindex="-1">
+        <div class="modal-dialog">
+          <div class="modal-content">
+            <div class="modal-header py-2">
+              <h5 class="modal-title fs-6">Import shared layer</h5>
+              <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+            </div>
+            <div class="modal-body">
+              <div class="mb-2">
+                <label class="form-label small mb-1">User</label>
+                <input type="text" class="form-control form-control-sm"
+                       placeholder="Type to search a user..."
+                       v-model="importUserSearchQuery"
+                       v-on:input="ImportUserSearchChanged()">
+                <div v-if="importUserSearchResults.length > 0"
+                     class="list-group list-group-flush mt-1 border rounded"
+                     style="max-height:8em; overflow-y:auto">
+                  <button type="button"
+                          class="list-group-item list-group-item-action py-1 small"
+                          v-for="user in importUserSearchResults"
+                          :key="user.name"
+                          v-on:click="ImportUserSelect(user)">
+                    {{ user.name }}
+                  </button>
+                </div>
+                <div v-if="importSelectedUser" class="mt-1 small text-muted">
+                  Selected: <strong>{{ importSelectedUser.name }}</strong>
+                </div>
+              </div>
+              <div v-show="importSelectedUser !== ''">
+                <label class="form-label small mb-1">Layer</label>
+                <select class="form-select form-select-sm" v-model="importSelectedLayer">
+                  <option value="">Select a layer...</option>
+                  <option v-for="layer in importAvailableLayers" :value="layer.id">{{layer.name}}</option>
+                </select>
+              </div>
+            </div>
+            <div class="modal-footer py-1">
+              <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
+              <button type="button" class="btn btn-sm btn-primary"
+                      :disabled="!importSelectedLayer"
+                      v-on:click="ImportLayerConfirmed()">Import</button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+
+      <div style="display:none">
+        <div id="popover-rotate">
+          <div class="container">
+            <div class="row mb-2">
+              <div class="btn-group btn-group-sm" role="group">
+                <button type="button" class="btn btn-outline-dark" v-on:click="ResetRotation()">Reset</button>
+                <button type="button" class="btn btn-outline-dark" v-on:click="RotateBy(90)">+90&deg;</button>
+                <button type="button" class="btn btn-outline-dark" v-on:click="RotateBy(-90)">-90&deg;</button>
+              </div>
+            </div>
+            <div class="row">
+              <input type="range" class="form-range" min="-180" max="180"
+                     v-model.number="rotationDeg"
+                     v-on:input="SetMapRotation()" v-on:change="SetMapRotation()">
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div style="display:none">
+        <div id="popover-adjustments">
+          <div class="container">
+            <div class="mb-1">
+              <label for="range-brightness" class="form-label">Brightness: <b class="ps-2">{{brightness}}</b></label>
+              <input id="range-brightness" type="range" class="form-range" min="-100" max="100" step="1"
+                     v-model.number="brightness" v-on:input="map.render()">
+            </div>
+            <div class="mb-1">
+              <label for="range-contrast" class="form-label">Contrast: <b class="ps-2">{{contrast}}</b></label>
+              <input id="range-contrast" type="range" class="form-range" min="-100" max="100" step="1"
+                     v-model.number="contrast" v-on:input="map.render()">
+            </div>
+            <div class="mb-1">
+              <label for="range-saturation" class="form-label">Saturation: <b class="ps-2">{{saturation}}</b></label>
+              <input id="range-saturation" type="range" class="form-range" min="-100" max="100" step="1"
+                     v-model.number="saturation" v-on:input="map.render()">
+            </div>
+            <div class="d-grid">
+              <button class="btn btn-primary" type="button" v-on:click="ResetAdjustments()">
+                Reset
+              </button>
+            </div>
           </div>
         </div>
       </div>
     </div>
 
-    <!-- This is the version of jQuery that is used by Orthanc Explorer -->
-    <script src="../../app/libs/jquery.min.js"></script>
 
-    <script src="../libs/js/popper.min.js"></script>
+    <script src="../libs/js/axios.min.js"></script>
+    <script src="../libs/js/vue.min.js"></script>
+    <script src="../libs/js/popper.min.js"></script>   <!-- Popper is needed by Bootstrap (for popovers) -->
     <script src="../libs/js/bootstrap.min.js"></script>
     <script src="../libs/js/ol.js"></script>
+    <script src="../libs/js/modern-screenshot.js"></script>
+
     <script src="viewer.js"></script>
   </body>
 </html>
--- a/ViewerPlugin/WebApplication/viewer.js	Mon Sep 07 15:12:02 2026 +0200
+++ b/ViewerPlugin/WebApplication/viewer.js	Mon Sep 07 20:58:51 2026 +0200
@@ -21,191 +21,1651 @@
  **/
 
 
+var app = new Vue({
+  el: '#app',
+
+  data() {
+    return {
+      projectId: '',
+      level: '',
+      resourceId: '',
+      frameNumber: 0,
+      brightness: 0, // In the range between [-100,100]
+      contrast: 0,   // In the range between [-100,100]
+      saturation: 0, // In the range between [-100,100]
+      // hue: 0,        // Degrees, in the range between [-180,180]
+
+      // Main state for annotations
+      workspaceInfo: {},
+      imageDescription: '',
+      userLayers: [],
+      importedLayers: [],
+      activeUserLayerId: null,
+
+      // UI state
+      toolbarsVisible: false,
+      panelOpen: false,
+      mapBackground: '',
+      rotationDeg: 0,
+      activeDrawTool: null,
+      showMagnificationButtons: false,
+
+      // Bootstrap modals
+      modalDeleteUserLayer: null,
+      modalDeleteImportedLayer: null,
+      modalDeleteAnnotation: null,
+      pendingDelete: null,
+
+      // Loading/saving using the backend
+      isPendingChange: false,
+      isSaving: false,
+      showSpinner: false,
+
+      // Annotation selection panel
+      annotationProperties: [],
+      selectedFeature: null,
+
+      // OpenLayers objects
+      map: null,
+      drawSource: null,
+      drawLayer: null,  // Used in HTML
+      drawImportedSource: null,
+      drawImportedLayer: null,
+      drawLine: null,
+      drawPoint: null,
+      drawCircle: null,
+      drawRectangle: null,
+      drawClosedPolygon: null,
+      drawFreehand: null,
+      drawFreehandLine: null,
+      drawArrow: null,
+      moveFeature: null,
+      modifyFeature: null,
+      modifyFeatureCollection: null,
+      selectAnnotation: null,
+
+      /**
+       * Magnification at full-resolution image pixels, convention
+       * commonly used for pathology WSI: 40x scan = 0.25 µm/pixel
+       **/
+      referenceMagnification: 40,  // TODO - Could be read from pyramid
+
+      // Share layer modal
+      modalShareUserLayer: null,
+      shareLayerTarget: {},
+      shareLayerPublic: false,
+      shareLayerUsers: [],
+      shareLayerSearchQuery: '',
+      shareLayerSearchResults: [],
+
+      // Import layer modal
+      modalImportLayer: null,
+      importAvailableUsers: [],
+      importUserSearchQuery: '',
+      importUserSearchResults: [],
+      importSelectedUser: '',
+      importSelectedLayer: '',
+      importAvailableLayers: []
+    };
+  },
+
+  computed: {
+    shareLayerCanAddLearner: function() {
+      return (this.workspaceInfo.is_instructor === true ||
+              (this.workspaceInfo.is_learner === true &&
+               this.workspaceInfo.learner_to_learner_sharing === true));
+    }
+  },
+
+  watch: {
+    activeUserLayerId: function() {
+      if (this.activeDrawTool === 'modify') {
+        this.RefreshModifyFeatureCollection();
+      }
+    }
+  },
+
+  mounted: function() {
+    this.InitializePanelAnimation();
+
+    this.modalDeleteUserLayer = new bootstrap.Modal(document.getElementById('modal-delete-user-layer'));
+    this.modalDeleteImportedLayer = new bootstrap.Modal(document.getElementById('modal-delete-imported-layer'));
+    this.modalDeleteAnnotation = new bootstrap.Modal(document.getElementById('modal-delete-annotation'));
+    this.modalShareUserLayer = new bootstrap.Modal(document.getElementById('modal-share-user-layer'));
+    this.modalImportLayer = new bootstrap.Modal(document.getElementById('modal-import-layer'));
+
+    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) {
+      new bootstrap.Tooltip(el, { trigger: 'hover' });
+    });
+
+    // document.getElementById('right-panel-toggle').click();  // Open the side menu at startup
+
+    const params = new URLSearchParams(document.location.search);
+
+    if (params.has('project')) {
+      this.projectId = params.get('project');
+    }
+
+    if (params.has('description')) {
+      this.imageDescription = params.get('description');
+    }
+
+    if (params.has('series')) {
+      this.level = 'series';
+      this.resourceId = params.get('series');
+    } else if (params.has('instance')) {
+      this.level = 'instance';
+      this.resourceId = params.get('instance');
+
+      if (params.has('frame')) {
+        this.frameNumber = params.get('frame');
+      }
+    } else {
+      alert('Error - No series ID and no instance ID specified!');
+      return;
+    }
+
+    this.LoadPyramid();
+  },
+
+  methods: {
+
+    // -----------------------------------------------------------------------
+    // Persistence of layers and annotations
+    // -----------------------------------------------------------------------
+
+    CreatePostPayload: function(args) {
+      args['project'] = this.projectId;
+      args['level'] = this.level;
+      args['resource'] = this.resourceId;
+      args['frame'] = this.frameNumber;
+      return JSON.stringify(args);
+    },
+
+    LoadLayers: function(activeLayerId) {
+      var that = this;
+      axios.post('../api/list-user-layers',
+                 this.CreatePostPayload({}))
+        .then(function(response) {
+          that.userLayers = response.data['user-layers'];
+          that.importedLayers = response.data['imported-layers'];
+
+          if (that.userLayers.length == 0) {
+            that.CreateUserLayer();
+          } else if (activeLayerId !== undefined) {
+            that.activeUserLayerId = activeLayerId;
+          } else {
+            that.activeUserLayerId = that.userLayers[0].id;
+          }
+
+          that.LoadUserFeatures();
+          that.ReloadImportedFeatures();
+        })
+        .catch(function() {
+          console.error('Cannot load the saved annotations');
+        });
+    },
+
+    CreateUserLayer: function() {
+      var that = this;
+      axios.post('../api/create-user-layer',
+                 this.CreatePostPayload({}))
+        .then(function(response) {
+          that.LoadLayers(response.data.id);
+        })
+        .catch(function() {
+          console.error('Cannot create a new user layer');
+        });
+    },
+
+    SaveUserLayer: function(layer) {
+      var that = this;
+      axios.post('../api/save-user-layer',
+                 this.CreatePostPayload({
+                   'layer': layer
+                 }))
+        .catch(function() {
+          console.error('Cannot save user layer');
+        });
+    },
+
+    LoadUserFeatures: function() {
+      console.assert(this.drawSource !== null);  // InitializeAnnotations() must have been invoked
+      console.assert(this.workspaceInfo.enabled !== undefined);  // LoadLayers() must have been invoked
+
+      if (!this.workspaceInfo.enabled) {
+        return;
+      }
+
+      this.showSpinner = true;
+
+      var that = this;
+      axios.post('../api/load-user-features',
+                 this.CreatePostPayload({}))
+        .then(function(response) {
+          that.drawSource.clear();
+
+          // We check that the original layer is still available (could have been some write error)
+          var availableLayerIds = [];
+          for (let i = 0; i < that.userLayers.length; i++) {
+            availableLayerIds.push(that.userLayers[i].id);
+          }
+
+          for (let i = 0; i < response.data.features.length; i++) {
+            var layerId = response.data.features[i]['layer-id'];
+
+            if (availableLayerIds.includes(layerId)) {
+              UnserializeFeatureOntoMap(that.drawSource, response.data.features[i]);
+            }
+          }
+
+          // Now that the user features are loaded, we can install the save callback
+          that.drawSource.on('addfeature', function (e) {
+            that.SaveUserFeatures();
+          });
+          that.drawSource.on('removefeature', function (e) {
+            that.SaveUserFeatures();
+          });
+        })
+        .catch(function() {
+          console.error('Cannot load user features');
+        })
+        .finally(function() {
+          that.showSpinner = false;
+        });
+    },
+
+    SaveUserFeatures: function()
+    {
+      var that = this;
+
+      function Execute()
+      {
+        var features = [];
+
+        that.drawSource.getFeatures().forEach(function (feature, index) {
+          var item = SerializeFeature(feature);
+
+          if (item !== null) {
+            item['layer-id'] = feature.get('layer-id');
+
+            var label = feature.get('label');
+            if (label !== undefined) {
+              item['label'] = label;
+            }
+
+            var date = feature.get('creation-datetime');
+            if (date !== undefined) {
+              item['creation-datetime'] = date.getTime();
+            }
+
+            features.push(item);
+          }
+        });
+
+        that.isPendingChange = false;
+        that.isSaving = true;
+        that.showSpinner = true;
+        window.addEventListener('beforeunload', BeforeUnloadHandler);
+
+        axios.post('../api/save-user-features',
+                   that.CreatePostPayload({
+                     'features': features
+                   }))
+          .then(function() {
+            // Success
+          })
+          .catch(function() {
+            console.error('Cannot save the annotations');
+          })
+          .finally(function() {
+            console.assert(that.isSaving === true);
+
+            if (that.isPendingChange) {
+              Execute();
+            } else {
+              that.isSaving = false;
+              that.showSpinner = false;
+              window.removeEventListener('beforeunload', BeforeUnloadHandler);
+            }
+          });
+      }
+
+      this.isPendingChange = true;
+
+      if (!this.isSaving) {
+        Execute();
+      }
+    },
+
+    DeleteUserLayer: function(id) {
+      this.pendingDelete = id;
+      this.modalDeleteUserLayer.show();
+    },
+
+    UserLayerDeleteConfirmed: function() {
+      var layerId = this.pendingDelete;   // The ID of the layer to be removed
+
+      this.modalDeleteUserLayer.hide();
+      var that = this;
+      axios.post('../api/delete-user-layer',
+                 this.CreatePostPayload({
+                   'layer-id': layerId
+                 })
+                )
+        .then(function(response) {
+          that.LoadLayers();
+
+          // Remove the features that were part of this layer
+          that.drawSource.getFeatures().forEach(function(feature) {
+            if (feature.get('layer-id') === layerId) {
+              that.drawSource.removeFeature(feature);
+            }
+          });
+        })
+        .catch(function(error) {
+          console.error('Cannot delete the layer');
+        });
+    },
+
+    TakeScreenshot: function() {
+      modernScreenshot.domToBlob(document.body, {
+        filter: function (element) {
+          if (element.classList === undefined) {
+            return true;
+          } else {
+            return (element.id !== 'toolbar-top' &&
+                    element.id !== 'toolbar-left' &&
+                    !element.classList.contains('tooltip') &&
+                    !element.classList.contains('ol-control'));  // "+", "-", and "rotate" buttons
+          }
+        }
+      })
+        .then(function (blob) {
+          navigator.clipboard.write([
+            new ClipboardItem({
+              'image/png': blob
+            })
+          ])
+            .then(function () {
+              alert('Screenshot copied to clipboard!');
+            })
+            .catch(function (error) {
+              alert('Could not copy screenshot\n\n(' + error + ')');
+            });
+        });
+    },
+
+    LoadAnnotationsInfo: function() {
+      var that = this;
+      axios.post('../api/workspace-info',
+                 this.CreatePostPayload({}))
+        .then(function(response) {
+          that.workspaceInfo = response.data;
+
+          if (that.workspaceInfo.enabled) {
+            that.LoadLayers();
+          }
+        })
+        .catch(function(error) {
+        });
+    },
+
+    // -----------------------------------------------------------------------
+    // Annotation selection panel
+    // -----------------------------------------------------------------------
+
+    AddReadOnlyProperty: function(label, value) {
+      this.annotationProperties.push({
+        type: 'readonly',
+        label: label,
+        value: value
+      });
+    },
+
+    AddReadOnlyTextAreaProperty: function(label, value) {
+      this.annotationProperties.push({
+        type: 'readonly-textarea',
+        label: label,
+        value: value
+      });
+    },
+
+    AddEditableProperty: function(label, value, featureProp) {
+      this.annotationProperties.push({
+        type: 'editable',
+        label: label,
+        value: value,
+        featureProp: featureProp
+      });
+    },
+
+    AddTextAreaProperty: function(label, value, featureProp) {
+      this.annotationProperties.push({
+        type: 'editable-textarea',
+        label: label,
+        value: value,
+        featureProp: featureProp
+      });
+    },
+
+    AddDropdownProperty: function(label, options, selectedValue, featureProp) {
+      this.annotationProperties.push({
+        type: 'dropdown',
+        label: label,
+        value: selectedValue,
+        options: options,
+        featureProp: featureProp
+      });
+    },
+
+    ClearAnnotationSelection: function(clearOlSelection) {
+      if (clearOlSelection === undefined) {
+        clearOlSelection = true;
+      }
+
+      if (clearOlSelection && this.selectAnnotation !== null) {
+        this.selectAnnotation.getFeatures().clear();
+      }
+
+      this.selectedFeature = null;
+      this.annotationProperties = [];
+    },
+
+    UpdateAnnotationProperty: function(prop) {
+      if (this.selectedFeature &&
+          prop.featureProp &&
+          this.drawSource !== null &&
+          this.drawSource.hasFeature(this.selectedFeature)) {
+        this.selectedFeature.set(prop.featureProp, prop.value);
+        this.SaveUserFeatures();
+      }
+    },
+
+    FocusAnnotation: function() {
+      if (this.selectedFeature) {
+        bootstrap.Offcanvas.getOrCreateInstance(document.getElementById('right-panel')).hide();
+        this.map.getView().fit(this.selectedFeature.getGeometry().getExtent(), { padding: [40, 40, 40, 40], duration: 300 });
+      }
+    },
+
+    // -----------------------------------------------------------------------
+    // Rotation controls (driven from the popover)
+    // -----------------------------------------------------------------------
+
+    ResetRotation: function() {
+      this.rotationDeg = 0;
+      this.SetMapRotation();
+    },
+
+    RotateBy: function(deg) {
+      this.rotationDeg = parseInt(this.rotationDeg) + deg;
+
+      while (this.rotationDeg > 180) {
+        this.rotationDeg -= 360;
+      }
+
+      while (this.rotationDeg < -180) {
+        this.rotationDeg += 360;
+      }
+
+      this.SetMapRotation();
+    },
+
+    SetMapRotation: function() {
+      this.map.getView().setRotation(this.rotationDeg / 180 * Math.PI);
+    },
+
+    // -----------------------------------------------------------------------
+    // Draw tool activation
+    // -----------------------------------------------------------------------
+
+    DeactivateAll: function() {
+      this.map.removeInteraction(this.drawLine);
+      this.map.removeInteraction(this.drawPoint);
+      this.map.removeInteraction(this.drawCircle);
+      this.map.removeInteraction(this.drawRectangle);
+      this.map.removeInteraction(this.drawClosedPolygon);
+      this.map.removeInteraction(this.drawFreehand);
+      this.map.removeInteraction(this.drawFreehandLine);
+      this.map.removeInteraction(this.drawArrow);
+      this.map.removeInteraction(this.moveFeature);
+      this.map.removeInteraction(this.modifyFeature);
+      this.map.removeInteraction(this.selectAnnotation);
+
+      this.activeDrawTool = null;
+      this.map.getViewport().style.cursor = '';
+      this.ClearAnnotationSelection(true);
+    },
+
+    ToggleSelectTool: function() {
+      var wasActive = this.activeDrawTool === 'select';
+      this.DeactivateAll();
+      if (!wasActive) {
+        this.map.addInteraction(this.selectAnnotation);
+        this.activeDrawTool = 'select';
+        this.map.getViewport().style.cursor = 'pointer';
+      }
+    },
+
+    ToggleDrawTool: function(toolName) {
+      var interactions = {
+        'line':           this.drawLine,
+        'point':          this.drawPoint,
+        'circle':         this.drawCircle,
+        'rectangle':      this.drawRectangle,
+        'closed-polygon': this.drawClosedPolygon,
+        'freehand':       this.drawFreehand,
+        'freehand-line':  this.drawFreehandLine,
+        'arrow':          this.drawArrow,
+        'move':           this.moveFeature,
+        'modify':         this.modifyFeature
+      };
+
+      var cursors = {
+        'freehand':      'crosshair',
+        'freehand-line': 'crosshair'
+      };
+
+      // Draw tools that keep selectAnnotation active to highlight the newly drawn feature
+      var drawTools = ['line', 'point', 'circle', 'rectangle', 'closed-polygon', 'freehand', 'freehand-line'];
+
+      var wasActive = this.activeDrawTool === toolName;
+      this.DeactivateAll();
+      if (!wasActive) {
+        if (toolName === 'modify') {
+          this.RefreshModifyFeatureCollection();
+        }
+        this.map.addInteraction(interactions[toolName]);
+        if (drawTools.indexOf(toolName) !== -1) {
+          this.map.addInteraction(this.selectAnnotation);  // kept active to show blue highlight
+        }
+        this.activeDrawTool = toolName;
+        var cursor = cursors[toolName];
+        if (cursor) {
+          this.map.getViewport().style.cursor = cursor;
+        }
+      }
+    },
+
+    RefreshModifyFeatureCollection: function() {
+      if (this.modifyFeatureCollection !== null &
+          this.drawSource !== null) {
+        var activeLayerId = this.activeUserLayerId;
+        this.modifyFeatureCollection.clear();
+        this.drawSource.getFeatures().forEach(function(feature) {
+          if (feature.get('layer-id') === activeLayerId) {
+            app.modifyFeatureCollection.push(feature);
+          }
+        });
+      }
+    },
+
+    DeleteSelectedAnnotation: function() {
+      var selected = this.selectAnnotation.getFeatures();
+      if (selected.getLength() > 0 &&
+          this.drawSource !== null &&
+          this.drawSource.hasFeature(selected.item(0))) {
+        this.modalDeleteAnnotation.show();
+      }
+    },
+
+    ConfirmDeleteAnnotation: function() {
+      var selected = this.selectAnnotation.getFeatures();
+
+      var that = this;
+      selected.forEach(function(feature) {
+        that.drawSource.removeFeature(feature);
+      });
+
+      selected.clear();
+      this.ClearAnnotationSelection(false);
+      this.modalDeleteAnnotation.hide();
+    },
+
+    // -----------------------------------------------------------------------
+    // Panel animation
+    // -----------------------------------------------------------------------
+
+    InitializePanelAnimation: function() {
+      var that = this;
+      var panel = document.getElementById('right-panel');
+      var toggle = document.getElementById('right-panel-toggle');
+      var isResizing = false;
+
+      // Ensure the initial chevron direction matches the real offcanvas state.
+      that.panelOpen = panel.classList.contains('show');
+
+      function ResizingLoop() {
+        // If the offcanvas is hidden (e.g. before workspace load), its left edge can be 0,
+        // which would incorrectly move the toggle off-screen. Clamp to [0, panelWidth].
+        var panelRect = panel.getBoundingClientRect();
+        var panelWidth = Math.max(0, panelRect.width || 0);
+        var right = window.innerWidth - panelRect.left;
+
+        if (!isFinite(right)) {
+          right = 0;
+        }
+
+        right = Math.max(0, Math.min(right, panelWidth));
+        toggle.style.right = right + 'px';
+
+        if (isResizing) {
+          requestAnimationFrame(ResizingLoop);
+        }
+      }
+
+      function StartResizing() {
+        isResizing = true;
+        requestAnimationFrame(ResizingLoop);
+      }
+
+      function StopResizing() {
+        isResizing = false;
+        ResizingLoop();
+      }
+
+      ResizingLoop();
+
+      panel.addEventListener('hide.bs.offcanvas', function() {
+        that.panelOpen = false;
+        StartResizing();
+      });
+      panel.addEventListener('hidden.bs.offcanvas', StopResizing);
+
+      panel.addEventListener('show.bs.offcanvas', function() {
+        that.panelOpen = true;
+        StartResizing();
+      });
+      panel.addEventListener('shown.bs.offcanvas', StopResizing);
+
+      // Keep the toggle aligned if viewport size changes.
+      window.addEventListener('resize', StopResizing);
+    },
+
+    // -----------------------------------------------------------------------
+    // Pyramid loading and map initialization
+    // -----------------------------------------------------------------------
+
+    LoadPyramid: function() {
+      var that = this;
+
+      if (this.level == 'series')
+      {
+        axios.get('../pyramids/' + this.resourceId)
+          .then(function(response) {
+            that.InitializePyramid(response.data, '../tiles/' + that.resourceId + '/');
+          })
+          .catch(function(error) {
+            alert('Error - Cannot get the pyramid structure of series: ' + that.resourceId);
+          });
+      }
+      else if (this.level == 'instance')
+      {
+        axios.get('../frames-pyramids/' + this.resourceId + '/' + this.frameNumber)
+          .then(function(response) {
+            that.InitializePyramid(response.data, '../frames-tiles/' + that.resourceId + '/' + that.frameNumber + '/');
+          })
+          .catch(function(error) {
+            alert('Error - Cannot get the pyramid structure of frame ' + that.frameNumber + ' of instance: ' + that.resourceId);
+          });
+      }
+    },
+
+    InitializePyramid: function(pyramid, tilesBaseUrl) {
+      this.mapBackground = pyramid['BackgroundColor'];  // New in WSI 2.1
+
+      var width = pyramid['TotalWidth'];
+      var height = pyramid['TotalHeight'];
+      var countLevels = pyramid['Resolutions'].length;
+
+      var metersPerUnit = null;
+      var imagedVolumeWidth = pyramid['ImagedVolumeWidth'];  // In millimeters
+      var imagedVolumeHeight = pyramid['ImagedVolumeHeight'];
+      if (imagedVolumeWidth !== undefined &&
+          imagedVolumeHeight !== undefined) {
+        var metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(width));
+        var metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(height));
+        if (IsNear(metersPerUnitX / metersPerUnitY, 1)) {
+          metersPerUnit = metersPerUnitX;
+        } else {
+          // Backward compatibility with OrthancWSIDicomizer <= 3.2, where X/Y were swapped
+          metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(height));
+          metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(width));
+          if (IsNear(metersPerUnitX / metersPerUnitY, 1)) {
+            metersPerUnit = metersPerUnitX;
+          } else {
+            console.error('Anisotropic pixel spacing (may result from an inconsistency ' +
+                          'in the imaged volume size), not showing the scale');
+          }
+        }
+      }
+
+      if (metersPerUnit) {
+        this.showMagnificationButtons = true;
+      }
+
+      // Maps always need a projection, but Zoomify layers are not geo-referenced, and
+      // are only measured in pixels.  So, we create a fake projection that the map
+      // can use to properly display the layer.
+      var proj = new ol.proj.Projection({
+        code: 'pixel',
+        units: 'pixel',
+        metersPerUnit: metersPerUnit,
+        extent: [0, 0, width, height]
+      });
+
+      var extent = [0, -height, width, 0];
+
+      var rotateControl = new ol.control.Rotate({
+        target: 'toolbar-left',
+        autoHide: false,  // Show the button even if rotation is 0
+        resetNorth: function() {  // Disable the default action
+        }
+      });
+
+      new bootstrap.Popover(rotateControl.element, {
+        placement: 'right',
+        container: 'body',
+        html: true,
+        content: document.getElementById('popover-rotate')
+      });
+
+      new bootstrap.Popover(document.getElementById('button-adjustments'), {
+        placement: 'right',
+        container: 'body',
+        html: true,
+        content: document.getElementById('popover-adjustments')
+      });
+
+      // Disable the rotation of the map, and inertia while panning
+      // http://stackoverflow.com/a/25682186
+      var interactions = ol.interaction.defaults.defaults({
+        //pinchRotate : false,
+        dragPan: false  // disable kinetics
+        //shiftDragZoom: false  // disable zoom box
+      }).extend([
+        new ol.interaction.DragPan(),
+        new ol.interaction.DragRotate({
+          //condition: ol.events.condition.shiftKeyOnly  // Rotate only when Shift key is pressed
+        })
+      ]);
+
+      var controls = ol.control.defaults.defaults({
+        attribution: false,
+        rotate: false        // remove the default rotate
+      }).extend([
+        rotateControl,
+
+        /*new ol.control.ScaleLine({
+          minWidth: 100
+          })*/
+        new MicroscopeScaleLine({
+          minWidth: 100,
+          referenceMagnification: this.referenceMagnification
+        })
+
+      ]);
+
+      if (this.imageDescription !== null) {
+        controls.extend([
+          new ol.control.Attribution({
+            attributions: this.imageDescription,
+            collapsible: false
+          })
+        ]);
+      }
+
+      var tileLayer = new ol.layer.Tile({
+        extent: extent,
+        source: new ol.source.TileImage({
+          projection: proj,
+          tileUrlFunction: function(tileCoord, pixelRatio, projection) {
+            return (tilesBaseUrl + (countLevels - 1 - tileCoord[0]) + '/' + tileCoord[1] + '/' + tileCoord[2]);
+          },
+          tileGrid: new ol.tilegrid.TileGrid({
+            extent: extent,
+            resolutions: pyramid['Resolutions'].reverse(),
+            tileSizes: pyramid['TilesSizes'].reverse()
+          })
+        }),
+        wrapX: false,
+        projection: proj
+      });
+
+      var that = this;
+      tileLayer.on('prerender', (event) => {
+        const context = event.context;
+
+        if (context) {
+          context.save();
+          var brightness = Math.pow(4, that.brightness / 100.0);  // Ranges between 0.25 and 4
+          var contrast = Math.pow(4, that.contrast / 100.0);      // Ranges between 0.25 and 4
+          var saturation = Math.pow(4, that.saturation / 100.0);  // Ranges between 0.25 and 4
+          context.filter =
+            'brightness(' + brightness.toFixed(4) + ') ' +
+            'contrast(' + contrast.toFixed(4) + ')' +
+            'saturate(' + saturation.toFixed(4) + ')';
+          // 'hue-rotate(' + that.hue + 'deg)';
+        }
+      });
+
+      tileLayer.on('postrender', (event) => {
+        const context = event.context;
+
+        if (context) {
+          context.restore();
+        }
+      });
+
+      this.map = new ol.Map({
+        target: 'map',
+        layers: [ tileLayer ],
+        view: new ol.View({
+          projection: proj,
+          center: [width / 2, -height / 2],
+          zoom: 0,
+          minResolution: 0.1   // "1" means "do not interpelate over pixels"
+        }),
+        interactions: interactions,
+        controls: controls
+      });
+
+      // Prevent toolbar pointer events from reaching OL interactions (e.g. Select)
+      [ 'toolbar-left', 'toolbar-top' ].forEach(function(id) {
+        var el = document.getElementById(id);
+        [ 'pointerdown', 'pointerup', 'pointermove', 'click' ].forEach(function(type) {
+          el.addEventListener(type, function(e) {
+            e.stopPropagation();
+          });
+        });
+      });
+
+      // Re-append toolbars inside the map viewport so they inherit OL's scaling
+      var viewport = this.map.getViewport();
+      viewport.appendChild(document.getElementById('toolbar-left'));
+      viewport.appendChild(document.getElementById('toolbar-top'));
+
+      this.map.once('postrender', function() {
+        // Match Bootstrap button size to OL button size
+        /*var olBtnSize = document.querySelector('.ol-zoom button').offsetWidth + 'px';
+          document.querySelectorAll('.icon-btn').forEach(function(el) {
+          el.style.width = olBtnSize;
+          el.style.height = olBtnSize;
+          });*/
+
+        // Move the top toolbar directly right to the zoom control, regardless of scaling
+        var zoomEl = document.querySelector('.ol-zoom');
+        document.getElementById('toolbar-top').style.left = (zoomEl.offsetLeft + zoomEl.offsetWidth) + 'px';
+        document.getElementById('toolbar-top').style.top = zoomEl.offsetTop + 'px';
+
+        // Move the left toolbar directly below the zoom control, regardless of scaling
+        document.getElementById('toolbar-left').style.left = zoomEl.offsetLeft + 'px';
+        document.getElementById('toolbar-left').style.top = (zoomEl.offsetTop + zoomEl.offsetHeight) + 'px';
+
+        // Move the vertical buttons below the rotate control, regardless of scaling
+        var rotateEl = document.querySelector('.ol-rotate');
+        document.getElementById('toolbar-left-content').style.top = (rotateEl.offsetTop + rotateEl.offsetHeight) + 'px';
+      });
+
+      this.map.getView().fit(extent, this.map.getSize());
+
+      this.toolbarsVisible = true;
+      this.InitializeAnnotations();
+    },
+
+    ResetAdjustments: function() {
+      this.brightness = 0;
+      this.contrast = 0;
+      this.saturation = 0;
+      this.map.render();
+    },
+
+    // -----------------------------------------------------------------------
+    // Drawing annotations
+    // -----------------------------------------------------------------------
+
+    InitializeAnnotations: function() {
+      function GetLayerById(id) {
+        for (var i = 0; i < app.userLayers.length; i++) {
+          if (app.userLayers[i].id == id) {
+            return app.userLayers[i];
+          }
+        }
+        return null;
+      }
+
+      function GetImportedLayerById(id) {
+        for (var i = 0; i < app.importedLayers.length; i++) {
+          if (app.importedLayers[i].id == id) {
+            return app.importedLayers[i];
+          }
+        }
+        return null;
+      }
+
+      function GetLayerOfFeature(feature) {
+        var layerId = feature.get('layer-id');
+        console.assert(layerId !== null);
+        var layer = GetLayerById(layerId);
+        console.assert(layer !== null);
+        return layer;
+      }
+
+      function IsFeatureVisible(feature) {
+        var layerId = feature.get('layer-id');
+        var userLayer = GetLayerById(layerId);
+        if (userLayer !== null) {
+          return userLayer.visible;
+        }
+
+        var importedLayer = GetImportedLayerById(layerId);
+        return importedLayer !== null && importedLayer.visible;
+      }
+
+      // Single vector source holding all features from all layers
+      this.drawSource = new ol.source.Vector();
+
+      this.drawLayer = new ol.layer.Vector({
+        source: this.drawSource,
+        style: function(feature, resolution) {
+          var layer = GetLayerOfFeature(feature);
+          if (layer.visible) {
+            return CreateFeatureStyle(feature, resolution, layer.color);
+          } else {
+            return null;
+          }
+        }
+      });
+
+      this.map.addLayer(this.drawLayer);
+
+      // Shared annotations: a separate read-only source for all imported layers
+      this.drawImportedSource = new ol.source.Vector();
+      this.drawImportedLayer = new ol.layer.Vector({
+        source: this.drawImportedSource,
+        style: function(feature, resolution) {
+          var entry = GetImportedLayerById(feature.get('layer-id'));
+          if (!entry || !entry.visible) {
+            return null;
+          }
+
+          return CreateFeatureStyle(feature, resolution, entry.color);
+        }
+      });
+      this.map.addLayer(this.drawImportedLayer);
+
+      // Draw interactions (inactive until toggled)
+      this.drawLine = new ol.interaction.Draw({
+        source: this.drawSource,
+        type: 'LineString',
+        maxPoints: 2
+      });
+      this.drawPoint = new ol.interaction.Draw({ source: this.drawSource, type: 'Point' });
+      this.drawCircle = new ol.interaction.Draw({ source: this.drawSource, type: 'Circle' });
+      this.drawRectangle = new ol.interaction.Draw({
+        source: this.drawSource,
+        type: 'Circle',
+        geometryFunction: ol.interaction.Draw.createBox()
+      });
+      this.drawClosedPolygon = new ol.interaction.Draw({ source: this.drawSource, type: 'Polygon' });
+      this.drawFreehand = new ol.interaction.Draw({ source: this.drawSource, type: 'Polygon', freehand: true });
+      this.drawFreehandLine = new ol.interaction.Draw({ source: this.drawSource, type: 'LineString', freehand: true });
+      this.drawArrow = new ol.interaction.Draw({
+        source: this.drawSource,
+        type: 'LineString',
+        maxPoints: 2
+      });
+
+      this.moveFeature = new ol.interaction.Translate({
+        source: this.drawSource,
+        filter: function(feature) {
+          return feature.get('layer-id') === app.activeUserLayerId;
+        }
+      });
+      this.modifyFeatureCollection = new ol.Collection();
+      this.modifyFeature = new ol.interaction.Modify({ features: this.modifyFeatureCollection });
+
+      // Select interaction (inactive until toggled).
+      // The condition restricts user-click selection to the dedicated select tool only,
+      // preventing spurious selection events when starting a draw near an existing feature.
+      this.selectAnnotation = new ol.interaction.Select({
+        layers: [ this.drawLayer, this.drawImportedLayer ],
+        filter: function(feature) {
+          return IsFeatureVisible(feature);
+        },
+        condition: function(e) {
+          return ol.events.condition.singleClick(e) && app.activeDrawTool === 'select';
+        },
+        hitTolerance: 5,  /* pixels around the feature that count as a hit */
+        style: function(feature, resolution) {
+          return CreateFeatureStyle(feature, resolution, '#0000ff');  /* selected annotations are in blue */
+        }
+      });
+
+
+      var that = this;
+
+      function preventDoubleClickZoom() {
+        that.map.getInteractions().forEach(function(interaction) {
+          if (interaction instanceof ol.interaction.DoubleClickZoom) {
+            interaction.setActive(false);
+            setTimeout(function() { interaction.setActive(true); }, 50);
+          }
+        });
+      }
+
+      function onDrawEnd(e, callPreventDoubleClickZoom) {
+        e.feature.set('layer-id', app.activeUserLayerId);
+        e.feature.set('creation-datetime', new Date());
+        if (callPreventDoubleClickZoom) {
+          preventDoubleClickZoom();
+        }
+        that.selectAnnotation.getFeatures().clear();
+        that.selectAnnotation.getFeatures().push(e.feature);
+        that.selectAnnotation.dispatchEvent({ type: 'select', selected: [e.feature], deselected: [] });
+      }
+
+      this.drawLine.on('drawend', function(e) { onDrawEnd(e, true); });
+      this.drawPoint.on('drawend', function(e) { onDrawEnd(e, true); });
+      this.drawCircle.on('drawend', function(e) { onDrawEnd(e, true); });
+      this.drawRectangle.on('drawend', function(e) { onDrawEnd(e, true); });
+      this.drawClosedPolygon.on('drawend', function(e) { onDrawEnd(e, true); });
+      this.drawFreehand.on('drawend', function(e) { onDrawEnd(e, false); });
+      this.drawFreehandLine.on('drawend', function(e) { onDrawEnd(e, false); });
+      this.drawArrow.on('drawend', function(e) {
+        e.feature.set('type', 'arrow');
+        onDrawEnd(e, false);
+      });
+
+      this.moveFeature.on('translateend', function(e) { that.SaveUserFeatures(); });
+      this.modifyFeature.on('modifyend', function(e) { that.SaveUserFeatures(); });
+
+      this.selectAnnotation.on('select', function(e) {
+        if (e.selected.length === 1) {
+          that.annotationProperties = [];
+
+          var feature = e.selected[0];
+          that.selectedFeature = feature;
+
+          var date = feature.get('creation-datetime');
+          if (date === undefined) {
+            that.AddReadOnlyProperty('Date', '');
+          } else {
+            that.AddReadOnlyProperty('Date', date.toLocaleString());
+          }
+
+          var geometry = feature.getGeometry();
+
+          if (geometry.getType() === 'LineString') {
+            // Line, freehand
+            that.AddReadOnlyProperty('Length', FormatLength(geometry.getLength(), that.map.getView().getProjection()));
+          } else if (geometry.getType() === 'Circle') {
+            // geometry.getArea() is not available on circles
+            var radius = geometry.getRadius();
+            var area = Math.PI * radius * radius;
+            that.AddReadOnlyProperty('Area', FormatArea(area, that.map.getView().getProjection()));
+          } else if (geometry.getType() === 'Polygon') {
+            // Rectangle, closed polygon, freehand polygon
+            that.AddReadOnlyProperty('Area', FormatArea(geometry.getArea(), that.map.getView().getProjection()));
+          }
+
+          if (that.drawSource !== null &&
+              that.drawSource.hasFeature(feature)) {
+            that.AddTextAreaProperty('Label', feature.get('label') || '', 'label');
+          } else {
+            that.AddReadOnlyTextAreaProperty('Label', feature.get('label') || '');
+          }
+
+          /*
+          // TODO
+          that.AddDropdownProperty('Category', [
+          { value: 'tumor',    label: 'Tumor' },
+          { value: 'stroma',   label: 'Stroma' },
+          { value: 'necrosis', label: 'Necrosis' }
+          ], feature.get('category') || '', 'category');
+          */
+
+          bootstrap.Offcanvas.getOrCreateInstance(document.getElementById('right-panel')).show();
+        } else {
+          that.ClearAnnotationSelection(false);
+        }
+      });
+
+      this.LoadAnnotationsInfo();
+    },
+
+    // -----------------------------------------------------------------------
+    // Imported layers
+    // -----------------------------------------------------------------------
+
+    ShowShareUserLayerModal: function(layer) {
+      this.shareLayerTarget = layer;
+      this.shareLayerPublic = layer.public;
+      this.shareLayerUsers = layer.shared_with;
+      this.shareLayerSearchQuery = '';
+      this.shareLayerSearchResults = [];
+      this.modalShareUserLayer.show();
+    },
+
+    ShareLayerIsUserSelected: function(user) {
+      for (var i = 0; i < this.shareLayerUsers.length; i++) {
+        if (this.shareLayerUsers[i].type == user.type &&
+            this.shareLayerUsers[i].name == user.name) {
+          return true;
+        }
+      }
+
+      return false;
+    },
+
+    ShareLayerAddUser: function(user) {
+      if (!this.ShareLayerIsUserSelected(user)) {
+        this.shareLayerUsers.push(user);
+      }
+
+      this.shareLayerSearchQuery = '';
+      this.shareLayerSearchResults = [];
+    },
+
+    ShareLayerAddStandardUser: function(name) {
+      var that = this;
+      axios.post('../api/create-standard-user',
+                 this.CreatePostPayload({ 'name': name }))
+        .then(function(response) {
+          that.ShareLayerAddUser(response.data);
+        })
+        .catch(function() {
+          console.error('Cannot create standard user');
+        });
+
+      this.shareLayerSearchQuery = '';
+      this.shareLayerSearchResults = [];
+    },
+
+    ShareLayerSearchUsers: function() {
+      var query = this.shareLayerSearchQuery.trim();
+      if (!query) {
+        this.shareLayerSearchResults = [];
+      } else {
+        var that = this;
+        axios.post('../api/search-active-users',
+                   this.CreatePostPayload({ 'query': query }))
+          .then(function(response) {
+            that.shareLayerSearchResults = [];
+            for (var i = 0; i < response.data.length; i++) {
+              if (!that.ShareLayerIsUserSelected(response.data[i])) {
+                that.shareLayerSearchResults.push(response.data[i]);
+              }
+            }
+          })
+          .catch(function() {
+            that.shareLayerSearchResults = [];
+          });
+      }
+    },
+
+    ShareLayerSave: function() {
+      this.shareLayerTarget.public = this.shareLayerPublic;
+      this.shareLayerTarget.shared_with = this.shareLayerUsers;
+      this.SaveUserLayer(this.shareLayerTarget);
+      this.modalShareUserLayer.hide();
+    },
+
+
+
+    ShowImportLayerModal: function() {
+      this.importUserSearchQuery = '';
+      this.importUserSearchResults = [];
+      this.importSelectedUser = '';
+      this.importSelectedLayer = '';
+      this.importAvailableUsers = [];
+      this.importAvailableLayers = [];
+      this.modalImportLayer.show();
+
+      var that = this;
+      axios.post('../api/list-sharing-users',
+                 this.CreatePostPayload({}))
+        .then(function(response) {
+          that.importAvailableUsers = response.data;
+        })
+        .catch(function() {
+          console.error('Cannot load users sharing layers');
+        });
+    },
+
+    ImportUserSearchChanged: function() {
+      this.importSelectedUser = '';
+      this.importSelectedLayer = '';
+      this.importAvailableLayers = [];
+      this.importUserSearchResults = [];
+
+      var query = this.importUserSearchQuery.trim().toLowerCase();
+
+      if (query !== '') {
+        for (var i = 0; i < this.importAvailableUsers.length; i++) {
+          if (this.importAvailableUsers[i].name.toLowerCase().indexOf(query) !== -1) {
+            this.importUserSearchResults.push(this.importAvailableUsers[i]);
+          }
+        }
+      }
+    },
+
+    ImportUserSelect: function(userId) {
+      this.importUserSearchQuery = '';
+      this.importUserSearchResults = [];
+      this.importSelectedUser = userId;
+      this.ImportUserChanged();
+    },
+
+    ImportUserChanged: function() {
+      this.importSelectedLayer = '';
+      this.importAvailableLayers = [];
+
+      var that = this;
+      axios.post('../api/list-shared-layers',
+                 this.CreatePostPayload({ 'author': this.importSelectedUser }))
+        .then(function(response) {
+          that.importAvailableLayers = response.data;
+        })
+        .catch(function() {
+          console.error('Cannot load imported layers');
+        });
+    },
+
+    ImportLayerConfirmed: function() {
+      this.modalImportLayer.hide();
+      var userId = this.importSelectedUser;
+      var layerId = this.importSelectedLayer;
+
+      var that = this;
+      axios.post('../api/import-layer',
+                 this.CreatePostPayload({
+                   'author': userId,
+                   'layer': layerId
+                 }))
+        .then(function(response) {
+          that.LoadLayers();
+        })
+        .catch(function() {
+          console.error('Cannot import layer');
+        });
+    },
+
+
+    DeleteImportedLayer: function(layerId) {
+      this.pendingDelete = layerId;
+      this.modalDeleteImportedLayer.show();
+    },
+
+    ImportedLayerDeleteConfirmed: function() {
+      this.modalDeleteImportedLayer.hide();
+      var layerId = this.pendingDelete;
+
+      var that = this;
+      axios.post('../api/remove-imported-layer',
+                 this.CreatePostPayload({
+                   'layer': layerId
+                 }))
+        .then(function(response) {
+          that.LoadLayers();
+        })
+        .catch(function() {
+          console.error('Cannot remove imported layer');
+        });
+    },
+
+    SaveImportedLayer: function(layer) {
+      var that = this;
+      axios.post('../api/save-imported-layer',
+                 this.CreatePostPayload({
+                   'layer': layer
+                 }))
+        .catch(function() {
+          console.error('Cannot save imported layer');
+        });
+    },
+
+
+    ReloadImportedFeatures: function() {
+      // Reset current selection when refreshing imported content.
+      this.ClearAnnotationSelection(true);
+
+      console.assert(this.drawImportedSource !== null);  // InitializeAnnotations() must have been invoked
+      console.assert(this.workspaceInfo.enabled !== undefined);  // LoadLayers() must have been invoked
+
+      if (!this.workspaceInfo.sharing) {
+        return;
+      }
+
+      var that = this;
+      axios.post('../api/load-imported-features',
+                 this.CreatePostPayload({}))
+        .then(function(response) {
+          that.drawImportedSource.clear();
+
+          for (let i = 0; i < response.data.features.length; i++) {
+            UnserializeFeatureOntoMap(that.drawImportedSource, response.data.features[i]);
+          }
+        })
+        .catch(function() {
+          console.error('Cannot load imported features');
+        });
+    }
+  }
+});
+
+
 function IsNear(a, b)
 {
   return Math.abs(a - b) <= 0.01;
 }
 
 
-function InitializePyramid(pyramid, tilesBaseUrl)
+function FormatUnit(value, units)
 {
-  $('#map').css('background', pyramid['BackgroundColor']);  // New in WSI 2.1
-
-  var width = pyramid['TotalWidth'];
-  var height = pyramid['TotalHeight'];
-  var countLevels = pyramid['Resolutions'].length;
+  // Order by unit size ascending (factor descending)
+  for (var i = 0; i < units.length - 1; i++) {
+    var nextScaled = value * units[i + 1].factor;
 
-  var metersPerUnit = null;
-  var imagedVolumeWidth = pyramid['ImagedVolumeWidth'];  // In millimeters
-  var imagedVolumeHeight = pyramid['ImagedVolumeHeight'];
-  if (imagedVolumeWidth !== undefined &&
-      imagedVolumeHeight !== undefined) {
-    var metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(width));
-    var metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(height));
-    if (IsNear(metersPerUnitX / metersPerUnitY, 1)) {
-      metersPerUnit = metersPerUnitX;
-    } else {
-      // Backward compatibility with OrthancWSIDicomizer <= 3.2, where X/Y were swapped
-      metersPerUnitX = parseFloat(imagedVolumeWidth) / (1000.0 * parseFloat(height));
-      metersPerUnitY = parseFloat(imagedVolumeHeight) / (1000.0 * parseFloat(width));
-      if (IsNear(metersPerUnitX / metersPerUnitY, 1)) {
-        metersPerUnit = metersPerUnitX;
-      } else {
-        console.error('Anisotropic pixel spacing (may result from an inconsistency ' +
-                      'in the imaged volume size), not showing the scale');
-      }
+    // Stop when the next larger unit would produce a value below 1
+    if (Math.abs(nextScaled) < 1) {
+      var scaled = value * units[i].factor;
+      return scaled.toFixed(2) + ' ' + units[i].label;
     }
   }
 
-  // Maps always need a projection, but Zoomify layers are not geo-referenced, and
-  // are only measured in pixels.  So, we create a fake projection that the map
-  // can use to properly display the layer.
-  var proj = new ol.proj.Projection({
-    code: 'pixel',
-    units: 'pixel',
-    metersPerUnit: metersPerUnit,
-    extent: [0, 0, width, height]
-  });
+  // Use the largest unit available
+  var largest = units[units.length - 1];
+  var scaled = value * largest.factor;
+  return scaled.toFixed(2) + ' ' + largest.label;
+}
 
-  var extent = [0, -height, width, 0];
+
+function FormatLength(lengthPx, projection)
+{
+  var metersPerUnit = projection.getMetersPerUnit();
+  if (metersPerUnit) {
+    var meters = lengthPx * metersPerUnit;
 
-  var rotateControl = new ol.control.Rotate({
-    autoHide: false,  // Show the button even if rotation is 0
-    resetNorth: function() {  // Disable the default action
-    }
-  });
+    return FormatUnit(meters, [
+      { label: 'μm', factor: 1e6 },
+      { label: 'mm', factor: 1e3 },
+      { label: 'cm', factor: 1e2 },
+      { label: 'm',  factor: 1 },
+      { label: 'km', factor: 1e-3 }
+    ]);
 
-  new bootstrap.Popover(rotateControl.element, {
-    placement: 'right',
-    container: 'body',
-    html: true,
-    content: $('#popover-content')
-  });
+  } else {
+    return lengthPx.toFixed(0) + ' px';
+  }
+}
+
 
-  // Disable the rotation of the map, and inertia while panning
-  // http://stackoverflow.com/a/25682186
-  var interactions = ol.interaction.defaults.defaults({
-    //pinchRotate : false,
-    dragPan: false  // disable kinetics
-    //shiftDragZoom: false  // disable zoom box
-  }).extend([
-    new ol.interaction.DragPan(),
-    new ol.interaction.DragRotate({
-      //condition: ol.events.condition.shiftKeyOnly  // Rotate only when Shift key is pressed
-    })
-  ]);
+function FormatArea(areaPx, projection)
+{
+  var metersPerUnit = projection.getMetersPerUnit();
+  if (metersPerUnit) {
+    var sqMeters = areaPx * metersPerUnit * metersPerUnit;
+
+    return FormatUnit(sqMeters, [
+      { label: 'μm²', factor: 1e12 },
+      { label: 'mm²', factor: 1e6 },
+      { label: 'cm²', factor: 1e4 },
+      { label: 'm²',  factor: 1 },
+      { label: 'km²', factor: 1e-6 }
+    ]);
 
-  var controls = ol.control.defaults.defaults({
-    attribution: false
-  }).extend([
-    rotateControl,
-    new ol.control.ScaleLine({
-      minWidth: 100
-    })
-  ]);
+  } else {
+    return areaPx.toFixed(0) + ' px²';
+  }
+}
+
 
-  const params = new URLSearchParams(document.location.search);
-  if (params.has('description')) {
-    controls.extend([
-      new ol.control.Attribution({
-        attributions: params.get('description'),
-        collapsible: false
-      })
-    ]);
+function CreateLayerStyle(color)
+{
+  function HexToRGBA(hex, alpha)
+  {
+    var r = parseInt(hex.slice(1, 3), 16);
+    var g = parseInt(hex.slice(3, 5), 16);
+    var b = parseInt(hex.slice(5, 7), 16);
+    return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
   }
 
-
-  var layer = new ol.layer.Tile({
-    extent: extent,
-    source: new ol.source.TileImage({
-      projection: proj,
-      tileUrlFunction: function(tileCoord, pixelRatio, projection) {
-        return (tilesBaseUrl + (countLevels - 1 - tileCoord[0]) + '/' + tileCoord[1] + '/' + tileCoord[2]);
-      },
-      tileGrid: new ol.tilegrid.TileGrid({
-        extent: extent,
-        resolutions: pyramid['Resolutions'].reverse(),
-        tileSizes: pyramid['TilesSizes'].reverse()
-      })
-    }),
-    wrapX: false,
-    projection: proj
-  });
-
-
-  var map = new ol.Map({
-    target: 'map',
-    layers: [ layer ],
-    view: new ol.View({
-      projection: proj,
-      center: [width / 2, -height / 2],
-      zoom: 0,
-      minResolution: 0.1   // "1" means "do not interpelate over pixels"
-    }),
-    interactions: interactions,
-    controls: controls
-  });
-
-  map.getView().fit(extent, map.getSize());
-
-
-  $('#rotation-slider').on('input change', function() {
-    map.getView().setRotation(this.value / 180 * Math.PI);
-  });
-
-  $('#rotation-reset').click(function() {
-    $('#rotation-slider').val(0).change();
-  });
-
-  $('#rotation-minus90').click(function() {
-    var angle = parseInt($('#rotation-slider').val()) - 90;
-    if (angle < -180) {
-      angle += 360;
-    }
-    $('#rotation-slider').val(angle).change();
-  });
-
-  $('#rotation-plus90').click(function() {
-    var angle = parseInt($('#rotation-slider').val()) + 90;
-    if (angle > 180) {
-      angle -= 360;
-    }
-    $('#rotation-slider').val(angle).change();
+  return new ol.style.Style({
+    stroke: new ol.style.Stroke({ color: color, width: 2 }),
+    fill: new ol.style.Fill({ color: HexToRGBA(color, 0.2) }),
+    // The "image" style is used by point annotations
+    image: new ol.style.Circle({
+      radius: 5,
+      fill: new ol.style.Fill({ color: color })
+    })
   });
 }
 
 
-$(document).ready(function() {
-  const params = new URLSearchParams(document.location.search);
+function CreateArrowStyle(feature, resolution, color)
+{
+  const coordinates = feature.getGeometry().getCoordinates();
+
+  if (coordinates.length == 2) {
+    const start = coordinates[0];
+    const end = coordinates[1];
+
+    const angle = Math.atan2(
+      end[1] - start[1],
+      end[0] - start[0]
+    );
+
+    const headLength = 10 * resolution;
+    const headAngle = Math.PI / 6;
+
+    const p1 = [
+      end[0] - headLength * Math.cos(angle - headAngle),
+      end[1] - headLength * Math.sin(angle - headAngle)
+    ];
+
+    const p2 = [
+      end[0] - headLength * Math.cos(angle + headAngle),
+      end[1] - headLength * Math.sin(angle + headAngle)
+    ];
+
+    return [
+      // shaft
+      new ol.style.Style({
+        stroke: new ol.style.Stroke({
+          color: color,
+          width: 2
+        })
+      }),
 
-  if (params.has('series')) {
-    var seriesId = params.get('series');
-    $.ajax({
-      url : '../pyramids/' + seriesId,
-      error: function() {
-        alert('Error - Cannot get the pyramid structure of series: ' + seriesId);
-      },
-      success : function(pyramid) {
-        InitializePyramid(pyramid, '../tiles/' + seriesId + '/');
-      }
-    });
-  } else if (params.has('instance')) {
-    var frameNumber = 0;
-    if (params.has('frame')) {
-      frameNumber = params.get('frame');
+      // arrow head
+      new ol.style.Style({
+        geometry: new ol.geom.LineString([
+          p1, end, p2
+        ]),
+        stroke: new ol.style.Stroke({
+          color: color,
+          width: 2
+        })
+      })
+    ];
+  }
+}
+
+
+function IsArrowFeature(feature)
+{
+  return feature.get('type') === 'arrow';
+}
+
+
+function CreateFeatureStyle(feature, resolution, color)
+{
+  if (IsArrowFeature(feature)) {
+    return CreateArrowStyle(feature, resolution, color);
+  } else {
+    return CreateLayerStyle(color);
+  }
+}
+
+
+function SerializeFeature(feature)
+{
+  var type = feature.getGeometry().getType();
+
+  if (type === 'LineString') {
+    var s;
+    if (IsArrowFeature(feature)) {
+      s = 'arrow';
+    } else {
+      s = 'polyline';
     }
 
-    var instanceId = params.get('instance');
-    $.ajax({
-      url : '../frames-pyramids/' + instanceId + '/' + frameNumber,
-      error: function() {
-        alert('Error - Cannot get the pyramid structure of frame ' + frameNumber + ' of instance: ' + instanceId);
-      },
-      success : function(pyramid) {
-        InitializePyramid(pyramid, '../frames-tiles/' + instanceId + '/' + frameNumber + '/');
+    return {
+      'type' : s,
+      'coordinates' : feature.getGeometry().getCoordinates()
+    };
+  } else if (type === 'Point') {
+    return {
+      'type' : 'point',
+      'coordinates' : feature.getGeometry().getCoordinates()
+    };
+  } else if (type === 'Circle') {
+    return {
+      'type' : 'circle',
+      'center' : feature.getGeometry().getCenter(),
+      'radius' : feature.getGeometry().getRadius()
+    };
+  } else if (type === 'Polygon') {
+    return {
+      'type' : 'polygon',
+      'coordinates' : feature.getGeometry().getCoordinates()
+    };
+  } else {
+    console.error('Not implemented: ' + type);
+    return null;
+  }
+}
+
+
+function UnserializeGeometry(json)
+{
+  if (json.type === 'polyline') {
+    return new ol.geom.LineString(json['coordinates']);
+  } else if (json.type === 'arrow') {
+    var feature = new ol.geom.LineString(json['coordinates']);
+    feature.set('type', 'arrow');
+    return feature;
+  } else if (json.type === 'point') {
+    return new ol.geom.Point(json['coordinates']);
+  } else if (json.type === 'circle') {
+    return new ol.geom.Circle(json['center'], json['radius']);
+  } else if (json.type === 'polygon') {
+    return new ol.geom.Polygon(json['coordinates']);
+  } else {
+    console.error('Not implemented: ' + json.type);
+    return null;
+  }
+}
+
+
+function UnserializeFeatureOntoMap(mapSource, serialized)
+{
+  var geometry = UnserializeGeometry(serialized);
+
+  if (geometry !== null) {
+    var feature = new ol.Feature(geometry);
+
+    var layerId = serialized['layer-id'];
+    console.assert(layerId !== undefined);
+    feature.set('layer-id', layerId);
+
+    var type = serialized['type'];
+    if (type !== undefined) {
+      feature.set('type', type);
+    }
+
+    var label = serialized['label'];
+    if (label !== undefined) {
+      feature.set('label', label);
+    }
+
+    var date = serialized['creation-datetime'];  // This is a numerical timestamp
+    if (date !== undefined) {
+      feature.set('creation-datetime', new Date(date));
+    }
+
+    mapSource.addFeature(feature);
+  }
+}
+
+
+function BeforeUnloadHandler(event)
+{
+  event.preventDefault();
+
+  // Included for legacy support, e.g. Chrome/Edge < 119
+  event.returnValue = true;
+};
+
+
+
+
+function SetMagnification(map, referenceMagnification, magnification)
+{
+  var view = map.getView();
+  var projection = view.getProjection();
+
+  if (projection.getMetersPerUnit()) {  // Ensure that "metersPerUnit" is not null
+    var resolution = referenceMagnification / magnification;
+
+    view.animate({
+      resolution: view.getConstrainedResolution(resolution),
+      duration: 250
+    });
+  }
+}
+
+
+function GetMagnification(map, referenceMagnification)
+{
+  var view = map.getView();
+  var projection = view.getProjection();
+
+  if (projection.getMetersPerUnit() !== undefined) {  // Ensure that "metersPerUnit" is not null
+    var resolution = view.getResolution();
+
+    return referenceMagnification / resolution;
+  }
+}
+
+
+/**
+ * A ScaleLine control that also displays the equivalent microscope
+ * objective magnification (4x, 10x, 40x,...) for the current zoom level.
+ */
+class MicroscopeScaleLine extends ol.control.ScaleLine {
+  constructor(options = {}) {
+    super(options);
+
+    this.referenceMagnification_ = options.referenceMagnification;
+    console.assert(this.referenceMagnification_ !== undefined);
+
+    this.magnificationElement_ = document.createElement('div');
+    this.magnificationElement_.className = 'ol-scale-magnification';
+    this.element.appendChild(this.magnificationElement_);
+  }
+
+  updateElement_() {
+    super.updateElement_();
+
+    var map = this.getMap();
+    if (map && this.magnificationElement_) {
+      var magnification = GetMagnification(map, this.referenceMagnification_);
+      if (magnification) {
+        this.magnificationElement_.innerText = magnification.toFixed(2) + 'x';
       }
-    });
-  } else {
-    alert('Error - No series ID and no instance ID specified!');
+    }
   }
-});
+}