Current section
Files
Jump to
Current section
Files
lib/ex_dicom/reader/find_end_of_encapsulated_element.ex
defmodule ExDicom.Reader.FindEndOfEncapsulatedElement do
@moduledoc """
Internal helper functions for parsing encapsulated DICOM pixel data elements.
Handles reading and managing fragments of encapsulated pixel data.
"""
alias ExDicom.ByteStream
alias ExDicom.Reader.ReadTag
@sequence_delimiter_tag "xfffee0dd"
@item_tag "xfffee000"
@typedoc """
Type representing a pixel data fragment
"""
@type fragment :: %{
offset: non_neg_integer(),
position: non_neg_integer(),
length: non_neg_integer()
}
@doc """
Reads an encapsulated pixel data element and adds an array of fragments to the element
containing the offset and length of each fragment and any offsets from the basic offset
table.
## Parameters
* byte_stream - The byte stream to read from
* element - The element to add fragment information to
* warnings - List to collect any warnings during parsing
## Returns
The modified element with fragment information added
## Raises
* RuntimeError if required parameters are missing or if basic offset table is not found
"""
# Success case
@spec find(byte_stream :: ByteStream.t(), element :: map(), warnings :: list()) ::
{:ok, map(), ByteStream.t(), list()}
# Error case
| {:error, String.t()}
def find(nil, _element, _warnings) do
{:error,
"ExDicom.Reader.FindEndOfEncapsulatedElement.find: missing required parameter 'byte_stream'"}
end
def find(_byte_stream, nil, _warnings) do
{:error,
"ExDicom.Reader.FindEndOfEncapsulatedElement.find: missing required parameter 'element'"}
end
def find(%ByteStream{} = byte_stream, element, warnings) do
# Initialize element with encapsulated pixel data properties
element =
element
|> Map.put(:encapsulated_pixel_data, true)
|> Map.put(:basic_offset_table, [])
|> Map.put(:fragments, [])
# Read and validate basic offset table
case ReadTag.read_tag(byte_stream) do
{:ok, @item_tag, stream} ->
{element, final_stream, final_warnings} =
read_basic_offset_table(stream, element)
|> read_fragments(stream, warnings)
# Ensure we return the correct tuple format
{:ok, element, final_stream, final_warnings}
{:ok, other_tag, _} ->
{:error,
"ExDicom.Reader.FindEndOfEncapsulatedElement: basic offset table not found, got tag: #{inspect(other_tag)}"}
{:error, reason} ->
{:error, reason}
end
end
# Reads the basic offset table and adds offsets to the element
@spec read_basic_offset_table(byte_stream :: ByteStream.t(), element :: map()) :: map()
defp read_basic_offset_table(%ByteStream{} = byte_stream, element) do
case ByteStream.read_uint32(byte_stream) do
{:ok, basic_offset_table_length, updated_stream} ->
num_fragments = div(basic_offset_table_length, 4)
{basic_offset_table, _final_stream} =
Enum.reduce(1..num_fragments, {[], updated_stream}, fn _i, {offsets, stream} ->
{:ok, offset, stream} = ByteStream.read_uint32(stream)
{offsets ++ [offset], stream}
end)
%{element | basic_offset_table: basic_offset_table}
{:error, reason} ->
raise "Failed to read basic offset table length: #{reason}"
end
end
# Reads fragments until sequence delimiter or end of stream is reached
@spec read_fragments(element :: map(), byte_stream :: ByteStream.t(), warnings :: list()) ::
{map(), ByteStream.t(), list()}
defp read_fragments(element, %ByteStream{} = byte_stream, warnings) do
base_offset = byte_stream.position
read_fragments_loop(element, byte_stream, warnings, base_offset)
end
defp read_fragments_loop(element, %ByteStream{} = byte_stream, warnings, base_offset) do
if byte_stream.position >= byte_stream.byte_array.length do
updated_warnings =
add_warning(
warnings,
"pixel data element #{element.tag} missing sequence delimiter tag #{@sequence_delimiter_tag}"
)
{element, byte_stream, updated_warnings}
else
{:ok, tag, read_byte_stream} = ReadTag.read_tag(byte_stream)
{:ok, length, read_uint32_byte_stream} = ByteStream.read_uint32(read_byte_stream)
case tag do
@sequence_delimiter_tag ->
{:ok, new_byte_stream} = ByteStream.seek(read_uint32_byte_stream, length)
updated_element = %{element | length: new_byte_stream.position - element.data_offset}
{updated_element, new_byte_stream, warnings}
@item_tag ->
fragment = %{
offset: read_uint32_byte_stream.position - base_offset - 8,
position: read_uint32_byte_stream.position,
length: length
}
updated_element = update_in(element.fragments, &(&1 ++ [fragment]))
{:ok, new_byte_stream} = ByteStream.seek(read_uint32_byte_stream, length)
read_fragments_loop(updated_element, new_byte_stream, warnings, base_offset)
unexpected_tag ->
handle_unexpected_tag(
element,
read_uint32_byte_stream,
warnings,
base_offset,
unexpected_tag,
length
)
end
end
end
# Handles unexpected tags during fragment reading
defp handle_unexpected_tag(element, byte_stream, warnings, base_offset, tag, length) do
updated_warnings =
add_warning(
warnings,
"unexpected tag #{tag} while searching for end of pixel data element with undefined length"
)
length = min(length, byte_stream.byte_array.length - byte_stream.position)
fragment = %{
offset: byte_stream.position - base_offset - 8,
position: byte_stream.position,
length: length
}
updated_element = update_in(element.fragments, &(&1 ++ [fragment]))
{:ok, updated_byte_stream} = ByteStream.seek(byte_stream, length)
final_element = %{
updated_element
| length: updated_byte_stream.position - element.data_offset
}
{final_element, updated_byte_stream, updated_warnings}
end
# Helper function to add warnings if warnings list is provided
defp add_warning(nil, _warning), do: nil
defp add_warning(warnings, warning) when is_list(warnings), do: warnings ++ [warning]
end