Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lua_image

中文文档 · English

lua_image is a Lua C module (built with Rust) binding the image crate: multi-format image decoding, encoding and processing for Lua.

Unlike the two earlier attempts in the ecosystem (mlua-image, abandoned and built on a vendored Lua VM; maki's image module, coupled to its plugin host), this module links the host Lua ABI (module mode, never a second VM), guards decoding against pixel-bomb attacks, and ships with tests and CI following the lua-stb standard.

Features

  • decode / probe / open — PNG, JPEG, GIF, WebP, BMP, TIFF, PNM, QOI, TGA, ICO (from bytes or file)
  • encode — PNG, JPEG, WebP (lossless), GIF, BMP, TIFF, PNM, QOI, TGA, ICO
  • processresize (nearest/triangle/catmullrom/gaussian/lanczos3), crop, rotate (90/180/270), flip_horizontal / flip_vertical
  • raw pixelsto_rgba / to_rgb return bytes, width, height (same convention as lua_webp's forced colorspaces)
  • pixel-bomb guard — images declaring more than 50 MP are rejected before any allocation

Requirements

  • Rust (stable, 1.88+) — Windows additionally needs the x86_64-pc-windows-gnu toolchain
  • Lua 5.4 (dev headers + pkg-config; overridable via LUA_LIB/LUA_LIB_NAME)

Install examples:

# Debian / Ubuntu
sudo apt-get install lua5.4 liblua5.4-dev pkg-config

# macOS (Homebrew)
brew install lua@5.4 pkgconf

# Windows (MSYS2 / UCRT64) + rustup GNU toolchain
rustup toolchain install stable-x86_64-pc-windows-gnu
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-make \
  mingw-w64-ucrt-x86_64-pkgconf mingw-w64-ucrt-x86_64-lua54

Build & Test

make build       # cargo build --release + module symlink
make test        # regenerate fixtures + run tests/test.lua
make test-rust   # cargo test: Rust unit tests + the full Lua suite
make mutants     # mutation testing (needs `cargo install cargo-mutants`)

The Lua suite runs inside cargo test too (tests/lua_tests.rs loads the built cdylib through the raw Lua C API), which is what makes make mutants meaningful: every injected mutant is exercised against the full Lua assertion set, so a surviving mutant is a genuine coverage gap, not unreached code.

Test suite

tests/test.lua (~150 assertions) covers:

  • probe/decode/open: format/width/height detection, pixel-exact to_rgba/to_rgb/to_luma output, color-type variants (L8/LA8/RGB8/ RGBA8/16-bit), odd dimensions, 1×1, non-square shapes
  • encode: PNG/JPEG/WebP(lossless)/GIF/BMP/QOI/TGA output, jpeg quality and png compression parameters (strict validation), alpha-drop on jpeg
  • transforms: resize (fit vs resize_exact), thumbnail aspect preservation, crop bounds, rotate 90/180/270, flip, color ops (grayscale/invert/brighten/contrast/huerotate/blur)
  • metamorphic invariants: flip/rotate identity (pixel-exact), full-crop identity, lossless re-encode round-trips (png/webp/tiff/bmp/qoi/pnm pixel-exact), resize dimension round-trip
  • guards/errors: 50 MP output cap (resize/thumbnail), pixel-bomb rejection before allocation, out-of-bounds crop/pixel access, save round-trips, garbage/corrupt input

CI (GitHub Actions)

.github/workflows/ci.yml runs on every push/PR:

Job Runner Steps
linux ubuntu-latest make buildmake testmake test-rust
macos macos-latest same (Homebrew lua@5.4 via PKG_CONFIG_PATH/LUA_BIN)
windows windows-latest MSYS2 UCRT64 + GNU Rust toolchain → mingw32-make build/test/test-rust

All jobs build the same source; the Windows job verifies the GNU linker path (MinGW Lua ABI). Rust unit tests (pixel-bomb guard, 50 MP boundary, round-trips) run under cargo test too.

Usage

local image = require "lua_image"

-- probe header-only (no pixel decode)
local info = image.probe(raw_bytes)      -- { format="png", width=4, height=4 }

-- decode from bytes or open from a file
local img = image.decode(raw_bytes)
local img = image.open("photo.jpg")

-- inspect and transform
print(img:width(), img:height(), img:format())
local thumb = img:resize(256, 256, "lanczos3")
local cropped = img:crop(10, 10, 200, 200)
local rotated = img:rotate(90)

-- encode to bytes (or save to a file)
local png = img:encode("png")
local webp = img:encode("webp")          -- lossless
img:save("out.png")
thumb:save("thumb.jpg", "jpeg")

-- raw pixels, tightly packed, with dimensions
local rgba, w, h = img:to_rgba()
local rgb, w, h = img:to_rgb()

API

Function Returns
image.probe(data) { format, width, height }
image.decode(data) Image userdata
image.open(path) Image userdata
img:width() / img:height() / img:format() integers / format string
img:color_type() / img:has_alpha() "rgb8" etc. / boolean
img:encode(fmt, params?) encoded bytes (string)
img:save(path, fmt?) — (fmt inferred from extension if omitted)
img:resize(w, h, filter?) new Image (aspect preserved, fits inside w×h)
img:resize_exact(w, h, filter?) new Image (exact dimensions)
img:thumbnail(w, h) new Image (aspect preserved)
img:crop(x, y, w, h) new Image (RGBA)
img:rotate(deg) new Image (90/180/270)
img:flip_horizontal() / img:flip_vertical() new Image
img:grayscale() / img:invert() new Image
img:brighten(factor) / img:contrast(c) new Image
img:huerotate(deg) / img:blur(sigma) new Image
img:get_pixel(x, y) r, g, b, a
img:set_pixel(x, y, r, g, b, a?) r, g, b, a (modifies in place)
img:to_rgba() / img:to_rgb() / img:to_luma() bytes, width, height
  • filter is one of "nearest", "triangle", "catmullrom", "gaussian", "lanczos3" (default "triangle").
  • fmt is one of "png", "jpeg"/"jpg", "webp", "gif", "bmp", "tiff", "pnm"/"ppm"/"pgm"/"pam", "qoi", "tga", "ico".
  • params (encode): { quality = 0-100 } for jpeg (default 90), { compression = "default"|"fast"|"best"|"uncompressed" } for png. A parameter that does not apply to the chosen format, or an unknown key, raises an error.
  • All failures raise Lua errors prefixed with lua_image:.

Known limitations

  • WebP encoding is lossless-only (the image crate has no lossy WebP encoder); for lossy WebP use lua_webp.
  • JPEG encoding drops alpha (RGBA → RGB); encode back to PNG to keep it.
  • Animated GIF/WebP decode the first frame only (no warning).
  • resize keeps the aspect ratio (fits inside w×h); use resize_exact for exact dimensions.
  • crop/flip_* always produce RGBA8; other transforms keep the source color/bit depth. to_rgba/to_rgb/to_luma silently truncate 16-bit sources to 8-bit.
  • All transforms enforce the same 50 MP output cap as decode; a request beyond it raises an error instead of OOM-aborting the host.
  • Decoding and transforms run synchronously on the Lua thread; very large images block the caller.
  • Lua string ↔ pixel data transfer copies the bytes.

License

MIT. See LICENSE.

About

Lua bindings for the Rust image crate (multi-format decode/encode/process)

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages