Repository: pydicom/pydicom

Issue:
Incorrect validation applied to LUT Descriptor (0028, 3002) in filewriter.py
**Describe the bug**
When attempting to write a dataset containing tag (0028. 3002) LUTDescriptor, validation logic is being applied to the first value within the multivalued field.  

The VR specified on the field (either US or SS) applies only to the second value in the array - not values 1 or 3.  

Exception:
```
Traceback (most recent call last):
  File "/home/wetzeljad/env/app_deid_core/lib/python3.8/site-packages/pydicom/tag.py", line 28, in tag_in_exception
    yield
  File "/home/wetzeljad/env/app_deid_core/lib/python3.8/site-packages/pydicom/filewriter.py", line 660, in write_dataset
    write_data_element(fp, dataset.get_item(tag), dataset_encoding)
  File "/home/wetzeljad/env/app_deid_core/lib/python3.8/site-packages/pydicom/filewriter.py", line 565, in write_data_element
    fn(buffer, elem, param)  # type: ignore[operator]
  File "/home/wetzeljad/env/app_deid_core/lib/python3.8/site-packages/pydicom/filewriter.py", line 282, in write_numbers
    raise IOError(
OSError: short format requires (-0x7fff - 1) <= number <= 0x7fff
for data_element:
(0028, 3002) LUT Descriptor                      SS: [32768, 0, 16]
```


**Expected behavior**
An exception should not have occurred with the data encountered above.   While value 1 (32768) is outside the valid range for an SS VR, this validation logic should only have applied to the second value in the array as described in section [C.11.1.1.1](https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.html#sect_C.11.1.1.1) of the DICOM Spec. 


**Your environment**
module       | version
------       | -------
platform     | Linux-5.4.0-104-generic-x86_64-with-glibc2.29
Python       | 3.8.10 (default, Nov 22 2023, 10:22:35)  [GCC 9.4.0]
pydicom      | 2.4.4
gdcm         | _module not found_
jpeg_ls      | _module not found_
numpy        | 1.23.4
PIL          | 10.0.0
pylibjpeg    | _module not found_
openjpeg     | _module not found_
libjpeg      | _module not found_

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pydicom/pydicom/pull/2083?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.30%. Comparing base [(`a1d823e`)](https://app.codecov.io/gh/pydicom/pydicom/commit/a1d823e32c44514ea4853ad6bf582cda9a2108e9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) to head [(`493f0ac`)](https://app.codecov.io/gh/pydicom/pydicom/commit/493f0ac7ace40119a9cb3485f4cc7adab93d119c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).

> :exclamation: **Current head 493f0ac differs from pull request most recent head a37460f**
> 
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) reports for the commit a37460f to get more accurate results.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2083   +/-   ##
=======================================
  Coverage   98.30%   98.30%           
=======================================
  Files          76       76           
  Lines       12907    12916    +9     
=======================================
+ Hits        12688    12697    +9     
  Misses        219      219           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pydicom/pydicom/pull/2083?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).


Patch:
diff --git a/doc/release_notes/v3.0.0.rst b/doc/release_notes/v3.0.0.rst
index 5c5c5503c..ba491a6aa 100644
--- a/doc/release_notes/v3.0.0.rst
+++ b/doc/release_notes/v3.0.0.rst
@@ -251,6 +251,8 @@ Fixes
   a private block (:issue:`2025`)
 * Fixed non-unique keywords for the concept codes in ``pydicom.sr`` (:issue:`1388`)
 * Fixed keywords using Python identifiers in ``pydicom.sr`` (:issue:`1273`)
+* Fixed being unable to write *LUT Descriptor* when the VR is **SS** and the first
+  value is greater than 32767 (:issue:`2081`)
 
 
 Deprecations
diff --git a/src/pydicom/dataelem.py b/src/pydicom/dataelem.py
index 5268f4d7e..994c0ffaa 100644
--- a/src/pydicom/dataelem.py
+++ b/src/pydicom/dataelem.py
@@ -527,6 +527,20 @@ class DataElement:
         if len(val) == 1:
             return self._convert(val[0])
 
+        # Some ambiguous VR elements ignore the VR for part of the value
+        # e.g. LUT Descriptor is 'US or SS' and VM 3, but the first and
+        #   third values are always US (the third should be <= 16, so SS is OK)
+        if self.tag in _LUT_DESCRIPTOR_TAGS and val:
+
+            def _skip_conversion(val: Any) -> Any:
+                return val
+
+            validate_value(VR_.US, val[0], self.validation_mode)
+            for value in val[1:]:
+                validate_value(self.VR, value, self.validation_mode)
+
+            return MultiValue(_skip_conversion, val)
+
         return MultiValue(self._convert, val)
 
     def _convert(self, val: Any) -> Any:
diff --git a/src/pydicom/filewriter.py b/src/pydicom/filewriter.py
index 9e6829af0..c86a6a82c 100644
--- a/src/pydicom/filewriter.py
+++ b/src/pydicom/filewriter.py
@@ -10,7 +10,12 @@ import zlib
 
 from pydicom import config
 from pydicom.charset import default_encoding, convert_encodings, encode_string
-from pydicom.dataelem import DataElement_from_raw, DataElement, RawDataElement
+from pydicom.dataelem import (
+    DataElement_from_raw,
+    DataElement,
+    RawDataElement,
+    _LUT_DESCRIPTOR_TAGS,
+)
 from pydicom.dataset import Dataset, validate_file_meta, FileMetaDataset
 from pydicom.filebase import DicomFile, DicomBytesIO, DicomIO, WriteableBuffer
 from pydicom.fileutil import path_from_pathlike, PathType
@@ -345,11 +350,11 @@ def write_numbers(fp: DicomIO, elem: DataElement, struct_format: str) -> None:
     struct_format : str
         The character format as used by the struct module.
     """
-    endianChar = "><"[fp.is_little_endian]
     value = elem.value
     if value is None or value == "":
         return  # don't need to write anything for no or empty value
 
+    endianChar = "><"[fp.is_little_endian]
     format_string = endianChar + struct_format
     try:
         try:
@@ -358,10 +363,16 @@ def write_numbers(fp: DicomIO, elem: DataElement, struct_format: str) -> None:
         except AttributeError:  # is a single value - the usual case
             fp.write(pack(format_string, value))
         else:
-            for val in cast(Iterable[Any], value):
-                fp.write(pack(format_string, val))
-    except Exception as e:
-        raise OSError(f"{str(e)}\nfor data_element:\n{str(elem)}")
+            # Some ambiguous VR elements ignore the VR for part of the value
+            # e.g. LUT Descriptor is 'US or SS' and VM 3, but the first and
+            #   third values are always US (the third should be <= 16, so SS is OK)
+            if struct_format == "h" and elem.tag in _LUT_DESCRIPTOR_TAGS and value:
+                fp.write(pack(f"{endianChar}H", value[0]))
+                value = value[1:]
+
+            fp.write(pack(f"{endianChar}{len(value)}{struct_format}", *value))
+    except Exception as exc:
+        raise OSError(f"{exc}\nfor data_element:\n{elem}")
 
 
 def write_OBvalue(fp: DicomIO, elem: DataElement) -> None:


Test patch:
diff --git a/tests/test_filewriter.py b/tests/test_filewriter.py
index 22a2d7fd9..8faac1273 100644
--- a/tests/test_filewriter.py
+++ b/tests/test_filewriter.py
@@ -2674,6 +2674,26 @@ class TestWriteNumbers:
         write_numbers(fp, elem, fmt)
         assert fp.getvalue() == b"\x00\x01"
 
+    def test_write_lut_descriptor(self):
+        """Test writing LUT Descriptor"""
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x00283002, "SS", [32768, 0, 16])
+        write_numbers(fp, elem, "h")
+        assert fp.getvalue() == b"\x00\x80\x00\x00\x10\x00"
+
+        fp = DicomBytesIO()
+        fp.is_little_endian = True
+        elem = DataElement(0x00283002, "SS", [])
+        write_numbers(fp, elem, "h")
+        assert fp.getvalue() == b""
+
+        fp = DicomBytesIO()
+        fp.is_little_endian = False
+        elem = DataElement(0x00283002, "SS", [32768, 0, 16])
+        write_numbers(fp, elem, "h")
+        assert fp.getvalue() == b"\x80\x00\x00\x00\x00\x10"
+
 
 class TestWriteOtherVRs:
     """Tests for writing the 'O' VRs like OB, OW, OF, etc."""