moonimage

    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.

    image
    images
    image-codec
    codec
    codecs
    png
    apng
    jpeg
    jpg
    progressive-jpeg
    exif
    icc
    gif
    webp
    qoi
    bmp
    tga
    tiff
    lzw
    packbits
    bigtiff
    ico
    cur
    icon
    cursor
    pnm
    pbm
    pgm
    ppm
    pam
    netpbm
    hdr
    radiance
    rgbe
    exr
    openexr
    b44
    b44a
    dwa
    dwab
    dct
    half-float
    float-image
    farbfeld
    dds
    pcx
    sgi
    sun-raster
    sunos
    avif
    av1
    bc1
    bc3
    bc5
    bc6h
    bc7
    bptc
    dxt
    decode
    encode
    transcode
    convert
    raster
    pixel
    pixels
    rgba
    resize
    scale
    resample
    lanczos
    bilinear
    catmull-rom
    gaussian
    blur
    sharpen
    unsharp-mask
    crop
    rotate
    flip
    premultiplied
    alpha
    animation
    metadata
    thumbnail
    texture
    moonbit
    Download zip
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    23 hours ago
    Downloads
    85

    #moonimage

    Image codecs for MoonBit: eighteen formats read, seventeen written, and the pixel arithmetic that sits between them.

    No I/O, all four backends, and two dependencies: the deflate stream a PNG needs and the colour package a hue rotation needs. Nothing here imports moonbitlang/x; the only place in this repository that does is the conformance harness, which lives in a module of its own under integration/ because a file system belongs beside the check rather than in the library.

    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)
    }

    Two runs to start from: moon run src/examples/01-tour walks one picture through every format and every operation, and moon run src/examples/02-guide is the program behind the guide.

    #Reading and writing

    decode reads the signature and hands the bytes to the package that knows that format. It takes the limits a reader works under, which are checked before anything is allocated:

    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)
    }

    A file that is damaged, truncated, or larger than the limits raises Refused, which says where it stopped and what it wanted:

    test {
    let refused = try {
    ignore(@moonimage.decode(b"not an image"))
    abort("unreachable")
    } catch {
    error => error
    }
    assert_eq(refused is @moonimage.Refused::Unknown(_), true)
    }

    Unexpected and Checksum mean damaged bytes, Truncated means the file ends early, OverLimit means the caller's limits refused it, Unknown means it is not a format this library reads, and Unsupported means a part of a format it does not implement.

    encode writes the format the caller names, with that format's defaults. It raises Refused when a format cannot hold what is asked of it — a negative sample in an unsigned BC6H block, say. A caller that wants to choose how it is written goes to the format's own package, which takes its options record: a compression for OpenEXR or TIFF, a tile size for TIFF, a block format for a texture, a quality for a lossy WebP.

    FormatReadWriteAnimationPackage
    PNGevery colour type and depth, palette and tRNS, Adam7, five filters, CRC, gAMA cHRM sRGB iCCP tEXt zTXt iTXt8 and 16 bits, palette when it is smaller, adaptive filtering, interlaced on request, metadataAPNG@moonimage/png
    QOIall six opcodesthe reference encoder's choices, byte for byte—@moonimage/qoi
    BMPevery header version, 1/4/8/16/24/32 bits, BI_RGB BI_RLE4 BI_RLE8 BI_BITFIELDS, both row orders24 and 32 bits—@moonimage/bmp
    TGAcolour mapped, true colour and grey, run lengths, 8/15/16/24/32 bits, both row orders24 and 32 bits, run lengths on request—@moonimage/tga
    JPEGbaseline and progressive, every subsampling, greyscale, EXIF orientationbaseline, quality 1–100, three subsamplings—@moonimage/jpeg
    GIFpalettes, transparency, interlace, run lengthspalette by exact colours or median cut, ditheringyes@moonimage/gif
    WebPlossless (VP8L) with its four transforms, and lossy (VP8) with its boolean coder, predictors, loop filter and chroma upsampling; alpha in either; animated fileslossless by encode, lossy by encode_lossy, which takes a qualityyes@moonimage/webp
    TIFFstrips 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 page8 and 16 bits, three compressions, prediction on request, several pages, tiles on requestpages@moonimage/tiff
    ICO / CURPNG entries and bitmap entries at 1/4/8/24/32 bits with the one-bit mask, every size in the filePNG entries, at any list of sizessizes@moonimage/ico
    PNMPBM, PGM, PPM and PAM, both spellings, any maximumall four shapes, both spellings—@moonimage/pnm
    Radiance HDRRGBE, the new run-length code and the flat one, all four originsRGBE with the run-length code—@moonimage/hdr
    OpenEXRscanlines or tiles, one part or several, in half or float, uncompressed, RLE, ZIPS, ZIP, PIZ, PXR24, B44, B44A, DWAA or DWABscanlines, all nine compressions, one part or several—@moonimage/exr
    farbfeldthe whole formatthe whole format—@moonimage/farbfeld
    DDSBC1 to BC7 blocks, and uncompressed surfacesBC1 to BC7 blocks, and uncompressed surfaces—@moonimage/dds
    PCX1, 2, 4 and 8 bits a pixel, one to four planes, the header palette and the one at the end, run lengths8 bits a plane, three planes or four, run lengths—@moonimage/pcx
    SGIone, three or four channels, eight or sixteen bits a sample, stored plaineight bits a sample, three or four channels—@moonimage/sgi
    Sun raster1, 8, 24 and 32 bits a pixel, a colour map of three planes, rows padded to a word, byte-encoded runs24 bits, or 32 with this library's alpha in the fourth byte—@moonimage/ras
    AVIFstill 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

    Both halves of WebP are read; the lossless one, VP8L, is also written. VP8 is the intra frame of a video codec, so its picture is rebuilt rather than decoded: a boolean arithmetic coder over prediction modes and transform coefficients, ten predictors for four-by-four blocks and four for sixteen-by-sixteen ones, a residual carried as quantised coefficients, a loop filter along every macroblock and transform edge, and chroma interpolated between the two rows it sits between. What comes out is the reconstruction the format defines, sample for sample — a lossy decoder that is "nearly right" is one with a bug in it.

    An AVIF is AV1 in a HEIF container, so reading one means reading a still AV1 frame: a multi-symbol arithmetic coder over its own CDF tables, ten ways of splitting a superblock, thirteen intra predictors with an edge filter and an angle delta between them, chroma from luma with an alpha for each plane, the transform types and their quantisation matrices, coefficients coded as tokens with their own classes, deblocking, CDEF, and the colour matrix the container names. AV1 is normative, so the picture is not "close enough": seven streams written by libavif are decoded by libdav1d and every plane is compared with this decoder's, sample for sample.

    A lossy picture has no alpha in VP8 itself, so a transparent one carries a separate ALPH chunk beside the picture. That is read too, in a still picture and inside an animation frame, and comes back as four samples per pixel.

    A TIFF is a directory of tags pointing at data anywhere in the file, and a page of it holds either strips — rows grouped for compression — or tiles, which are the same rows cut into rectangles. Both are read, and either can be written. An EXR is read the same way: a part of it holds scanlines or tiles, and a file may hold several parts, each with its own channels and its own compression.

    A DDS is a block grid: four by four pixels become eight or sixteen bytes, and what those bytes hold is a format of its own — two endpoints and an index a pixel in BC1 to BC5, a mode number and a table of shapes in BC7, and half-precision light in BC6H. decode gives the picture in the layout the block is, and decode takes a format for the one a caller is drawing with, because a block layout is rarely a display layout.

    #The image

    An image is a rectangle of samples, the format that says how to read them, and the metadata that travelled with them.

    pub(all) struct Image {
    width : Int32
    height : Int32
    format : PixelFormat
    stride : Int32
    pixels : Bytes
    premultiplied : Bool
    metadata : Metadata
    }

    The row stride is separate from the width because a caller uploading to a GPU needs rows padded to a multiple of 256 bytes. Pixels are immutable: every operation returns a new image rather than writing into the old one.

    Pixel formatSamplesBytes a pixel
    Gray8, GrayAlpha8, Rgb8, Rgba88-bit1, 2, 3, 4
    Gray16, GrayAlpha16, Rgb16, Rgba1616-bit, big-endian2, 4, 6, 8
    RgbaF32floats, little-endian16

    #Moving pixels

    Conversions widen, narrow, add or drop alpha, and turn grey into three equal channels. Narrowing rounds rather than truncates, and colour becoming grey is a luminance rather than one of the channels:

    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)
    }

    The operations that are a permutation keep the format and every sample exactly:

    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)
    }

    Scaling has five filters. Nearest is what pixel art wants, bilinear what a photograph wants when it is scaled a little, Catmull-Rom and Lanczos what it wants when the edge matters, and the Gaussian the one to reach for when ringing is the problem:

    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)
    }
    }

    Alpha is resampled premultiplied even when the image is not stored that way, so a transparent pixel's colour does not bleed into its neighbours. Every filter widens its footprint when the image is made smaller, bilinear included: what it reaches for at a downscale is everything the output pixel stands for, which is the whole of antialiasing.

    A rotation that is not a right angle resamples, and can grow the canvas to hold what comes out:

    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)
    }

    #Filters and colour

    Blurring is a separable Gaussian of the radius given, on premultiplied samples; sharpening is the difference between a picture and its own blur:

    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)
    }

    kernel chooses how the Gaussian is worked out. The default convolves with the kernel, which is what the number means. Boxes does what Pillow, Skia and the browsers do — three box passes whose widths are chosen so that their variance is the variance asked for — which is cheaper and within a step of the kernel everywhere, and is the one to reach for when a picture has to match one of those:

    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)
    }

    Brightness multiplies, contrast turns about mid-grey, the hue rotation keeps saturation and lightness, and inverting leaves alpha alone:

    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)
    }

    A hue rotation is a conversion, so it goes through the colour package that owns the spaces: a pixel becomes a moonrgba colour and comes back.

    Compositing is source over, with the source converted to the destination's format and its alpha scaled by the opacity asked for:

    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)
    }

    #Animation

    Three formats carry frames, and they agree on the shape even where they disagree on the arithmetic: a canvas, a rectangle per frame, a delay, a rule for what to do with the pixels underneath, and a rule for what to leave behind.

    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)
    }

    compose applies the disposal and blending rules in order, which is what a viewer shows. A file that does not animate comes back as one frame over the whole canvas, and a TIFF comes back as a frame a page.

    #Metadata

    What a file says about itself travels with the image, and goes back out when it is written. Nothing here is interpreted: colour management is not this library's business.

    pub(all) struct Metadata {
    gamma : Double?
    chromaticities : Chromaticities?
    intent : Intent?
    icc : Icc?
    texts : Array<Text>
    }

    Text keeps the flavour of the chunk it came from — Latin for tEXt, Compressed for zTXt, International for iTXt — so that writing the image back produces the same kind of chunk. An ICC profile read from a PNG or a TIFF is carried and written back byte for byte.

    The orientation a camera wrote is read on its own, and applied when the caller asks for it:

    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)
    }

    #Limits

    Every reader takes a Limits record: the widest and tallest image, the most pixels, and the most bytes an expansion may produce.

    test {
    assert_eq(@moonimage.Limits::default().width, 1_048_576)
    assert_eq(@moonimage.Limits::strict().pixels, 67_108_864)
    }

    The defaults are what a desktop application can live with. A caller reading untrusted files should pass strict or a tighter record of its own.

    #Consistency

    A format is implemented when two other implementations agree with this one about the same file. Both directions are measured on real pictures.

    Reading. The test images of libtiff, the pictures of openexr-images and the samples of Netpbm — twenty-one files at pinned commits — are decoded here, by Pillow, by the image crate, by tifffile for pages whose samples are in separate planes, and by OpenEXR's own library for that format. Nineteen of them agree to within a step of an eight-bit sample, or exactly where the samples are eight bits. The other two are refused on purpose, and named:

    FileRefused because
    quad-tile.jpg.tiffits tiles are JPEG-compressed
    logluv-3c-16b.tiffthe samples are LogLuv encoded

    Where a reference reader cannot be used as one, the harness says so rather than counting a silent pass: Pillow drops the alpha plane of a separated-plane TIFF, which tifffile confirms exactly, and the image crate refuses those pages and the single-channel OpenEXR ramps. Every file has to be held against at least one reader that is not this library.

    An AVIF is held to libdav1d rather than to a picture of its own: the seven streams of scripts/fixtures/avif_samples.py — flat, gradient, pattern and noise, from sixteen by sixteen to sixty-four by forty-eight — are decoded by libavif and libdav1d, and the whole of each plane is compared with this decoder's. Eleven thousand five hundred and twenty samples, all equal.

    Writing. Every format this library produces is written once and read back by Pillow and by image — by OpenEXR too for the nine compressions of that format, by tifffile for the tiles of a page, and by libwebp through Pillow for a lossy WebP — and they have to agree about what the file says. A format that is lossy is not required to give back what went in, only to mean the same thing to everyone who reads it.

    FormatLargest disagreement
    PNG, BMP, TGA, GIF, TIFF, tiled TIFF, ICO, PNM, QOI, farbfeldnone
    WebP, losslessnone
    OpenEXR, uncompressed, RLE, ZIP, PIZ, PXR24none
    OpenEXR, B44one step, on one sample of three thousand
    DDS, uncompressednone
    DDS, BC1 to BC7none, between two readers of the same blocks
    WebP, lossynone, between libwebp and this reader
    OpenEXR, several partsnone
    Radiance HDRone step
    JPEGthree steps

    The three that are not exact are the three whose decoders do an inverse transform from different arithmetic: libjpeg's integer DCT and this library's; a Radiance RGBE triplet, which has eight bits of exponent to lose a mantissa in; and a B44 block, whose differences are added back up from the first of the sixteen samples and where a rounding at the last bit of a half is a step of an eight-bit sample once in a while. A lossy WebP is exact between readers because the reader is libwebp's own arithmetic in both cases: what this library writes is a frame that library reconstructs, not an approximation of one.

    Counted, not described. A tolerance says whether two implementations agree; it does not say by how much they were apart. scripts/samples/delta.py writes one fixture in every format the writer produces and counts the difference twice — between Pillow and this library about the same bytes, and between the file and the picture that went in:

    FormatSame file: largestvs fixture: largestvs fixture: PSNR
    PNG, QOI, BMP, TGA, WebP lossless, TIFF, tiled TIFF, DDS plain, ICO, PNM, EXR uncompressed, RLE, ZIPS, ZIP, PIZ, PXR24, several parts00exact
    WebP lossy, quality 90014522.72 dB
    WebP lossy, quality 20020120.53 dB
    JPEG214719.81 dB
    GIF06322.64 dB
    DDS BC1016719.58 dB
    DDS BC3016722.32 dB
    DDS BC5025510.01 dB
    DDS BC6H123721.75 dB
    DDS BC7021325.42 dB
    OpenEXR B44, B44A13239.21 dB
    OpenEXR DWAA, DWAB1453.32 dB

    Thirty-six files on a thirty-seven by twenty-three fixture: thirty read the same by both libraries, twenty-three lossless to the sample. The one step on a B44, a DWA and a BC6H block is the reference and this library rounding the same arithmetic differently at the last bit; the five formats with a large number in the third column are the nine that are lossy by design. Two formats are counted and not scored: neither Pillow nor OpenEXR's library reads a farbfeld or a Radiance HDR, and an unread file is named as such rather than filled in with a zero.

    Two rows of that table are held differently for a reason outside this library. Pillow refuses a thirty-two bit PCX by name ("unknown PCX mode"), so the file it is handed is the twenty-four bit one, and the four-plane spelling this library writes for a picture with transparency is held by its own reader instead. And Pillow reads a Sun raster's colour map as three bytes a colour where the format says three planes, so the palette samples of that format are held to the builder that wrote them; the twenty-four and thirty-two bit samples are read by Pillow and counted like the rest.

    #Mutation

    A test that passes is worth what it notices. Each entry below is one line of the library changed into a plausible mistake that still compiles, applied on its own, with the package's tests run and the change reverted:

    python scripts/mutation/run.py

    The catalogue lives in that script, one literal replacement per entry, so a refactor cannot quietly move a mutation onto a different function: the text to find has to appear exactly once. Continuous integration runs all of them on every push, and a change the suite does not notice fails the job instead of being written down as a curiosity.

    ChangeCaught by
    The Paeth predictor takes the pixel above when the gradient is a tiepng_test.mbt, the pngsuite decode
    QOI's running index is thirty-two slots instead of sixty-fourqoi_test.mbt, a fixture decoded against the reference
    GIF's interlaced flag is read from the bit beside itgif_test.mbt, the still samples
    A BC1 two-thirds weight is divided by fourencode_test.mbt, a block that follows a gradient
    A B44 shift rounds at half a step instead of at the halfway pointb44_wbtest.mbt, the ties-to-even table
    A JPEG block's coefficients are read in raster orderjpeg_test.mbt, every sample
    The Lanczos window is two lobes wide instead of threefilter_check_test.mbt, scaling against Pillow
    A Radiance pixel is read one power of two too brighthdr_test.mbt, the floats the generator computed
    A PCX run is read as seven bits of count where the format gives sixpcx_test.mbt, every sample
    An SGI is read top down where the format stores it bottom firstsgi_test.mbt, every sample
    A Sun raster row loses the padding keeping it on a wordras_test.mbt, every sample

    The B44 entry is the one that was not caught when the catalogue was first run. Its block tests are held to a bound of a whole step, because that is what the format costs, and a rounding rule that is wrong only at an exact tie stays inside that bound on every picture in the suite. What catches it now is a white-box test that names sixteen values and the number each one has to become — written from the definition, not from the implementation — which is the only kind of test that can see a difference of one at the halfway point.

    #What it does not do

    • Writing an AVIF. Reading one is an AV1 decoder; writing one is that and an encoder on top: a mode search, a rate model and a quantiser. The reader is narrow in ways it says out loud rather than approximating — eight bits a sample, 4:2:0, one tile, no segmentation, no film grain, no super resolution and no intra block copy, each refused by name.
    • Writing a partitioned block of a DDS. BC1 to BC7 are all written; the encoder fits one region per block, and a search over the sixty-four shapes of the partitioned modes would win a few tenths of a decibel on a photograph.
    • SGI files whose rows are run-length coded. The header says which of the two storages a file uses, and this library reads the plain one, which is what every current writer produces. The run-length rows are refused by name rather than decoded on trust: no reference here both writes and reads them, so a transcription could not be held to anything.
    • Writing a lossless AVIF, a lossy animation, or a mipmap chain. The first needs the same AV1 encoder the last section refuses; the other two are containers this library writes one shape of.
    • Photoshop, JPEG 2000 and JPEG XL. Each is a codec of its own rather than a container this library already speaks: PSD wants its layer records and its two-bytes-a-plane run lengths, JPEG 2000 wants wavelets with an embedded block coder, and JPEG XL wants a whole new entropy layer. They are named here rather than listed as supported because a reader that returns the flattened composite of a PSD and nothing else would look complete and be wrong.
    • Vector formats. SVG, PDF, EPS and the Windows metafiles are drawings, not pictures: a path has to be flattened and filled before there is a pixel to hand to anything in this library, and text needs a shaper on top of that. The rasterising is a different product, and a half of it would be worse than none.
    • XBM and XPM. Both are text, both are trivial to read, and neither is worth a package: an XBM is a C array of bits and an XPM is a C array of strings, and a caller with one of those already has the bytes.

    #Interface

    CallWhat 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_animatedwhat the bytes are, and how they are named
    Image::new, Image::ofan image, blank or over bytes that already exist
    Image::row, Image::pixelone row, one pixel
    Image::convertthe same image in another sample format
    Image::to_premultiplied, Image::to_straightcolour × alpha, and back
    Image::flip_horizontal, Image::flip_vertical, Image::rotate, Image::rotate_by, Image::cropmoving pixels without reading them
    Image::scalea new size, by any of five filters
    Image::blur, Image::unsharp_mask, Kernela Gaussian by the kernel or by three box passes, and the difference that undoes it
    Image::brightness, Image::contrast, Image::hue_rotate, Image::invertthe four colour knobs
    Image::compositeanother picture laid over this one
    Image::orient, Image::uprightthe way up a camera said the picture was
    Pixel::to_color, Pixel::of_color, Pixel::rotate_huethe seam to the colour package
    Animation::new, Animation::composeframes, and every frame as a full canvas
    png.encode, png.decode, png.EncodeOptions, png.FilterPNG, with its compression level and row filter
    qoi.encode, qoi.decodeQOI
    bmp.encode, bmp.decode, bmp.EncodeOptions, bmp.DepthBMP, 24 or 32 bits
    tga.encode, tga.decode, tga.EncodeOptions, tga.DepthTGA, with run lengths
    jpeg.encode, jpeg.decode, jpeg.EncodeOptions, jpeg.Subsampling, jpeg.exif_orientationJPEG, and the orientation a camera wrote
    gif.encode, gif.encode_animation, gif.decode, gif.decode_animation, gif.EncodeOptionsGIF, with dithering and transparency
    webp.decode, webp.decode_animation, webp.encode, webp.encode_animationWebP: lossless and lossy read, lossless written
    tiff.decode, tiff.decode_pages, tiff.encode, tiff.encode_pages, tiff.EncodeOptions, tiff.Compression, tiff.orientationTIFF, its compressions and its pages
    ico.decode, ico.decode_all, ico.encode, ico.EncodeOptions, ico.Entry, ico.Kindicons and cursors, at every size in the file
    pnm.decode, pnm.encode, pnm.EncodeOptions, pnm.Kindthe portable anymap, four shapes and two spellings
    hdr.decode, hdr.encodeRadiance HDR, samples above one and all
    exr.decode, exr.encode, exr.EncodeOptions, exr.Compression, exr.half_to_float, exr.float_to_halfOpenEXR, and the half-precision conversion it turns on
    farbfeld.decode, farbfeld.encodefarbfeld
    dds.decode, dds.Blockblock-compressed textures, read
    avif.decode, avif.decode_planes, avif.decode_frameAVIF, as RGBA or as the three planes the bitstream carries

    #Licence

    Apache-2.0. See LICENSE.

    Animation

    A sequence of frames over one canvas.

    Blend

    How a frame's pixels meet what is already on the canvas.

    Chromaticities

    The white point and the three primaries, as chromaticity coordinates.

    Eight numbers rather than a matrix: this is what a file writes, and a matrix would be a conversion, which this library does not do.

    Dispose

    What to do with the frame's rectangle once the frame has been shown.

    Format

    The formats this library reads and writes.

    Frame

    One frame: a picture, where it goes, and how long it stays.

    Icc

    An embedded ICC profile, kept by name and bytes. Not applied: a caller that wants colour management hands these bytes to something that does it.

    Image

    An image: its shape, its layout, and the bytes.

    Intent

    What a column of pixels was meant to be seen in.

    Kernel

    How a blur is worked out.

    Limits

    The ceilings a decoder works under.

    The defaults are the ones a desktop application can live with: a texture no larger than a modern GPU takes, and a megabyte of pixels at the very most. A caller reading files from the network should set them lower; a caller reading its own assets can leave them alone.

    Metadata

    Everything the file said about itself besides its pixels.

    Pixel

    One pixel, in the units every operation in the library works in: four samples from 0 to 1, alpha straight.

    A grey pixel has r, g and b equal, which is why there is no grey variant: a reader that has to branch on whether a pixel is grey is a reader that will forget to inflate one of the three channels.

    PixelFormat

    How the samples of an image are laid out.

    The names are the usual ones: Gray8 is one byte per pixel, Rgba8 four, Rgba16 eight with each channel big-endian, RgbaF32 four floats.

    Refused

    A refusal to read bytes as an image, or to write an image as bytes.

    The variants separate the three questions a caller asks: is this the wrong kind of file (Unknown), is the file damaged (Unexpected, Truncated, Checksum), or is the caller asking for more than it allowed (OverLimit). Unsupported is the fourth: a part of a format this library does not read.

    Sampling

    How a scaler decides what an unknown pixel is worth.

    Text

    A text note, in the flavour of the chunk it came from, so that writing the image back produces the same kind of chunk.

    decode

    The image in these bytes, whichever format they are.

    Without format the picture comes back in the layout the file itself holds, which is what a second decode or a comparison against the original wants. With it, the picture comes back in the layout a caller is drawing with: a canvas wants Rgba8, a GPU upload of a normal map wants Rgb8, and a block texture's own layout is neither. That is one conversion, and none at all when the file already holds the format asked for.

    The limits default to Limits::default(); a caller reading untrusted files should pass Limits::strict() or a tighter record.

    decode_animation

    Every frame in these bytes, whichever format they are.

    A format that does not animate comes back as an animation of one frame over the whole canvas, so a caller that only draws animations does not need to branch. format is applied to every frame, for the same reason it exists on decode: a canvas draws in one layout and a file may hold several. A TIFF comes back as one frame a page, and an icon as one frame a size, because both are a sequence of pictures that a caller walking a sequence wants to see.

    encode

    The image as bytes of the named format, with that format's defaults.

    A caller that wants to choose how it is written calls the format's own package, which takes its options record.

    Dds is written as a surface of plain samples. A block-compressed texture is written by that package's own call, which takes the block format.

    encode_animation

    The animation as bytes of the named format, with that format's defaults.

    The formats that animate keep every frame; the ones that do not write the first, which is what a viewer of a still image shows.

    Source Files