changeset 807:bfbadbfae1e2 pixel-anon

merged default -> pixel-anon
author Alain Mazy <am@orthanc.team>
date Thu, 10 Apr 2025 16:33:10 +0200
parents 8dfe8f2590b0 (current diff) b047f7e84741 (diff)
children
files
diffstat 8 files changed, 132 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Tue Mar 25 15:30:05 2025 +0100
+++ b/.hgtags	Thu Apr 10 16:33:10 2025 +0200
@@ -47,3 +47,4 @@
 7bfc8992ab8fc44bd811bc60ebf3332303bc87ed Orthanc-1.12.4
 847b3c6b360b9b0daeab327133703c60e14e51f0 Orthanc-1.12.5
 287aae544b3133f6cecdf768f5a09dacbd75cf91 Orthanc-1.12.6
+2eca398d9676e2378343c48769a4b3938ba96005 Orthanc-1.12.7
--- a/CITATION.cff	Tue Mar 25 15:30:05 2025 +0100
+++ b/CITATION.cff	Thu Apr 10 16:33:10 2025 +0200
@@ -10,5 +10,5 @@
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.6
-date-released: 2025-01-22
+version: 1.12.7
+date-released: 2025-04-07
Binary file Database/TransferSyntaxes/1.2.840.10008.1.2.1.99.dcm has changed
--- a/NewTests/Authorization/test_authorization.py	Tue Mar 25 15:30:05 2025 +0100
+++ b/NewTests/Authorization/test_authorization.py	Thu Apr 10 16:33:10 2025 +0200
@@ -341,6 +341,16 @@
             o.upload_files_dicom_web(paths = [here / "../../Database/Beaufix/IM-0001-0001.dcm"])
             o_admin.instances.delete(orthanc_ids=instances_ids)
 
+        if o_admin.is_plugin_version_at_least("authorization", 0, 9, 1):
+
+            # uploader-a shall not be able to upload a study through DICOMWeb using /dicom-web/studies/<StudyInstanceUID of label_b>
+            self.assert_is_forbidden(lambda: o.upload_files_dicom_web(paths = [here / "../../Database/Knix/Loc/IM-0001-0002.dcm"], endpoint=f"/dicom-web/studies/{self.label_b_study_dicom_id}"))
+
+            # uploader-a shall be able to upload a study through DICOMWeb using /dicom-web/studies/<StudyInstanceUID of label_a>
+            o.upload_files_dicom_web(paths = [here / "../../Database/Knix/Loc/IM-0001-0002.dcm"], endpoint=f"/dicom-web/studies/{self.label_a_study_dicom_id}")
+
+            # note that, uploader-a is allowed to upload to /dicom-web/studies without checking any labels :-()
+            o.upload_files_dicom_web(paths = [here / "../../Database/Knix/Loc/IM-0001-0002.dcm"], endpoint=f"/dicom-web/studies")
 
     def test_resource_token(self):
 
--- a/NewTests/PostgresUpgrades/test_pg_upgrades.py	Tue Mar 25 15:30:05 2025 +0100
+++ b/NewTests/PostgresUpgrades/test_pg_upgrades.py	Thu Apr 10 16:33:10 2025 +0200
@@ -71,6 +71,8 @@
         subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-under-tests"], check=True)
         time.sleep(2)
 
+    
+    # test a full upgrade from a very old version, and a downgrade to the previous version (where we run the integration tests)
     def test_upgrades_downgrades_with_pg_15(self):
 
         # remove everything including the DB from previous tests
@@ -144,10 +146,11 @@
         o.wait_started()
 
         # time.sleep(10000)
-        subprocess.run(["docker", "compose", "up", "orthanc-tests"], check=True)
+        subprocess.run(["docker", "compose", "up", "orthanc-tests", "--exit-code-from", "orthanc-tests"], check=True)
 
 
 
+    # make sure we can still start a new Orthanc with an Old PG server
     def test_latest_orthanc_with_pg_9(self):
 
         # remove everything including the DB from previous tests
--- a/NewTests/requirements.txt	Tue Mar 25 15:30:05 2025 +0100
+++ b/NewTests/requirements.txt	Thu Apr 10 16:33:10 2025 +0200
@@ -1,3 +1,3 @@
-orthanc-api-client>=0.18.4
+orthanc-api-client>=0.18.5
 orthanc-tools>=0.13.0
 uvicorn
\ No newline at end of file
--- a/Plugins/Transfers/Run.py	Tue Mar 25 15:30:05 2025 +0100
+++ b/Plugins/Transfers/Run.py	Thu Apr 10 16:33:10 2025 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
 
--- a/Tests/Tests.py	Tue Mar 25 15:30:05 2025 +0100
+++ b/Tests/Tests.py	Thu Apr 10 16:33:10 2025 +0200
@@ -2562,30 +2562,30 @@
         a = ExtractDicomTags(Anonymize(u, { 'PatientName' : 'toto' }), tags)
         for i in range(4):
             self.assertNotEqual(ids[i], a[i])
-        self.assertFalse(a[4].startswith('Orthanc'))
+        self.assertNotIn('PS 3.15', a[4])
 
         a = ExtractDicomTags(Anonymize(u, { 'SOPInstanceUID' : 'instance' }), tags)
         self.assertEqual('instance', a[3])
-        self.assertFalse(a[4].startswith('Orthanc'))
+        self.assertNotIn('PS 3.15', a[4])
 
         a = ExtractDicomTags(Anonymize(u, { 'SeriesInstanceUID' : 'series' }), tags)
         self.assertEqual('series', a[2])
-        self.assertFalse(a[4].startswith('Orthanc'))
+        self.assertNotIn('PS 3.15', a[4])
 
         a = ExtractDicomTags(Anonymize(u, { 'StudyInstanceUID' : 'study' }), tags)
         self.assertEqual('study', a[1])
-        self.assertFalse(a[4].startswith('Orthanc'))
+        self.assertNotIn('PS 3.15', a[4])
 
         a = ExtractDicomTags(Anonymize(u, { 'PatientID' : 'patient' }), tags)
         self.assertEqual('patient', a[0])
-        self.assertFalse(a[4].startswith('Orthanc'))
+        self.assertNotIn('PS 3.15', a[4])
 
         a = ExtractDicomTags(Anonymize(u, { 'PatientID' : 'patient',
                                             'StudyInstanceUID' : 'study',
                                             'SeriesInstanceUID' : 'series',
                                             'SOPInstanceUID' : 'instance' }), tags)
         self.assertEqual('patient', a[0])
-        self.assertFalse(a[4].startswith('Orthanc'))
+        self.assertNotIn('PS 3.15', a[4])
 
         self.assertEqual(1, len(DoGet(_REMOTE, '/instances')))
 
@@ -6702,7 +6702,6 @@
         SYNTAXES = [
             '1.2.840.10008.1.2',        
             '1.2.840.10008.1.2.1',
-            '1.2.840.10008.1.2.1.99',  # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5)
             '1.2.840.10008.1.2.2',
             '1.2.840.10008.1.2.4.50',
             '1.2.840.10008.1.2.4.51',
@@ -6710,6 +6709,10 @@
             '1.2.840.10008.1.2.4.70',
         ]
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+            SYNTAXES.append('1.2.840.10008.1.2.1.99')  # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5)
+
+
         if HasGdcmPlugin(_REMOTE):
             SYNTAXES = SYNTAXES + [
                 '1.2.840.10008.1.2.4.80',  # This makes DCMTK 3.6.2 crash
@@ -7377,7 +7380,6 @@
         SYNTAXES = [
             '1.2.840.10008.1.2',        
             '1.2.840.10008.1.2.1',
-            '1.2.840.10008.1.2.1.99',  # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5)
             '1.2.840.10008.1.2.2',
             '1.2.840.10008.1.2.4.50',
             '1.2.840.10008.1.2.4.51',
@@ -7385,6 +7387,11 @@
             '1.2.840.10008.1.2.4.70',
         ]
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+            SYNTAXES.append('1.2.840.10008.1.2.1.99')  # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5)
+
+
+
         if HasGdcmPlugin(_REMOTE):
             SYNTAXES = SYNTAXES + [
                 '1.2.840.10008.1.2.4.80',  # This makes DCMTK 3.6.2 crash
@@ -8356,6 +8363,7 @@
         tags2021b = GetTags(study, { 'DicomVersion' : '2021b' })
         tags2023b = GetTags(study, { 'DicomVersion' : '2023b' })
         tagsDefault = GetTags(study, {})
+        tagsReplace = GetTags(study, { 'Replace' : { 'StationName': 'tutu' }})
 
         orthancVersion = DoGet(_REMOTE, '/system') ['Version']
         if orthancVersion.startswith('mainline-'):  # happens in unstable orthancteam/orthanc images
@@ -8365,6 +8373,9 @@
         self.assertEqual('Orthanc %s - PS 3.15-2017c Table E.1-1 Basic Profile' % orthancVersion, tags2017c['0012,0063'])
         self.assertEqual('Orthanc %s - PS 3.15-2021b Table E.1-1 Basic Profile' % orthancVersion, tags2021b['0012,0063'])
         self.assertEqual('Orthanc %s - PS 3.15-2023b Table E.1-1 Basic Profile' % orthancVersion, tags2023b['0012,0063'])
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+            self.assertEqual('Orthanc %s' % orthancVersion, tagsReplace['0012,0063'])
+
         self.assertEqual(tagsDefault['0012,0063'], tags2023b['0012,0063'])
 
         self.assertEqual(len(tags2021b), len(tags2023b))
@@ -11953,3 +11964,97 @@
                                                     ]
                                                 })
             self.assertEqual(1, len(a))
+
+    def test_deflated_invalid_size(self):  # https://discourse.orthanc-server.org/t/transcoding-to-deflated-transfer-syntax-fails/5489
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+            instanceId = '6582b1c0-292ad5ab-ba0f088f-f7a1766f-9a29a54f'
+
+            r = UploadInstance(_REMOTE, 'TransferSyntaxes/1.2.840.10008.1.2.1.99.dcm')
+            attachments = DoGet(_REMOTE, '/instances/' + instanceId + '/attachments/dicom/info/')
+            self.assertEqual(instanceId, r['ID'])
+            self.assertEqual(181071, int(attachments['UncompressedSize']))
+
+            DoDelete(_REMOTE, '/instances/' + instanceId)
+
+            subprocess.check_call([ FindExecutable('storescu'), '-xd', # propose deflated
+                                _REMOTE['Server'], str(_REMOTE['DicomPort']),
+                                GetDatabasePath('TransferSyntaxes/1.2.840.10008.1.2.1.99.dcm') ])
+            attachments = DoGet(_REMOTE, '/instances/' + instanceId + '/attachments/dicom/info/')
+            self.assertLessEqual(181071, int(attachments['UncompressedSize']))
+            self.assertGreaterEqual(181073, int(attachments['UncompressedSize']))  # there might be some padding added
+
+    def test_embed_jpeg(self):
+        if not IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+            return
+
+        with open(GetDatabasePath('Lena.jpg'), 'rb') as f:
+            jpeg = f.read()
+
+        i = DoPost(_REMOTE, '/tools/create-dicom', json.dumps({
+            'Content' : 'data:image/jpeg;base64,%s' % base64.b64encode(jpeg).decode(),
+            'Encapsulate' : True,
+            'Tags' : {
+                'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.7',
+            }
+        })) ['ID']
+
+        tags = DoGet(_REMOTE, '/instances/%s/tags?simplify' % i)
+        self.assertEqual(tags['BitsAllocated'], '8')
+        self.assertEqual(tags['BitsStored'], '8')
+        self.assertEqual(tags['Columns'], '512')
+        self.assertEqual(tags['HighBit'], '7')
+        self.assertEqual(tags['PhotometricInterpretation'], 'YBR_FULL_422')
+        self.assertEqual(tags['PixelData'], None)
+        self.assertEqual(tags['PixelRepresentation'], '0')
+        self.assertEqual(tags['PlanarConfiguration'], '0')
+        self.assertEqual(tags['Rows'], '512')
+        self.assertEqual(tags['SOPClassUID'], '1.2.840.10008.5.1.4.1.1.7')
+        self.assertEqual(tags['SamplesPerPixel'], '3')
+        self.assertEqual(tags['SpecificCharacterSet'], 'ISO_IR 100')
+        pixelData = DoGet(_REMOTE, '/instances/%s/content/7fe0,0010' % i)
+        self.assertEqual(len(pixelData), 2)
+        self.assertEqual(pixelData[0], '0')
+        self.assertEqual(pixelData[1], '1')
+        resp, embedded = DoGetRaw(_REMOTE, '/instances/%s/content/7fe0,0010/1' % i)
+        self.assertEqual('200', resp['status'])
+        self.assertEqual(len(embedded), len(jpeg))
+        self.assertEqual(embedded, jpeg)
+
+        b = io.BytesIO()
+        UncompressImage(jpeg).convert('L').save(b, format = 'jpeg')
+
+        b.seek(0)
+        grayscale = b.read()
+
+        if len(grayscale) % 2 != 0:
+            grayscale = grayscale + '\0'   # Add padding to OW boundaries (2 bytes)
+
+        i = DoPost(_REMOTE, '/tools/create-dicom', json.dumps({
+            'Content' : 'data:image/jpeg;base64,%s' % base64.b64encode(grayscale).decode(),
+            'Encapsulate' : True,
+            'Tags' : {
+                'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.7',
+            }
+        })) ['ID']
+
+        tags = DoGet(_REMOTE, '/instances/%s/tags?simplify' % i)
+        self.assertEqual(tags['BitsAllocated'], '8')
+        self.assertEqual(tags['BitsStored'], '8')
+        self.assertEqual(tags['Columns'], '512')
+        self.assertEqual(tags['HighBit'], '7')
+        self.assertEqual(tags['PhotometricInterpretation'], 'MONOCHROME2')
+        self.assertEqual(tags['PixelData'], None)
+        self.assertEqual(tags['PixelRepresentation'], '0')
+        self.assertFalse('PlanarConfiguration' in tags)
+        self.assertEqual(tags['Rows'], '512')
+        self.assertEqual(tags['SOPClassUID'], '1.2.840.10008.5.1.4.1.1.7')
+        self.assertEqual(tags['SamplesPerPixel'], '1')
+        self.assertEqual(tags['SpecificCharacterSet'], 'ISO_IR 100')
+        pixelData = DoGet(_REMOTE, '/instances/%s/content/7fe0,0010' % i)
+        self.assertEqual(len(pixelData), 2)
+        self.assertEqual(pixelData[0], '0')
+        self.assertEqual(pixelData[1], '1')
+        resp, embedded = DoGetRaw(_REMOTE, '/instances/%s/content/7fe0,0010/1' % i)
+        self.assertEqual('200', resp['status'])
+        self.assertEqual(len(embedded), len(grayscale))
+        self.assertEqual(embedded, grayscale)