Repository: pydicom/pydicom

Issue:
Add support for missing VRs
Missing: OV, SV, UV



Discussion:

Patch:
diff --git a/doc/release_notes/v1.4.0.rst b/doc/release_notes/v1.4.0.rst
index d0c543075..571e92bb8 100644
--- a/doc/release_notes/v1.4.0.rst
+++ b/doc/release_notes/v1.4.0.rst
@@ -73,6 +73,7 @@ Enhancements
   <pydicom.dataelem.DataElement.is_undefined_length>` is now set automatically
   based on whether the Dataset's Transfer Syntax is compressed (:issue:`1006`)
 * Updated DICOM dictionary to 2019e edition (:issue:`1013`)
+* Added support for new VRs OV, SV, UV (:issue:`1016`)
 
 Changes
 .......
diff --git a/pydicom/filewriter.py b/pydicom/filewriter.py
index 5d307b943..cebf4add8 100644
--- a/pydicom/filewriter.py
+++ b/pydicom/filewriter.py
@@ -955,35 +955,40 @@ write_file = dcmwrite  # write_file before pydicom 1.0, kept for compatibility
 # for write_numbers, the Writer maps to a tuple (function, struct_format)
 #   (struct_format is python's struct module format)
 writers = {
-    'UL': (write_numbers, 'L'),
-    'SL': (write_numbers, 'l'),
-    'US': (write_numbers, 'H'),
-    'SS': (write_numbers, 'h'),
-    'FL': (write_numbers, 'f'),
+    'AE': (write_string, None),
+    'AS': (write_string, None),
+    'AT': (write_ATvalue, None),
+    'CS': (write_string, None),
+    'DA': (write_DA, None),
+    'DS': (write_number_string, None),
+    'DT': (write_DT, None),
     'FD': (write_numbers, 'd'),
-    'OF': (write_numbers, 'f'),
+    'FL': (write_numbers, 'f'),
+    'IS': (write_number_string, None),
+    'LO': (write_text, None),
+    'LT': (write_text, None),
     'OB': (write_OBvalue, None),
     'OD': (write_OWvalue, None),
+    'OF': (write_numbers, 'f'),
     'OL': (write_OWvalue, None),
-    'UI': (write_UI, None),
-    'SH': (write_text, None),
-    'DA': (write_DA, None),
-    'TM': (write_TM, None),
-    'CS': (write_string, None),
+    'OW': (write_OWvalue, None),
+    'OV': (write_OWvalue, None),
     'PN': (write_PN, None),
-    'LO': (write_text, None),
-    'IS': (write_number_string, None),
-    'DS': (write_number_string, None),
-    'AE': (write_string, None),
-    'AS': (write_string, None),
-    'LT': (write_text, None),
+    'SH': (write_text, None),
+    'SL': (write_numbers, 'l'),
     'SQ': (write_sequence, None),
+    'SS': (write_numbers, 'h'),
+    'ST': (write_text, None),
+    'SV': (write_numbers, 'q'),
+    'TM': (write_TM, None),
     'UC': (write_text, None),
+    'UI': (write_UI, None),
+    'UL': (write_numbers, 'L'),
     'UN': (write_UN, None),
     'UR': (write_string, None),
-    'AT': (write_ATvalue, None),
-    'ST': (write_text, None),
-    'OW': (write_OWvalue, None),
+    'US': (write_numbers, 'H'),
+    'UT': (write_text, None),
+    'UV': (write_numbers, 'Q'),
     'US or SS': (write_OWvalue, None),
     'US or OW': (write_OWvalue, None),
     'US or SS or OW': (write_OWvalue, None),
@@ -991,6 +996,4 @@ writers = {
     'OB/OW': (write_OBvalue, None),
     'OB or OW': (write_OBvalue, None),
     'OW or OB': (write_OBvalue, None),
-    'DT': (write_DT, None),
-    'UT': (write_text, None),
 }  # note OW/OB depends on other items, which we don't know at write time
diff --git a/pydicom/values.py b/pydicom/values.py
index f56765f1b..f03a9ba82 100644
--- a/pydicom/values.py
+++ b/pydicom/values.py
@@ -300,6 +300,15 @@ def convert_OWvalue(byte_string, is_little_endian, struct_format=None):
     return convert_OBvalue(byte_string, is_little_endian)
 
 
+def convert_OVvalue(byte_string, is_little_endian, struct_format=None):
+    """Return the encoded 'OV' value as :class:`bytes` or :class:`str`.
+
+    No byte swapping will be performed.
+    """
+    # for now, Maybe later will have own routine
+    return convert_OBvalue(byte_string, is_little_endian)
+
+
 def convert_PN(byte_string, encodings=None):
     """Return a decoded 'PN' value.
 
@@ -635,35 +644,40 @@ convert_retry_VR_order = [
 # (function, struct_format)
 # (struct_format in python struct module style)
 converters = {
-    'UL': (convert_numbers, 'L'),
-    'SL': (convert_numbers, 'l'),
-    'US': (convert_numbers, 'H'),
-    'SS': (convert_numbers, 'h'),
-    'FL': (convert_numbers, 'f'),
+    'AE': convert_AE_string,
+    'AS': convert_string,
+    'AT': convert_ATvalue,
+    'CS': convert_string,
+    'DA': convert_DA_string,
+    'DS': convert_DS_string,
+    'DT': convert_DT_string,
     'FD': (convert_numbers, 'd'),
-    'OF': (convert_numbers, 'f'),
+    'FL': (convert_numbers, 'f'),
+    'IS': convert_IS_string,
+    'LO': convert_text,
+    'LT': convert_single_string,
     'OB': convert_OBvalue,
     'OD': convert_OBvalue,
+    'OF': (convert_numbers, 'f'),
     'OL': convert_OBvalue,
-    'UI': convert_UI,
-    'SH': convert_text,
-    'DA': convert_DA_string,
-    'TM': convert_TM_string,
-    'CS': convert_string,
+    'OW': convert_OWvalue,
+    'OV': convert_OVvalue,
     'PN': convert_PN,
-    'LO': convert_text,
-    'IS': convert_IS_string,
-    'DS': convert_DS_string,
-    'AE': convert_AE_string,
-    'AS': convert_string,
-    'LT': convert_single_string,
+    'SH': convert_text,
+    'SL': (convert_numbers, 'l'),
     'SQ': convert_SQ,
+    'SS': (convert_numbers, 'h'),
+    'ST': convert_single_string,
+    'SV': (convert_numbers, 'q'),
+    'TM': convert_TM_string,
     'UC': convert_text,
+    'UI': convert_UI,
+    'UL': (convert_numbers, 'L'),
     'UN': convert_UN,
     'UR': convert_UR_string,
-    'AT': convert_ATvalue,
-    'ST': convert_single_string,
-    'OW': convert_OWvalue,
+    'US': (convert_numbers, 'H'),
+    'UT': convert_single_string,
+    'UV': (convert_numbers, 'Q'),
     'OW/OB': convert_OBvalue,  # note OW/OB depends on other items,
     'OB/OW': convert_OBvalue,  # which we don't know at read time
     'OW or OB': convert_OBvalue,
@@ -672,6 +686,4 @@ converters = {
     'US or OW': convert_OWvalue,
     'US or SS or OW': convert_OWvalue,
     'US\\US or SS\\US': convert_OWvalue,
-    'DT': convert_DT_string,
-    'UT': convert_single_string,
 }


Test patch:
diff --git a/pydicom/tests/test_filereader.py b/pydicom/tests/test_filereader.py
index c30a3377d..573b67f82 100644
--- a/pydicom/tests/test_filereader.py
+++ b/pydicom/tests/test_filereader.py
@@ -17,6 +17,7 @@ import pydicom.config
 from pydicom import config
 from pydicom.dataset import Dataset, FileDataset
 from pydicom.data import get_testdata_files
+from pydicom.datadict import add_dict_entries
 from pydicom.filereader import dcmread, read_dataset
 from pydicom.dataelem import DataElement, DataElement_from_raw
 from pydicom.errors import InvalidDicomError
@@ -837,6 +838,31 @@ class TestReadDataElement(object):
         ds.URNCodeValue = 'http://test.com'  # VR of UR
         ds.RetrieveURL = 'ftp://test.com  '  # Test trailing spaces ignored
         ds.DestinationAE = '    TEST  12    '  # 16 characters max for AE
+        # 8-byte values
+        ds.ExtendedOffsetTable = (  # VR of OV
+            b'\x00\x00\x00\x00\x00\x00\x00\x00'
+            b'\x01\x02\x03\x04\x05\x06\x07\x08'
+        )
+
+        # No public elements with VR of SV or UV yet...
+        add_dict_entries({
+            0xFFFE0001: (
+                'SV', '1', 'SV Element Minimum', '', 'SVElementMinimum'
+            ),
+            0xFFFE0002: (
+                'SV', '1', 'SV Element Maximum', '', 'SVElementMaximum'
+            ),
+            0xFFFE0003: (
+                'UV', '1', 'UV Element Minimum', '', 'UVElementMinimum'
+            ),
+            0xFFFE0004: (
+                'UV', '1', 'UV Element Maximum', '', 'UVElementMaximum'
+            ),
+        })
+        ds.SVElementMinimum = -2**63
+        ds.SVElementMaximum = 2**63 - 1
+        ds.UVElementMinimum = 0
+        ds.UVElementMaximum = 2**64 - 1
 
         self.fp = BytesIO()  # Implicit little
         file_ds = FileDataset(self.fp, ds)
@@ -939,6 +965,113 @@ class TestReadDataElement(object):
         ds = dcmread(self.fp, force=True)
         assert 'TEST  12' == ds.DestinationAE
 
+    def test_read_OV_implicit_little(self):
+        """Check reading element with VR of OV encoded as implicit"""
+        ds = dcmread(self.fp, force=True)
+        val = (
+            b'\x00\x00\x00\x00\x00\x00\x00\x00'
+            b'\x01\x02\x03\x04\x05\x06\x07\x08'
+        )
+        elem = ds['ExtendedOffsetTable']
+        assert 'OV' == elem.VR
+        assert 0x7FE00001 == elem.tag
+        assert val == elem.value
+
+        new = DataElement(0x7FE00001, 'OV', val)
+        assert elem == new
+
+    def test_read_OV_explicit_little(self):
+        """Check reading element with VR of OV encoded as explicit"""
+        ds = dcmread(self.fp_ex, force=True)
+        val = (
+            b'\x00\x00\x00\x00\x00\x00\x00\x00'
+            b'\x01\x02\x03\x04\x05\x06\x07\x08'
+        )
+        elem = ds['ExtendedOffsetTable']
+        assert 'OV' == elem.VR
+        assert 0x7FE00001 == elem.tag
+        assert val == elem.value
+
+        new = DataElement(0x7FE00001, 'OV', val)
+        assert elem == new
+
+    def test_read_SV_implicit_little(self):
+        """Check reading element with VR of SV encoded as implicit"""
+        ds = dcmread(self.fp, force=True)
+        elem = ds['SVElementMinimum']
+        assert 'SV' == elem.VR
+        assert 0xFFFE0001 == elem.tag
+        assert -2**63 == elem.value
+
+        new = DataElement(0xFFFE0001, 'SV', -2**63)
+        assert elem == new
+
+        elem = ds['SVElementMaximum']
+        assert 'SV' == elem.VR
+        assert 0xFFFE0002 == elem.tag
+        assert 2**63 - 1 == elem.value
+
+        new = DataElement(0xFFFE0002, 'SV', 2**63 - 1)
+        assert elem == new
+
+    @pytest.mark.skip("No public elements with VR of SV")
+    def test_read_SV_explicit_little(self):
+        """Check reading element with VR of SV encoded as explicit"""
+        ds = dcmread(self.fp_ex, force=True)
+        elem = ds['SVElementMinimum']
+        assert 'SV' == elem.VR
+        assert 0xFFFE0001 == elem.tag
+        assert -2**63 == elem.value
+
+        new = DataElement(0xFFFE0001, 'SV', -2**63)
+        assert elem == new
+
+        elem = ds['SVElementMaximum']
+        assert 'SV' == elem.VR
+        assert 0xFFFE0002 == elem.tag
+        assert 2**63 - 1 == elem.value
+
+        new = DataElement(0xFFFE0002, 'SV', 2**63 - 1)
+        assert elem == new
+
+    def test_read_UV_implicit_little(self):
+        """Check reading element with VR of UV encoded as implicit"""
+        ds = dcmread(self.fp, force=True)
+        elem = ds['UVElementMinimum']
+        assert 'UV' == elem.VR
+        assert 0xFFFE0003 == elem.tag
+        assert 0 == elem.value
+
+        new = DataElement(0xFFFE0003, 'UV', 0)
+        assert elem == new
+
+        elem = ds['UVElementMaximum']
+        assert 'UV' == elem.VR
+        assert 0xFFFE0004 == elem.tag
+        assert 2**64 - 1 == elem.value
+
+        new = DataElement(0xFFFE0004, 'UV', 2**64 - 1)
+        assert elem == new
+
+    def test_read_UV_explicit_little(self):
+        """Check reading element with VR of UV encoded as explicit"""
+        ds = dcmread(self.fp_ex, force=True)
+        elem = ds['UVElementMinimum']
+        assert 'UV' == elem.VR
+        assert 0xFFFE0003 == elem.tag
+        assert 0 == elem.value
+
+        new = DataElement(0xFFFE0003, 'UV', 0)
+        assert elem == new
+
+        elem = ds['UVElementMaximum']
+        assert 'UV' == elem.VR
+        assert 0xFFFE0004 == elem.tag
+        assert 2**64 - 1 == elem.value
+
+        new = DataElement(0xFFFE0004, 'UV', 2**64 - 1)
+        assert elem == new
+
 
 class TestDeferredRead(object):
     """Test that deferred data element reading (for large size)