changeset 6481:a9abcdac2eee pixel-anon

integration mainline->pixel-anon
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 25 Nov 2025 13:54:52 +0100
parents e26d2e84d9f5 (diff) 360953cb921b (current diff)
children 8f511144bd9f
files
diffstat 38 files changed, 1375 insertions(+), 202 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Nov 24 15:37:02 2025 +0100
+++ b/.hgignore	Tue Nov 25 13:54:52 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/OrthancFrameworkConfiguration.cmake	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Nov 25 13:54:52 2025 +0100
@@ -599,6 +599,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/DicomTransferSyntaxes.json	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Resources/CodeGeneration/DicomTransferSyntaxes.json	Tue Nov 25 13:54:52 2025 +0100
@@ -5,7 +5,8 @@
     "Value" : "LittleEndianImplicit",
     "Retired" : false,
     "DCMTK" : "EXS_LittleEndianImplicit",
-    "GDCM" : "gdcm::TransferSyntax::ImplicitVRLittleEndian"
+    "GDCM" : "gdcm::TransferSyntax::ImplicitVRLittleEndian",
+    "Raw": true, "Lossless": true
   },
   
   {
@@ -14,7 +15,8 @@
     "Value" : "LittleEndianExplicit",
     "Retired" : false,
     "DCMTK" : "EXS_LittleEndianExplicit",
-    "GDCM" : "gdcm::TransferSyntax::ExplicitVRLittleEndian"
+    "GDCM" : "gdcm::TransferSyntax::ExplicitVRLittleEndian",
+    "Raw": true, "Lossless": true
   },
   
   {
@@ -22,7 +24,8 @@
     "Name" : "Deflated Explicit VR Little Endian",
     "Value" : "DeflatedLittleEndianExplicit",
     "Retired" : false,
-    "DCMTK" : "EXS_DeflatedLittleEndianExplicit"
+    "DCMTK" : "EXS_DeflatedLittleEndianExplicit",
+    "Raw": false, "Lossless": true
   },
   
   {
@@ -30,7 +33,8 @@
     "Name" : "Explicit VR Big Endian",
     "Value" : "BigEndianExplicit",
     "Retired" : false,
-    "DCMTK" : "EXS_BigEndianExplicit"
+    "DCMTK" : "EXS_BigEndianExplicit",
+    "Raw": true, "Lossless": true
   },
   
   {
@@ -41,7 +45,8 @@
     "Note" : "Default Transfer Syntax for Lossy JPEG 8-bit Image Compression",
     "DCMTK" : "EXS_JPEGProcess1",
     "DCMTK360" : "EXS_JPEGProcess1TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1"
+    "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1",
+    "Raw": false, "Lossless": false
   },
   
   {
@@ -52,7 +57,8 @@
     "Note" : "Default Transfer Syntax for Lossy JPEG (lossy, 8/12 bit), 12-bit Image Compression (Process 4 only)",
     "DCMTK" : "EXS_JPEGProcess2_4",
     "DCMTK360" : "EXS_JPEGProcess2_4TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4"
+    "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -61,7 +67,8 @@
     "Value" : "JPEGProcess3_5",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess3_5",
-    "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -70,7 +77,8 @@
     "Value" : "JPEGProcess6_8",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess6_8",
-    "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -79,7 +87,8 @@
     "Value" : "JPEGProcess7_9",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess7_9",
-    "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -88,7 +97,8 @@
     "Value" : "JPEGProcess10_12",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess10_12",
-    "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -97,7 +107,8 @@
     "Value" : "JPEGProcess11_13",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess11_13",
-    "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -107,7 +118,8 @@
     "Retired" : false,
     "DCMTK" : "EXS_JPEGProcess14",
     "DCMTK360" : "EXS_JPEGProcess14TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14"
+    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -116,7 +128,8 @@
     "Value" : "JPEGProcess15",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess15",
-    "DCMTK360" : "EXS_JPEGProcess15TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess15TransferSyntax",
+    "Raw": false, "Lossless": true
   },
   
   {
@@ -125,7 +138,8 @@
     "Value" : "JPEGProcess16_18",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess16_18",
-    "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax",
+    "Raw": false, "Lossless": false
   },
   
   {
@@ -134,7 +148,8 @@
     "Value" : "JPEGProcess17_19",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess17_19",
-    "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax",
+    "Raw": false, "Lossless": false
   },
   
   {
@@ -143,7 +158,8 @@
     "Value" : "JPEGProcess20_22",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess20_22",
-    "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax",
+    "Raw": false, "Lossless": false
   },
   
   {
@@ -152,7 +168,8 @@
     "Value" : "JPEGProcess21_23",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess21_23",
-    "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax",
+    "Raw": false, "Lossless": false
   },
   
   {
@@ -161,7 +178,8 @@
     "Value" : "JPEGProcess24_26",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess24_26",
-    "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax",
+    "Raw": false, "Lossless": false
   },
   
   {
@@ -170,7 +188,8 @@
     "Value" : "JPEGProcess25_27",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess25_27",
-    "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax",
+    "Raw": false, "Lossless": false
   },
   
   {
@@ -179,7 +198,8 @@
     "Value" : "JPEGProcess28",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess28",
-    "DCMTK360" : "EXS_JPEGProcess28TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess28TransferSyntax",
+    "Raw": false, "Lossless": true
   },
   
   {
@@ -188,7 +208,8 @@
     "Value" : "JPEGProcess29",
     "Retired" : true,
     "DCMTK" : "EXS_JPEGProcess29",
-    "DCMTK360" : "EXS_JPEGProcess29TransferSyntax"
+    "DCMTK360" : "EXS_JPEGProcess29TransferSyntax",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -199,7 +220,8 @@
     "Note" : "Default Transfer Syntax for Lossless JPEG Image Compression",
     "DCMTK" : "EXS_JPEGProcess14SV1",
     "DCMTK360" : "EXS_JPEGProcess14SV1TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1"
+    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -208,7 +230,8 @@
     "Value" : "JPEGLSLossless",
     "Retired" : false,
     "DCMTK" : "EXS_JPEGLSLossless",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLSLossless"
+    "GDCM" : "gdcm::TransferSyntax::JPEGLSLossless",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -217,7 +240,8 @@
     "Value" : "JPEGLSLossy",
     "Retired" : false,
     "DCMTK" : "EXS_JPEGLSLossy",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLSNearLossless"
+    "GDCM" : "gdcm::TransferSyntax::JPEGLSNearLossless",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -226,7 +250,8 @@
     "Value" : "JPEG2000LosslessOnly",
     "Retired" : false,
     "DCMTK" : "EXS_JPEG2000LosslessOnly",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000Lossless"
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Lossless",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -235,7 +260,8 @@
     "Value" : "JPEG2000",
     "Retired" : false,
     "DCMTK" : "EXS_JPEG2000",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000"
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -244,7 +270,8 @@
     "Value" : "JPEG2000MulticomponentLosslessOnly",
     "Retired" : false,
     "DCMTK" : "EXS_JPEG2000MulticomponentLosslessOnly",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2Lossless"
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2Lossless",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -253,7 +280,8 @@
     "Value" : "JPEG2000Multicomponent",
     "Retired" : false,
     "DCMTK" : "EXS_JPEG2000Multicomponent",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2"
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -261,7 +289,8 @@
     "Name" : "JPIP Referenced",
     "Value" : "JPIPReferenced",
     "Retired" : false,
-    "DCMTK" : "EXS_JPIPReferenced"
+    "DCMTK" : "EXS_JPIPReferenced",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -269,7 +298,8 @@
     "Name" : "JPIP Referenced Deflate",
     "Value" : "JPIPReferencedDeflate",
     "Retired" : false,
-    "DCMTK" : "EXS_JPIPReferencedDeflate"
+    "DCMTK" : "EXS_JPIPReferencedDeflate",
+    "Raw": false, "Lossless": true
   },
 
   {
@@ -277,7 +307,8 @@
     "Name" : "MPEG2 Main Profile / Main Level",
     "Value" : "MPEG2MainProfileAtMainLevel",
     "Retired" : false,
-    "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel"
+    "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -285,7 +316,8 @@
     "Name" : "MPEG2 Main Profile / High Level",
     "Value" : "MPEG2MainProfileAtHighLevel",
     "Retired" : false,
-    "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel"
+    "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -294,7 +326,8 @@
     "Value" : "MPEG4HighProfileLevel4_1",
     "Retired" : false,
     "DCMTK" : "EXS_MPEG4HighProfileLevel4_1",
-    "SinceDCMTK" : "361"
+    "SinceDCMTK" : "361",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -303,7 +336,8 @@
     "Value" : "MPEG4BDcompatibleHighProfileLevel4_1",
     "Retired" : false,
     "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1",
-    "SinceDCMTK" : "361"
+    "SinceDCMTK" : "361",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -312,7 +346,8 @@
     "Value" : "MPEG4HighProfileLevel4_2_For2DVideo",
     "Retired" : false,
     "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo",
-    "SinceDCMTK" : "361"
+    "SinceDCMTK" : "361",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -321,7 +356,8 @@
     "Value" : "MPEG4HighProfileLevel4_2_For3DVideo",
     "Retired" : false,
     "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo",
-    "SinceDCMTK" : "361"
+    "SinceDCMTK" : "361",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -330,7 +366,8 @@
     "Value" : "MPEG4StereoHighProfileLevel4_2",
     "Retired" : false,
     "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2",
-    "SinceDCMTK" : "361"
+    "SinceDCMTK" : "361",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -339,7 +376,8 @@
     "Value" : "HEVCMainProfileLevel5_1",
     "Retired" : false,
     "DCMTK" : "EXS_HEVCMainProfileLevel5_1",
-    "SinceDCMTK" : "362"
+    "SinceDCMTK" : "362",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -348,7 +386,8 @@
     "Value" : "HEVCMain10ProfileLevel5_1",
     "Retired" : false,
     "DCMTK" : "EXS_HEVCMain10ProfileLevel5_1",
-    "SinceDCMTK" : "362"
+    "SinceDCMTK" : "362",
+    "Raw": false, "Lossless": false
   },
 
   {
@@ -357,7 +396,8 @@
     "Value" : "RLELossless",
     "Retired" : false,
     "DCMTK" : "EXS_RLELossless",
-    "GDCM" : "gdcm::TransferSyntax::RLELossless"
+    "GDCM" : "gdcm::TransferSyntax::RLELossless",
+    "Raw": false, "Lossless": true
   },
 
   {
--- a/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache	Tue Nov 25 13:54:52 2025 +0100
@@ -82,4 +82,44 @@
     target.insert(DicomTransferSyntax_{{Value}});
     {{/Syntaxes}}
   }
+
+
+  bool IsLossyTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      {{#Syntaxes}}
+      case DicomTransferSyntax_{{Value}}:
+        {{#Lossless}}
+        return false;
+        {{/Lossless}}
+        {{^Lossless}}
+        return true;
+        {{/Lossless}}
+
+      {{/Syntaxes}}
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool IsRawTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      {{#Syntaxes}}
+      case DicomTransferSyntax_{{Value}}:
+        {{#Raw}}
+        return true;
+        {{/Raw}}
+        {{^Raw}}
+        return false;
+        {{/Raw}}
+
+      {{/Syntaxes}}
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 }
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Tue Nov 25 13:54:52 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)
@@ -1324,6 +1336,14 @@
       privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
     }
 
+    // TODO-PIXEL-ANON: remove
+    // // New in Orthanc 1.X.X
+    // if (request.isMember("MaskPixelData") && request["MaskPixelData"].isObject())
+    // {
+    //   pixelMasker_.reset(new DicomPixelMasker());
+    //   pixelMasker_->ParseRequest(request);
+    // }
+
     if (!force)
     {
       /**
@@ -1387,7 +1407,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 +1421,7 @@
     {
       if (request["DicomVersion"].type() != Json::stringValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat, "DicomVersion should be a string");
       }
       else
       {
@@ -1443,11 +1463,26 @@
     {
       privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
     }
+
+    // TODO-PIXEL-ANON: remove
+    // // New in Orthanc 1.X.X
+    // if (request.isMember("MaskPixelData") && request["MaskPixelData"].isObject())
+    // {
+    //   pixelMasker_.reset(new DicomPixelMasker());
+    //   pixelMasker_->ParseRequest(request);
+    // }
   }
 
-  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 +1662,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 +1938,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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/DicomModification.h	Tue Nov 25 13:54:52 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);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.cpp	Tue Nov 25 13:54:52 2025 +0100
@@ -0,0 +1,386 @@
+/**
+ * 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 "DicomPixelMasker.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "../Logging.h"
+
+namespace Orthanc
+{
+  static const char* KEY_MASK_PIXELS = "MaskPixelData";
+  static const char* KEY_MASK_TYPE = "MaskType";
+  static const char* KEY_MASK_TYPE_FILL = "Fill";
+  static const char* KEY_MASK_TYPE_MEAN_FILTER = "MeanFilter";
+  static const char* KEY_FILTER_WIDTH = "FilterWidth";
+  static const char* KEY_FILL_VALUE = "FillValue";
+  static const char* KEY_REGIONS = "Regions";
+  static const char* KEY_REGION_TYPE = "RegionType";
+  static const char* KEY_REGION_2D = "2D";
+  static const char* KEY_REGION_3D = "3D";
+  static const char* KEY_ORIGIN = "Origin";
+  static const char* KEY_END = "End";
+  static const char* KEY_TARGET_SERIES = "TargetSeries";
+  static const char* KEY_TARGET_INSTANCES = "TargetInstances";
+
+  DicomPixelMasker::DicomPixelMasker()
+  {
+  }
+
+  DicomPixelMasker::~DicomPixelMasker()
+  {
+    for (std::list<BaseRegion*>::iterator it = regions_.begin(); it != regions_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+  DicomPixelMasker::BaseRegion::BaseRegion() :
+    mode_(DicomPixelMaskerMode_Undefined),
+    fillValue_(0),
+    filterWidth_(0)
+  {
+  }
+
+  DicomPixelMasker::Region2D::Region2D(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2) :
+    x1_(x1),
+    y1_(y1),
+    x2_(x2),
+    y2_(y2)
+  {
+  }
+
+  DicomPixelMasker::Region3D::Region3D(double x1, double y1, double z1, double x2, double y2, double z2) :
+    x1_(x1),
+    y1_(y1),
+    z1_(z1),
+    x2_(x2),
+    y2_(y2),
+    z2_(z2)
+  {
+  }
+
+  bool DicomPixelMasker::BaseRegion::IsTargeted(const ParsedDicomFile& file) const
+  {
+    DicomInstanceHasher hasher = file.GetHasher();
+    const std::string& seriesId = hasher.HashSeries();
+    const std::string& instanceId = hasher.HashInstance();
+
+    if (targetSeries_.size() > 0 && targetSeries_.find(seriesId) == targetSeries_.end())
+    {
+      return false;
+    }
+
+    if (targetInstances_.size() > 0 && targetInstances_.find(instanceId) == targetInstances_.end())
+    {
+      return false;
+    }
+
+    return true;
+  }
+
+  bool DicomPixelMasker::Region2D::GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const
+  {
+    if (IsTargeted(file))
+    {
+      x1 = x1_;
+      y1 = y1_;
+      x2 = x2_;
+      y2 = y2_;
+      return true;
+    }
+
+    return false;
+  }
+
+  static void GetDoubleVector(std::vector<double>& target, const std::string& strValue, const DicomTag& tag, size_t expectedSize)
+  {
+    target.clear();
+
+    std::vector<std::string> strVector;
+    Toolbox::SplitString(strVector, strValue, '\\');
+
+    if (strVector.size() != expectedSize)
+    {
+      throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, tag " + tag.Format() + " length is invalid");
+    }
+
+    for (size_t i = 0; i < strVector.size(); ++i)
+    {
+      try
+      {
+        target.push_back(boost::lexical_cast<double>(strVector[i]));
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, tag " + tag.Format() + " contains invalid value " + strVector[i]);
+      }
+    }
+  }
+
+  static void GetDoubleVector(std::vector<double>& target, const ParsedDicomFile& file, const DicomTag& tag, size_t expectedSize)
+  {
+    std::string str;
+    if (!file.GetTagValue(str, tag))
+    {
+      throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing tag" + tag.Format());
+    }
+
+    GetDoubleVector(target, str, tag, expectedSize);
+  }
+
+  bool DicomPixelMasker::Region3D::GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const
+  {
+    if (IsTargeted(file))
+    {
+      DicomMap tags;
+      file.ExtractDicomSummary(tags, 256);
+
+      std::vector<double> imagePositionPatient;
+      std::vector<double> imageOrientationPatient;
+      std::vector<double> pixelSpacing;
+      double sliceSpacing = 0.0;
+
+      if (file.HasTag(DICOM_TAG_IMAGE_POSITION_PATIENT) && file.HasTag(DICOM_TAG_IMAGE_ORIENTATION_PATIENT))
+      {
+        GetDoubleVector(imagePositionPatient, file, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
+        GetDoubleVector(imageOrientationPatient, file, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6);
+      }
+      else if (file.HasTag(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE)) // find it in the detector info sequence (for some multi frame instances like NM or scintigraphy) TODO-PIXEL-ANON: to validate
+      {
+        const Json::Value& jsonSequence = tags.GetValue(DICOM_TAG_DETECTOR_INFORMATION_SEQUENCE).GetSequenceContent();
+        if (jsonSequence.size() == 1)
+        {
+          std::string strImagePositionPatient = jsonSequence[0]["0020,0032"]["Value"].asString();
+          std::string strImageOrientationPatient = jsonSequence[0]["0020,0037"]["Value"].asString();
+
+          GetDoubleVector(imagePositionPatient, strImagePositionPatient, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
+          GetDoubleVector(imageOrientationPatient, strImageOrientationPatient, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient in DetectorInformationSequence, invalid sequence size");
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError, "Unable to find ImagePositionPatient or ImageOrientationPatient");
+      }
+
+      GetDoubleVector(pixelSpacing, file, DICOM_TAG_PIXEL_SPACING, 2);
+
+      if (file.HasTag(DICOM_TAG_SPACING_BETWEEN_SLICES))
+      {
+        std::string strSliceSpacing;
+        if (file.GetTagValue(strSliceSpacing, DICOM_TAG_SPACING_BETWEEN_SLICES))
+        {
+          sliceSpacing = boost::lexical_cast<double>(strSliceSpacing);
+        }
+      }
+
+      double z = imagePositionPatient[2];
+      
+      if (sliceSpacing != 0.0)
+      {
+        z = z - frameIndex * sliceSpacing;
+      }
+
+      // note: To simplify, for the z, we only check that imagePositionPatient is between the authorized z values.
+      //       This won't be perfectly true for weird images with slices that are not parallel but let's wait for someone to complain ...
+      if (z < std::min(z1_, z2_) || 
+          z > std::max(z1_, z2_))
+      {
+        return false;
+      }
+      
+
+      double deltaX1 = x1_ - imagePositionPatient[0];
+      double deltaY1 = y1_ - imagePositionPatient[1];
+      double deltaZ1 = z1_ - z;
+      double deltaX2 = x2_ - imagePositionPatient[0];
+      double deltaY2 = y2_ - imagePositionPatient[1];
+      double deltaZ2 = z2_ - z;
+
+      double ix1 = (deltaX1 * imageOrientationPatient[0] + deltaY1 * imageOrientationPatient[1] + deltaZ1 * imageOrientationPatient[2]) / pixelSpacing[0];
+      double iy1 = (deltaX1 * imageOrientationPatient[3] + deltaY1 * imageOrientationPatient[4] + deltaZ1 * imageOrientationPatient[5]) / pixelSpacing[1];
+      double ix2 = (deltaX2 * imageOrientationPatient[0] + deltaY2 * imageOrientationPatient[1] + deltaZ2 * imageOrientationPatient[2]) / pixelSpacing[0];
+      double iy2 = (deltaX2 * imageOrientationPatient[3] + deltaY2 * imageOrientationPatient[4] + deltaZ2 * imageOrientationPatient[5]) / pixelSpacing[1];
+
+      std::string strRows;
+      std::string strColumns;
+
+      if (!file.GetTagValue(strRows, DICOM_TAG_ROWS) || !file.GetTagValue(strColumns, DICOM_TAG_COLUMNS))
+      {
+        throw OrthancException(ErrorCode_InexistentTag, "Unable to perform 3D -> 2D conversion, missing ROWS or COLUMNS tag");
+      }
+
+      // clip on image size
+      double rows = boost::lexical_cast<double>(strRows);
+      double columns = boost::lexical_cast<double>(strColumns);
+
+      x1 = static_cast<unsigned int>(std::max(0.0, std::min(ix1, ix2)));
+      y1 = static_cast<unsigned int>(std::max(0.0, std::min(iy1, iy2)));
+      x2 = static_cast<unsigned int>(std::min(columns, std::max(ix1, ix2)));
+      y2 = static_cast<unsigned int>(std::min(rows, std::max(iy1, iy2)));
+
+      return true;
+    }
+    
+    return false;
+  }
+
+  void DicomPixelMasker::Apply(ParsedDicomFile& toModify)
+  {
+    for (std::list<BaseRegion*>::const_iterator itr = regions_.begin(); itr != regions_.end(); ++itr)
+    {
+      const BaseRegion* r = *itr;
+
+      for (unsigned int i = 0; i < toModify.GetFramesCount(); ++i)
+      {
+        unsigned int x1, y1, x2, y2;
+
+        if (r->GetPixelMaskArea(x1, y1, x2, y2, toModify, i))
+        {
+          ImageAccessor imageRegion;
+          toModify.GetRawFrame(i)->GetRegion(imageRegion, x1, y1, x2 - x1, y2 - y1);
+
+          if (r->GetMode() == DicomPixelMaskerMode_MeanFilter)
+          {
+            ImageProcessing::MeanFilter(imageRegion, r->GetFilterWidth(), r->GetFilterWidth());
+          }
+          else if (r->GetMode() == DicomPixelMaskerMode_Fill)
+          {
+            ImageProcessing::Set(imageRegion, r->GetFillValue());
+          }
+        }
+      }
+    }
+  }
+
+  void DicomPixelMasker::ParseRequest(const Json::Value& request)
+  {
+    if (request.isMember(KEY_MASK_PIXELS) && request[KEY_MASK_PIXELS].isObject())
+    {
+      const Json::Value& maskPixelsJson = request[KEY_MASK_PIXELS];
+
+      if (maskPixelsJson.isMember(KEY_REGIONS) && maskPixelsJson[KEY_REGIONS].isArray())
+      {
+        const Json::Value& regionsJson = maskPixelsJson[KEY_REGIONS];
+
+        for (Json::ArrayIndex i = 0; i < regionsJson.size(); ++i)
+        {
+          const Json::Value& regionJson = regionsJson[i];
+
+          std::unique_ptr<BaseRegion> region;
+          
+          if (regionJson.isMember(KEY_REGION_TYPE) && regionJson[KEY_REGION_TYPE].isString())
+          {
+            if (regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_2D)
+            {
+              if (regionJson.isMember(KEY_ORIGIN) && regionJson[KEY_ORIGIN].isArray() && regionJson[KEY_ORIGIN].size() == 2 &&
+                  regionJson.isMember(KEY_END) && regionJson[KEY_END].isArray() && regionJson[KEY_END].size() == 2)
+              {
+                unsigned int x = regionJson[KEY_ORIGIN][0].asUInt();
+                unsigned int y = regionJson[KEY_ORIGIN][1].asUInt();
+                unsigned int width = regionJson[KEY_END][0].asUInt() - x;
+                unsigned int height = regionJson[KEY_END][1].asUInt() - y;
+                
+                region.reset(new Region2D(x, y, width, height));
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadFileFormat, "2D Region: invalid coordinates");
+              }
+
+            }
+            else if (regionJson[KEY_REGION_TYPE].asString() == KEY_REGION_3D)
+            {
+              if (regionJson.isMember(KEY_ORIGIN) && regionJson[KEY_ORIGIN].isArray() && regionJson[KEY_ORIGIN].size() == 3 &&
+                  regionJson.isMember(KEY_END) && regionJson[KEY_END].isArray() && regionJson[KEY_END].size() == 3)
+              {
+                double x1 = regionJson[KEY_ORIGIN][0].asDouble();
+                double y1 = regionJson[KEY_ORIGIN][1].asDouble();
+                double z1 = regionJson[KEY_ORIGIN][2].asDouble();
+                double x2 = regionJson[KEY_END][0].asDouble();
+                double y2 = regionJson[KEY_END][1].asDouble();
+                double z2 = regionJson[KEY_END][2].asDouble();
+                
+                region.reset(new Region3D(x1, y1, z1, x2, y2, z2));
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_BadFileFormat, "2D Region: invalid coordinates");
+              }
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadFileFormat, std::string(KEY_REGION_TYPE) + " unrecognized value '" + regionJson[KEY_REGION_TYPE].asString() +"'");
+            }
+          }
+
+          if (regionJson.isMember(KEY_MASK_TYPE) && regionJson[KEY_MASK_TYPE].isString())
+          {
+            if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_FILL)
+            {
+              if (regionJson.isMember(KEY_FILL_VALUE) && regionJson[KEY_FILL_VALUE].isInt())
+              {
+                region->SetFillValue(regionJson[KEY_FILL_VALUE].asInt());
+              }
+            }
+            else if (regionJson[KEY_MASK_TYPE].asString() == KEY_MASK_TYPE_MEAN_FILTER)
+            {
+              if (regionJson.isMember(KEY_FILTER_WIDTH) && regionJson[KEY_FILTER_WIDTH].isUInt())
+              {
+                region->SetMeanFilter(regionJson[KEY_FILTER_WIDTH].asUInt());
+              }
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadFileFormat, std::string(KEY_MASK_TYPE) + " should be '" + KEY_MASK_TYPE_FILL +"' or '" + KEY_MASK_TYPE_MEAN_FILTER + "'.");
+            }
+          }
+
+          if (regionJson.isMember(KEY_TARGET_SERIES) && regionJson[KEY_TARGET_SERIES].isArray())
+          {
+            std::set<std::string> targetSeries;
+            SerializationToolbox::ReadSetOfStrings(targetSeries, regionJson, KEY_TARGET_SERIES);
+            region->SetTargetSeries(targetSeries);
+          }
+
+          if (regionJson.isMember(KEY_TARGET_INSTANCES) && regionJson[KEY_TARGET_INSTANCES].isArray())
+          {
+            std::set<std::string> targetInstances;
+            SerializationToolbox::ReadSetOfStrings(targetInstances, regionJson, KEY_TARGET_INSTANCES);
+            region->SetTargetInstances(targetInstances);
+          }
+
+          regions_.push_back(region.release());
+        }
+      }
+    }
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DicomPixelMasker.h	Tue Nov 25 13:54:52 2025 +0100
@@ -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-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 "ParsedDicomFile.h"
+#include "../Images/ImageProcessing.h"
+
+#include <set>
+
+
+namespace Orthanc
+{
+  enum DicomPixelMaskerMode
+  {
+    DicomPixelMaskerMode_Fill,
+    DicomPixelMaskerMode_MeanFilter,
+
+    DicomPixelMaskerMode_Undefined
+  };
+
+  class ORTHANC_PUBLIC DicomPixelMasker : public boost::noncopyable
+  {
+    class BaseRegion
+    {
+      DicomPixelMaskerMode    mode_;
+      int32_t                 fillValue_;     // pixel value
+      uint32_t                filterWidth_;   // filter width
+      std::set<std::string>   targetSeries_;
+      std::set<std::string>   targetInstances_;
+
+    protected:
+      bool IsTargeted(const ParsedDicomFile& file) const;
+      BaseRegion();
+
+    public:
+      
+      virtual ~BaseRegion()
+      {
+      }
+
+      virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const = 0;
+
+      DicomPixelMaskerMode GetMode() const
+      {
+        return mode_;
+      }
+
+      int32_t GetFillValue() const
+      {
+        assert(mode_ == DicomPixelMaskerMode_Fill);
+        return fillValue_;
+      }
+
+      int32_t GetFilterWidth() const
+      {
+        assert(mode_ == DicomPixelMaskerMode_MeanFilter);
+        return filterWidth_;
+      }
+
+      void SetFillValue(int32_t value)
+      {
+        mode_ = DicomPixelMaskerMode_Fill;
+        fillValue_ = value;
+      }
+
+      void SetMeanFilter(uint32_t value)
+      {
+        mode_ = DicomPixelMaskerMode_MeanFilter;
+        filterWidth_ = value;
+      }
+
+      void SetTargetSeries(const std::set<std::string> targetSeries)
+      {
+        targetSeries_ = targetSeries;
+      }
+
+      void SetTargetInstances(const std::set<std::string> targetInstances)
+      {
+        targetInstances_ = targetInstances;
+      }
+    };
+
+    class Region2D : public BaseRegion
+    {
+      unsigned int            x1_;
+      unsigned int            y1_;
+      unsigned int            x2_;
+      unsigned int            y2_;
+
+    public:
+      Region2D(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2);
+      
+      virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const ORTHANC_OVERRIDE;
+    };
+
+    class Region3D : public BaseRegion
+    {
+      double             x1_;
+      double             y1_;
+      double             z1_;
+      double             x2_;
+      double             y2_;
+      double             z2_;
+
+    public:
+      Region3D(double x1, double y1, double z1, double x2, double y2, double z2);
+
+      virtual bool GetPixelMaskArea(unsigned int& x1, unsigned int& y1, unsigned int& x2, unsigned int& y2, const ParsedDicomFile& file, unsigned int frameIndex) const ORTHANC_OVERRIDE;
+    };
+
+  private:
+    std::list<BaseRegion*>   regions_;
+
+  public:
+    DicomPixelMasker();
+    
+    ~DicomPixelMasker();
+
+    void Apply(ParsedDicomFile& toModify);
+
+    void ParseRequest(const Json::Value& request);
+  };
+}
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Tue Nov 25 13:54:52 2025 +0100
@@ -226,6 +226,28 @@
         fragment = dynamic_cast<DcmPixelItem*>(pixelSequence_->nextInContainer(fragment));
       }
     }
+
+    virtual uint8_t* GetRawFrameBuffer(unsigned int index)
+    {
+      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,12 @@
         memcpy(&frame[0], pixelData_ + index * frameSize_, frameSize_);
       }
     }
+
+    virtual uint8_t* GetRawFrameBuffer(unsigned int index)
+    {
+      return pixelData_ + index * frameSize_;
+    }
+
   };
 
 
@@ -308,6 +336,12 @@
         memcpy(&frame[0], reinterpret_cast<const uint8_t*>(&pixelData_[0]) + index * frameSize_, frameSize_);
       }
     }
+
+    virtual uint8_t* GetRawFrameBuffer(unsigned int index)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
   };
 
 
@@ -415,4 +449,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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Tue Nov 25 13:54:52 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);
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Nov 25 13:54:52 2025 +0100
@@ -1824,6 +1824,46 @@
     }
   }
 
+  ImageAccessor* ParsedDicomFile::GetRawFrame(unsigned int frame)
+  {
+    E_TransferSyntax transferSyntax = GetDcmtkObjectConst().getDataset()->getCurrentXfer();
+    if (transferSyntax != EXS_LittleEndianImplicit && 
+        transferSyntax != EXS_BigEndianImplicit && 
+        transferSyntax != EXS_LittleEndianExplicit && 
+        transferSyntax != EXS_BigEndianExplicit)
+    {
+      throw OrthancException(ErrorCode_NotImplemented, "ParseDicomFile::GetRawFrame only works with uncompressed transfer syntaxes");
+    }
+
+    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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Tue Nov 25 13:54:52 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* GetRawFrame(unsigned int frame);
+
     void InjectEmptyPixelData(ValueRepresentation vr);
 
     // Remove all the tags after pixel data
--- a/OrthancFramework/Sources/Enumerations.h	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/Enumerations.h	Tue Nov 25 13:54:52 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);
@@ -966,6 +973,12 @@
   DicomTransferSyntax GetTransferSyntax(const std::string& uid);
 
   ORTHANC_PUBLIC
+  bool IsRawTransferSyntax(DicomTransferSyntax syntax);
+
+  ORTHANC_PUBLIC
+  bool IsLossyTransferSyntax(DicomTransferSyntax syntax);
+
+  ORTHANC_PUBLIC
   const char* GetResourceTypeText(ResourceType type,
                                   bool isPlural,
                                   bool isUpperCase);
--- a/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h	Tue Nov 25 13:54:52 2025 +0100
@@ -602,4 +602,276 @@
     target.insert(DicomTransferSyntax_RFC2557MimeEncapsulation);
     target.insert(DicomTransferSyntax_XML);
   }
+
+
+  bool IsLossyTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        return false;
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess1:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess15:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess28:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess29:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        return true;
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000:
+        return true;
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        return true;
+
+      case DicomTransferSyntax_JPIPReferenced:
+        return false;
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        return false;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        return true;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        return true;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        return true;
+
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        return true;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        return true;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        return true;
+
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        return true;
+
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        return true;
+
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        return true;
+
+      case DicomTransferSyntax_RLELossless:
+        return false;
+
+      case DicomTransferSyntax_RFC2557MimeEncapsulation:
+        return true;
+
+      case DicomTransferSyntax_XML:
+        return true;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool IsRawTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        return true;
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        return true;
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess1:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess14:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess15:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess28:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess29:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        return false;
+
+      case DicomTransferSyntax_JPIPReferenced:
+        return false;
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        return false;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        return false;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        return false;
+
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        return false;
+
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        return false;
+
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        return false;
+
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        return false;
+
+      case DicomTransferSyntax_RLELossless:
+        return false;
+
+      case DicomTransferSyntax_RFC2557MimeEncapsulation:
+        return false;
+
+      case DicomTransferSyntax_XML:
+        return false;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 }
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/Sources/Images/ImageProcessing.h	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Nov 25 13:54:52 2025 +0100
@@ -5792,11 +5792,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/Include/orthanc/OrthancCPlugin.h	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Nov 25 13:54:52 2025 +0100
@@ -10710,6 +10710,20 @@
     return context->InvokeService(context, _OrthancPluginService_RegisterAuditLogHandler, &params);
   }
 
+  // TODO-PIXEL-ANON: cleanup (or implement ;-) )
+  // ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterCustomModifier(
+  //   OrthancPluginContext*                  context,
+  //   OrthancPluginCustomModifierFactory     factory,    // signature like CreateCustomModifier(void* output, 
+  //                                                      //                                     const void* request, uint64_t requestSize // the full json request or only the "CustomModifier part" ?
+  //                                                      //                                     const char* resourceId, ResourceLevel ..  // that would allow the plugin e.g. to detect the areas to clear by e.g: detecting the patient face ...
+  //                                                      // )
+  //                                                      // Creates a CustomModifier from: 
+  //                                                      // - either a request payload 
+  //                                                      // - or, from a serialized modification job (the job would serialize its creation request.  This way, we don't need to implement serialize/unserialize in the CustomModifier)
+  //   OrthancPluginApplyCustomModifier       apply,      // signature like OrthancPluginDicomInstance* ApplyCustomModifier(void* modifier, OrthancPluginDicomInstance* source)
+  //   OrthancPluginFreeCustomModifier        free,       // signature like void FreeCustomModifier(void* modifier)
+  // )
+  
 
   typedef struct
   {
--- a/OrthancServer/Plugins/Samples/CppSkeleton/CMakeLists.txt	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Plugins/Samples/CppSkeleton/CMakeLists.txt	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Plugins/Samples/CppSkeleton/Resources/SyncOrthancFolder.py	Tue Nov 25 13:54:52 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/Sources/OrthancGetRequestHandler.cpp	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Nov 25 13:54:52 2025 +0100
@@ -219,35 +219,9 @@
       ServerContext::DicomCacheLocker locker(context, id);
       modified.reset(locker.GetDicom().Clone(true));
     }
-    
-    modification.Apply(*modified);
-
-    if (transcode)
-    {
-      IDicomTranscoder::DicomImage source;
-      source.AcquireParsed(*modified);  // "modified" is invalid below this point
-      
-      IDicomTranscoder::DicomImage transcoded;
-
-      std::set<DicomTransferSyntax> s;
-      s.insert(targetSyntax);
-      
-      if (context.Transcode(transcoded, source, s, true, lossyQuality))
-      {      
-        call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
-                                      transcoded.GetBufferSize(), MimeType_Dicom);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot transcode to transfer syntax: " +
-                               std::string(GetTransferSyntaxUid(targetSyntax)));
-      }
-    }
-    else
-    {
-      modified->Answer(call.GetOutput());
-    }
+  
+    context.Modify(modified, modification, transcode, targetSyntax, lossyQuality, false /* keepSOPInstanceUidDuringLossyTranscoding*/);
+    modified->Answer(call.GetOutput());
   }
 
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Nov 25 13:54:52 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());
 
@@ -1942,7 +1943,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);
@@ -2016,7 +2017,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());
@@ -2032,7 +2033,7 @@
   bool ServerContext::Transcode(DicomImage& target,
                                 DicomImage& source /* in, "GetParsed()" possibly modified */,
                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                bool allowNewSopInstanceUid)
+                                TranscodingSopInstanceUidMode mode)
   {
     unsigned int lossyQuality;
 
@@ -2041,19 +2042,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;
       }
@@ -2063,7 +2064,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;
       }
@@ -2077,7 +2078,7 @@
 
     if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
     {
-      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid, lossyQuality);
+      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, mode, lossyQuality);
     }
     else
     {
@@ -2085,6 +2086,100 @@
     }
   }
 
+  void ServerContext::Modify(std::unique_ptr<ParsedDicomFile>& dicomFile, 
+                             DicomModification& modification,
+                             bool transcode,
+                             DicomTransferSyntax targetSyntax,
+                             unsigned int lossyQuality,
+                             bool keepSOPInstanceUidDuringLossyTranscoding)
+  {
+    const std::string originalSopInstanceUid = IDicomTranscoder::GetSopInstanceUid(dicomFile->GetDcmtkObject());  
+    std::string forceModifiedSopInstanceUid;
+
+    // do we need to transcode before applying e.g custom pixels modifications ?
+    DicomTransferSyntax currentTransferSyntax;
+    if (// modification.RequiresUncompressedTransferSyntax() &&   // TODO-PIXEL-ANON
+        dicomFile->LookupTransferSyntax(currentTransferSyntax) &&
+        !IsRawTransferSyntax(currentTransferSyntax))
+    {
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(*dicomFile);  // "dicomFile" is invalid below this point
+
+      IDicomTranscoder::DicomImage transcoded;
+
+      std::set<DicomTransferSyntax> uncompressedTransferSyntax;
+      uncompressedTransferSyntax.insert(DicomTransferSyntax_LittleEndianExplicit);
+      Transcode(transcoded, source, uncompressedTransferSyntax, TranscodingSopInstanceUidMode_AllowNew);
+
+      dicomFile.reset(transcoded.ReleaseAsParsedDicomFile());
+
+      if (IsLossyTransferSyntax(currentTransferSyntax))
+      {
+        // TODO-PIXEL-ANON:  Test this path with IngestTranscoding = lossy syntax
+        // TODO-PIXEL-ANON:  + Test with source = lossy + modification requires a transcoding to a raw TS -> the transcoding shall not be performed after pixel modification since it is already being done now but the SOPInstance UID must be changed afterwards
+        
+        // this means we have moved from lossy to raw -> the SOPInstanceUID should have changed here but, 
+        // let's keep the SOPInstanceUID unchanged during this pre-transcoding to make sure the orthanc ids are 
+        // still the original ones when the pixelMasker is applied (since the pixelMasker has a filter on Orthanc ids).
+        // however, after the modification, we must make sure that we change the SOPInstanceUID.
+        if (dicomFile.get() && dicomFile->GetDcmtkObject().getDataset())
+        {
+          const char* sopInstanceUid;
+          if (dicomFile->GetDcmtkObject().getDataset()->findAndGetString(DCM_SOPInstanceUID, sopInstanceUid, OFFalse).good())
+          {
+            forceModifiedSopInstanceUid = sopInstanceUid;
+            dicomFile->GetDcmtkObject().getDataset()->putAndInsertString(DCM_SOPInstanceUID, originalSopInstanceUid.c_str(), OFTrue /* replace */);
+          }
+        }
+      }
+
+      if (!transcode) // if we had to change the TS for the modification, we will need to restore the original TS afterwards
+      {
+        transcode = true;
+        targetSyntax = currentTransferSyntax;
+      }
+    }
+
+    modification.Apply(dicomFile);
+
+    if (transcode)
+    {
+      const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(dicomFile->GetDcmtkObject());
+
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(*dicomFile);  // "dicomFile" is invalid below this point
+      
+      IDicomTranscoder::DicomImage transcoded;
+
+      std::set<DicomTransferSyntax> s;
+      s.insert(targetSyntax);
+
+      TranscodingSopInstanceUidMode mode;
+      if (keepSOPInstanceUidDuringLossyTranscoding)
+      {
+        mode = TranscodingSopInstanceUidMode_Preserve;
+      }
+      else
+      {
+        mode = TranscodingSopInstanceUidMode_AllowNew;
+      }
+
+      if (Transcode(transcoded, source, s, mode, lossyQuality))
+      {
+        dicomFile.reset(transcoded.ReleaseAsParsedDicomFile());
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot transcode to transfer syntax " +
+                               std::string(GetTransferSyntaxUid(targetSyntax)));
+      }
+    }
+    // TODO-PIXEL-ANON: set forceModifiedSopInstanceUid if required
+  }
+
+
+
   const std::string& ServerContext::GetDeidentifiedContent(const DicomElement &element) const
   {
     static const std::string redactedContent = "*** POTENTIAL PHI ***";
--- a/OrthancServer/Sources/ServerContext.h	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Tue Nov 25 13:54:52 2025 +0100
@@ -590,12 +590,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,
@@ -604,6 +604,13 @@
                                     const std::string& attachmentId, // for the storage cache
                                     DicomTransferSyntax targetSyntax);
 
+    void Modify(std::unique_ptr<ParsedDicomFile>& toModify, 
+                DicomModification& modification,
+                bool transcode,
+                DicomTransferSyntax targetSyntax,
+                unsigned int lossyQuality,
+                bool keepSOPInstanceUidDuringLossyTranscoding);
+
     bool IsTranscodeDicomProtocol() const
     {
       return transcodeDicomProtocol_;
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Nov 25 13:54:52 2025 +0100
@@ -92,7 +92,7 @@
 
     try
     {
-      modification_->Apply(*modified);
+      context_.Modify(modified, *modification_, false, DicomTransferSyntax_LittleEndianExplicit /* not used */, 100 /* not used */, false /*keepSOPInstanceUidDuringLossyTranscoding*/);
 
       std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(*modified));
       assert(origin_ == RequestOrigin_Lua);
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Tue Nov 25 13:54:52 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	Mon Nov 24 15:37:02 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Tue Nov 25 13:54:52 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);
+      GetContext().Modify(modified, *modification_, transcode_, transferSyntax_, 100 /* not used */, true /* keepSOPInstanceUidDuringLossyTranscoding*/);  // TODO-PIXEL-ANON: get lossy quality from ???
 
       if (modification_->AreLabelsKept())
       {
@@ -252,36 +252,6 @@
 
     const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject());
     
-    if (transcode_)
-    {
-      std::set<DicomTransferSyntax> syntaxes;
-      syntaxes.insert(transferSyntax_);
-
-      IDicomTranscoder::DicomImage source;
-      source.AcquireParsed(*modified);  // "modified" is invalid below this point
-      
-      IDicomTranscoder::DicomImage transcoded;
-      if (GetContext().Transcode(transcoded, source, syntaxes, true))
-      {
-        modified.reset(transcoded.ReleaseAsParsedDicomFile());
-
-        // Fix the SOP instance UID in order the preserve the
-        // references between instance UIDs in the DICOM hierarchy
-        // (the UID might have changed in the case of lossy transcoding)
-        if (modified.get() == NULL ||
-            modified->GetDcmtkObject().getDataset() == NULL ||
-            !modified->GetDcmtkObject().getDataset()->putAndInsertString(
-              DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good())
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-      else
-      {
-        LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance;
-        modified.reset(source.ReleaseAsParsedDicomFile());
-      }
-    }
 
     assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()));