Imcodec package illustration

Imcodec

Imcodec is a focused Flutter image codec for BMP, GIF, JPEG, JPEG XL, OpenEXR, PNG, QOI, TGA, TIFF, and WebP. It keeps a small straight-alpha RGBA image model and exposes synchronous pure-Dart encoders and decoders, which makes expensive conversions suitable for Isolate.run.

Note

Animated GIF, JPEG XL, PNG, and WebP inputs currently return their first visible frame. Decoding preserves straight alpha and hidden RGB values where the source format carries them.

Usage

import 'dart:isolate';
import 'dart:typed_data';

import 'package:imcodec/imcodec.dart' as img;

Future<Uint8List> exportWebP(
  Uint8List straightRgba,
  int width,
  int height,
) => Isolate.run(() {
  final img.Image image = img.Image.fromBytes(
    width: width,
    height: height,
    bytes: straightRgba.buffer,
    bytesOffset: straightRgba.offsetInBytes,
    numChannels: 4,
    order: img.ChannelOrder.rgba,
  );
  return img.encodeWebP(image);
});

The encoder entry points mirror the subset used by almost any image editor:

final Uint8List png = img.encodePng(image);
final Uint8List gif = img.encodeGif(
  image,
  options: const img.GifEncodeOptions(
    colorCount: 64,
    ditherAmount: 75,
  ),
); // one palette-indexed frame
final Uint8List jpeg = img.encodeJpg(
  image,
  options: const img.JpegEncodeOptions(quality: 90),
);
final Uint8List jpegXl = img.encodeJpegXl(image); // lossless Modular
final Uint8List quickJxl = img.encodeJpegXl(
  image,
  options: const img.JpegXlEncodeOptions(effort: img.JpegXlEffort.fast),
);
final Uint8List openExr = img.encodeOpenExr(image); // scene-linear half float
final Uint8List webp = img.encodeWebP(image); // lossless VP8L
final Uint8List lossyWebP = img.encodeWebP(
  image,
  options: const img.WebPEncodeOptions(quality: 82),
);
final Uint8List bmp = img.encodeBmp(image);
final Uint8List tga = img.encodeTga(image); // RLE by default
final Uint8List qoi = img.encodeQoi(image);
final Uint8List tiff = img.encodeTiff(image); // PackBits by default

Decode supported data with format detection or a format-specific function:

final img.Image decoded = img.decodeImage(encodedBytes);
final img.Image png = img.decodePng(
  pngBytes,
  options: const img.PngDecodeOptions(maxPixels: 25_000_000),
);

Editors that must retain authored precision or process channels can use the metadata-aware API. It keeps PNG/TIFF 16-bit samples, TIFF/OpenEXR float32 samples, CMYK JPEG/TIFF channels, and embedded ICC payloads without changing the small RGBA8 Image API used by existing callers:

final img.DecodedImageMetadata? metadata = img.inspectImage(encodedBytes);
if (metadata?.requiresExactDecoding ?? false) {
  final img.DecodedImage decoded = img.decodeImageData(encodedBytes);
  // decoded.bytes contains straight RGB+A or CMYK+A samples in native depth.
}

Unsigned 16-bit and float32 samples in DecodedImage.bytes are little-endian. inspectImage bounds ICC decompression through maxIccProfileBytes. It also retains opaque EXIF, IPTC-IIM, and XMP packets where their container has a standard representation: EXIF and XMP in PNG and WebP, EXIF/IPTC/XMP in JPEG, and IPTC/XMP in TIFF. Each descriptive packet is copied into immutable bytes and bounded independently through maxDescriptiveMetadataBytes; Imcodec does not reinterpret or rewrite the packet contents. OpenEXR decoding additionally bounds its floating-point output and expanded scan-line working blocks through maxDecodedBytes. Applications with an existing extended-sRGB float buffer can call encodeOpenExrFloat32Rgba to retain values above display white.

Spreading encoding across isolates

encodeWith is part of RasterCodec and RasterEncoder, so every codec accepts a runner and encodeImageWith dispatches on format just like encodeImage. Use runSequentially to run every task on the current isolate, onIsolates to run one isolate per job, or onBoundedIsolates to cap the number of concurrent isolates:

final Uint8List jpeg = await img.encodeJpgWith(
  img.onBoundedIsolates,
  image,
  options: const img.JpegEncodeOptions(quality: 90),
);

JPEG transforms MCU bands independently, JPEG XL spreads its modular groups and context work, PNG filters row bands independently, and lossless WebP selects and applies predictor-block bands independently. Lossy WebP currently encodes inline. JPEG, PNG, and WebP keep small images inline because isolate startup and byte transfer would cost more than the work saved. Their parallel output is byte-for-byte identical to synchronous output.

BMP, GIF, TGA, and TIFF are dominated by inexpensive byte shuffling, quantization, or run-length passes, while QOI carries state from every pixel to the next. Measurements show that moving their buffers between isolates is slower, so these formats accept a runner for API consistency but intentionally encode inline.

decodeImage defaults to a 100-million-pixel allocation limit. Supply a typed decode-options object to a format-specific helper when a lower limit is needed.

maxPixels alone no longer bounds memory once samples are kept natively: a CMYK float32 pixel needs 20 bytes where an RGBA8 pixel needs 4. The metadata-aware functions therefore also take maxDecodedBytes, which defaults to the 400 MB an RGBA8 image at RasterDecodeOptions.defaultMaxPixels would occupy, and is checked from container metadata before any pixel buffer is allocated.

The format classes can also be used through dart:convert:

const img.PngCodec codec = img.PngCodec();
final Uint8List encoded = codec.encoder.convert(
  image,
  encodeOptions: const img.PngEncodeOptions(level: 7),
);
final img.Image decoded = codec.decode(
  encoded,
  decodeOptions: const img.PngDecodeOptions(maxPixels: 25_000_000),
);

Each RasterCodec composes an immutable RasterEncoder and RasterDecoder. Compression and allocation choices are immutable per-operation options. The shared RasterDecodeOptions.defaultMaxPixels value (100 million) is used unless a lower limit is supplied to a decoding helper.

Format behavior

A documentation on the behavior and implementation of formats in available in docs/formats.md.

The JPEG and WebP encoder implementations contain code derived from the MIT licensed Dart image package.

The JPEG XL implementation is adapted from koni_jxl, released by Jonathan Urzúa under the MIT License. Its decoding logic includes work derived from the MIT licensed JXLatte project.


Built for Focale, an advanced local image editor. Discover what these packages make possible in a real creative workflow.

Libraries

imcodec
Focused raster image encoding and decoding for Flutter.