1# -*- coding: utf-8 -*- 2# Copyright 2008-2018 pydicom authors. See LICENSE file for details. 3"""test cases for pydicom.filewriter module""" 4import tempfile 5from copy import deepcopy 6from datetime import date, datetime, time, timedelta, timezone 7from io import BytesIO 8import os 9from pathlib import Path 10from platform import python_implementation 11 12from struct import unpack 13from tempfile import TemporaryFile 14import zlib 15 16import pytest 17 18from pydicom._storage_sopclass_uids import CTImageStorage 19from pydicom import config, __version_info__, uid 20from pydicom.data import get_testdata_file, get_charset_files 21from pydicom.dataset import Dataset, FileDataset, FileMetaDataset 22from pydicom.dataelem import DataElement, RawDataElement 23from pydicom.filebase import DicomBytesIO 24from pydicom.filereader import dcmread, read_dataset 25from pydicom.filewriter import ( 26 write_data_element, write_dataset, correct_ambiguous_vr, 27 write_file_meta_info, correct_ambiguous_vr_element, write_numbers, 28 write_PN, _format_DT, write_text, write_OWvalue 29) 30from pydicom.multival import MultiValue 31from pydicom.sequence import Sequence 32from pydicom.uid import (ImplicitVRLittleEndian, ExplicitVRBigEndian, 33 PYDICOM_IMPLEMENTATION_UID) 34from pydicom.util.hexutil import hex2bytes 35from pydicom.valuerep import DA, DT, TM 36from pydicom.values import convert_text 37from ._write_stds import impl_LE_deflen_std_hex 38 39rtplan_name = get_testdata_file("rtplan.dcm") 40rtdose_name = get_testdata_file("rtdose.dcm") 41ct_name = get_testdata_file("CT_small.dcm") 42mr_name = get_testdata_file("MR_small.dcm") 43mr_implicit_name = get_testdata_file("MR_small_implicit.dcm") 44mr_bigendian_name = get_testdata_file("MR_small_bigendian.dcm") 45jpeg_name = get_testdata_file("JPEG2000.dcm") 46no_ts = get_testdata_file("meta_missing_tsyntax.dcm") 47color_pl_name = get_testdata_file("color-pl.dcm") 48sc_rgb_name = get_testdata_file("SC_rgb.dcm") 49datetime_name = mr_name 50 51unicode_name = get_charset_files("chrH31.dcm")[0] 52multiPN_name = get_charset_files("chrFrenMulti.dcm")[0] 53deflate_name = get_testdata_file("image_dfl.dcm") 54 55base_version = '.'.join(str(i) for i in __version_info__) 56 57 58def files_identical(a, b): 59 """Return a tuple (file a == file b, index of first difference)""" 60 with open(a, "rb") as A: 61 with open(b, "rb") as B: 62 a_bytes = A.read() 63 b_bytes = B.read() 64 65 return bytes_identical(a_bytes, b_bytes) 66 67 68def bytes_identical(a_bytes, b_bytes): 69 """Return a tuple 70 (bytes a == bytes b, index of first difference)""" 71 if len(a_bytes) != len(b_bytes): 72 return False, min([len(a_bytes), len(b_bytes)]) 73 elif a_bytes == b_bytes: 74 return True, 0 # True, dummy argument 75 else: 76 pos = 0 77 while a_bytes[pos] == b_bytes[pos]: 78 pos += 1 79 return False, pos # False if not identical, position of 1st diff 80 81 82def as_assertable(dataset): 83 """Copy the elements in a Dataset (including the file_meta, if any) 84 to a set that can be safely compared using pytest's assert. 85 (Datasets can't be so compared because DataElements are not 86 hashable.)""" 87 safe_dict = dict((str(elem.tag) + " " + elem.keyword, elem.value) 88 for elem in dataset) 89 if hasattr(dataset, "file_meta"): 90 safe_dict.update(as_assertable(dataset.file_meta)) 91 return safe_dict 92 93 94class TestWriteFile: 95 def setup(self): 96 self.file_out = TemporaryFile('w+b') 97 98 def teardown(self): 99 self.file_out.close() 100 101 def compare(self, in_filename): 102 """Read Dataset from in_filename, write to file, compare""" 103 with open(in_filename, 'rb') as f: 104 bytes_in = BytesIO(f.read()) 105 bytes_in.seek(0) 106 107 ds = dcmread(bytes_in) 108 ds.save_as(self.file_out, write_like_original=True) 109 self.file_out.seek(0) 110 bytes_out = BytesIO(self.file_out.read()) 111 bytes_in.seek(0) 112 bytes_out.seek(0) 113 same, pos = bytes_identical(bytes_in.getvalue(), bytes_out.getvalue()) 114 assert same 115 116 def compare_bytes(self, bytes_in, bytes_out): 117 """Compare two bytestreams for equality""" 118 same, pos = bytes_identical(bytes_in, bytes_out) 119 assert same 120 121 def testRTPlan(self): 122 """Input file, write back and verify 123 them identical (RT Plan file)""" 124 self.compare(rtplan_name) 125 126 def testRTDose(self): 127 """Input file, write back and 128 verify them identical (RT Dose file)""" 129 self.compare(rtdose_name) 130 131 def testCT(self): 132 """Input file, write back and 133 verify them identical (CT file).....""" 134 self.compare(ct_name) 135 136 def testMR(self): 137 """Input file, write back and verify 138 them identical (MR file).....""" 139 self.compare(mr_name) 140 141 def testUnicode(self): 142 """Ensure decoded string DataElements 143 are written to file properly""" 144 self.compare(unicode_name) 145 146 def testMultiPN(self): 147 """Ensure multiple Person Names are written 148 to the file correctly.""" 149 self.compare(multiPN_name) 150 151 def testJPEG2000(self): 152 """Input file, write back and verify 153 them identical (JPEG2K file).""" 154 self.compare(jpeg_name) 155 156 def test_pathlib_path_filename(self): 157 """Check that file can be written using pathlib.Path""" 158 ds = dcmread(Path(ct_name)) 159 ds.save_as(self.file_out, write_like_original=True) 160 self.file_out.seek(0) 161 ds1 = dcmread(self.file_out) 162 assert ds.PatientName == ds1.PatientName 163 164 def testListItemWriteBack(self): 165 """Change item in a list and confirm 166 it is written to file""" 167 DS_expected = 0 168 CS_expected = "new" 169 SS_expected = 999 170 ds = dcmread(ct_name) 171 ds.ImagePositionPatient[2] = DS_expected 172 ds.ImageType[1] = CS_expected 173 ds[(0x0043, 0x1012)].value[0] = SS_expected 174 ds.save_as(self.file_out, write_like_original=True) 175 self.file_out.seek(0) 176 # Now read it back in and check that the values were changed 177 ds = dcmread(self.file_out) 178 assert CS_expected == ds.ImageType[1] 179 assert SS_expected == ds[0x00431012].value[0] 180 assert DS_expected == ds.ImagePositionPatient[2] 181 182 def testwrite_short_uid(self): 183 ds = dcmread(rtplan_name) 184 ds.SOPInstanceUID = "1.2" 185 ds.save_as(self.file_out, write_like_original=True) 186 self.file_out.seek(0) 187 ds = dcmread(self.file_out) 188 assert "1.2" == ds.SOPInstanceUID 189 190 def test_write_no_ts(self): 191 """Test reading a file with no ts and writing it out identically.""" 192 ds = dcmread(no_ts) 193 ds.save_as(self.file_out, write_like_original=True) 194 self.file_out.seek(0) 195 with open(no_ts, 'rb') as ref_file: 196 written_bytes = self.file_out.read() 197 read_bytes = ref_file.read() 198 self.compare_bytes(read_bytes, written_bytes) 199 200 def test_write_double_filemeta(self): 201 """Test writing file meta from Dataset doesn't work""" 202 ds = dcmread(ct_name) 203 ds.TransferSyntaxUID = '1.1' 204 with pytest.raises(ValueError): 205 ds.save_as(self.file_out) 206 207 def test_write_ffff_ffff(self): 208 """Test writing element (FFFF, FFFF) to file #92""" 209 fp = DicomBytesIO() 210 ds = Dataset() 211 ds.file_meta = FileMetaDataset() 212 ds.is_little_endian = True 213 ds.is_implicit_VR = True 214 ds.add_new(0xFFFFFFFF, 'LO', '123456') 215 ds.save_as(fp, write_like_original=True) 216 217 fp.seek(0) 218 ds = dcmread(fp, force=True) 219 assert ds[0xFFFFFFFF].value == b'123456' 220 221 def test_write_removes_grouplength(self): 222 ds = dcmread(color_pl_name) 223 assert 0x00080000 in ds 224 ds.save_as(self.file_out, write_like_original=True) 225 self.file_out.seek(0) 226 ds = dcmread(self.file_out) 227 # group length has been removed 228 assert 0x00080000 not in ds 229 230 def test_write_empty_sequence(self): 231 """Make sure that empty sequence is correctly written.""" 232 # regression test for #1030 233 ds = dcmread(get_testdata_file('test-SR.dcm')) 234 ds.save_as(self.file_out) 235 self.file_out.seek(0) 236 ds = dcmread(self.file_out) 237 assert ds.PerformedProcedureCodeSequence == [] 238 239 def test_write_deflated_retains_elements(self): 240 """Read a Deflated Explicit VR Little Endian file, write it, 241 and then read the output, to verify that the written file 242 contains the same data. 243 """ 244 original = dcmread(deflate_name) 245 original.save_as(self.file_out) 246 247 self.file_out.seek(0) 248 rewritten = dcmread(self.file_out) 249 250 assert as_assertable(rewritten) == as_assertable(original) 251 252 def test_write_deflated_deflates_post_file_meta(self): 253 """Read a Deflated Explicit VR Little Endian file, write it, 254 and then check the bytes in the output, to verify that the 255 written file is deflated past the file meta information. 256 """ 257 original = dcmread(deflate_name) 258 original.save_as(self.file_out) 259 260 first_byte_past_file_meta = 0x14e 261 with open(deflate_name, "rb") as original_file: 262 original_file.seek(first_byte_past_file_meta) 263 original_post_meta_file_bytes = original_file.read() 264 unzipped_original = zlib.decompress(original_post_meta_file_bytes, 265 -zlib.MAX_WBITS) 266 267 self.file_out.seek(first_byte_past_file_meta) 268 rewritten_post_meta_file_bytes = self.file_out.read() 269 unzipped_rewritten = zlib.decompress(rewritten_post_meta_file_bytes, 270 -zlib.MAX_WBITS) 271 272 assert unzipped_rewritten == unzipped_original 273 274 def test_write_dataset_without_encoding(self): 275 """Test that write_dataset() raises if encoding not set.""" 276 ds = Dataset() 277 bs = BytesIO() 278 msg = ( 279 r"'Dataset.is_little_endian' and 'Dataset.is_implicit_VR' must " 280 r"be set appropriately before saving" 281 ) 282 with pytest.raises(AttributeError, match=msg): 283 write_dataset(bs, ds) 284 285 286class TestScratchWriteDateTime(TestWriteFile): 287 """Write and reread simple or multi-value DA/DT/TM data elements""" 288 289 def setup(self): 290 config.datetime_conversion = True 291 self.file_out = TemporaryFile('w+b') 292 293 def teardown(self): 294 config.datetime_conversion = False 295 self.file_out.close() 296 297 def test_multivalue_DA(self): 298 """Write DA/DT/TM data elements..........""" 299 multi_DA_expected = (date(1961, 8, 4), date(1963, 11, 22)) 300 DA_expected = date(1961, 8, 4) 301 tzinfo = timezone(timedelta(seconds=-21600), '-0600') 302 multi_DT_expected = (datetime(1961, 8, 4), datetime( 303 1963, 11, 22, 12, 30, 0, 0, tzinfo)) 304 multi_TM_expected = (time(1, 23, 45), time(11, 11, 11)) 305 TM_expected = time(11, 11, 11, 1) 306 ds = dcmread(datetime_name) 307 # Add date/time data elements 308 ds.CalibrationDate = MultiValue(DA, multi_DA_expected) 309 ds.DateOfLastCalibration = DA(DA_expected) 310 ds.ReferencedDateTime = MultiValue(DT, multi_DT_expected) 311 ds.CalibrationTime = MultiValue(TM, multi_TM_expected) 312 ds.TimeOfLastCalibration = TM(TM_expected) 313 ds.save_as(self.file_out, write_like_original=True) 314 self.file_out.seek(0) 315 # Now read it back in and check the values are as expected 316 ds = dcmread(self.file_out) 317 assert all([a == b 318 for a, b in zip(ds.CalibrationDate, multi_DA_expected)]) 319 assert DA_expected == ds.DateOfLastCalibration 320 assert all([a == b 321 for a, b in zip(ds.ReferencedDateTime, multi_DT_expected)]) 322 assert all([a == b 323 for a, b in zip(ds.CalibrationTime, multi_TM_expected)]) 324 assert TM_expected == ds.TimeOfLastCalibration 325 326 327class TestWriteDataElement: 328 """Attempt to write data elements has the expected behaviour""" 329 330 def setup(self): 331 # Create a dummy (in memory) file to write to 332 self.f1 = DicomBytesIO() 333 self.f1.is_little_endian = True 334 self.f1.is_implicit_VR = True 335 336 @staticmethod 337 def encode_element(elem, is_implicit_VR=True, is_little_endian=True): 338 """Return the encoded `elem`. 339 340 Parameters 341 ---------- 342 elem : pydicom.dataelem.DataElement 343 The element to encode 344 is_implicit_VR : bool 345 Encode using implicit VR, default True 346 is_little_endian : bool 347 Encode using little endian, default True 348 349 Returns 350 ------- 351 str or bytes 352 The encoded element as str (python2) or bytes (python3) 353 """ 354 with DicomBytesIO() as fp: 355 fp.is_implicit_VR = is_implicit_VR 356 fp.is_little_endian = is_little_endian 357 write_data_element(fp, elem) 358 return fp.parent.getvalue() 359 360 def test_empty_AT(self): 361 """Write empty AT correctly..........""" 362 # Was issue 74 363 data_elem = DataElement(0x00280009, "AT", []) 364 expected = hex2bytes(( 365 " 28 00 09 00" # (0028,0009) Frame Increment Pointer 366 " 00 00 00 00" # length 0 367 )) 368 write_data_element(self.f1, data_elem) 369 assert expected == self.f1.getvalue() 370 371 def check_data_element(self, data_elem, expected): 372 encoded_elem = self.encode_element(data_elem) 373 assert expected == encoded_elem 374 375 def test_write_empty_LO(self): 376 data_elem = DataElement(0x00080070, 'LO', None) 377 expected = (b'\x08\x00\x70\x00' # tag 378 b'\x00\x00\x00\x00' # length 379 ) # value 380 self.check_data_element(data_elem, expected) 381 382 def test_write_DA(self): 383 data_elem = DataElement(0x00080022, 'DA', '20000101') 384 expected = (b'\x08\x00\x22\x00' # tag 385 b'\x08\x00\x00\x00' # length 386 b'20000101') # value 387 self.check_data_element(data_elem, expected) 388 data_elem = DataElement(0x00080022, 'DA', date(2000, 1, 1)) 389 self.check_data_element(data_elem, expected) 390 391 def test_write_multi_DA(self): 392 data_elem = DataElement(0x0014407E, 'DA', ['20100101', b'20101231']) 393 expected = (b'\x14\x00\x7E\x40' # tag 394 b'\x12\x00\x00\x00' # length 395 b'20100101\\20101231 ') # padded value 396 self.check_data_element(data_elem, expected) 397 data_elem = DataElement(0x0014407E, 'DA', [date(2010, 1, 1), 398 date(2010, 12, 31)]) 399 self.check_data_element(data_elem, expected) 400 401 def test_write_TM(self): 402 data_elem = DataElement(0x00080030, 'TM', '010203') 403 expected = (b'\x08\x00\x30\x00' # tag 404 b'\x06\x00\x00\x00' # length 405 b'010203') # padded value 406 self.check_data_element(data_elem, expected) 407 data_elem = DataElement(0x00080030, 'TM', b'010203') 408 self.check_data_element(data_elem, expected) 409 data_elem = DataElement(0x00080030, 'TM', time(1, 2, 3)) 410 self.check_data_element(data_elem, expected) 411 412 def test_write_multi_TM(self): 413 data_elem = DataElement(0x0014407C, 'TM', ['082500', b'092655']) 414 expected = (b'\x14\x00\x7C\x40' # tag 415 b'\x0E\x00\x00\x00' # length 416 b'082500\\092655 ') # padded value 417 self.check_data_element(data_elem, expected) 418 data_elem = DataElement(0x0014407C, 'TM', [time(8, 25), 419 time(9, 26, 55)]) 420 self.check_data_element(data_elem, expected) 421 422 def test_write_DT(self): 423 data_elem = DataElement(0x0008002A, 'DT', '20170101120000') 424 expected = (b'\x08\x00\x2A\x00' # tag 425 b'\x0E\x00\x00\x00' # length 426 b'20170101120000') # value 427 self.check_data_element(data_elem, expected) 428 data_elem = DataElement(0x0008002A, 'DT', b'20170101120000') 429 self.check_data_element(data_elem, expected) 430 data_elem = DataElement(0x0008002A, 'DT', datetime(2017, 1, 1, 12)) 431 self.check_data_element(data_elem, expected) 432 433 def test_write_multi_DT(self): 434 data_elem = DataElement(0x0040A13A, 'DT', 435 ['20120820120804', b'20130901111111']) 436 expected = (b'\x40\x00\x3A\xA1' # tag 437 b'\x1E\x00\x00\x00' # length 438 b'20120820120804\\20130901111111 ') # padded value 439 self.check_data_element(data_elem, expected) 440 data_elem = DataElement( 441 0x0040A13A, 'DT', '20120820120804\\20130901111111') 442 self.check_data_element(data_elem, expected) 443 data_elem = DataElement( 444 0x0040A13A, 'DT', b'20120820120804\\20130901111111') 445 self.check_data_element(data_elem, expected) 446 447 data_elem = DataElement(0x0040A13A, 'DT', 448 [datetime(2012, 8, 20, 12, 8, 4), 449 datetime(2013, 9, 1, 11, 11, 11)]) 450 self.check_data_element(data_elem, expected) 451 452 def test_write_ascii_vr_with_padding(self): 453 expected = (b'\x08\x00\x54\x00' # tag 454 b'\x0C\x00\x00\x00' # length 455 b'CONQUESTSRV ') # padded value 456 data_elem = DataElement(0x00080054, 'AE', 'CONQUESTSRV') 457 self.check_data_element(data_elem, expected) 458 data_elem = DataElement(0x00080054, 'AE', b'CONQUESTSRV') 459 self.check_data_element(data_elem, expected) 460 461 expected = (b'\x08\x00\x62\x00' # tag 462 b'\x06\x00\x00\x00' # length 463 b'1.2.3\x00') # padded value 464 data_elem = DataElement(0x00080062, 'UI', '1.2.3') 465 self.check_data_element(data_elem, expected) 466 data_elem = DataElement(0x00080062, 'UI', b'1.2.3') 467 self.check_data_element(data_elem, expected) 468 469 expected = (b'\x08\x00\x60\x00' # tag 470 b'\x04\x00\x00\x00' # length 471 b'REG ') 472 data_elem = DataElement(0x00080060, 'CS', 'REG') 473 self.check_data_element(data_elem, expected) 474 data_elem = DataElement(0x00080060, 'CS', b'REG') 475 self.check_data_element(data_elem, expected) 476 477 def test_write_OD_implicit_little(self): 478 """Test writing elements with VR of OD works correctly.""" 479 # VolumetricCurvePoints 480 bytestring = b'\x00\x01\x02\x03\x04\x05\x06\x07' \ 481 b'\x01\x01\x02\x03\x04\x05\x06\x07' 482 elem = DataElement(0x0070150d, 'OD', bytestring) 483 encoded_elem = self.encode_element(elem) 484 # Tag pair (0070, 150d): 70 00 0d 15 485 # Length (16): 10 00 00 00 486 # | Tag | Length | Value -> 487 ref_bytes = b'\x70\x00\x0d\x15\x10\x00\x00\x00' + bytestring 488 assert ref_bytes == encoded_elem 489 490 # Empty data 491 elem.value = b'' 492 encoded_elem = self.encode_element(elem) 493 ref_bytes = b'\x70\x00\x0d\x15\x00\x00\x00\x00' 494 assert ref_bytes == encoded_elem 495 496 def test_write_OD_explicit_little(self): 497 """Test writing elements with VR of OD works correctly. 498 499 Elements with a VR of 'OD' use the newer explicit VR 500 encoding (see PS3.5 Section 7.1.2). 501 """ 502 # VolumetricCurvePoints 503 bytestring = b'\x00\x01\x02\x03\x04\x05\x06\x07' \ 504 b'\x01\x01\x02\x03\x04\x05\x06\x07' 505 elem = DataElement(0x0070150d, 'OD', bytestring) 506 encoded_elem = self.encode_element(elem, False, True) 507 # Tag pair (0070, 150d): 70 00 0d 15 508 # VR (OD): \x4f\x44 509 # Reserved: \x00\x00 510 # Length (16): \x10\x00\x00\x00 511 # | Tag | VR | 512 ref_bytes = b'\x70\x00\x0d\x15\x4f\x44' \ 513 b'\x00\x00\x10\x00\x00\x00' + bytestring 514 # |Rsrvd | Length | Value -> 515 assert ref_bytes == encoded_elem 516 517 # Empty data 518 elem.value = b'' 519 encoded_elem = self.encode_element(elem, False, True) 520 ref_bytes = b'\x70\x00\x0d\x15\x4f\x44\x00\x00\x00\x00\x00\x00' 521 assert ref_bytes == encoded_elem 522 523 def test_write_OL_implicit_little(self): 524 """Test writing elements with VR of OL works correctly.""" 525 # TrackPointIndexList 526 bytestring = b'\x00\x01\x02\x03\x04\x05\x06\x07' \ 527 b'\x01\x01\x02\x03' 528 elem = DataElement(0x00660129, 'OL', bytestring) 529 encoded_elem = self.encode_element(elem) 530 # Tag pair (0066, 0129): 66 00 29 01 531 # Length (12): 0c 00 00 00 532 # | Tag | Length | Value -> 533 ref_bytes = b'\x66\x00\x29\x01\x0c\x00\x00\x00' + bytestring 534 assert ref_bytes == encoded_elem 535 536 # Empty data 537 elem.value = b'' 538 encoded_elem = self.encode_element(elem) 539 ref_bytes = b'\x66\x00\x29\x01\x00\x00\x00\x00' 540 assert ref_bytes == encoded_elem 541 542 def test_write_OL_explicit_little(self): 543 """Test writing elements with VR of OL works correctly. 544 545 Elements with a VR of 'OL' use the newer explicit VR 546 encoding (see PS3.5 Section 7.1.2). 547 """ 548 # TrackPointIndexList 549 bytestring = b'\x00\x01\x02\x03\x04\x05\x06\x07' \ 550 b'\x01\x01\x02\x03' 551 elem = DataElement(0x00660129, 'OL', bytestring) 552 encoded_elem = self.encode_element(elem, False, True) 553 # Tag pair (0066, 0129): 66 00 29 01 554 # VR (OL): \x4f\x4c 555 # Reserved: \x00\x00 556 # Length (12): 0c 00 00 00 557 # | Tag | VR | 558 ref_bytes = b'\x66\x00\x29\x01\x4f\x4c' \ 559 b'\x00\x00\x0c\x00\x00\x00' + bytestring 560 # |Rsrvd | Length | Value -> 561 assert ref_bytes == encoded_elem 562 563 # Empty data 564 elem.value = b'' 565 encoded_elem = self.encode_element(elem, False, True) 566 ref_bytes = b'\x66\x00\x29\x01\x4f\x4c\x00\x00\x00\x00\x00\x00' 567 assert ref_bytes == encoded_elem 568 569 def test_write_UC_implicit_little(self): 570 """Test writing elements with VR of UC works correctly.""" 571 # VM 1, even data 572 elem = DataElement(0x00189908, 'UC', 'Test') 573 encoded_elem = self.encode_element(elem) 574 # Tag pair (0018, 9908): 08 00 20 01 575 # Length (4): 04 00 00 00 576 # Value: \x54\x65\x73\x74 577 ref_bytes = b'\x18\x00\x08\x99\x04\x00\x00\x00\x54\x65\x73\x74' 578 assert ref_bytes == encoded_elem 579 580 # VM 1, odd data - padded to even length 581 elem.value = 'Test.' 582 encoded_elem = self.encode_element(elem) 583 ref_bytes = b'\x18\x00\x08\x99\x06\x00\x00\x00\x54\x65\x73\x74\x2e\x20' 584 assert ref_bytes == encoded_elem 585 586 # VM 3, even data 587 elem.value = ['Aa', 'B', 'C'] 588 encoded_elem = self.encode_element(elem) 589 ref_bytes = b'\x18\x00\x08\x99\x06\x00\x00\x00\x41\x61\x5c\x42\x5c\x43' 590 assert ref_bytes == encoded_elem 591 592 # VM 3, odd data - padded to even length 593 elem.value = ['A', 'B', 'C'] 594 encoded_elem = self.encode_element(elem) 595 ref_bytes = b'\x18\x00\x08\x99\x06\x00\x00\x00\x41\x5c\x42\x5c\x43\x20' 596 assert ref_bytes == encoded_elem 597 598 # Empty data 599 elem.value = '' 600 encoded_elem = self.encode_element(elem) 601 ref_bytes = b'\x18\x00\x08\x99\x00\x00\x00\x00' 602 assert ref_bytes == encoded_elem 603 604 def test_write_UC_explicit_little(self): 605 """Test writing elements with VR of UC works correctly. 606 607 Elements with a VR of 'UC' use the newer explicit VR 608 encoding (see PS3.5 Section 7.1.2). 609 """ 610 # VM 1, even data 611 elem = DataElement(0x00189908, 'UC', 'Test') 612 encoded_elem = self.encode_element(elem, False, True) 613 # Tag pair (0018, 9908): 08 00 20 01 614 # VR (UC): \x55\x43 615 # Reserved: \x00\x00 616 # Length (4): \x04\x00\x00\x00 617 # Value: \x54\x65\x73\x74 618 ref_bytes = b'\x18\x00\x08\x99\x55\x43\x00\x00\x04\x00\x00\x00' \ 619 b'\x54\x65\x73\x74' 620 assert ref_bytes == encoded_elem 621 622 # VM 1, odd data - padded to even length 623 elem.value = 'Test.' 624 encoded_elem = self.encode_element(elem, False, True) 625 ref_bytes = b'\x18\x00\x08\x99\x55\x43\x00\x00\x06\x00\x00\x00' \ 626 b'\x54\x65\x73\x74\x2e\x20' 627 assert ref_bytes == encoded_elem 628 629 # VM 3, even data 630 elem.value = ['Aa', 'B', 'C'] 631 encoded_elem = self.encode_element(elem, False, True) 632 ref_bytes = b'\x18\x00\x08\x99\x55\x43\x00\x00\x06\x00\x00\x00' \ 633 b'\x41\x61\x5c\x42\x5c\x43' 634 assert ref_bytes == encoded_elem 635 636 # VM 3, odd data - padded to even length 637 elem.value = ['A', 'B', 'C'] 638 encoded_elem = self.encode_element(elem, False, True) 639 ref_bytes = b'\x18\x00\x08\x99\x55\x43\x00\x00\x06\x00\x00\x00' \ 640 b'\x41\x5c\x42\x5c\x43\x20' 641 assert ref_bytes == encoded_elem 642 643 # Empty data 644 elem.value = '' 645 encoded_elem = self.encode_element(elem, False, True) 646 ref_bytes = b'\x18\x00\x08\x99\x55\x43\x00\x00\x00\x00\x00\x00' 647 assert ref_bytes == encoded_elem 648 649 def test_write_UR_implicit_little(self): 650 """Test writing elements with VR of UR works correctly.""" 651 # Even length URL 652 elem = DataElement(0x00080120, 'UR', 653 'http://github.com/darcymason/pydicom') 654 encoded_elem = self.encode_element(elem) 655 # Tag pair (0008, 2001): 08 00 20 01 656 # Length (36): 24 00 00 00 657 # Value: 68 to 6d 658 ref_bytes = b'\x08\x00\x20\x01\x24\x00\x00\x00\x68\x74' \ 659 b'\x74\x70\x3a\x2f\x2f\x67\x69\x74\x68\x75' \ 660 b'\x62\x2e\x63\x6f\x6d\x2f\x64\x61\x72\x63' \ 661 b'\x79\x6d\x61\x73\x6f\x6e\x2f\x70\x79\x64' \ 662 b'\x69\x63\x6f\x6d' 663 assert ref_bytes == encoded_elem 664 665 # Odd length URL has trailing \x20 (SPACE) padding 666 elem.value = '../test/test.py' 667 encoded_elem = self.encode_element(elem) 668 # Tag pair (0008, 2001): 08 00 20 01 669 # Length (16): 10 00 00 00 670 # Value: 2e to 20 671 ref_bytes = b'\x08\x00\x20\x01\x10\x00\x00\x00\x2e\x2e' \ 672 b'\x2f\x74\x65\x73\x74\x2f\x74\x65\x73\x74' \ 673 b'\x2e\x70\x79\x20' 674 assert ref_bytes == encoded_elem 675 676 # Empty value 677 elem.value = '' 678 encoded_elem = self.encode_element(elem) 679 assert b'\x08\x00\x20\x01\x00\x00\x00\x00' == encoded_elem 680 681 def test_write_UR_explicit_little(self): 682 """Test writing elements with VR of UR works correctly. 683 684 Elements with a VR of 'UR' use the newer explicit VR 685 encoded (see PS3.5 Section 7.1.2). 686 """ 687 # Even length URL 688 elem = DataElement(0x00080120, 'UR', 'ftp://bits') 689 encoded_elem = self.encode_element(elem, False, True) 690 # Tag pair (0008, 2001): 08 00 20 01 691 # VR (UR): \x55\x52 692 # Reserved: \x00\x00 693 # Length (4): \x0a\x00\x00\x00 694 # Value: \x66\x74\x70\x3a\x2f\x2f\x62\x69\x74\x73 695 ref_bytes = b'\x08\x00\x20\x01\x55\x52\x00\x00\x0a\x00\x00\x00' \ 696 b'\x66\x74\x70\x3a\x2f\x2f\x62\x69\x74\x73' 697 assert ref_bytes == encoded_elem 698 699 # Odd length URL has trailing \x20 (SPACE) padding 700 elem.value = 'ftp://bit' 701 encoded_elem = self.encode_element(elem, False, True) 702 ref_bytes = b'\x08\x00\x20\x01\x55\x52\x00\x00\x0a\x00\x00\x00' \ 703 b'\x66\x74\x70\x3a\x2f\x2f\x62\x69\x74\x20' 704 assert ref_bytes == encoded_elem 705 706 # Empty value 707 elem.value = '' 708 encoded_elem = self.encode_element(elem, False, True) 709 ref_bytes = b'\x08\x00\x20\x01\x55\x52\x00\x00\x00\x00\x00\x00' 710 assert ref_bytes == encoded_elem 711 712 def test_write_UN_implicit_little(self): 713 """Test writing UN VR in implicit little""" 714 elem = DataElement(0x00100010, 'UN', b'\x01\x02') 715 assert self.encode_element(elem) == ( 716 b'\x10\x00\x10\x00\x02\x00\x00\x00\x01\x02') 717 718 def test_write_unknown_vr_raises(self): 719 """Test exception raised trying to write unknown VR element""" 720 fp = DicomBytesIO() 721 fp.is_implicit_VR = True 722 fp.is_little_endian = True 723 elem = DataElement(0x00100010, 'ZZ', 'Test') 724 with pytest.raises(NotImplementedError, 725 match="write_data_element: unknown Value " 726 "Representation 'ZZ'"): 727 write_data_element(fp, elem) 728 729 730class TestCorrectAmbiguousVR: 731 """Test correct_ambiguous_vr.""" 732 733 def test_pixel_representation_vm_one(self): 734 """Test correcting VM 1 elements which require PixelRepresentation.""" 735 ref_ds = Dataset() 736 737 # If PixelRepresentation is 0 then VR should be US 738 ref_ds.PixelRepresentation = 0 739 ref_ds.SmallestValidPixelValue = b'\x00\x01' # Little endian 256 740 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 741 assert 256 == ds.SmallestValidPixelValue 742 assert 'US' == ds[0x00280104].VR 743 744 # If PixelRepresentation is 1 then VR should be SS 745 ref_ds.PixelRepresentation = 1 746 ref_ds.SmallestValidPixelValue = b'\x00\x01' # Big endian 1 747 ds = correct_ambiguous_vr(deepcopy(ref_ds), False) 748 assert 1 == ds.SmallestValidPixelValue 749 assert 'SS' == ds[0x00280104].VR 750 751 # If no PixelRepresentation and no PixelData is present 'US' is set 752 ref_ds = Dataset() 753 ref_ds.SmallestValidPixelValue = b'\x00\x01' # Big endian 1 754 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 755 assert 'US' == ds[0x00280104].VR 756 757 # If no PixelRepresentation but PixelData is present 758 # AttributeError shall be raised 759 ref_ds.PixelData = b'123' 760 with pytest.raises(AttributeError, 761 match=r"Failed to resolve ambiguous VR for tag " 762 r"\(0028, 0104\):.* 'PixelRepresentation'"): 763 correct_ambiguous_vr(deepcopy(ref_ds), True) 764 765 def test_pixel_representation_vm_three(self): 766 """Test correcting VM 3 elements which require PixelRepresentation.""" 767 ref_ds = Dataset() 768 769 # If PixelRepresentation is 0 then VR should be US - Little endian 770 ref_ds.PixelRepresentation = 0 771 ref_ds.LUTDescriptor = b'\x01\x00\x00\x01\x10\x00' # 1\256\16 772 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 773 assert [1, 256, 16] == ds.LUTDescriptor 774 assert 'US' == ds[0x00283002].VR 775 776 # If PixelRepresentation is 1 then VR should be SS 777 ref_ds.PixelRepresentation = 1 778 ref_ds.LUTDescriptor = b'\x01\x00\x00\x01\x00\x10' 779 ds = correct_ambiguous_vr(deepcopy(ref_ds), False) 780 assert [256, 1, 16] == ds.LUTDescriptor 781 assert 'SS' == ds[0x00283002].VR 782 783 # If no PixelRepresentation and no PixelData is present 'US' is set 784 ref_ds = Dataset() 785 ref_ds.LUTDescriptor = b'\x01\x00\x00\x01\x00\x10' 786 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 787 assert 'US' == ds[0x00283002].VR 788 789 # If no PixelRepresentation AttributeError shall be raised 790 ref_ds.PixelData = b'123' 791 with pytest.raises(AttributeError, 792 match=r"Failed to resolve ambiguous VR for tag " 793 r"\(0028, 3002\):.* 'PixelRepresentation'"): 794 correct_ambiguous_vr(deepcopy(ref_ds), False) 795 796 def test_pixel_data(self): 797 """Test correcting PixelData.""" 798 ref_ds = Dataset() 799 800 # If BitsAllocated > 8 then VR must be OW 801 ref_ds.BitsAllocated = 16 802 ref_ds.PixelData = b'\x00\x01' # Little endian 256 803 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) # Little endian 804 assert b'\x00\x01' == ds.PixelData 805 assert 'OW' == ds[0x7fe00010].VR 806 ds = correct_ambiguous_vr(deepcopy(ref_ds), False) # Big endian 807 assert b'\x00\x01' == ds.PixelData 808 assert 'OW' == ds[0x7fe00010].VR 809 810 # If BitsAllocated <= 8 then VR can be OB or OW: we set it to OB 811 ref_ds = Dataset() 812 ref_ds.BitsAllocated = 8 813 ref_ds.Rows = 2 814 ref_ds.Columns = 2 815 ref_ds.PixelData = b'\x01\x00\x02\x00\x03\x00\x04\x00' 816 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 817 assert b'\x01\x00\x02\x00\x03\x00\x04\x00' == ds.PixelData 818 assert 'OB' == ds[0x7fe00010].VR 819 820 # If no BitsAllocated set then AttributesError is raised 821 ref_ds = Dataset() 822 ref_ds.PixelData = b'\x00\x01' # Big endian 1 823 with pytest.raises(AttributeError, 824 match=r"Failed to resolve ambiguous VR for tag " 825 r"\(7fe0, 0010\):.* 'BitsAllocated'"): 826 correct_ambiguous_vr(deepcopy(ref_ds), True) 827 828 def test_waveform_bits_allocated(self): 829 """Test correcting elements which require WaveformBitsAllocated.""" 830 ref_ds = Dataset() 831 ref_ds.is_implicit_VR = False 832 833 # If WaveformBitsAllocated > 8 then VR must be OW 834 ref_ds.WaveformBitsAllocated = 16 835 ref_ds.WaveformData = b'\x00\x01' # Little endian 256 836 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) # Little endian 837 assert b'\x00\x01' == ds.WaveformData 838 assert 'OW' == ds[0x54001010].VR 839 ds = correct_ambiguous_vr(deepcopy(ref_ds), False) # Big endian 840 assert b'\x00\x01' == ds.WaveformData 841 assert 'OW' == ds[0x54001010].VR 842 843 # If WaveformBitsAllocated == 8 then VR is OB or OW - set it to OB 844 ref_ds.WaveformBitsAllocated = 8 845 ref_ds.WaveformData = b'\x01\x02' 846 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 847 assert b'\x01\x02' == ds.WaveformData 848 assert 'OB' == ds[0x54001010].VR 849 850 # For implicit VR, VR is always OW 851 ref_ds.is_implicit_VR = True 852 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 853 assert b'\x01\x02' == ds.WaveformData 854 assert 'OW' == ds[0x54001010].VR 855 ref_ds.is_implicit_VR = False 856 857 # If no WaveformBitsAllocated then AttributeError shall be raised 858 ref_ds = Dataset() 859 ref_ds.WaveformData = b'\x00\x01' # Big endian 1 860 with pytest.raises(AttributeError, 861 match=r"Failed to resolve ambiguous VR for tag " 862 r"\(5400, 1010\):.* 'WaveformBitsAllocated'"): 863 correct_ambiguous_vr(deepcopy(ref_ds), True) 864 865 def test_lut_descriptor(self): 866 """Test correcting elements which require LUTDescriptor.""" 867 ref_ds = Dataset() 868 ref_ds.PixelRepresentation = 0 869 870 # If LUTDescriptor[0] is 1 then LUTData VR is 'US' 871 ref_ds.LUTDescriptor = b'\x01\x00\x00\x01\x10\x00' # 1\256\16 872 ref_ds.LUTData = b'\x00\x01' # Little endian 256 873 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) # Little endian 874 assert 1 == ds.LUTDescriptor[0] 875 assert 'US' == ds[0x00283002].VR 876 assert 256 == ds.LUTData 877 assert 'US' == ds[0x00283006].VR 878 879 # If LUTDescriptor[0] is not 1 then LUTData VR is 'OW' 880 ref_ds.LUTDescriptor = b'\x02\x00\x00\x01\x10\x00' # 2\256\16 881 ref_ds.LUTData = b'\x00\x01\x00\x02' 882 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) # Little endian 883 assert 2 == ds.LUTDescriptor[0] 884 assert 'US' == ds[0x00283002].VR 885 assert b'\x00\x01\x00\x02' == ds.LUTData 886 assert 'OW' == ds[0x00283006].VR 887 888 # If no LUTDescriptor then raise AttributeError 889 ref_ds = Dataset() 890 ref_ds.LUTData = b'\x00\x01' 891 with pytest.raises(AttributeError, 892 match=r"Failed to resolve ambiguous VR for tag " 893 r"\(0028, 3006\):.* 'LUTDescriptor'"): 894 correct_ambiguous_vr(deepcopy(ref_ds), True) 895 896 def test_overlay(self): 897 """Test correcting OverlayData""" 898 # VR must be 'OW' 899 ref_ds = Dataset() 900 ref_ds.is_implicit_VR = True 901 ref_ds.add(DataElement(0x60003000, 'OB or OW', b'\x00')) 902 ref_ds.add(DataElement(0x601E3000, 'OB or OW', b'\x00')) 903 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 904 assert 'OW' == ds[0x60003000].VR 905 assert 'OW' == ds[0x601E3000].VR 906 assert 'OB or OW' == ref_ds[0x60003000].VR 907 assert 'OB or OW' == ref_ds[0x601E3000].VR 908 909 ref_ds.is_implicit_VR = False 910 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 911 assert 'OW' == ds[0x60003000].VR 912 assert 'OB or OW' == ref_ds[0x60003000].VR 913 914 def test_sequence(self): 915 """Test correcting elements in a sequence.""" 916 ref_ds = Dataset() 917 ref_ds.BeamSequence = [Dataset()] 918 ref_ds.BeamSequence[0].PixelRepresentation = 0 919 ref_ds.BeamSequence[0].SmallestValidPixelValue = b'\x00\x01' 920 ref_ds.BeamSequence[0].BeamSequence = [Dataset()] 921 922 ref_ds.BeamSequence[0].BeamSequence[0].PixelRepresentation = 0 923 ref_ds.BeamSequence[0].BeamSequence[0].SmallestValidPixelValue = \ 924 b'\x00\x01' 925 926 ds = correct_ambiguous_vr(deepcopy(ref_ds), True) 927 assert ds.BeamSequence[0].SmallestValidPixelValue == 256 928 assert ds.BeamSequence[0][0x00280104].VR == 'US' 929 assert ( 930 ds.BeamSequence[0].BeamSequence[ 931 0].SmallestValidPixelValue == 256) 932 assert ds.BeamSequence[0].BeamSequence[0][0x00280104].VR == 'US' 933 934 def test_write_new_ambiguous(self): 935 """Regression test for #781""" 936 ds = Dataset() 937 ds.is_little_endian = True 938 ds.is_implicit_VR = True 939 ds.SmallestImagePixelValue = 0 940 assert ds[0x00280106].VR == 'US or SS' 941 ds.PixelRepresentation = 0 942 ds.LUTDescriptor = [1, 0] 943 assert ds[0x00283002].VR == 'US or SS' 944 ds.LUTData = 0 945 assert ds[0x00283006].VR == 'US or OW' 946 ds.save_as(DicomBytesIO()) 947 948 assert ds[0x00280106].VR == 'US' 949 assert ds.SmallestImagePixelValue == 0 950 assert ds[0x00283006].VR == 'US' 951 assert ds.LUTData == 0 952 assert ds[0x00283002].VR == 'US' 953 assert ds.LUTDescriptor == [1, 0] 954 955 def dataset_with_modality_lut_sequence(self, pixel_repr): 956 ds = Dataset() 957 ds.PixelRepresentation = pixel_repr 958 ds.ModalityLUTSequence = [Dataset()] 959 ds.ModalityLUTSequence[0].LUTDescriptor = [0, 0, 16] 960 ds.ModalityLUTSequence[0].LUTExplanation = None 961 ds.ModalityLUTSequence[0].ModalityLUTType = 'US' # US = unspecified 962 ds.ModalityLUTSequence[0].LUTData = b'\x0000\x149a\x1f1c\xc2637' 963 ds.is_little_endian = True 964 return ds 965 966 def test_ambiguous_element_in_sequence_explicit_using_attribute(self): 967 """Test that writing a sequence with an ambiguous element 968 as explicit transfer syntax works if accessing the tag via keyword.""" 969 # regression test for #804 970 ds = self.dataset_with_modality_lut_sequence(pixel_repr=0) 971 ds.is_implicit_VR = False 972 fp = BytesIO() 973 ds.save_as(fp, write_like_original=True) 974 ds = dcmread(fp, force=True) 975 assert 'US' == ds.ModalityLUTSequence[0][0x00283002].VR 976 977 ds = self.dataset_with_modality_lut_sequence(pixel_repr=1) 978 ds.is_implicit_VR = False 979 fp = BytesIO() 980 ds.save_as(fp, write_like_original=True) 981 ds = dcmread(fp, force=True) 982 assert 'SS' == ds.ModalityLUTSequence[0][0x00283002].VR 983 984 def test_ambiguous_element_in_sequence_explicit_using_index(self): 985 """Test that writing a sequence with an ambiguous element 986 as explicit transfer syntax works if accessing the tag 987 via the tag number.""" 988 ds = self.dataset_with_modality_lut_sequence(pixel_repr=0) 989 ds.is_implicit_VR = False 990 fp = BytesIO() 991 ds.save_as(fp, write_like_original=True) 992 ds = dcmread(fp, force=True) 993 assert 'US' == ds[0x00283000][0][0x00283002].VR 994 995 ds = self.dataset_with_modality_lut_sequence(pixel_repr=1) 996 ds.is_implicit_VR = False 997 fp = BytesIO() 998 ds.save_as(fp, write_like_original=True) 999 ds = dcmread(fp, force=True) 1000 assert 'SS' == ds[0x00283000][0][0x00283002].VR 1001 1002 def test_ambiguous_element_in_sequence_implicit_using_attribute(self): 1003 """Test that reading a sequence with an ambiguous element 1004 from a file with implicit transfer syntax works if accessing the 1005 tag via keyword.""" 1006 # regression test for #804 1007 ds = self.dataset_with_modality_lut_sequence(pixel_repr=0) 1008 ds.is_implicit_VR = True 1009 fp = BytesIO() 1010 ds.save_as(fp, write_like_original=True) 1011 ds = dcmread(fp, force=True) 1012 assert 'US' == ds.ModalityLUTSequence[0][0x00283002].VR 1013 1014 ds = self.dataset_with_modality_lut_sequence(pixel_repr=1) 1015 ds.is_implicit_VR = True 1016 fp = BytesIO() 1017 ds.save_as(fp, write_like_original=True) 1018 ds = dcmread(fp, force=True) 1019 assert 'SS' == ds.ModalityLUTSequence[0][0x00283002].VR 1020 1021 def test_ambiguous_element_in_sequence_implicit_using_index(self): 1022 """Test that reading a sequence with an ambiguous element 1023 from a file with implicit transfer syntax works if accessing the tag 1024 via the tag number.""" 1025 ds = self.dataset_with_modality_lut_sequence(pixel_repr=0) 1026 ds.is_implicit_VR = True 1027 fp = BytesIO() 1028 ds.save_as(fp, write_like_original=True) 1029 ds = dcmread(fp, force=True) 1030 assert 'US' == ds[0x00283000][0][0x00283002].VR 1031 1032 ds = self.dataset_with_modality_lut_sequence(pixel_repr=1) 1033 ds.is_implicit_VR = True 1034 fp = BytesIO() 1035 ds.save_as(fp, write_like_original=True) 1036 ds = dcmread(fp, force=True) 1037 assert 'SS' == ds[0x00283000][0][0x00283002].VR 1038 1039 1040class TestCorrectAmbiguousVRElement: 1041 """Test filewriter.correct_ambiguous_vr_element""" 1042 1043 def test_not_ambiguous(self): 1044 """Test no change in element if not ambiguous""" 1045 elem = DataElement(0x60003000, 'OB', b'\x00') 1046 out = correct_ambiguous_vr_element(elem, Dataset(), True) 1047 assert out.VR == 'OB' 1048 assert out.tag == 0x60003000 1049 assert out.value == b'\x00' 1050 1051 def test_not_ambiguous_raw_data_element(self): 1052 """Test no change in raw data element if not ambiguous""" 1053 elem = RawDataElement(0x60003000, 'OB', 1, b'\x00', 0, True, True) 1054 out = correct_ambiguous_vr_element(elem, Dataset(), True) 1055 assert out == elem 1056 assert isinstance(out, RawDataElement) 1057 1058 def test_correct_ambiguous_data_element(self): 1059 """Test correct ambiguous US/SS element""" 1060 ds = Dataset() 1061 ds.PixelPaddingValue = b'\xfe\xff' 1062 out = correct_ambiguous_vr_element(ds[0x00280120], ds, True) 1063 # assume US if PixelData is not set 1064 assert 'US' == out.VR 1065 1066 ds = Dataset() 1067 ds.PixelPaddingValue = b'\xfe\xff' 1068 ds.PixelData = b'3456' 1069 with pytest.raises(AttributeError, 1070 match=r"Failed to resolve ambiguous VR for tag " 1071 r"\(0028, 0120\):.* 'PixelRepresentation'"): 1072 correct_ambiguous_vr_element(ds[0x00280120], ds, True) 1073 1074 ds.PixelRepresentation = 0 1075 out = correct_ambiguous_vr_element(ds[0x00280120], ds, True) 1076 assert out.VR == 'US' 1077 assert out.value == 0xfffe 1078 1079 def test_correct_ambiguous_raw_data_element(self): 1080 """Test that correcting ambiguous US/SS raw data element 1081 works and converts it to a data element""" 1082 ds = Dataset() 1083 elem = RawDataElement( 1084 0x00280120, 'US or SS', 2, b'\xfe\xff', 0, True, True) 1085 ds[0x00280120] = elem 1086 ds.PixelRepresentation = 0 1087 out = correct_ambiguous_vr_element(elem, ds, True) 1088 assert isinstance(out, DataElement) 1089 assert out.VR == 'US' 1090 assert out.value == 0xfffe 1091 1092 def test_empty_value(self): 1093 """Regression test for #1193: empty value raises exception.""" 1094 ds = Dataset() 1095 elem = RawDataElement(0x00280106, 'US or SS', 0, None, 0, True, True) 1096 ds[0x00280106] = elem 1097 out = correct_ambiguous_vr_element(elem, ds, True) 1098 assert isinstance(out, DataElement) 1099 assert out.VR == 'US' 1100 1101 ds.LUTDescriptor = [1, 1, 1] 1102 elem = RawDataElement(0x00283006, 'US or SS', 0, None, 0, True, True) 1103 assert out.value is None 1104 ds[0x00283006] = elem 1105 out = correct_ambiguous_vr_element(elem, ds, True) 1106 assert isinstance(out, DataElement) 1107 assert out.VR == 'US' 1108 assert out.value is None 1109 1110 1111class TestWriteAmbiguousVR: 1112 """Attempt to write data elements with ambiguous VR.""" 1113 1114 def setup(self): 1115 # Create a dummy (in memory) file to write to 1116 self.fp = DicomBytesIO() 1117 self.fp.is_implicit_VR = False 1118 self.fp.is_little_endian = True 1119 1120 def test_write_explicit_vr_raises(self): 1121 """Test writing explicit vr raises exception if unsolved element.""" 1122 ds = Dataset() 1123 ds.PerimeterValue = b'\x00\x01' 1124 with pytest.raises(ValueError): 1125 write_dataset(self.fp, ds) 1126 1127 def test_write_explicit_vr_little_endian(self): 1128 """Test writing explicit little data for ambiguous elements.""" 1129 # Create a dataset containing element with ambiguous VRs 1130 ref_ds = Dataset() 1131 ref_ds.PixelRepresentation = 0 1132 ref_ds.SmallestValidPixelValue = b'\x00\x01' # Little endian 256 1133 1134 fp = BytesIO() 1135 file_ds = FileDataset(fp, ref_ds) 1136 file_ds.is_implicit_VR = False 1137 file_ds.is_little_endian = True 1138 file_ds.save_as(fp, write_like_original=True) 1139 fp.seek(0) 1140 1141 ds = read_dataset(fp, False, True, parent_encoding='latin1') 1142 assert 256 == ds.SmallestValidPixelValue 1143 assert 'US' == ds[0x00280104].VR 1144 assert not ds.read_implicit_vr 1145 assert ds.read_little_endian 1146 assert ds.read_encoding == 'latin1' 1147 1148 def test_write_explicit_vr_big_endian(self): 1149 """Test writing explicit big data for ambiguous elements.""" 1150 # Create a dataset containing element with ambiguous VRs 1151 ref_ds = Dataset() 1152 ref_ds.PixelRepresentation = 1 1153 ref_ds.SmallestValidPixelValue = b'\x00\x01' # Big endian 1 1154 ref_ds.SpecificCharacterSet = b'ISO_IR 192' 1155 1156 fp = BytesIO() 1157 file_ds = FileDataset(fp, ref_ds) 1158 file_ds.is_implicit_VR = False 1159 file_ds.is_little_endian = False 1160 file_ds.save_as(fp, write_like_original=True) 1161 fp.seek(0) 1162 1163 ds = read_dataset(fp, False, False) 1164 assert 1 == ds.SmallestValidPixelValue 1165 assert 'SS' == ds[0x00280104].VR 1166 assert not ds.read_implicit_vr 1167 assert not ds.read_little_endian 1168 assert ['UTF8'] == ds.read_encoding 1169 1170 1171class TestScratchWrite: 1172 """Simple dataset from scratch, written in all endian/VR combinations""" 1173 1174 def setup(self): 1175 # Create simple dataset for all tests 1176 ds = Dataset() 1177 ds.PatientName = "Name^Patient" 1178 ds.InstanceNumber = None 1179 1180 # Set up a simple nested sequence 1181 # first, the innermost sequence 1182 subitem1 = Dataset() 1183 subitem1.ContourNumber = 1 1184 subitem1.ContourData = ['2', '4', '8', '16'] 1185 subitem2 = Dataset() 1186 subitem2.ContourNumber = 2 1187 subitem2.ContourData = ['32', '64', '128', '196'] 1188 1189 sub_ds = Dataset() 1190 sub_ds.ContourSequence = Sequence((subitem1, subitem2)) 1191 1192 # Now the top-level sequence 1193 ds.ROIContourSequence = Sequence((sub_ds,)) # Comma to make one-tuple 1194 1195 # Store so each test can use it 1196 self.ds = ds 1197 1198 def compare_write(self, hex_std, file_ds): 1199 """Write file and compare with expected byte string 1200 1201 :arg hex_std: the bytes which should be written, as space separated hex 1202 :arg file_ds: a FileDataset instance containing the dataset to write 1203 """ 1204 out_filename = "scratch.dcm" 1205 file_ds.save_as(out_filename, write_like_original=True) 1206 std = hex2bytes(hex_std) 1207 with open(out_filename, 'rb') as f: 1208 bytes_written = f.read() 1209 # print "std :", bytes2hex(std) 1210 # print "written:", bytes2hex(bytes_written) 1211 same, pos = bytes_identical(std, bytes_written) 1212 assert same 1213 1214 if os.path.exists(out_filename): 1215 os.remove(out_filename) # get rid of the file 1216 1217 def testImpl_LE_deflen_write(self): 1218 """Scratch Write for implicit VR little endian, defined length SQs""" 1219 file_ds = FileDataset("test", self.ds) 1220 self.compare_write(impl_LE_deflen_std_hex, file_ds) 1221 1222 1223class TestWriteToStandard: 1224 """Unit tests for writing datasets to the DICOM standard""" 1225 1226 def test_preamble_default(self): 1227 """Test that the default preamble is written correctly when present.""" 1228 fp = DicomBytesIO() 1229 ds = dcmread(ct_name) 1230 ds.preamble = b'\x00' * 128 1231 ds.save_as(fp, write_like_original=False) 1232 fp.seek(0) 1233 assert fp.read(128) == b'\x00' * 128 1234 1235 def test_preamble_custom(self): 1236 """Test that a custom preamble is written correctly when present.""" 1237 fp = DicomBytesIO() 1238 ds = dcmread(ct_name) 1239 ds.preamble = b'\x01\x02\x03\x04' + b'\x00' * 124 1240 ds.save_as(fp, write_like_original=False) 1241 fp.seek(0) 1242 assert fp.read(128) == b'\x01\x02\x03\x04' + b'\x00' * 124 1243 1244 def test_no_preamble(self): 1245 """Test that a default preamble is written when absent.""" 1246 fp = DicomBytesIO() 1247 ds = dcmread(ct_name) 1248 del ds.preamble 1249 ds.save_as(fp, write_like_original=False) 1250 fp.seek(0) 1251 assert fp.read(128) == b'\x00' * 128 1252 1253 def test_none_preamble(self): 1254 """Test that a default preamble is written when None.""" 1255 fp = DicomBytesIO() 1256 ds = dcmread(ct_name) 1257 ds.preamble = None 1258 ds.save_as(fp, write_like_original=False) 1259 fp.seek(0) 1260 assert fp.read(128) == b'\x00' * 128 1261 1262 def test_bad_preamble(self): 1263 """Test that ValueError is raised when preamble is bad.""" 1264 ds = dcmread(ct_name) 1265 ds.preamble = b'\x00' * 127 1266 with pytest.raises(ValueError): 1267 ds.save_as(DicomBytesIO(), write_like_original=False) 1268 ds.preamble = b'\x00' * 129 1269 with pytest.raises(ValueError): 1270 ds.save_as(DicomBytesIO(), write_like_original=False) 1271 1272 def test_bad_filename(self): 1273 """Test that TypeError is raised for a bad filename.""" 1274 ds = dcmread(ct_name) 1275 with pytest.raises(TypeError, match="dcmwrite: Expected a file path " 1276 "or a file-like, but got None"): 1277 ds.save_as(None) 1278 with pytest.raises(TypeError, match="dcmwrite: Expected a file path " 1279 "or a file-like, but got int"): 1280 ds.save_as(42) 1281 1282 def test_prefix(self): 1283 """Test that the 'DICM' prefix 1284 is written with preamble.""" 1285 # Has preamble 1286 fp = DicomBytesIO() 1287 ds = dcmread(ct_name) 1288 ds.preamble = b'\x00' * 128 1289 ds.save_as(fp, write_like_original=False) 1290 fp.seek(128) 1291 assert fp.read(4) == b'DICM' 1292 1293 def test_prefix_none(self): 1294 """Test the 'DICM' prefix is written when preamble is None""" 1295 fp = DicomBytesIO() 1296 ds = dcmread(ct_name) 1297 ds.preamble = None 1298 ds.save_as(fp, write_like_original=False) 1299 fp.seek(128) 1300 assert fp.read(4) == b'DICM' 1301 1302 def test_ds_changed(self): 1303 """Test writing the dataset changes its file_meta.""" 1304 ds = dcmread(rtplan_name) 1305 ref_ds = dcmread(rtplan_name) 1306 for ref_elem, test_elem in zip(ref_ds.file_meta, ds.file_meta): 1307 assert ref_elem == test_elem 1308 1309 ds.save_as(DicomBytesIO(), write_like_original=False) 1310 assert ref_ds.file_meta != ds.file_meta 1311 del ref_ds.file_meta 1312 del ds.file_meta 1313 1314 # Ensure no RawDataElements in ref_ds and ds 1315 for _ in ref_ds: 1316 pass 1317 for _ in ds: 1318 pass 1319 assert ref_ds == ds 1320 1321 def test_raw_elements_preserved_implicit_vr(self): 1322 """Test writing the dataset preserves raw elements.""" 1323 ds = dcmread(rtplan_name) 1324 1325 # raw data elements after reading 1326 assert ds.get_item(0x00080070).is_raw # Manufacturer 1327 assert ds.get_item(0x00100020).is_raw # Patient ID 1328 assert ds.get_item(0x300A0006).is_raw # RT Plan Date 1329 assert ds.get_item(0x300A0010).is_raw # Dose Reference Sequence 1330 1331 ds.save_as(DicomBytesIO(), write_like_original=False) 1332 1333 # data set still contains raw data elements after writing 1334 assert ds.get_item(0x00080070).is_raw # Manufacturer 1335 assert ds.get_item(0x00100020).is_raw # Patient ID 1336 assert ds.get_item(0x300A0006).is_raw # RT Plan Date 1337 assert ds.get_item(0x300A0010).is_raw # Dose Reference Sequence 1338 1339 def test_raw_elements_preserved_explicit_vr(self): 1340 """Test writing the dataset preserves raw elements.""" 1341 ds = dcmread(color_pl_name) 1342 1343 # raw data elements after reading 1344 assert ds.get_item(0x00080070).is_raw # Manufacturer 1345 assert ds.get_item(0x00100010).is_raw # Patient Name 1346 assert ds.get_item(0x00080030).is_raw # Study Time 1347 assert ds.get_item(0x00089215).is_raw # Derivation Code Sequence 1348 1349 ds.save_as(DicomBytesIO(), write_like_original=False) 1350 1351 # data set still contains raw data elements after writing 1352 assert ds.get_item(0x00080070).is_raw # Manufacturer 1353 assert ds.get_item(0x00100010).is_raw # Patient Name 1354 assert ds.get_item(0x00080030).is_raw # Study Time 1355 assert ds.get_item(0x00089215).is_raw # Derivation Code Sequence 1356 1357 def test_convert_implicit_to_explicit_vr(self): 1358 # make sure conversion from implicit to explicit VR works 1359 # without private tags 1360 ds = dcmread(mr_implicit_name) 1361 ds.is_implicit_VR = False 1362 ds.file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.1' 1363 fp = DicomBytesIO() 1364 ds.save_as(fp, write_like_original=False) 1365 fp.seek(0) 1366 ds_out = dcmread(fp) 1367 ds_explicit = dcmread(mr_name) 1368 1369 for elem_in, elem_out in zip(ds_explicit, ds_out): 1370 assert elem_in == elem_out 1371 1372 def test_write_dataset(self): 1373 # make sure writing and reading back a dataset works correctly 1374 ds = dcmread(mr_implicit_name) 1375 fp = DicomBytesIO() 1376 write_dataset(fp, ds) 1377 fp.seek(0) 1378 ds_read = read_dataset(fp, is_implicit_VR=True, is_little_endian=True) 1379 for elem_orig, elem_read in zip(ds_read, ds): 1380 assert elem_orig == elem_read 1381 1382 def test_write_dataset_with_explicit_vr(self): 1383 # make sure conversion from implicit to explicit VR does not 1384 # raise (regression test for #632) 1385 ds = dcmread(mr_implicit_name) 1386 fp = DicomBytesIO() 1387 fp.is_implicit_VR = False 1388 fp.is_little_endian = True 1389 write_dataset(fp, ds) 1390 fp.seek(0) 1391 ds_read = read_dataset(fp, is_implicit_VR=False, is_little_endian=True) 1392 for elem_orig, elem_read in zip(ds_read, ds): 1393 assert elem_orig == elem_read 1394 1395 def test_convert_implicit_to_explicit_vr_using_destination(self): 1396 # make sure conversion from implicit to explicit VR works 1397 # if setting the property in the destination 1398 ds = dcmread(mr_implicit_name) 1399 ds.is_implicit_VR = False 1400 ds.file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.1' 1401 fp = DicomBytesIO() 1402 fp.is_implicit_VR = False 1403 fp.is_little_endian = True 1404 ds.save_as(fp, write_like_original=False) 1405 fp.seek(0) 1406 ds_out = dcmread(fp) 1407 ds_explicit = dcmread(mr_name) 1408 1409 for elem_in, elem_out in zip(ds_explicit, ds_out): 1410 assert elem_in == elem_out 1411 1412 def test_convert_explicit_to_implicit_vr(self): 1413 # make sure conversion from explicit to implicit VR works 1414 # without private tags 1415 ds = dcmread(mr_name) 1416 ds.is_implicit_VR = True 1417 ds.file_meta.TransferSyntaxUID = uid.ImplicitVRLittleEndian 1418 fp = DicomBytesIO() 1419 ds.save_as(fp, write_like_original=False) 1420 fp.seek(0) 1421 ds_out = dcmread(fp) 1422 ds_implicit = dcmread(mr_implicit_name) 1423 1424 for elem_in, elem_out in zip(ds_implicit, ds_out): 1425 assert elem_in == elem_out 1426 1427 def test_convert_big_to_little_endian(self): 1428 # make sure conversion from big to little endian works 1429 # except for pixel data 1430 ds = dcmread(mr_bigendian_name) 1431 ds.is_little_endian = True 1432 ds.file_meta.TransferSyntaxUID = uid.ExplicitVRLittleEndian 1433 fp = DicomBytesIO() 1434 ds.save_as(fp, write_like_original=False) 1435 fp.seek(0) 1436 ds_out = dcmread(fp) 1437 ds_explicit = dcmread(mr_name) 1438 1439 # pixel data is not converted automatically 1440 del ds_out.PixelData 1441 del ds_explicit.PixelData 1442 1443 for elem_in, elem_out in zip(ds_explicit, ds_out): 1444 assert elem_in == elem_out 1445 1446 def test_convert_little_to_big_endian(self): 1447 # make sure conversion from little to big endian works 1448 # except for pixel data 1449 ds = dcmread(mr_name) 1450 ds.is_little_endian = False 1451 ds.file_meta.TransferSyntaxUID = uid.ExplicitVRBigEndian 1452 fp = DicomBytesIO() 1453 ds.save_as(fp, write_like_original=False) 1454 fp.seek(0) 1455 ds_out = dcmread(fp) 1456 ds_explicit = dcmread(mr_bigendian_name) 1457 1458 # pixel data is not converted automatically 1459 del ds_out.PixelData 1460 del ds_explicit.PixelData 1461 1462 for elem_in, elem_out in zip(ds_explicit, ds_out): 1463 assert elem_in == elem_out 1464 1465 def test_changed_character_set(self): 1466 """Make sure that a changed character set is reflected 1467 in the written data elements.""" 1468 ds = dcmread(multiPN_name) 1469 # Latin 1 original encoding 1470 assert ds.get_item(0x00100010).value == b'Buc^J\xe9r\xf4me' 1471 1472 # change encoding to UTF-8 1473 ds.SpecificCharacterSet = 'ISO_IR 192' 1474 fp = DicomBytesIO() 1475 ds.save_as(fp, write_like_original=False) 1476 fp.seek(0) 1477 ds_out = dcmread(fp) 1478 # patient name shall be UTF-8 encoded 1479 assert ds_out.get_item(0x00100010).value == b'Buc^J\xc3\xa9r\xc3\xb4me' 1480 # decoded values shall be the same as in original dataset 1481 for elem_in, elem_out in zip(ds, ds_out): 1482 assert elem_in == elem_out 1483 1484 def test_transfer_syntax_added(self): 1485 """Test TransferSyntaxUID is added/updated if possible.""" 1486 # Only done for ImplVR LE and ExplVR BE 1487 # Added 1488 ds = dcmread(rtplan_name) 1489 ds.is_implicit_VR = True 1490 ds.is_little_endian = True 1491 ds.save_as(DicomBytesIO(), write_like_original=False) 1492 assert ds.file_meta.TransferSyntaxUID == ImplicitVRLittleEndian 1493 1494 # Updated 1495 ds.is_implicit_VR = False 1496 ds.is_little_endian = False 1497 ds.save_as(DicomBytesIO(), write_like_original=False) 1498 assert ds.file_meta.TransferSyntaxUID == ExplicitVRBigEndian 1499 1500 def test_private_tag_vr_from_implicit_data(self): 1501 """Test that private tags have the correct VR if converting 1502 a dataset from implicit to explicit VR. 1503 """ 1504 # convert a dataset with private tags to Implicit VR 1505 ds_orig = dcmread(ct_name) 1506 ds_orig.is_implicit_VR = True 1507 ds_orig.is_little_endian = True 1508 fp = DicomBytesIO() 1509 ds_orig.save_as(fp, write_like_original=False) 1510 fp.seek(0) 1511 ds_impl = dcmread(fp) 1512 1513 # convert the dataset back to explicit VR - private tag VR now unknown 1514 ds_impl.is_implicit_VR = False 1515 ds_impl.is_little_endian = True 1516 ds_impl.file_meta.TransferSyntaxUID = uid.ExplicitVRLittleEndian 1517 fp = DicomBytesIO() 1518 ds_impl.save_as(fp, write_like_original=False) 1519 fp.seek(0) 1520 ds_expl = dcmread(fp) 1521 1522 assert ds_expl[(0x0009, 0x0010)].VR == 'LO' # private creator 1523 assert ds_expl[(0x0009, 0x1001)].VR == 'LO' 1524 assert ds_expl[(0x0009, 0x10e7)].VR == 'UL' 1525 assert ds_expl[(0x0043, 0x1010)].VR == 'US' 1526 1527 def test_convert_rgb_from_implicit_to_explicit_vr(self, no_numpy_use): 1528 """Test converting an RGB dataset from implicit to explicit VR 1529 and vice verse.""" 1530 ds_orig = dcmread(sc_rgb_name) 1531 ds_orig.is_implicit_VR = True 1532 ds_orig.is_little_endian = True 1533 fp = DicomBytesIO() 1534 ds_orig.save_as(fp, write_like_original=False) 1535 fp.seek(0) 1536 ds_impl = dcmread(fp) 1537 for elem_orig, elem_conv in zip(ds_orig, ds_impl): 1538 assert elem_orig.value == elem_conv.value 1539 assert 'OW' == ds_impl[0x7fe00010].VR 1540 1541 ds_impl.is_implicit_VR = False 1542 ds_impl.is_little_endian = True 1543 ds_impl.file_meta.TransferSyntaxUID = uid.ExplicitVRLittleEndian 1544 fp = DicomBytesIO() 1545 ds_impl.save_as(fp, write_like_original=False) 1546 fp.seek(0) 1547 # used to raise, see #620 1548 ds_expl = dcmread(fp) 1549 for elem_orig, elem_conv in zip(ds_orig, ds_expl): 1550 assert elem_orig.value == elem_conv.value 1551 1552 def test_transfer_syntax_not_added(self): 1553 """Test TransferSyntaxUID is not added if ExplVRLE.""" 1554 ds = dcmread(rtplan_name) 1555 del ds.file_meta.TransferSyntaxUID 1556 ds.is_implicit_VR = False 1557 ds.is_little_endian = True 1558 with pytest.raises(ValueError): 1559 ds.save_as(DicomBytesIO(), write_like_original=False) 1560 assert 'TransferSyntaxUID' not in ds.file_meta 1561 1562 def test_transfer_syntax_raises(self): 1563 """Test TransferSyntaxUID is raises 1564 NotImplementedError if ImplVRBE.""" 1565 ds = dcmread(rtplan_name) 1566 ds.is_implicit_VR = True 1567 ds.is_little_endian = False 1568 with pytest.raises(NotImplementedError): 1569 ds.save_as(DicomBytesIO(), write_like_original=False) 1570 1571 def test_media_storage_sop_class_uid_added(self): 1572 """Test MediaStorageSOPClassUID and InstanceUID are added.""" 1573 fp = DicomBytesIO() 1574 ds = Dataset() 1575 ds.is_little_endian = True 1576 ds.is_implicit_VR = True 1577 ds.SOPClassUID = CTImageStorage 1578 ds.SOPInstanceUID = '1.2.3' 1579 ds.save_as(fp, write_like_original=False) 1580 assert ds.file_meta.MediaStorageSOPClassUID == CTImageStorage 1581 assert ds.file_meta.MediaStorageSOPInstanceUID == '1.2.3' 1582 1583 def test_write_no_file_meta(self): 1584 """Test writing a dataset with no file_meta""" 1585 fp = DicomBytesIO() 1586 version = 'PYDICOM ' + base_version 1587 ds = dcmread(rtplan_name) 1588 transfer_syntax = ds.file_meta.TransferSyntaxUID 1589 ds.file_meta = FileMetaDataset() 1590 ds.save_as(fp, write_like_original=False) 1591 fp.seek(0) 1592 out = dcmread(fp) 1593 assert out.file_meta.MediaStorageSOPClassUID == ds.SOPClassUID 1594 assert out.file_meta.MediaStorageSOPInstanceUID == ds.SOPInstanceUID 1595 assert (out.file_meta.ImplementationClassUID == 1596 PYDICOM_IMPLEMENTATION_UID) 1597 assert out.file_meta.ImplementationVersionName == version 1598 assert out.file_meta.TransferSyntaxUID == transfer_syntax 1599 1600 fp = DicomBytesIO() 1601 del ds.file_meta 1602 ds.save_as(fp, write_like_original=False) 1603 fp.seek(0) 1604 out = dcmread(fp) 1605 assert out.file_meta.MediaStorageSOPClassUID == ds.SOPClassUID 1606 assert out.file_meta.MediaStorageSOPInstanceUID == ds.SOPInstanceUID 1607 assert (out.file_meta.ImplementationClassUID == 1608 PYDICOM_IMPLEMENTATION_UID) 1609 assert out.file_meta.ImplementationVersionName == version 1610 assert out.file_meta.TransferSyntaxUID == transfer_syntax 1611 1612 def test_raise_no_file_meta(self): 1613 """Test exception is raised if trying to write with no file_meta.""" 1614 ds = dcmread(rtplan_name) 1615 del ds.SOPInstanceUID 1616 ds.file_meta = FileMetaDataset() 1617 with pytest.raises(ValueError): 1618 ds.save_as(DicomBytesIO(), write_like_original=False) 1619 del ds.file_meta 1620 with pytest.raises(ValueError): 1621 ds.save_as(DicomBytesIO(), write_like_original=False) 1622 1623 def test_add_file_meta(self): 1624 """Test that file_meta is added if it doesn't exist""" 1625 fp = DicomBytesIO() 1626 ds = Dataset() 1627 ds.is_little_endian = True 1628 ds.is_implicit_VR = True 1629 ds.SOPClassUID = CTImageStorage 1630 ds.SOPInstanceUID = '1.2.3' 1631 ds.save_as(fp, write_like_original=False) 1632 assert isinstance(ds.file_meta, Dataset) 1633 1634 def test_standard(self): 1635 """Test preamble + file_meta + dataset written OK.""" 1636 fp = DicomBytesIO() 1637 ds = dcmread(ct_name) 1638 preamble = ds.preamble[:] 1639 ds.save_as(fp, write_like_original=False) 1640 fp.seek(0) 1641 assert fp.read(128) == preamble 1642 assert fp.read(4) == b'DICM' 1643 1644 fp.seek(0) 1645 ds_out = dcmread(fp) 1646 assert ds_out.preamble == preamble 1647 assert 'PatientID' in ds_out 1648 assert 'TransferSyntaxUID' in ds_out.file_meta 1649 1650 def test_commandset_no_written(self): 1651 """Test that Command Set elements aren't written.""" 1652 fp = DicomBytesIO() 1653 ds = dcmread(ct_name) 1654 preamble = ds.preamble[:] 1655 ds.MessageID = 3 1656 ds.save_as(fp, write_like_original=False) 1657 fp.seek(0) 1658 assert fp.read(128) == preamble 1659 assert fp.read(4) == b'DICM' 1660 assert 'MessageID' in ds 1661 1662 fp.seek(0) 1663 ds_out = dcmread(fp) 1664 assert ds_out.preamble == preamble 1665 assert 'PatientID' in ds_out 1666 assert 'TransferSyntaxUID' in ds_out.file_meta 1667 assert 'MessageID' not in ds_out 1668 1669 1670class TestWriteFileMetaInfoToStandard: 1671 """Unit tests for writing File Meta Info to the DICOM standard.""" 1672 1673 def test_bad_elements(self): 1674 """Test that non-group 2 elements aren't written to the file meta.""" 1675 fp = DicomBytesIO() 1676 meta = Dataset() 1677 meta.PatientID = '12345678' 1678 meta.MediaStorageSOPClassUID = '1.1' 1679 meta.MediaStorageSOPInstanceUID = '1.2' 1680 meta.TransferSyntaxUID = '1.3' 1681 meta.ImplementationClassUID = '1.4' 1682 with pytest.raises(ValueError): 1683 write_file_meta_info(fp, meta, enforce_standard=True) 1684 1685 def test_missing_elements(self): 1686 """Test that missing required elements raises ValueError.""" 1687 fp = DicomBytesIO() 1688 meta = Dataset() 1689 with pytest.raises(ValueError): 1690 write_file_meta_info(fp, meta) 1691 meta.MediaStorageSOPClassUID = '1.1' 1692 with pytest.raises(ValueError): 1693 write_file_meta_info(fp, meta) 1694 meta.MediaStorageSOPInstanceUID = '1.2' 1695 with pytest.raises(ValueError): 1696 write_file_meta_info(fp, meta) 1697 meta.TransferSyntaxUID = '1.3' 1698 write_file_meta_info(fp, meta, enforce_standard=True) 1699 1700 def test_group_length(self): 1701 """Test that the value for FileMetaInformationGroupLength is OK.""" 1702 fp = DicomBytesIO() 1703 meta = Dataset() 1704 meta.MediaStorageSOPClassUID = '1.1' 1705 meta.MediaStorageSOPInstanceUID = '1.2' 1706 meta.TransferSyntaxUID = '1.3' 1707 write_file_meta_info(fp, meta, enforce_standard=True) 1708 1709 class_length = len(PYDICOM_IMPLEMENTATION_UID) 1710 if class_length % 2: 1711 class_length += 1 1712 version_length = len(meta.ImplementationVersionName) 1713 # Padded to even length 1714 if version_length % 2: 1715 version_length += 1 1716 1717 fp.seek(8) 1718 test_length = unpack('<I', fp.read(4))[0] 1719 assert test_length == 66 + class_length + version_length 1720 1721 def test_group_length_updated(self): 1722 """Test that FileMetaInformationGroupLength gets updated if present.""" 1723 fp = DicomBytesIO() 1724 meta = Dataset() 1725 meta.FileMetaInformationGroupLength = 100 # Not actual length 1726 meta.MediaStorageSOPClassUID = '1.1' 1727 meta.MediaStorageSOPInstanceUID = '1.2' 1728 meta.TransferSyntaxUID = '1.3' 1729 write_file_meta_info(fp, meta, enforce_standard=True) 1730 1731 class_length = len(PYDICOM_IMPLEMENTATION_UID) 1732 if class_length % 2: 1733 class_length += 1 1734 version_length = len(meta.ImplementationVersionName) 1735 # Padded to even length 1736 if version_length % 2: 1737 version_length += 1 1738 1739 fp.seek(8) 1740 test_length = unpack('<I', fp.read(4))[0] 1741 assert test_length == (61 + class_length 1742 + version_length 1743 + len(base_version)) 1744 # Check original file meta is unchanged/updated 1745 assert meta.FileMetaInformationGroupLength == test_length 1746 assert meta.FileMetaInformationVersion == b'\x00\x01' 1747 assert meta.MediaStorageSOPClassUID == '1.1' 1748 assert meta.MediaStorageSOPInstanceUID == '1.2' 1749 assert meta.TransferSyntaxUID == '1.3' 1750 # Updated to meet standard 1751 assert meta.ImplementationClassUID == PYDICOM_IMPLEMENTATION_UID 1752 assert meta.ImplementationVersionName == 'PYDICOM ' + base_version 1753 1754 def test_version(self): 1755 """Test that the value for FileMetaInformationVersion is OK.""" 1756 fp = DicomBytesIO() 1757 meta = Dataset() 1758 meta.MediaStorageSOPClassUID = '1.1' 1759 meta.MediaStorageSOPInstanceUID = '1.2' 1760 meta.TransferSyntaxUID = '1.3' 1761 write_file_meta_info(fp, meta, enforce_standard=True) 1762 1763 fp.seek(12 + 12) 1764 assert fp.read(2) == b'\x00\x01' 1765 1766 def test_implementation_version_name_length(self): 1767 """Test that the written Implementation Version Name length is OK""" 1768 fp = DicomBytesIO() 1769 meta = Dataset() 1770 meta.MediaStorageSOPClassUID = '1.1' 1771 meta.MediaStorageSOPInstanceUID = '1.2' 1772 meta.TransferSyntaxUID = '1.3' 1773 write_file_meta_info(fp, meta, enforce_standard=True) 1774 version_length = len(meta.ImplementationVersionName) 1775 # VR of SH, 16 bytes max 1776 assert version_length <= 16 1777 1778 def test_implementation_class_uid_length(self): 1779 """Test that the written Implementation Class UID length is OK""" 1780 fp = DicomBytesIO() 1781 meta = Dataset() 1782 meta.MediaStorageSOPClassUID = '1.1' 1783 meta.MediaStorageSOPInstanceUID = '1.2' 1784 meta.TransferSyntaxUID = '1.3' 1785 write_file_meta_info(fp, meta, enforce_standard=True) 1786 class_length = len(meta.ImplementationClassUID) 1787 # VR of UI, 64 bytes max 1788 assert class_length <= 64 1789 1790 def test_filelike_position(self): 1791 """Test that the file-like's ending position is OK.""" 1792 fp = DicomBytesIO() 1793 meta = Dataset() 1794 meta.MediaStorageSOPClassUID = '1.1' 1795 meta.MediaStorageSOPInstanceUID = '1.2' 1796 meta.TransferSyntaxUID = '1.3' 1797 write_file_meta_info(fp, meta, enforce_standard=True) 1798 1799 # 8 + 4 bytes FileMetaInformationGroupLength 1800 # 12 + 2 bytes FileMetaInformationVersion 1801 # 8 + 4 bytes MediaStorageSOPClassUID 1802 # 8 + 4 bytes MediaStorageSOPInstanceUID 1803 # 8 + 4 bytes TransferSyntaxUID 1804 # 8 + XX bytes ImplementationClassUID 1805 # 8 + YY bytes ImplementationVersionName 1806 # 78 + XX + YY bytes total 1807 class_length = len(PYDICOM_IMPLEMENTATION_UID) 1808 if class_length % 2: 1809 class_length += 1 1810 version_length = len(meta.ImplementationVersionName) 1811 # Padded to even length 1812 if version_length % 2: 1813 version_length += 1 1814 1815 assert fp.tell() == 78 + class_length + version_length 1816 1817 fp = DicomBytesIO() 1818 # 8 + 6 bytes MediaStorageSOPInstanceUID 1819 meta.MediaStorageSOPInstanceUID = '1.4.1' 1820 write_file_meta_info(fp, meta, enforce_standard=True) 1821 # Check File Meta length 1822 assert fp.tell() == 80 + class_length + version_length 1823 1824 # Check Group Length - 68 + XX + YY as bytes 1825 fp.seek(8) 1826 test_length = unpack('<I', fp.read(4))[0] 1827 assert test_length == 68 + class_length + version_length 1828 1829 1830class TestWriteNonStandard: 1831 """Unit tests for writing datasets not to the DICOM standard.""" 1832 1833 def setup(self): 1834 """Create an empty file-like for use in testing.""" 1835 self.fp = DicomBytesIO() 1836 self.fp.is_little_endian = True 1837 self.fp.is_implicit_VR = True 1838 1839 def compare_bytes(self, bytes_in, bytes_out): 1840 """Compare two bytestreams for equality""" 1841 same, pos = bytes_identical(bytes_in, bytes_out) 1842 assert same 1843 1844 def ensure_no_raw_data_elements(self, ds): 1845 for _ in ds.file_meta: 1846 pass 1847 for _ in ds: 1848 pass 1849 1850 def test_preamble_default(self): 1851 """Test that the default preamble is written correctly when present.""" 1852 ds = dcmread(ct_name) 1853 ds.preamble = b'\x00' * 128 1854 ds.save_as(self.fp, write_like_original=True) 1855 self.fp.seek(0) 1856 assert b'\x00' * 128 == self.fp.read(128) 1857 1858 def test_preamble_custom(self): 1859 """Test that a custom preamble is written correctly when present.""" 1860 ds = dcmread(ct_name) 1861 ds.preamble = b'\x01\x02\x03\x04' + b'\x00' * 124 1862 self.fp.seek(0) 1863 ds.save_as(self.fp, write_like_original=True) 1864 self.fp.seek(0) 1865 assert b'\x01\x02\x03\x04' + b'\x00' * 124 == self.fp.read(128) 1866 1867 def test_no_preamble(self): 1868 """Test no preamble or prefix is written if preamble absent.""" 1869 ds = dcmread(ct_name) 1870 preamble = ds.preamble[:] 1871 del ds.preamble 1872 ds.save_as(self.fp, write_like_original=True) 1873 self.fp.seek(0) 1874 assert b'\x00' * 128 != self.fp.read(128) 1875 self.fp.seek(0) 1876 assert preamble != self.fp.read(128) 1877 self.fp.seek(0) 1878 assert b'DICM' != self.fp.read(4) 1879 1880 def test_ds_unchanged(self): 1881 """Test writing the dataset doesn't change it.""" 1882 ds = dcmread(rtplan_name) 1883 ref_ds = dcmread(rtplan_name) 1884 ds.save_as(self.fp, write_like_original=True) 1885 1886 self.ensure_no_raw_data_elements(ds) 1887 self.ensure_no_raw_data_elements(ref_ds) 1888 assert ref_ds == ds 1889 1890 def test_file_meta_unchanged(self): 1891 """Test no file_meta elements are added if missing.""" 1892 ds = dcmread(rtplan_name) 1893 ds.file_meta = FileMetaDataset() 1894 ds.save_as(self.fp, write_like_original=True) 1895 assert Dataset() == ds.file_meta 1896 1897 def test_dataset(self): 1898 """Test dataset written OK with no preamble or file meta""" 1899 ds = dcmread(ct_name) 1900 del ds.preamble 1901 del ds.file_meta 1902 ds.save_as(self.fp, write_like_original=True) 1903 self.fp.seek(0) 1904 assert b'\x00' * 128 != self.fp.read(128) 1905 self.fp.seek(0) 1906 assert b'DICM' != self.fp.read(4) 1907 1908 self.fp.seek(0) 1909 ds_out = dcmread(self.fp, force=True) 1910 assert ds_out.preamble is None 1911 assert Dataset() == ds_out.file_meta 1912 assert 'PatientID' in ds_out 1913 1914 def test_preamble_dataset(self): 1915 """Test dataset written OK with no file meta""" 1916 ds = dcmread(ct_name) 1917 del ds.file_meta 1918 preamble = ds.preamble[:] 1919 ds.save_as(self.fp, write_like_original=True) 1920 self.fp.seek(0) 1921 assert preamble == self.fp.read(128) 1922 assert b'DICM' == self.fp.read(4) 1923 1924 self.fp.seek(0) 1925 ds_out = dcmread(self.fp, force=True) 1926 assert Dataset() == ds_out.file_meta 1927 assert 'PatientID' in ds_out 1928 1929 def test_filemeta_dataset(self): 1930 """Test file meta written OK if preamble absent.""" 1931 ds = dcmread(ct_name) 1932 preamble = ds.preamble[:] 1933 del ds.preamble 1934 ds.save_as(self.fp, write_like_original=True) 1935 self.fp.seek(0) 1936 assert b'\x00' * 128 != self.fp.read(128) 1937 self.fp.seek(0) 1938 assert preamble != self.fp.read(128) 1939 self.fp.seek(0) 1940 assert b'DICM' != self.fp.read(4) 1941 1942 self.fp.seek(0) 1943 ds_out = dcmread(self.fp, force=True) 1944 assert 'ImplementationClassUID' in ds_out.file_meta 1945 assert ds_out.preamble is None 1946 assert 'PatientID' in ds_out 1947 1948 def test_preamble_filemeta_dataset(self): 1949 """Test non-standard file meta written with preamble OK""" 1950 ds = dcmread(ct_name) 1951 preamble = ds.preamble[:] 1952 ds.save_as(self.fp, write_like_original=True) 1953 self.fp.seek(0) 1954 assert preamble == self.fp.read(128) 1955 assert b'DICM' == self.fp.read(4) 1956 1957 self.fp.seek(0) 1958 ds_out = dcmread(self.fp, force=True) 1959 self.ensure_no_raw_data_elements(ds) 1960 self.ensure_no_raw_data_elements(ds_out) 1961 1962 assert ds.file_meta[:] == ds_out.file_meta[:] 1963 assert 'TransferSyntaxUID' in ds_out.file_meta[:] 1964 assert preamble == ds_out.preamble 1965 assert 'PatientID' in ds_out 1966 1967 def test_commandset_dataset(self): 1968 """Test written OK with command set/dataset""" 1969 ds = dcmread(ct_name) 1970 preamble = ds.preamble[:] 1971 del ds.preamble 1972 del ds.file_meta 1973 ds.is_little_endian = True 1974 ds.is_implicit_VR = True 1975 ds.CommandGroupLength = 8 1976 ds.MessageID = 1 1977 ds.MoveDestination = 'SOME_SCP' 1978 ds.Status = 0x0000 1979 ds.save_as(self.fp, write_like_original=True) 1980 self.fp.seek(0) 1981 assert preamble != self.fp.read(128) 1982 self.fp.seek(0) 1983 assert b'\x00' * 128 != self.fp.read(128) 1984 self.fp.seek(0) 1985 assert b'DICM' != self.fp.read(4) 1986 # Ensure Command Set Elements written as little endian implicit VRe 1987 self.fp.seek(0) 1988 assert (b'\x00\x00\x00\x00\x04\x00\x00\x00\x08\x00\x00\x00' == 1989 self.fp.read(12)) 1990 1991 self.fp.seek(0) 1992 ds_out = dcmread(self.fp, force=True) 1993 assert Dataset() == ds_out.file_meta 1994 assert 'Status' in ds_out 1995 assert 'PatientID' in ds_out 1996 1997 def test_preamble_commandset_dataset(self): 1998 """Test written OK with preamble/command set/dataset""" 1999 ds = dcmread(ct_name) 2000 preamble = ds.preamble[:] 2001 del ds.file_meta 2002 ds.CommandGroupLength = 8 2003 ds.MessageID = 1 2004 ds.MoveDestination = 'SOME_SCP' 2005 ds.Status = 0x0000 2006 ds.save_as(self.fp, write_like_original=True) 2007 self.fp.seek(0) 2008 assert preamble == self.fp.read(128) 2009 assert b'DICM' == self.fp.read(4) 2010 # Ensure Command Set Elements written as little endian implicit VR 2011 assert (b'\x00\x00\x00\x00\x04\x00\x00\x00\x08\x00\x00\x00' == 2012 self.fp.read(12)) 2013 2014 self.fp.seek(0) 2015 ds_out = dcmread(self.fp, force=True) 2016 assert Dataset() == ds_out.file_meta 2017 assert 'Status' in ds_out 2018 assert 'PatientID' in ds_out 2019 2020 def test_preamble_commandset_filemeta_dataset(self): 2021 """Test written OK with preamble/command set/file meta/dataset""" 2022 ds = dcmread(ct_name) 2023 preamble = ds.preamble[:] 2024 ds.CommandGroupLength = 8 2025 ds.MessageID = 1 2026 ds.MoveDestination = 'SOME_SCP' 2027 ds.Status = 0x0000 2028 ds.save_as(self.fp, write_like_original=True) 2029 self.fp.seek(0) 2030 assert preamble == self.fp.read(128) 2031 assert b'DICM' == self.fp.read(4) 2032 2033 self.fp.seek(0) 2034 ds_out = dcmread(self.fp, force=True) 2035 assert 'TransferSyntaxUID' in ds_out.file_meta 2036 assert 'Status' in ds_out 2037 assert 'PatientID' in ds_out 2038 2039 def test_commandset_filemeta_dataset(self): 2040 """Test written OK with command set/file meta/dataset""" 2041 ds = dcmread(ct_name) 2042 preamble = ds.preamble[:] 2043 del ds.preamble 2044 ds.CommandGroupLength = 8 2045 ds.MessageID = 1 2046 ds.MoveDestination = 'SOME_SCP' 2047 ds.Status = 0x0000 2048 ds.save_as(self.fp, write_like_original=True) 2049 self.fp.seek(0) 2050 assert preamble != self.fp.read(128) 2051 self.fp.seek(0) 2052 assert b'\x00' * 128 != self.fp.read(128) 2053 self.fp.seek(0) 2054 assert b'DICM' != self.fp.read(4) 2055 # Ensure Command Set Elements written as little endian implicit VR 2056 self.fp.seek(0) 2057 2058 ds_out = dcmread(self.fp, force=True) 2059 assert 'TransferSyntaxUID' in ds_out.file_meta 2060 assert 'Status' in ds_out 2061 assert 'PatientID' in ds_out 2062 2063 def test_commandset(self): 2064 """Test written OK with command set""" 2065 ds = dcmread(ct_name) 2066 del ds[:] 2067 del ds.preamble 2068 del ds.file_meta 2069 ds.CommandGroupLength = 8 2070 ds.MessageID = 1 2071 ds.MoveDestination = 'SOME_SCP' 2072 ds.Status = 0x0000 2073 ds.save_as(self.fp, write_like_original=True) 2074 self.fp.seek(0) 2075 with pytest.raises(EOFError): 2076 self.fp.read(128, need_exact_length=True) 2077 self.fp.seek(0) 2078 assert b'DICM' != self.fp.read(4) 2079 # Ensure Command Set Elements written as little endian implicit VR 2080 self.fp.seek(0) 2081 2082 fp = BytesIO(self.fp.getvalue()) # Workaround to avoid #358 2083 ds_out = dcmread(fp, force=True) 2084 assert Dataset() == ds_out.file_meta 2085 assert 'Status' in ds_out 2086 assert 'PatientID' not in ds_out 2087 assert Dataset() == ds_out[0x00010000:] 2088 2089 def test_commandset_filemeta(self): 2090 """Test dataset written OK with command set/file meta""" 2091 ds = dcmread(ct_name) 2092 preamble = ds.preamble[:] 2093 del ds[:] 2094 del ds.preamble 2095 ds.CommandGroupLength = 8 2096 ds.MessageID = 1 2097 ds.MoveDestination = 'SOME_SCP' 2098 ds.Status = 0x0000 2099 ds.save_as(self.fp, write_like_original=True) 2100 self.fp.seek(0) 2101 assert preamble != self.fp.read(128) 2102 self.fp.seek(0) 2103 assert b'DICM' != self.fp.read(4) 2104 # Ensure Command Set Elements written as little endian implicit VR 2105 self.fp.seek(0) 2106 2107 fp = BytesIO(self.fp.getvalue()) # Workaround to avoid #358 2108 ds_out = dcmread(fp, force=True) 2109 assert 'TransferSyntaxUID' in ds_out.file_meta 2110 assert 'Status' in ds_out 2111 assert 'PatientID' not in ds_out 2112 assert Dataset() == ds_out[0x00010000:] 2113 2114 def test_preamble_commandset(self): 2115 """Test written OK with preamble/command set""" 2116 ds = dcmread(ct_name) 2117 del ds[:] 2118 del ds.file_meta 2119 ds.CommandGroupLength = 8 2120 ds.MessageID = 1 2121 ds.MoveDestination = 'SOME_SCP' 2122 ds.Status = 0x0000 2123 ds.save_as(self.fp, write_like_original=True) 2124 self.fp.seek(0) 2125 assert ds.preamble == self.fp.read(128) 2126 assert b'DICM' == self.fp.read(4) 2127 # Ensure Command Set Elements written as little endian implicit VR 2128 assert (b'\x00\x00\x00\x00\x04\x00\x00\x00\x08\x00\x00\x00' == 2129 self.fp.read(12)) 2130 2131 fp = BytesIO(self.fp.getvalue()) # Workaround to avoid #358 2132 ds_out = dcmread(fp, force=True) 2133 assert Dataset() == ds_out.file_meta 2134 assert 'Status' in ds_out 2135 assert 'PatientID' not in ds_out 2136 assert Dataset() == ds_out[0x00010000:] 2137 2138 def test_preamble_commandset_filemeta(self): 2139 """Test written OK with preamble/command set/file meta""" 2140 ds = dcmread(ct_name) 2141 del ds[:] 2142 ds.CommandGroupLength = 8 2143 ds.MessageID = 1 2144 ds.MoveDestination = 'SOME_SCP' 2145 ds.Status = 0x0000 2146 ds.save_as(self.fp, write_like_original=True) 2147 self.fp.seek(0) 2148 assert ds.preamble == self.fp.read(128) 2149 assert b'DICM' == self.fp.read(4) 2150 2151 self.fp.seek(0) 2152 ds_out = dcmread(self.fp, force=True) 2153 assert 'Status' in ds_out 2154 assert 'TransferSyntaxUID' in ds_out.file_meta 2155 assert 'PatientID' not in ds_out 2156 assert Dataset() == ds_out[0x00010000:] 2157 2158 def test_read_write_identical(self): 2159 """Test the written bytes matches the read bytes.""" 2160 for dcm_in in [rtplan_name, rtdose_name, ct_name, mr_name, jpeg_name, 2161 no_ts, unicode_name, multiPN_name]: 2162 with open(dcm_in, 'rb') as f: 2163 bytes_in = BytesIO(f.read()) 2164 ds_in = dcmread(bytes_in) 2165 bytes_out = BytesIO() 2166 ds_in.save_as(bytes_out, write_like_original=True) 2167 self.compare_bytes(bytes_in.getvalue(), bytes_out.getvalue()) 2168 2169 2170class TestWriteFileMetaInfoNonStandard: 2171 """Unit tests for writing File Meta Info not to the DICOM standard.""" 2172 2173 def setup(self): 2174 """Create an empty file-like for use in testing.""" 2175 self.fp = DicomBytesIO() 2176 2177 def test_transfer_syntax_not_added(self): 2178 """Test that the TransferSyntaxUID isn't added if missing""" 2179 ds = dcmread(no_ts) 2180 write_file_meta_info(self.fp, ds.file_meta, enforce_standard=False) 2181 assert 'TransferSyntaxUID' not in ds.file_meta 2182 assert 'ImplementationClassUID' in ds.file_meta 2183 2184 # Check written meta dataset doesn't contain TransferSyntaxUID 2185 written_ds = dcmread(self.fp, force=True) 2186 assert 'ImplementationClassUID' in written_ds.file_meta 2187 assert 'TransferSyntaxUID' not in written_ds.file_meta 2188 2189 def test_bad_elements(self): 2190 """Test that non-group 2 elements aren't written to the file meta.""" 2191 meta = Dataset() 2192 meta.PatientID = '12345678' 2193 meta.MediaStorageSOPClassUID = '1.1' 2194 meta.MediaStorageSOPInstanceUID = '1.2' 2195 meta.TransferSyntaxUID = '1.3' 2196 meta.ImplementationClassUID = '1.4' 2197 with pytest.raises(ValueError): 2198 write_file_meta_info(self.fp, meta, enforce_standard=False) 2199 2200 def test_missing_elements(self): 2201 """Test that missing required elements doesn't raise ValueError.""" 2202 meta = Dataset() 2203 write_file_meta_info(self.fp, meta, enforce_standard=False) 2204 meta.MediaStorageSOPClassUID = '1.1' 2205 write_file_meta_info(self.fp, meta, enforce_standard=False) 2206 meta.MediaStorageSOPInstanceUID = '1.2' 2207 write_file_meta_info(self.fp, meta, enforce_standard=False) 2208 meta.TransferSyntaxUID = '1.3' 2209 write_file_meta_info(self.fp, meta, enforce_standard=False) 2210 meta.ImplementationClassUID = '1.4' 2211 write_file_meta_info(self.fp, meta, enforce_standard=False) 2212 2213 def test_group_length_updated(self): 2214 """Test that FileMetaInformationGroupLength gets updated if present.""" 2215 meta = Dataset() 2216 meta.FileMetaInformationGroupLength = 100 2217 meta.MediaStorageSOPClassUID = '1.1' 2218 meta.MediaStorageSOPInstanceUID = '1.2' 2219 meta.TransferSyntaxUID = '1.3' 2220 meta.ImplementationClassUID = '1.4' 2221 write_file_meta_info(self.fp, meta, enforce_standard=False) 2222 2223 # 8 + 4 bytes FileMetaInformationGroupLength 2224 # 8 + 4 bytes MediaStorageSOPClassUID 2225 # 8 + 4 bytes MediaStorageSOPInstanceUID 2226 # 8 + 4 bytes TransferSyntaxUID 2227 # 8 + 4 bytes ImplementationClassUID 2228 # 60 bytes total, - 12 for group length = 48 2229 self.fp.seek(8) 2230 assert b'\x30\x00\x00\x00' == self.fp.read(4) 2231 # Check original file meta is unchanged/updated 2232 assert 48 == meta.FileMetaInformationGroupLength 2233 assert 'FileMetaInformationVersion' not in meta 2234 assert '1.1' == meta.MediaStorageSOPClassUID 2235 assert '1.2' == meta.MediaStorageSOPInstanceUID 2236 assert '1.3' == meta.TransferSyntaxUID 2237 assert '1.4' == meta.ImplementationClassUID 2238 2239 def test_filelike_position(self): 2240 """Test that the file-like's ending position is OK.""" 2241 # 8 + 4 bytes MediaStorageSOPClassUID 2242 # 8 + 4 bytes MediaStorageSOPInstanceUID 2243 # 8 + 4 bytes TransferSyntaxUID 2244 # 8 + 4 bytes ImplementationClassUID 2245 # 48 bytes total 2246 meta = Dataset() 2247 meta.MediaStorageSOPClassUID = '1.1' 2248 meta.MediaStorageSOPInstanceUID = '1.2' 2249 meta.TransferSyntaxUID = '1.3' 2250 meta.ImplementationClassUID = '1.4' 2251 write_file_meta_info(self.fp, meta, enforce_standard=False) 2252 assert 48 == self.fp.tell() 2253 2254 # 8 + 6 bytes ImplementationClassUID 2255 # 50 bytes total 2256 self.fp.seek(0) 2257 meta.ImplementationClassUID = '1.4.1' 2258 write_file_meta_info(self.fp, meta, enforce_standard=False) 2259 # Check File Meta length 2260 assert 50 == self.fp.tell() 2261 2262 def test_meta_unchanged(self): 2263 """Test that the meta dataset doesn't change when writing it""" 2264 # Empty 2265 meta = Dataset() 2266 write_file_meta_info(self.fp, meta, enforce_standard=False) 2267 assert Dataset() == meta 2268 2269 # Incomplete 2270 meta = Dataset() 2271 meta.MediaStorageSOPClassUID = '1.1' 2272 meta.MediaStorageSOPInstanceUID = '1.2' 2273 meta.TransferSyntaxUID = '1.3' 2274 meta.ImplementationClassUID = '1.4' 2275 ref_meta = deepcopy(meta) 2276 write_file_meta_info(self.fp, meta, enforce_standard=False) 2277 assert ref_meta == meta 2278 2279 # Conformant 2280 meta = Dataset() 2281 meta.FileMetaInformationGroupLength = 62 # Correct length 2282 meta.FileMetaInformationVersion = b'\x00\x01' 2283 meta.MediaStorageSOPClassUID = '1.1' 2284 meta.MediaStorageSOPInstanceUID = '1.2' 2285 meta.TransferSyntaxUID = '1.3' 2286 meta.ImplementationClassUID = '1.4' 2287 ref_meta = deepcopy(meta) 2288 write_file_meta_info(self.fp, meta, enforce_standard=False) 2289 assert ref_meta == meta 2290 2291 2292class TestWriteNumbers: 2293 """Test filewriter.write_numbers""" 2294 2295 def test_write_empty_value(self): 2296 """Test writing an empty value does nothing""" 2297 fp = DicomBytesIO() 2298 fp.is_little_endian = True 2299 elem = DataElement(0x00100010, 'US', '') 2300 fmt = 'H' 2301 write_numbers(fp, elem, fmt) 2302 assert fp.getvalue() == b'' 2303 2304 def test_write_list(self): 2305 """Test writing an element value with VM > 1""" 2306 fp = DicomBytesIO() 2307 fp.is_little_endian = True 2308 elem = DataElement(0x00100010, 'US', [1, 2, 3, 4]) 2309 fmt = 'H' 2310 write_numbers(fp, elem, fmt) 2311 assert fp.getvalue() == b'\x01\x00\x02\x00\x03\x00\x04\x00' 2312 2313 def test_write_singleton(self): 2314 """Test writing an element value with VM = 1""" 2315 fp = DicomBytesIO() 2316 fp.is_little_endian = True 2317 elem = DataElement(0x00100010, 'US', 1) 2318 fmt = 'H' 2319 write_numbers(fp, elem, fmt) 2320 assert fp.getvalue() == b'\x01\x00' 2321 2322 def test_exception(self): 2323 """Test exceptions raise IOError""" 2324 fp = DicomBytesIO() 2325 fp.is_little_endian = True 2326 elem = DataElement(0x00100010, 'US', b'\x00') 2327 fmt = 'H' 2328 with pytest.raises(IOError, 2329 match=r"for data_element:\n\(0010, 0010\)"): 2330 write_numbers(fp, elem, fmt) 2331 2332 def test_write_big_endian(self): 2333 """Test writing big endian""" 2334 fp = DicomBytesIO() 2335 fp.is_little_endian = False 2336 elem = DataElement(0x00100010, 'US', 1) 2337 fmt = 'H' 2338 write_numbers(fp, elem, fmt) 2339 assert fp.getvalue() == b'\x00\x01' 2340 2341 2342class TestWriteOtherVRs: 2343 """Tests for writing the 'O' VRs like OB, OW, OF, etc.""" 2344 def test_write_of(self): 2345 """Test writing element with VR OF""" 2346 fp = DicomBytesIO() 2347 fp.is_little_endian = True 2348 elem = DataElement(0x7fe00008, 'OF', b'\x00\x01\x02\x03') 2349 write_OWvalue(fp, elem) 2350 assert fp.getvalue() == b'\x00\x01\x02\x03' 2351 2352 def test_write_of_dataset(self): 2353 """Test writing a dataset with an element with VR OF.""" 2354 fp = DicomBytesIO() 2355 fp.is_little_endian = True 2356 fp.is_implicit_VR = False 2357 ds = Dataset() 2358 ds.is_little_endian = True 2359 ds.is_implicit_VR = False 2360 ds.FloatPixelData = b'\x00\x01\x02\x03' 2361 ds.save_as(fp) 2362 assert fp.getvalue() == ( 2363 # Tag | VR | Length | Value 2364 b'\xe0\x7f\x08\x00\x4F\x46\x00\x00\x04\x00\x00\x00\x00\x01\x02\x03' 2365 ) 2366 2367 2368class TestWritePN: 2369 """Test filewriter.write_PN""" 2370 2371 def test_no_encoding(self): 2372 """If PN element has no encoding info, default is used""" 2373 fp = DicomBytesIO() 2374 fp.is_little_endian = True 2375 # data element with encoded value 2376 elem = DataElement(0x00100010, 'PN', 'Test') 2377 write_PN(fp, elem) 2378 assert b'Test' == fp.getvalue() 2379 2380 fp = DicomBytesIO() 2381 fp.is_little_endian = True 2382 # data element with decoded value 2383 elem = DataElement(0x00100010, 'PN', 'Test') 2384 write_PN(fp, elem) 2385 assert b'Test' == fp.getvalue() 2386 2387 def test_single_byte_multi_charset_groups(self): 2388 """Test component groups with different encodings""" 2389 fp = DicomBytesIO() 2390 fp.is_little_endian = True 2391 encodings = ['latin_1', 'iso_ir_126'] 2392 # data element with encoded value 2393 encoded = (b'Dionysios=\x1b\x2d\x46' 2394 b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2') 2395 elem = DataElement(0x00100010, 'PN', encoded) 2396 write_PN(fp, elem) 2397 assert encoded == fp.getvalue() 2398 2399 # regression test: make sure no warning is issued, e.g. the 2400 # PersonName value has not saved the default encoding 2401 fp = DicomBytesIO() 2402 fp.is_little_endian = True 2403 with pytest.warns(None) as warnings: 2404 write_PN(fp, elem, encodings) 2405 assert not warnings 2406 assert encoded == fp.getvalue() 2407 2408 fp = DicomBytesIO() 2409 fp.is_little_endian = True 2410 # data element with decoded value 2411 elem = DataElement(0x00100010, 'PN', 'Dionysios=Διονυσιος') 2412 write_PN(fp, elem, encodings=encodings) 2413 assert encoded == fp.getvalue() 2414 2415 def test_single_byte_multi_charset_values(self): 2416 """Test multiple values with different encodings""" 2417 fp = DicomBytesIO() 2418 fp.is_little_endian = True 2419 encodings = ['latin_1', 'iso_ir_144', 'iso_ir_126'] 2420 # data element with encoded value 2421 encoded = (b'Buc^J\xe9r\xf4me\\\x1b\x2d\x46' 2422 b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2\\' 2423 b'\x1b\x2d\x4C' 2424 b'\xbb\xee\xda\x63\x65\xdc\xd1\x79\x70\xd3 ') 2425 elem = DataElement(0x00100060, 'PN', encoded) 2426 write_PN(fp, elem) 2427 assert encoded == fp.getvalue() 2428 2429 fp = DicomBytesIO() 2430 fp.is_little_endian = True 2431 # data element with decoded value 2432 elem = DataElement(0x00100060, 'PN', ['Buc^Jérôme', 'Διονυσιος', 2433 'Люкceмбypг']) 2434 write_PN(fp, elem, encodings=encodings) 2435 assert encoded == fp.getvalue() 2436 2437 2438class TestWriteText: 2439 """Test filewriter.write_PN""" 2440 2441 def test_no_encoding(self): 2442 """If text element has no encoding info, default is used""" 2443 fp = DicomBytesIO() 2444 fp.is_little_endian = True 2445 # data element with encoded value 2446 elem = DataElement(0x00081039, 'LO', 'Test') 2447 write_text(fp, elem) 2448 assert b'Test' == fp.getvalue() 2449 2450 fp = DicomBytesIO() 2451 fp.is_little_endian = True 2452 # data element with decoded value 2453 elem = DataElement(0x00081039, 'LO', 'Test') 2454 write_text(fp, elem) 2455 assert b'Test' == fp.getvalue() 2456 2457 def test_single_byte_multi_charset_text(self): 2458 """Test changed encoding inside the string""" 2459 fp = DicomBytesIO() 2460 fp.is_little_endian = True 2461 encoded = (b'Dionysios=\x1b\x2d\x46' 2462 b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2') 2463 # data element with encoded value 2464 elem = DataElement(0x00081039, 'LO', encoded) 2465 encodings = ['latin_1', 'iso_ir_126'] 2466 write_text(fp, elem) 2467 assert encoded == fp.getvalue() 2468 2469 fp = DicomBytesIO() 2470 fp.is_little_endian = True 2471 # data element with decoded value 2472 elem = DataElement(0x00081039, 'LO', 'Dionysios is Διονυσιος') 2473 write_text(fp, elem, encodings=encodings) 2474 # encoding may not be the same, so decode it first 2475 encoded = fp.getvalue() 2476 assert 'Dionysios is Διονυσιος' == convert_text(encoded, encodings) 2477 2478 def test_encode_mixed_charsets_text(self): 2479 """Test encodings used inside the string in arbitrary order""" 2480 fp = DicomBytesIO() 2481 fp.is_little_endian = True 2482 encodings = ['latin_1', 'euc_kr', 'iso-2022-jp', 'iso_ir_127'] 2483 decoded = '山田-قباني-吉洞-لنزار' 2484 2485 # data element with encoded value 2486 elem = DataElement(0x00081039, 'LO', decoded) 2487 write_text(fp, elem, encodings=encodings) 2488 encoded = fp.getvalue() 2489 # make sure that the encoded string can be converted back 2490 assert decoded == convert_text(encoded, encodings) 2491 2492 def test_single_byte_multi_charset_text_multivalue(self): 2493 """Test multiple values with different encodings""" 2494 fp = DicomBytesIO() 2495 fp.is_little_endian = True 2496 encoded = (b'Buc^J\xe9r\xf4me\\\x1b\x2d\x46' 2497 b'\xc4\xe9\xef\xed\xf5\xf3\xe9\xef\xf2\\' 2498 b'\x1b\x2d\x4C' 2499 b'\xbb\xee\xda\x63\x65\xdc\xd1\x79\x70\xd3 ') 2500 # data element with encoded value 2501 elem = DataElement(0x00081039, 'LO', encoded) 2502 encodings = ['latin_1', 'iso_ir_144', 'iso_ir_126'] 2503 write_text(fp, elem, encodings=encodings) 2504 assert encoded == fp.getvalue() 2505 2506 fp = DicomBytesIO() 2507 fp.is_little_endian = True 2508 # data element with decoded value 2509 decoded = ['Buc^Jérôme', 'Διονυσιος', 'Люкceмбypг'] 2510 elem = DataElement(0x00081039, 'LO', decoded) 2511 write_text(fp, elem, encodings=encodings) 2512 # encoding may not be the same, so decode it first 2513 encoded = fp.getvalue() 2514 assert decoded == convert_text(encoded, encodings) 2515 2516 def test_invalid_encoding(self, allow_invalid_values): 2517 """Test encoding text with invalid encodings""" 2518 fp = DicomBytesIO() 2519 fp.is_little_endian = True 2520 # data element with decoded value 2521 elem = DataElement(0x00081039, 'LO', 'Dionysios Διονυσιος') 2522 msg = 'Failed to encode value with encodings: iso-2022-jp' 2523 expected = b'Dionysios \x1b$B&$&I&O&M&T&R&I&O\x1b(B? ' 2524 with pytest.warns(UserWarning, match=msg): 2525 # encode with one invalid encoding 2526 write_text(fp, elem, encodings=['iso-2022-jp']) 2527 assert expected == fp.getvalue() 2528 2529 fp = DicomBytesIO() 2530 fp.is_little_endian = True 2531 # data element with decoded value 2532 elem = DataElement(0x00081039, 'LO', 'Dionysios Διονυσιος') 2533 msg = 'Failed to encode value with encodings: iso-2022-jp, iso_ir_58' 2534 with pytest.warns(UserWarning, match=msg): 2535 # encode with two invalid encodings 2536 write_text(fp, elem, encodings=['iso-2022-jp', 'iso_ir_58']) 2537 assert expected == fp.getvalue() 2538 2539 def test_invalid_encoding_enforce_standard(self, enforce_valid_values): 2540 """Test encoding text with invalid encodings with 2541 `config.enforce_valid_values` enabled""" 2542 fp = DicomBytesIO() 2543 fp.is_little_endian = True 2544 # data element with decoded value 2545 elem = DataElement(0x00081039, 'LO', 'Dionysios Διονυσιος') 2546 msg = (r"'iso2022_jp' codec can't encode character u?'\\u03c2' in " 2547 r"position 18: illegal multibyte sequence") 2548 with pytest.raises(UnicodeEncodeError, match=msg): 2549 # encode with one invalid encoding 2550 write_text(fp, elem, encodings=['iso-2022-jp']) 2551 2552 fp = DicomBytesIO() 2553 fp.is_little_endian = True 2554 # data element with decoded value 2555 elem = DataElement(0x00081039, 'LO', 'Dionysios Διονυσιος') 2556 with pytest.raises(UnicodeEncodeError, match=msg): 2557 # encode with two invalid encodings 2558 write_text(fp, elem, encodings=['iso-2022-jp', 'iso_ir_58']) 2559 2560 def test_single_value_with_delimiters(self): 2561 """Test that text with delimiters encodes correctly""" 2562 fp = DicomBytesIO() 2563 fp.is_little_endian = True 2564 decoded = 'Διονυσιος\r\nJérôme/Люкceмбypг\tJérôme' 2565 elem = DataElement(0x00081039, 'LO', decoded) 2566 encodings = ('latin_1', 'iso_ir_144', 'iso_ir_126') 2567 write_text(fp, elem, encodings=encodings) 2568 encoded = fp.getvalue() 2569 assert decoded == convert_text(encoded, encodings) 2570 2571 2572class TestWriteDT: 2573 """Test filewriter.write_DT""" 2574 2575 def test_format_dt(self): 2576 """Test _format_DT""" 2577 elem = DataElement(0x00181078, 'DT', DT('20010203123456.123456')) 2578 assert hasattr(elem.value, 'original_string') 2579 assert _format_DT(elem.value) == '20010203123456.123456' 2580 del elem.value.original_string 2581 assert not hasattr(elem.value, 'original_string') 2582 assert elem.value.microsecond > 0 2583 assert _format_DT(elem.value) == '20010203123456.123456' 2584 2585 elem = DataElement(0x00181078, 'DT', DT('20010203123456')) 2586 del elem.value.original_string 2587 assert _format_DT(elem.value) == '20010203123456' 2588 2589 2590class TestWriteUndefinedLengthPixelData: 2591 """Test write_data_element() for pixel data with undefined length.""" 2592 2593 def setup(self): 2594 self.fp = DicomBytesIO() 2595 2596 def test_little_endian_correct_data(self): 2597 """Pixel data starting with an item tag is written.""" 2598 self.fp.is_little_endian = True 2599 self.fp.is_implicit_VR = False 2600 pixel_data = DataElement(0x7fe00010, 'OB', 2601 b'\xfe\xff\x00\xe0' 2602 b'\x00\x01\x02\x03', 2603 is_undefined_length=True) 2604 write_data_element(self.fp, pixel_data) 2605 2606 expected = (b'\xe0\x7f\x10\x00' # tag 2607 b'OB\x00\x00' # VR 2608 b'\xff\xff\xff\xff' # length 2609 b'\xfe\xff\x00\xe0\x00\x01\x02\x03' # contents 2610 b'\xfe\xff\xdd\xe0\x00\x00\x00\x00') # SQ delimiter 2611 self.fp.seek(0) 2612 assert self.fp.read() == expected 2613 2614 def test_big_endian_correct_data(self): 2615 """Pixel data starting with an item tag is written.""" 2616 self.fp.is_little_endian = False 2617 self.fp.is_implicit_VR = False 2618 pixel_data = DataElement(0x7fe00010, 'OB', 2619 b'\xff\xfe\xe0\x00' 2620 b'\x00\x01\x02\x03', 2621 is_undefined_length=True) 2622 write_data_element(self.fp, pixel_data) 2623 expected = (b'\x7f\xe0\x00\x10' # tag 2624 b'OB\x00\x00' # VR 2625 b'\xff\xff\xff\xff' # length 2626 b'\xff\xfe\xe0\x00\x00\x01\x02\x03' # contents 2627 b'\xff\xfe\xe0\xdd\x00\x00\x00\x00') # SQ delimiter 2628 self.fp.seek(0) 2629 assert self.fp.read() == expected 2630 2631 def test_little_endian_incorrect_data(self): 2632 """Writing pixel data not starting with an item tag raises.""" 2633 self.fp.is_little_endian = True 2634 self.fp.is_implicit_VR = False 2635 pixel_data = DataElement(0x7fe00010, 'OB', 2636 b'\xff\xff\x00\xe0' 2637 b'\x00\x01\x02\x03' 2638 b'\xfe\xff\xdd\xe0', 2639 is_undefined_length=True) 2640 msg = ( 2641 r"Pixel Data has an undefined length indicating " 2642 r"that it's compressed, but the data isn't encapsulated" 2643 ) 2644 with pytest.raises(ValueError, match=msg): 2645 write_data_element(self.fp, pixel_data) 2646 2647 def test_big_endian_incorrect_data(self): 2648 """Writing pixel data not starting with an item tag raises.""" 2649 self.fp.is_little_endian = False 2650 self.fp.is_implicit_VR = False 2651 pixel_data = DataElement(0x7fe00010, 'OB', 2652 b'\x00\x00\x00\x00' 2653 b'\x00\x01\x02\x03' 2654 b'\xff\xfe\xe0\xdd', 2655 is_undefined_length=True) 2656 msg = ( 2657 r"Pixel Data has an undefined length indicating " 2658 r"that it's compressed, but the data isn't encapsulated" 2659 ) 2660 with pytest.raises(ValueError, match=msg): 2661 write_data_element(self.fp, pixel_data) 2662 2663 def test_writing_to_gzip(self): 2664 file_path = tempfile.NamedTemporaryFile(suffix='.dcm').name 2665 ds = dcmread(rtplan_name) 2666 import gzip 2667 with gzip.open(file_path, 'w') as fp: 2668 ds.save_as(fp, write_like_original=False) 2669 with gzip.open(file_path, 'r') as fp: 2670 ds_unzipped = dcmread(fp) 2671 for elem_in, elem_out in zip(ds, ds_unzipped): 2672 assert elem_in == elem_out 2673 2674 def test_writing_too_big_data_in_explicit_encoding(self): 2675 """Data too large to be written in explicit transfer syntax.""" 2676 self.fp.is_little_endian = True 2677 self.fp.is_implicit_VR = True 2678 # make a multi-value larger than 64kB 2679 single_value = b'123456.789012345' 2680 large_value = b'\\'.join([single_value] * 4500) 2681 # can be written with implicit transfer syntax, 2682 # where the length field is 4 bytes long 2683 pixel_data = DataElement(0x30040058, 'DS', 2684 large_value, 2685 is_undefined_length=False) 2686 write_data_element(self.fp, pixel_data) 2687 self.fp.seek(0) 2688 ds = read_dataset(self.fp, True, True) 2689 assert 'DS' == ds[0x30040058].VR 2690 2691 self.fp = DicomBytesIO() 2692 self.fp.is_little_endian = True 2693 self.fp.is_implicit_VR = False 2694 2695 msg = (r"The value for the data element \(3004, 0058\) exceeds the " 2696 r"size of 64 kByte and cannot be written in an explicit " 2697 r"transfer syntax. The data element VR is changed from " 2698 r"'DS' to 'UN' to allow saving the data.") 2699 2700 with pytest.warns(UserWarning, match=msg): 2701 write_data_element(self.fp, pixel_data) 2702 self.fp.seek(0) 2703 ds = read_dataset(self.fp, False, True) 2704 assert 'UN' == ds[0x30040058].VR 2705 2706 # we expect the same behavior in Big Endian transfer syntax 2707 self.fp = DicomBytesIO() 2708 self.fp.is_little_endian = False 2709 self.fp.is_implicit_VR = False 2710 with pytest.warns(UserWarning, match=msg): 2711 write_data_element(self.fp, pixel_data) 2712 self.fp.seek(0) 2713 ds = read_dataset(self.fp, False, False) 2714 assert 'UN' == ds[0x30040058].VR 2715