image

    一个纯 MoonBit 实现的图像解码库,支持 BMP/QOI/TGA/PNG/GIF/JPEG 六种格式,内置完整 DEFLATE 解压器和 LZW/RLE 压缩支持,一行代码即可完成图像解码。

    image
    decoder
    png
    bmp
    qoi
    tga
    gif
    jpeg
    graphics
    Download zip
    Author
    Version
    0.1.4
    License
    MIT
    Last updated
    last month
    Downloads
    35

    #image - Pure MoonBit Image Decoding Library

    A comprehensive image decoding library written entirely in MoonBit, supporting multiple popular image formats with zero external dependencies.

    #Supported Formats

    FormatStatusDescription
    BMP✅ Complete1/4/8/24/32-bit, top-down & bottom-up
    QOI✅ CompleteRGB & RGBA, full spec compliance
    TGA✅ CompleteUncompressed & RLE, 8/16/24/32-bit
    PNG✅ Complete8-bit grayscale/RGB/RGBA, indexed, Adam7, full DEFLATE
    GIF✅ CompleteGIF87a/89a, LZW decompression, interlace, transparency
    JPEG✅ BaselineGrayscale + YCbCr color, DCT/IDCT, Huffman

    #Installation

    moon add shunge/image

    #Quick Start

    fn main {
    // Auto-detect format and decode
    let img = @image.decode(data)

    println("Image: \{img.width} x \{img.height}")
    println("Format: \{pixel_format_name(img.format)}")

    // Access individual pixels
    let pixel = img.get_pixel(10, 20)
    println("Pixel: R=\{pixel.r} G=\{pixel.g} B=\{pixel.b} A=\{pixel.a}")
    }

    #API Reference

    #Unified Decode

    // Auto-detect format and decode
    pub fn decode(data : Bytes) -> Image raise Failure

    // Decode with known format
    pub fn decode_by_format(data : Bytes, format : ImageFormat) -> Image raise Failure

    // Detect format from magic bytes
    pub fn detect_format(data : Bytes) -> Option[ImageFormat]

    // Check if data matches any supported format
    pub fn is_supported_format(data : Bytes) -> Bool

    // Get human-readable pixel format name
    pub fn pixel_format_name(format : PixelFormat) -> String

    #Format-Specific Decoders

    pub fn decode_bmp(data : Bytes) -> Image raise Failure
    pub fn decode_qoi(data : Bytes) -> Image raise Failure
    pub fn decode_tga(data : Bytes) -> Image raise Failure
    pub fn decode_png(data : Bytes) -> Image raise Failure
    pub fn decode_gif(data : Bytes) -> Image raise Failure
    pub fn decode_jpeg(data : Bytes) -> Image raise Failure
    pub fn decode_gif_all(data : Bytes) -> AnimatedImage raise Failure // animated GIF

    #Image Type

    pub struct Image {
    width : Int
    height : Int
    format : PixelFormat
    data : Bytes // Raw pixel data, row-major
    }

    // Constructors & methods
    pub fn Image::new(width : Int, height : Int, format : PixelFormat, data : Bytes) -> Image
    pub fn Image::get_pixel(self : Image, x : Int, y : Int) -> Color
    pub fn Image::to_rgba8(self : Image) -> Image
    pub fn Image::bytes_per_pixel(self : Image) -> Int

    #PixelFormat

    pub enum PixelFormat {
    Gray8 // 8-bit grayscale
    GrayA8 // 16-bit grayscale + alpha
    RGB8 // 24-bit RGB
    RGBA8 // 32-bit RGBA
    }

    #Architecture

    shunge/image ├── lib.mbt # Main entry: format detection + dispatch ├── types.mbt # Core types: Image, Color, PixelFormat, BitReader ├── utils.mbt # Byte reading utilities, CRC32, Adler32 ├── color.mbt # IDCT, YCbCr→RGB color conversion ├── transform.mbt # Image transform utilities (crop/flip/resize/rotate) │ ├── bmp.mbt # BMP decoder ├── qoi.mbt # QOI decoder ├── tga.mbt # TGA decoder ├── png.mbt # PNG decoder + DEFLATE decompressor ├── gif.mbt # GIF decoder + LZW decompressor ├── jpeg.mbt # JPEG baseline decoder (DCT/IDCT/Huffman) │ ├── bmp_writer.mbt # BMP encoder ├── qoi_writer.mbt # QOI encoder │ ├── image_test.mbt # Core tests: format-specific decoders + error paths ├── medium_image_test.mbt # Medium-size tests: 64×64 images ├── complex_image_test.mbt # Complex pattern tests: 128×128 checkerboard, noise, Mandelbrot ├── comprehensive_test.mbt # Comprehensive tests: GIF/JPEG features, animated GIF, errors ├── fuzz_test.mbt # Fuzz testing (random bytes → decoders, must not crash) ├── roundtrip_test.mbt # Round-trip encode/decode tests ├── jpeg_real_test.mbt # Real JPEG photo tests (11 photos, decode_jpeg verification) │ ├── example/ # CLI example: image_info ├── tools/ # Test image generation scripts ├── test_images/ # Test images (up to 2048×2048 + real photos) ├── ARTICLE.md # Article: hand-writing DEFLATE in MoonBit └── LICENSE # MIT

    #Supported BMP Features

    • 32-bit BGRA (uncompressed)
    • 24-bit BGR (uncompressed)
    • 8-bit indexed (with palette)
    • 4-bit indexed (with palette)
    • 1-bit monochrome
    • Top-down and bottom-up scan order
    • Automatic 4-byte row alignment handling

    #Supported QOI Features

    • RGB (3-channel) and RGBA (4-channel)
    • All QOI chunk types: INDEX, DIFF, LUMA, RUN, RGB, RGBA
    • Full color hash cache implementation
    • sRGB and linear colorspace detection

    #Supported TGA Features

    • Type 2: Uncompressed true-color (24/32-bit)
    • Type 3: Uncompressed grayscale (8-bit)
    • Type 10: RLE compressed true-color (24/32-bit)
    • Type 11: RLE compressed grayscale (8-bit)
    • Top-left and bottom-left image origin
    • 16-bit A1R5G5B5 format support

    #Supported PNG Features

    • 8-bit depth: grayscale, RGB, RGBA, grayscale+alpha, indexed color (PLTE)
    • Full DEFLATE decompression (RFC 1951)
      • Uncompressed blocks (BTYPE=0)
      • Fixed Huffman codes (BTYPE=1)
      • Dynamic Huffman codes (BTYPE=2)
    • All five PNG filter types: None, Sub, Up, Average, Paeth
    • zlib wrapper (RFC 1950) with header verification
    • CRC32 chunk integrity checking
    • Adam7 interlaced images

    #Supported GIF Features

    • GIF87a and GIF89a formats
    • LZW decompression with variable-length codes (up to 12 bits)
    • Global and local color tables
    • 4-pass interlacing
    • Transparency via Graphic Control Extension
    • Output format: RGBA8 (palette expansion)

    #Supported JPEG Features

    • Baseline JPEG (SOF0) with 8-bit precision
    • Grayscale (Gray8) and YCbCr color (RGB8) images
    • Huffman-coded DC and AC coefficients with O(1) prefix table lookup
    • Zigzag deordering, dequantization, and IDCT
    • Sub-sampling: 4:4:4, 4:2:2, 4:2:0
    • Restart marker (RST) support
    • Output formats: Gray8 / RGB8

    #Limitations (Future Work)

    • JPEG progressive mode (baseline only)
    • 16-bit per channel depth (currently 8-bit only)
    • Lossless JPEG support
    • Arithmetic coding in JPEG
    • Ancillary chunk parsing (gAMA, cHRM, sRGB, etc.)
    • Streaming/incremental decode
    • Image encoders for PNG/JPEG/GIF/TGA (BMP and QOI encoding available)

    #License

    MIT

    #Testing

    The project includes 108 tests across 6 test files:

    moon test # Run all 108 tests

    Tests cover:
    • Basic decoding (2×2 ~ 16×16): format-specific decoder correctness for BMP/TGA/QOI/PNG/GIF/JPEG
    • Error handling (14 tests): truncated data, invalid magic bytes, CRC mismatches, corrupted streams
    • Fuzz testing (9 tests): random byte sequences fed to each decoder — must not crash
    • Round-trip tests (17 tests): encode → decode → verify identity for BMP/QOI/TGA
    • Medium images (64×64): full pixel checksum verification for BMP/TGA/PNG/QOI
    • Complex patterns (128×128): checkerboard, noise, radial gradient, Mandelbrot fractal — stresses DEFLATE, RLE, and Huffman decoding
    • Comprehensive format tests (20 tests): PNG filters, multi-IDAT, Adam7, GIF interlace/transparency/animation, JPEG subsampling
    • Real photo JPEG tests (11 tests): decode_jpeg() on embedded JPEG thumbnails of real-world photos (1080p–1706px) with pixel-accurate checksums
    • Large images (up to 2048×2048): verified via Python/PIL reference decoder

    #Contributing

    This library aims to build out the MoonBit image processing ecosystem. Planned future additions:
    • Additional formats (WebP, TIFF, AVIF)
    • Image processing operations (resize, rotate, filters)
    • CLI/Web demo application

    AnimatedImage

    pub struct AnimatedImage {
    frames : Array[Image]
    delays : Array[Int]
    width : Int
    height : Int
    loop_count : Int
    }

    A multi-frame animated image with per-frame delay timing

    AnimatedImage::frame_count

    fn AnimatedImage::frame_count(self : AnimatedImage) -> Int

    AnimatedImage::new

    fn AnimatedImage::new(frames : Array[Image], delays : Array[Int], width : Int, height : Int, loop_count : Int) -> AnimatedImage

    Color

    pub struct Color {
    r : Int
    g : Int
    b : Int
    a : Int
    }

    A color represented as 8-bit RGBA components

    Color::blend

    fn Color::blend(self : Color, other : Color) -> Color

    Blend this color with another color using alpha compositing (over operator) The result replaces the background color with this (foreground) color on top alpha is in 0..255 range for per-pixel control

    Color::blend_alpha

    fn Color::blend_alpha(self : Color, other : Color, alpha : Int) -> Color

    Alpha blend with a specific alpha override (0..255)

    Color::default

    fn Color::default() -> Color

    Constructors and utilities for Color

    Color::distance

    fn Color::distance(self : Color, other : Color) -> Int

    Compute a simple perceptual color distance (weighted Euclidean) Uses simplified CIE76-like formula with weighted RGB components. Returns 0..441 for maximum distance (black vs white at max alpha).

    Color::from_gray

    fn Color::from_gray(v : Int) -> Color

    Color::from_hsl

    fn Color::from_hsl(hue : Int, saturation : Int, lightness : Int) -> Color

    Create a Color from HSL values (Hue 0..360, Saturation 0..100, Lightness 0..100)

    Color::from_hsv

    fn Color::from_hsv(hue : Int, saturation : Int, value : Int) -> Color

    Create a Color from HSV values (Hue 0..360, Saturation 0..100, Value 0..100)

    Color::from_rgb

    fn Color::from_rgb(r : Int, g : Int, b : Int) -> Color

    Color::invert

    fn Color::invert(self : Color) -> Color

    Invert this color (255 - each channel), preserves alpha

    Color::lerp

    fn Color::lerp(self : Color, other : Color, t : Int) -> Color

    Linear interpolation between two colors (t in 0..255 fixed-point) Returns an integer-blended color without floating-point

    Color::new

    fn Color::new(r : Int, g : Int, b : Int, a : Int) -> Color

    Color::to_gray

    fn Color::to_gray(self : Color) -> Int

    Color::to_hsl

    fn Color::to_hsl(self : Color) -> (Int, Int, Int)

    Convert an RGB color to HSL (Hue 0..360, Saturation 0..100, Lightness 0..100) Uses integer arithmetic to avoid floating-point precision issues

    Color::to_hsv

    fn Color::to_hsv(self : Color) -> (Int, Int, Int)

    Convert RGB to HSV (Hue 0..360, Saturation 0..100, Value 0..100)

    Color::with_alpha

    fn Color::with_alpha(self : Color, a : Int) -> Color

    Image

    pub struct Image {
    width : Int
    height : Int
    format : PixelFormat
    data : Bytes
    }

    A decoded image with pixel data

    Image::average_color

    fn Image::average_color(self : Image) -> Color

    Compute the average color of the image

    Image::brighten

    fn Image::brighten(self : Image, delta : Int) -> Image

    Brighten the image by adding a delta to all channels Positive delta brightens, negative darkens. Clamped to 0..255. Returns a new Image (does not modify self).

    Image::bytes_per_pixel

    fn Image::bytes_per_pixel(self : Image) -> Int

    Number of bytes per pixel for this format

    Image::crop

    fn Image::crop(self : Image, x : Int, y : Int, w : Int, h : Int) -> Image raise Failure

    Extract a sub-rectangle from the image.

    Errors

    Raises Failure if the crop region is out of bounds or has non-positive dimensions.

    Image::data_size

    fn Image::data_size(self : Image) -> Int

    Total size of pixel data in bytes

    Image::flip_horizontal

    fn Image::flip_horizontal(self : Image) -> Image

    Flip the image horizontally (mirror left-to-right). Each row is reversed in-place into a new buffer.

    Image::flip_vertical

    fn Image::flip_vertical(self : Image) -> Image

    Flip the image vertically (mirror top-to-bottom). Rows are copied in reverse order to a new buffer.

    Image::get_pixel

    fn Image::get_pixel(self : Image, x : Int, y : Int) -> Color

    Get the color at a specific pixel coordinate

    Image::histogram

    fn Image::histogram(self : Image) -> Array[Int]

    Compute a 256-bin luminance histogram from the image Returns an array[256] of pixel counts, indexed by luminance value (0-255)

    Image::new

    fn Image::new(width : Int, height : Int, format : PixelFormat, data : Bytes) -> Image

    Image::resize_bilinear

    fn Image::resize_bilinear(self : Image, new_w : Int, new_h : Int) -> Image

    Resize the image using bilinear interpolation. Each output pixel is a weighted blend of its 4 nearest source neighbors. Interpolation is performed independently on each byte channel.

    Image::resize_nearest

    fn Image::resize_nearest(self : Image, new_w : Int, new_h : Int) -> Image

    Resize the image using nearest-neighbor sampling. Uses integer arithmetic: src_x = dst_x * old_w / new_w.

    Image::rotate_180

    fn Image::rotate_180(self : Image) -> Image

    Rotate the image 180 degrees. Implemented as flip horizontal then flip vertical.

    Image::rotate_270

    fn Image::rotate_270(self : Image) -> Image

    Rotate the image 270 degrees clockwise (equivalent to 90 degrees counter-clockwise). New width = old height, new height = old width.

    Image::rotate_90

    fn Image::rotate_90(self : Image) -> Image

    Rotate the image 90 degrees clockwise. New width = old height, new height = old width.

    Image::stride

    fn Image::stride(self : Image) -> Int

    Number of bytes per row (stride) for this image

    Image::to_grayscale

    fn Image::to_grayscale(self : Image) -> Image

    Convert an image to grayscale in-place (returns Gray8 or GrayA8)

    Image::to_rgba8

    fn Image::to_rgba8(self : Image) -> Image

    Convert this image to RGBA8 format (always 4 bytes per pixel) Uses per-format bulk loops for performance (avoids per-pixel match dispatch)

    ImageFormat

    pub enum ImageFormat {
    BMP
    QOI
    TGA
    PNG
    GIF
    JPEG
    }

    Image file formats that can be detected and decoded

    PixelFormat

    pub enum PixelFormat {
    Gray8
    GrayA8
    RGB8
    RGBA8
    }

    All pixel formats supported by this library

    decode

    fn decode(data : Bytes) -> Image raise Failure

    Auto-detect format and decode an image from raw bytes

    decode_bmp

    fn decode_bmp(data : Bytes) -> Image raise Failure

    Decode a BMP image from raw bytes

    decode_by_format

    fn decode_by_format(data : Bytes, format : ImageFormat) -> Image raise Failure

    Decode an image with a known format

    decode_gif

    fn decode_gif(data : Bytes) -> Image raise Failure

    Decode a GIF image from raw bytes (returns the first frame)

    decode_gif_all

    fn decode_gif_all(data : Bytes) -> AnimatedImage raise Failure

    Decode all frames from an animated GIF, returning an AnimatedImage. Parses Graphic Control Extensions for frame delays and transparency, and the Netscape Application Extension for loop count. Frames are composited onto the full canvas respecting disposal methods.

    decode_jpeg

    fn decode_jpeg(data : Bytes) -> Image raise Failure

    Decode a JPEG image from raw bytes. Supports grayscale (1-component) and YCbCr color (3-component) baseline JPEG.

    decode_png

    fn decode_png(data : Bytes) -> Image raise Failure

    Decode a PNG image from raw bytes

    decode_qoi

    fn decode_qoi(data : Bytes) -> Image raise Failure

    Decode a QOI image from raw bytes

    decode_tga

    fn decode_tga(data : Bytes) -> Image raise Failure

    Decode a TGA image from raw bytes

    detect_format

    fn detect_format(data : Bytes) -> ImageFormat?

    Detect the image format from magic bytes (file signature) PNG and QOI have distinctive signatures, BMP has "BM", TGA uses heuristic + footer

    encode

    fn encode(img : Image, format : ImageFormat) -> Bytes raise Failure

    Encode an image to the specified format Supports BMP, QOI encoding. TGA encoding planned.

    encode_bmp

    fn encode_bmp(image : Image) -> Bytes raise Failure

    Encode an Image as an uncompressed BMP file.

    The image is converted to RGBA8 internally. If all pixels are fully opaque (alpha = 255 everywhere), the output is 24-bit BGR. Otherwise it is 32-bit BGRA to preserve transparency.

    Pixel data is written top-down with a negative height in the DIB header, so most BMP readers display the image correctly without additional flipping.

    Errors

    Raises Failure if the image has zero width or height.

    encode_qoi

    fn encode_qoi(image : Image) -> Bytes raise Failure

    Encode an image to QOI (Quite OK Image) format. Returns the encoded bytes ready to write to a file.

    image_dimensions

    fn image_dimensions(data : Bytes) -> (Int, Int, ImageFormat) raise Failure

    Get image dimensions from a file header without decoding pixels Much faster than decode() for inspecting image metadata

    image_format_name

    fn image_format_name(format : ImageFormat) -> String

    Get a human-readable string for an image format

    is_supported_format

    fn is_supported_format(data : Bytes) -> Bool

    Check if data appears to be a supported image format

    jpeg_dimensions

    fn jpeg_dimensions(data : Bytes) -> (Int, Int) raise Failure

    Read JPEG dimensions from header without decoding pixels. Fast path: scans for SOF0 marker and extracts width/height.

    pixel_format_name

    fn pixel_format_name(format : PixelFormat) -> String

    Get a human-readable string for a pixel format