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.

Read the guide The workbench

A lens test plate: a Siemens star, a colour strip, a sixteen-step grey wedge and four rulings, printed on warm paper.
Test plate 071200 × 800

The plate, read in this browser

decoded and written by the library, not by a server

format — size — layout — bytes —

Written back

FormatBytesRound 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.

Drop any image on this panel — PNG, JPEG, GIF, WebP, BMP, TGA, QOI, TIFF, an icon, a PNM, farbfeld, Radiance HDR, OpenEXR, a texture, a PCX, an SGI, a Sun raster or an AVIF — and it is read the same way.

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.

13formats, read; twelve written
9pixel layouts
5resampling filters

Held against the reference

the official pictures in, every format it writes out

21official pictures at pinned commits
9OpenEXR compressions
36formats counted against Pillow
11mutations, every one caught
DirectionWho else reads itLargest 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.