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