Initial commit
This commit is contained in:
349
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/jsonrep.py
vendored
Executable file
349
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/jsonrep.py
vendored
Executable file
@@ -0,0 +1,349 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user