Mercurial > hg > orthanc
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, ¶meters)) { + fixer.Apply(dicom); selectedSyntax = DicomTransferSyntax_JPEGProcess1; return true; } @@ -199,6 +208,7 @@ DJ_RPLossy parameters(lossyQuality); if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, ¶meters)) { + fixer.Apply(dicom); selectedSyntax = DicomTransferSyntax_JPEGProcess2_4; return true; } @@ -215,6 +225,7 @@ 0 /* opt_point_transform */); if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, ¶meters)) { + fixer.Apply(dicom); selectedSyntax = DicomTransferSyntax_JPEGProcess14; return true; } @@ -271,6 +282,7 @@ **/ if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, ¶meters)) { + 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, ¶ms); } + // 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()));
