ExRatatui.Pixels (ExRatatui v0.18.0)

Copy Markdown View Source

Raw operations on the RGB8 bitmaps that ExRatatui.CellSession.Region carries.

A region's :data is row-major, three bytes per pixel, no padding. A consumer painting those bitmaps onto a real surface sometimes needs the pixels in a different arrangement than the one the widget rasterized — a panel mounted on its side wants the bitmap turned, a framebuffer wants it scaled to the rect and packed into its own pixel format. Doing that per pixel in Elixir costs a consumer about as much again as painting the panel in the first place, so it happens here instead, in one pass over the buffer.

The work is bounded: a region's longest side is capped at 1280 px, so the largest bitmap in play is a few megabytes.

Rotation

Rotations are clockwise, and use the same corner mapping a framebuffer consumer uses for its screen. For a width × height source, the pixel landing at destination (px, py) comes from source:

  • 90 — (py, height - 1 - px), destination is height × width
  • 180 — (width - 1 - px, height - 1 - py), destination keeps its size
  • 270 — (width - 1 - py, px), destination is height × width

Packing

pack_rgb8/5 takes a region's bitmap all the way to the bytes a panel stores, in a single pass with no intermediate copies:

  1. Sample onto :scale_to (the rect's pixel size in the app's orientation) by floor nearest: logical pixel (lx, ly) is source (div(lx * width, scale_w), div(ly * height, scale_h)).
  2. Clip to the top-left :clip part of that. Sampling is against the full :scale_to, so a region cut off at the edge of the grid samples as if it were whole.
  3. Rotate clockwise by :rotate, with the corner mapping above.
  4. Pack each pixel:
FormatBytes per pixelLayout
:rgb83r, g, b (no packing)
:rgb5652rrrrrggg gggbbbbb, little-endian
:xrgb88884b, g, r, 255
:mono_bayer410 (ink) or 255 (paper)

:mono_bayer4 compares Rec. 601 luma div(299 * r + 587 * g + 114 * b, 1000) against a Bayer 4×4 threshold, bayer[rem(y, 4)][rem(x, 4)] * 16 + 8 with bayer = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]]. x and y are panel positions: :origin is where the output's top-left pixel lands, so the dither pattern stays fixed to the panel wherever a region sits.

:rgb8 is there for consumers with formats of their own: they still get the sampling, clipping and rotation natively and pack the result themselves.

A pass costs one or two nanoseconds a pixel on a laptop, the same flat or turned; packing per pixel in Elixir is closer to 20–30.

Summary

Types

A packed pixel layout; see the Packing section above.

Options for pack_rgb8/5

Functions

Resamples, clips, rotates and packs an RGB8 bitmap in one pass.

Rotates an RGB8 bitmap clockwise by angle degrees.

Types

angle()

@type angle() :: 0 | 90 | 180 | 270

format()

@type format() :: :rgb8 | :rgb565 | :xrgb8888 | :mono_bayer4

A packed pixel layout; see the Packing section above.

pack_opts()

@type pack_opts() :: [
  scale_to: {non_neg_integer(), non_neg_integer()},
  clip: {non_neg_integer(), non_neg_integer()},
  rotate: angle(),
  origin: {non_neg_integer(), non_neg_integer()}
]

Options for pack_rgb8/5:

  • :scale_to — {width, height} the bitmap is resampled to, before rotation. Defaults to the source size
  • :clip — {width, height} of the top-left part of the resampled image that is kept. Defaults to :scale_to
  • :rotate — clockwise turn, 0, 90, 180 or 270. Defaults to 0
  • :origin — {x, y} panel position of the output's top-left pixel, used by the :mono_bayer4 dither. Defaults to {0, 0}

Functions

pack_rgb8(data, width, height, format, opts \\ [])

Resamples, clips, rotates and packs an RGB8 bitmap in one pass.

Returns the packed bytes with the output's width and height: the :clip size, swapped at 90 and 270. See the Packing section of the moduledoc for each step and the byte layout of every format, and pack_opts/0 for the options.

Raises ArgumentError on an unknown format or option, a malformed option value, data that does not hold exactly width * height * 3 bytes, or a :clip larger than :scale_to.

Examples

Red then blue, as RGB565 and as XRGB8888:

iex> ExRatatui.Pixels.pack_rgb8(<<255, 0, 0, 0, 0, 255>>, 2, 1, :rgb565)
{<<0, 248, 31, 0>>, 2, 1}

iex> ExRatatui.Pixels.pack_rgb8(<<255, 0, 0, 0, 0, 255>>, 2, 1, :xrgb8888)
{<<0, 0, 255, 255, 255, 0, 0, 255>>, 2, 1}

Turned a quarter clockwise, the row becomes a column, red on top:

iex> ExRatatui.Pixels.pack_rgb8(<<255, 0, 0, 0, 0, 255>>, 2, 1, :rgb8, rotate: 90)
{<<255, 0, 0, 0, 0, 255>>, 1, 2}

One pixel scaled up to a 2×2 rect repeats it:

iex> ExRatatui.Pixels.pack_rgb8(<<9, 8, 7>>, 1, 1, :rgb8, scale_to: {2, 2})
{<<9, 8, 7, 9, 8, 7, 9, 8, 7, 9, 8, 7>>, 2, 2}

rotate_rgb8(data, width, height, angle)

Rotates an RGB8 bitmap clockwise by angle degrees.

Returns the rotated bytes with the dimensions they now describe — at 90 and 270 the width and height are swapped. 0 hands back the input untouched, without going through the NIF.

Raises ArgumentError when data does not hold exactly width * height * 3 bytes, or when angle is not one of 0, 90, 180 or 270.

Examples

A two-pixel row, red then blue, turned a quarter clockwise is a two-pixel column, red on top. The bytes are unchanged; the shape is not:

iex> ExRatatui.Pixels.rotate_rgb8(<<255, 0, 0, 0, 0, 255>>, 2, 1, 90)
{<<255, 0, 0, 0, 0, 255>>, 1, 2}

Half a turn keeps the shape and reverses the pixels:

iex> ExRatatui.Pixels.rotate_rgb8(<<255, 0, 0, 0, 0, 255>>, 2, 1, 180)
{<<0, 0, 255, 255, 0, 0>>, 2, 1}

No turn is the identity:

iex> ExRatatui.Pixels.rotate_rgb8(<<1, 2, 3>>, 1, 1, 0)
{<<1, 2, 3>>, 1, 1}