984 lines
34 KiB
Python
Executable File
984 lines
34 KiB
Python
Executable File
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
|
"""Define the DataElement class.
|
|
|
|
A DataElement has a tag,
|
|
a value representation (VR),
|
|
a value multiplicity (VM)
|
|
and a value.
|
|
"""
|
|
|
|
import base64
|
|
from collections.abc import Callable, MutableSequence
|
|
import copy
|
|
from io import BufferedIOBase
|
|
import json
|
|
from typing import Any, TYPE_CHECKING, NamedTuple
|
|
|
|
from pydicom import config # don't import datetime_conversion directly
|
|
from pydicom.config import logger
|
|
from pydicom.datadict import (
|
|
dictionary_has_tag,
|
|
dictionary_description,
|
|
dictionary_keyword,
|
|
dictionary_is_retired,
|
|
private_dictionary_description,
|
|
dictionary_VR,
|
|
repeater_has_tag,
|
|
)
|
|
from pydicom.hooks import hooks
|
|
from pydicom.jsonrep import JsonDataElementConverter, BulkDataType
|
|
from pydicom.misc import warn_and_log
|
|
from pydicom.multival import MultiValue
|
|
from pydicom.tag import Tag, BaseTag, _LUT_DESCRIPTOR_TAGS
|
|
from pydicom.uid import UID
|
|
from pydicom import jsonrep
|
|
from pydicom.fileutil import check_buffer, buffer_length, buffer_equality
|
|
import pydicom.valuerep # don't import DS directly as can be changed by config
|
|
from pydicom.valuerep import (
|
|
BUFFERABLE_VRS,
|
|
PersonName,
|
|
BYTES_VR,
|
|
AMBIGUOUS_VR,
|
|
STR_VR,
|
|
ALLOW_BACKSLASH,
|
|
DEFAULT_CHARSET_VR,
|
|
LONG_VALUE_VR,
|
|
VR as VR_,
|
|
validate_value,
|
|
)
|
|
|
|
if config.have_numpy:
|
|
import numpy
|
|
|
|
if TYPE_CHECKING: # pragma: no cover
|
|
from pydicom.dataset import Dataset
|
|
|
|
|
|
def empty_value_for_VR(
|
|
VR: str | None, raw: bool = False
|
|
) -> bytes | list[str] | str | None | PersonName:
|
|
"""Return the value for an empty element for `VR`.
|
|
|
|
The behavior of this property depends on the setting of
|
|
:attr:`config.use_none_as_empty_value`. If that is set to ``True``,
|
|
an empty value is represented by ``None`` (except for VR 'SQ'), otherwise
|
|
it depends on `VR`. For text VRs (this includes 'AE', 'AS', 'CS', 'DA',
|
|
'DT', 'LO', 'LT', 'PN', 'SH', 'ST', 'TM', 'UC', 'UI', 'UR' and 'UT') an
|
|
empty string is used as empty value representation, for all other VRs
|
|
except 'SQ', ``None``. For empty sequence values (VR 'SQ') an empty list
|
|
is used in all cases.
|
|
Note that this is used only if decoding the element - it is always
|
|
possible to set the value to another empty value representation,
|
|
which will be preserved during the element object lifetime.
|
|
|
|
Parameters
|
|
----------
|
|
VR : str or None
|
|
The VR of the corresponding element.
|
|
raw : bool, optional
|
|
If ``True``, returns the value for a :class:`RawDataElement`,
|
|
otherwise for a :class:`DataElement`
|
|
|
|
Returns
|
|
-------
|
|
str or bytes or None or list
|
|
The value a data element with `VR` is assigned on decoding
|
|
if it is empty.
|
|
"""
|
|
if VR == VR_.SQ:
|
|
return b"" if raw else []
|
|
|
|
if config.use_none_as_empty_text_VR_value:
|
|
return None
|
|
|
|
if VR == VR_.PN:
|
|
return b"" if raw else PersonName("")
|
|
|
|
# DS and IS are treated more like int/float than str
|
|
if VR in STR_VR - {VR_.DS, VR_.IS}:
|
|
return b"" if raw else ""
|
|
|
|
return None
|
|
|
|
|
|
class DataElement:
|
|
"""Contain and manipulate a DICOM Element.
|
|
|
|
Examples
|
|
--------
|
|
|
|
While its possible to create a new :class:`DataElement` directly and add
|
|
it to a :class:`~pydicom.dataset.Dataset`:
|
|
|
|
>>> from pydicom import Dataset
|
|
>>> elem = DataElement(0x00100010, 'PN', 'CITIZEN^Joan')
|
|
>>> ds = Dataset()
|
|
>>> ds.add(elem)
|
|
|
|
Its far more convenient to use a :class:`~pydicom.dataset.Dataset`
|
|
to add a new :class:`DataElement`, as the VR and tag are determined
|
|
automatically from the DICOM dictionary:
|
|
|
|
>>> ds = Dataset()
|
|
>>> ds.PatientName = 'CITIZEN^Joan'
|
|
|
|
Empty DataElement objects (e.g. with VM = 0) show an empty string as
|
|
value for text VRs and `None` for non-text (binary) VRs:
|
|
|
|
>>> ds = Dataset()
|
|
>>> ds.PatientName = None
|
|
>>> ds.PatientName
|
|
''
|
|
|
|
>>> ds.BitsAllocated = None
|
|
>>> ds.BitsAllocated
|
|
|
|
>>> str(ds.BitsAllocated)
|
|
'None'
|
|
|
|
Attributes
|
|
----------
|
|
descripWidth : int
|
|
For string display, this is the maximum width of the description
|
|
field (default ``35``).
|
|
is_undefined_length : bool
|
|
Indicates whether the length field for the element was ``0xFFFFFFFFL``
|
|
(ie undefined).
|
|
maxBytesToDisplay : int
|
|
For string display, elements with values containing data which is
|
|
longer than this value will display ``"array of # bytes"``
|
|
(default ``16``).
|
|
showVR : bool
|
|
For string display, include the element's VR just before it's value
|
|
(default ``True``).
|
|
tag : pydicom.tag.BaseTag
|
|
The element's tag.
|
|
validation_mode : int
|
|
The mode used to validate the element's value. See
|
|
:attr:`Settings.writing_validation_mode
|
|
<pydicom.config.Settings.writing_validation_mode>` for more information.
|
|
VR : str
|
|
The element's Value Representation.
|
|
"""
|
|
|
|
descripWidth = 35
|
|
maxBytesToDisplay = 16
|
|
showVR = True
|
|
is_raw = False
|
|
|
|
def __init__(
|
|
self,
|
|
tag: int | str | tuple[int, int],
|
|
VR: str,
|
|
value: Any,
|
|
file_value_tell: int | None = None,
|
|
is_undefined_length: bool = False,
|
|
already_converted: bool = False,
|
|
validation_mode: int | None = None,
|
|
) -> None:
|
|
"""Create a new :class:`DataElement`.
|
|
|
|
Parameters
|
|
----------
|
|
tag : int | str | tuple[int, int]
|
|
The DICOM (group, element) tag in any form accepted by
|
|
:func:`~pydicom.tag.Tag` such as ``'PatientName'``,
|
|
``(0x10, 0x10)``, ``0x00100010``, etc.
|
|
VR : str
|
|
The 2 character DICOM value representation (see DICOM Standard,
|
|
Part 5, :dcm:`Section 6.2<part05/sect_6.2.html>`).
|
|
value : Any
|
|
The value of the data element, the :doc:`allowed type depends on the VR
|
|
</guides/element_value_types>` and includes:
|
|
|
|
* a single :class:`str` value
|
|
* an :class:`int` or :class:`int`
|
|
* a :class:`list` or :class:`tuple` containing only a single type of item
|
|
such as :class:`str`, :class:`int` or :class:`float`
|
|
* a raw :class:`bytes` value
|
|
file_value_tell : int, optional
|
|
The byte offset to the start of the encoded element value.
|
|
is_undefined_length : bool, optional
|
|
Used internally to store whether the length field for this element
|
|
was ``0xFFFFFFFF``, i.e. 'undefined length'. Default is ``False``.
|
|
already_converted : bool, optional
|
|
Used to determine whether or not the element's value requires
|
|
conversion to a value with VM > 1. Default is ``False``.
|
|
validation_mode : int, optional
|
|
Defines if values are validated and how validation errors are
|
|
handled.
|
|
"""
|
|
if validation_mode is None:
|
|
validation_mode = config.settings.reading_validation_mode
|
|
|
|
if not isinstance(tag, BaseTag):
|
|
tag = Tag(tag)
|
|
self.tag = tag
|
|
|
|
# a known tag shall only have the VR 'UN' if it has a length that
|
|
# exceeds the size that can be encoded in 16 bit - all other cases
|
|
# can be seen as an encoding error and can be corrected
|
|
if (
|
|
VR == VR_.UN
|
|
and not tag.is_private
|
|
and config.replace_un_with_known_vr
|
|
and (is_undefined_length or value is None or len(value) < 0xFFFF)
|
|
):
|
|
try:
|
|
VR = dictionary_VR(tag)
|
|
except KeyError:
|
|
pass
|
|
|
|
self.VR = VR # Note: you must set VR before setting value
|
|
self.validation_mode = validation_mode
|
|
|
|
if already_converted:
|
|
self._value = value
|
|
else:
|
|
self.value = value # calls property setter which will convert
|
|
|
|
self.file_tell = file_value_tell
|
|
self.is_undefined_length: bool = is_undefined_length
|
|
self.private_creator: str | None = None
|
|
|
|
def validate(self, value: Any) -> None:
|
|
"""Validate the current value against the DICOM standard.
|
|
See :func:`~pydicom.valuerep.validate_value` for details.
|
|
"""
|
|
validate_value(self.VR, value, self.validation_mode)
|
|
|
|
@classmethod
|
|
def from_json(
|
|
cls: type["DataElement"],
|
|
dataset_class: type["Dataset"],
|
|
tag: str,
|
|
vr: str,
|
|
value: Any,
|
|
value_key: str | None,
|
|
bulk_data_uri_handler: (
|
|
Callable[[str, str, str], BulkDataType]
|
|
| Callable[[str], BulkDataType]
|
|
| None
|
|
) = None,
|
|
) -> "DataElement":
|
|
"""Return a :class:`DataElement` from a DICOM JSON Model attribute
|
|
object.
|
|
|
|
Parameters
|
|
----------
|
|
dataset_class : dataset.Dataset derived class
|
|
The class object to use for **SQ** element items.
|
|
tag : str
|
|
The data element's tag as uppercase hex.
|
|
vr : str
|
|
The data element's value representation (VR).
|
|
value : str or list[None | str | int | float | bytes | dict]
|
|
The data element's value(s).
|
|
value_key : str or None
|
|
The attribute name for `value`, should be one of:
|
|
``{"Value", "InlineBinary", "BulkDataURI"}``. If the element's VM
|
|
is ``0`` and none of the keys are used then will be ``None``.
|
|
bulk_data_uri_handler: callable or None
|
|
Callable function that accepts either the `tag`, `vr` and
|
|
"BulkDataURI" `value` or just the "BulkDataURI" `value` of the JSON
|
|
representation of a data element and returns the actual value of
|
|
that data element (retrieved via DICOMweb WADO-RS). If no
|
|
`bulk_data_uri_handler` is specified (default) then the
|
|
corresponding element will have an "empty" value such as
|
|
``""``, ``b""`` or ``None`` depending on the `vr` (i.e. the
|
|
Value Multiplicity will be 0).
|
|
|
|
Returns
|
|
-------
|
|
DataElement
|
|
"""
|
|
# TODO: test wado-rs retrieve wrapper
|
|
converter = JsonDataElementConverter(
|
|
dataset_class, tag, vr, value, value_key, bulk_data_uri_handler
|
|
)
|
|
elem_value = converter.get_element_values()
|
|
|
|
if (
|
|
vr == VR_.UN
|
|
and config.replace_un_with_known_vr
|
|
and isinstance(elem_value, bytes)
|
|
):
|
|
raw = RawDataElement(
|
|
Tag(tag), vr, len(elem_value), elem_value, 0, True, True
|
|
)
|
|
elem_value = convert_raw_data_element(raw).value
|
|
|
|
try:
|
|
return cls(tag=tag, value=elem_value, VR=vr)
|
|
except Exception as exc:
|
|
raise ValueError(
|
|
f"Data element '{tag}' could not be loaded from JSON: {elem_value}"
|
|
) from exc
|
|
|
|
def to_json_dict(
|
|
self,
|
|
bulk_data_element_handler: Callable[["DataElement"], str] | None,
|
|
bulk_data_threshold: int,
|
|
) -> dict[str, Any]:
|
|
"""Return a dictionary representation of the :class:`DataElement`
|
|
conforming to the DICOM JSON Model as described in the DICOM
|
|
Standard, Part 18, :dcm:`Annex F<part18/chaptr_F.html>`.
|
|
|
|
Parameters
|
|
----------
|
|
bulk_data_element_handler : callable or None
|
|
Callable that accepts a bulk :class`data element
|
|
<pydicom.dataelem.DataElement>` and returns the
|
|
"BulkDataURI" as a :class:`str` for retrieving the value of the
|
|
data element via DICOMweb WADO-RS.
|
|
bulk_data_threshold : int
|
|
Size of base64 encoded data element above which a value will be
|
|
provided in form of a "BulkDataURI" rather than "InlineBinary".
|
|
Ignored if no `bulk_data_element_handler` is given.
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Mapping representing a JSON encoded data element as ``{str: Any}``.
|
|
"""
|
|
json_element: dict[str, Any] = {"vr": self.VR}
|
|
if self.VR in (BYTES_VR | AMBIGUOUS_VR) - {VR_.US_SS}:
|
|
if not self.is_empty:
|
|
binary_value = self.value
|
|
# Base64 makes the encoded value 1/3 longer.
|
|
if bulk_data_element_handler is not None and len(binary_value) > (
|
|
(bulk_data_threshold // 4) * 3
|
|
):
|
|
json_element["BulkDataURI"] = bulk_data_element_handler(self)
|
|
else:
|
|
# Json is exempt from padding to even length, see DICOM-CP1920
|
|
encoded_value = base64.b64encode(binary_value).decode("utf-8")
|
|
logger.info(f"encode bulk data element '{self.name}' inline")
|
|
json_element["InlineBinary"] = encoded_value
|
|
elif self.VR == VR_.SQ:
|
|
# recursive call to get sequence item JSON dicts
|
|
value = [
|
|
ds.to_json(
|
|
bulk_data_element_handler=bulk_data_element_handler,
|
|
bulk_data_threshold=bulk_data_threshold,
|
|
dump_handler=lambda d: d,
|
|
)
|
|
for ds in self.value
|
|
]
|
|
json_element["Value"] = value
|
|
elif self.VR == VR_.PN:
|
|
if not self.is_empty:
|
|
elem_value = []
|
|
if self.VM > 1:
|
|
value = self.value
|
|
else:
|
|
value = [self.value]
|
|
for v in value:
|
|
comps = {"Alphabetic": v.components[0]}
|
|
if len(v.components) > 1:
|
|
comps["Ideographic"] = v.components[1]
|
|
if len(v.components) > 2:
|
|
comps["Phonetic"] = v.components[2]
|
|
elem_value.append(comps)
|
|
json_element["Value"] = elem_value
|
|
elif self.VR == VR_.AT:
|
|
if not self.is_empty:
|
|
value = self.value
|
|
if self.VM == 1:
|
|
value = [value]
|
|
json_element["Value"] = [format(v, "08X") for v in value]
|
|
else:
|
|
if not self.is_empty:
|
|
if self.VM > 1:
|
|
value = self.value
|
|
else:
|
|
value = [self.value]
|
|
json_element["Value"] = [v for v in value]
|
|
if "Value" in json_element:
|
|
json_element["Value"] = jsonrep.convert_to_python_number(
|
|
json_element["Value"], self.VR
|
|
)
|
|
return json_element
|
|
|
|
def to_json(
|
|
self,
|
|
bulk_data_threshold: int = 1024,
|
|
bulk_data_element_handler: Callable[["DataElement"], str] | None = None,
|
|
dump_handler: Callable[[dict[str, Any]], str] | None = None,
|
|
) -> str:
|
|
"""Return a JSON representation of the :class:`DataElement`.
|
|
|
|
Parameters
|
|
----------
|
|
bulk_data_threshold : int, optional
|
|
Size of base64 encoded data element above which a value will be
|
|
provided in form of a "BulkDataURI" rather than "InlineBinary".
|
|
Ignored if no `bulk_data_element_handler` is given.
|
|
bulk_data_element_handler : callable, optional
|
|
Callable that accepts a bulk :class`data element
|
|
<pydicom.dataelem.DataElement>` and returns the
|
|
"BulkDataURI" as a :class:`str` for retrieving the value of the
|
|
data element via DICOMweb WADO-RS.
|
|
dump_handler : callable, optional
|
|
Callable function that accepts a :class:`dict` of ``{str: Any}``
|
|
and returns the serialized (dumped) JSON :class:`str` (by default
|
|
uses :func:`json.dumps`).
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
Mapping representing a JSON encoded data element
|
|
|
|
See also
|
|
--------
|
|
Dataset.to_json
|
|
"""
|
|
|
|
def json_dump(d: dict[str, Any]) -> str:
|
|
return json.dumps(d, sort_keys=True)
|
|
|
|
dump_handler = json_dump if dump_handler is None else dump_handler
|
|
|
|
return dump_handler(
|
|
self.to_json_dict(bulk_data_element_handler, bulk_data_threshold)
|
|
)
|
|
|
|
@property
|
|
def value(self) -> Any:
|
|
"""Get or set the element's value.
|
|
|
|
Parameters
|
|
----------
|
|
val : Any
|
|
The value to use to set the element's value, should be an :doc:`appropriate
|
|
type for the VR</guides/element_value_types>`. The value will be validated
|
|
in accordance with the element's :attr:`~DataElement.validation_mode`.
|
|
|
|
Returns
|
|
-------
|
|
Any
|
|
The element's value.
|
|
"""
|
|
return self._value
|
|
|
|
@value.setter
|
|
def value(self, val: Any) -> None:
|
|
# O* elements set using a buffer object
|
|
if isinstance(val, BufferedIOBase):
|
|
if self.VR not in BUFFERABLE_VRS:
|
|
supported = sorted(str(vr) for vr in BUFFERABLE_VRS if "or" not in vr)
|
|
raise ValueError(
|
|
f"Elements with a VR of '{self.VR}' cannot be used with buffered "
|
|
f"values, supported VRs are: {', '.join(supported)}"
|
|
)
|
|
|
|
# Ensure pre-conditions are met - we will check these when reading the
|
|
# value as well but better to fail early if possible
|
|
try:
|
|
check_buffer(val)
|
|
except Exception as exc:
|
|
raise type(exc)(f"Invalid buffer for {self.tag} '{self.name}': {exc}")
|
|
|
|
self._value = val
|
|
return
|
|
|
|
# Check if is multiple values separated by backslash
|
|
# If so, turn them into a list of separate values
|
|
# Exclude splitting values with backslash characters based on:
|
|
# * Which str-like VRs can have backslashes in Part 5, Section 6.2
|
|
# * All byte-like VRs
|
|
# * Ambiguous VRs that may be byte-like
|
|
if self.VR not in ALLOW_BACKSLASH:
|
|
if isinstance(val, str):
|
|
val = val.split("\\") if "\\" in val else val
|
|
elif isinstance(val, bytes):
|
|
val = val.split(b"\\") if b"\\" in val else val
|
|
|
|
self._value = self._convert_value(val)
|
|
|
|
@property
|
|
def VM(self) -> int:
|
|
"""Return the value multiplicity of the element as :class:`int`.
|
|
|
|
.. versionchanged:: 3.0
|
|
|
|
**SQ** elements now always return a VM of ``1``.
|
|
"""
|
|
if self.VR == VR_.SQ:
|
|
return 1
|
|
|
|
if self.value is None:
|
|
return 0
|
|
|
|
if isinstance(self.value, str | bytes | PersonName):
|
|
return 1 if self.value else 0
|
|
|
|
if isinstance(self.value, BufferedIOBase):
|
|
try:
|
|
return 1 if buffer_length(self.value) else 0
|
|
except Exception as exc:
|
|
raise type(exc)(f"Invalid buffer for {self.tag} '{self.name}': {exc}")
|
|
|
|
try:
|
|
iter(self.value)
|
|
except TypeError:
|
|
return 1
|
|
|
|
return len(self.value)
|
|
|
|
@property
|
|
def is_buffered(self) -> bool:
|
|
"""Return ``True`` if the element's value is a :class:`io.BufferedIOBase`
|
|
instance, ``False`` otherwise.
|
|
"""
|
|
return isinstance(self._value, BufferedIOBase)
|
|
|
|
@property
|
|
def is_empty(self) -> bool:
|
|
"""Return ``True`` if the element has no value."""
|
|
if self.VR == VR_.SQ:
|
|
return not bool(self.value)
|
|
|
|
return self.VM == 0
|
|
|
|
@property
|
|
def empty_value(self) -> bytes | list[str] | None | str | PersonName:
|
|
"""Return the value for an empty element.
|
|
|
|
See :func:`empty_value_for_VR` for more information.
|
|
|
|
Returns
|
|
-------
|
|
str or None
|
|
The value this data element is assigned on decoding if it is empty.
|
|
"""
|
|
return empty_value_for_VR(self.VR)
|
|
|
|
def clear(self) -> None:
|
|
"""Clears the value, e.g. sets it to the configured empty value.
|
|
|
|
See :func:`empty_value_for_VR`.
|
|
"""
|
|
self._value = self.empty_value
|
|
|
|
def _convert_value(self, val: Any) -> Any:
|
|
"""Convert `val` to an appropriate type and return the result.
|
|
|
|
Uses the element's VR in order to determine the conversion method and
|
|
resulting type.
|
|
"""
|
|
if (
|
|
self.tag == 0x7FE00010
|
|
and config.have_numpy
|
|
and isinstance(val, numpy.ndarray)
|
|
):
|
|
raise TypeError(
|
|
"The value for (7FE0,0010) 'Pixel Data' should be set using 'bytes' "
|
|
"not 'numpy.ndarray'. See the Dataset.set_pixel_data() method for "
|
|
"an alternative that supports ndarrays."
|
|
)
|
|
|
|
if self.VR == VR_.SQ: # a sequence - leave it alone
|
|
from pydicom.sequence import Sequence
|
|
|
|
if isinstance(val, Sequence):
|
|
return val
|
|
|
|
return Sequence(val)
|
|
|
|
# if the value is a list, convert each element
|
|
if not hasattr(val, "append"):
|
|
return self._convert(val)
|
|
|
|
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:
|
|
"""Convert `val` to an appropriate type for the element's VR."""
|
|
# If the value is bytes and has a VR that can only be encoded
|
|
# using the default character repertoire, convert it to a string
|
|
if self.VR in DEFAULT_CHARSET_VR and isinstance(val, bytes):
|
|
val = val.decode()
|
|
|
|
if self.VR == VR_.IS:
|
|
return pydicom.valuerep.IS(val, self.validation_mode)
|
|
|
|
if self.VR == VR_.DA and config.datetime_conversion:
|
|
return pydicom.valuerep.DA(val, validation_mode=self.validation_mode)
|
|
|
|
if self.VR == VR_.DS:
|
|
return pydicom.valuerep.DS(val, False, self.validation_mode)
|
|
|
|
if self.VR == VR_.DT and config.datetime_conversion:
|
|
return pydicom.valuerep.DT(val, validation_mode=self.validation_mode)
|
|
|
|
if self.VR == VR_.TM and config.datetime_conversion:
|
|
return pydicom.valuerep.TM(val, validation_mode=self.validation_mode)
|
|
|
|
if self.VR == VR_.UI:
|
|
return UID(val, self.validation_mode) if val is not None else None
|
|
|
|
if self.VR == VR_.PN:
|
|
return PersonName(val, validation_mode=self.validation_mode)
|
|
|
|
if self.VR == VR_.AT and (val == 0 or val):
|
|
return val if isinstance(val, BaseTag) else Tag(val)
|
|
|
|
self.validate(val)
|
|
return val
|
|
|
|
def __deepcopy__(self, memo: dict[int, Any]) -> "DataElement":
|
|
"""Implementation of copy.deepcopy()."""
|
|
# Overridden to allow for a nice exception message for buffered elements
|
|
cls = self.__class__
|
|
result = cls.__new__(cls)
|
|
memo[id(self)] = result
|
|
for k, v in self.__dict__.items():
|
|
if self.is_buffered and k == "_value":
|
|
try:
|
|
setattr(result, k, copy.deepcopy(v, memo))
|
|
except Exception as exc:
|
|
raise type(exc)(
|
|
f"Error deepcopying the buffered element {self.tag} "
|
|
f"'{self.name}': {exc}"
|
|
)
|
|
else:
|
|
setattr(result, k, copy.deepcopy(v, memo))
|
|
|
|
return result
|
|
|
|
def __eq__(self, other: Any) -> Any:
|
|
"""Compare `self` and `other` for equality.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
The result if `self` and `other` are the same class
|
|
NotImplemented
|
|
If `other` is not the same class as `self` then returning
|
|
:class:`NotImplemented` delegates the result to
|
|
``superclass.__eq__(subclass)``.
|
|
"""
|
|
# Faster result if same object
|
|
if other is self:
|
|
return True
|
|
|
|
if isinstance(other, self.__class__):
|
|
if self.tag != other.tag or self.VR != other.VR:
|
|
return False
|
|
|
|
# tag and VR match, now check the value
|
|
if config.have_numpy and isinstance(self.value, numpy.ndarray):
|
|
return len(self.value) == len(other.value) and numpy.allclose(
|
|
self.value, other.value
|
|
)
|
|
|
|
if not self.is_buffered and not other.is_buffered:
|
|
return self.value == other.value
|
|
|
|
try:
|
|
# `self` is buffered, `other` may or may not be buffered
|
|
if self.is_buffered:
|
|
return buffer_equality(self.value, other.value)
|
|
|
|
# `other` is buffered, `self` is not
|
|
return buffer_equality(other.value, self.value)
|
|
except Exception as exc:
|
|
raise type(exc)(f"Invalid buffer for {self.tag} '{self.name}': {exc}")
|
|
|
|
return NotImplemented
|
|
|
|
def __ne__(self, other: Any) -> Any:
|
|
"""Compare `self` and `other` for inequality."""
|
|
return not (self == other)
|
|
|
|
def __str__(self) -> str:
|
|
"""Return :class:`str` representation of the element."""
|
|
value = self.repval or ""
|
|
name = f"{self.name[:self.descripWidth]:<{self.descripWidth}}"
|
|
|
|
if self.showVR:
|
|
return f"{self.tag} {name} {self.VR}: {value}"
|
|
|
|
return f"{self.tag} {name} {value}"
|
|
|
|
@property
|
|
def repval(self) -> str:
|
|
"""Return a :class:`str` representation of the element's value."""
|
|
if self.is_buffered:
|
|
# in case the buffer is a stream and non-seekable we don't want
|
|
# to consume any bytes
|
|
return repr(self.value)
|
|
|
|
# If the VR is byte-like or long text (1024+), show a summary instead
|
|
if self.VR in LONG_VALUE_VR:
|
|
try:
|
|
length = len(self.value)
|
|
except TypeError:
|
|
pass
|
|
else:
|
|
if length > self.maxBytesToDisplay:
|
|
return f"Array of {length} elements"
|
|
|
|
if self.VM > self.maxBytesToDisplay:
|
|
return f"Array of {self.VM} elements"
|
|
|
|
if isinstance(self.value, UID):
|
|
return self.value.name
|
|
|
|
return repr(self.value)
|
|
|
|
def __getitem__(self, key: int) -> Any:
|
|
"""Return the item at `key` if the element's value is indexable."""
|
|
try:
|
|
return self.value[key]
|
|
except TypeError:
|
|
raise TypeError("DataElement value is unscriptable (not a Sequence)")
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the DICOM dictionary name for the element as :class:`str`.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
* For officially registered DICOM Data Elements this will be the
|
|
*Name* as given in
|
|
:dcm:`Table 6-1<part06/chapter_6.html#table_6-1>`.
|
|
* For private elements known to *pydicom* this will be the *Name*
|
|
in the format ``'[name]'``.
|
|
* For unknown private elements this will be ``'Private tag data'``.
|
|
* Otherwise returns an empty string ``''``.
|
|
"""
|
|
if self.tag.is_private:
|
|
if self.private_creator:
|
|
try:
|
|
# If we have the name from the private dictionary, use it,
|
|
# but put it in square brackets to make clear
|
|
# that the tag cannot be accessed by that name
|
|
name = private_dictionary_description(
|
|
self.tag, self.private_creator
|
|
)
|
|
return f"[{name}]"
|
|
except KeyError:
|
|
pass
|
|
elif self.tag.element >> 8 == 0:
|
|
return "Private Creator"
|
|
|
|
return "Private tag data" # default
|
|
|
|
if dictionary_has_tag(self.tag) or repeater_has_tag(self.tag):
|
|
return dictionary_description(self.tag)
|
|
|
|
# implied Group Length dicom versions < 3
|
|
if self.tag.element == 0:
|
|
return "Group Length"
|
|
|
|
return ""
|
|
|
|
@property
|
|
def is_private(self) -> bool:
|
|
"""Return ``True`` if the element's tag is private.
|
|
|
|
.. versionadded:: 2.1
|
|
"""
|
|
return self.tag.is_private
|
|
|
|
@property
|
|
def is_retired(self) -> bool:
|
|
"""Return the element's retired status as :class:`bool`.
|
|
|
|
For officially registered DICOM Data Elements this will be ``True`` if
|
|
the retired status as given in the DICOM Standard, Part 6,
|
|
:dcm:`Table 6-1<part06/chapter_6.html#table_6-1>` is 'RET'. For private
|
|
or unknown elements this will always be ``False``.
|
|
"""
|
|
if dictionary_has_tag(self.tag):
|
|
return dictionary_is_retired(self.tag)
|
|
|
|
return False
|
|
|
|
@property
|
|
def keyword(self) -> str:
|
|
"""Return the element's keyword (if known) as :class:`str`.
|
|
|
|
For officially registered DICOM Data Elements this will be the
|
|
*Keyword* as given in
|
|
:dcm:`Table 6-1<part06/chapter_6.html#table_6-1>`. For private or
|
|
unknown elements this will return an empty string ``''``.
|
|
"""
|
|
if dictionary_has_tag(self.tag):
|
|
return dictionary_keyword(self.tag)
|
|
|
|
return ""
|
|
|
|
def __repr__(self) -> str:
|
|
"""Return the representation of the element."""
|
|
return str(self)
|
|
|
|
|
|
class RawDataElement(NamedTuple):
|
|
"""Container for the data from a raw (mostly) undecoded element."""
|
|
|
|
tag: BaseTag
|
|
VR: str | None
|
|
length: int
|
|
value: bytes | None
|
|
value_tell: int
|
|
is_implicit_VR: bool
|
|
is_little_endian: bool
|
|
is_raw: bool = True
|
|
is_buffered: bool = False
|
|
|
|
|
|
def convert_raw_data_element(
|
|
raw: RawDataElement,
|
|
*,
|
|
encoding: str | MutableSequence[str] | None = None,
|
|
ds: "Dataset | None" = None,
|
|
) -> DataElement:
|
|
"""Return a :class:`DataElement` created from `raw`.
|
|
|
|
.. versionadded:: 3.0
|
|
|
|
**Customization Hooks**
|
|
|
|
All callback functions for :func:`convert_raw_data_element`
|
|
use the function signature::
|
|
|
|
def func(
|
|
raw: RawDataElement,
|
|
data: dict[str, Any],
|
|
*,
|
|
encoding: str | MutableSequence[str] | None = None,
|
|
ds: pydicom.dataset.Dataset | None = None,
|
|
**kwargs: dict[str, Any],
|
|
) -> None:
|
|
...
|
|
|
|
Where `raw`, `encoding` and `ds` are the objects passed to this function,
|
|
`data` is a :class:`dict` that's persistent for each raw element and is used
|
|
to store the results of the corresponding callback functions, and `kwargs`
|
|
are keyword arguments passed by setting the ``"raw_element_kwargs"`` hook
|
|
via :meth:`~pydicom.hooks.Hooks.register_kwargs`.
|
|
|
|
Available :class:`hooks<pydicom.hooks.Hooks>` are (in order of execution):
|
|
|
|
* ``"raw_element_vr"``: a function to perform the VR lookup for the raw element,
|
|
required to add ``{"VR": str}`` to `data`.
|
|
* ``"raw_element_value"``: a function to perform the conversion of the raw element
|
|
:class:`bytes` value to an appropriate type (such as :class:`int` for VR
|
|
**US**), required to add ``{"value": Any}`` to `data`.
|
|
|
|
:meth:`~pydicom.hooks.Hooks.register_callback` is used to set the callback
|
|
function for a given hook, and only one callback can be set per hook.
|
|
|
|
Parameters
|
|
----------
|
|
raw : pydicom.dataelem.RawDataElement
|
|
The raw data to convert to a :class:`DataElement`.
|
|
encoding : str | MutableSequence[str] | None
|
|
The character set encodings for the raw data element.
|
|
ds : pydicom.dataset.Dataset | None
|
|
The parent dataset of `raw`.
|
|
|
|
Returns
|
|
-------
|
|
pydicom.dataelem.DataElement
|
|
A :class:`~pydicom.dataelem.DataElement` instance created from `raw`.
|
|
"""
|
|
data: dict[str, Any] = {}
|
|
if config.data_element_callback:
|
|
raw = config.data_element_callback(raw, **config.data_element_callback_kwargs)
|
|
|
|
# Initializing **kwargs for each hook is a bit expensive, so avoid it if we can
|
|
if hooks.raw_element_kwargs:
|
|
hooks.raw_element_vr(
|
|
raw, data, encoding=encoding, ds=ds, **hooks.raw_element_kwargs
|
|
)
|
|
hooks.raw_element_value(
|
|
raw, data, encoding=encoding, ds=ds, **hooks.raw_element_kwargs
|
|
)
|
|
else:
|
|
hooks.raw_element_vr(raw, data, encoding=encoding, ds=ds)
|
|
hooks.raw_element_value(raw, data, encoding=encoding, ds=ds)
|
|
|
|
return DataElement(
|
|
raw.tag,
|
|
data["VR"],
|
|
data["value"],
|
|
raw.value_tell,
|
|
raw.length == 0xFFFFFFFF,
|
|
already_converted=True,
|
|
)
|
|
|
|
|
|
def _DataElement_from_raw(
|
|
raw_data_element: RawDataElement,
|
|
encoding: str | MutableSequence[str] | None = None,
|
|
dataset: "Dataset | None" = None,
|
|
) -> DataElement:
|
|
"""Return a :class:`DataElement` created from `raw_data_element`.
|
|
|
|
.. deprecated:: 3.0
|
|
|
|
``DataElement_from_raw`` will be removed in v4.0, use
|
|
:func:`~pydicom.dataelem.convert_raw_data_element` instead.
|
|
|
|
Call the configured data_element_callbacks to do relevant
|
|
pre/post-processing and convert values from raw to native types
|
|
|
|
Parameters
|
|
----------
|
|
raw_data_element : pydicom.dataelem.RawDataElement
|
|
The raw data to convert to a :class:`DataElement`.
|
|
encoding : str | list[str] | None
|
|
The character encoding of the raw data.
|
|
dataset : pydicom.dataset.Dataset | None
|
|
If given, used to resolve the VR for known private tags.
|
|
|
|
Returns
|
|
-------
|
|
DataElement
|
|
A :class:`~pydicom.dataelem.DataElement` instance created from `raw`.
|
|
"""
|
|
msg = (
|
|
"'pydicom.dataelem.DataElement_from_raw' is deprecated and will be removed "
|
|
"in v4.0, please use 'pydicom.dataelem.convert_raw_data_element' instead"
|
|
)
|
|
warn_and_log(msg, DeprecationWarning)
|
|
|
|
return convert_raw_data_element(
|
|
raw=raw_data_element,
|
|
encoding=encoding,
|
|
ds=dataset,
|
|
)
|
|
|
|
|
|
_DEPRECATED = {
|
|
"DataElement_from_raw": _DataElement_from_raw,
|
|
}
|
|
|
|
|
|
def __getattr__(name: str) -> Any:
|
|
if name in _DEPRECATED and not config._use_future:
|
|
return _DEPRECATED[name]
|
|
|
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|