Current section
Files
Jump to
Current section
Files
lib/dicom/data_set.ex
defmodule Dicom.DataSet do
@moduledoc """
A [DICOM data set](https://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html) is a collection of `Dicom.DataElement`s uniquely identified by their numeric tags.
"""
alias Dicom.UidRegistry
alias Dicom.DataSet
alias Dicom.DataElement
defstruct [:elements, :file_meta]
@type t :: %__MODULE__{}
@doc """
Create an empty data set.
## Examples
iex> DataSet.empty()
"""
@spec empty() :: t()
def empty() do
%__MODULE__{
elements: %{},
file_meta: nil
}
end
@doc """
Create a data set from a enumerable of `Dicom.DataElement`s.
## Examples
iex> (for i <- 1..3, do: Dicom.DataElement.from(0x0021, i, :US, i))
...> |> Dicom.DataSet.from_elements()
"""
@spec from_elements(Enumerable.t(DataElement.t())) :: t()
def from_elements(elements) do
elem_map =
elements
|> Enum.map(&{DataElement.tag(&1), &1})
|> Enum.into(%{})
%__MODULE__{elements: elem_map, file_meta: nil}
end
@doc """
Create a data set from a keyword-value mapping.
All keywords must be contained in the data dictionary.
## Examples
iex> Dicom.DataSet.from_keyword_list(PatientID: "JDOE123", PatientName: "Doe^John")
"""
@spec from_keyword_list([{atom(), any()}]) :: t()
def from_keyword_list(value_list) do
value_list
|> Enum.map(fn {keyword, value} ->
Dicom.DataElement.from(keyword, if(is_list(value), do: value, else: [value]))
end)
|> from_elements()
end
@typedoc """
Index value to access an element inside a data set.
It can be either:
1. an atom representing a known element keyword from the data dictionary,
2. a 32-bit integer representing the tag or
3. a tuple of 2 16-bit integers representing group and element of the tag respectively.
"""
@type key_type() :: atom() | integer() | {integer(), integer()}
defp resolve_key(key) do
case key do
kw when is_atom(kw) ->
case Dicom.TagDict.get_tag_parts_by_keyword(kw) do
{:ok, {group_no, elem_no}} -> {:ok, Bitwise.bsl(group_no, 16) + elem_no}
:error -> :error
end
{group_no, elem_no} ->
{:ok, Bitwise.bsl(group_no, 16) + elem_no}
tag when is_integer(tag) ->
{:ok, tag}
_ ->
:error
end
end
@doc """
Fetches the `Dicom.DataElement` identified by `key` from the `ds`.
If `ds` contains the element, it is returned in the shape of `{:ok, element}`. If not,
the atom `:error` is returned instead.
## Examples
iex> ds = DataSet.from_keyword_list(PatientID: "JDOE123")
iex> {:ok, _element} = DataSet.fetch(ds, :PatientID)
iex> {:ok, _element} = DataSet.fetch(ds, {0x0010, 0x0020})
iex> {:ok, _element} = DataSet.fetch(ds, 0x00100020)
iex> :error = DataSet.fetch(ds, :StudyID)
"""
@spec fetch(t(), key_type()) :: {:ok, DataElement.t()} | {:error, atom()}
def fetch(ds, key) do
with {:ok, tag} <- resolve_key(key),
{:ok, de} <- Map.fetch(ds.elements, tag) do
{:ok, de}
else
_ -> :error
end
end
@doc """
Fetches the `Dicom.DataElement` for `key` from `ds`, erroring out if it does not exist.
If `ds` contains `key`, the `Dicom.DataElement` is returned. If not,
a `KeyError` exception is raised.
## Examples
iex> ds = DataSet.from_keyword_list(PatientID: "JDOE123")
iex> _element = DataSet.fetch!(ds, :PatientID)
"""
@spec fetch!(t(), key_type()) :: DataElement.t()
def fetch!(ds, key) do
case fetch(ds, key) do
{:ok, de} -> de
:error -> raise KeyError, "Element #{key} not found in data set."
end
end
@doc """
Fetches the value of the data element identified by `key`.
This is a shortcut to directly access a specific value. If the
data element has a value multiplicity > 1, a specific index can
be accessed with the `index` parameter.
## Examples
iex> ds = DataSet.from_keyword_list(OtherPatientNames: ["Max", "Tom"])
iex> DataSet.value_for!(ds, :OtherPatientNames, 1)
"Tom"
"""
@spec value_for!(t(), key_type(), integer()) :: any()
def value_for!(ds, key, index \\ 0) when is_integer(index) do
ds
|> fetch!(key)
|> DataElement.value(index)
end
@doc """
Updates element identified by `key` in `data_set`.
The element identified by `key` is updated to `value_representation`
and `values`. If `key` already exists, it is overridden.
Returns the updated data set.
## Examples
iex> ds = DataSet.empty()
...> |> DataSet.put(:SOPInstanceUID, :UI, ["1.2.3"])
iex> DataSet.value_for!(ds, :SOPInstanceUID)
"1.2.3"
"""
@spec put(t(), key_type(), atom(), [any()]) :: t()
def put(data_set, key, value_representation, values) do
{:ok, tag} = resolve_key(key)
data_element = Dicom.DataElement.new(tag, value_representation, values)
%DataSet{
elements: Map.put(data_set.elements, tag, data_element),
file_meta: data_set.file_meta
}
end
@doc """
Checks if `data_set` contains an element `key`.
## Examples
iex> ds = DataSet.from_keyword_list(StudyID: "1.2.3")
iex> DataSet.has_key?(ds, :StudyID)
true
iex> DataSet.has_key?(ds, :PatientID)
false
"""
@spec has_key?(t(), key_type()) :: boolean()
def has_key?(data_set, key) do
{:ok, tag} = resolve_key(key)
Map.has_key?(data_set.elements, tag)
end
@doc """
Set a default value for `key` in `data_set` if there is no existing value.
## Examples
iex> ds = DataSet.from_keyword_list(StudyID: "1.2.3")
iex> ds = ds
...> |> DataSet.put_default(:PatientID, :SH, ["PAT123"])
...> |> DataSet.put_default(:StudyID, :SH, ["already_set"])
iex> DataSet.value_for!(ds, :PatientID)
"PAT123"
iex> DataSet.value_for!(ds, :StudyID)
"1.2.3"
"""
@spec put_default(t(), key_type(), atom(), [any()]) :: t()
def put_default(data_set, key, value_representation, values) do
{:ok, tag} = resolve_key(key)
if not has_key?(data_set, tag) do
put(data_set, tag, value_representation, values)
else
data_set
end
end
@doc """
Access `file_meta` of a data set.
Returns an empty data set if no `file_meta` exists.
"""
@spec file_meta(t()) :: t()
def file_meta(data_set) do
if(is_nil(data_set.file_meta), do: DataSet.empty(), else: data_set.file_meta)
end
@doc """
Add `file_meta` to dataset by the `meta_attributes` keyword list.
## Examples
iex> ds = DataSet.from_keyword_list(SOPInstanceUID: "1.2.3")
...> |> DataSet.with_file_meta_from_keywords(TransferSyntaxUID: "1.2.840.10008.1.2.1")
iex> ds
...> |> DataSet.file_meta()
...> |> DataSet.value_for!(:TransferSyntaxUID)
"1.2.840.10008.1.2.1"
"""
@spec with_file_meta_from_keywords(t(), [{atom(), any()}]) :: t()
def with_file_meta_from_keywords(data_set, meta_attributes) do
file_meta = DataSet.from_keyword_list(meta_attributes)
%__MODULE__{elements: data_set.elements, file_meta: file_meta}
end
@doc """
Add transfer syntax by `transfer_syntax_name` to `file_meta` of `data_set`.
## Examples
iex> ds = DataSet.empty()
...> |> DataSet.with_transfer_syntax(:explicit_vr_little_endian)
iex> ds
...> |> DataSet.file_meta()
...> |> DataSet.value_for!(:TransferSyntaxUID)
"1.2.840.10008.1.2.1"
"""
@spec with_transfer_syntax(t(), atom()) :: t()
def with_transfer_syntax(data_set, transfer_syntax_name) do
ts_uid = UidRegistry.get_transfer_syntax_uid_by_name!(transfer_syntax_name)
file_meta_new =
data_set
|> DataSet.file_meta()
|> DataSet.put(:TransferSyntaxUID, :UI, [ts_uid])
%__MODULE__{elements: data_set.elements, file_meta: file_meta_new}
end
end
defimpl Enumerable, for: Dicom.DataSet do
def member?(ds, element) do
Enumerable.Map.member?(ds.elements, element)
end
def reduce(ds, acc, fun) do
Enumerable.Map.reduce(ds.elements, acc, fun)
end
def slice(ds) do
Enumerable.Map.slice(ds.elements)
end
def count(ds) do
Enumerable.Map.count(ds.elements)
end
end
defimpl String.Chars, for: Dicom.DataSet do
def to_string(ds) do
type_str =
if Enum.all?(ds.elements, fn {tag, _el} -> tag < 0x00030000 end) do
"# File Meta"
else
"# Data Set"
end
ds_str =
ds.elements
|> Enum.sort(fn {tag1, _el1}, {tag2, _el2} -> tag1 <= tag2 end)
|> Enum.map(fn {_tag, el} -> String.Chars.to_string(el) end)
|> Enum.join("\n")
parts = [type_str, ds_str]
parts =
if is_nil(ds.file_meta) do
parts
else
[String.Chars.to_string(ds.file_meta) | parts]
end
Enum.join(parts, "\n")
end
end