Modules

Image

moduleextensionSource

Available on macOS, Linux, Windows, Wasm·since 0.28.0

#import "Extensions/Image";

Decodes, encodes and transforms raster images (PNG, JPEG, QOI, BMP and PNM) in plain Jai, on every target including wasm.

decode_image reads any supported file; encode_png, encode_qoi, encode_bmp and encode_ppm write one. resize, crop, convert and the flips and rotation change an Image. Procedures that return an Image or a [] u8 allocate with context.allocator; free the image with deinit. Errors are static strings.

Example↗
#import "Extensions/Image";

image := make_image(2, 2, .RGBA8);
defer deinit(*image);
image.pixels[0] = 255;  // top left pixel: red
image.pixels[3] = 255;

png := encode_png(image);
defer free(png.data);

decoded, ok, error := decode_image(png);
defer deinit(*decoded);
assert(ok && decoded.width == 2 && decoded.pixels[0] == 255);

Procedures#

General#

make_imagesince 0.28.0Source
(width: s32, height: s32, format: Pixel_Format) -> Image

A zeroed image of the given size, or an empty one when the size or format is invalid. Free it with deinit.

image := make_image(16, 8, .RGBA8);
defer deinit(*image);
assert(image.width == 16 && image.pixels.count == 16 * 8 * 4);
assert(make_image(0, 8, .RGBA8).pixels.count == 0);
↗
decode_imagesince 0.28.0Source
(data: [] u8, max_pixels: s64 = DEFAULT_MAX_PIXELS) -> (image: Image, ok: bool, error: string)

Decodes a PNG, JPEG, QOI, BMP or PNM file into the nearest pixel format to the one it's stored in. Returns the image, whether it worked and, if not, why. Files with more than max_pixels pixels are refused before anything is allocated.

qoi := encode_qoi(make_image(4, 4, .RGB8));
defer free(qoi.data);

image, ok, error := decode_image(qoi);
defer deinit(*image);
assert(ok && image.format == .RGB8);

_, ok, error = decode_image(cast([] u8) "not an image");
assert(!ok);
↗
(data: [] u8, format: Pixel_Format, max_pixels: s64 = DEFAULT_MAX_PIXELS) -> (image: Image, ok: bool, error: string)

Like decode_image, then converts to format if the file holds something else.

qoi := encode_qoi(make_image(4, 4, .RGB8));
defer free(qoi.data);

image, ok := decode_image_as(qoi, .RGBA8);
defer deinit(*image);
assert(ok && image.format == .RGBA8 && image.pixels[3] == 255);
↗

Convert#

convert_lumasince 0.28.0Source
(r: s32, g: s32, b: s32) -> u8

The Rec. 709 brightness of an RGB colour, from 0 to 255.

convertsince 0.28.0Source
(image: Image, format: Pixel_Format) -> Image

The same pixels in another format. Gray is made from colour with Rec. 709 luma, colour from gray by repeating it, alpha is added as 255 and dropped without compositing. Always a new image (a copy when the format already matches); an invalid image gives an empty one.

rgb := make_image(1, 1, .RGB8);
defer deinit(*rgb);
rgb.pixels[0], rgb.pixels[1], rgb.pixels[2] = 255, 255, 255;

gray := convert(rgb, .GRAY8);
defer deinit(*gray);
assert(gray.pixels[0] == 255);
↗

Transform#

resizesince 0.28.0Source
(image: Image, width: s32, height: s32, filter := Resize_Filter.BILINEAR) -> Image

A new image of width by height. NEAREST is fastest, BILINEAR is the default and is smooth when enlarging, and BOX is the one to shrink with. Alpha is not premultiplied, so the colour of transparent pixels bleeds into their neighbours. An invalid image or size gives an empty image.

image := make_image(100, 100, .RGB8);
defer deinit(*image);

thumb := resize(image, 25, 25, .BOX);
defer deinit(*thumb);
assert(thumb.width == 25 && thumb.height == 25);
↗
cropsince 0.28.0Source
(image: Image, x: s32, y: s32, width: s32, height: s32) -> (Image, ok: bool)

The width by height rectangle with its top left corner at x, y. Not ok when any part of it lies outside the image, or it's empty.

image := make_image(10, 10, .RGBA8);
defer deinit(*image);

part, ok := crop(image, 2, 2, 4, 4);
defer deinit(*part);
assert(ok && part.width == 4);

_, ok = crop(image, 8, 8, 4, 4);
assert(!ok);
↗
rotate_90since 0.28.0Source
(image: Image, clockwise := true) -> Image

A new image turned a quarter turn, clockwise or counter-clockwise. Its width is the old height.

image := make_image(4, 2, .RGB8);
defer deinit(*image);

turned := rotate_90(image, true);
defer deinit(*turned);
assert(turned.width == 2 && turned.height == 4);
↗
(image: *Image)

Multiplies the colour channels by alpha, in place. Formats without alpha are left alone.

image := make_image(1, 1, .RGBA8);
defer deinit(*image);
image.pixels[0], image.pixels[3] = 200, 128;
premultiply_alpha(*image);
assert(image.pixels[0] == 100);
↗

PNG#

decode_pngsince 0.28.0Source
(data: [] u8, max_pixels: s64 = DEFAULT_MAX_PIXELS) -> (image: Image, ok: bool, error: string)

Decodes a PNG to GRAY8, GRAY_ALPHA8, RGB8 or RGBA8. Palettes are expanded, transparency is kept as alpha, and 16-bit samples keep their high byte. Interlaced files work; gamma and colour profiles are ignored; of an animated PNG only the first image is read.

png := encode_png(make_image(5, 3, .GRAY_ALPHA8));
defer free(png.data);

image, ok, error := decode_png(png);
defer deinit(*image);
assert(ok && image.format == .GRAY_ALPHA8 && image.height == 3);
↗
encode_pngsince 0.28.0Source
(image: Image, level: s32 = 6) -> [] u8

Encodes an 8-bit PNG: gray, gray with alpha, RGB or RGBA (BGRA is written as RGBA). level is the deflate level from 0 (stored) to 9 (smallest, slowest).

image := make_image(64, 64, .RGB8);
defer deinit(*image);

fast := encode_png(image, 1);
small := encode_png(image, 9);
defer free(fast.data);
defer free(small.data);
assert(small.count <= fast.count);
↗

JPEG#

decode_jpegsince 0.28.0Source
(data: [] u8, max_pixels: s64) -> (image: Image, ok: bool, error: string)

Decodes a baseline, extended sequential or progressive JPEG with 8-bit samples. Grayscale, YCbCr, RGB, CMYK and YCCK images work. Arithmetic coding and 12-bit samples are refused. Damaged data never reads out of bounds; a file cut off inside the pixel data still decodes, with the missing blocks grey.

#import "File";

file, found := read_entire_file("photo.jpg");
image, ok, error := decode_jpeg(cast([] u8) file, DEFAULT_MAX_PIXELS);
↗

This one is only checked to compile, not run.

JPEG huffman#

jpeg_extendsince 0.28.0Source
(value: s32, size: s32) -> s32

Sign-extends a size-bit magnitude read from the stream.

(h: *Jpeg_Huffman, bits: u64) -> (symbol: s32, length: s32)

Decodes a code longer than 9 bits (or reports a bad code as symbol 0). bits is the left-aligned bit buffer.

(n: s32) -> s32

Reads n bits, 1 <= n <= 16. At least n bits must be buffered.

jpeg_refillsince 0.28.0Source
()

Tops the buffer up to at least 32 bits (one code plus its extra bits is at most 31).

(d: *Jpeg_Decoder, expected: *s32, skipped: *s32) -> bool

Called between restart intervals: drops the buffered bits and finds the next RSTn marker. expected is the number (0..7) the next restart marker should carry; skipped returns how many whole intervals were lost when the marker found is further ahead. False when the entropy data has no further restart marker (the scan is over).

JPEG scan#

JPEG output#

(block: *s32, last: s32, out: *u8, stride: s64)

The 8x8 inverse DCT is a separable integer transform with 13-bit constants: a column pass that keeps two extra fractional bits, then a row pass that rounds, adds the level shift of 128 and clamps. Columns and rows that hold only a DC term take a shortcut. last is the highest zigzag position that may be non-zero; a block with only a DC term becomes a flat tile.

jpeg_idct_1dsince 0.28.0Source
(i0: s32, i1: s32, i2: s32, i3: s32, i4: s32, i5: s32, i6: s32, i7: s32, o: *[8] s32)

One 8-point transform scaled by 2^13, a rotation-based factorisation: an even half over inputs 0, 2, 4, 6 and an odd half over 1, 3, 5, 7.

(c: *u8, m: *u8, y: *u8, k: *u8, out: *u8, n: s64)

Four-component rows. Adobe files store inverted ink values, so the colour is the product of the stored value and the stored black.

(d: *Jpeg_Decoder, rows: *Jpeg_Rows, c: s32, y: s32) -> *u8

A full-resolution row of component c: the plane itself, or the upsampled neighbours. Chroma that is halved horizontally (and vertically) gets the triangle filter, other ratios repeat samples.

QOI#

decode_qoisince 0.28.0Source
(data: [] u8, max_pixels: s64 = DEFAULT_MAX_PIXELS) -> (image: Image, ok: bool, error: string)

Decodes a QOI file to RGB8 (3 channels) or RGBA8 (4 channels).

qoi := encode_qoi(make_image(8, 8, .RGBA8));
defer free(qoi.data);

image, ok := decode_qoi(qoi);
defer deinit(*image);
assert(ok && image.format == .RGBA8);
↗
encode_qoisince 0.28.0Source
(image: Image) -> [] u8

Encodes a QOI file, with 4 channels when the image has alpha and 3 otherwise. Gray is written as colour. QOI is far quicker to write and read than PNG, at a larger size.

qoi := encode_qoi(make_image(8, 8, .RGB8));
defer free(qoi.data);
assert(detect_image_format(qoi) == .QOI);
↗

BMP#

decode_bmpsince 0.28.0Source
(data: [] u8, max_pixels: s64 = DEFAULT_MAX_PIXELS) -> (image: Image, ok: bool, error: string)

Decodes a BMP file of 1, 4, 8 (palette, optionally RLE), 16, 24 or 32 bits per pixel. Palette and 24-bit images become RGB8; 16 and 32-bit images become RGBA8 when they have alpha, RGB8 otherwise.

bmp := encode_bmp(make_image(3, 3, .RGB8));
defer free(bmp.data);

image, ok := decode_bmp(bmp);
defer deinit(*image);
assert(ok && image.width == 3);
↗

PNM#

decode_pnmsince 0.28.0Source
(data: [] u8, max_pixels: s64 = DEFAULT_MAX_PIXELS) -> (image: Image, ok: bool, error: string)

Decodes a PGM (P2, P5) to GRAY8 or a PPM (P3, P6) to RGB8. Samples are scaled to 8 bits.

image, ok := decode_pnm(cast([] u8) "P2\n2 1\n255\n0 255\n");
defer deinit(*image);
assert(ok && image.format == .GRAY8 && image.pixels[1] == 255);
↗
encode_ppmsince 0.28.0Source
(image: Image) -> [] u8

Encodes a binary PPM (P6), or a PGM (P5) for gray images. Alpha is dropped.

image := make_image(2, 2, .RGB8);
defer deinit(*image);
ppm := encode_ppm(image);
defer free(ppm.data);
assert(ppm[0] == #char "P" && ppm[1] == #char "6");
↗

Types#

Pixel_Formatenumsince 0.28.0Source
:: enum

How the bytes of one pixel are laid out. Channels are 8 bits; decoders reduce deeper samples.

GRAY8
GRAY_ALPHA8
RGB8
RGBA8
BGRA8
Imagestructsince 0.28.0Source
:: struct

A decoded image. Rows are packed top to bottom with no padding: pixel (x, y) starts at (y * width + x) * bytes_per_pixel(format) in pixels.

width: s32
height: s32
format: Pixel_Format
pixels: [] u8
image := make_image(3, 2, .RGB8);
defer deinit(*image);
assert(image.pixels.count == 3 * 2 * 3);

x, y := 2, 1;
offset := (y * image.width + x) * bytes_per_pixel(image.format);
image.pixels[offset] = 200;  // red channel of the bottom right pixel
↗
Image_File_Formatenumsince 0.28.0Source
:: enum

A file format detect_image_format can name. PNM covers PPM (P6), PGM (P5) and their text forms (P3, P2).

UNKNOWN
PNG
JPEG
QOI
BMP
PNM
PPM (P6), PGM (P5) and their text forms (P3, P2)
Resize_Filterenumsince 0.28.0Source
:: enum

How resize samples the source: NEAREST takes the closest pixel, BILINEAR blends the four around a point, BOX averages the whole area under each output pixel.

NEAREST
BILINEAR
BOX
Jpeg_Componentstructsince 0.28.0Source
:: struct
id: s32
h: s32
v: s32
quant_id: s32
width: s32
Size in samples, and in blocks for a scan that holds this component alone.
height: s32
blocks_w: s32
blocks_h: s32
alloc_w: s32
Blocks allocated per row and per column, rounded up to whole MCUs.
alloc_h: s32
stride: s64
plane: *u8
coefs: *s16
pred: s32
quant_taken: bool
quant: [64] s32
dequantizers in zigzag order
quant_natural: [64] s32
Jpeg_Decoderstructsince 0.28.0Source
:: struct
data: *u8
end: s64
pos: s64
bits: u64
Entropy-coded segment reader.
nbits: s64
pad: s64
marker: u8
eobrun: s32
width: s32
height: s32
ncomp: s32
hmax: s32
vmax: s32
mcus_x: s32
mcus_y: s32
progressive: bool
frame_seen: bool
scans: s32
restart_interval: s32
adobe_transform: s32
adobe_seen: bool
jfif_seen: bool
comps: [4] Jpeg_Component
quant_defined: [4] bool
quant_tables: [4][64] s32
dc_tables: [4] Jpeg_Huffman
ac_tables: [4] Jpeg_Huffman
block: [64] s32
Jpeg_Huffmanstructsince 0.28.0Source
:: struct
defined: bool
fast: [512] u16
(length << 8) | symbol for every 9-bit prefix of a code of at most 9 bits, else 0.
ac_fast: [512] u32
For AC tables, per 9-bit prefix: the whole run/size code plus its extra bits when they fit in 9 bits. Low byte: bits used. Next byte: run, or 255 for end of block. High half: the coefficient. 0 when the prefix needs the general path.
limit: [17] s32
one past the last code of each length, left-aligned to 16 bits
first_code: [17] s32
first_index: [17] s32
symbols: [256] u8
Jpeg_Colorstructsince 0.28.0Source
:: struct
cr_r: [256] s32
cb_b: [256] s32
cr_g: [256] s32
cb_g: [256] s32
range: [768] u8
clamps -256..511 to 0..255 (index + 256)

Constants#

:: 1 << 26

Decoders refuse images with more pixels than this, before allocating them.

JPEG_ZIGZAGsince 0.28.0Source
:: u8.[ 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 2 ...