The size of one pixel in bytes.
assert(bytes_per_pixel(.GRAY8) == 1);
assert(bytes_per_pixel(.RGBA8) == 4);↗#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.
#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);The size of one pixel in bytes.
assert(bytes_per_pixel(.GRAY8) == 1);
assert(bytes_per_pixel(.RGBA8) == 4);↗Whether the format has an alpha channel.
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);↗Frees the pixels and empties the image.
Which file format the bytes start like. It looks at the signature only, not the rest of the file.
png := encode_png(make_image(1, 1, .RGB8));
defer free(png.data);
assert(detect_image_format(png) == .PNG);
assert(detect_image_format(cast([] u8) "hello") == .UNKNOWN);↗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);↗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);↗The Rec. 709 brightness of an RGB colour, from 0 to 255.
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);↗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);↗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.
Mirrors the image top to bottom, in place.
image := make_image(1, 2, .GRAY8);
defer deinit(*image);
image.pixels[0] = 10;
image.pixels[1] = 20;
flip_vertical(*image);
assert(image.pixels[0] == 20);↗Mirrors the image left to right, in place.
image := make_image(2, 1, .GRAY8);
defer deinit(*image);
image.pixels[0] = 10;
image.pixels[1] = 20;
flip_horizontal(*image);
assert(image.pixels[0] == 20);↗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);↗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);↗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);↗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);↗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.
Walks the marker segments after SOI. Returns an empty string on success.
Sign-extends a size-bit magnitude read from the stream.
Decodes a code longer than 9 bits (or reports a bad code as symbol 0). bits is the
left-aligned bit buffer.
Reads n bits, 1 <= n <= 16. At least n bits must be buffered.
Tops the buffer up to at least 32 bits (one code plus its extra bits is at most 31).
Byte-at-a-time refill that unstuffs 0xFF00, stops at markers and pads with zero bits.
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).
One more bit of precision for coefficients that are already non-zero, and new +-1 coefficients.
Dequantizes and transforms the accumulated coefficients of a progressive image.
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.
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.
Four-component rows. Adobe files store inverted ink values, so the colour is the product of the stored value and the stored black.
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.
Writes the finished image: 1 byte per pixel for one component, else RGB.
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);↗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);↗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);↗Encodes an uncompressed BMP: 24-bit, or 32-bit with an alpha mask when the image has alpha. Gray is written as colour.
image := make_image(2, 2, .RGBA8);
defer deinit(*image);
bmp := encode_bmp(image);
defer free(bmp.data);
assert(detect_image_format(bmp) == .BMP);↗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);↗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");↗How the bytes of one pixel are laid out. Channels are 8 bits; decoders reduce deeper samples.
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.
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↗A file format detect_image_format can name. PNM covers PPM (P6), PGM (P5) and their text forms (P3, P2).
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.
Decoders refuse images with more pixels than this, before allocating them.