changeset 791:5140a8917254 attach-custom-data

merged default -> attach-custom-data
author Alain Mazy <am@orthanc.team>
date Mon, 10 Mar 2025 18:58:07 +0100
parents d08518883f2f (current diff) 3da6edb11ee9 (diff)
children 0572c2c12c70
files NewTests/PostgresUpgrades/docker-compose.yml NewTests/PostgresUpgrades/downgrade.sh NewTests/PostgresUpgrades/run-integ-tests-from-docker.sh NewTests/PostgresUpgrades/test_pg_upgrades.py NewTests/requirements.txt
diffstat 9 files changed, 315 insertions(+), 80 deletions(-) [+]
line wrap: on
line diff
Binary file Database/sample-pdf.dcm has changed
--- a/NewTests/Authorization/test_authorization.py	Fri Feb 07 12:24:05 2025 +0100
+++ b/NewTests/Authorization/test_authorization.py	Mon Mar 10 18:58:07 2025 +0100
@@ -57,7 +57,8 @@
                 },
                 "DicomWeb": {
                     "Enable": True
-                }
+                },
+                "StableAge": 5000 # not to be disturbed by StableAge events while debugging
             }
 
         config_path = cls.generate_configuration(
@@ -121,6 +122,17 @@
         cls.no_label_series_dicom_id = o.series.get_tags(cls.no_label_series_id)["SeriesInstanceUID"]
         cls.no_label_instance_dicom_id = o.instances.get_tags(cls.no_label_instance_id)["SOPInstanceUID"]
 
+        cls.both_labels_instance_id = o.upload_file(here / "../../Database/Phenix/IM-0001-0001.dcm")[0]
+        cls.both_labels_study_id = o.instances.get_parent_study_id(cls.both_labels_instance_id)
+        cls.both_labels_series_id = o.instances.get_parent_series_id(cls.both_labels_instance_id)
+        cls.both_labels_study_dicom_id = o.studies.get_tags(cls.both_labels_study_id)["StudyInstanceUID"]
+        cls.both_labels_series_dicom_id = o.series.get_tags(cls.both_labels_series_id)["SeriesInstanceUID"]
+        cls.both_labels_instance_dicom_id = o.instances.get_tags(cls.both_labels_instance_id)["SOPInstanceUID"]
+        o.studies.add_label(cls.both_labels_study_id, "label_a")
+        o.studies.add_label(cls.both_labels_study_id, "label_b")
+        o.series.add_label(cls.both_labels_series_id, "label_a")
+        o.series.add_label(cls.both_labels_series_id, "label_b")
+
 
     def assert_is_forbidden(self, api_call):
         with self.assertRaises(orthanc_exceptions.HttpError) as ctx:
@@ -145,14 +157,15 @@
         instances_ids = o.series.get_instances_ids(series_ids[0])
         o.instances.get_tags(instances_ids[0])
 
-        # make sure labels filtering still works
-        self.assertEqual(3, len(o.studies.find(query={},
-                                               labels=[],
-                                               labels_constraint='Any')))
+        if o.is_plugin_version_at_least("authorization", 0, 9, 0):
+            # make sure labels filtering still works
+            self.assertEqual(4, len(o.studies.find(query={},
+                                                labels=[],
+                                                labels_constraint='Any')))
 
-        self.assertEqual(2, len(o.studies.find(query={},
-                                               labels=['label_a', 'label_b'],
-                                               labels_constraint='Any')))
+            self.assertEqual(3, len(o.studies.find(query={},
+                                                labels=['label_a', 'label_b'],
+                                                labels_constraint='Any')))
 
         self.assertEqual(2, len(o.studies.find(query={},
                                                labels=['label_a'],
@@ -192,19 +205,24 @@
         # make sure we can not access series and instances of the label_b studies
         self.assert_is_forbidden(lambda: o.studies.get_series_ids(self.label_b_study_id))
 
-        # make sure tools/find only returns the label_a studies
-        studies = o.studies.find(query={},
-                                 labels=[],
-                                 labels_constraint='Any')
-        self.assertEqual(1, len(studies))
-        self.assertEqual(self.label_a_study_id, studies[0].orthanc_id)
+        if o_admin.is_plugin_version_at_least("authorization", 0, 9, 0):
+            # make sure tools/find only returns the label_a studies
+            studies = o.studies.find(query={},
+                                    labels=[],
+                                    labels_constraint='Any')
+            studies_orthanc_ids = [x.orthanc_id for x in studies]
+            self.assertEqual(2, len(studies_orthanc_ids))
+            self.assertIn(self.label_a_study_id, studies_orthanc_ids)
+            self.assertIn(self.both_labels_study_id, studies_orthanc_ids)
 
-        # if searching Any of label_a & label_b, return only label_a
-        studies = o.studies.find(query={},
-                                 labels=['label_a', 'label_b'],
-                                 labels_constraint='Any')
-        self.assertEqual(1, len(studies))
-        self.assertEqual(self.label_a_study_id, studies[0].orthanc_id)
+            # if searching Any of label_a & label_b, return only label_a
+            studies = o.studies.find(query={},
+                                    labels=['label_a', 'label_b'],
+                                    labels_constraint='Any')
+            studies_orthanc_ids = [x.orthanc_id for x in studies]
+            self.assertEqual(2, len(studies_orthanc_ids))
+            self.assertIn(self.label_a_study_id, studies_orthanc_ids)
+            self.assertIn(self.both_labels_study_id, studies_orthanc_ids)
 
         # if searching Any of label_b, expect a Forbidden access
         self.assert_is_forbidden(lambda: o.studies.find(query={},
@@ -272,6 +290,31 @@
             i = o_admin.get_binary(f"dicom-web/studies/{self.label_a_study_dicom_id}/series/{self.label_a_series_dicom_id}/instances/{self.label_a_instance_dicom_id}")
             i = o_admin.get_json(f"dicom-web/studies/{self.label_a_study_dicom_id}/series?includefield=00080021%2C00080031%2C0008103E%2C00200011")
 
+        if o_admin.is_plugin_version_at_least("authorization", 0, 9, 0):
+            # the user_a shall only see the label_a in the returned labels
+            studies = o.post(endpoint="/tools/find", json={"Level": "Study", "Query": {}, "Labels": [], "LabelsConstraint": "Any", "Expand": True}).json()
+            self.assertEqual(2, len(studies))
+            self.assertEqual(1, len(studies[0]["Labels"]))
+            self.assertEqual("label_a", studies[0]["Labels"][0])
+            self.assertEqual(1, len(studies[1]["Labels"]))
+            self.assertEqual("label_a", studies[1]["Labels"][0])
+
+            r = o.get(endpoint=f"/studies/{self.both_labels_study_id}").json()
+            self.assertEqual(1, len(r["Labels"]))
+            self.assertEqual("label_a", r["Labels"][0])
+
+            r = o.get(endpoint=f"/studies/{self.both_labels_study_id}/series?expand").json()
+            self.assertEqual(1, len(r[0]["Labels"]))
+            self.assertEqual("label_a", r[0]["Labels"][0])
+
+            r = o.get(endpoint=f"/studies/{self.both_labels_study_id}/labels").json()
+            self.assertEqual(1, len(r))
+            self.assertEqual("label_a", r[0])
+
+            r = o.get(endpoint=f"/series/{self.both_labels_series_id}/study").json()
+            self.assertEqual(1, len(r["Labels"]))
+            self.assertEqual("label_a", r["Labels"][0])
+
 
     def test_uploader_a(self):
         o_admin = OrthancApiClient(self.o._root_url, headers={"user-token-key": "token-admin"})
--- a/NewTests/PostgresUpgrades/docker-compose.yml	Fri Feb 07 12:24:05 2025 +0100
+++ b/NewTests/PostgresUpgrades/docker-compose.yml	Mon Mar 10 18:58:07 2025 +0100
@@ -16,9 +16,9 @@
       AC_AUTHENTICATION_ENABLED: "false"
 
   # Orthanc previous version
-  orthanc-pg-15-6rev3:
-    image: orthancteam/orthanc:25.1.1
-    container_name: orthanc-pg-15-6rev3
+  orthanc-pg-15-previous-revision:
+    image: orthancteam/orthanc:25.2.0
+    container_name: orthanc-pg-15-previous-revision
     depends_on: [pg-15]
     restart: unless-stopped
     ports: ["8052:8042"]
@@ -29,9 +29,9 @@
       ORTHANC__AUTHENTICATION_ENABLED: "false"
 
   # Orthanc previous version to run the integration tests
-  orthanc-pg-15-6rev3-for-integ-tests:
-    image: orthancteam/orthanc:25.1.1
-    container_name: orthanc-pg-15-6rev3-for-integ-tests
+  orthanc-pg-15-previous-revision-for-integ-tests:
+    image: orthancteam/orthanc:25.2.0
+    container_name: orthanc-pg-15-previous-revision-for-integ-tests
     depends_on: [pg-15]
     restart: unless-stopped
     ports: ["8053:8042"]
@@ -50,7 +50,7 @@
     image: jodogne/orthanc-tests
     container_name: orthanc-tests
     depends_on:
-      - orthanc-pg-15-6rev3-for-integ-tests
+      - orthanc-pg-15-previous-revision-for-integ-tests
     volumes:
       - ../../:/tests/orthanc-tests
       - ./wait-for-it.sh:/scripts/wait-for-it.sh
--- a/NewTests/PostgresUpgrades/downgrade.sh	Fri Feb 07 12:24:05 2025 +0100
+++ b/NewTests/PostgresUpgrades/downgrade.sh	Mon Mar 10 18:58:07 2025 +0100
@@ -8,7 +8,7 @@
 pushd orthanc-databases
 hg update -r attach-custom-data
 
-psql -U postgres -f /scripts/orthanc-databases/PostgreSQL/Plugins/SQL/Downgrades/Rev4ToRev3.sql
+psql -U postgres -f /scripts/orthanc-databases/PostgreSQL/Plugins/SQL/Downgrades/Rev5ToRev4.sql
 
 # if you want to test a downgrade procedure, you may use this code ...
 # psql -U postgres -f downgrade.sql
--- a/NewTests/PostgresUpgrades/run-integ-tests-from-docker.sh	Fri Feb 07 12:24:05 2025 +0100
+++ b/NewTests/PostgresUpgrades/run-integ-tests-from-docker.sh	Mon Mar 10 18:58:07 2025 +0100
@@ -2,6 +2,6 @@
 
 set -ex
 
-/scripts/wait-for-it.sh orthanc-pg-15-6rev3-for-integ-tests:8042 -t 60
-# python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-6rev3-for-integ-tests --force --docker -- -v  Orthanc.test_lua_deadlock
-python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-6rev3-for-integ-tests --force --docker -- -v
+/scripts/wait-for-it.sh orthanc-pg-15-previous-revision-for-integ-tests:8042 -t 60
+# python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-previous-revision-for-integ-tests --force --docker -- -v  Orthanc.test_lua_deadlock
+python /tests/orthanc-tests/Tests/Run.py --server=orthanc-pg-15-previous-revision-for-integ-tests --force --docker -- -v
--- a/NewTests/PostgresUpgrades/test_pg_upgrades.py	Fri Feb 07 12:24:05 2025 +0100
+++ b/NewTests/PostgresUpgrades/test_pg_upgrades.py	Mon Mar 10 18:58:07 2025 +0100
@@ -27,7 +27,7 @@
         cls.cleanup()
 
 
-    def test_upgrade_6rev2_to_6rev3(self):
+    def test_upgrade_previous_revision_to_current(self):
         # remove everything including the DB from previous tests
         TestPgUpgrades.cleanup()
 
@@ -38,16 +38,16 @@
         subprocess.run(["docker", "compose", "up", "pg-15", "-d"], check=True)
         wait_container_healthy("pg-15")
 
-        print("Launching Orthanc with DB 6rev3")
-        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev3", "-d"], check=True)
+        print("Launching Orthanc with DB previous-revision")
+        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-previous-revision", "-d"], check=True)
 
         o = OrthancApiClient("http://localhost:8052")
         o.wait_started()
 
         instances = o.upload_folder(here / "../../Database/Knee")
 
-        print("Stopping Orthanc with DB 6rev3")
-        subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-6rev3"], check=True)
+        print("Stopping Orthanc with DB previous-revision")
+        subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-previous-revision"], check=True)
         time.sleep(2)
 
         print("Launching newest Orthanc")
@@ -115,12 +115,12 @@
         subprocess.run(["docker", "compose", "stop", "orthanc-pg-15-under-tests"], check=True)
         time.sleep(2)
 
-        print("Downgrading Orthanc DB to 6rev3")
+        print("Downgrading Orthanc DB to previous-revision")
         subprocess.run(["docker", "exec", "pg-15", "./scripts/downgrade.sh"], check=True)
         time.sleep(2)
 
-        print("Launching previous Orthanc (DB 6rev3)")
-        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev3", "-d"], check=True)
+        print("Launching previous Orthanc (DB previous-revision)")
+        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-previous-revision", "-d"], check=True)
 
         o = OrthancApiClient("http://localhost:8052")
         o.wait_started()
@@ -135,10 +135,10 @@
         self.assertEqual(0, int(o.get_json('statistics')['TotalDiskSize']))
 
         print("run the integration tests after a downgrade")
-        # first create the containers (orthanc-tests + orthanc-pg-15-6rev3-for-integ-tests) so they know each other
+        # first create the containers (orthanc-tests + orthanc-pg-15-previous-revision-for-integ-tests) so they know each other
         subprocess.run(["docker", "compose", "create", "orthanc-tests"], check=True)
 
-        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-6rev3-for-integ-tests", "-d"], check=True)
+        subprocess.run(["docker", "compose", "up", "orthanc-pg-15-previous-revision-for-integ-tests", "-d"], check=True)
 
         o = OrthancApiClient("http://localhost:8053", user="alice", pwd="orthanctest")
         o.wait_started()
--- a/NewTests/requirements.txt	Fri Feb 07 12:24:05 2025 +0100
+++ b/NewTests/requirements.txt	Mon Mar 10 18:58:07 2025 +0100
@@ -1,3 +1,8 @@
+<<<<<<< working copy
 orthanc-api-client>=0.18.0
 orthanc-tools>=0.15.1
+=======
+orthanc-api-client>=0.18.4
+orthanc-tools>=0.13.0
+>>>>>>> merge rev
 uvicorn
\ No newline at end of file
--- a/Tests/Tests.py	Fri Feb 07 12:24:05 2025 +0100
+++ b/Tests/Tests.py	Mon Mar 10 18:58:07 2025 +0100
@@ -278,6 +278,12 @@
             self.assertIn('Uuid', attachmentInfo)
             self.assertEqual(1, attachmentInfo['ContentType'])
 
+
+            if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+                resp, content = DoGetRaw(_REMOTE, '/instances/%s/attachments/dicom/data?filename=toto.dcm' % instance)
+                self.assertEqual('filename="toto.dcm"', resp['content-disposition'])
+
+
         s = sizeDummyCT + j
 
         if isCompressed:
@@ -727,7 +733,7 @@
         kneeStudy = DoGet(_REMOTE, '/studies')[0]
         kneeSeries = DoGet(_REMOTE, '/series')[0]
 
-        z = GetArchive(_REMOTE, '/patients/%s/archive' % kneePatient)
+        z, resp = GetArchive(_REMOTE, '/patients/%s/archive' % kneePatient)
         self.assertEqual(2, len(z.namelist()))
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
             self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
@@ -735,7 +741,7 @@
         else:
             self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
-        z = GetArchive(_REMOTE, '/studies/%s/archive' % kneeStudy)
+        z, resp = GetArchive(_REMOTE, '/studies/%s/archive' % kneeStudy)
         self.assertEqual(2, len(z.namelist()))
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
             self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
@@ -743,7 +749,7 @@
         else:
             self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000000.dcm', z.namelist())
 
-        z = GetArchive(_REMOTE, '/series/%s/archive' % kneeSeries)
+        z, resp = GetArchive(_REMOTE, '/series/%s/archive' % kneeSeries)
         self.assertEqual(1, len(z.namelist()))
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 6):
             self.assertIn('887 KNEE/A10003245599 IRM DU GENOU/MR T1W_aTSE/MR000001.dcm', z.namelist())
@@ -754,7 +760,7 @@
         brainixPatient = '16738bc3-e47ed42a-43ce044c-a3414a45-cb069bd0'
         brainixStudy = '27f7126f-4f66fb14-03f4081b-f9341db2-53925988'
 
-        z = GetArchive(_REMOTE, '/patients/%s/archive' % kneePatient)
+        z, resp = GetArchive(_REMOTE, '/patients/%s/archive' % kneePatient)
         self.assertEqual(2, len(z.namelist()))
 
         # archive with 2 patients
@@ -814,15 +820,15 @@
             self.assertEqual(helloPatient, worldPatient)
 
             # when downloading the Patient, we do not really know what PatientName we will get in the zip
-            z = GetArchive(_REMOTE, '/patients/%s/archive' % helloPatient)
+            z, resp = GetArchive(_REMOTE, '/patients/%s/archive' % helloPatient)
             self.assertEqual(2, len(z.namelist()))
 
             # when downloading studies individually, we want to have the PatientName that appears in the study
-            z = GetArchive(_REMOTE, '/studies/%s/archive' % helloStudy)
+            z, resp = GetArchive(_REMOTE, '/studies/%s/archive' % helloStudy)
             self.assertEqual(1, len(z.namelist()))
             self.assertIn('COMMON HELLO/HELLO SERIES/Unknown Series/00000000.dcm', z.namelist())
 
-            z = GetArchive(_REMOTE, '/studies/%s/archive' % worldStudy)
+            z, resp = GetArchive(_REMOTE, '/studies/%s/archive' % worldStudy)
             self.assertEqual(1, len(z.namelist()))
             self.assertIn('COMMON WORLD/WORLD SERIES/Unknown Series/00000000.dcm', z.namelist())
 
@@ -832,7 +838,7 @@
         UploadInstance(_REMOTE, 'Knee/T1/IM-0001-0001.dcm')
         UploadInstance(_REMOTE, 'Knee/T2/IM-0001-0001.dcm')
 
-        z = GetArchive(_REMOTE, '/patients/%s/media' % DoGet(_REMOTE, '/patients')[0])
+        z, resp = GetArchive(_REMOTE, '/patients/%s/media' % DoGet(_REMOTE, '/patients')[0])
         self.assertEqual(3, len(z.namelist()))
         self.assertTrue('IMAGES/IM0' in z.namelist())
         self.assertTrue('IMAGES/IM1' in z.namelist())
@@ -4745,7 +4751,7 @@
     def test_extended_media(self):
         UploadInstance(_REMOTE, 'Knee/T1/IM-0001-0001.dcm')
 
-        z = GetArchive(_REMOTE, '/patients/%s/media?extended' % DoGet(_REMOTE, '/patients')[0])
+        z, resp = GetArchive(_REMOTE, '/patients/%s/media?extended' % DoGet(_REMOTE, '/patients')[0])
         self.assertEqual(2, len(z.namelist()))
         self.assertTrue('IMAGES/IM0' in z.namelist())
         self.assertTrue('DICOMDIR' in z.namelist())
@@ -5176,12 +5182,15 @@
 
         job = MonitorJob2(_REMOTE, lambda: DoPost
                           (_REMOTE, '/series/%s/archive' % kneeT1, {
-                              'Synchronous' : False
+                              'Synchronous' : False,
+                              'Filename': 'toto.zip'
                           }))
 
-        z = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
+        z, resp = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
         self.assertEqual(1, len(z.namelist()))
         self.assertFalse('DICOMDIR' in z.namelist())
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+            self.assertEqual('filename="toto.zip"', resp['content-disposition'])
 
         info = DoGet(_REMOTE, '/jobs/%s' % job)
         self.assertEqual(0, info['Content']['ArchiveSizeMB'])  # New in Orthanc 1.8.1
@@ -5197,7 +5206,7 @@
         # archive from second job (as MediaArchiveSize == 1)
         self.assertRaises(Exception, lambda: GetArchive(_REMOTE, '/jobs/%s/archive' % job))
 
-        z = GetArchive(_REMOTE, '/jobs/%s/archive' % job2)
+        z, resp = GetArchive(_REMOTE, '/jobs/%s/archive' % job2)
         self.assertEqual(2, len(z.namelist()))
         self.assertTrue('DICOMDIR' in z.namelist())
 
@@ -5212,7 +5221,7 @@
                               'Resources' : [ kneeT1, kneeT2 ],
                           }))
 
-        z = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
+        z, resp = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
         self.assertEqual(2, len(z.namelist()))
         self.assertFalse('DICOMDIR' in z.namelist())
         
@@ -5227,7 +5236,7 @@
                               'Resources' : [ kneeT1, kneeT2 ],
                           }))
 
-        z = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
+        z, resp = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
         self.assertEqual(3, len(z.namelist()))
         self.assertTrue('DICOMDIR' in z.namelist())
 
@@ -5249,7 +5258,7 @@
                                 'Synchronous' : False
                             }))
 
-            z = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
+            z, resp = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
             # delete the output
             DoDelete(_REMOTE, '/jobs/%s/archive' % job)
             # make sure it is not available anymore afterwards
@@ -5260,7 +5269,7 @@
                             (_REMOTE, '/series/%s/archive' % kneeT2, {
                                 'Synchronous' : False
                             }))
-            z = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
+            z, resp = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
             # delete the output
             DoDelete(_REMOTE, '/jobs/%s/archive' % job)
             # make sure it is not available anymore afterwards
@@ -5278,7 +5287,7 @@
                                 (_REMOTE, '/series/%s/archive' % kneeT2, {
                                     'Synchronous' : False
                                 }))
-                z = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
+                z, resp = GetArchive(_REMOTE, '/jobs/%s/archive' % job)
                 # delete the job itself
                 DoDelete(_REMOTE, '/jobs/%s' % job)
                 # make sure it is not available anymore afterwards (and its output is not available either)
@@ -5523,7 +5532,7 @@
         a = UploadInstance(_REMOTE, 'Issue124.dcm')['ID']
         s = DoGet(_REMOTE, '/instances/%s/series' % a)['ID']
 
-        z = GetArchive(_REMOTE, '/series/%s/media' % s)
+        z, resp = GetArchive(_REMOTE, '/series/%s/media' % s)
         self.assertEqual(2, len(z.namelist()))
 
 
@@ -6693,7 +6702,7 @@
         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.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',
@@ -6729,25 +6738,48 @@
             else:
                 self.assertEqual(a, b)
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            transcoded = DoPost(_REMOTE, '/instances/%s/modify' % i, {
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 40
+                })
+            ratio40 = ExtractDicomTags(transcoded, [ 'LossyImageCompressionRatio' ]) [0]
+
+            transcoded = DoPost(_REMOTE, '/instances/%s/modify' % i, {
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 80
+                })
+            ratio80 = ExtractDicomTags(transcoded, [ 'LossyImageCompressionRatio' ]) [0]
+            self.assertGreater(ratio40, ratio80)
+
+
     def test_archive_transcode(self):
         info = UploadInstance(_REMOTE, 'KarstenHilbertRF.dcm')
 
         # GET on "/media"
-        z = GetArchive(_REMOTE, '/patients/%s/media' % info['ParentPatient'])
+        z, resp = GetArchive(_REMOTE, '/patients/%s/media' % info['ParentPatient'])
         self.assertEqual(2, len(z.namelist()))
         self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(z.read('IMAGES/IM0')))
 
         self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/patients/%s/media?transcode=nope' % info['ParentPatient']))
 
-        z = GetArchive(_REMOTE, '/patients/%s/media?transcode=1.2.840.10008.1.2.4.50' % info['ParentPatient'])
+        z, resp = GetArchive(_REMOTE, '/patients/%s/media?transcode=1.2.840.10008.1.2.4.50' % info['ParentPatient'])
         self.assertEqual('1.2.840.10008.1.2.4.50', GetTransferSyntax(z.read('IMAGES/IM0')))
 
-        z = GetArchive(_REMOTE, '/studies/%s/media?transcode=1.2.840.10008.1.2.4.51' % info['ParentStudy'])
+        z, resp = GetArchive(_REMOTE, '/studies/%s/media?transcode=1.2.840.10008.1.2.4.51' % info['ParentStudy'])
         self.assertEqual('1.2.840.10008.1.2.4.51', GetTransferSyntax(z.read('IMAGES/IM0')))
 
-        z = GetArchive(_REMOTE, '/series/%s/media?transcode=1.2.840.10008.1.2.4.57' % info['ParentSeries'])
+        z, resp = GetArchive(_REMOTE, '/series/%s/media?transcode=1.2.840.10008.1.2.4.57' % info['ParentSeries'])
         self.assertEqual('1.2.840.10008.1.2.4.57', GetTransferSyntax(z.read('IMAGES/IM0')))
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            z40, resp40 = GetArchive(_REMOTE, '/patients/%s/media?transcode=1.2.840.10008.1.2.4.50&lossy-quality=40' % info['ParentPatient'])
+            z80, resp80 = GetArchive(_REMOTE, '/patients/%s/media?transcode=1.2.840.10008.1.2.4.50&lossy-quality=80' % info['ParentPatient'])
+
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
+
 
         # POST on "/media"
         self.assertRaises(Exception, lambda: PostArchive(
@@ -6768,23 +6800,45 @@
             })
         self.assertEqual('1.2.840.10008.1.2.4.57', GetTransferSyntax(z.read('IMAGES/IM0')))
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            z40 = PostArchive(_REMOTE, '/series/%s/media' % info['ParentSeries'], {
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 40
+                })
+            z80 = PostArchive(_REMOTE, '/series/%s/media' % info['ParentSeries'], {
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 80
+                })
+
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
+
         
         # GET on "/archive"
-        z = GetArchive(_REMOTE, '/patients/%s/archive' % info['ParentPatient'])
+        z, resp = GetArchive(_REMOTE, '/patients/%s/archive' % info['ParentPatient'])
         self.assertEqual(1, len(z.namelist()))
         self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(z.read(z.namelist()[0])))
 
         self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/patients/%s/archive?transcode=nope' % info['ParentPatient']))
 
-        z = GetArchive(_REMOTE, '/patients/%s/archive?transcode=1.2.840.10008.1.2' % info['ParentPatient'])
+        z, resp = GetArchive(_REMOTE, '/patients/%s/archive?transcode=1.2.840.10008.1.2' % info['ParentPatient'])
         self.assertEqual('1.2.840.10008.1.2', GetTransferSyntax(z.read(z.namelist()[0])))
 
-        z = GetArchive(_REMOTE, '/studies/%s/archive?transcode=1.2.840.10008.1.2.2' % info['ParentStudy'])
+        z, resp = GetArchive(_REMOTE, '/studies/%s/archive?transcode=1.2.840.10008.1.2.2' % info['ParentStudy'])
         self.assertEqual('1.2.840.10008.1.2.2', GetTransferSyntax(z.read(z.namelist()[0])))
 
-        z = GetArchive(_REMOTE, '/series/%s/archive?transcode=1.2.840.10008.1.2.4.70' % info['ParentSeries'])
+        z, resp = GetArchive(_REMOTE, '/series/%s/archive?transcode=1.2.840.10008.1.2.4.70' % info['ParentSeries'])
         self.assertEqual('1.2.840.10008.1.2.4.70', GetTransferSyntax(z.read(z.namelist()[0])))
 
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            z40, resp40 = GetArchive(_REMOTE, '/patients/%s/archive?transcode=1.2.840.10008.1.2.4.50&lossy-quality=40' % info['ParentPatient'])
+            z80, resp80 = GetArchive(_REMOTE, '/patients/%s/archive?transcode=1.2.840.10008.1.2.4.50&lossy-quality=80' % info['ParentPatient'])
+
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
+
 
         # POST on "/archive"
         self.assertRaises(Exception, lambda: PostArchive(
@@ -6805,6 +6859,20 @@
             })
         self.assertEqual('1.2.840.10008.1.2.4.57', GetTransferSyntax(z.read(z.namelist()[0])))
         
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            z40 = PostArchive(_REMOTE, '/series/%s/archive' % info['ParentSeries'], {
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 40
+                })
+            z80 = PostArchive(_REMOTE, '/series/%s/archive' % info['ParentSeries'], {
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 80
+                })
+
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
+
 
         # "/tools/create-*"
         z = PostArchive(_REMOTE, '/tools/create-archive', {
@@ -6829,30 +6897,96 @@
         self.assertEqual('1.2.840.10008.1.2.4.57', GetTransferSyntax(z.read('IMAGES/IM0')))
 
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 2):
-            z = GetArchive(_REMOTE, '/tools/create-archive?resources=%s&transcode=1.2.840.10008.1.2.4.50' % info['ParentStudy'])
+            z, resp = GetArchive(_REMOTE, '/tools/create-archive?resources=%s&transcode=1.2.840.10008.1.2.4.50' % info['ParentStudy'])
             self.assertEqual(1, len(z.namelist()))
             self.assertEqual('1.2.840.10008.1.2.4.50', GetTransferSyntax(z.read(z.namelist()[0])))
 
-            z = GetArchive(_REMOTE, '/tools/create-media?resources=%s&transcode=1.2.840.10008.1.2.4.51' % info['ParentStudy'])
+            z, resp = GetArchive(_REMOTE, '/tools/create-media?resources=%s&transcode=1.2.840.10008.1.2.4.51' % info['ParentStudy'])
             self.assertEqual(2, len(z.namelist()))
             self.assertEqual('1.2.840.10008.1.2.4.51', GetTransferSyntax(z.read('IMAGES/IM0')))
 
-            z = GetArchive(_REMOTE, '/tools/create-media-extended?resources=%s&transcode=1.2.840.10008.1.2.4.57' % info['ParentStudy'])
+            z, resp = GetArchive(_REMOTE, '/tools/create-media-extended?resources=%s&transcode=1.2.840.10008.1.2.4.57&filename=toto.zip' % info['ParentStudy'])
             self.assertEqual(2, len(z.namelist()))
             self.assertEqual('1.2.840.10008.1.2.4.57', GetTransferSyntax(z.read('IMAGES/IM0')))
-
-
+            if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+                self.assertEqual('filename="toto.zip"', resp['content-disposition'])
+
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            z40 = PostArchive(_REMOTE, '/tools/create-archive', {
+                'Resources' : [ info['ParentStudy'] ],
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 40
+                })
+            z80 = PostArchive(_REMOTE, '/tools/create-archive', {
+                'Resources' : [ info['ParentStudy'] ],
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 80
+                })
+                
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
+
+            z40 = PostArchive(_REMOTE, '/tools/create-media', {
+                'Resources' : [ info['ParentStudy'] ],
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 40
+                })
+            z80 = PostArchive(_REMOTE, '/tools/create-media', {
+                'Resources' : [ info['ParentStudy'] ],
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 80
+                })
+                
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
+
+            z40 = PostArchive(_REMOTE, '/tools/create-media-extended', {
+                'Resources' : [ info['ParentStudy'] ],
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 40
+                })
+            z80 = PostArchive(_REMOTE, '/tools/create-media-extended', {
+                'Resources' : [ info['ParentStudy'] ],
+                'Transcode' : '1.2.840.10008.1.2.4.50',
+                'LossyQuality': 80
+                })
+                
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
+
+            z40, resp = GetArchive(_REMOTE, '/tools/create-archive?resources=%s&transcode=1.2.840.10008.1.2.4.50&lossy-quality=40' % info['ParentStudy'])
+            z80, resp = GetArchive(_REMOTE, '/tools/create-archive?resources=%s&transcode=1.2.840.10008.1.2.4.50&lossy-quality=80' % info['ParentStudy'])
+            size40 = sum([zinfo.file_size for zinfo in z40.filelist])
+            size80 = sum([zinfo.file_size for zinfo in z80.filelist])
+            self.assertLess(size40, size80)
 
 
     def test_download_file_transcode(self):
         if IsOrthancVersionAbove(_REMOTE, 1, 12, 2):
 
             info = UploadInstance(_REMOTE, 'TransferSyntaxes/1.2.840.10008.1.2.1.dcm')
-            self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(
-                DoGet(_REMOTE, '/instances/%s/file' % info['ID'])))
-
-            self.assertEqual('1.2.840.10008.1.2.4.50', GetTransferSyntax(
-                DoGet(_REMOTE, '/instances/%s/file?transcode=1.2.840.10008.1.2.4.50' % info['ID'])))
+            # self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(
+            #     DoGet(_REMOTE, '/instances/%s/file' % info['ID'])))
+
+            # self.assertEqual('1.2.840.10008.1.2.4.50', GetTransferSyntax(
+            #     DoGet(_REMOTE, '/instances/%s/file?transcode=1.2.840.10008.1.2.4.50' % info['ID'])))
+
+            if IsOrthancVersionAbove(_REMOTE, 1, 12, 7):
+                # resp, content = DoGetRaw(_REMOTE, '/instances/%s/file?filename=toto.dcm' % info['ID'])
+                # self.assertEqual('filename="toto.dcm"', resp['content-disposition'])
+
+                # resp, content = DoGetRaw(_REMOTE, '/instances/%s/file?transcode=1.2.840.10008.1.2.4.50&filename=toto.dcm' % info['ID'])
+                # self.assertEqual('filename="toto.dcm"', resp['content-disposition'])
+
+                # resp, content = DoGetRaw(_REMOTE, '/instances/%s/file?filename="toto".dcm' % info['ID'])
+                # self.assertEqual('filename="\"toto\".dcm"', resp['content-disposition'])
+
+                resp, content40 = DoGetRaw(_REMOTE, '/instances/%s/file?transcode=1.2.840.10008.1.2.4.50&lossy-quality=40' % info['ID'])
+                resp, content80 = DoGetRaw(_REMOTE, '/instances/%s/file?transcode=1.2.840.10008.1.2.4.50&lossy-quality=80' % info['ID'])
+                self.assertLess(len(content40), len(content80))
 
 
     def test_modify_keep_source(self):
@@ -7243,7 +7377,7 @@
         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.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',
@@ -11767,3 +11901,55 @@
             self.assertEqual(11, int(resp['content-length']))
             self.assertEqual('application/octet-stream', resp['content-type'])
             self.assertEqual('bytes 10-20/%d' % len(compressed), resp['content-range'])
+
+    def test_order_by_non_existing_metadata(self):
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            r = UploadInstance(_REMOTE, 'sample-pdf.dcm')
+
+            # order by a metadata that does not exist (PDF do not have IndexInSeries)
+            a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Instances',
+                                                    'ParentSeries': r['ParentSeries'],
+                                                    'Query' : { 
+                                                    },
+                                                    'OrderBy' : [
+                                                        {
+                                                            'Type': 'Metadata',
+                                                            'Key': 'IndexInSeries',
+                                                            'Direction': 'ASC'
+                                                        }
+                                                    ]
+                                                })
+            self.assertEqual(1, len(a))
+
+            a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Instances',
+                                                    'ParentSeries': r['ParentSeries'],
+                                                    'Query' : { 
+                                                    },
+                                                    'OrderBy' : [
+                                                        {
+                                                            'Type': 'Metadata',
+                                                            'Key': '9876',
+                                                            'Direction': 'ASC'
+                                                        }
+                                                    ]
+                                                })
+            self.assertEqual(1, len(a))
+
+    def test_order_by_non_existing_tag(self):
+        if IsOrthancVersionAbove(_REMOTE, 1, 12, 7) and HasExtendedFind(_REMOTE):
+            r = UploadInstance(_REMOTE, 'sample-pdf.dcm')
+
+            # order by a DICOM Tag that does not exist (PDF do not have ROWS)
+            a = DoPost(_REMOTE, '/tools/find', {    'Level' : 'Instances',
+                                                    'ParentSeries': r['ParentSeries'],
+                                                    'Query' : { 
+                                                    },
+                                                    'OrderBy' : [
+                                                        {
+                                                            'Type': 'DicomTag',
+                                                            'Key': 'Rows',
+                                                            'Direction': 'ASC'
+                                                        }
+                                                    ]
+                                                })
+            self.assertEqual(1, len(a))
--- a/Tests/Toolbox.py	Fri Feb 07 12:24:05 2025 +0100
+++ b/Tests/Toolbox.py	Mon Mar 10 18:58:07 2025 +0100
@@ -240,7 +240,8 @@
         return zipfile.ZipFile(StringIO(s), "r")
 
 def GetArchive(orthanc, uri):
-    return ParseArchive(DoGet(orthanc, uri))
+    (resp, content) = DoGetRaw(orthanc, uri)
+    return ParseArchive(content), resp
 
 def PostArchive(orthanc, uri, body):
     # http://stackoverflow.com/a/1313868/881731