changeset 6494:f9fd84b5ffeb

merge
author Alain Mazy <am@orthanc.team>
date Tue, 25 Nov 2025 15:52:40 +0100
parents ecdc569d04a2 (current diff) 028624638b10 (diff)
children 31e793f606bc
files OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 40 files changed, 490 insertions(+), 112 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Nov 25 15:48:26 2025 +0100
+++ b/.hgignore	Tue Nov 25 15:52:40 2025 +0100
@@ -9,6 +9,7 @@
 *~
 *.cmake.orig
 .idea/
+OrthancFramework/Resources/CodeGeneration/.venv/
 
 # when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore:
 .settings/
--- a/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Tue Nov 25 15:52:40 2025 +0100
@@ -181,6 +181,10 @@
     check_type_size("off_t"  SIZEOF_OFF_T)
     check_type_size("socklen_t" CURL_SIZEOF_CURL_SOCKLEN_T)
 
+    if (SIZEOF_LONG_LONG)
+      set(HAVE_LONGLONG 1)
+    endif()
+
     check_function_exists("accept4"       HAVE_ACCEPT4)
     check_function_exists("fnmatch"       HAVE_FNMATCH)
     check_symbol_exists("basename"        "${CURL_INCLUDES};string.h" HAVE_BASENAME)  # libgen.h unistd.h
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Nov 25 15:52:40 2025 +0100
@@ -600,6 +600,7 @@
     list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/DcmtkTranscoder.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/IDicomTranscoder.cpp
+      ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/Internals/SopInstanceUidFixer.cpp
       ${CMAKE_CURRENT_LIST_DIR}/../../Sources/DicomParsing/MemoryBufferTranscoder.cpp
       )
   else()
--- a/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Resources/CodeGeneration/CheckDcmtkTransferSyntaxes.py	Tue Nov 25 15:52:40 2025 +0100
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
--- a/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Tue Nov 25 15:52:40 2025 +0100
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py	Tue Nov 25 15:52:40 2025 +0100
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # Orthanc - A Lightweight, RESTful DICOM Store
 # Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Tue Nov 25 15:52:40 2025 +0100
@@ -102,6 +102,8 @@
   static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e);
   static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060);
 
+  static const DicomTag DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE(0x0054, 0x0022);
+
   // The following is used for "modify/anonymize" operations
   static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
   static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002);
@@ -196,6 +198,7 @@
   static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052);
   static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053);
   static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050);
+  static const DicomTag DICOM_TAG_SPACING_BETWEEN_SLICES(0x0018, 0x0088);
   static const DicomTag DICOM_TAG_WINDOW_CENTER(0x0028, 0x1050);
   static const DicomTag DICOM_TAG_WINDOW_WIDTH(0x0028, 0x1051);
   static const DicomTag DICOM_TAG_DOSE_GRID_SCALING(0x3004, 0x000e);
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -581,7 +581,7 @@
         targetSyntaxes.insert(preferredTransferSyntax);
         attemptedSyntaxes.insert(preferredTransferSyntax);
 
-        success = transcoder.Transcode(transcoded, source, targetSyntaxes, true);
+        success = transcoder.Transcode(transcoded, source, targetSyntaxes, TranscodingSopInstanceUidMode_AllowNew);
         isDestructiveCompressionAllowed = true;
       }
 
@@ -612,7 +612,7 @@
 
         if (!targetSyntaxes.empty())
         {
-          success = transcoder.Transcode(transcoded, source, targetSyntaxes, false);
+          success = transcoder.Transcode(transcoded, source, targetSyntaxes, TranscodingSopInstanceUidMode_NoChange);
           isDestructiveCompressionAllowed = false;
         }
       }
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -36,6 +36,7 @@
 
 
 #include "FromDcmtkBridge.h"
+#include "Internals/SopInstanceUidFixer.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
@@ -105,11 +106,12 @@
     return false;
   }
 
+
   bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
                                          std::string& failureReason /* out */,
                                          DcmFileFormat& dicom, /* in/out */
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                         bool allowNewSopInstanceUid,
+                                         TranscodingSopInstanceUidMode mode,
                                          unsigned int lossyQuality) 
   {
     std::vector<std::string> failureReasons;
@@ -155,6 +157,12 @@
       return true;
     }
 
+    // The SOP Instance UID fixer has only an effect if "mode" is TranscodingSopInstanceUidMode_Preserve
+    // and if transcoding to a lossy transfer syntax
+    const Internals::SopInstanceUidFixer fixer(mode, dicom);
+
+    const bool allowNewSopInstanceUid = (mode == TranscodingSopInstanceUidMode_AllowNew ||
+                                         mode == TranscodingSopInstanceUidMode_Preserve);
 
 #if ORTHANC_ENABLE_DCMTK_JPEG == 1
     if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end())
@@ -174,6 +182,7 @@
           
         if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
         {
+          fixer.Apply(dicom);
           selectedSyntax = DicomTransferSyntax_JPEGProcess1;
           return true;
         }
@@ -199,6 +208,7 @@
         DJ_RPLossy parameters(lossyQuality);
         if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
         {
+          fixer.Apply(dicom);
           selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
           return true;
         }
@@ -215,6 +225,7 @@
                                0 /* opt_point_transform */);
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, &parameters))
       {
+        fixer.Apply(dicom);
         selectedSyntax = DicomTransferSyntax_JPEGProcess14;
         return true;
       }
@@ -271,6 +282,7 @@
        **/              
       if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, &parameters))
       {
+        fixer.Apply(dicom);
         selectedSyntax = DicomTransferSyntax_JPEGLSLossy;
         return true;
       }
@@ -316,16 +328,16 @@
   bool DcmtkTranscoder::Transcode(DicomImage& target,
                                   DicomImage& source /* in, "GetParsed()" possibly modified */,
                                   const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                  bool allowNewSopInstanceUid)
+                                  TranscodingSopInstanceUidMode mode)
   {
-    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, defaultLossyQuality_);
+    return Transcode(target, source, allowedSyntaxes, mode, defaultLossyQuality_);
   }
 
 
   bool DcmtkTranscoder::Transcode(DicomImage& target,
                                   DicomImage& source /* in, "GetParsed()" possibly modified */,
                                   const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                  bool allowNewSopInstanceUid,
+                                  TranscodingSopInstanceUidMode mode,
                                   unsigned int lossyQuality)
   {
     Semaphore::Locker lock(maxConcurrentExecutionsSemaphore_); // limit the number of concurrent executions
@@ -373,7 +385,7 @@
       return true;
     }
     else if (InplaceTranscode(targetSyntax, failureReason, source.GetParsed(),
-                              allowedSyntaxes, allowNewSopInstanceUid, lossyQuality))
+                              allowedSyntaxes, mode, lossyQuality))
     {   
       // Sanity check
       DicomTransferSyntax targetSyntax2;
@@ -385,9 +397,13 @@
         source.Clear();
         
 #if !defined(NDEBUG)
-        // Only run the sanity check in debug mode
-        CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
-                         allowedSyntaxes, allowNewSopInstanceUid);
+        {
+          // Only run the sanity check in debug mode
+          const bool allowNewSopInstanceUid = (mode == TranscodingSopInstanceUidMode_AllowNew ||
+                                               mode == TranscodingSopInstanceUidMode_Preserve);
+          CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
+                           allowedSyntaxes, allowNewSopInstanceUid);
+        }
 #endif
         
         return true;
--- a/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Tue Nov 25 15:52:40 2025 +0100
@@ -48,7 +48,7 @@
                           std::string& failureReason /* out */,
                           DcmFileFormat& dicom,
                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                          bool allowNewSopInstanceUid,
+                          TranscodingSopInstanceUidMode mode,
                           unsigned int lossyQuality);
     
   public:
@@ -63,12 +63,12 @@
     virtual bool Transcode(DicomImage& target,
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+                           TranscodingSopInstanceUidMode mode) ORTHANC_OVERRIDE;
 
     virtual bool Transcode(DicomImage& target,
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid,
+                           TranscodingSopInstanceUidMode mode,
                            unsigned int lossyQuality) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -471,7 +471,7 @@
 
     if (previous == uidMap_.end())
     {
-      if (identifierGenerator_ == NULL)
+      if (identifierGenerator_.get() == NULL)
       {
         mapped = FromDcmtkBridge::GenerateUniqueIdentifier(level);
       }
@@ -541,9 +541,8 @@
     keepSeriesInstanceUid_(false),
     keepSopInstanceUid_(false),
     updateReferencedRelationships_(true),
-    isAnonymization_(false),
-    //privateCreator_("PrivateCreator"),
-    identifierGenerator_(NULL)
+    isAnonymization_(false)
+    //privateCreator_("PrivateCreator")
   {
   }
 
@@ -958,13 +957,18 @@
     }        
   }
 
-  void DicomModification::Apply(ParsedDicomFile& toModify)
+  void DicomModification::Apply(std::unique_ptr<ParsedDicomFile>& toModify)
   {
     // Check the request
     assert(ResourceType_Patient + 1 == ResourceType_Study &&
            ResourceType_Study + 1 == ResourceType_Series &&
            ResourceType_Series + 1 == ResourceType_Instance);
 
+    if (toModify.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
     if (IsRemoved(DICOM_TAG_PATIENT_ID) ||
         IsRemoved(DICOM_TAG_STUDY_INSTANCE_UID) ||
         IsRemoved(DICOM_TAG_SERIES_INSTANCE_UID) ||
@@ -1015,12 +1019,19 @@
       }
     }
 
+    // (0.1) New in Orthanc 1.12.10: Apply custom modifications (e.g. pixels masking)
+    // This is done before modifying any tags because the dicomModifier_ might have filters on the Orthanc ids ->
+    // the DICOM UID tags must not be modified before.
+    if (dicomModifier_ != NULL)
+    {
+      dicomModifier_->Apply(toModify);
+    }
 
-    // (0) Create a summary of the source file, if a custom generator
+    // (0.2) Create a summary of the source file, if a custom generator
     // is provided
     if (identifierGenerator_ != NULL)
     {
-      toModify.ExtractDicomSummary(currentSource_, ORTHANC_MAXIMUM_TAG_LENGTH);
+      toModify->ExtractDicomSummary(currentSource_, ORTHANC_MAXIMUM_TAG_LENGTH);
     }
 
     // (1) Make sure the relationships are updated with the ids that we force too
@@ -1031,7 +1042,7 @@
       {
         std::string original;
         std::string replacement = GetReplacementAsString(DICOM_TAG_STUDY_INSTANCE_UID);
-        const_cast<const ParsedDicomFile&>(toModify).GetTagValue(original, DICOM_TAG_STUDY_INSTANCE_UID);
+        const_cast<const ParsedDicomFile&>(*toModify).GetTagValue(original, DICOM_TAG_STUDY_INSTANCE_UID);
         RegisterMappedDicomIdentifier(original, replacement, ResourceType_Study);
       }
 
@@ -1039,7 +1050,7 @@
       {
         std::string original;
         std::string replacement = GetReplacementAsString(DICOM_TAG_SERIES_INSTANCE_UID);
-        const_cast<const ParsedDicomFile&>(toModify).GetTagValue(original, DICOM_TAG_SERIES_INSTANCE_UID);
+        const_cast<const ParsedDicomFile&>(*toModify).GetTagValue(original, DICOM_TAG_SERIES_INSTANCE_UID);
         RegisterMappedDicomIdentifier(original, replacement, ResourceType_Series);
       }
 
@@ -1047,7 +1058,7 @@
       {
         std::string original;
         std::string replacement = GetReplacementAsString(DICOM_TAG_SOP_INSTANCE_UID);
-        const_cast<const ParsedDicomFile&>(toModify).GetTagValue(original, DICOM_TAG_SOP_INSTANCE_UID);
+        const_cast<const ParsedDicomFile&>(*toModify).GetTagValue(original, DICOM_TAG_SOP_INSTANCE_UID);
         RegisterMappedDicomIdentifier(original, replacement, ResourceType_Instance);
       }
     }
@@ -1056,21 +1067,21 @@
     // (2) Remove the private tags, if need be
     if (removePrivateTags_)
     {
-      toModify.RemovePrivateTags(privateTagsToKeep_);
+      toModify->RemovePrivateTags(privateTagsToKeep_);
     }
 
     // (3) Clear the tags specified by the user
     for (SetOfTags::const_iterator it = clearings_.begin(); 
          it != clearings_.end(); ++it)
     {
-      toModify.Clear(*it, true /* only clear if the tag exists in the original file */);
+      toModify->Clear(*it, true /* only clear if the tag exists in the original file */);
     }
 
     // (4) Remove the tags specified by the user
     for (SetOfTags::const_iterator it = removals_.begin(); 
          it != removals_.end(); ++it)
     {
-      toModify.Remove(*it);
+      toModify->Remove(*it);
     }
 
     // (5) Replace the tags
@@ -1078,7 +1089,7 @@
          it != replacements_.end(); ++it)
     {
       assert(it->second != NULL);
-      toModify.Replace(it->first, *it->second, true /* decode data URI scheme */,
+      toModify->Replace(it->first, *it->second, true /* decode data URI scheme */,
                        DicomReplaceMode_InsertIfAbsent, privateCreator_);
     }
 
@@ -1092,7 +1103,7 @@
       }
       else
       {
-        MapDicomTags(toModify, ResourceType_Study);
+        MapDicomTags(*toModify, ResourceType_Study);
       }
     }
 
@@ -1105,7 +1116,7 @@
       }
       else
       {
-        MapDicomTags(toModify, ResourceType_Series);
+        MapDicomTags(*toModify, ResourceType_Series);
       }
     }
 
@@ -1118,7 +1129,7 @@
       }
       else
       {
-        MapDicomTags(toModify, ResourceType_Instance);
+        MapDicomTags(*toModify, ResourceType_Instance);
       }
     }
 
@@ -1129,11 +1140,11 @@
 
       if (updateReferencedRelationships_)
       {
-        const_cast<const ParsedDicomFile&>(toModify).Apply(visitor);
+        const_cast<const ParsedDicomFile&>(*toModify).Apply(visitor);
       }
       else
       {
-        visitor.RemoveRelationships(toModify);
+        visitor.RemoveRelationships(*toModify);
       }
     }
 
@@ -1142,7 +1153,7 @@
          it != removeSequences_.end(); ++it)
     {
       assert(it->GetPrefixLength() > 0);
-      toModify.RemovePath(*it);
+      toModify->RemovePath(*it);
     }
 
     for (SequenceReplacements::const_iterator it = sequenceReplacements_.begin();
@@ -1150,9 +1161,10 @@
     {
       assert(*it != NULL);
       assert((*it)->GetPath().GetPrefixLength() > 0);
-      toModify.ReplacePath((*it)->GetPath(), (*it)->GetValue(), true /* decode data URI scheme */,
-                           DicomReplaceMode_InsertIfAbsent, privateCreator_);
+      toModify->ReplacePath((*it)->GetPath(), (*it)->GetValue(), true /* decode data URI scheme */,
+                            DicomReplaceMode_InsertIfAbsent, privateCreator_);
     }
+
   }
 
   void DicomModification::SetAllowManualIdentifiers(bool check)
@@ -1387,7 +1399,7 @@
   {
     if (!request.isObject())
     {
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat, "The payload should be a JSON object.");
     }
 
     bool force = GetBooleanValue("Force", request, false);
@@ -1401,7 +1413,7 @@
     {
       if (request["DicomVersion"].type() != Json::stringValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat, "DicomVersion should be a string");
       }
       else
       {
@@ -1445,9 +1457,16 @@
     }
   }
 
-  void DicomModification::SetDicomIdentifierGenerator(DicomModification::IDicomIdentifierGenerator &generator)
+  void DicomModification::SetDicomIdentifierGenerator(DicomModification::IDicomIdentifierGenerator* generator)
   {
-    identifierGenerator_ = &generator;
+    if (generator == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      identifierGenerator_.reset(generator);
+    }
   }
 
 
@@ -1627,8 +1646,7 @@
   }
 
   
-  DicomModification::DicomModification(const Json::Value& serialized) :
-    identifierGenerator_(NULL)
+  DicomModification::DicomModification(const Json::Value& serialized)
   {
     removePrivateTags_ = SerializationToolbox::ReadBoolean(serialized, REMOVE_PRIVATE_TAGS);
     level_ = StringToResourceType(SerializationToolbox::ReadString(serialized, LEVEL).c_str());
@@ -1904,4 +1922,16 @@
       target.insert(it->first);
     }
   }
+
+  void DicomModification::SetDicomModifier(IDicomModifier* modifier)
+  {
+    if (modifier == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      dicomModifier_.reset(modifier);
+    }
+  }
 }
--- a/OrthancFramework/Sources/DicomParsing/DicomModification.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h	Tue Nov 25 15:52:40 2025 +0100
@@ -31,6 +31,7 @@
 
 namespace Orthanc
 {
+
   class ORTHANC_PUBLIC DicomModification : public boost::noncopyable
   {
     /**
@@ -54,6 +55,18 @@
                          const DicomMap& sourceDicom) = 0;                       
     };
 
+    class ORTHANC_PUBLIC IDicomModifier : public boost::noncopyable
+    {
+    public:
+      virtual ~IDicomModifier()
+      {
+      }
+
+      // It is allowed for the implementation to replace "dicom" by a
+      // brand new ParsedDicomFile instance
+      virtual void Apply(std::unique_ptr<ParsedDicomFile>& dicom) = 0;
+    };
+
   private:
     class RelationshipsVisitor;
 
@@ -145,7 +158,7 @@
     DicomMap currentSource_;
     std::string privateCreator_;
 
-    IDicomIdentifierGenerator* identifierGenerator_;
+    std::unique_ptr<IDicomIdentifierGenerator> identifierGenerator_;
 
     // New in Orthanc 1.9.4
     SetOfTags            uids_;
@@ -154,6 +167,9 @@
     ListOfPaths          removeSequences_;       // Must *never* be a path whose prefix is empty
     SequenceReplacements sequenceReplacements_;  // Must *never* be a path whose prefix is empty
 
+    // New in Orthanc 1.12.10
+    std::unique_ptr<IDicomModifier>   dicomModifier_;
+
     std::string MapDicomIdentifier(const std::string& original,
                                    ResourceType level);
 
@@ -235,7 +251,8 @@
 
     void SetupAnonymization(DicomVersion version);
 
-    void Apply(ParsedDicomFile& toModify);
+    // The "toModify" might be replaced by a new object
+    void Apply(std::unique_ptr<ParsedDicomFile>& toModify);
 
     void SetAllowManualIdentifiers(bool check);
 
@@ -248,7 +265,7 @@
     void ParseAnonymizationRequest(bool& patientNameOverridden /* out */,
                                    const Json::Value& request);
 
-    void SetDicomIdentifierGenerator(IDicomIdentifierGenerator& generator);
+    void SetDicomIdentifierGenerator(IDicomIdentifierGenerator* generator /* takes ownership */);
 
     void Serialize(Json::Value& value) const;
 
@@ -268,5 +285,12 @@
                  bool safeForAnonymization);
 
     bool IsAlteredTag(const DicomTag& tag) const;
+
+    bool IsAnonymization() const
+    {
+      return isAnonymization_;
+    }
+
+    void SetDicomModifier(IDicomModifier* modifier);
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Tue Nov 25 15:52:40 2025 +0100
@@ -56,8 +56,6 @@
 
       void Serialize();
 
-      DcmFileFormat* ReleaseParsed();
-
     public:
       DicomImage();
       
@@ -81,6 +79,8 @@
 
       DcmFileFormat& GetParsed();
 
+      DcmFileFormat* ReleaseParsed();
+
       ParsedDicomFile* ReleaseAsParsedDicomFile();
 
       const void* GetBufferData();
@@ -114,12 +114,12 @@
     virtual bool Transcode(DicomImage& target,
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) = 0;
+                           TranscodingSopInstanceUidMode mode) = 0;
 
     virtual bool Transcode(DicomImage& target,
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid,
+                           TranscodingSopInstanceUidMode mode,
                            unsigned int lossyQuality) = 0;
 
     static std::string GetSopInstanceUid(DcmFileFormat& dicom);
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -226,6 +226,28 @@
         fragment = dynamic_cast<DcmPixelItem*>(pixelSequence_->nextInContainer(fragment));
       }
     }
+
+    virtual uint8_t* GetRawFrameBuffer(unsigned int index) ORTHANC_OVERRIDE
+    {
+      if (index >= startFragment_.size())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      if (countFragments_[index] != 1)
+      {
+        throw OrthancException(ErrorCode_NotImplemented, "GetRawFrameBuffer is currently not implemented if there are more fragments than frames.");
+      }
+
+      DcmPixelItem* fragment = startFragment_[index];
+      uint8_t* content = NULL;
+      if (!fragment->getUint8Array(content).good() ||
+          content == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      return content;
+    }
   };
 
 
@@ -277,6 +299,11 @@
         memcpy(&frame[0], pixelData_ + index * frameSize_, frameSize_);
       }
     }
+
+    virtual uint8_t* GetRawFrameBuffer(unsigned int index) ORTHANC_OVERRIDE
+    {
+      return pixelData_ + index * frameSize_;
+    }
   };
 
 
@@ -308,6 +335,11 @@
         memcpy(&frame[0], reinterpret_cast<const uint8_t*>(&pixelData_[0]) + index * frameSize_, frameSize_);
       }
     }
+
+    virtual uint8_t* GetRawFrameBuffer(unsigned int index) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
   };
 
 
@@ -415,4 +447,20 @@
       throw OrthancException(ErrorCode_BadFileFormat, "Cannot access a raw frame");
     }
   }
+
+  uint8_t* DicomFrameIndex::GetRawFrameBuffer(unsigned int index)
+  {
+    if (index >= countFrames_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else if (index_.get() != NULL)
+    {
+      return index_->GetRawFrameBuffer(index);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Cannot access a raw frame");
+    }
+  }
 }
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Tue Nov 25 15:52:40 2025 +0100
@@ -48,6 +48,8 @@
 
       virtual void GetRawFrame(std::string& frame,
                                unsigned int index) const = 0;
+
+      virtual uint8_t* GetRawFrameBuffer(unsigned int index) = 0;
     };
 
     class FragmentIndex;
@@ -69,5 +71,7 @@
                      unsigned int index) const;
 
     static unsigned int GetFramesCount(DcmDataset& dicom);
+
+    uint8_t* GetRawFrameBuffer(unsigned int index);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/Internals/SopInstanceUidFixer.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -0,0 +1,85 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "SopInstanceUidFixer.h"
+
+#include "../../OrthancException.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    SopInstanceUidFixer::SopInstanceUidFixer(TranscodingSopInstanceUidMode mode,
+                                             IDicomTranscoder::DicomImage& source) :
+      fix_(mode == TranscodingSopInstanceUidMode_Preserve)
+    {
+      if (fix_)
+      {
+        sopInstanceUid_ = IDicomTranscoder::GetSopInstanceUid(source.GetParsed());
+      }
+    }
+
+
+    SopInstanceUidFixer::SopInstanceUidFixer(TranscodingSopInstanceUidMode mode,
+                                             DcmFileFormat& source) :
+      fix_(mode == TranscodingSopInstanceUidMode_Preserve)
+    {
+      if (fix_)
+      {
+        sopInstanceUid_ = IDicomTranscoder::GetSopInstanceUid(source);
+      }
+    }
+
+
+    void SopInstanceUidFixer::Apply(DcmFileFormat& target) const
+    {
+      if (fix_)
+      {
+        if (target.getDataset() == NULL ||
+            !target.getDataset()->putAndInsertString(
+              DCM_SOPInstanceUID, sopInstanceUid_.c_str(), OFTrue /* replace */).good())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+
+
+    void SopInstanceUidFixer::Apply(IDicomTranscoder::DicomImage& target) const
+    {
+      if (fix_)
+      {
+        std::unique_ptr<DcmFileFormat> dicom(target.ReleaseParsed());
+        Apply(*dicom);
+
+        target.Clear();
+        target.AcquireParsed(dicom.release());
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/Internals/SopInstanceUidFixer.h	Tue Nov 25 15:52:40 2025 +0100
@@ -0,0 +1,60 @@
+/**
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Enumerations.h"
+#include "../IDicomTranscoder.h"
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1
+#endif
+
+#include <boost/noncopyable.hpp>
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    class SopInstanceUidFixer : public boost::noncopyable
+    {
+    private:
+      bool         fix_;
+      std::string  sopInstanceUid_;
+
+    public:
+      SopInstanceUidFixer(TranscodingSopInstanceUidMode mode,
+                          IDicomTranscoder::DicomImage& source);
+
+      SopInstanceUidFixer(TranscodingSopInstanceUidMode mode,
+                          DcmFileFormat& source);
+
+      void Apply(IDicomTranscoder::DicomImage& target) const;
+
+      void Apply(DcmFileFormat& target) const;
+    };
+  }
+}
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -28,6 +28,7 @@
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "FromDcmtkBridge.h"
+#include "Internals/SopInstanceUidFixer.h"
 
 #if !defined(NDEBUG)  // For debugging
 #  include "ParsedDicomFile.h"
@@ -59,16 +60,16 @@
   bool MemoryBufferTranscoder::Transcode(DicomImage& target,
                                          DicomImage& source,
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                         bool allowNewSopInstanceUid,
+                                         TranscodingSopInstanceUidMode mode,
                                          unsigned int lossyQualityNotUsed)
   {
-    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+    return Transcode(target, source, allowedSyntaxes, mode);
   }
 
   bool MemoryBufferTranscoder::Transcode(DicomImage& target,
                                          DicomImage& source,
                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                         bool allowNewSopInstanceUid)
+                                         TranscodingSopInstanceUidMode mode)
   {
     target.Clear();
     
@@ -84,6 +85,10 @@
     const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
 #endif
 
+    Internals::SopInstanceUidFixer fixer(mode, source);
+    const bool allowNewSopInstanceUid = (mode == TranscodingSopInstanceUidMode_AllowNew ||
+                                         mode == TranscodingSopInstanceUidMode_Preserve);
+
     std::string buffer;
     if (TranscodeBuffer(buffer, source.GetBufferData(), source.GetBufferSize(),
                         allowedSyntaxes, allowNewSopInstanceUid))
@@ -98,6 +103,7 @@
                        allowedSyntaxes, allowNewSopInstanceUid);
 #endif
 
+      fixer.Apply(target);
       return true;
     }
     else
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Tue Nov 25 15:52:40 2025 +0100
@@ -42,12 +42,12 @@
     virtual bool Transcode(DicomImage& target /* out */,
                            DicomImage& source,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+                           TranscodingSopInstanceUidMode mode) ORTHANC_OVERRIDE;
 
     virtual bool Transcode(DicomImage& target /* out */,
                            DicomImage& source,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid,
+                           TranscodingSopInstanceUidMode mode,
                            unsigned int lossyQualityNotUsed) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -1824,6 +1824,67 @@
     }
   }
 
+  ImageAccessor* ParsedDicomFile::GetRawFrameForInplaceModification(unsigned int frame)
+  {
+    E_TransferSyntax transferSyntax = GetDcmtkObjectConst().getDataset()->getCurrentXfer();
+
+    bool ok = false;
+    switch (Toolbox::DetectEndianness())
+    {
+      case Endianness_Little:
+        if (transferSyntax == EXS_LittleEndianImplicit ||
+            transferSyntax == EXS_LittleEndianExplicit)
+        {
+          ok = true;
+        }
+        break;
+
+      case Endianness_Big:
+        if (transferSyntax == EXS_BigEndianImplicit ||
+            transferSyntax == EXS_BigEndianExplicit)
+        {
+          ok = true;
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (!ok)
+    {
+      throw OrthancException(ErrorCode_NotImplemented, "ParsedDicomFile::GetRawFrameForInplaceModification() only works with uncompressed transfer syntaxes and matching host endianness");
+    }
+
+    if (pimpl_->frameIndex_.get() == NULL)
+    {
+      assert(pimpl_->file_ != NULL &&
+             GetDcmtkObjectConst().getDataset() != NULL);
+      pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObjectConst().getDataset()));
+    }
+
+    DicomMap m;
+    std::set<DicomTag> ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomSummary(m, *GetDcmtkObjectConst().getDataset(), DicomImageInformation::GetUsefulTagLength(), ignoreTagLength);
+
+    DicomImageInformation info(m);
+    PixelFormat format;
+    
+    if (!info.ExtractPixelFormat(format, false))
+    {
+      LOG(WARNING) << "Unsupported DICOM image: " << info.GetBitsStored() 
+                   << "bpp, " << info.GetChannelCount() << " channels, " 
+                   << (info.IsSigned() ? "signed" : "unsigned")
+                   << (info.IsPlanar() ? ", planar, " : ", non-planar, ")
+                   << EnumerationToString(info.GetPhotometricInterpretation())
+                   << " photometric interpretation";
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    std::unique_ptr<ImageAccessor> img(new ImageAccessor());
+    img->AssignWritable(format, info.GetWidth(), info.GetHeight(), info.GetWidth() * GetBytesPerPixel(format), pimpl_->frameIndex_->GetRawFrameBuffer(frame));
+    return img.release();
+  }
 
   static bool HasGenericGroupLength(const DicomPath& path)
   {
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Tue Nov 25 15:52:40 2025 +0100
@@ -316,6 +316,10 @@
     ImageAccessor* DecodeAllOverlays(int& originX,
                                      int& originY) const;
 
+    // Returns an image accessor to the raw frame only if the DicomFile is in an uncompressed TS.
+    // This enables modification of pixels data in place.
+    ImageAccessor* GetRawFrameForInplaceModification(unsigned int frame);
+
     void InjectEmptyPixelData(ValueRepresentation vr);
 
     // Remove all the tags after pixel data
--- a/OrthancFramework/Sources/Enumerations.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/Enumerations.h	Tue Nov 25 15:52:40 2025 +0100
@@ -809,6 +809,13 @@
     ErrorPayloadType_RetrieveJob = 3,
   };
 
+  enum TranscodingSopInstanceUidMode
+  {
+    TranscodingSopInstanceUidMode_NoChange,   // Never change the SOP Instance UID (only allows transcoding to lossless)
+    TranscodingSopInstanceUidMode_AllowNew,   // Allow transcoding to lossless and lossy (if lossy, a new SOP Instance UID is generated)
+    TranscodingSopInstanceUidMode_Preserve    // Allow transcoding to lossless and lossy (if lossy, preserve the original SOP Instance UID)
+  };
+
 
   ORTHANC_PUBLIC
   const char* EnumerationToString(ErrorCode code);
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -2789,6 +2789,19 @@
         }
         break;
 
+      case PixelFormat_Grayscale16:
+        if (useRound)
+        {
+          SeparableConvolutionFloat<uint16_t, 1u, true>
+            (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization);
+        }
+        else
+        {
+          SeparableConvolutionFloat<uint16_t, 1u, false>
+            (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization);
+        }
+        break;
+
       case PixelFormat_RGB24:
         if (useRound)
         {
@@ -2822,6 +2835,14 @@
   }
 
 
+  void ImageProcessing::MeanFilter(ImageAccessor& image, size_t horizontalKernelWidth, size_t verticalKernelWidth)
+  {
+    std::vector<float> hKernel(horizontalKernelWidth, 1.0f);
+    std::vector<float> vKernel(verticalKernelWidth, 1.0f);
+    
+    SeparableConvolution(image, hKernel, horizontalKernelWidth / 2, vKernel, verticalKernelWidth / 2, false);
+  }
+
   void ImageProcessing::FitSize(ImageAccessor& target,
                                 const ImageAccessor& source)
   {
--- a/OrthancFramework/Sources/Images/ImageProcessing.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/Sources/Images/ImageProcessing.h	Tue Nov 25 15:52:40 2025 +0100
@@ -201,6 +201,8 @@
     static void SmoothGaussian5x5(ImageAccessor& image,
                                   bool useRound /* this is expensive */);
 
+    static void MeanFilter(ImageAccessor& image, size_t horizontalAverageWidth, size_t verticalAverageWidth);
+
     static void FitSize(ImageAccessor& target,
                         const ImageAccessor& source);
 
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -116,7 +116,7 @@
     std::unique_ptr<ParsedDicomFile> f(o.Clone(false));
     if (i > 4)
       o.ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
-    m.Apply(*f);
+    m.Apply(f);
     f->SaveToFile(b);
   }
 }
@@ -135,26 +135,26 @@
   ASSERT_EQ(0x1020, privateTag2.GetElement());
 
   std::string s;
-  ParsedDicomFile o(true);
-  o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
-  ASSERT_FALSE(o.GetTagValue(s, privateTag));
-  o.Insert(privateTag, "private tag", false, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  std::unique_ptr<ParsedDicomFile> o(new ParsedDicomFile(true));
+  o->ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
+  ASSERT_FALSE(o->GetTagValue(s, privateTag));
+  o->Insert(privateTag, "private tag", false, "OrthancCreator");
+  ASSERT_TRUE(o->GetTagValue(s, privateTag));
   ASSERT_STREQ("private tag", s.c_str());
 
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException);
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator");
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  ASSERT_FALSE(o->GetTagValue(s, privateTag2));
+  ASSERT_THROW(o->Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException);
+  ASSERT_FALSE(o->GetTagValue(s, privateTag2));
+  o->Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator");
+  ASSERT_FALSE(o->GetTagValue(s, privateTag2));
+  o->Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
+  ASSERT_TRUE(o->GetTagValue(s, privateTag2));
   ASSERT_STREQ("hello", s.c_str());
-  o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  o->Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
+  ASSERT_TRUE(o->GetTagValue(s, privateTag2));
   ASSERT_STREQ("hello world", s.c_str());
 
-  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(o->GetTagValue(s, DICOM_TAG_PATIENT_NAME));
   ASSERT_FALSE(Toolbox::IsUuid(s));
 
   DicomModification m;
@@ -163,14 +163,14 @@
 
   m.Apply(o);
 
-  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(o->GetTagValue(s, DICOM_TAG_PATIENT_NAME));
   ASSERT_TRUE(Toolbox::IsUuid(s));
-  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  ASSERT_TRUE(o->GetTagValue(s, privateTag));
   ASSERT_STREQ("private tag", s.c_str());
   
   m.SetupAnonymization(DicomVersion_2008);
   m.Apply(o);
-  ASSERT_FALSE(o.GetTagValue(s, privateTag));
+  ASSERT_FALSE(o->GetTagValue(s, privateTag));
 }
 
 
@@ -2772,7 +2772,7 @@
       modif.Replace(DicomPath(DICOM_TAG_PATIENT_NAME), "Hello1", false);
       modif.Replace(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID"), "Hello2", false);
       modif.Replace(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence[0].CodeValue"), "Hello3", false);
-      modif.Apply(*dicom);
+      modif.Apply(dicom);
     }
     
     Json::Value vv;
@@ -2794,7 +2794,7 @@
       modif.Remove(DicomPath(DICOM_TAG_PATIENT_NAME));
       modif.Remove(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPClassUID"));
       modif.Remove(DicomPath::Parse("RelatedSeriesSequence[0].PurposeOfReferenceCodeSequence"));
-      modif.Apply(*dicom);
+      modif.Apply(dicom);
     }
     
     Json::Value vv;
@@ -2815,8 +2815,8 @@
     {
       DicomModification modif;
       modif.SetupAnonymization(DicomVersion_2023b);
-      modif.Apply(*dicom1);
-      modif.Apply(*dicom2);
+      modif.Apply(dicom1);
+      modif.Apply(dicom2);
     }
 
     // Same anonymization context and same input DICOM => hence, same output DICOM    
@@ -2844,7 +2844,7 @@
       modif.SetupAnonymization(DicomVersion_2023b);
       modif.Keep(DicomPath::Parse("ReferencedImageSequence[1].ReferencedSOPInstanceUID"));
       modif.Keep(DicomPath::Parse("RelatedSeriesSequence"));
-      modif.Apply(*dicom);
+      modif.Apply(dicom);
     }
     
     Json::Value vv;
@@ -3681,7 +3681,7 @@
     IDicomTranscoder::DicomImage source, target;
     source.AcquireParsed(dynamic_cast<DcmFileFormat*>(toto->clone()));
 
-    if (!transcoder.Transcode(target, source, s, true))
+    if (!transcoder.Transcode(target, source, s, TranscodingSopInstanceUidMode_AllowNew))
     {
       printf("**************** CANNOT: [%s] => [%s]\n",
              GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a));
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -1084,7 +1084,7 @@
     modification.Remove(DICOM_TAG_SERIES_DESCRIPTION);
     modification.Replace(DICOM_TAG_PATIENT_NAME, "Test 4", true);
 
-    modification.Apply(*modified);
+    modification.Apply(modified);
 
     s = 42;
     modification.Serialize(s);
@@ -1095,7 +1095,7 @@
     ASSERT_EQ(ResourceType_Series, modification.GetLevel());
     
     std::unique_ptr<ParsedDicomFile> second(source.Clone(true));
-    modification.Apply(*second);
+    modification.Apply(second);
 
     std::string t;
     ASSERT_TRUE(second->GetTagValue(t, DICOM_TAG_STUDY_DESCRIPTION));
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -6015,11 +6015,10 @@
 
           IDicomTranscoder::DicomImage transcoded;
           bool success;
-          
+
           {
             PImpl::ServerContextReference lock(*pimpl_);
-            success = lock.GetContext().Transcode(
-              transcoded, source, syntaxes, true /* allow new sop */);
+            success = lock.GetContext().Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew);
           }
 
           if (success)
--- a/OrthancServer/Plugins/Samples/CppSkeleton/CMakeLists.txt	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Plugins/Samples/CppSkeleton/CMakeLists.txt	Tue Nov 25 15:52:40 2025 +0100
@@ -19,7 +19,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-cmake_minimum_required(VERSION 2.8)
+cmake_minimum_required(VERSION 2.8...4.0)
 
 project(OrthancSkeleton)
 
--- a/OrthancServer/Plugins/Samples/CppSkeleton/Resources/SyncOrthancFolder.py	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Plugins/Samples/CppSkeleton/Resources/SyncOrthancFolder.py	Tue Nov 25 15:52:40 2025 +0100
@@ -40,7 +40,7 @@
     ('orthanc', 'OrthancFramework/Resources/CMake/Compiler.cmake', 'CMake'),
     ('orthanc', 'OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake', 'CMake'),
     ('orthanc', 'OrthancFramework/Resources/CMake/DownloadPackage.cmake', 'CMake'),
-    ('orthanc', 'OrthancFramework/Resources/EmbedResources.py', 'CMake'),
+    ('orthanc', 'OrthancFramework/Resources/CMake/EmbedResources.py', 'CMake'),
 
     ('orthanc', 'OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake', 'Toolchains'),
     ('orthanc', 'OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake', 'Toolchains'),
--- a/OrthancServer/Resources/RunCppCheck-2.1.sh	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Resources/RunCppCheck-2.1.sh	Tue Nov 25 15:52:40 2025 +0100
@@ -13,7 +13,7 @@
 constParameter:../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
 knownArgument:../../OrthancFramework/UnitTestsSources/ImageTests.cpp
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp
-nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322
+nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
 stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1535
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
--- a/OrthancServer/Resources/RunCppCheck-2.17.1.sh	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Resources/RunCppCheck-2.17.1.sh	Tue Nov 25 15:52:40 2025 +0100
@@ -17,7 +17,7 @@
 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp:112
 constParameterCallback:../../OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp:113
 constParameterCallback:../../OrthancFramework/Sources/Pkcs11.cpp:125
-constParameterCallback:../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp:3489
+constParameterCallback:../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp:3493
 constParameterCallback:../../OrthancServer/Sources/OrthancGetRequestHandler.cpp:47
 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:447
 constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:451
@@ -39,7 +39,7 @@
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2298
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2299
 knownConditionTrueFalse:../../OrthancServer/Plugins/Engine/OrthancPlugins.cpp:2300
-nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:322
+nullPointer:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp:321
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -325,7 +325,7 @@
       std::set<DicomTransferSyntax> ts;
       ts.insert(selectedSyntax);
       
-      if (context_.Transcode(transcoded, source, ts, true))
+      if (context_.Transcode(transcoded, source, ts, TranscodingSopInstanceUidMode_AllowNew))
       {
         // Transcoding has succeeded
         DcmDataset *stDetailTmp = NULL;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -220,7 +220,7 @@
       modified.reset(locker.GetDicom().Clone(true));
     }
     
-    modification.Apply(*modified);
+    modification.Apply(modified);
 
     if (transcode)
     {
@@ -232,7 +232,7 @@
       std::set<DicomTransferSyntax> s;
       s.insert(targetSyntax);
       
-      if (context.Transcode(transcoded, source, s, true, lossyQuality))
+      if (context.Transcode(transcoded, source, s, TranscodingSopInstanceUidMode_AllowNew, lossyQuality))
       {      
         call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
                                       transcoded.GetBufferSize(), MimeType_Dicom);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -432,7 +432,7 @@
         std::set<DicomTransferSyntax> allowedSyntaxes;
         allowedSyntaxes.insert(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
 
-        if (context.Transcode(targetImage, sourceImage, allowedSyntaxes, true, lossyQuality))
+        if (context.Transcode(targetImage, sourceImage, allowedSyntaxes, TranscodingSopInstanceUidMode_AllowNew, lossyQuality))
         {
           call.GetOutput().SetContentFilename(filename.c_str());
           call.GetOutput().AnswerBuffer(targetImage.GetBufferData(), targetImage.GetBufferSize(), MimeType_Dicom);
--- a/OrthancServer/Sources/ServerContext.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -50,6 +50,7 @@
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmnet/dimse.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
 #include <boost/regex.hpp>
@@ -1017,7 +1018,7 @@
         
         IDicomTranscoder::DicomImage transcoded;
         
-        if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
+        if (Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew /* allow new SOP instance UID */))
         {
           std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
 
@@ -1941,7 +1942,7 @@
       source.SetExternalBuffer(buffer, size);
       allowedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
 
-      if (Transcode(explicitTemporaryImage, source, allowedSyntaxes, true))
+      if (Transcode(explicitTemporaryImage, source, allowedSyntaxes, TranscodingSopInstanceUidMode_AllowNew))
       {
         std::unique_ptr<ParsedDicomFile> file(explicitTemporaryImage.ReleaseAsParsedDicomFile());
         return file->DecodeFrame(frameIndex);
@@ -2015,7 +2016,7 @@
       std::set<DicomTransferSyntax> syntaxes;
       syntaxes.insert(targetSyntax);
 
-      if (Transcode(targetDicom, sourceDicom, syntaxes, true))
+      if (Transcode(targetDicom, sourceDicom, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
       {
         cacheAccessor.AddTranscodedInstance(attachmentId, targetSyntax, reinterpret_cast<const char*>(targetDicom.GetBufferData()), targetDicom.GetBufferSize());
         target = std::string(reinterpret_cast<const char*>(targetDicom.GetBufferData()), targetDicom.GetBufferSize());
@@ -2031,7 +2032,7 @@
   bool ServerContext::Transcode(DicomImage& target,
                                 DicomImage& source /* in, "GetParsed()" possibly modified */,
                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                bool allowNewSopInstanceUid)
+                                TranscodingSopInstanceUidMode mode)
   {
     unsigned int lossyQuality;
 
@@ -2040,19 +2041,19 @@
       lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
     }
 
-    return Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality);
+    return Transcode(target, source, allowedSyntaxes, mode, lossyQuality);
   }
 
 
   bool ServerContext::Transcode(DicomImage& target,
                                 DicomImage& source /* in, "GetParsed()" possibly modified */,
                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                bool allowNewSopInstanceUid,
+                                TranscodingSopInstanceUidMode mode,
                                 unsigned int lossyQuality)
   {
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
     {
-      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality))
+      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, mode, lossyQuality))
       {
         return true;
       }
@@ -2062,7 +2063,7 @@
     if (HasPlugins() &&
         GetPlugins().HasCustomTranscoder())
     {
-      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))  // TODO: pass lossyQuality to plugins -> needs a new plugin interface
+      if (GetPlugins().Transcode(target, source, allowedSyntaxes, mode))  // TODO: pass lossyQuality to plugins -> needs a new plugin interface
       {
         return true;
       }
@@ -2076,7 +2077,7 @@
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality);
+      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, mode, lossyQuality);
     }
     else
     {
@@ -2084,6 +2085,7 @@
     }
   }
 
+
   const std::string& ServerContext::GetDeidentifiedContent(const DicomElement &element) const
   {
     static const std::string redactedContent = "*** POTENTIAL PHI ***";
--- a/OrthancServer/Sources/ServerContext.h	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Tue Nov 25 15:52:40 2025 +0100
@@ -589,12 +589,12 @@
     virtual bool Transcode(DicomImage& target,
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
-    
+                           TranscodingSopInstanceUidMode mode) ORTHANC_OVERRIDE;
+
     virtual bool Transcode(DicomImage& target,
                            DicomImage& source /* in, "GetParsed()" possibly modified */,
                            const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid,
+                           TranscodingSopInstanceUidMode mode,
                            unsigned int lossyQuality) ORTHANC_OVERRIDE;
 
     virtual bool TranscodeWithCache(std::string& target,
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -116,7 +116,7 @@
         IDicomTranscoder::DicomImage source, transcoded;
         source.SetExternalBuffer(sourceBuffer);
 
-        if (context_.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */, lossyQuality_))
+        if (context_.Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew, lossyQuality_))
         {
           transcodedBuffer.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()), transcoded.GetBufferSize());
           return true;
--- a/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -92,7 +92,7 @@
 
     try
     {
-      modification_->Apply(*modified);
+      modification_->Apply(modified);
 
       std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*modified));
       assert(origin_ == RequestOrigin_Lua);
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -69,7 +69,7 @@
         IDicomTranscoder::DicomImage source, transcoded;
         source.SetExternalBuffer(dicom);
 
-        if (context_.Transcode(transcoded, source, syntaxes, true))
+        if (context_.Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
         {
           body.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()),
                       transcoded.GetBufferSize());
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Tue Nov 25 15:48:26 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Tue Nov 25 15:52:40 2025 +0100
@@ -238,7 +238,7 @@
     {
       boost::recursive_mutex::scoped_lock lock(mutex_);  // DicomModification object is not thread safe, we must protect it from here
 
-      modification_->Apply(*modified);
+      modification_->Apply(modified);
 
       if (modification_->AreLabelsKept())
       {
@@ -261,7 +261,7 @@
       source.AcquireParsed(*modified);  // "modified" is invalid below this point
       
       IDicomTranscoder::DicomImage transcoded;
-      if (GetContext().Transcode(transcoded, source, syntaxes, true))
+      if (GetContext().Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew))
       {
         modified.reset(transcoded.ReleaseAsParsedDicomFile());