Image codecs, in pure MoonBit
Eighteen image formats, seventeen of them written.
PNG, JPEG, GIF, WebP, BMP, TGA, QOI, TIFF, ICO, PNM, Radiance HDR, OpenEXR, farbfeld, textures, PCX, SGI, Sun raster and AVIF, with the pixel arithmetic between them: layouts, transparency, rotation, cropping, scaling, blurring, colour. One package, no C bindings.
The plate, read in this browser
decoded and written by the library, not by a server
Written back
| Format | Bytes | Round trip |
|---|---|---|
| reading the plate… | ||
The row above the rule is the file on disk: a PNG written by a drawing script at 1200 × 800. Every row below it is this library decoding that file, writing the pixels in another format, and reading them back. Exact means the samples came back identical; JPEG quantises, and GIF has two hundred and fifty-six colours to spend.
What it does
one package, eighteen codecs, the arithmetic between them
Eighteen formats, one call
decode looks at the first bytes and picks the codec;
encode takes a picture and a format. The same picture
goes out as PNG, QOI, BMP, TGA, JPEG, GIF, lossless WebP, TIFF, an
icon, a PNM, farbfeld, Radiance HDR, OpenEXR, a texture, a PCX, an
SGI or a Sun raster, without the call site changing. OpenEXR takes any of its nine compressions,
the wavelet of PIZ, the plane-splitting of PXR24, the blocks of B44
and the frequencies of DWA included; a TIFF can be written as strips
or as tiles; a texture as plain samples or as blocks of BC1 to BC7;
a WebP as lossless bits or as a lossy frame. An AVIF is the one
that goes the other way: an AV1 bitstream is read whole, plane for
plane against libdav1d, and never written.
Pixels you can count
Nine layouts: grey or colour, eight or sixteen bits, with or without
alpha, and 32-bit floats. convert moves between them,
pixel gives back four numbers, pixels is
the bytes themselves.
Geometry
Mirror on either axis, rotate by quarter turns or by any angle, crop to a rectangle, and scale with nearest, bilinear, Catmull-Rom, Gaussian or Lanczos resampling. Every filter widens its footprint at a downscale, bilinear included. Blur by the Gaussian kernel or by the three box passes Pillow and the browsers use, sharpen, and turn the four colour knobs. Each returns a new picture; the original is untouched.
Animation
GIF, APNG and animated WebP carry frames with delays, disposal and
blending. All three arrive as one shape, and compose
applies the rules in order to give the frames a viewer shows.
Refuses rather than guesses
A file this library will not read raises Refused with
the offset, what it found and what it expected. Limits
puts a ceiling on the allocation before a hostile header can ask for
it.
Metadata, kept
A PNG keeps its gamma, chromaticities, rendering intent, ICC profile and text chunks, and a TIFF keeps its own. The orientation a camera wrote can be read on its own, or applied in one call.
Held against the reference
the official pictures in, every format it writes out
| Direction | Who else reads it | Largest disagreement |
|---|---|---|
| Reading the corpora | Pillow, the image crate, OpenEXR, tifffile |
one step, where a half or a float comes down to eight bits |
| Writing every format | Pillow, the image crate, OpenEXR |
none, except three steps for JPEG and one for Radiance HDR |
The test images of libtiff, the pictures of openexr-images and the samples of Netpbm are decoded here and by the others, and every file has to be held against at least one reader that is not this library. One file is refused on purpose and named — a LogLuv TIFF — as are the shapes a reference reader is known to misread. The formats that do not come back exact are the ones whose inverse transforms are done in different arithmetic.
Agreement is the floor, so it is counted as well as bounded. Every format the writer produces is written once and read back by Pillow: thirty of thirty-six files are read the same by both libraries, and twenty-three are lossless to the sample — the rest are the ones that are lossy by design, from 10.01 dB for a BC5 block to 53.32 dB for a DWA one. Separately, one line of the library is broken at a time in a way that still compiles, and the suite has to notice: eleven changes, eleven caught, run on every push.
A smaller file, in one function
the whole of the call site, for a picture that has to travel
fn small_copy(file : Bytes) -> Bytes raise @moonimage.Refused {
let image = @moonimage.decode(file) // the format is in the bytes
let small = image.scale(320, 200) // bilinear, the default
@moonimage.encode(small, Format::Webp) // lossless, no options
}
The guide starts from an empty project and ends with all of this, each step with the output it really printed.