Mercurial > hg > orthanc-tests
view Plugins/DicomWeb/Run.py @ 574:ee71526e11ed
contributing
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 14 Aug 2023 10:19:50 +0200 |
parents | be69d6d8260c |
children | 28fef24147fa |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # 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) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium # # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU 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 # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # You must add the following to the configuration file: # # "DicomWeb" : { # "Servers" : { # "sample" : [ "http://localhost:8042/dicom-web/", "alice", "orthanctest" ] # } # } import argparse import copy import os import pprint import pydicom import re import sys import unittest import xml.dom.minidom from PIL import ImageChops from io import BytesIO from DicomWeb import * sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'Tests')) from Toolbox import * ## ## Parse the command-line arguments ## parser = argparse.ArgumentParser(description = 'Run the integration tests for the DICOMweb plugin.') parser.add_argument('--server', default = 'localhost', help = 'Address of the Orthanc server to test') parser.add_argument('--rest', type = int, default = 8042, help = 'Port to the REST API') parser.add_argument('--username', default = 'alice', help = 'Username to the REST API') parser.add_argument('--password', default = 'orthanctest', help = 'Password to the REST API') parser.add_argument('--wado', default = '/wado', help = 'Path to the WADO API') parser.add_argument('--dicomweb', default = '/dicom-web/', help = 'Path to the DICOMweb API') parser.add_argument('--force', help = 'Do not warn the user', action = 'store_true') parser.add_argument('options', metavar = 'N', nargs = '*', help='Arguments to Python unittest') args = parser.parse_args() ## ## Configure the testing context ## if not args.force: print(""" WARNING: This test will remove all the content of your Orthanc instance running on %s! Are you sure ["yes" to go on]?""" % args.server) if sys.stdin.readline().strip() != 'yes': print('Aborting...') exit(0) ORTHANC = DefineOrthanc(server = args.server, username = args.username, password = args.password, restPort = args.rest) ## ## The tests ## def UploadAndGetWadoPath(dicom): i = UploadInstance(ORTHANC, dicom) ['ID'] study = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['StudyInstanceUID'] series = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['SeriesInstanceUID'] instance = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['SOPInstanceUID'] return '/studies/%s/series/%s/instances/%s' % (study, series, instance) class Orthanc(unittest.TestCase): def setUp(self): if (sys.version_info >= (3, 0)): # Remove annoying warnings about unclosed socket in Python 3 import warnings warnings.simplefilter("ignore", ResourceWarning) #print("In test: ", self._testMethodName) DropOrthanc(ORTHANC) def test_wado_dicom(self): UploadInstance(ORTHANC, 'Brainix/Flair/IM-0001-0001.dcm') SIZE = 169478 INSTANCE = '1.3.46.670589.11.0.0.11.4.2.0.8743.5.5396.2006120114314079549' SERIES = '1.3.46.670589.11.0.0.11.4.2.0.8743.5.5396.2006120114285654497' STUDY = '2.16.840.1.113669.632.20.1211.10000357775' self.assertRaises(Exception, lambda: DoGet(ORTHANC, args.wado)) self.assertRaises(Exception, lambda: DoGet(ORTHANC, args.wado + '?requestType=WADO')) self.assertRaises(Exception, lambda: DoGet(ORTHANC, args.wado + '?objectUID=%s' % INSTANCE)) dicom = DoGet(ORTHANC, args.wado + '?contentType=application/dicom&requestType=WADO&objectUID=%s' % INSTANCE) self.assertEqual(SIZE, len(dicom)) dicom = DoGet(ORTHANC, args.wado + '?contentType=application/dicom&requestType=WADO&objectUID=%s&seriesUID=%s' % (INSTANCE, SERIES)) self.assertEqual(SIZE, len(dicom)) dicom = DoGet(ORTHANC, args.wado + '?contentType=application/dicom&requestType=WADO&objectUID=%s&seriesUID=%s&studyUID=%s' % (INSTANCE, SERIES, STUDY)) self.assertEqual(SIZE, len(dicom)) dicom = DoGet(ORTHANC, args.wado + '?contentType=application/dicom&requestType=WADO&objectUID=%s&seriesUID=%s' % (INSTANCE, SERIES)) self.assertEqual(SIZE, len(dicom)) dicom = DoGet(ORTHANC, args.wado + '?contentType=application/dicom&requestType=WADO&objectUID=%s&studyUID=%s' % (INSTANCE, STUDY)) self.assertEqual(SIZE, len(dicom)) self.assertRaises(Exception, lambda: DoGet(ORTHANC, args.wado + '?requestType=WADO&objectUID=%s&seriesUID=nope' % INSTANCE)) self.assertRaises(Exception, lambda: DoGet(ORTHANC, args.wado + '?requestType=WADO&objectUID=%s&studyUID=nope' % INSTANCE)) self.assertRaises(Exception, lambda: DoGet(ORTHANC, args.wado + '?requestType=WADO&objectUID=%s&seriesUID=nope&studyUID=nope' % INSTANCE)) def test_wado_image(self): UploadInstance(ORTHANC, 'Phenix/IM-0001-0001.dcm') INSTANCE = '1.2.840.113704.7.1.1.6632.1127829031.2' im = GetImage(ORTHANC, args.wado + '?requestType=WADO&objectUID=%s' % INSTANCE) self.assertEqual('JPEG', im.format) self.assertEqual('L', im.mode) self.assertEqual(512, im.size[0]) self.assertEqual(358, im.size[1]) im = GetImage(ORTHANC, args.wado + '?contentType=image/jpg&requestType=WADO&objectUID=%s' % INSTANCE) self.assertEqual('JPEG', im.format) im = GetImage(ORTHANC, args.wado + '?contentType=image/png&requestType=WADO&objectUID=%s' % INSTANCE) self.assertEqual('PNG', im.format) self.assertEqual('L', im.mode) self.assertEqual(512, im.size[0]) self.assertEqual(358, im.size[1]) def test_stow(self): self.assertEqual(0, len(DoGet(ORTHANC, '/instances'))) SendStow(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Phenix/IM-0001-0001.dcm')) self.assertEqual(1, len(DoGet(ORTHANC, '/instances'))) a = SendStow(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Phenix/IM-0001-0001.dcm')) self.assertEqual(1, len(DoGet(ORTHANC, '/instances'))) self.assertEqual(4, len(a)) # Specific character set self.assertTrue('00080005' in a) self.assertEqual('CS', a['00080005']['vr']) self.assertTrue(a['00081190']['Value'][0].endswith('studies/2.16.840.1.113669.632.20.1211.10000098591')) self.assertEqual('UR', a['00081190']['vr']) self.assertFalse('Value' in a['00081198']) # No error => empty sequence self.assertEqual('SQ', a['00081198']['vr']) self.assertEqual(1, len(a['00081199']['Value'])) # 1 success self.assertEqual('SQ', a['00081199']['vr']) b = a['00081199']['Value'][0] # Referenced SOP class UID self.assertEqual('UI', b['00081150']['vr']) self.assertEqual(1, len(b['00081150']['Value'])) self.assertEqual('1.2.840.10008.5.1.4.1.1.2', b['00081150']['Value'][0]) # Referenced SOP instance UID self.assertEqual('UI', b['00081155']['vr']) self.assertEqual(1, len(b['00081155']['Value'])) self.assertEqual('1.2.840.113704.7.1.1.6632.1127829031.2', b['00081155']['Value'][0]) # Retrieve URL self.assertEqual('UR', b['00081190']['vr']) self.assertEqual(1, len(b['00081190']['Value'])) self.assertTrue(b['00081190']['Value'][0]. endswith('series/1.2.840.113704.1.111.5692.1127828999.2/instances/1.2.840.113704.7.1.1.6632.1127829031.2')) # Remove the "http://localhost:8042" prefix url = a['00081190']['Value'][0] url = re.sub(r'(http|https)://[^/]+(/.*)', r'\2', url) # Get the content-length of all the multiparts of this WADO-RS # request (prevent transcoding by setting transfer-syntax to # "*", necessary since release 1.5 of the DICOMweb plugin) b = DoGet(ORTHANC, url, headers = { 'Accept' : 'multipart/related;type=application/dicom;transfer-syntax=*' }).decode('utf-8', 'ignore') parts = re.findall(r'^Content-Length:\s*(\d+)\s*', b, re.IGNORECASE | re.MULTILINE) self.assertEqual(1, len(parts)) self.assertEqual(os.path.getsize(GetDatabasePath('Phenix/IM-0001-0001.dcm')), int(parts[0])) def test_server_get(self): try: DoDelete(ORTHANC, '/dicom-web/servers/google') # If "AllWindowsStart.sh" is used except: pass try: DoDelete(ORTHANC, '/dicom-web/servers/hello') # If "test_add_server" fails except: pass UploadInstance(ORTHANC, 'Knee/T1/IM-0001-0001.dcm') self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/servers'))) self.assertTrue('sample' in DoGet(ORTHANC, '/dicom-web/servers')) serversReadback = DoGet(ORTHANC, '/dicom-web/servers?expand') self.assertEqual('http://localhost:8042/dicom-web/', serversReadback['sample']['Url']) self.assertEqual('alice', serversReadback['sample']['Username']) sample = DoGet(ORTHANC, '/dicom-web/servers/sample') self.assertEqual(5, len(sample)) self.assertTrue('stow' in sample) self.assertTrue('retrieve' in sample) self.assertTrue('get' in sample) self.assertTrue('wado' in sample) # New in 0.7 self.assertTrue('qido' in sample) # New in 0.7 # application/dicom+xml self.assertEqual(2, len(re.findall('^--', DoGet(ORTHANC, '/dicom-web/studies', headers = { 'Accept' : 'application/dicom+xml' }), re.MULTILINE))) self.assertEqual(2, len(re.findall('^--', DoPost (ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '/studies', 'HttpHeaders' : { 'Accept' : 'application/dicom+xml' } }), re.MULTILINE))) # application/dicom+json self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies', headers = { 'Accept' : 'application/dicom+json' }))) self.assertEqual(1, len(DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '/studies', 'HttpHeaders' : { 'Accept' : 'application/dicom+json' }}))) # application/json self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies', headers = { 'Accept' : 'application/json' }))) self.assertEqual(1, len(DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '/studies', 'HttpHeaders' : { 'Accept' : 'application/json' }}))) # application/dicom+json is the default as of OrthancDicomWeb-0.5 self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies'))) self.assertEqual(1, len(DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '/studies' }))) def test_server_stow(self): UploadInstance(ORTHANC, 'Knee/T1/IM-0001-0001.dcm') self.assertRaises(Exception, lambda: DoPost(ORTHANC, '/dicom-web/servers/sample/stow', { 'Resources' : [ 'nope' ], 'Synchronous' : True })) # inexisting resource l = 3 # For >= 1.10.1 # study r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', { 'Resources' : [ '0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918' ], 'Synchronous' : True }) self.assertEqual(l, len(r)) self.assertEqual("0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918", r['Resources']['Studies'][0]) # series r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', { 'Resources' : [ '6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285' ], 'Synchronous' : True }) self.assertEqual(l, len(r)) self.assertEqual("6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285", r['Resources']['Series'][0]) # instances r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', { 'Resources' : [ 'c8df6478-d7794217-0f11c293-a41237c9-31d98357' ], 'Synchronous' : True }) self.assertEqual(l, len(r)) self.assertEqual("c8df6478-d7794217-0f11c293-a41237c9-31d98357", r['Resources']['Instances'][0]) # altogether r = DoPost(ORTHANC, '/dicom-web/servers/sample/stow', { 'Resources' : [ 'ca29faea-b6a0e17f-067743a1-8b778011-a48b2a17', '0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918', '6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285', 'c8df6478-d7794217-0f11c293-a41237c9-31d98357' ], 'Synchronous' : True }) # pprint.pprint(r) self.assertEqual(l, len(r)) self.assertEqual("ca29faea-b6a0e17f-067743a1-8b778011-a48b2a17", r['Resources']['Patients'][0]) self.assertEqual("0a9b3153-2512774b-2d9580de-1fc3dcf6-3bd83918", r['Resources']['Studies'][0]) self.assertEqual("6de73705-c4e65c1b-9d9ea1b5-cabcd8e7-f15e4285", r['Resources']['Series'][0]) self.assertEqual("c8df6478-d7794217-0f11c293-a41237c9-31d98357", r['Resources']['Instances'][0]) def test_server_retrieve(self): COUNT = 'ReceivedInstancesCount' #COUNT = 'Instances' # In version <= 0.6 UploadInstance(ORTHANC, 'Knee/T1/IM-0001-0001.dcm') UploadInstance(ORTHANC, 'Knee/T1/IM-0001-0002.dcm') UploadInstance(ORTHANC, 'Knee/T2/IM-0001-0001.dcm') self.assertRaises(Exception, lambda: DoPost(ORTHANC, '/dicom-web/servers/sample/retrieve', { 'Resources' : [ { 'Study' : 'nope' } ]})) # inexisting resource t = DoPost(ORTHANC, '/dicom-web/servers/sample/retrieve', { 'Resources' : [ { 'Study' : '2.16.840.1.113669.632.20.121711.10000160881' } ] }) self.assertEqual(3, int(t[COUNT])) # Missing "Study" field self.assertRaises(Exception, lambda: DoPost(ORTHANC, '/dicom-web/servers/sample/retrieve', { 'Resources' : [ { 'Series' : '1.3.46.670589.11.17521.5.0.3124.2008081908564160709' } ]})) t = DoPost(ORTHANC, '/dicom-web/servers/sample/retrieve', { 'Resources' : [ { 'Study' : '2.16.840.1.113669.632.20.121711.10000160881', 'Series' : '1.3.46.670589.11.17521.5.0.3124.2008081908564160709' } ] }) self.assertEqual(2, int(t[COUNT])) t = DoPost(ORTHANC, '/dicom-web/servers/sample/retrieve', { 'Resources' : [ { 'Study' : '2.16.840.1.113669.632.20.121711.10000160881', 'Series' : '1.3.46.670589.11.17521.5.0.3124.2008081909090037350' } ] }) self.assertEqual(1, int(t[COUNT])) t = DoPost(ORTHANC, '/dicom-web/servers/sample/retrieve', { 'Resources' : [ { 'Study' : '2.16.840.1.113669.632.20.121711.10000160881', 'Series' : '1.3.46.670589.11.17521.5.0.3124.2008081909090037350' }, { 'Study' : '2.16.840.1.113669.632.20.121711.10000160881', 'Series' : '1.3.46.670589.11.17521.5.0.3124.2008081908564160709' } ] }) self.assertEqual(3, int(t[COUNT])) t = DoPost(ORTHANC, '/dicom-web/servers/sample/retrieve', { 'Resources' : [ { 'Study' : '2.16.840.1.113669.632.20.121711.10000160881', 'Series' : '1.3.46.670589.11.17521.5.0.3124.2008081909090037350', 'Instance' : '1.3.46.670589.11.17521.5.0.3124.2008081909113806560' } ] }) self.assertEqual(1, int(t[COUNT])) def test_bitbucket_issue_53(self): # DICOMWeb plugin support for "limit" and "offset" parameters in QIDO-RS # https://bugs.orthanc-server.com/show_bug.cgi?id=53 UploadInstance(ORTHANC, 'Brainix/Flair/IM-0001-0001.dcm') UploadInstance(ORTHANC, 'Knee/T1/IM-0001-0001.dcm') brainix = '2.16.840.1.113669.632.20.1211.10000357775' knee = '2.16.840.1.113669.632.20.121711.10000160881' a = DoGet(ORTHANC, '/dicom-web/studies', headers = { 'accept' : 'application/json' }) self.assertEqual(2, len(a)) b = [] a = DoGet(ORTHANC, '/dicom-web/studies?limit=1', headers = { 'accept' : 'application/json' }) self.assertEqual(1, len(a)) b.append(a[0]['0020000D']['Value'][0]) a = DoGet(ORTHANC, '/dicom-web/studies?limit=1&offset=1', headers = { 'accept' : 'application/json' }) self.assertEqual(1, len(a)) b.append(a[0]['0020000D']['Value'][0]) self.assertTrue(brainix in b) self.assertTrue(knee in b) def test_bitbucket_issue_111(self): # Wrong serialization of empty values # https://bugs.orthanc-server.com/show_bug.cgi?id=111 # https://bitbucket.org/sjodogne/orthanc-dicomweb/issues/3 # According to the standard, section F.2.5 # (http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.5.html), # null values behave as follows: If an attribute is present in # DICOM but empty (i.e., Value Length is 0), it shall be # preserved in the DICOM JSON attribute object containing no # "Value", "BulkDataURI" or "InlineBinary". # https://bugs.orthanc-server.com/show_bug.cgi?id=111 UploadInstance(ORTHANC, 'Issue111.dcm') # Test WADO-RS a = DoGet(ORTHANC, '/dicom-web/studies/1.2.276.0.7230010.3.1.2.8323329.30185.1551199973.371589/metadata') self.assertEqual(1, len(a)) self.assertTrue('00080050' in a[0]) # AccessionNumber is null self.assertEqual(1, len(a[0]['00080050'])) # 'vr' is the only field to be present self.assertEqual('SH', a[0]['00080050']['vr']) # Test QIDO-RS a = DoGet(ORTHANC, '/dicom-web/studies') self.assertEqual(1, len(a)) self.assertTrue('00080050' in a[0]) # AccessionNumber is null self.assertEqual(1, len(a[0]['00080050'])) # 'vr' is the only field to be present self.assertEqual('SH', a[0]['00080050']['vr']) # this test fails if SeriesMetadata = "MainDicomTags" (this is expected since the reference json is the full json) def test_wado_hierarchy(self): def CheckJson(uri, headers = {}): with open(GetDatabasePath('DummyCT.json'), 'r') as f: expected = json.loads(f.read()) actual = DoGet(ORTHANC, uri, headers) self.assertEqual(1, len(actual)) AssertAlmostEqualRecursive(self, expected, actual[0]) UploadInstance(ORTHANC, 'DummyCT.dcm') study = '1.2.840.113619.2.176.2025.1499492.7391.1171285944.390' series = '1.2.840.113619.2.176.2025.1499492.7391.1171285944.394' instance = '1.2.840.113619.2.176.2025.1499492.7040.1171286242.109' URI = '/dicom-web/studies/%s/series/%s/instances/%s/metadata' self.assertRaises(Exception, lambda: DoGet(ORTHANC, URI % (study, series, instance), headers = { 'accept' : 'application/nope' })) CheckJson(URI % (study, series, instance), headers = { 'accept' : 'application/dicom+json' }) CheckJson('/dicom-web/studies/%s/series/%s/metadata' % (study, series)) CheckJson('/dicom-web/studies/%s/metadata' % study) self.assertRaises(Exception, lambda: DoGet(ORTHANC, URI % ('nope', series, instance))) self.assertRaises(Exception, lambda: DoGet(ORTHANC, URI % (study, 'nope', instance))) self.assertRaises(Exception, lambda: DoGet(ORTHANC, URI % (study, series, 'nope'))) self.assertRaises(Exception, lambda: DoGet(ORTHANC, '/dicom-web/studies/%s/series/%s/metadata' % ('nope', series))) self.assertRaises(Exception, lambda: DoGet(ORTHANC, '/dicom-web/studies/%s/series/%s/metadata' % (study, 'nope'))) self.assertRaises(Exception, lambda: DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % 'nope')) def test_wado_pixel_data(self): orthanc = UploadInstance(ORTHANC, 'Issue29.dcm') ['ID'] a = DoGet(ORTHANC, '/dicom-web/instances') self.assertEqual(1, len(a)) url = a[0]['00081190']['Value'][0] prefix = 'http://%s:%s' % (args.server, args.rest) self.assertTrue(url.startswith(prefix)) b = DoGet(ORTHANC, url[len(prefix):] + '/metadata') self.assertEqual('OB', b[0]['7FE00010']['vr']) self.assertEqual(2, len(b[0]['7FE00010'])) self.assertTrue('BulkDataURI' in b[0]['7FE00010']) url = b[0]['7FE00010']['BulkDataURI'] self.assertTrue(url.startswith(prefix)) p = DoGetMultipart(ORTHANC, url[len(prefix):]) self.assertEqual(2, len(p)) # There are 2 fragments in this image self.assertEqual(4, len(p[0])) self.assertEqual(114486, len(p[1])) def test_wado_hierarchy_bulk(self): def CheckBulk(value, bulk): self.assertEqual(2, len(value)) self.assertTrue('BulkDataURI' in value) self.assertTrue('vr' in value) self.assertEqual(value['BulkDataURI'], bulk) orthanc = UploadInstance(ORTHANC, 'PrivateTags.dcm') ['ID'] study = '1.2.840.113619.2.115.147416.1094281639.0.29' series = '1.2.840.113619.2.115.147416.1094281639.0.30' sop = '1.2.840.113619.2.115.147416.1094281639.0.38' # WARNING: This test will fail on Orthanc <= 1.5.5, because # the following fix was not included yet: # https://hg.orthanc-server.com/orthanc/rev/b88937ef597b a = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study) self.assertEqual(1, len(a)) BASE_URI = '/dicom-web/studies/%s/series/%s/instances/%s/bulk' % (study, series, sop) BASE_URL = 'http://%s:%s%s' % (args.server, args.rest, BASE_URI) self.assertEqual(2, len(a[0]['60031010']['Value'])) CheckBulk(a[0]['60031010']['Value'][0]['60031011'], '%s/60031010/1/60031011' % BASE_URL) CheckBulk(a[0]['60031010']['Value'][1]['60031011'], '%s/60031010/2/60031011' % BASE_URL) CheckBulk(a[0]['7FE00010'], '%s/7fe00010' % BASE_URL) b = DoGetRaw(ORTHANC, '/instances/%s/content/6003-1010/0/6003-1011' % orthanc) [1] c = DoGetMultipart(ORTHANC, '%s/60031010/1/60031011' % BASE_URI) self.assertEqual(12288, len(b)) self.assertEqual(1, len(c)) self.assertEqual(b, c[0]) def test_bitbucket_issue_112(self): # Wrong serialization of number values # https://bugs.orthanc-server.com/show_bug.cgi?id=112 # https://bitbucket.org/sjodogne/orthanc-dicomweb/issues/4/ UploadInstance(ORTHANC, 'DummyCT.dcm') study = '1.2.840.113619.2.176.2025.1499492.7391.1171285944.390' # This is the WADO-RS testing a = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study) self.assertEqual(1, len(a)) self.assertEqual('IS', a[0]['00180091']['vr']) # EchoTrainLength if (sys.version_info >= (3, 0)): types = (int) else: types = (int, long) b = a[0]['00180091']['Value'][0] self.assertTrue(isinstance(b, types)) self.assertEqual(10, b) # This is the QIDO-RS testing a = DoGet(ORTHANC, '/dicom-web/studies') self.assertEqual(1, len(a)) self.assertEqual('IS', a[0]['00201208']['vr']) # Number of Study Related Instances b = a[0]['00201208']['Value'][0] self.assertTrue(isinstance(b, types)) self.assertEqual(1, b) def test_bitbucket_issue_113(self): # Wrong serialization of PN VR # https://bugs.orthanc-server.com/show_bug.cgi?id=113 # https://bitbucket.org/sjodogne/orthanc-dicomweb/issues/2/ # Make sure UTF-8 encoding is used self.assertEqual('Utf8', DoPut(ORTHANC, '/tools/default-encoding', 'Utf8')) UploadInstance(ORTHANC, 'Encodings/DavidClunie/SCSX1') study = '1.3.6.1.4.1.5962.1.2.0.1175775771.5711.0' # This is the WADO-RS testing a = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study) self.assertEqual(1, len(a)) pn = a[0]['00100010'] # Patient name self.assertEqual('PN', pn['vr']) self.assertEqual(1, len(pn['Value'])) self.assertEqual('Wang^XiaoDong', pn['Value'][0]['Alphabetic']) self.assertEqual(u'王^小東', pn['Value'][0]['Ideographic']) # This is the QIDO-RS testing a = DoGet(ORTHANC, '/dicom-web/studies') self.assertEqual(1, len(a)) pn = a[0]['00100010'] # Patient name self.assertEqual('PN', pn['vr']) self.assertEqual(1, len(pn['Value'])) self.assertEqual('Wang^XiaoDong', pn['Value'][0]['Alphabetic']) self.assertEqual(u'王^小東', pn['Value'][0]['Ideographic']) def test_bitbucket_issue_96(self): # WADO-RS RetrieveFrames rejects valid accept headers # https://bugs.orthanc-server.com/show_bug.cgi?id=96 # https://bitbucket.org/sjodogne/orthanc-dicomweb/issues/5/ UploadInstance(ORTHANC, 'Brainix/Epi/IM-0001-0001.dcm') a = DoGet(ORTHANC, '/dicom-web/instances') self.assertEqual(1, len(a)) self.assertEqual(256, a[0]['00280010']['Value'][0]) # Rows self.assertEqual(256, a[0]['00280011']['Value'][0]) # Columns self.assertEqual(16, a[0]['00280100']['Value'][0]) # Bits allocated url = a[0]['00081190']['Value'][0] prefix = 'http://%s:%s' % (args.server, args.rest) self.assertTrue(url.startswith(prefix)) uri = url[len(prefix):] self.assertRaises(Exception, lambda: DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 0))) self.assertRaises(Exception, lambda: DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 2))) b = DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1)) self.assertEqual(1, len(b)) self.assertEqual(256 * 256 * 2, len(b[0])) self.assertEqual('ce394eb4d4de4eeef348436108101f3b', ComputeMD5(b[0])) c = DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'multipart/related; type=application/octet-stream' }) self.assertEqual(1, len(c)) self.assertEqual(b[0], c[0]) self.assertEqual('ce394eb4d4de4eeef348436108101f3b', ComputeMD5(c[0])) c = DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'multipart/related; type="application/octet-stream"' }) self.assertEqual(1, len(c)) self.assertEqual(b[0], c[0]) self.assertEqual('ce394eb4d4de4eeef348436108101f3b', ComputeMD5(c[0])) self.assertRaises(Exception, lambda: DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'multipart/related; type="nope"' })) self.assertRaises(Exception, lambda: DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'multipart/related; type=nope' })) self.assertRaises(Exception, lambda: DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'nope' })) def test_qido_fields(self): UploadInstance(ORTHANC, 'DummyCT.dcm') a = DoGet(ORTHANC, '/dicom-web/studies') self.assertEqual(1, len(a)) self.assertFalse('00280010' in a[0]) # Rows a = DoGet(ORTHANC, '/dicom-web/studies?includefield=Rows') self.assertEqual(1, len(a)) self.assertTrue('00280010' in a[0]) self.assertEqual(512, a[0]['00280010']['Value'][0]) a = DoGet(ORTHANC, '/dicom-web/studies?Rows=128') self.assertEqual(0, len(a)) a = DoGet(ORTHANC, '/dicom-web/studies?Rows=512') self.assertEqual(1, len(a)) self.assertTrue('00280010' in a[0]) self.assertEqual(512, a[0]['00280010']['Value'][0]) def test_stow_errors(self): def CheckSequences(a): self.assertEqual(3, len(a)) self.assertTrue('00080005' in a) self.assertTrue('00081198' in a) self.assertTrue('00081199' in a) self.assertEqual('CS', a['00080005']['vr']) self.assertEqual('SQ', a['00081198']['vr']) self.assertEqual('SQ', a['00081199']['vr']) # Pushing an instance to a study that is not its parent (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies/nope', GetDatabasePath('Phenix/IM-0001-0001.dcm')) self.assertEqual(409, status) CheckSequences(a) self.assertFalse('Value' in a['00081199']) # No success instance self.assertEqual(1, len(a['00081198']['Value'])) # One failed instance self.assertEqual('1.2.840.10008.5.1.4.1.1.2', a['00081198']['Value'][0]['00081150']['Value'][0]) self.assertEqual('1.2.840.113704.7.1.1.6632.1127829031.2', a['00081198']['Value'][0]['00081155']['Value'][0]) self.assertEqual(0x0110, # Processing failure a['00081198']['Value'][0]['00081197']['Value'][0]) # Pushing an instance with missing tags (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Issue111.dcm')) self.assertEqual(400, status) CheckSequences(a) self.assertFalse('Value' in a['00081198']) # No failed instance, as tags are missing self.assertFalse('Value' in a['00081199']) # No success instance # Pushing a file that is not in the DICOM format (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Issue111.dump')) self.assertEqual(400, status) CheckSequences(a) self.assertFalse('Value' in a['00081198']) # No failed instance, as non-DICOM self.assertFalse('Value' in a['00081199']) # No success instance # Pushing a DICOM instance with only SOP class and instance UID (status, a) = SendStowRaw(ORTHANC, args.dicomweb + '/studies', GetDatabasePath('Issue196.dcm')) self.assertEqual(400, status) CheckSequences(a) self.assertFalse('Value' in a['00081199']) # No success instance self.assertEqual(1, len(a['00081198']['Value'])) # One failed instance self.assertEqual('1.2.840.10008.5.1.4.1.1.4', a['00081198']['Value'][0]['00081150']['Value'][0]) self.assertEqual('1.2.840.113619.2.176.2025.1499492.7040.1171286242.109', a['00081198']['Value'][0]['00081155']['Value'][0]) self.assertEqual(0xC000, # Error: Cannot understand (cannot understand certain Data Elements) a['00081198']['Value'][0]['00081197']['Value'][0]) def test_allowed_methods(self): self.assertEqual(0, len(DoGet(ORTHANC, '/dicom-web/studies'))) e = DoPutRaw(ORTHANC, '/dicom-web/studies') self.assertEqual(405, int(e[0]['status'])) self.assertEqual('GET,POST', e[0]['allow']) e = DoDeleteRaw(ORTHANC, '/dicom-web/studies') self.assertEqual(405, int(e[0]['status'])) self.assertEqual('GET,POST', e[0]['allow']) def test_add_server(self): try: DoDelete(ORTHANC, '/dicom-web/servers/hello') except: pass try: DoDelete(ORTHANC, '/dicom-web/servers/google') # If "AllWindowsStart.sh" is used except: pass l = DoGet(ORTHANC, '/dicom-web/servers') self.assertEqual(1, len(l)) self.assertTrue('sample' in l) url = 'http://localhost:8042/dicom-web/' DoPut(ORTHANC, '/dicom-web/servers/hello', { 'Url': url, 'HttpHeaders' : { 'Hello' : 'World' }, 'Username' : 'bob', 'Password' : 'password', 'UserProperty' : 'Test', 'HasDelete' : True, 'Timeout' : 66 # New in 1.6 }) l = DoGet(ORTHANC, '/dicom-web/servers') self.assertEqual(2, len(l)) self.assertTrue('sample' in l) self.assertTrue('hello' in l) o = DoGet(ORTHANC, '/dicom-web/servers/sample') self.assertEqual(5, len(o)) self.assertTrue('stow' in o) self.assertTrue('retrieve' in o) self.assertTrue('get' in o) self.assertTrue('wado' in o) # New in 0.7 self.assertTrue('qido' in o) # New in 0.7 o = DoGet(ORTHANC, '/dicom-web/servers/hello') self.assertEqual(6, len(o)) self.assertTrue('stow' in o) self.assertTrue('retrieve' in o) self.assertTrue('get' in o) self.assertTrue('wado' in o) # New in 0.7 self.assertTrue('qido' in o) # New in 0.7 self.assertTrue('delete' in o) # New in 0.7 s = DoGet(ORTHANC, '/dicom-web/servers?expand') self.assertEqual(8, len(s['hello'])) self.assertEqual(url, s['hello']['Url']) self.assertEqual('bob', s['hello']['Username']) self.assertEqual(None, s['hello']['Password']) self.assertFalse(s['hello']['Pkcs11']) self.assertEqual(1, len(s['hello']['HttpHeaders'])) self.assertTrue('Hello' in s['hello']['HttpHeaders']) self.assertEqual('Test', s['hello']['UserProperty']) self.assertEqual('1', s['hello']['HasDelete']) self.assertEqual(66, s['hello']['Timeout']) # New in 1.6 (interpreted as a string in <= 1.5) DoDelete(ORTHANC, '/dicom-web/servers/hello') def test_bitbucket_issue_143(self): # WADO-RS metadata request returns "500 Internal Server Error" # instead of "404 Not Found" for missing instance # https://bugs.orthanc-server.com/show_bug.cgi?id=143 UploadInstance(ORTHANC, 'Issue143.dcm') e = DoGetRaw(ORTHANC, '/dicom-web/studies/1.2.840.113619.2.55.3.671756986.106.1316467036.460/series/1.2.840.113619.2.55.3.671756986.106.1316467036.465/instances/0.0.0.0.0/metadata') self.assertEqual(404, int(e[0]['status'])) DoGet(ORTHANC, '/dicom-web/studies/1.3.6.1.4.1.34261.90254037371867.41912.1553085024.2/series/1.3.6.1.4.1.34261.90254037371867.41912.1553085024.3/instances/1.2.276.0.7230010.3.1.4.253549293.36648.1555586123.754/metadata') e = DoGetRaw(ORTHANC, '/dicom-web/studies/0.0.0.0.0/series/1.3.6.1.4.1.34261.90254037371867.41912.1553085024.3/instances/1.2.276.0.7230010.3.1.4.253549293.36648.1555586123.754/metadata') self.assertEqual(404, int(e[0]['status'])) e = DoGetRaw(ORTHANC, '/dicom-web/studies/1.3.6.1.4.1.34261.90254037371867.41912.1553085024.2/series/0.0.0.0.0/instances/1.2.276.0.7230010.3.1.4.253549293.36648.1555586123.754/metadata') self.assertEqual(404, int(e[0]['status'])) e = DoGetRaw(ORTHANC, '/dicom-web/studies/0.0.0.0.0/series/0.0.0.0.0/instances/0.0.0.0.0/metadata') self.assertEqual(404, int(e[0]['status'])) def test_encodings_qido(self): # The "DefaultEncoding" condifuration option is set to "UTF8" # in the integration tests, so all the QIDO-RS requests must # lead to a "ISO_IR 192" specific character set def GetPatientName(dicom, onlyAlphabetic): i = UploadInstance(ORTHANC, dicom) ['ID'] j = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['StudyInstanceUID'] qido = DoGet(ORTHANC, '/dicom-web/studies?0020000D=%s' % j) self.assertEqual(1, len(qido)) self.assertEqual('CS', qido[0]['00080005']['vr']) self.assertEqual('ISO_IR 192', qido[0]['00080005']['Value'][0]) if onlyAlphabetic: self.assertEqual(1, len(qido[0]['00100010']['Value'][0])) else: self.assertEqual(3, len(qido[0]['00100010']['Value'][0])) return qido[0]['00100010']['Value'][0] # Make sure UTF-8 encoding is used self.assertEqual('Utf8', DoPut(ORTHANC, '/tools/default-encoding', 'Utf8')) # Check out "test_issue_95_encodings" in "../../Tests/Tests.py" self.assertEqual(u'Buc^Jérôme', GetPatientName('Encodings/DavidClunie/SCSFREN', True) ['Alphabetic']) self.assertEqual(u'Äneas^Rüdiger', GetPatientName('Encodings/DavidClunie/SCSGERM', True)['Alphabetic']) self.assertEqual(u'Διονυσιος', GetPatientName('Encodings/DavidClunie/SCSGREEK', True)['Alphabetic']) self.assertEqual(u'Люкceмбypг', GetPatientName('Encodings/DavidClunie/SCSRUSS', True)['Alphabetic']) self.assertEqual(u'שרון^דבורה', GetPatientName('Encodings/DavidClunie/SCSHBRW', True)['Alphabetic']) self.assertEqual(u'قباني^لنزار', GetPatientName('Encodings/DavidClunie/SCSARAB', True)['Alphabetic']) self.assertEqual(u'Hong^Gildong', GetPatientName('Encodings/DavidClunie/SCSI2', False)['Alphabetic']) self.assertEqual(u'洪^吉洞', GetPatientName('Encodings/DavidClunie/SCSI2', False)['Ideographic']) self.assertEqual(u'홍^길동', GetPatientName('Encodings/DavidClunie/SCSI2', False)['Phonetic']) self.assertEqual(u'Wang^XiaoDong', GetPatientName('Encodings/DavidClunie/SCSX2', False)['Alphabetic']) self.assertEqual(u'王^小东', GetPatientName('Encodings/DavidClunie/SCSX2', False)['Ideographic']) self.assertEqual(u'', GetPatientName('Encodings/DavidClunie/SCSX2', False)['Phonetic']) self.assertEqual(u'Wang^XiaoDong', GetPatientName('Encodings/DavidClunie/SCSX1', False)['Alphabetic']) self.assertEqual(u'王^小東', GetPatientName('Encodings/DavidClunie/SCSX1', False)['Ideographic']) self.assertEqual(u'', GetPatientName('Encodings/DavidClunie/SCSX1', False)['Phonetic']) self.assertEqual(u'Yamada^Tarou', GetPatientName('Encodings/DavidClunie/SCSH31', False)['Alphabetic']) self.assertEqual(u'山田^太郎', GetPatientName('Encodings/DavidClunie/SCSH31', False)['Ideographic']) self.assertEqual(u'やまだ^たろう', GetPatientName('Encodings/DavidClunie/SCSH31', False)['Phonetic']) self.assertEqual(u'ヤマダ^タロウ', GetPatientName('Encodings/DavidClunie/SCSH32', False)['Alphabetic']) # TODO - Not supported yet by the Orthanc core as of 1.5.7 #self.assertEqual(u'山田^太郎', GetPatientName('Encodings/DavidClunie/SCSH32')['Ideographic']) #self.assertEqual(u'やまだ^たろう', GetPatientName('Encodings/DavidClunie/SCSH32')['Phonetic']) def test_encodings_wado_metadata(self): # If querying the instance metadata, the "DefaultEncoding" # configuration is not used, but the actual encoding def GetEncoding(dicom, length): qido = DoGet(ORTHANC, '/dicom-web/%s/metadata' % UploadAndGetWadoPath(dicom)) self.assertEqual(1, len(qido)) self.assertEqual(length, len(qido[0]['00080005']['Value'])) self.assertEqual('CS', qido[0]['00080005']['vr']) return qido[0]['00080005']['Value'] self.assertEqual('ISO_IR 100', GetEncoding('Encodings/DavidClunie/SCSFREN', 1)[0]) self.assertEqual('ISO_IR 100', GetEncoding('Encodings/DavidClunie/SCSGERM', 1)[0]) self.assertEqual('ISO_IR 126', GetEncoding('Encodings/DavidClunie/SCSGREEK', 1)[0]) self.assertEqual('ISO_IR 144', GetEncoding('Encodings/DavidClunie/SCSRUSS', 1)[0]) self.assertEqual('ISO_IR 138', GetEncoding('Encodings/DavidClunie/SCSHBRW', 1)[0]) self.assertEqual('ISO_IR 127', GetEncoding('Encodings/DavidClunie/SCSARAB', 1)[0]) self.assertEqual('ISO 2022 IR 149', GetEncoding('Encodings/DavidClunie/SCSI2', 1)[0]) self.assertEqual('GB18030', GetEncoding('Encodings/DavidClunie/SCSX2', 1)[0]) self.assertEqual('ISO_IR 192', GetEncoding('Encodings/DavidClunie/SCSX1', 1)[0]) self.assertEqual('ISO 2022 IR 87', GetEncoding('Encodings/DavidClunie/SCSH31', 1)[0]) self.assertEqual('ISO 2022 IR 13', GetEncoding('Encodings/DavidClunie/SCSH32', 2)[0]) self.assertEqual('ISO 2022 IR 87', GetEncoding('Encodings/DavidClunie/SCSH32', 2)[1]) def test_rendered(self): def RenderFrame(path, i): return DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '%s/frames/%d/rendered' % (path, i) }) # This image has 76 frames path = UploadAndGetWadoPath('Multiframe.dcm') self.assertRaises(Exception, lambda: RenderFrame(path, 0)) frame1 = RenderFrame(path, 1) im = UncompressImage(frame1) self.assertEqual("L", im.mode) self.assertEqual(512, im.size[0]) self.assertEqual(512, im.size[1]) im = UncompressImage(RenderFrame(path, 76)) self.assertEqual("L", im.mode) self.assertEqual(512, im.size[0]) self.assertEqual(512, im.size[1]) self.assertRaises(Exception, lambda: RenderFrame(path, 77)) defaultFrame = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '%s/rendered' % path }) self.assertEqual(len(frame1), len(defaultFrame)) self.assertEqual(frame1, defaultFrame) # This image has 1 frame path = UploadAndGetWadoPath('Phenix/IM-0001-0001.dcm') self.assertRaises(Exception, lambda: RenderFrame(path, 0)) self.assertRaises(Exception, lambda: RenderFrame(path, 2)) frame1 = RenderFrame(path, 1) im = UncompressImage(frame1) self.assertEqual("L", im.mode) self.assertEqual(512, im.size[0]) self.assertEqual(358, im.size[1]) defaultFrame = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '%s/rendered' % path }) self.assertEqual(len(frame1), len(defaultFrame)) self.assertEqual(frame1, defaultFrame) def test_qido_parent_attributes(self): UploadInstance(ORTHANC, 'Brainix/Flair/IM-0001-0001.dcm') study = '2.16.840.1.113669.632.20.1211.10000357775' series = '1.3.46.670589.11.0.0.11.4.2.0.8743.5.5396.2006120114285654497' instance = '1.3.46.670589.11.0.0.11.4.2.0.8743.5.5396.2006120114314079549' a = DoGet(ORTHANC, '/dicom-web/studies') self.assertEqual(1, len(a)) self.assertFalse('00080018' in a[0]) # SOPInstanceUID self.assertFalse('0020000E' in a[0]) # SeriesInstanceUID self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual('BRAINIX', a[0]['00100010']['Value'][0]['Alphabetic']) a = DoGet(ORTHANC, '/dicom-web/studies?0020000D=%s' % study) self.assertEqual(1, len(a)) self.assertFalse('00080018' in a[0]) # SOPInstanceUID self.assertFalse('0020000E' in a[0]) # SeriesInstanceUID self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual('BRAINIX', a[0]['00100010']['Value'][0]['Alphabetic']) a = DoGet(ORTHANC, '/dicom-web/series') self.assertEqual(1, len(a)) self.assertFalse('00080018' in a[0]) self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) self.assertEqual('MR', a[0]['00080060']['Value'][0]) self.assertEqual('BRAINIX', a[0]['00100010']['Value'][0]['Alphabetic']) a = DoGet(ORTHANC, '/dicom-web/instances') self.assertEqual(1, len(a)) self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) self.assertEqual(instance, a[0]['00080018']['Value'][0]) self.assertEqual('MR', a[0]['00080060']['Value'][0]) self.assertEqual('BRAINIX', a[0]['00100010']['Value'][0]['Alphabetic']) a = DoGet(ORTHANC, '/dicom-web/studies/%s/series' % study) self.assertEqual(1, len(a)) self.assertFalse('00080018' in a[0]) self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) self.assertEqual('MR', a[0]['00080060']['Value'][0]) self.assertEqual('BRAINIX', a[0]['00100010']['Value'][0]['Alphabetic']) a = DoGet(ORTHANC, '/dicom-web/studies/%s/series/%s/instances' % (study, series)) self.assertEqual(1, len(a)) self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) self.assertEqual(instance, a[0]['00080018']['Value'][0]) self.assertEqual('MR', a[0]['00080060']['Value'][0]) self.assertEqual('BRAINIX', a[0]['00100010']['Value'][0]['Alphabetic']) # "If {StudyInstanceUID} is not specified, all Study-level # attributes specified in Table 6.7.1-2" => Here, # {StudyInstanceUID} *is* specified, so we must *not* get the # PatientName. # http://dicom.nema.org/medical/dicom/2019a/output/html/part18.html#table_6.7.1-2a a = DoGet(ORTHANC, '/dicom-web/series?0020000D=%s' % study) self.assertEqual(1, len(a)) self.assertFalse('00100010' in a[0]) # PatientName self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) self.assertEqual('MR', a[0]['00080060']['Value'][0]) # if we ask explicitely for the Patient and Study tags, we must get it a = DoGet(ORTHANC, '/dicom-web/series?0020000D=%s&includefield=00100010&includefield=00080020' % study) self.assertEqual(1, len(a)) self.assertTrue('00100010' in a[0]) # PatientName self.assertTrue('00080020' in a[0]) # StudyDate # if {StudyInstanceUID} *is not* specified, we must get the PatientName a = DoGet(ORTHANC, '/dicom-web/series') self.assertTrue('00100010' in a[0]) # PatientName # http://dicom.nema.org/medical/dicom/2019a/output/html/part18.html#table_6.7.1-2b a = DoGet(ORTHANC, '/dicom-web/instances?0020000D=%s' % study) self.assertEqual(1, len(a)) self.assertFalse('00100010' in a[0]) # PatientName self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) self.assertEqual('MR', a[0]['00080060']['Value'][0]) a = DoGet(ORTHANC, '/dicom-web/instances?0020000E=%s' % series) self.assertEqual(1, len(a)) self.assertFalse('00080060' in a[0]) # Modality self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) self.assertEqual('BRAINIX', a[0]['00100010']['Value'][0]['Alphabetic']) a = DoGet(ORTHANC, '/dicom-web/instances?0020000D=%s&0020000E=%s' % (study, series)) self.assertEqual(1, len(a)) self.assertFalse('00100010' in a[0]) # PatientName self.assertFalse('00080060' in a[0]) # Modality self.assertEqual(study, a[0]['0020000D']['Value'][0]) self.assertEqual(series, a[0]['0020000E']['Value'][0]) # if we ask explicitely for the Patient and Study tags, we must get it a = DoGet(ORTHANC, '/dicom-web/instances?0020000D=%s&includefield=00100010&includefield=00080020' % study) self.assertEqual(1, len(a)) self.assertTrue('00100010' in a[0]) # PatientName self.assertTrue('00080020' in a[0]) # StudyDate # if {StudyInstanceUID} *is not* specified, we must get all Study, Series and Patient tags a = DoGet(ORTHANC, '/dicom-web/instances') self.assertTrue('00100010' in a[0]) # PatientName self.assertTrue('00080020' in a[0]) # StudyDate self.assertTrue('00080060' in a[0]) # Modality #@unittest.skip("Skip this test on GDCM 2.8.4") def test_bitbucket_issue_164(self): # WARNING - This makes GDCM 2.8.4 crash # https://bugs.orthanc-server.com/show_bug.cgi?id=164 UploadInstance(ORTHANC, 'Issue164.dcm') p = DoGetMultipart(ORTHANC, 'dicom-web/studies/1.2.276.0.26.1.1.1.2.2020.45.52293.1506048/series/1.2.276.0.26.1.1.1.2.2020.45.52293.6384450/instances/1.2.276.0.26.1.1.1.2.2020.45.52366.2551599.179568640/frames/5') self.assertEqual(1, len(p)) self.assertEqual(743 * 975 * 3, len(p[0])) metadata = DoGet(ORTHANC, 'dicom-web/studies/1.2.276.0.26.1.1.1.2.2020.45.52293.1506048/series/1.2.276.0.26.1.1.1.2.2020.45.52293.6384450/instances/1.2.276.0.26.1.1.1.2.2020.45.52366.2551599.179568640/metadata') self.assertEqual("YBR_FULL_422", metadata[0]['00280004']['Value'][0]) # TODO # # starting from X.XX.X, Orthanc won't convert YBR to RGB anymore -> new checksum (https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533) # if IsOrthancVersionAbove(ORTHANC, 1, XX, 1): # expectedDcmtkChecksum = '7535a11e7da0fa590c467ac9d323c5c1' # else: expectedDcmtkChecksum = 'b3662c4bfa24a0c73abb08548c63319b' if HasGdcmPlugin(ORTHANC): self.assertTrue(ComputeMD5(p[0]) in [ 'b952d67da9ff004b0adae3982e89d620', # GDCM >= 3.0 expectedDcmtkChecksum # Fallback to DCMTK ]) else: self.assertEqual(expectedDcmtkChecksum, ComputeMD5(p[0])) # DCMTK def test_bitbucket_issue_168(self): # "Plugins can't read private tags from the configuration # file" This test will fail if DCMTK <= 3.6.1 (e.g. on Ubuntu # 16.04), or if Orthanc <= 1.5.8 # https://bugs.orthanc-server.com/show_bug.cgi?id=168 UploadInstance(ORTHANC, 'Issue168.dcm') a = DoGet(ORTHANC, '/dicom-web/studies') self.assertEqual(1, len(a)) self.assertFalse('00090010' in a[0]) self.assertFalse('00091001' in a[0]) self.assertEqual('20170404', a[0]['00080020']['Value'][0]) a = DoGet(ORTHANC, '/dicom-web/studies?includefield=00091001') self.assertEqual(1, len(a)) self.assertFalse('00090010' in a[0]) self.assertTrue('00091001' in a[0]) # This fails if DCMTK <= 3.6.1 self.assertEqual('DS', a[0]['00091001']['vr']) self.assertEqual(1, len(a[0]['00091001']['Value'])) self.assertAlmostEqual(98.41, a[0]['00091001']['Value'][0]) a = DoGet(ORTHANC, '/dicom-web/studies?00090010=Lunit&includefield=00091001') self.assertEqual(1, len(a)) self.assertTrue('00090010' in a[0]) self.assertEqual('LO', a[0]['00090010']['vr']) self.assertEqual(1, len(a[0]['00090010']['Value'])) self.assertEqual('Lunit', a[0]['00090010']['Value'][0]) self.assertTrue('00091001' in a[0]) self.assertEqual('DS', a[0]['00091001']['vr']) self.assertEqual(1, len(a[0]['00091001']['Value'])) self.assertAlmostEqual(98.41, a[0]['00091001']['Value'][0]) a = DoGet(ORTHANC, '/dicom-web/studies?00090010=Lunit2&includefield=00091001') self.assertEqual(0, len(a)) def test_rendered_studies_series(self): i = UploadInstance(ORTHANC, 'Phenix/IM-0001-0001.dcm') ['ID'] study = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['StudyInstanceUID'] series = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['SeriesInstanceUID'] instance = DoGet(ORTHANC, '/instances/%s/tags?simplify' % i) ['SOPInstanceUID'] a = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '/studies/%s/series/%s/instances/%s/rendered' % (study, series, instance) }) im = UncompressImage(a) self.assertEqual("L", im.mode) self.assertEqual(512, im.size[0]) self.assertEqual(358, im.size[1]) b = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '/studies/%s/series/%s/rendered' % (study, series) }) self.assertEqual(len(a), len(b)) self.assertEqual(a, b) c = DoPost(ORTHANC, '/dicom-web/servers/sample/get', { 'Uri' : '/studies/%s/rendered' % study }) self.assertEqual(len(a), len(c)) self.assertEqual(a, c) def test_multiple_mime_accept_wado_rs(self): # "Multiple MIME type Accept Headers for Wado-RS" # https://groups.google.com/forum/#!msg/orthanc-users/P3B6J9abZpE/syn5dnW2AwAJ UploadInstance(ORTHANC, 'DummyCT.dcm') study = '1.2.840.113619.2.176.2025.1499492.7391.1171285944.390' self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study))) self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'application/json, application/dicom+json' }))) self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'application/json' }))) self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'application/dicom+json' }))) self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'toto, application/dicom+json' }))) self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'application/json, tata' }))) self.assertRaises(Exception, lambda: DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'toto' })) self.assertRaises(Exception, lambda: DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'toto, tata' })) # https://groups.google.com/d/msg/orthanc-users/9o5kItsMQI0/Og6B27YyBgAJ self.assertEqual(1, len(DoGetMultipart(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'multipart/related;type=application/dicom+xml' }))) self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'application/json, application/dicom+xml' }))) self.assertEqual(1, len(DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % study, headers = { 'Accept' : 'application/dicom+xml, application/json' }))) def test_bitbucket_issue_56(self): # "Case-insensitive matching over accents" => DICOMweb part # from AlexanderM on 2020-03-20 # https://bugs.orthanc-server.com/show_bug.cgi?id=56 UploadInstance(ORTHANC, 'Issue56-NoPixelData.dcm') self.assertEqual(1, len(DoPost(ORTHANC, '/tools/find', { 'Level' : 'Patient', 'Query' : { 'PatientName' : 'Гусева*', }, }))) self.assertEqual(1, len(DoPost(ORTHANC, '/tools/find', { 'Level' : 'Patient', 'Query' : { 'PatientName' : 'гусева*', }, }))) self.assertEqual(1, len(DoGet(ORTHANC, u'/dicom-web/studies?PatientName=Гусева*', headers = { 'accept' : 'application/json' }))) # This line is the issue self.assertEqual(1, len(DoGet(ORTHANC, u'/dicom-web/studies?PatientName=гусева*', headers = { 'accept' : 'application/json' }))) def test_frames_transcoding(self): ACCEPT = { '1.2.840.10008.1.2' : 'multipart/related; type=application/octet-stream; transfer-syntax=1.2.840.10008.1.2', '1.2.840.10008.1.2.1' : 'multipart/related; type=application/octet-stream; transfer-syntax=1.2.840.10008.1.2.1', '1.2.840.10008.1.2.4.50' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.50', '1.2.840.10008.1.2.4.51' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.51', '1.2.840.10008.1.2.4.57' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.57', '1.2.840.10008.1.2.4.70' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.70', } uri = 'dicom-web%s' % UploadAndGetWadoPath('TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm') truthRGB = Image.open(GetDatabasePath('TransferSyntaxes/1.2.840.10008.1.2.4.50.png')) # TODO # # starting from X.XX.X, Orthanc won't convert YBR to RGB anymore -> new checksum (https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533) # with open(GetDatabasePath('TransferSyntaxes/1.2.840.10008.1.2.4.50.YBR.raw'), 'rb') as f: # truthRawYbr = f.read() # first test: no transcoding since we accept the JPEG transfer syntax a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }, returnHeaders = True) self.assertEqual(1, len(a)) self.assertEqual(2, len(a[0])) self.assertEqual('%s%s/frames/1' % (ORTHANC['Url'], uri), a[0][1]['Content-Location']) self.assertEqual(ACCEPT['1.2.840.10008.1.2.4.50'], 'multipart/related; type=%s' % a[0][1]['Content-Type']) self.assertEqual(53476, len(a[0][0])) self.assertEqual('142fdb8a1dc2aa7e6b8952aa294a6e22', ComputeMD5(a[0][0])) # second test: no accept header -> defaults to raw explicit a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri) self.assertEqual(1, len(a)) self.assertEqual(480 * 640 * 3, len(a[0])) # Orthanc is now returning the YBR image instead of the RGB # TODO # # starting from X.XX.X, Orthanc won't convert YBR to RGB anymore -> new checksum (https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533) # if IsOrthancVersionAbove(ORTHANC, 1, XX, 1) and not HasGdcmPlugin(ORTHANC): # # GetMaxImageDifference does not work with YBR images -> strict comparison with the output of dcmdjpeg # self.assertEqual('d4aacc6c7758c7c968a4fc8d59b041d5', ComputeMD5(a[0])) # else: # http://effbot.org/zone/pil-comparing-images.htm img = Image.frombytes('RGB', [ 640, 480 ], a[0]) self.assertLessEqual(GetMaxImageDifference(img, truthRGB), 2) ACCEPT2 = copy.deepcopy(ACCEPT) if HasGdcmPlugin(ORTHANC): IS_GDCM = True ACCEPT2['1.2.840.10008.1.2.1'] = 'multipart/related; type=application/octet-stream' del ACCEPT2['1.2.840.10008.1.2'] else: if not IsOrthancVersionAbove(ORTHANC, 1, 12, 1): self.assertEqual('dfdc79f5070926bbb8ac079ee91f5b91', ComputeMD5(a[0])) IS_GDCM = False a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, headers = { 'Accept' : ACCEPT2['1.2.840.10008.1.2.1'] }) self.assertEqual(1, len(a)) self.assertEqual(480 * 640 * 3, len(a[0])) # TODO # # starting from X.XX.X, Orthanc won't convert YBR to RGB anymore -> new checksum (https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533) # if IsOrthancVersionAbove(ORTHANC, 1, XX, X) and not HasGdcmPlugin(ORTHANC): # # GetMaxImageDifference does not work with YBR images -> strict comparison with the output of dcmdjpeg # self.assertEqual('d4aacc6c7758c7c968a4fc8d59b041d5', ComputeMD5(a[0])) # else: # http://effbot.org/zone/pil-comparing-images.htm img = Image.frombytes('RGB', [ 640, 480 ], a[0]) self.assertLessEqual(GetMaxImageDifference(img, truthRGB), 2) # TODO: if not IS_GDCM and not IsOrthancVersionAbove(ORTHANC, 1, XX, X): if not IS_GDCM: self.assertEqual('dfdc79f5070926bbb8ac079ee91f5b91', ComputeMD5(a[0])) # Test download using the same transfer syntax RESULTS = { '1.2.840.10008.1.2' : 'f54c7ea520ab3ec32b6303581ecd262f', '1.2.840.10008.1.2.1' : '4b350b9353a93c747917c7c3bf9b8f44', '1.2.840.10008.1.2.4.50' : '142fdb8a1dc2aa7e6b8952aa294a6e22', '1.2.840.10008.1.2.4.51' : '8b37945d75f9d2899ed868bdba429a0d', '1.2.840.10008.1.2.4.57' : '75c84823eddb560d127b1d24c9406f30', '1.2.840.10008.1.2.4.70' : '2c35726328f0200396e583a0038b0269', } if IS_GDCM: # This file was failing with GDCM, as it has 2 fragments, # and only the first one was returned => the MD5 below is BAD #RESULTS['1.2.840.10008.1.2.4.51'] = '901963a322a817946b074f9ed0afa060' pass for syntax in ACCEPT2: uri = '/dicom-web%s' % UploadAndGetWadoPath('TransferSyntaxes/%s.dcm' % syntax) a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, headers = { 'Accept' : ACCEPT2[syntax] }) self.assertEqual(1, len(a)) self.assertEqual(RESULTS[syntax], ComputeMD5(a[0])) # Test transcoding to all the possible transfer syntaxes uri = 'dicom-web%s' % UploadAndGetWadoPath('KarstenHilbertRF.dcm') for syntax in ACCEPT2: a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, headers = { 'Accept' : ACCEPT2[syntax] }, returnHeaders = True) self.assertEqual(1, len(a)) self.assertEqual(2, len(a[0])) self.assertEqual('%s%s/frames/1' % (ORTHANC['Url'], uri), a[0][1]['Content-Location']) self.assertEqual(ACCEPT[syntax], 'multipart/related; type=%s' % a[0][1]['Content-Type']) if IS_GDCM: self.assertEqual({ '1.2.840.10008.1.2' : '1c8cebde0c74450ce4dfb75dd52ddad7', '1.2.840.10008.1.2.1' : '1c8cebde0c74450ce4dfb75dd52ddad7', '1.2.840.10008.1.2.4.50' : 'f4d145e5f33fbd39375ce0f91453d6cc', '1.2.840.10008.1.2.4.51' : 'f4d145e5f33fbd39375ce0f91453d6cc', '1.2.840.10008.1.2.4.57' : 'dc55800ce1a8ac556c266cdb26d75757', '1.2.840.10008.1.2.4.70' : 'dc55800ce1a8ac556c266cdb26d75757', } [syntax], ComputeMD5(a[0][0])) else: self.assertEqual({ '1.2.840.10008.1.2' : '1c8cebde0c74450ce4dfb75dd52ddad7', '1.2.840.10008.1.2.1' : '1c8cebde0c74450ce4dfb75dd52ddad7', '1.2.840.10008.1.2.4.50' : '0a0ab74fe7c68529bdd416fc9e5e742a', '1.2.840.10008.1.2.4.51' : '33d1ab2fe169c5b5ba932a9bbc3c6306', '1.2.840.10008.1.2.4.57' : '3d21c969da846ca41e0498a0dcfad061', '1.2.840.10008.1.2.4.70' : '49d5353c8673208629847ad45a855557', } [syntax], ComputeMD5(a[0][0])) # JPEG image with many fragments for 2 frames uri = '/dicom-web%s' % UploadAndGetWadoPath('LenaTwiceWithFragments.dcm') a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }) self.assertEqual(1, len(a)) self.assertEqual(69214, len(a[0])) self.assertEqual('0eaf36d4881c513ca70b6684bfaa5b08', ComputeMD5(a[0])) b = DoGetMultipart(ORTHANC, '%s/frames/2' % uri, headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }) self.assertEqual(1, len(b)) self.assertEqual(a[0], b[0]) b = DoGetMultipart(ORTHANC, '%s/frames/1,2' % uri, headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }) self.assertEqual(2, len(b)) self.assertEqual(a[0], b[0]) self.assertEqual(a[0], b[1]) def test_wado_transcoding(self): uri = '/dicom-web%s' % UploadAndGetWadoPath('TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm') compressedSize = os.path.getsize(GetDatabasePath('TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm')) self.assertRaises(Exception, lambda: DoGetMultipart(ORTHANC, '%s' % uri, headers = { 'Accept' : 'nope' })) # Up to release 1.5 of the DICOMweb plugin, if no # transfer-syntax was specified, no transcoding occured. This # was because of an undefined behavior up to DICOM # 2016b. Starting with DICOM 2016c, the standard explicitly # states that the image should be transcoded to Little Endian # Explicit. a = DoGetMultipart(ORTHANC, '%s' % uri, headers = { }) self.assertEqual(1, len(a)) self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(a[0])) self.assertTrue(10 * compressedSize < len(a[0])) uncompressedSize = len(a[0]) a = DoGetMultipart(ORTHANC, '%s' % uri, headers = { 'Accept' : 'multipart/related' }) self.assertEqual(1, len(a)) self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(a[0])) self.assertEqual(uncompressedSize, len(a[0])) a = DoGetMultipart(ORTHANC, '%s' % uri, headers = { 'Accept' : 'multipart/related; type=application/dicom' }) self.assertEqual(1, len(a)) self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(a[0])) self.assertEqual(uncompressedSize, len(a[0])) a = DoGetMultipart(ORTHANC, '%s' % uri, headers = { 'Accept' : 'multipart/related; type=application/dicom; transfer-syntax=*' }) self.assertEqual(1, len(a)) self.assertEqual('1.2.840.10008.1.2.4.50', GetTransferSyntax(a[0])) self.assertEqual(compressedSize, len(a[0])) # Use source transfer syntax a = DoGetMultipart(ORTHANC, '%s' % uri, headers = { 'Accept' : 'multipart/related; type=application/dicom; transfer-syntax=1.2.840.10008.1.2.4.50' }) self.assertEqual(1, len(a)) self.assertEqual('1.2.840.10008.1.2.4.50', GetTransferSyntax(a[0])) self.assertEqual(compressedSize, len(a[0])) a = DoGetMultipart(ORTHANC, '%s' % uri, headers = { 'Accept' : 'multipart/related; type=application/dicom; transfer-syntax=1.2.840.10008.1.2.1' }) self.assertEqual(1, len(a)) self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax(a[0])) self.assertEqual(uncompressedSize, len(a[0])) # Transcoding a = DoGetMultipart(ORTHANC, '%s' % uri, headers = { 'Accept' : 'multipart/related; type=application/dicom; transfer-syntax=1.2.840.10008.1.2.4.57' }) self.assertEqual(1, len(a)) self.assertEqual('1.2.840.10008.1.2.4.57', GetTransferSyntax(a[0])) self.assertNotEqual(compressedSize, len(a[0])) self.assertNotEqual(uncompressedSize, len(a[0])) def test_compare_wado_uri_and_rs(self): # https://groups.google.com/d/msg/orthanc-users/mKgr2QAKTCU/R7u4I1LvBAAJ # Image "2020-08-12-Christopher.dcm" corresponds to the result of: # $ gdcmconv --raw 1.2.840.113704.9.1000.16.2.20190613104005642000100010001.dcm 2020-08-12-Christopher.dcm # Image "2020-08-12-Christopher.png" corresponds to "2.png" i = UploadInstance(ORTHANC, '2020-08-12-Christopher.dcm') ['ID'] STUDY = '1.2.840.113704.9.1000.16.0.20190613103939444' SERIES = '1.2.840.113704.9.1000.16.1.2019061310394289000010001' INSTANCE = '1.2.840.113704.9.1000.16.2.20190613104005642000100010001' with open(GetDatabasePath('2020-08-12-Christopher.png'), 'rb') as f: truth = UncompressImage(f.read()) im1 = GetImage(ORTHANC, args.wado + '?requestType=WADO&objectUID=%s&contentType=image/jpg' % INSTANCE) self.assertEqual('JPEG', im1.format) im2 = GetImage(ORTHANC, args.wado + '?requestType=WADO&objectUID=%s&contentType=image/png' % INSTANCE) self.assertEqual('PNG', im2.format) im3 = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/1/rendered?window=200,800,linear' % (STUDY, SERIES, INSTANCE)) self.assertEqual('JPEG', im3.format) im4 = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/rendered?window=200,800,linear' % (STUDY, SERIES, INSTANCE), headers = { 'Accept' : 'image/png' }) self.assertEqual('PNG', im4.format) im5 = GetImage(ORTHANC, '/instances/%s/rendered' % i, { 'Accept' : 'image/jpeg' }) self.assertEqual('JPEG', im5.format) im6 = GetImage(ORTHANC, '/instances/%s/rendered' % i) self.assertEqual('PNG', im6.format) for im in [ truth, im1, im2, im3, im4, im5, im6 ]: self.assertEqual('L', im.mode) self.assertEqual(512, im.size[0]) self.assertEqual(512, im.size[1]) # The following fails in DICOMweb plugin <= 1.2, as "/rendered" # was redirecting to the "/preview" route of Orthanc # http://effbot.org/zone/pil-comparing-images.htm self.assertLess(ImageChops.difference(im1, im3).getextrema() [1], 10) self.assertLess(ImageChops.difference(im2, im4).getextrema() [1], 2) self.assertLess(ImageChops.difference(im3, im5).getextrema() [1], 10) self.assertLess(ImageChops.difference(im4, im6).getextrema() [1], 2) self.assertTrue(ImageChops.difference(im1, im5).getbbox() is None) self.assertTrue(ImageChops.difference(im2, im6).getbbox() is None) bbox = ImageChops.difference(im2, truth).getbbox() if bbox != None: # Tolerance of just 1 pixel of difference (needed on Windows) #print(im2.getpixel((238,275))) # => 255 #print(truth.getpixel((238,275))) # => 254 self.assertLessEqual(abs(bbox[2] - bbox[0]), 1) self.assertLessEqual(abs(bbox[3] - bbox[1]), 1) def test_issue_195(self): # This fails on Orthanc <= 1.9.2 # https://bugs.orthanc-server.com/show_bug.cgi?id=195 a = UploadInstance(ORTHANC, 'Issue195.dcm') ['ID'] b = DoGet(ORTHANC, 'dicom-web/studies/1.2.276.0.7230010.3.1.2.8323329.13188.1620309604.848733/series/1.2.276.0.7230010.3.1.3.8323329.13188.1620309604.848734/instances/1.2.276.0.7230010.3.1.4.8323329.13188.1620309604.848735/metadata', headers = { 'Accept' : 'application/dicom+json' }) self.assertEqual(1, len(b)) self.assertEqual(5, len(b[0])) # The expected result can be found by typing "dcm2json Database/Issue195.dcm" self.assertEqual(2, len(b[0]["00080018"])) self.assertEqual("UI", b[0]["00080018"]["vr"]) self.assertEqual("1.2.276.0.7230010.3.1.4.8323329.13188.1620309604.848735", b[0]["00080018"]["Value"][0]) self.assertEqual(2, len(b[0]["0020000D"])) self.assertEqual("UI", b[0]["0020000D"]["vr"]) self.assertEqual("1.2.276.0.7230010.3.1.2.8323329.13188.1620309604.848733", b[0]["0020000D"]["Value"][0]) self.assertEqual(2, len(b[0]["0020000E"])) self.assertEqual("UI", b[0]["0020000E"]["vr"]) self.assertEqual("1.2.276.0.7230010.3.1.3.8323329.13188.1620309604.848734", b[0]["0020000E"]["Value"][0]) self.assertEqual(1, len(b[0]["00081030"])) # Case of an empty value self.assertEqual("LO", b[0]["00081030"]["vr"]) self.assertEqual(2, len(b[0]["0008103E"])) self.assertEqual("LO", b[0]["0008103E"]["vr"]) self.assertEqual("Hello1", b[0]["0008103E"]["Value"][0]) DoDelete(ORTHANC, 'instances/%s' % a) a = UploadInstance(ORTHANC, 'Issue195-bis.dcm') ['ID'] URI = 'dicom-web/studies/1.2.276.0.7230010.3.1.2.8323329.6792.1625504071.652468/series/1.2.276.0.7230010.3.1.3.8323329.6792.1625504071.652469/instances/1.2.276.0.7230010.3.1.4.8323329.6792.1625504071.652470' b = DoGet(ORTHANC, '%s/metadata' % URI, headers = { 'Accept' : 'application/dicom+json' }) self.assertEqual(1, len(b)) self.assertEqual(5, len(b[0])) # The expected result can be found by typing "dcm2json ../../Database/Issue195-bis.dcm" self.assertEqual(2, len(b[0]["00080018"])) self.assertEqual("UI", b[0]["00080018"]["vr"]) self.assertEqual("1.2.276.0.7230010.3.1.4.8323329.6792.1625504071.652470", b[0]["00080018"]["Value"][0]) self.assertEqual(2, len(b[0]["0020000D"])) self.assertEqual("UI", b[0]["0020000D"]["vr"]) self.assertEqual("1.2.276.0.7230010.3.1.2.8323329.6792.1625504071.652468", b[0]["0020000D"]["Value"][0]) self.assertEqual(2, len(b[0]["0020000E"])) self.assertEqual("UI", b[0]["0020000E"]["vr"]) self.assertEqual("1.2.276.0.7230010.3.1.3.8323329.6792.1625504071.652469", b[0]["0020000E"]["Value"][0]) self.assertEqual(2, len(b[0]["00084567"])) self.assertEqual("UN", b[0]["00084567"]["vr"]) self.assertEqual('http://%s:%s%s' % (args.server, args.rest, '/%s/bulk/00084567' % URI), b[0]["00084567"]["BulkDataURI"]) c = DoGet(ORTHANC, '%s/bulk/00084567' % URI) self.assertTrue('Content-Length: 2\r\n' in c) index = c.find('\r\n\r\n') self.assertEqual(0x42, ord(c[index + 4])) self.assertEqual(0x00, ord(c[index + 5])) # Case of an empty value, fails in Orthanc <= 1.9.2 because of issue #195 self.assertEqual(1, len(b[0]["00084565"])) self.assertEqual("UN", b[0]["00084565"]["vr"]) def test_multiframe_windowing(self): # Fixed in DICOMweb 1.7 def GetLinear(x, c, w): # http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.2.1 ymin = 0.0 ymax = 255.0 if float(x) <= float(c) - 0.5 - (float(w) - 1.0) / 2.0: return ymin elif float(x) > float(c) - 0.5 + (float(w) - 1.0) / 2.0 : return ymax else: return ((float(x) - (float(c) - 0.5)) / (float(w) - 1.0) + 0.5) * (ymax - ymin) + ymin def GetLinearExact(x, c, w): # http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.3.2 ymin = 0.0 ymax = 255.0 if float(x) <= float(c) - float(w) / 2.0: return ymin elif float(x) > float(c) + float(w) / 2.0: return ymax else: return (float(x) - float(c)) / float(w) * (ymax- ymin) + ymin def GetSigmoid(x, c, w): # http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.3.1 ymin = 0.0 ymax = 255.0 return (ymax - ymin) / (1.0 + math.exp(-4 * (float(x) - float(c)) / float(w))) self.assertAlmostEqual(GetLinear(10, 0, 100), 154.54545454545456) self.assertAlmostEqual(GetLinear(-1000, 2048, 4096), 0) self.assertAlmostEqual(GetLinear(5096, 2048, 4096), 255) self.assertAlmostEqual(GetLinear(333, 2048, 4096), 20.7362637362637) self.assertAlmostEqual(GetLinear(16, 127, 256), 17) self.assertAlmostEqual(GetLinearExact(-1000, 2048, 4096), 0) self.assertAlmostEqual(GetLinearExact(5096, 2048, 4096), 255) self.assertAlmostEqual(GetLinearExact(150, 127, 256), 22.91015625) self.assertAlmostEqual(GetSigmoid(150, 127, 256), 150.166728345797) UploadInstance(ORTHANC, 'MultiframeWindowing.dcm') STUDY = '1.2.840.113619.2.176.2025.1499492.7391.1175285944.390' SERIES = '1.2.840.113619.2.176.2025.1499492.7391.1175285944.394' INSTANCE = '1.2.840.113619.2.176.2025.1499492.7040.1175286242.109' im = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/1/rendered?window=127,256,linear' % (STUDY, SERIES, INSTANCE)) self.assertLessEqual(abs(GetLinear(0x00, 127, 256) - im.getpixel((0, 0))), 1) self.assertLessEqual(abs(GetLinear(0x10, 127, 256) - im.getpixel((1, 0))), 1) self.assertLessEqual(abs(GetLinear(0x20, 127, 256) - im.getpixel((0, 1))), 1.1) self.assertLessEqual(abs(GetLinear(0x30, 127, 256) - im.getpixel((1, 1))), 1.1) im = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/1/rendered?window=0,256,linear-exact' % (STUDY, SERIES, INSTANCE)) self.assertLessEqual(abs(GetLinearExact(0x00, 0, 256) - im.getpixel((0, 0))), 1) self.assertLessEqual(abs(GetLinearExact(0x10, 0, 256) - im.getpixel((1, 0))), 1) self.assertLessEqual(abs(GetLinearExact(0x20, 0, 256) - im.getpixel((0, 1))), 1.2) self.assertLessEqual(abs(GetLinearExact(0x30, 0, 256) - im.getpixel((1, 1))), 1.2) im = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/1/rendered?window=127,256,sigmoid' % (STUDY, SERIES, INSTANCE)) self.assertLessEqual(abs(GetSigmoid(0x00, 127, 256) - im.getpixel((0, 0))), 3) self.assertLessEqual(abs(GetSigmoid(0x10, 127, 256) - im.getpixel((1, 0))), 1) self.assertLessEqual(abs(GetSigmoid(0x20, 127, 256) - im.getpixel((0, 1))), 1) self.assertLessEqual(abs(GetSigmoid(0x30, 127, 256) - im.getpixel((1, 1))), 1) im = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/1/rendered?window=16,128,linear' % (STUDY, SERIES, INSTANCE)) self.assertLessEqual(abs(GetLinear(0x00, 16, 128) - im.getpixel((0, 0))), 1) self.assertLessEqual(abs(GetLinear(0x10, 16, 128) - im.getpixel((1, 0))), 1) self.assertLessEqual(abs(GetLinear(0x20, 16, 128) - im.getpixel((0, 1))), 2) self.assertLessEqual(abs(GetLinear(0x30, 16, 128) - im.getpixel((1, 1))), 2) im = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/2/rendered?window=127,256,linear' % (STUDY, SERIES, INSTANCE)) ri = 100.0 rs = 1.0 self.assertLessEqual(abs(GetLinear(0x00 * rs + ri, 127, 256) - im.getpixel((0, 0))), 1) self.assertLessEqual(abs(GetLinear(0x10 * rs + ri, 127, 256) - im.getpixel((1, 0))), 1) self.assertLessEqual(abs(GetLinear(0x20 * rs + ri, 127, 256) - im.getpixel((0, 1))), 1) self.assertLessEqual(abs(GetLinear(0x30 * rs + ri, 127, 256) - im.getpixel((1, 1))), 1) im = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/3/rendered?window=127,256,linear' % (STUDY, SERIES, INSTANCE)) ri = 0.0 rs = 2.0 self.assertLessEqual(abs(GetLinear(0x00 * rs + ri, 127, 256) - im.getpixel((0, 0))), 1) self.assertLessEqual(abs(GetLinear(0x10 * rs + ri, 127, 256) - im.getpixel((1, 0))), 1.1) self.assertLessEqual(abs(GetLinear(0x20 * rs + ri, 127, 256) - im.getpixel((0, 1))), 1) self.assertLessEqual(abs(GetLinear(0x30 * rs + ri, 127, 256) - im.getpixel((1, 1))), 1) im = GetImage(ORTHANC, '/dicom-web/studies/%s/series/%s/instances/%s/frames/4/rendered?window=127,256,linear' % (STUDY, SERIES, INSTANCE)) ri = 100.0 rs = 2.0 self.assertLessEqual(abs(GetLinear(0x00 * rs + ri, 127, 256) - im.getpixel((0, 0))), 1) self.assertLessEqual(abs(GetLinear(0x10 * rs + ri, 127, 256) - im.getpixel((1, 0))), 1) self.assertLessEqual(abs(GetLinear(0x20 * rs + ri, 127, 256) - im.getpixel((0, 1))), 1) self.assertLessEqual(abs(GetLinear(0x30 * rs + ri, 127, 256) - im.getpixel((1, 1))), 1) def test_forwarded_headers(self): study = UploadInstance(ORTHANC, 'ColorTestImageJ.dcm')['ParentStudy'] studyId = DoGet(ORTHANC, '/studies/%s' % study)['MainDicomTags']['StudyInstanceUID'] m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyId) self.assertIn(ORTHANC['Url'], m[0][u'7FE00010']['BulkDataURI']) m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyId, headers= { 'host': 'my-domain' }) self.assertIn("http://my-domain/dicom-web", m[0][u'7FE00010']['BulkDataURI']) m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyId, headers= { 'forwarded': 'host=my-domain;proto=https' }) self.assertIn("https://my-domain/dicom-web", m[0][u'7FE00010']['BulkDataURI']) if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 13, 1): m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyId, headers= { 'X-Forwarded-Host': 'my-domain', 'X-Forwarded-Proto': 'https' }) self.assertIn("https://my-domain/dicom-web", m[0][u'7FE00010']['BulkDataURI']) def test_issue_216(self): study = UploadInstance(ORTHANC, 'ColorTestImageJ.dcm')['ParentStudy'] studyUid = DoGet(ORTHANC, '/studies/%s' % study)['MainDicomTags']['StudyInstanceUID'] m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyUid, headers = { 'accept': 'image/webp, */*;q=0.8, text/html, application/xhtml+xml, application/xml;q=0.9' }) self.assertEqual(1, len(m)) self.assertEqual(studyUid, m[0]['0020000D']['Value'][0]) m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyUid, headers = { 'accept': 'text/html, application/xhtml+xml, application/xml, image/webp, */*;q=0.8' }) self.assertEqual(1, len(m)) self.assertEqual(studyUid, m[0]['0020000D']['Value'][0]) if IsPluginVersionAbove(ORTHANC, "dicom-web", 1, 13, 1) and IsOrthancVersionAbove(ORTHANC, 1, 12, 1): # This fails on DICOMweb <= 1.13 because of the "; q=.2", # since multiple accepts were not supported # https://bugs.orthanc-server.com/show_bug.cgi?id=216 m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyUid, headers = { 'accept': 'text/html, image/gif, image/jpeg, */*; q=.2, */*; q=.2' }) self.assertEqual(1, len(m)) self.assertEqual(studyUid, m[0]['0020000D']['Value'][0]) # This fails on Orthanc <= 1.12.0 because of the ";q=0.9" m = DoGet(ORTHANC, '/dicom-web/studies/%s/metadata' % studyUid, headers = { 'accept': 'text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8' }) self.assertEqual(1, len(m)) self.assertEqual(studyUid, m[0]['0020000D']['Value'][0]) def test_accept_negotiation(self): def CheckBadRequest(uri, accept): response = DoGetRaw(ORTHANC, uri, headers = { 'accept': accept }) self.assertEqual(int(response[0]['status']), 400) def CheckIsJson(uri, accept): if accept != None: response = DoGetRaw(ORTHANC, uri, headers = { 'accept': accept }) else: response = DoGetRaw(ORTHANC, uri) self.assertEqual(int(response[0]['status']), 200) self.assertEqual(response[0]['content-type'], 'application/dicom+json') json.loads(response[1]) def CheckIsXml(uri, accept): response = DoGetMultipart(ORTHANC, uri, headers = { 'accept': accept }, returnHeaders = True) self.assertEqual(1, len(response)) self.assertEqual(2, len(response[0])) self.assertEqual('application/dicom+xml', response[0][1]['Content-Type']) xml.dom.minidom.parseString(response[0][0]) def CheckIsDicom(uri, accept): if accept != None: response = DoGetMultipart(ORTHANC, uri, headers = { 'accept': accept }, returnHeaders = True) else: response = DoGetMultipart(ORTHANC, uri, returnHeaders = True) self.assertEqual(1, len(response)) self.assertEqual(2, len(response[0])) self.assertEqual('application/dicom', response[0][1]['Content-Type']) pydicom.dcmread(BytesIO(response[0][0]), force=True) def CheckIsBulk(uri, accept): if accept != None: response = DoGetMultipart(ORTHANC, uri, headers = { 'accept': accept }, returnHeaders = True) else: response = DoGetMultipart(ORTHANC, uri, returnHeaders = True) self.assertEqual(1, len(response)) self.assertEqual(2, len(response[0])) self.assertEqual('application/octet-stream', response[0][1]['Content-Type']) self.assertTrue(len(response[0][0]) > 1) study = UploadInstance(ORTHANC, 'ColorTestImageJ.dcm')['ParentStudy'] studyUid = DoGet(ORTHANC, '/studies/%s' % study)['MainDicomTags']['StudyInstanceUID'] CheckIsJson('/dicom-web/studies/%s/metadata' % studyUid, None) CheckBadRequest('/dicom-web/studies/%s/metadata' % studyUid, 'application/nope') CheckIsJson('/dicom-web/studies/%s/metadata' % studyUid, 'application/json') CheckIsJson('/dicom-web/studies/%s/metadata' % studyUid, 'application/dicom+json') CheckBadRequest('/dicom-web/studies/%s/metadata' % studyUid, 'multipart/related') CheckIsXml('/dicom-web/studies/%s/metadata' % studyUid, 'multipart/related; type=application/dicom+xml') CheckIsXml('/dicom-web/studies/%s/metadata' % studyUid, 'multipart/related; type="application/dicom+xml"') CheckBadRequest('/dicom-web/studies/%s/metadata' % studyUid, 'multipart/related; type="application/nope"') CheckBadRequest('/dicom-web/studies/%s/metadata' % studyUid, 'multipart/related; type=application/dicom+xml; transfer-syntax=nope') CheckBadRequest('/dicom-web/studies/%s' % studyUid, 'multipart/nope') CheckIsDicom('/dicom-web/studies/%s' % studyUid, None) CheckIsDicom('/dicom-web/studies/%s' % studyUid, 'multipart/related') CheckIsDicom('/dicom-web/studies/%s' % studyUid, 'multipart/related; type=application/dicom') CheckIsDicom('/dicom-web/studies/%s' % studyUid, 'multipart/related; type="application/dicom"') CheckBadRequest('/dicom-web/studies/%s' % studyUid, 'multipart/related; type=application/nope') CheckIsDicom('/dicom-web/studies/%s' % studyUid, 'multipart/related; transfer-syntax=*') CheckIsDicom('/dicom-web/studies/%s' % studyUid, 'multipart/related; type=application/dicom; transfer-syntax=*') CheckBadRequest('/dicom-web/studies/%s' % studyUid, 'multipart/related; transfer-syntax=nope') CheckIsDicom('/dicom-web/studies/%s' % studyUid, 'multipart/related; type=application/dicom; transfer-syntax=1.2.840.10008.1.2.1') uri = '/dicom-web/studies/%s/series/%s/instances/%s/bulk/0010,0010' % ( studyUid, '1.3.12.2.1107.5.99.2.1255.30000007020811545343700000012', '1.2.276.0.7230010.3.1.4.2455711835.6056.1170936079.1') CheckIsBulk(uri, None) CheckBadRequest(uri, 'multipart/nope') CheckIsBulk(uri, 'multipart/related') CheckIsBulk(uri, 'multipart/related;type=application/octet-stream') CheckIsBulk(uri, 'multipart/related; type = "application/octet-stream" ') CheckBadRequest(uri, 'multipart/related; type = " application/octet-stream" ') CheckBadRequest(uri, 'multipart/related;type=application/nope') CheckBadRequest(uri, 'multipart/related;range=') try: print('\nStarting the tests...') unittest.main(argv = [ sys.argv[0] ] + args.options) finally: print('\nDone')