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 isheight×width180—(width - 1 - px, height - 1 - py), destination keeps its size270—(width - 1 - py, px), destination isheight×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:
- 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)). - Clip to the top-left
:clippart 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. - Rotate clockwise by
:rotate, with the corner mapping above. - Pack each pixel:
| Format | Bytes per pixel | Layout |
|---|---|---|
:rgb8 | 3 | r, g, b (no packing) |
:rgb565 | 2 | rrrrrggg gggbbbbb, little-endian |
:xrgb8888 | 4 | b, g, r, 255 |
:mono_bayer4 | 1 | 0 (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
Functions
Resamples, clips, rotates and packs an RGB8 bitmap in one pass.
Rotates an RGB8 bitmap clockwise by angle degrees.
Types
@type angle() :: 0 | 90 | 180 | 270
@type format() :: :rgb8 | :rgb565 | :xrgb8888 | :mono_bayer4
A packed pixel layout; see the Packing section above.
@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,180or270. Defaults to0:origin—{x, y}panel position of the output's top-left pixel, used by the:mono_bayer4dither. Defaults to{0, 0}
Functions
@spec pack_rgb8(binary(), non_neg_integer(), non_neg_integer(), format(), pack_opts()) :: {binary(), non_neg_integer(), non_neg_integer()}
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}
@spec rotate_rgb8(binary(), non_neg_integer(), non_neg_integer(), angle()) :: {binary(), non_neg_integer(), non_neg_integer()}
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}