Sign in

    pixelforge

    Pure MoonBit image processing library with filters, morphology, color-space operations, affine transforms, drawing, compositing, PNG/GIF/QOI/BMP codecs, TIFF and lossless WebP decoding, and JPEG/WebP encoding adapters. Backend-agnostic (js, wasm-gc, native) with a browser Playground.

    image
    image-processing
    filters
    convolution
    wasm
    canvas
    Download zip
    Author
    Version
    0.19.0
    License
    Apache-2.0
    Last updated
    25 days ago
    Downloads
    163

    #PixelForge

    CI

    简体中文 | English

    PixelForge 是使用 MoonBit 编写的 RGBA8 图像处理库,提供滤镜、几何变换、绘图、分析和编解码,以及 native CLI 和浏览器 Playground。核心支持 JavaScript、wasm-gc 和 native;Playground 另提供线性内存 WebAssembly 引擎。

    当前版本为 0.19.0。 本版移除了旧 AV1/AVIF 解析和像素解码接口,从 0.18.0 升级前请阅读迁移说明。历史 GitHub v1.0.0 对应早期 0.10.0 包,不代表当前版本。版本历史见 CHANGELOG。

    #快速开始

    安装 MoonBit 工具链,新建项目并添加依赖:

    moon new pixel-demo --user demo_user cd pixel-demo moon add 0717lee/pixelforge@0.19.0

    将 cmd/main/moon.pkg 设置为:

    import {
    "0717lee/pixelforge",
    }

    pkgtype(kind: "executable")

    将 cmd/main/main.mbt 替换为以下完整示例:

    ///|
    fn main {
    let image = @pixelforge.Image::new(2, 1)
    image.set_pixel(0, 0, b'\xFF', b'\x00', b'\x00', b'\xFF')
    image.set_pixel(1, 0, b'\x00', b'\xFF', b'\x00', b'\xFF')
    let pipeline = @pixelforge.Pipeline::new()
    .append(@pixelforge.Filter::Grayscale)
    .append(@pixelforge.Filter::Brightness(10))
    let processed = image.apply_pipeline(pipeline)
    let png = @pixelforge.png_encode(processed)
    let decoded = match @pixelforge.png_decode(png) {
    Some(value) => value
    None => abort("PNG decoding failed")
    }
    if decoded.data != processed.data {
    abort("PNG round-trip changed pixels")
    }
    println("\{decoded.width}x\{decoded.height} PNG round-trip OK")
    }

    运行 moon run cmd/main,预期输出 2x1 PNG round-trip OK。切换后端使用 --target js 或 --target native;native 需要平台 C 编译器。仓库内的同一示例见 cmd/quickstart,可用 moon run cmd/quickstart 验证当前源码。

    #功能与 API

    类别主要能力
    滤镜与色调灰度、反色、亮度、对比度、阈值、棕褐色、像素化、中值、直方图均衡、色调分离、gamma、暗角、饱和度、色相、HSL 亮度、色阶、自动对比度
    卷积与边缘自定义 Kernel / convolve,高斯与盒式模糊、锐化、浮雕、Laplacian、Sobel、Scharr、Canny、双边滤波、锐化蒙版
    几何与绘图裁剪、填充、翻转、旋转、仿射矩阵、最近邻/双线性/双三次缩放,线/矩形/圆,5×7 位图文字,source-over 与八种混合模式
    分析与特征RGB/luma 直方图与统计,Otsu/Sauvola/局部均值阈值,形态学、骨架、连通域/区域统计/轮廓、Harris、HOG、LBP、积分图、距离变换、泛洪填充
    比较与噪声aHash/dHash/汉明距离,MSE/PSNR/SSIM,确定性高斯与椒盐噪声,Floyd–Steinberg 抖动
    组合与遍历类型化 Filter / Pipeline、数字 id 派发、共享像素的 for_each_tile / for_each_row

    当前公开签名见 pkg.generated.mbti,参数说明在对应源码注释中。mooncakes API 文档 对应已发布版本。

    Image 按行存储 RGBA 字节,长度必须为 width * height * 4。from_bytes 包装原缓冲区,修改缓冲区也会改变图像;需要独立所有权时使用 copy()。滤镜、几何变换和管线返回新图像;set_pixel、绘图与文字方法原地修改图像。tile/row 与原图共享像素。

    mse / psnr 比较 RGBA 四通道;luma_mse / ssim 使用 Rec.601 亮度并忽略 alpha。ssim 使用整幅图像的单个总体统计窗口,相同尺寸的空图返回 1。双线性/双三次缩放分别插值 RGBA 通道,不进行预乘 alpha 转换。

    #格式与后端支持

    “核心”指 JavaScript、wasm-gc 和 native。Playground 图片加载使用浏览器解码器,其支持范围与核心编解码器分开。

    格式核心解码核心编码主要边界
    PNG支持支持非交错、8-bit 灰度/灰度透明/调色板/RGB/RGBA;tRNS;CRC-32/Adler-32 校验;输出 RGBA8
    GIF支持支持LZW、交错、透明索引;编码单帧、最多 256 个调色板项,alpha < 128 视为透明,超限 abort
    QOI支持支持RGBA 无损往返
    BMP支持支持无压缩 24/32-bit;24-bit 输出不保留 alpha
    JPEG支持支持mizchi/image 的 baseline JPEG;有损,不保留 alpha;jpeg_encode(image, quality)
    WebPLossless VP8LLossless VP8L解码 predictor、cross-color、subtract-green、color-indexing 变换;不解码有损 VP8
    TIFF支持子集不支持classic:chunky 8-bit 灰度/RGB/RGBA,多条带或 tiles,未压缩/PackBits/LZW/Deflate,Predictor=2;BigTIFF:单条带,未压缩/PackBits/LZW,偏移和值在支持的 Int 范围内
    AVIF仅探测和 metadataJS 宿主适配无 AV1 像素解码;编码只接受不透明图像,浏览器需支持 Canvas AVIF 编码,依赖在 Node.js 中使用本地 ffmpeg;native/wasm-gc 编码 abort

    detect_image_format() 识别签名;image_metadata() 读取支持的尺寸信息,均不证明完整像素数据可解码。TIFF/BigTIFF 支持大小端探测。AVIF metadata 读取主图关联的 ispe,不支持无主图尺寸的纯轨道序列。

    gif_decode_all() 返回帧图像及位置、延迟、透明索引和 disposal 元数据,调用方负责播放和帧合成;gif_decode() 返回首帧。核心不提供 AVIF 网格或动画解码。

    处理算子使用 moonbitlang/core;JPEG 编解码、WebP 编码与 AVIF 适配使用 mizchi/image,native CLI 使用 moonbitlang/x/fs。依赖版本和许可见 moon.mod 与 第三方说明。

    #错误、尺寸和参数

    • 像素解码返回 Image?(多帧 GIF 返回帧数组选项);格式错误或不支持的变体返回 None,由调用方处理。
    • Image::new / from_bytes 对负尺寸、超过 100,000,000 像素或缓冲区长度不匹配执行 abort。abort 是终止当前执行的 panic,不是可通过 catch 恢复的普通解码错误。坐标越界、比较/合成尺寸不匹配也会 abort。
    • 任一边为零的图像合法。三个缩放方法将目标边长至少设为 1;空源图生成该尺寸的透明图像。其他算子按各自文档使用。
    • 编码器可能因格式限制而 abort;先检查格式支持、尺寸、GIF 调色板或 AVIF alpha 条件。宿主应按内存预算限制输入文件大小和像素数。
    • Pipeline 与数字派发共用默认规则:阈值 ≤ 0 使用 128,像素块 < 2 使用 8,色阶数 < 2 使用 4,gamma ≤ 0 使用 2.2,暗角 ≤ 0 使用 0.5,Canny 高阈值 ≤ 0 使用 100。需要显式零值时直接调用 threshold(0) / vignette(0.0)。数字 id 对照表见 dispatch.mbt。

    #浏览器 Playground 与性能

    在仓库根目录运行(需要 Node.js 22 或更新版本):

    node serve.mjs

    打开 localhost:8123。支持上传/拖拽/粘贴、参数化管线、撤销/重做、JS/WASM 切换、Worker 和 PNG 下载。GIF 保留动画预览,处理管线编辑载入帧;WebP/AVIF 加载依赖浏览器原生支持。

    上传限制为 20 MiB、16,000,000 像素、单边 8192;预览最长边缩放到 1024,下载也使用预览尺寸。处理原始分辨率请使用库或 CLI。

    主线程和 Worker 通过 WasmPipeline 管理线性内存:同缓冲区长度复用像素分配,长度变化则替换实例,让旧实例可被回收。直接使用低层 alloc 的宿主也须管理实例生命周期,不能每次渲染无限分配。

    JS/WASM 的快慢取决于输入、滤镜、宿主和拷贝成本。页面对比使用当前预览和管线,每引擎预热一次,再运行十次取平均;包含像素复制和计算,不含解码、绘制或 Worker 通信。可复现的 Node.js 基准:

    node scripts/benchmark.mjs

    脚本报告 Node/操作系统/CPU、固定输入与操作、预热/采样次数、median/p95 和 WASM 内存;先校验两引擎像素一致,再计时。这些结果不代表浏览器性能,也不构成通用速度倍率。

    #CLI

    moon run --target native cmd/cli -- --help moon run --target native cmd/cli -- info --input input.tiff moon run --target native cmd/cli -- convert --from png --to qoi --pipeline grayscale,contrast:1.2 --input input.png --output output.qoi

    info 对 AVIF 仅读 metadata;convert 不接受 AVIF 输入或输出。格式、管线、退出和文件覆盖行为见 CLI 文档。CLI 示例不随 mooncakes 库包发布,需使用仓库源码。

    #开发与贡献

    工具链、验证命令、规范和 Web 产物更新见 CONTRIBUTING。HANDOFF 记录维护范围和验收证据;发布指南 说明版本、包文档与 GitHub Release 的同步步骤。

    根目录为核心及单元测试;cmd/ 为示例;web/ 为 Playground;wasmcore/ 为线性内存绑定;scripts/ 为构建、文档、CLI 和性能验证。

    反馈问题请附版本、后端和最小复现。安全问题按 SECURITY 私下报告;参与规范见 CODE_OF_CONDUCT。

    #许可证

    Apache-2.0,依赖归属见 THIRD_PARTY_NOTICES。

    Affine

    pub(all) struct Affine {
    a : Double
    b : Double
    c : Double
    d : Double
    e : Double
    f : Double
    }

    Row-major 2x3 affine matrix mapping (x, y) to (a*x + b*y + c, d*x + e*y + f).

    Affine::apply

    fn Affine::apply(self : Affine, x : Double, y : Double) -> (Double, Double)

    Applies the transform to a point.

    Affine::identity

    fn Affine::identity() -> Affine

    Identity transform.

    Affine::invert

    fn Affine::invert(self : Affine) -> Affine?

    Inverse transform, or None when the matrix is singular (determinant close to zero) and cannot be inverted.

    Affine::rotation

    fn Affine::rotation(degrees : Double) -> Affine

    Rotation about the origin. Positive angles rotate clockwise on screen (y grows downward in image coordinates), matching rotate90.

    Affine::scaling

    fn Affine::scaling(sx : Double, sy : Double) -> Affine

    Scaling about the origin.

    Affine::shear

    fn Affine::shear(shx : Double, shy : Double) -> Affine

    Shear: x' = x + shx*y, y' = shy*x + y.

    Affine::then

    fn Affine::then(self : Affine, next : Affine) -> Affine

    Composition: returns the transform that applies self first and next second (next * self as matrices).

    Affine::translation

    fn Affine::translation(dx : Double, dy : Double) -> Affine

    Translation by (dx, dy).

    BlendMode

    pub(all) enum BlendMode {
    Normal
    Multiply
    Screen
    Overlay
    Darken
    Lighten
    Difference
    Add
    }

    Separable blend modes applied per channel before alpha compositing.

    BoundingBox

    pub(all) struct BoundingBox {
    min_x : Int
    min_y : Int
    max_x : Int
    max_y : Int
    }

    Inclusive pixel bounds for one connected component.

    ComponentStats

    pub(all) struct ComponentStats {
    label : Int
    area : Int
    bbox : BoundingBox
    centroid : (Double, Double)
    min_x : Int
    min_y : Int
    max_x : Int
    max_y : Int
    centroid_x : Double
    centroid_y : Double
    }

    Measurements for one connected foreground component.

    centroid_x and centroid_y are arithmetic means of the foreground pixel coordinates (pixel centres are represented by their integer coordinates). The bounding box is inclusive: a one-pixel component has equal minimum and maximum coordinates on both axes.

    ContourPoint

    pub(all) struct ContourPoint {
    x : Int
    y : Int
    }

    One boundary pixel in a component contour. Coordinates are integer pixel positions with (0, 0) at the image's top-left corner.

    Corner

    pub(all) struct Corner {
    x : Int
    y : Int
    response : Double
    }

    One detected Harris corner. Coordinates use integer pixel positions.

    Filter

    pub(all) enum Filter {
    Grayscale
    Invert
    Brightness(Int)
    Contrast(Double)
    Blur
    Sharpen
    Emboss
    Edges
    Sobel
    Sepia
    Threshold(Int)
    Pixelate(Int)
    Median
    HistogramEqualize
    FlipHorizontal
    FlipVertical
    Posterize(Int)
    Gamma(Double)
    Vignette(Double)
    Scharr
    Canny(Int)
    Otsu
    DitherMono
    }

    A filter operation that can be composed into a [Pipeline].

    Each constructor corresponds to one of the stable apply_filter_id operations. Keeping the operation as an enum means callers cannot create a pipeline with an unknown filter id, while the existing numeric API remains available for host bindings.

    GifFrame

    pub(all) struct GifFrame {
    image : Image
    left : Int
    top : Int
    delay_cs : Int
    disposal : Int
    transparent_index : Int
    }

    A decoded GIF image descriptor. image contains the descriptor's own pixels (without compositing against the logical screen); left/top locate it in that screen. delay_cs is the Graphic Control Extension delay in hundredths of a second. disposal is the GIF disposal method (0 when unspecified), and transparent_index is -1 when transparency is not enabled for this frame.

    Image

    pub(all) struct Image {
    width : Int
    height : Int
    data : FixedArray[Byte]
    }

    An 8-bit RGBA raster image.

    Pixels are stored row-major in data, four bytes per pixel in R, G, B, A order, so data.length() == width * height * 4. This layout matches the browser ImageData.data buffer, which lets a <canvas> hand its pixels to the library without any conversion step.

    Image::adaptive_mean

    fn Image::adaptive_mean(self : Image, radius : Int, bias : Int) -> Image

    Applies a local mean threshold. bias is subtracted from the local mean, so a positive bias makes foreground detection more permissive. It is clamped to [-255, 255], the useful luma range, to keep extreme host values deterministic. Window clipping and alpha handling match sauvola.

    Image::add_gaussian_noise

    fn Image::add_gaussian_noise(self : Image, seed : UInt64, sigma : Double) -> Image

    Adds zero-mean Gaussian noise with standard deviation sigma (in luma steps) to R, G and B; alpha is untouched. The normal deviate is the classic CLT approximation (sum of 12 uniforms minus 6), which is branch-free and deterministic. sigma <= 0 returns a copy.

    Image::add_salt_pepper

    fn Image::add_salt_pepper(self : Image, seed : UInt64, density : Double) -> Image

    Salt-and-pepper noise: each pixel independently becomes pure white ("salt") or pure black ("pepper") with probability density / 2 each. density is clamped into [0, 1]; alpha is preserved. Deterministic for a given seed.

    Image::adjust_lightness

    fn Image::adjust_lightness(self : Image, delta : Double) -> Image

    Shifts every pixel's HSL lightness by delta (in [-1, 1]), clamping the result into [0, 1]. Positive values lighten toward white while preserving hue; negative values darken toward black. delta == 0 returns a copy.

    Image::affine

    fn Image::affine(self : Image, t : Affine) -> Image

    Applies an affine transform, keeping the canvas size. Each output pixel is inverse-mapped into the source and bilinearly sampled; areas with no source coverage become transparent. Aborts on singular transforms.

    Image::apply_filter_id

    fn Image::apply_filter_id(self : Image, filter_id : Int, amount : Double) -> Image

    Applies a filter selected by a numeric id. This single dispatch table is shared by every host binding — the js browser binding, the linear-memory wasm binding, and the native CLI — so they can never drift apart.

    idfilteramount
    0grayscale—
    1invert—
    2brightnessdelta (−255..255)
    3contrastfactor (0.0..3.0)
    4gaussian blur—
    5sharpen—
    6emboss—
    7laplacian edges—
    8sobel edges—
    9sepia—
    10thresholdlevel (default 128)
    11pixelateblock size (default 8)
    12median denoise—
    13histogram equalize—
    14flip horizontal—
    15flip vertical—
    16posterizelevels (default 4)
    17gammagamma value (default 2.2)
    18vignettestrength 0..1 (default 0.5)
    19scharr edges—
    20canny edgeshigh threshold (default 100, low = high/2)
    21otsu auto threshold—
    22floyd–steinberg dither (mono)—

    Image::apply_pipeline

    fn Image::apply_pipeline(self : Image, pipeline : Pipeline) -> Image

    Applies a typed filter pipeline to an image.

    Image::auto_contrast

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

    Auto-contrast: linearly stretches the luma range [luma_min, luma_max] to the full [0, 255], applying the same scale to every channel so hues are preserved. A flat image (min == max) is returned unchanged.

    Image::average_hash

    fn Image::average_hash(self : Image) -> UInt64

    Average hash (aHash): bit i = y*8 + x is set when the luma of cell (x, y) in the 8x8 thumbnail is strictly greater than the thumbnail mean. Robust to scaling and mild blur; brightness inversion flips every bit.

    Image::bilateral

    fn Image::bilateral(self : Image, radius : Int, sigma_space : Double, sigma_range : Double) -> Image

    Applies the bilateral filter with the given neighborhood radius, spatial sigma (pixels) and range sigma (luma steps, 0..255 scale). radius <= 0 returns a copy; non-positive sigmas are clamped to a small positive value. Alpha is preserved.

    Image::blur

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

    Gaussian blur convenience wrapper.

    Image::box_blur

    fn Image::box_blur(self : Image, radius : Int) -> Image

    Box blur (uniform mean) of the given radius using the integral image, so each output pixel costs a constant number of lookups no matter how large the window. radius <= 0 returns a copy; the window is clamped to the image at the borders. Alpha is preserved.

    Image::brightness

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

    Shifts every channel by delta (negative darkens, positive brightens). Out-of-range results saturate at 0 and 255.

    Image::canny

    fn Image::canny(self : Image, low : Int, high : Int) -> Image

    Runs Canny edge detection with the given hysteresis thresholds (low < high, both compared against the L1 gradient magnitude |Gx| + |Gy|). Pixels above high seed edges; pixels above low survive only when 8-connected to a seed. Alpha is preserved.

    Image::composite

    fn Image::composite(self : Image, top : Image, mode : BlendMode) -> Image

    Composites top over self (source-over) with the given blend mode. Both images must have identical dimensions (aborts otherwise).

    Per pixel, following the W3C compositing model: the source color is first mixed toward B(cb, cs) by the backdrop alpha, then source-over composited: ao = as + ab*(1-as) and co = (as*cs' + (1-as)*ab*cb) / ao.

    Image::contours

    fn Image::contours(self : Image, threshold : Int) -> Array[Array[ContourPoint]]

    Returns one deterministic boundary-pixel list for every 4-connected foreground component. Empty images and images without foreground return an empty array. A one-pixel component therefore has a one-point contour.

    Image::contrast

    fn Image::contrast(self : Image, factor : Double) -> Image

    Scales contrast around the 128 mid-gray point. factor > 1.0 boosts contrast, 0.0 < factor < 1.0 flattens it, and 1.0 is a no-op.

    Image::convolve

    fn Image::convolve(self : Image, kernel : Kernel) -> Image

    Convolves the image with kernel, processing R, G and B independently and preserving alpha. Out-of-bounds neighbors are clamped to the edge. This is the compute-heavy core the WebAssembly demo is built to show off.

    Image::copy

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

    Returns a deep copy that shares no mutable state with the original.

    Image::count_components

    fn Image::count_components(self : Image, threshold : Int) -> Int

    Counts the 4-connected foreground blobs (luma > threshold).

    Image::crop

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

    Extracts the w x h rectangle whose top-left corner is (x, y). The requested rectangle is clamped to the source bounds, so the result is never larger than the overlap with the image; out-of-range or empty requests yield a 1x1 transparent image. Pixels are copied verbatim.

    Image::difference_hash

    fn Image::difference_hash(self : Image) -> UInt64

    Difference hash (dHash): bit i = y*8 + x is set when cell (x+1, y) of a 9x8 luma thumbnail is brighter than cell (x, y) — a horizontal gradient signature that is highly robust to global tonal changes.

    Image::dilate

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

    Dilation: each channel takes the 3x3 neighborhood maximum, growing bright regions by one pixel per application.

    Image::distance_transform

    fn Image::distance_transform(self : Image, threshold : Int) -> Image

    Distance transform normalized to a grayscale image: background stays black, and foreground brightness grows with distance from the edge, scaled so the deepest pixel maps to 255. Alpha is preserved.

    Image::dither_grayscale

    fn Image::dither_grayscale(self : Image, levels : Int) -> Image

    Grayscale Floyd–Steinberg dithering to levels tones (clamped to [2, 256]). The image is first reduced to luma; error diffuses in a left-to-right, top-to-bottom scan. Output is gray; alpha preserved.

    Image::dither_mono

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

    1-bit (black/white) Floyd–Steinberg dithering — the classic newsprint look. Equivalent to dither_grayscale(2).

    Image::draw_char

    fn Image::draw_char(self : Image, x : Int, y : Int, ch : Char, scale : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Draws one character with its top-left corner at (x, y), each font pixel scaled to a scale x scale block (scale is clamped to >= 1).

    Image::draw_circle

    fn Image::draw_circle(self : Image, cx : Int, cy : Int, radius : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Draws a circle outline centered at (cx, cy) using the midpoint circle algorithm (integer-only, 8-way symmetric). radius 0 plots one pixel; negative radii draw nothing.

    Image::draw_line

    fn Image::draw_line(self : Image, x0 : Int, y0 : Int, x1 : Int, y1 : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Draws a straight line from (x0, y0) to (x1, y1) inclusive with the classic integer-only Bresenham algorithm.

    Image::draw_rect

    fn Image::draw_rect(self : Image, x : Int, y : Int, w : Int, h : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Draws the 1-pixel outline of the rectangle with top-left corner (x, y) and the given size. Degenerate sizes (< 1) draw nothing.

    Image::draw_text

    fn Image::draw_text(self : Image, x : Int, y : Int, text : String, scale : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Draws a text string starting at (x, y); the pen advances 6 font columns (5 glyph + 1 spacing) per character. Returns nothing; text overflowing the canvas is clipped, not wrapped.

    Image::edges

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

    Laplacian edge convenience wrapper.

    Image::emboss

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

    Emboss convenience wrapper.

    Image::erode

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

    Erosion: each channel takes the 3x3 neighborhood minimum, shrinking bright regions by one pixel per application.

    Image::fill_circle

    fn Image::fill_circle(self : Image, cx : Int, cy : Int, radius : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Fills the disk dx^2 + dy^2 <= radius^2 centered at (cx, cy).

    Image::fill_rect

    fn Image::fill_rect(self : Image, x : Int, y : Int, w : Int, h : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Fills the rectangle with top-left corner (x, y) and the given size.

    Image::flip_horizontal

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

    Horizontal mirror: dst(x, y) = src(width-1-x, y). Each pixel's four channels travel together, so alpha is preserved per pixel.

    Image::flip_vertical

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

    Vertical mirror: dst(x, y) = src(x, height-1-y).

    Image::flood_fill

    fn Image::flood_fill(self : Image, x : Int, y : Int, r : Byte, g : Byte, b : Byte, a : Byte, tolerance : Int) -> Image

    Fills the 4-connected region around (x, y) matching the seed color (within tolerance per channel) with (r, g, b, a). Returns a new image; the original is untouched. An out-of-bounds seed returns a plain copy. If the fill color already matches the seed, the copy is returned unchanged (nothing to do), which also avoids an infinite revisit.

    Image::for_each_row

    fn Image::for_each_row(self : Image, visit : (ImageRow) -> Unit) -> Unit
    Visits rows in order without allocating row buffers.

    Image::for_each_tile

    fn Image::for_each_tile(self : Image, tile_width : Int, tile_height : Int, visit : (ImageTile) -> Unit) -> Unit
    Visits all tiles in row-major order using a zero-copy view.

    The final row or column is clipped when the image dimensions are not multiples of the requested tile size. tile_width and tile_height must both be positive. The callback may mutate pixels through the view; changes are applied directly to the source image.

    Image::from_bytes

    fn Image::from_bytes(width : Int, height : Int, data : FixedArray[Byte]) -> Image

    Wraps an existing RGBA buffer. Aborts if data does not hold exactly width * height * 4 bytes, so a malformed buffer fails fast instead of producing silently corrupt output.

    Image::gamma

    fn Image::gamma(self : Image, value : Double) -> Image

    Gamma correction through a 256-entry lookup table: out = 255 * (c / 255) ^ (1 / value). value > 1 lifts midtones, 0 < value < 1 deepens them, and 1.0 is an exact identity. A non-positive value falls back to the identity.

    Image::gaussian

    fn Image::gaussian(self : Image, radius : Int) -> Image

    Gaussian blur with the given radius (clamped to [0, 32]; radius 0 is a plain copy). Two separable binomial passes — for radius r this equals convolving with the (2r+1)² discrete Gaussian, but costs O(r) per pixel instead of O(r²).

    Image::get_pixel

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

    Reads the (R, G, B, A) channels of the pixel at (x, y).

    Aborts when the coordinates are outside the image bounds or when a host supplies an Image record whose backing buffer does not match its size.

    Image::grayscale

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

    Converts the image to grayscale using the Rec. 601 luma weights (0.299, 0.587, 0.114). The weights are scaled by 1000 so the whole computation stays in integer arithmetic and is bit-for-bit reproducible.

    Image::harris_corners

    fn Image::harris_corners(self : Image, threshold : Double, max_corners : Int) -> Array[Corner]

    Detects Harris corners using threshold as the minimum raw response and returns at most max_corners strongest local maxima.

    Image::histogram_equalize

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

    Histogram equalization on luma: spreads the tonal range to maximize global contrast. Builds the luma histogram, integrates it into a CDF, then remaps every pixel through the normalized CDF. Output is grayscale.

    Image::histogram_luma

    fn Image::histogram_luma(self : Image) -> FixedArray[Int]

    256-bin luma histogram (BT.601 integer weights): result[v] counts the pixels whose luma is exactly v.

    Image::histogram_rgb

    fn Image::histogram_rgb(self : Image) -> (FixedArray[Int], FixedArray[Int], FixedArray[Int])

    Per-channel 256-bin histograms, returned as (red, green, blue).

    Image::hog

    fn Image::hog(self : Image, cell_size : Int, bins : Int, block_size : Int) -> Array[Double]
    Returns a normalized unsigned HOG descriptor.

    cell_size is the side length of one cell in pixels. bins is the number of evenly spaced orientation bins over [0, pi). block_size is the side length of each normalization block in cells. The output is an Array[Double] in block-major order; its length is (cells_x - block_size + 1) * (cells_y - block_size + 1) *block_size * block_size * bins when enough complete cells are present.

    Image::hue_rotate

    fn Image::hue_rotate(self : Image, degrees : Double) -> Image

    Rotates every pixel's hue by degrees around the HSV color wheel while keeping saturation and value untouched.

    Image::integral_image

    fn Image::integral_image(self : Image) -> Integral

    Builds the summed-area table over the R, G and B channels.

    Image::invert

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

    Produces the photographic negative by flipping each channel around 255.

    Image::label_components

    fn Image::label_components(self : Image, threshold : Int) -> (Array[Int], Int)

    Labels 4-connected foreground components. Returns (labels, count) where labels has one entry per pixel — 0 for background, 1..count for each component — in row-major order.

    Image::lbp_codes

    fn Image::lbp_codes(self : Image) -> FixedArray[Byte]
    Returns one 8-bit LBP code for every pixel in row-major order.

    For an interior pixel, bit 0 is top-left and subsequent bits proceed clockwise (top, top-right, ..., left). A bit is set when its neighbour's luma is greater than or equal to the centre luma. Border entries are zero because they cannot provide eight samples.

    Image::lbp_histogram

    fn Image::lbp_histogram(self : Image) -> FixedArray[Int]
    Returns the 256-bin histogram of radius-one LBP codes.

    Only pixels with a complete 3x3 neighbourhood contribute, so the bin sum is max(width - 2, 0) * max(height - 2, 0). The histogram is useful as a compact texture descriptor and is invariant to the image's absolute brightness when local ordering is preserved.

    Image::levels

    fn Image::levels(self : Image, black : Int, white : Int, gamma : Double) -> Image

    Levels adjustment: maps the input window [black, white] onto [0, 255] with an optional midtone gamma (1.0 = linear; > 1 brightens midtones). Values are clamped, so inputs at or below black become 0 and inputs at or above white become 255. white is clamped to stay above black.

    Image::luma_mse

    fn Image::luma_mse(self : Image, other : Image) -> Double

    Mean squared error over Rec.601 luma only. Alpha is intentionally ignored.

    Image::map_rgb

    fn Image::map_rgb(self : Image, transform : (Int, Int, Int) -> (Int, Int, Int)) -> Image

    Applies a per-pixel transform to the R, G, B channels while leaving alpha untouched, clamping every result back into [0, 255]. This is the shared engine behind the point filters below, so each concrete filter only has to express its channel math.

    Image::median

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

    3x3 median filter applied independently to R, G and B; alpha is preserved.

    Image::morph_close

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

    Morphological closing (dilate then erode): fills dark pinholes smaller than the 3x3 element while roughly preserving larger shapes.

    Image::morph_open

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

    Morphological opening (erode then dilate): removes bright specks smaller than the 3x3 element while roughly preserving larger shapes.

    Image::mse

    fn Image::mse(self : Image, other : Image) -> Double

    Mean squared error over all RGBA channels. The result is in channel-value squared units, in the range [0, 65025] for valid 8-bit images.

    Image::new

    fn Image::new(width : Int, height : Int) -> Image

    Creates a fully transparent black image of the given size.

    Image::otsu

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

    Applies Otsu's automatic threshold, returning a binary (black/white) grayscale image. Pixels with luma greater than the computed threshold become white.

    Image::otsu_threshold

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

    Returns the Otsu threshold in 0..255: pixels with luma strictly greater than the returned value are the "foreground" class. Chooses the t that maximizes the between-class variance wB*wF*(mB-mF)^2. Variance is accumulated in Double (the textbook formulation) to stay correct on large images where an all-integer product would overflow.

    Image::pad

    fn Image::pad(self : Image, left : Int, top : Int, right : Int, bottom : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Image

    Surrounds the image with borders of the given thickness (in pixels), filled with the color (r, g, b, a). Negative border widths are treated as zero. The result is (left+width+right) x (top+height+bottom).

    Image::pixel_count

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

    The number of pixels (not bytes) in the image.

    Image::pixelate

    fn Image::pixelate(self : Image, block : Int) -> Image

    Pixelate (mosaic): replaces each block x block tile with its average color. Edge tiles are clipped to the image bounds.

    Image::posterize

    fn Image::posterize(self : Image, levels : Int) -> Image

    Posterize (tone separation): quantizes each channel into levels evenly spaced tones, producing the classic poster/print look. levels is clamped into [2, 256]; pure integer math keeps results exactly reproducible.

    Image::psnr

    fn Image::psnr(self : Image, other : Image) -> Double

    Peak signal-to-noise ratio in decibels, derived from RGBA mse with a peak channel value of 255. Identical images return positive infinity.

    Image::regionprops

    fn Image::regionprops(self : Image, threshold : Int) -> Array[ComponentStats]

    Computes area, inclusive bounds and centroid for every foreground component. Results are ordered by increasing label, which is the same row-major discovery order returned by label_components.

    The threshold comparison is strict (luma > threshold) and connectivity is four-way (up, down, left and right). An image with no foreground pixels returns an empty array.

    Image::resize_bicubic

    fn Image::resize_bicubic(self : Image, new_width : Int, new_height : Int) -> Image

    Bicubic (Catmull-Rom) resize to new_width x new_height (both clamped to at least 1). All four channels including alpha are interpolated; the cubic can overshoot near edges, so results are clamped back to 0..255. An empty source produces transparent pixels at the requested size.

    Image::resize_bilinear

    fn Image::resize_bilinear(self : Image, new_width : Int, new_height : Int) -> Image

    Bilinear resize: blends the four surrounding source pixels by coverage, using center-aligned sampling with clamp-to-edge, which keeps a same-size resize an exact identity. All four channels (including alpha) are blended. Target dimensions are clamped to at least 1; an empty source produces transparent pixels at the requested size.

    Image::resize_nearest

    fn Image::resize_nearest(self : Image, new_width : Int, new_height : Int) -> Image

    Nearest-neighbor resize: picks the closest source pixel. Fast and exact, ideal for pixel-art style scaling. Output is new_width x new_height (both clamped to at least 1). An empty source produces transparent pixels.

    Image::rotate

    fn Image::rotate(self : Image, degrees : Double) -> Image

    Rotates by an arbitrary angle (degrees, clockwise) about the image center on a same-size canvas; uncovered corners become transparent.

    Image::rotate90

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

    Rotates the image 90° clockwise. The result is height x width, so this is a library-level transform and is intentionally not exposed through the fixed-size apply_filter_id dispatch.

    Image::saturate

    fn Image::saturate(self : Image, factor : Double) -> Image

    Scales saturation in HSV space: factor > 1 makes colors more vivid, 0 fully desaturates (equivalent brightness-preserving grayscale in V).

    Image::sauvola

    fn Image::sauvola(self : Image, radius : Int, k : Double, dynamic_range : Int) -> Image

    Applies Sauvola local thresholding.

    radius defines a square window of (2 * radius + 1) pixels. Nonpositive radii use the single pixel at the current coordinate. k is clamped to [0, 1], and nonpositive dynamic_range uses 1; the latter keeps invalid host input deterministic instead of dividing by zero. The threshold is mean * (1 + k * (stddev / dynamic_range - 1)).

    Image::scharr

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

    Scharr edge detection: same structure as Sobel but with 3/10/3 weights, which are closer to rotational symmetry and give stronger, more uniform responses on diagonal edges.

    Image::sepia

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

    Sepia tone: the classic warm photographic look, using the widely used Microsoft sepia coefficients scaled by 1000 for exact integer math.

    Image::set_pixel

    fn Image::set_pixel(self : Image, x : Int, y : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit

    Writes the (R, G, B, A) channels of the pixel at (x, y).

    Aborts when the coordinates are outside the image bounds or when a host supplies an Image record whose backing buffer does not match its size.

    Image::sharpen

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

    Sharpen convenience wrapper.

    Image::skeletonize

    fn Image::skeletonize(self : Image, threshold : Int, max_iterations : Int) -> Image
    Returns a thinned binary image using a bounded Zhang--Suen iteration.

    Image::sobel

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

    Sobel edge detection with the classic 1/2/1 operators.

    Image::ssim

    fn Image::ssim(self : Image, other : Image) -> Double

    Global structural similarity (SSIM) over Rec.601 luma.

    This implementation uses one population-statistics window covering the complete image (there is no sliding-window padding or border policy), and the standard constants K1 = 0.01, K2 = 0.03, L = 255. Alpha is ignored. The result is clamped to [-1, 1] to absorb tiny floating-point overshoots at the theoretical bounds.

    Image::stats

    fn Image::stats(self : Image) -> Stats

    Computes per-channel min/max/mean and the luma min/max/mean over all pixels. For the maximum 100-megapixel image the largest channel sum is 255 * 1e8 ~= 2.55e10, which exceeds 32-bit range, so the accumulators are Int64.

    Image::threshold

    fn Image::threshold(self : Image, level : Int) -> Image

    Binary threshold: pixels whose luma is at least level become white, the rest black. Useful for masks and high-contrast stylization.

    Image::translate

    fn Image::translate(self : Image, dx : Double, dy : Double) -> Image

    Translates by a (possibly fractional) offset on a same-size canvas.

    Image::unsharp_mask

    fn Image::unsharp_mask(self : Image, radius : Int, amount : Double) -> Image

    Sharpens via out = original + amount * (original - gaussian(radius)). radius selects the detail scale (>= 1; 0 makes the mask empty and thus returns a copy); amount scales the enhancement (amount <= 0 returns a copy). Per-channel result is clamped to [0, 255]; alpha is preserved.

    Image::vignette

    fn Image::vignette(self : Image, strength : Double) -> Image

    Vignette: darkens pixels toward the corners while leaving the geometric center untouched. strength in [0, 1] is the fraction of light removed at the far corners; the falloff follows the squared distance from center.

    ImageFormat

    pub(all) enum ImageFormat {
    Png
    Gif
    Qoi
    Bmp
    Jpeg
    Webp
    Avif
    Tiff
    }

    ImageFormat::name

    fn ImageFormat::name(self : ImageFormat) -> String

    ImageMetadata

    pub(all) struct ImageMetadata {
    format : ImageFormat
    width : Int
    height : Int
    animated : Bool
    }

    Width, height and animation hint read from an image container header.

    ImageRow

    pub(all) struct ImageRow {
    image : Image
    row_y : Int
    }
    A zero-copy view of one image row for streaming scans.

    ImageRow::get_pixel

    fn ImageRow::get_pixel(self : ImageRow, x : Int) -> (Byte, Byte, Byte, Byte)
    Reads a pixel from this row.

    ImageRow::width

    fn ImageRow::width(self : ImageRow) -> Int
    Number of pixels in this row.

    ImageRow::y

    fn ImageRow::y(self : ImageRow) -> Int
    Y coordinate of this row in the source image.

    ImageTile

    pub(all) struct ImageTile {
    image : Image
    origin_x : Int
    origin_y : Int
    tile_width : Int
    tile_height : Int
    }
    A read/write view over one rectangular tile of an image.

    ImageTile::copy

    fn ImageTile::copy(self : ImageTile) -> Image
    Copies the tile into an independently-owned image.

    ImageTile::get_pixel

    fn ImageTile::get_pixel(self : ImageTile, x : Int, y : Int) -> (Byte, Byte, Byte, Byte)
    Reads a pixel relative to the tile origin.

    ImageTile::height

    fn ImageTile::height(self : ImageTile) -> Int
    Height of this tile in pixels.

    ImageTile::set_pixel

    fn ImageTile::set_pixel(self : ImageTile, x : Int, y : Int, r : Byte, g : Byte, b : Byte, a : Byte) -> Unit
    Writes a pixel relative to the tile origin without copying the tile.

    ImageTile::width

    fn ImageTile::width(self : ImageTile) -> Int
    Width of this tile in pixels.

    ImageTile::x

    fn ImageTile::x(self : ImageTile) -> Int
    Horizontal origin of this tile in the source image.

    ImageTile::y

    fn ImageTile::y(self : ImageTile) -> Int
    Vertical origin of this tile in the source image.

    Integral

    pub(all) struct Integral {
    width : Int
    height : Int
    sat : Array[Int64]
    }

    A summed-area table for one image: (width+1) x (height+1) per channel, with a zero first row/column so rectangle queries need no bounds checks.

    Integral::rect_sum

    fn Integral::rect_sum(self : Integral, c : Int, x0 : Int, y0 : Int, x1 : Int, y1 : Int) -> Int64

    Sum of channel c over the inclusive rectangle [x0, x1] x [y0, y1] (0-based pixel coordinates). Uses the standard four-corner SAT formula.

    Kernel

    pub(all) struct Kernel {
    size : Int
    weights : Array[Double]
    divisor : Double
    bias : Double
    }

    A square (odd-sized) convolution kernel plus its normalization terms.

    weights are stored row-major with size * size entries. Each output channel is (sum(weight_i * neighbor_i) / divisor) + bias, clamped back into [0, 255]. Keeping divisor and bias on the kernel lets a single convolve implementation serve blur, sharpen, emboss and edge kernels.

    Kernel::box_blur

    fn Kernel::box_blur() -> Kernel

    3x3 box blur: unweighted average of the neighborhood.

    Kernel::edges

    fn Kernel::edges() -> Kernel

    3x3 Laplacian edge kernel; flat regions collapse to black.

    Kernel::emboss

    fn Kernel::emboss() -> Kernel

    3x3 emboss kernel with a +128 bias so flat areas render as mid-gray relief.

    Kernel::gaussian_blur

    fn Kernel::gaussian_blur() -> Kernel

    3x3 Gaussian blur approximation with weights summing to 16.

    Kernel::new

    fn Kernel::new(size : Int, weights : Array[Double], divisor : Double, bias : Double) -> Kernel

    Builds a kernel, aborting if the shape is invalid (even/negative size or a weight count that does not match size * size).

    Kernel::sharpen

    fn Kernel::sharpen() -> Kernel

    3x3 sharpen: boosts the center and subtracts its 4-neighbors.

    Pipeline

    pub struct Pipeline {
    operations : Array[Filter]
    }

    An ordered, reusable sequence of image filters.

    append and then return a new pipeline and leave their receiver's operation list untouched. apply also leaves the source image unchanged, matching the copy-on-write behavior of individual filters.

    Pipeline::append

    fn Pipeline::append(self : Pipeline, filter : Filter) -> Pipeline

    Returns a new pipeline with filter appended at the end.

    Pipeline::apply

    fn Pipeline::apply(self : Pipeline, image : Image) -> Image

    Applies each operation in insertion order.

    Pipeline::from_filter

    fn Pipeline::from_filter(filter : Filter) -> Pipeline

    Creates a pipeline containing one operation.

    Pipeline::is_empty

    fn Pipeline::is_empty(self : Pipeline) -> Bool

    Whether this pipeline contains no operations.

    Pipeline::length

    fn Pipeline::length(self : Pipeline) -> Int

    Number of operations in this pipeline.

    Pipeline::new

    fn Pipeline::new() -> Pipeline

    Creates an empty pipeline. Applying it returns a deep copy of the input.

    Pipeline::then

    fn Pipeline::then(self : Pipeline, other : Pipeline) -> Pipeline

    Returns a new pipeline that applies self, then other.

    Stats

    pub struct Stats {
    min_r : Int
    max_r : Int
    mean_r : Int
    min_g : Int
    max_g : Int
    mean_g : Int
    min_b : Int
    max_b : Int
    mean_b : Int
    luma_min : Int
    luma_max : Int
    luma_mean : Int
    }

    Per-channel and luma summary of an image. Means are rounded to the nearest integer. The fields are read-only (pub): the struct is a result type, so keeping construction internal lets it gain fields later without breaking downstream code.

    avif_encode

    fn avif_encode(_image : Image) -> Array[Byte]

    AVIF encoding is unavailable on native and wasm-gc targets.

    bmp_decode

    fn bmp_decode(data : Array[Byte]) -> Image?

    Decodes an uncompressed 24- or 32-bit BMP. Returns None on malformed input (bad magic, unsupported bpp/compression, truncated pixel data). 24-bit sources decode with alpha set to 255; top-down bitmaps (negative height) are handled.

    bmp_encode

    fn bmp_encode(img : Image, with_alpha : Bool) -> Array[Byte]

    Encodes an image as an uncompressed BMP. with_alpha = true writes a 32-bit BGRA bitmap; otherwise a 24-bit BGR bitmap with padded rows.

    clamp_byte

    fn clamp_byte(value : Int) -> Byte

    Clamps an integer into the [0, 255] range and narrows it to a Byte. Channel math is done in Int to avoid wrap-around, then funnelled through this helper before being stored back into a pixel buffer.

    detect_image_format

    fn detect_image_format(data : Array[Byte]) -> ImageFormat?

    Identifies common raster containers from their signatures and brands.

    gif_decode

    fn gif_decode(data : Array[Byte]) -> Image?

    Decodes the first frame of a GIF87a/GIF89a file into an RGBA image. Supports global/local color tables, interlacing and the transparent index from the graphic control extension. Returns None on anything malformed (bad signature, truncated blocks, invalid LZW data).

    gif_decode_all

    fn gif_decode_all(data : Array[Byte]) -> Array[GifFrame]?

    Decodes every image descriptor in a GIF into independent RGBA frames.

    Frames are intentionally not composited: callers can use left, top, and disposal to implement the animation policy they need while keeping this decoder deterministic and allocation-bounded. Returns None for malformed input, including truncated extensions, invalid color tables, invalid LZW data, and files without an image descriptor.

    gif_encode

    fn gif_encode(img : Image) -> Array[Byte]
    Encodes one RGBA image as GIF89a.

    The encoder supports up to 256 RGB entries. Alpha values below 128 share one transparent palette entry. The LZW stream is deliberately literal-only with a clear code before each pixel, making the output deterministic and easy to validate across backends.

    hamming_distance

    fn hamming_distance(a : UInt64, b : UInt64) -> Int

    Number of differing bits between two 64-bit hashes (0..64). As a rule of thumb for aHash/dHash: <= 5 means "very likely the same image",
    = 20 means "clearly different".

    hsl_to_rgb

    fn hsl_to_rgb(h : Double, s : Double, l : Double) -> (Int, Int, Int)

    Converts HSL (hue in degrees, saturation/lightness in [0, 1]) back to 8-bit RGB using the standard chroma/sector decomposition.

    hsv_to_rgb

    fn hsv_to_rgb(h : Double, s : Double, v : Double) -> (Int, Int, Int)

    Converts HSV (hue in degrees, saturation/value in [0, 1]) back to 8-bit RGB using the standard sector decomposition.

    image_metadata

    fn image_metadata(data : Array[Byte]) -> ImageMetadata?

    Parses dimensions for supported headers. Returns None for truncated or valid-but-unhandled variants whose dimensions are not present in the probe.

    jpeg_decode

    fn jpeg_decode(data : Array[Byte]) -> Image?

    Decodes a baseline JPEG into RGBA8 pixels.

    jpeg_encode

    fn jpeg_encode(image : Image, quality : Int) -> Array[Byte]

    Encodes an RGBA8 image as JPEG. quality is clamped by the external codec.

    png_decode

    fn png_decode(data : Array[Byte]) -> Image?

    Decodes an 8-bit PNG (color type 6 RGBA, 2 RGB, 0 grayscale, 4 grayscale+alpha, or 3 indexed palette, no interlacing). Verifies the signature, every chunk CRC and the zlib checksum; understands all five row filters. Returns None on anything malformed or unsupported.

    png_encode

    fn png_encode(img : Image) -> Array[Byte]

    Encodes an image as an 8-bit RGBA non-interlaced PNG. Each row chooses the filter with the smallest sum of absolute signed residuals, then the filtered scanlines are written as one fixed-Huffman DEFLATE block.

    qoi_decode

    fn qoi_decode(data : Array[Byte]) -> Image?

    Decodes a QOI byte stream. Returns None on malformed input (bad magic, zero dimensions, truncated stream or unsupported channel count).

    qoi_encode

    fn qoi_encode(img : Image) -> Array[Byte]

    Encodes an image as a QOI byte stream (channels = 4, sRGB colorspace tag).

    rgb_to_hsl

    fn rgb_to_hsl(r : Int, g : Int, b : Int) -> (Double, Double, Double)

    Converts 8-bit RGB to HSL: hue in degrees [0, 360), saturation and lightness in [0, 1].

    rgb_to_hsv

    fn rgb_to_hsv(r : Int, g : Int, b : Int) -> (Double, Double, Double)

    Converts 8-bit RGB to HSV: hue in degrees [0, 360), saturation and value in [0, 1].

    rgb_to_ycbcr

    fn rgb_to_ycbcr(r : Int, g : Int, b : Int) -> (Int, Int, Int)

    Converts 8-bit RGB to BT.601 full-range YCbCr (all components 0..255).

    tiff_decode

    fn tiff_decode(data : Array[Byte]) -> Image?
    Decodes a classic uncompressed baseline TIFF into RGBA8 pixels.

    webp_decode

    fn webp_decode(data : Array[Byte]) -> Image?
    Decodes a RIFF/WEBP lossless VP8L image into RGBA8 pixels.

    Pure MoonBit decoding supports the VP8L header, Huffman trees, LZ77, color cache, predictor, cross-color, subtract-green, and color-indexing transforms.

    webp_encode

    fn webp_encode(image : Image) -> Array[Byte]

    Encodes an RGBA8 image as lossless WebP.

    ycbcr_to_rgb

    fn ycbcr_to_rgb(y : Int, cb : Int, cr : Int) -> (Int, Int, Int)

    Converts BT.601 full-range YCbCr back to 8-bit RGB.