Image codecs for MoonBit: PNG, JPEG, GIF, WebP, BMP, TGA, QOI, TIFF, ICO, PNM, Radiance HDR, OpenEXR, farbfeld, DDS, PCX, SGI and Sun raster read and written, and AVIF read, with the pixel arithmetic, filters and colour between them.
test {
let image = @moonimage.Image::new(640, 480, @moonimage.PixelFormat::Rgba8)
let small = image.scale(160, 120, sampling=@moonimage.Sampling::Bilinear)
let file = @moonimage.encode(small, @moonimage.Format::Png)
let back = @moonimage.decode(file)
assert_eq(back.width, 160)
assert_eq(back.height, 120)
}test {
let image = @moonimage.Image::new(8, 8, @moonimage.PixelFormat::Rgb8)
let file = @moonimage.encode(image, @moonimage.Format::Tiff)
let safe = @moonimage.decode(file, limits=@moonimage.Limits::strict())
assert_eq(safe.width, 8)
}test {
let refused = try {
ignore(@moonimage.decode(b"not an image"))
abort("unreachable")
} catch {
error => error
}
assert_eq(refused is @moonimage.Refused::Unknown(_), true)
}| Format | Read | Write | Animation | Package |
|---|---|---|---|---|
| PNG | every colour type and depth, palette and tRNS, Adam7, five filters, CRC, gAMA cHRM sRGB iCCP tEXt zTXt iTXt | 8 and 16 bits, palette when it is smaller, adaptive filtering, interlaced on request, metadata | APNG | @moonimage/png |
| QOI | all six opcodes | the reference encoder's choices, byte for byte | — | @moonimage/qoi |
| BMP | every header version, 1/4/8/16/24/32 bits, BI_RGB BI_RLE4 BI_RLE8 BI_BITFIELDS, both row orders | 24 and 32 bits | — | @moonimage/bmp |
| TGA | colour mapped, true colour and grey, run lengths, 8/15/16/24/32 bits, both row orders | 24 and 32 bits, run lengths on request | — | @moonimage/tga |
| JPEG | baseline and progressive, every subsampling, greyscale, EXIF orientation | baseline, quality 1–100, three subsamplings | — | @moonimage/jpeg |
| GIF | palettes, transparency, interlace, run lengths | palette by exact colours or median cut, dithering | yes | @moonimage/gif |
| WebP | lossless (VP8L) with its four transforms, and lossy (VP8) with its boolean coder, predictors, loop filter and chroma upsampling; alpha in either; animated files | lossless by encode, lossy by encode_lossy, which takes a quality | yes | @moonimage/webp |
| TIFF | strips or tiles, LZW, PackBits, deflate or none, horizontal prediction, grey, grey with alpha, RGB, RGBA and palette at 1 to 16 bits, planar or chunky, every page | 8 and 16 bits, three compressions, prediction on request, several pages, tiles on request | pages | @moonimage/tiff |
| ICO / CUR | PNG entries and bitmap entries at 1/4/8/24/32 bits with the one-bit mask, every size in the file | PNG entries, at any list of sizes | sizes | @moonimage/ico |
| PNM | PBM, PGM, PPM and PAM, both spellings, any maximum | all four shapes, both spellings | — | @moonimage/pnm |
| Radiance HDR | RGBE, the new run-length code and the flat one, all four origins | RGBE with the run-length code | — | @moonimage/hdr |
| OpenEXR | scanlines or tiles, one part or several, in half or float, uncompressed, RLE, ZIPS, ZIP, PIZ, PXR24, B44, B44A, DWAA or DWAB | scanlines, all nine compressions, one part or several | — | @moonimage/exr |
| farbfeld | the whole format | the whole format | — | @moonimage/farbfeld |
| DDS | BC1 to BC7 blocks, and uncompressed surfaces | BC1 to BC7 blocks, and uncompressed surfaces | — | @moonimage/dds |
| PCX | 1, 2, 4 and 8 bits a pixel, one to four planes, the header palette and the one at the end, run lengths | 8 bits a plane, three planes or four, run lengths | — | @moonimage/pcx |
| SGI | one, three or four channels, eight or sixteen bits a sample, stored plain | eight bits a sample, three or four channels | — | @moonimage/sgi |
| Sun raster | 1, 8, 24 and 32 bits a pixel, a colour map of three planes, rows padded to a word, byte-encoded runs | 24 bits, or 32 with this library's alpha in the fourth byte | — | @moonimage/ras |
| AVIF | still pictures of one tile: the AV1 bitstream, its arithmetic coder, the partition tree, thirteen intra predictors, chroma from luma, the transforms and their quantisation matrices, deblocking and CDEF | — | — | @moonimage/avif |
pub(all) struct Image {
width : Int32
height : Int32
format : PixelFormat
stride : Int32
pixels : Bytes
premultiplied : Bool
metadata : Metadata
}| Pixel format | Samples | Bytes a pixel |
|---|---|---|
| Gray8, GrayAlpha8, Rgb8, Rgba8 | 8-bit | 1, 2, 3, 4 |
| Gray16, GrayAlpha16, Rgb16, Rgba16 | 16-bit, big-endian | 2, 4, 6, 8 |
| RgbaF32 | floats, little-endian | 16 |
test {
let image = @moonimage.Image::new(4, 2, @moonimage.PixelFormat::Rgba8)
let grey = image.convert(@moonimage.PixelFormat::Gray8)
let wide = image.convert(@moonimage.PixelFormat::Rgba16)
assert_eq(grey.format, @moonimage.PixelFormat::Gray8)
assert_eq(wide.format, @moonimage.PixelFormat::Rgba16)
let flat = image.to_premultiplied()
assert_eq(flat.premultiplied, true)
assert_eq(flat.to_straight().premultiplied, false)
}test {
let image = @moonimage.Image::new(3, 2, @moonimage.PixelFormat::Rgba8)
assert_eq(image.flip_horizontal().width, 3)
assert_eq(image.flip_vertical().height, 2)
assert_eq(image.rotate(quarters=1).width, 2)
assert_eq(image.crop(0, 0, 2, 1).height, 1)
}test {
let image = @moonimage.Image::new(9, 7, @moonimage.PixelFormat::Rgb8)
for
sampling in [
@moonimage.Sampling::Nearest,
@moonimage.Sampling::Bilinear,
@moonimage.Sampling::CatmullRom,
@moonimage.Sampling::Gaussian,
@moonimage.Sampling::Lanczos,
] {
assert_eq(image.scale(16, 12, sampling~).width, 16)
}
}test {
let image = @moonimage.Image::new(8, 4, @moonimage.PixelFormat::Rgba8)
let tilted = image.rotate_by(30.0, expand=true)
assert_eq(tilted.width > 8, true)
assert_eq(image.rotate_by(90.0).width, 4)
}test {
let image = @moonimage.Image::new(6, 5, @moonimage.PixelFormat::Rgba8)
let soft = image.blur(1.5)
assert_eq(soft.width, 6)
let crisp = soft.unsharp_mask(radius=1.0, amount=150.0, threshold=3)
assert_eq(crisp.height, 5)
}test {
let image = @moonimage.Image::new(6, 5, @moonimage.PixelFormat::Rgba8)
let same_as_pillow = image.blur(1.5, kernel=@moonimage.Kernel::Boxes)
assert_eq(same_as_pillow.width, 6)
}test {
let image = @moonimage.Image::new(3, 3, @moonimage.PixelFormat::Rgba8)
assert_eq(image.brightness(1.5).contrast(1.2).hue_rotate(90.0).invert().width, 3)
}test {
let under = @moonimage.Image::new(4, 4, @moonimage.PixelFormat::Rgba8)
let over = @moonimage.Image::new(2, 2, @moonimage.PixelFormat::Rgba8)
let drawn = under.composite(over, x=1, y=1, opacity=0.5)
assert_eq(drawn.width, 4)
}test {
let image = @moonimage.Image::new(4, 4, @moonimage.PixelFormat::Rgba8)
let file = @moonimage.encode(image, @moonimage.Format::Gif)
let animation = @moonimage.decode_animation(file)
assert_eq(animation.frames.length(), 1)
assert_eq(animation.compose().length(), 1)
}pub(all) struct Metadata {
gamma : Double?
chromaticities : Chromaticities?
intent : Intent?
icc : Icc?
texts : Array<Text>
}test {
let image = @moonimage.Image::new(3, 2, @moonimage.PixelFormat::Rgb8)
// Six is a quarter turn clockwise, which swaps the canvas.
assert_eq(image.orient(6).width, 2)
assert_eq(image.upright(None).width, 3)
}test {
assert_eq(@moonimage.Limits::default().width, 1_048_576)
assert_eq(@moonimage.Limits::strict().pixels, 67_108_864)
}| File | Refused because |
|---|---|
| quad-tile.jpg.tiff | its tiles are JPEG-compressed |
| logluv-3c-16b.tiff | the samples are LogLuv encoded |
| Format | Largest disagreement |
|---|---|
| PNG, BMP, TGA, GIF, TIFF, tiled TIFF, ICO, PNM, QOI, farbfeld | none |
| WebP, lossless | none |
| OpenEXR, uncompressed, RLE, ZIP, PIZ, PXR24 | none |
| OpenEXR, B44 | one step, on one sample of three thousand |
| DDS, uncompressed | none |
| DDS, BC1 to BC7 | none, between two readers of the same blocks |
| WebP, lossy | none, between libwebp and this reader |
| OpenEXR, several parts | none |
| Radiance HDR | one step |
| JPEG | three steps |
| Format | Same file: largest | vs fixture: largest | vs fixture: PSNR |
|---|---|---|---|
| PNG, QOI, BMP, TGA, WebP lossless, TIFF, tiled TIFF, DDS plain, ICO, PNM, EXR uncompressed, RLE, ZIPS, ZIP, PIZ, PXR24, several parts | 0 | 0 | exact |
| WebP lossy, quality 90 | 0 | 145 | 22.72 dB |
| WebP lossy, quality 20 | 0 | 201 | 20.53 dB |
| JPEG | 2 | 147 | 19.81 dB |
| GIF | 0 | 63 | 22.64 dB |
| DDS BC1 | 0 | 167 | 19.58 dB |
| DDS BC3 | 0 | 167 | 22.32 dB |
| DDS BC5 | 0 | 255 | 10.01 dB |
| DDS BC6H | 1 | 237 | 21.75 dB |
| DDS BC7 | 0 | 213 | 25.42 dB |
| OpenEXR B44, B44A | 1 | 32 | 39.21 dB |
| OpenEXR DWAA, DWAB | 1 | 4 | 53.32 dB |
python scripts/mutation/run.py| Change | Caught by |
|---|---|
| The Paeth predictor takes the pixel above when the gradient is a tie | png_test.mbt, the pngsuite decode |
| QOI's running index is thirty-two slots instead of sixty-four | qoi_test.mbt, a fixture decoded against the reference |
| GIF's interlaced flag is read from the bit beside it | gif_test.mbt, the still samples |
| A BC1 two-thirds weight is divided by four | encode_test.mbt, a block that follows a gradient |
| A B44 shift rounds at half a step instead of at the halfway point | b44_wbtest.mbt, the ties-to-even table |
| A JPEG block's coefficients are read in raster order | jpeg_test.mbt, every sample |
| The Lanczos window is two lobes wide instead of three | filter_check_test.mbt, scaling against Pillow |
| A Radiance pixel is read one power of two too bright | hdr_test.mbt, the floats the generator computed |
| A PCX run is read as seven bits of count where the format gives six | pcx_test.mbt, every sample |
| An SGI is read top down where the format stores it bottom first | sgi_test.mbt, every sample |
| A Sun raster row loses the padding keeping it on a word | ras_test.mbt, every sample |
| Call | What it does |
|---|---|
| decode(bytes, format?, limits?) | read an image, whichever format the bytes are, in the layout asked for or the one the file holds |
| decode_animation(bytes, limits?) | read every frame; a still image is one frame |
| encode(image, format) | write an image, with that format's defaults |
| encode_animation(animation, format) | write the frames, or the first one where the format has no animation |
| Format::of(bytes), Format::extension, Format::mime, Format::is_animated | what the bytes are, and how they are named |
| Image::new, Image::of | an image, blank or over bytes that already exist |
| Image::row, Image::pixel | one row, one pixel |
| Image::convert | the same image in another sample format |
| Image::to_premultiplied, Image::to_straight | colour × alpha, and back |
| Image::flip_horizontal, Image::flip_vertical, Image::rotate, Image::rotate_by, Image::crop | moving pixels without reading them |
| Image::scale | a new size, by any of five filters |
| Image::blur, Image::unsharp_mask, Kernel | a Gaussian by the kernel or by three box passes, and the difference that undoes it |
| Image::brightness, Image::contrast, Image::hue_rotate, Image::invert | the four colour knobs |
| Image::composite | another picture laid over this one |
| Image::orient, Image::upright | the way up a camera said the picture was |
| Pixel::to_color, Pixel::of_color, Pixel::rotate_hue | the seam to the colour package |
| Animation::new, Animation::compose | frames, and every frame as a full canvas |
| png.encode, png.decode, png.EncodeOptions, png.Filter | PNG, with its compression level and row filter |
| qoi.encode, qoi.decode | QOI |
| bmp.encode, bmp.decode, bmp.EncodeOptions, bmp.Depth | BMP, 24 or 32 bits |
| tga.encode, tga.decode, tga.EncodeOptions, tga.Depth | TGA, with run lengths |
| jpeg.encode, jpeg.decode, jpeg.EncodeOptions, jpeg.Subsampling, jpeg.exif_orientation | JPEG, and the orientation a camera wrote |
| gif.encode, gif.encode_animation, gif.decode, gif.decode_animation, gif.EncodeOptions | GIF, with dithering and transparency |
| webp.decode, webp.decode_animation, webp.encode, webp.encode_animation | WebP: lossless and lossy read, lossless written |
| tiff.decode, tiff.decode_pages, tiff.encode, tiff.encode_pages, tiff.EncodeOptions, tiff.Compression, tiff.orientation | TIFF, its compressions and its pages |
| ico.decode, ico.decode_all, ico.encode, ico.EncodeOptions, ico.Entry, ico.Kind | icons and cursors, at every size in the file |
| pnm.decode, pnm.encode, pnm.EncodeOptions, pnm.Kind | the portable anymap, four shapes and two spellings |
| hdr.decode, hdr.encode | Radiance HDR, samples above one and all |
| exr.decode, exr.encode, exr.EncodeOptions, exr.Compression, exr.half_to_float, exr.float_to_half | OpenEXR, and the half-precision conversion it turns on |
| farbfeld.decode, farbfeld.encode | farbfeld |
| dds.decode, dds.Block | block-compressed textures, read |
| avif.decode, avif.decode_planes, avif.decode_frame | AVIF, as RGBA or as the three planes the bitstream carries |
fn decode_animation(bytes : BytesView, format? : PixelFormat, limits? : Limits) -> Animation raise RefusedInstall
Download zipImage codecs for MoonBit: PNG, JPEG, GIF, WebP, BMP, TGA, QOI, TIFF, ICO, PNM, Radiance HDR, OpenEXR, farbfeld, DDS, PCX, SGI and Sun raster read and written, and AVIF read, with the pixel arithmetic, filters and colour between them.