Files
René Mathieu 0fef8d96c5 Initial commit
2026-01-17 13:49:51 +01:00

350 lines
12 KiB
Python
Executable File

# Copyright 2008-2021 pydicom authors. See LICENSE file for details.
"""Methods for converting Datasets and DataElements to/from json"""
import base64
from inspect import signature
from typing import TypeAlias, Any, cast, TYPE_CHECKING
from collections.abc import Callable
from pydicom.misc import warn_and_log
from pydicom.valuerep import FLOAT_VR, INT_VR, VR
if TYPE_CHECKING: # pragma: no cover
from pydicom.dataset import Dataset
JSON_VALUE_KEYS = ("Value", "BulkDataURI", "InlineBinary")
def convert_to_python_number(value: Any, vr: str) -> Any:
"""When possible convert numeric-like values to either ints or floats
based on their value representation.
Parameters
----------
value : Any
Value of the data element.
vr : str
Value representation of the data element.
Returns
-------
Any
* If `value` is empty then returns the `value` unchanged.
* If `vr` is an integer-like VR type then returns ``int`` or
``List[int]``
* If `vr` is a float-like VR type then returns ``float`` or
``List[float]``
* Otherwise returns `value` unchanged
"""
from pydicom.dataelem import empty_value_for_VR
if value is None or "":
return value
number_type: type[int] | type[float] | None = None
if vr in (INT_VR - {VR.AT}) | {VR.US_SS}:
number_type = int
if vr in FLOAT_VR:
number_type = float
if number_type is None:
return value
if isinstance(value, list | tuple):
return [
number_type(v) if v is not None else empty_value_for_VR(vr) for v in value
]
return number_type(value)
OtherValueType = None | str | int | float
PNValueType = None | str | dict[str, str]
SQValueType = dict[str, Any] | None # Recursive
ValueType: TypeAlias = PNValueType | SQValueType | OtherValueType
InlineBinaryType: TypeAlias = str | list[str]
BulkDataURIType: TypeAlias = str | list[str]
JSONValueType = list[ValueType] | InlineBinaryType | BulkDataURIType
BulkDataType = None | str | int | float | bytes
BulkDataHandlerType = Callable[[str, str, str], BulkDataType] | None
class JsonDataElementConverter:
"""Convert from a JSON struct to a :class:`DataElement`.
References
----------
* :dcm:`Annex F of Part 18 of the DICOM Standard<part18/chapter_F.html>`
* `JSON to Python object conversion table
<https://docs.python.org/3/library/json.html#json-to-py-table>`_
"""
def __init__(
self,
dataset_class: type["Dataset"],
tag: str,
vr: str,
value: JSONValueType,
value_key: str | None,
bulk_data_uri_handler: (
BulkDataHandlerType | Callable[[str], BulkDataType] | None
) = None,
) -> None:
"""Create a new converter instance.
Parameters
----------
dataset_class : dataset.Dataset derived class
The class object to use for **SQ** element items.
tag : str
The data element's tag in uppercase hex format like ``"7FE00010"``.
vr : str
The data element value representation.
value : str or List[None | str | int | float | dict]
The attribute value for the JSON object's "Value", "InlineBinary"
or "BulkDataURI" field. If there's no such attribute then `value`
will be ``[""]``.
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, optional
Callable function that accepts either the `tag`, `vr` and the
"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).
"""
self.dataset_class = dataset_class
self.tag = tag
self.vr = vr
self.value = value
self.value_key = value_key
self.bulk_data_element_handler: BulkDataHandlerType
handler = bulk_data_uri_handler
if handler and len(signature(handler).parameters) == 1:
# `handler` is Callable[[str], BulkDataType]
def wrapper(tag: str, vr: str, value: str) -> BulkDataType:
x = cast(Callable[[str], BulkDataType], handler)
return x(value)
self.bulk_data_element_handler = wrapper
else:
self.bulk_data_element_handler = cast(BulkDataHandlerType, handler)
def get_element_values(self) -> Any:
"""Return a the data element value or list of values.
Returns
-------
None, str, float, int, bytes, dataset_class or a list of these
The value or value list of the newly created data element.
"""
from pydicom.dataelem import empty_value_for_VR
# An attribute with an empty value should have no "Value",
# "BulkDataURI" or "InlineBinary"
if self.value_key is None:
return empty_value_for_VR(self.vr)
if self.value_key == "Value":
if not isinstance(self.value, list):
raise TypeError(
f"'{self.value_key}' of data element '{self.tag}' must be a list"
)
if not self.value:
return empty_value_for_VR(self.vr)
val = cast(list[ValueType], self.value)
element_value = [self.get_regular_element_value(v) for v in val]
if len(element_value) == 1 and self.vr != VR.SQ:
element_value = element_value[0]
return convert_to_python_number(element_value, self.vr)
# The value for "InlineBinary" shall be encoded as a base64 encoded
# string, as shown in PS3.18, Table F.3.1-1, but the example in
# PS3.18, Annex F.4 shows the string enclosed in a list.
# We support both variants, as the standard is ambiguous here,
# and do the same for "BulkDataURI".
value = cast(str | list[str], self.value)
if isinstance(value, list):
value = value[0]
if self.value_key == "InlineBinary":
# The `value` should be a base64 encoded str
if not isinstance(value, str):
raise TypeError(
f"Invalid attribute value for data element '{self.tag}' - "
"the value for 'InlineBinary' must be str, not "
f"{type(value).__name__}"
)
return base64.b64decode(value) # bytes
if self.value_key == "BulkDataURI":
# The `value` should be a URI as a str
if not isinstance(value, str):
raise TypeError(
f"Invalid attribute value for data element '{self.tag}' - "
"the value for 'BulkDataURI' must be str, not "
f"{type(value).__name__}"
)
if self.bulk_data_element_handler is None:
warn_and_log(
"No bulk data URI handler provided for retrieval "
f'of value of data element "{self.tag}"'
)
return empty_value_for_VR(self.vr)
return self.bulk_data_element_handler(self.tag, self.vr, value)
raise ValueError(
f"Unknown attribute name '{self.value_key}' for tag {self.tag}"
)
def get_regular_element_value(self, value: ValueType) -> Any:
"""Return a the data element value created from a json "Value" entry.
Parameters
----------
value : None, str, int, float or dict
The data element's value from the json entry.
Returns
-------
None, str, int, float or Dataset
A single value of the corresponding :class:`DataElement`.
"""
from pydicom.dataelem import empty_value_for_VR
# Table F.2.3-1 has JSON type mappings
if self.vr == VR.SQ: # Dataset
# May be an empty dict
value = cast(dict[str, Any], value)
return self.get_sequence_item(value)
if value is None:
return empty_value_for_VR(self.vr)
if self.vr == VR.PN: # str
value = cast(dict[str, str], value)
return self.get_pn_element_value(value)
if self.vr == VR.AT: # Optional[int]
# May be an empty str
value = cast(str, value)
try:
return int(value, 16)
except ValueError:
warn_and_log(f"Invalid value '{value}' for AT element - ignoring it")
return None
return value
def get_sequence_item(self, value: SQValueType) -> "Dataset":
"""Return a sequence item for the JSON dict `value`.
Parameters
----------
value : dict or None
The sequence item from the JSON entry.
Returns
-------
dataset_class
The decoded dataset item.
Raises
------
KeyError
If the "vr" key is missing for a contained element
"""
from pydicom import DataElement
from pydicom.dataelem import empty_value_for_VR
ds = self.dataset_class()
value = {} if value is None else value
for key, val in value.items():
if "vr" not in val:
raise KeyError(f"Data element '{self.tag}' must have key 'vr'")
vr = val["vr"]
unique_value_keys = tuple(set(val.keys()) & set(JSON_VALUE_KEYS))
if not unique_value_keys:
# data element with no value
elem = DataElement(
tag=int(key, 16), value=empty_value_for_VR(vr), VR=vr
)
else:
value_key = unique_value_keys[0]
elem = DataElement.from_json(
self.dataset_class,
key,
vr,
val[value_key],
value_key,
self.bulk_data_element_handler,
)
ds.add(elem)
return ds
def get_pn_element_value(self, value: str | dict[str, str]) -> str:
"""Return a person name from JSON **PN** value as str.
Values with VR PN have a special JSON encoding, see the DICOM Standard,
Part 18, :dcm:`Annex F.2.2<part18/sect_F.2.2.html>`.
Parameters
----------
value : Dict[str, str]
The person name components in the JSON entry.
Returns
-------
str
The decoded PersonName object or an empty string.
"""
if not isinstance(value, dict):
# Some DICOMweb services get this wrong, so we
# workaround the issue and warn the user
# rather than raising an error.
warn_and_log(
f"Value of data element '{self.tag}' with VR Person Name (PN) "
"is not formatted correctly"
)
return value
if "Phonetic" in value:
comps = ["", "", ""]
elif "Ideographic" in value:
comps = ["", ""]
else:
comps = [""]
if "Alphabetic" in value:
comps[0] = value["Alphabetic"]
if "Ideographic" in value:
comps[1] = value["Ideographic"]
if "Phonetic" in value:
comps[2] = value["Phonetic"]
return "=".join(comps)