image_ffi

Native image decode, resize and encode for Dart, backed by Sean Barrett's stb single-file C libraries over FFI. The stb sources are compiled from source by a Dart build hook, so there is no prebuilt binary to ship and nothing to install beyond a C toolchain.
The pure-Dart image package is the right
choice when you need its wide manipulation and filter suite. image_ffi is
narrower on purpose: it does decode, high-quality resize, JPEG/PNG encode and
one-call thumbnails, and it runs those in native code. If your workload is
"read an image, make a thumbnail, write it back", this is several times faster.
For cropping, drawing, filters, format conversions and animation, use image.
Formats
- Decode: PNG, JPEG, BMP, GIF, PSD, TGA, HDR, PIC (whatever stb_image reads).
- Encode: JPEG and PNG.
Install
dart pub add image_ffi
Building the native library needs a C toolchain and Dart's native build hooks enabled (see Platforms).
Quick start
import 'dart:io';
import 'package:image_ffi/image_ffi.dart';
void main() {
final bytes = File('photo.jpg').readAsBytesSync();
// Dimensions without decoding the pixels.
final info = imageInfo(bytes);
print('${info.width}x${info.height}, ${info.channels} channels');
// Decode to raw pixels (native channel count, or force one).
final image = decodeImage(bytes, forceChannels: 4);
// High-quality, sRGB-correct resize.
final small = resizePixels(
image.pixels,
srcWidth: image.width,
srcHeight: image.height,
dstWidth: image.width ~/ 2,
dstHeight: image.height ~/ 2,
channels: image.channels,
);
// Encode back to JPEG or PNG.
File('half.jpg').writeAsBytesSync(
encodeJpeg(small, width: image.width ~/ 2, height: image.height ~/ 2,
channels: 4, quality: 90),
);
// Or do decode, downscale and encode in one call.
File('thumb.jpg').writeAsBytesSync(thumbnailJpeg(bytes, maxDimension: 256));
}
Decode and encode copy the native bytes into a Dart Uint8List and free the
native buffer before returning, so you never manage native memory. Invalid
input throws ImageFfiException with stb's own failure reason; bad arguments
throw ArgumentError.
Benchmark
Decode a 2000x2000 PNG and downscale it to a 256px thumbnail, image_ffi
against the pure-Dart image package doing the same work. Medians of 15 runs on
an Apple M-series laptop:

| Operation | image_ffi | image | Speedup |
|---|---|---|---|
| decode PNG | 21.9 ms | 94.5 ms | 4.3x |
| resize to 256px | 4.8 ms | 44.0 ms | 9.3x |
| decode + resize + JPEG encode | 27.3 ms | 141.7 ms | 5.2x |
The resize row uses cubic interpolation for the image package so both sides do
a comparable high-quality filter. The image package's default nearest-neighbor
resize is faster than either (about 0.4 ms here) but much lower quality, so it is
not a like-for-like comparison. Numbers are machine-dependent; reproduce them
with dart run bench/bench.dart.
API
decodeImage(bytes, {forceChannels})returns aDecodedImagewithwidth,height,channelsand row-majorpixels.imageInfo(bytes)returns(width, height, channels)from the header only.resizePixels(pixels, {srcWidth, srcHeight, dstWidth, dstHeight, channels, colorSpace}).colorSpaceisResizeColorSpace.srgbby default (right for photographic and UI images) or.linearfor masks and data pixels. Two-channel input is resampled as grayscale + alpha, four-channel as non-premultiplied RGBA, so edges against transparency stay clean.encodeJpeg(pixels, {width, height, channels, quality})andencodePng(pixels, {width, height, channels}).thumbnailJpeg(bytes, {maxDimension, quality})decodes, downscales so the longer side is at mostmaxDimension(never enlarging), and JPEG-encodes.thumbnailPng(bytes, {maxDimension})does the same but PNG-encodes, keeping the alpha channel a JPEG would drop.thumbnailJpegAsyncandthumbnailPngAsynctake the same arguments and return aFuture; see below.thumbnailJpegBatch(images, {maxDimension, quality, concurrency})andthumbnailPngBatch(images, {maxDimension, concurrency})thumbnail a whole folder off the main isolate while capping how many isolates run at once; see below.
Off the main isolate
thumbnailJpegAsync and thumbnailPngAsync run the whole decode, resize and
encode on a background isolate with Isolate.run, so a large image doesn't
block the isolate that called them:
final thumb = await thumbnailJpegAsync(bytes, maxDimension: 256);
This is the reason to reach for a native writer in a Flutter app: the pure-Dart
image package runs on the calling isolate and janks the UI while a big photo
is processed, and a plain synchronous FFI call does the same. The async variants
keep the UI isolate free. The input bytes are copied to the worker and the
result copied back; for a handful of images that copy is small next to the
decode. An ImageFfiException raised in the worker surfaces from the future.
A folder of images
Over a whole directory, do not reach for Future.wait on the async variants:
// Don't. This spawns one isolate per image at once, each holding a full
// decoded buffer, so a real folder can run the process out of memory.
final thumbs = await Future.wait(images.map(thumbnailJpegAsync));
Use thumbnailJpegBatch (or thumbnailPngBatch) instead. It runs the same
per-image work off the main isolate but caps how many isolates are live at
once, to concurrency, which defaults to Platform.numberOfProcessors:
await for (final thumb in thumbnailJpegBatch(images, concurrency: 4)) {
// write thumb
}
maxDimension and quality mean what they do on thumbnailJpeg. Each
thumbnail is emitted as it finishes, so results arrive in completion order, not
the order of images; pair a result with its source before the call if you
need the correspondence. A failure on one image surfaces as an error on the
stream while the rest keep going.
How it works

Platforms
The native library is compiled from the vendored stb sources by
hook/build.dart using Dart's native build hooks. It works anywhere Dart runs a
C compiler for the target: Linux, macOS and Windows on the Dart CLI and server,
and in Flutter apps whose build has native assets enabled, where the same hook
runs and the async variants keep image work off the UI isolate. Requires Dart
3.9 or later.
The CI matrix builds and tests on Ubuntu, macOS and Windows.
Credits
The image codecs and resampler are Sean Barrett's
stb libraries (stb_image,
stb_image_write, stb_image_resize2), released into the public domain. Their
vendored copies live in src/third_party/stb. See LICENSE for details.
Libraries
- image_ffi
- Native image decode, resize and encode for Dart, backed by Sean Barrett's stb single-file libraries over FFI.