Sign in

    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.5
    License
    MIT
    Last updated
    17 hours ago
    Downloads
    72

    No dependencies

    #image - Pure MoonBit Image Decoding Library

    A pure MoonBit image decoding library with no external runtime dependencies. Supported inputs and intentional limits are listed below; unsupported variants and malformed data raise Failure.

    #Supported Formats

    FormatSupported input
    BMP1/4/8-bit indexed, 24-bit BGR, 32-bit BGRA; RLE4/RLE8; selected 32-bit BITFIELDS layouts
    QOIRGB/RGBA headers, all six chunk types, RGBA8 output
    TGATypes 2/3/10/11, truecolor 16/24/32-bit and grayscale 8/16-bit, four image origins
    PNGStandard color/depth combinations, tRNS, Adam7, stored/fixed/dynamic DEFLATE
    GIFGIF87a/89a, LZW, interlace, transparency and composited animation frames
    JPEG8-bit SOF0 baseline, grayscale and conventional YCbCr 4:4:4/4:2:2/4:2:0

    #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: \{@image.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 the header resembles a supported format; this does not validate pixels
    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 # Embedded JPEG photographs with Pillow reference pixels ├── real_photo_test.mbt # Embedded photographs in BMP/PNG/TGA/QOI/GIF ├── *_regression_test.mbt # Format-specific independent pixel and malformed-input cases ├── decoder_stability_test.mbt # Structured malformed-input stability checks │ ├── 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

    • DIB header sizes 40, 52, 56, 108 and 124 bytes
    • 32-bit BGRA (uncompressed and canonical byte-aligned BITFIELDS masks)
    • 24-bit BGR (uncompressed)
    • 1/4/8-bit indexed images, respecting biClrUsed and actual palette extent
    • RLE4/RLE8 encoded runs, absolute runs, deltas, word padding and end markers
    • Top-down and bottom-up uncompressed rows; compressed images must be bottom-up
    • Checked header/pixel offsets, planes, compression/depth combinations and row strides

    32-bit input preserves the stored alpha byte, including BI_RGB input. Pixels omitted by RLE deltas retain the existing transparent fill. Palette entries are opaque. BITMAPCOREHEADER/OS/2, 16-bit BMP, arbitrary BITFIELDS masks and embedded JPEG/PNG compression are not supported.

    #Supported QOI Features

    • RGB (3-channel) and RGBA (4-channel)
    • All QOI chunk types: INDEX, DIFF, LUMA, RUN, RGB, RGBA
    • The 64-entry color cache, channel wraparound and bounded run lengths
    • Header channel/colorspace validation and required eight-byte end marker
    • RGBA8 output regardless of the informative header channel count

    Colorspace metadata is accepted without performing a color conversion. Truncated chunks, runs beyond the declared dimensions and extra pixel chunks are rejected.

    #Supported TGA Features

    • Types 2/10: raw/RLE truecolor at 16/24/32 bits per pixel
    • Types 3/11: raw/RLE grayscale at 8 bits, or grayscale+alpha at 16 bits
    • All four horizontal/vertical origins, normalized to top-left output
    • 16-bit BGR555 with an optional one-bit attribute; truecolor output is RGBA8
    • Checked image ID/color-map extents, descriptors, packet sizes and pixel counts

    Color-mapped image types 1/9 and interleaved storage are not supported. An unused color map attached to a supported truecolor/grayscale image is skipped. TGA 2.0 extension metadata and color correction are not interpreted.

    #Supported PNG Features

    • All standard color/depth combinations: grayscale (1/2/4/8/16-bit), indexed (1/2/4/8-bit), RGB/RGBA/grayscale+alpha (8/16-bit)
    • Packed grayscale samples expand to Gray8; 16-bit samples use their high byte to fit the existing 8-bit pixel formats
    • tRNS transparency for grayscale, RGB and indexed images, including partial palette alpha tables; transparent colors are compared at original precision
    • 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, window-size and Adler32 verification
    • Multiple DEFLATE blocks with preserved bit alignment and cross-block back-references
    • CRC32 verification for critical chunks and tRNS, chunk ordering/length checks, and palette index validation
    • Adam7 interlaced images at every supported depth, including narrow images and empty passes
    • Checked image/scanline sizes and decompression bounded by the expected filtered image size

    PNG decoding preserves the public API. Grayscale images with tRNS output GrayA8, RGB images with tRNS output RGBA8, and indexed images always output RGBA8. 16-bit input is decoded to 8-bit output; retaining 16-bit sample precision requires a future extension to PixelFormat. Color management and APNG animation are not implemented.

    #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
    • Logical-screen composition, frame rectangles and disposal methods 0 through 3
    • Frame delays and NETSCAPE/ANIMEXTS loop metadata
    • Output format: RGBA8 (palette expansion)

    decode_gif() returns the first composited frame; decode_gif_all() returns composited frames and delays. Plain-text rendering is not supported. Invalid palettes, frame bounds, LZW streams, plain-text blocks and unsupported disposal methods raise Failure.

    #Supported JPEG Features

    • Baseline JPEG (SOF0) with 8-bit precision
    • Grayscale (Gray8) and YCbCr color (RGB8) images in a single scan
    • 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
    • Grayscale sampling factors 1 through 4, normalized to noninterleaved blocks
    • APPn/COM metadata segments before or after the scan
    • Checked segment/table/scan structure, entropy padding and restart sequence
    • Output formats: Gray8 / RGB8

    Three-component input uses conventional component IDs 1/2/3 for Y/Cb/Cr. Progressive, extended sequential, lossless, arithmetic-coded, CMYK/YCCK, Adobe RGB-transform and separate component scans are not supported. JPEG pixels can differ slightly from libjpeg/Pillow because of IDCT and color rounding.

    #Resource Limits

    Decoder dimensions and buffer sizes are checked before allocation. Each checked decoded pixel buffer is limited to 256 MiB (268,435,456 bytes); PNG also limits the expected filtered scanline buffer and inflation output. GIF animation output additionally limits the sum of retained full-canvas frame bytes to 256 MiB.

    These are per-buffer and GIF frame-storage limits, not a process-wide memory budget. Input bytes, temporary buffers, copies, JPEG component planes and later image transformations can consume additional memory. Exceeding a decoder limit raises Failure.

    #Other Limitations

    • Preserving 16-bit PNG output precision (PNG input is supported and reduced to 8-bit samples)
    • PNG color management (gAMA/cHRM/sRGB/ICC) and APNG animation
    • Streaming/incremental decode
    • Image encoders for PNG/JPEG/GIF/TGA (BMP and QOI encoding available)
    • Formats such as WebP, TIFF and AVIF

    #License

    MIT

    #Testing

    Run strict checks without warning suppression:

    moon check --deny-warn moon fmt --check moon info git diff --exit-code -- pkg.generated.mbti moon test --deny-warn --target wasm-gc moon test --deny-warn --target js moon test --deny-warn --target native

    CI runs the wasm-gc, JavaScript and native targets on Linux. Compiler and core are pinned together to 0.10.14+7d59c7ec9; the reference job uses Node.js 24, Python 3.12 and Pillow 12.3.0. Native tests require a supported local C toolchain.

    Tests cover exact decoded pixels, row padding, origins, palette/transparency handling, PNG filters/Adam7/DEFLATE, GIF composition, JPEG sampling/restarts, malformed structures, resource limits and deterministic input mutation. Existing round-trip and pattern tests complement independent reference cases.

    The photographic test suites use embedded image bytes and Pillow-derived pixel expectations instead of decoder-derived checksums. Lossless comparisons are exact; JPEG comparisons allow a documented per-channel rounding tolerance. Regeneration also repairs invalid legacy QOI end markers.

    For a reusable comparison against an independent decoder, install Pillow and run:

    python -m pip install Pillow==12.3.0 python -B -m unittest discover -s tools -p test_pillow_reference.py python tools/verify_decoders.py python tools/verify_decoders.py --corpus /path/to/photos --max-side 128

    The verifier builds the actual MoonBit decoder for JavaScript and compares its RGBA bytes against Pillow. Embedded photographic JPEGs and optional corpus files are first compared directly, preserving original dimensions and encoding features. Unsupported, unreadable or corrupt originals are recorded as failures; the verifier continues with other files and writes its report before returning a failure status. It also generates cases from those photographs and deterministic patterns across the six formats, including JPEG 4:4:4, 4:2:2 and 4:2:0. --max-side bounds the derived cases; original-file comparisons keep their full dimensions.

    tools/pillow_reference.py supplements Pillow for PNG normalization and transparency cases that Pillow's RGBA conversion does not preserve: 16-bit grayscale uses the high byte rather than clamping, and grayscale/RGB tRNS comparisons use original sample precision, masking unused key bits according to the PNG specification. These expectations come from an independent Python implementation of zlib inflation, PNG filters and Adam7 sample extraction, followed by specification-defined normalization; they never use MoonBit decoder output. The five Python oracle tests cover normalization, transparency precision, interlacing/filters, unused transparency bits and continued processing after bad original files.

    Lossless results must match exactly; JPEG allows at most three levels of error per channel. Sources, generated cases and the JSON report _build/decoder-reference-results.json remain local; no network is required. The report records the Pillow version, case dimensions and pixel errors. These cases complement the format-specific fixtures rather than establishing support for every variant of a format.

    Regenerate the independent fixtures from the repository root, then format and rerun the tests. PNG generators use Python's standard library; the remaining generators require Pillow:

    python tools/generate_png_regression_fixtures.py python tools/generate_png_deflate_fixtures.py python tools/generate_bmp_regression_fixtures.py python tools/generate_gif_regression_fixtures.py python tools/generate_jpeg_regression_fixtures.py python tools/generate_qoi_tga_fixtures.py python tools/generate_photo_references.py moon fmt moon test --deny-warn

    Some fixtures assert specification behavior that differs from Pillow: odd RLE4 absolute runs are checked through equivalent uncompressed reference images; 32-bit BMP alpha and skipped RLE pixels retain this library's documented behavior. These exceptions are identified in their generators.

    PNG behavior follows the W3C PNG specification, RFC 1950 and RFC 1951.

    #Contributing

    This library aims to build out the MoonBit image processing ecosystem. Planned future additions:
    • Additional formats (WebP, TIFF, AVIF)
    • Additional image processing filters (resize and rotation already exist)
    • 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 composited GIF frames. Delays are in hundredths of a second.

    decode_jpeg

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

    Decode an eight-bit single-scan baseline JPEG (grayscale or JFIF YCbCr).

    decode_png

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

    Decode a PNG image from raw bytes Samples are returned in an 8-bit PixelFormat; 16-bit samples use their high byte. tRNS adds alpha to grayscale/RGB output and supplies indexed palette alpha.

    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_encode_supported

    fn is_encode_supported(format : ImageFormat) -> Bool

    Check if a format supports encoding (write/save)

    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 dimensions of a supported baseline frame without decoding pixels.

    pixel_format_name

    fn pixel_format_name(format : PixelFormat) -> String

    Get a human-readable string for a pixel format