Packages
Terminal emulation and driver infrastructure for Raxol. ANSI parsing, screen buffers, command processing, cursor management, input handling, session management, and termbox2 NIF integration.
Current section
Files
Jump to
Current section
Files
lib/raxol/terminal/ansi/sixel_parser.ex
defmodule Raxol.Terminal.ANSI.SixelParser do
@moduledoc """
Handles the parsing logic for Sixel graphics data streams within a DCS sequence.
"""
alias Raxol.Core.Runtime.Log
alias Raxol.Terminal.ANSI.SixelPalette
alias Raxol.Terminal.ANSI.Utils.SixelPatternMap
defmodule ParserState do
@moduledoc """
Represents the state during the parsing of a Sixel graphics data stream.
Tracks position, color, palette, and pixel buffer information.
"""
@type t :: %__MODULE__{
x: integer(),
y: integer(),
color_index: integer(),
repeat_count: integer(),
palette: map(),
raster_attrs: map(),
pixel_buffer: map(),
max_x: integer(),
max_y: integer()
}
defstruct [
:x,
:y,
:color_index,
:repeat_count,
:palette,
:raster_attrs,
:pixel_buffer,
:max_x,
:max_y
]
end
@spec parse(binary(), ParserState.t()) ::
{:ok, ParserState.t()} | {:error, atom()}
def parse(data, state) when is_binary(data) do
Log.debug(
"SixelParser: Incoming palette color 1 is #{inspect(Map.get(state.palette, 1, :not_found))}"
)
case data do
<<>> ->
{:ok, state}
<<"\eP", rest::binary>> ->
handle_dcs_start(rest, state)
<<"\e\\", _rest::binary>> ->
{:ok, state}
<<" ", rest::binary>> ->
parse(rest, state)
_ ->
handle_command(data, state)
end
end
defp handle_dcs_start(rest, state) do
case rest do
<<"q", rest::binary>> -> parse(rest, state)
_ -> {:error, :missing_or_misplaced_q}
end
end
defp handle_command(data, state) do
case data do
<<"\"", rest::binary>> ->
handle_raster_attributes(rest, state)
<<"#", rest::binary>> ->
handle_color_definition(rest, state)
<<"!", rest::binary>> ->
handle_repeat_command(rest, state)
<<"$", rest::binary>> ->
handle_carriage_return(rest, state)
<<"-", rest::binary>> ->
handle_new_line(rest, state)
<<char_byte, remaining_data::binary>> ->
handle_data_character(char_byte, remaining_data, state)
end
end
defp handle_raster_attributes(rest, state) do
case consume_integer_params(rest) do
{:ok, params, remaining_data} ->
new_attrs = create_raster_attrs(params)
parse(remaining_data, %{state | raster_attrs: new_attrs})
{:error, reason, _} ->
Raxol.Core.Runtime.Log.warning_with_context(
"Sixel Parser: Error parsing Raster Attributes: #{inspect(reason)}. Skipping.",
%{}
)
parse(rest, state)
end
end
defp create_raster_attrs([pan, pad, ph, pv]),
do: %{
aspect_num: pan || 1,
aspect_den: pad || 1,
width: ph,
height: pv
}
defp create_raster_attrs(params),
do: %{
aspect_num: Enum.at(params, 0) || 1,
aspect_den: Enum.at(params, 1) || 1,
width: Enum.at(params, 2),
height: Enum.at(params, 3)
}
defp handle_color_definition(rest, state) do
case consume_integer_params(rest) do
{:ok, [pc | color_params], remaining_data} ->
case color_params do
[] -> handle_color_selection([pc], remaining_data, state)
_ -> handle_color_params(pc, color_params, remaining_data, state)
end
{:ok, params, remaining_data} ->
handle_color_selection(params, remaining_data, state)
{:error, reason, _} ->
Raxol.Core.Runtime.Log.warning_with_context(
"Sixel Parser: Error parsing Color Definition: #{inspect(reason)}. Skipping.",
%{}
)
parse(rest, state)
end
end
defp handle_color_params(pc, color_params, remaining_data, state) do
case pc >= 0 and pc <= SixelPalette.max_colors() do
true ->
color_space = Enum.at(color_params, 0) || 1
px = Enum.at(color_params, 1) || 0
py = Enum.at(color_params, 2) || 0
pz = Enum.at(color_params, 3) || 0
case SixelPalette.convert_color(color_space, px, py, pz) do
{:ok, {r, g, b}} ->
new_palette = Map.put(state.palette, pc, {r, g, b})
parse(remaining_data, %{
state
| palette: new_palette,
color_index: pc
})
{:error, reason} ->
Raxol.Core.Runtime.Log.warning_with_context(
"Sixel Parser: Invalid color definition ##{pc}: #{inspect(reason)}. Skipping.",
%{}
)
parse(remaining_data, state)
end
false ->
Raxol.Core.Runtime.Log.warning_with_context(
"Sixel Parser: Invalid color index ##{pc}. Skipping.",
%{}
)
parse(remaining_data, state)
end
end
defp handle_color_selection(params, remaining_data, state) do
max_colors = SixelPalette.max_colors()
case params do
[pc] when pc >= 0 and pc <= max_colors ->
parse(remaining_data, %{state | color_index: pc})
[] ->
parse(remaining_data, %{state | color_index: 0})
_ ->
Raxol.Core.Runtime.Log.warning_with_context(
"Sixel Parser: Unexpected params for Color Definition: #{inspect(params)}. Skipping.",
%{}
)
parse(remaining_data, state)
end
end
defp handle_repeat_command(rest, state) do
case consume_integer_params(rest) do
{:ok, [pn], remaining_data} when pn > 0 ->
parse(remaining_data, %{state | repeat_count: pn})
{:ok, [pn], remaining_data} ->
Raxol.Core.Runtime.Log.warning_with_context(
"Sixel Parser: Invalid repeat count found (!#{pn}). Skipping repeat command.",
%{}
)
parse(remaining_data, state)
{:ok, [], remaining_data} ->
parse(remaining_data, state)
{:error, reason, _} ->
Raxol.Core.Runtime.Log.warning_with_context(
"Sixel Parser: Error parsing Repeat Command: #{inspect(reason)}. Skipping.",
%{}
)
parse(rest, state)
end
end
defp handle_carriage_return(rest, state) do
new_y = state.y + 6
parse(rest, %{
state
| x: 0,
y: new_y,
max_y: max(state.max_y, new_y + 5)
})
end
defp handle_new_line(rest, state) do
new_y = state.y + 6
parse(rest, %{
state
| x: 0,
y: new_y,
max_y: max(state.max_y, new_y + 5)
})
end
defp handle_data_character(char_byte, remaining_data, state) do
Log.debug(
"SixelParser: [handle_data_character] BEFORE pixel gen, palette color 1 is #{inspect(Map.get(state.palette, 1, :not_found))}"
)
Log.debug("SixelParser: Processing character byte: #{char_byte} ('#{<<char_byte>>}')")
case SixelPatternMap.get_pattern(char_byte) do
pattern_int when is_integer(pattern_int) ->
Log.debug("SixelParser: Got pattern #{pattern_int} for character #{char_byte}")
{final_buffer, final_x, final_max_x} =
generate_repeated_pixels(
pattern_int,
state.x,
state.y,
state.color_index,
state.repeat_count,
state.pixel_buffer,
state.max_x
)
Log.debug("SixelParser: Generated pixels, buffer size: #{map_size(final_buffer)}")
Log.debug("SixelParser: Final buffer: #{inspect(final_buffer)}")
Log.debug(
"SixelParser: [handle_data_character] AFTER pixel gen, palette color 1 is #{inspect(Map.get(state.palette, 1, :not_found))}"
)
parse(remaining_data, %{
state
| x: final_x,
repeat_count: 1,
pixel_buffer: final_buffer,
max_x: final_max_x,
max_y: max(state.max_y, state.y + 5)
})
nil ->
Log.debug("SixelParser: No pattern found for character #{char_byte}")
case remaining_data do
<<"\e\\", _::binary>> ->
parse(remaining_data, state)
_ ->
case String.contains?(remaining_data, "\e\\") do
true -> parse(remaining_data, state)
false -> {:error, :missing_st}
end
end
end
end
defp generate_pixels_for_pattern(pattern_int, x, y, color) do
pixels =
Enum.reduce(0..5, %{}, fn bit_index, acc ->
set? = Bitwise.band(pattern_int, Bitwise.bsl(1, bit_index)) != 0
case set? do
true -> Map.put(acc, {x, y + bit_index}, color)
false -> acc
end
end)
{pixels, x, y + 5}
end
defp generate_repeated_pixels(
pattern_int,
start_x,
y,
color,
repeat,
buffer,
max_x
) do
Enum.reduce(0..(repeat - 1), {buffer, start_x, max_x}, fn _i,
{current_buffer, current_x,
current_max_x} ->
{pixels, _, _} =
generate_pixels_for_pattern(pattern_int, current_x, y, color)
merged_buffer = Map.merge(current_buffer, pixels)
{merged_buffer, current_x + 1, max(current_max_x, current_x)}
end)
end
def consume_integer_params(input_binary) do
case Regex.run(~r/^([0-9;]*)(.*)/s, input_binary) do
[_full_match, param_section, rest_of_binary] when param_section != "" ->
case Raxol.Core.ErrorHandling.safe_call(fn ->
params =
param_section
|> String.split(";", trim: false)
|> Enum.map(fn
"" -> 0
str -> String.to_integer(str)
end)
{:ok, params, rest_of_binary}
end) do
{:ok, result} ->
result
{:error, reason} ->
{:error, {"Invalid integer parameter in '#{param_section}'", reason}, input_binary}
end
[_full_match, "", rest_of_binary] ->
{:ok, [], rest_of_binary}
nil ->
{:ok, [], input_binary}
end
end
end