Flutter package
flutter_bicubic_resize
The fastest image resize, crop and compress for Flutter — pure native C, identical results on every platform.
Advanced usage
ML preprocessing with tensor normalization, the crop system, edge modes, EXIF handling and error codes.
source: README.mdAdvanced
ML preprocessing
resizeForModel() runs a complete preprocessing pipeline and returns a Float32List tensor. Because it shares the same Catmull-Rom interpolation as OpenCV and PIL, the tensors match a Python training pipeline byte-for-byte.
- 1Decode — JPEG/PNG to raw pixels
- 2EXIF — correct orientation (optional)
- 3Crop — flexible anchor and aspect ratio
- 4Resize — bicubic interpolation
- 5Normalize — scale and shift values
- 6Layout — HWC or CHW format
- 7Channel order — RGB or BGR
| Type | Formula | Output range | Use case |
|---|---|---|---|
| none | pixel | [0, 255] | Raw values, backward compatible |
| simple | pixel / 255 | [0, 1] | TensorFlow Lite, basic models |
| centered | (pixel / 127.5) - 1 | [-1, 1] | MobileNet, some TFLite models |
| imageNet | (pixel/255 - mean) / std | ~[-2.5, 2.5] | ResNet, VGG, EfficientNet |
| custom | (pixel/255 - mean) / std | varies | Custom trained models |
ImageNet uses mean [0.485, 0.456, 0.406] and std [0.229, 0.224, 0.225]. Normalization is optimized with pre-computed scale and offset factors — output = pixel * scale + offset — with no divisions in the hot loop and a single pass through all pixels.
// For TensorFlow Lite / ImageNet models
final Float32List tensor = BicubicResizer.resizeForModel(
bytes: imageBytes,
outputWidth: 224,
outputHeight: 224,
normalization: NormalizationType.imageNet,
);// For MobileNet (centered normalization)
final tensor = BicubicResizer.resizeForModel(
bytes: imageBytes,
outputWidth: 224,
outputHeight: 224,
normalization: NormalizationType.centered, // [-1, 1]
);// For PyTorch (CHW layout)
final tensor = BicubicResizer.resizeForModel(
bytes: imageBytes,
outputWidth: 224,
outputHeight: 224,
normalization: NormalizationType.imageNet,
layout: TensorLayout.chw,
);// Custom normalization
final tensor = BicubicResizer.resizeForModel(
bytes: imageBytes,
outputWidth: 224,
outputHeight: 224,
normalization: NormalizationType.custom,
meanR: 0.5, meanG: 0.5, meanB: 0.5,
stdR: 0.5, stdG: 0.5, stdB: 0.5,
);Channel order (rgb for TensorFlow, bgr for OpenCV/PyTorch) and layout (hwc interleaved for TensorFlow, chw planar for PyTorch/ONNX) let the same call feed any framework. A full TFLite inference pipeline:
import 'package:flutter_bicubic_resize/flutter_bicubic_resize.dart';
import 'package:tflite_flutter/tflite_flutter.dart';
// Load model
final interpreter = await Interpreter.fromAsset('model.tflite');
// Preprocess image
final Float32List tensor = BicubicResizer.resizeForModel(
bytes: imageBytes,
outputWidth: 224,
outputHeight: 224,
normalization: NormalizationType.imageNet,
layout: TensorLayout.hwc,
);
// Run inference
final input = tensor.reshape([1, 224, 224, 3]);
final output = List.filled(1000, 0.0).reshape([1, 1000]);
interpreter.run(input, output);Advanced
Crop system
Cropping is controlled by three parameters — crop, cropAnchor and cropAspectRatio. The crop factor (0.0-1.0) decides how much of the image to keep: 1.0 uses the full image, 0.8 keeps 80%, 0.5 keeps 50%.
- Square (default), for a 1920×1080 image:
crop: 1.0crops a 1080×1080 square;crop: 0.8crops 864×864 (80% of the min dimension). - Original, for 1920×1080:
crop: 1.0uses the full 1920×1080;crop: 0.8crops 1536×864 (80% of both dimensions). - Custom crops to the specified aspect ratio that fits within the source.
The anchor decides where the kept region sits inside the source:
┌─────────────────┐
│ TL TC TR │
│ │
│ CL CENTER CR │
│ │
│ BL BC BR │
└─────────────────┘// Portrait crop: top portion
final portrait = BicubicResizer.resizeJpeg(
jpegBytes: photoBytes,
outputWidth: 300,
outputHeight: 400,
crop: 0.9,
cropAnchor: CropAnchor.topCenter,
cropAspectRatio: CropAspectRatio.custom,
aspectRatioWidth: 3.0,
aspectRatioHeight: 4.0,
);Advanced
Edge modes
The edgeMode parameter controls how the resampler treats pixels outside the image bounds.
| Mode | Description | Visual effect |
|---|---|---|
| clamp | Repeat edge pixels | [A B C D D D] — extends the last pixel |
| wrap | Wrap around (tile) | [A B C D A B] — image repeats |
| reflect | Mirror reflection | [A B C D C B] — mirror at the edge |
| zero | Black / transparent | [A B C D 0 0] — black pixels outside |
// Wrap mode - creates a tiled pattern
final tiled = BicubicResizer.resizeJpeg(
jpegBytes: textureBytes,
outputWidth: 512,
outputHeight: 512,
edgeMode: EdgeMode.wrap,
);Advanced
EXIF orientation
For JPEG images, the resize methods read and apply EXIF orientation metadata by default, so photos taken on mobile devices display the right way up. Pass applyExifOrientation: false to work on the raw pixel orientation instead.
| Value | Transformation |
|---|---|
| 1 | Normal (no transformation) |
| 2 | Flip horizontal |
| 3 | Rotate 180 degrees |
| 4 | Flip vertical |
| 5 | Transpose (rotate 90° CW + flip horizontal) |
| 6 | Rotate 90 degrees clockwise |
| 7 | Transverse (rotate 90° CCW + flip horizontal) |
| 8 | Rotate 90 degrees counter-clockwise |
// Get raw pixel orientation (ignore EXIF)
final raw = BicubicResizer.resizeJpeg(
jpegBytes: photoBytes,
outputWidth: 224,
outputHeight: 224,
applyExifOrientation: false,
);Advanced
Reading image metadata
getImageInfo() reads width, height, channels, format and EXIF orientation without decoding pixels — useful for deciding how to resize before doing the work. For orientations 5-8 the oriented width and height are swapped relative to the raw dimensions.
final info = BicubicResizer.getImageInfo(imageBytes);
print('${info.width}x${info.height}'); // Raw dimensions
print('${info.orientedWidth}x${info.orientedHeight}'); // After EXIF rotation
print('Format: ${info.format}'); // ImageFormat.jpeg or .png
print('Channels: ${info.channels}'); // 1, 3, or 4
print('EXIF: ${info.exifOrientation}'); // 1-8Advanced
Async & isolates
Every public method has an *Async() variant that offloads the work to a separate isolate via Isolate.run(), keeping the UI thread free while accepting the same parameters as the sync call.
// Non-blocking JPEG resize
final resized = await BicubicResizer.resizeJpegAsync(
jpegBytes: originalBytes,
outputWidth: 224,
outputHeight: 224,
);Good to know
Async methods use Isolate.run() (requires Dart 2.19+, satisfied by the SDK constraint), and the FFI native library is loaded automatically per isolate. Sync methods stay available and are recommended when latency is not a concern — the native C code is extremely fast.
- Use async methods for UI responsiveness — every method has an *Async() variant that runs in a separate isolate.
- Use sync methods for maximum throughput — the native C code is extremely fast (~15-30ms for 4K to 224×224).
- Memory efficiency — the whole decode → resize → encode pipeline runs in native code, minimizing overhead.
- PNG compression trade-off — a higher compressionLevel (closer to 9) is smaller but slower; use 6 for balance.
- JPEG quality 85-95 balances file size and visual quality; use 95+ for archival or quality-critical output.
Advanced
Error handling
Methods throw specific exceptions on failure. BicubicResizeException implements Exception, so existing catch (e) blocks keep working while giving you a typed error code and message.
| Exception | Cause |
|---|---|
| BicubicResizeException | Native resize operation failed (carries a specific error code) |
| ArgumentError | Input size mismatch for raw pixel methods, or zero std values in custom normalization |
| UnsupportedImageFormatException | Unsupported image format (not JPEG or PNG) in resize() / resizeForModel() |
try {
final resized = BicubicResizer.resizeJpeg(
jpegBytes: corruptBytes,
outputWidth: 224,
outputHeight: 224,
);
} on BicubicResizeException catch (e) {
print(e.error); // BicubicNativeError.decodeFailed
print(e.nativeCode); // -3
print(e.message); // "Image decoding failed (corrupt or unsupported data)"
}| Error | Code | Cause |
|---|---|---|
| nullInput | -1 | Null pointer passed to native function |
| invalidDims | -2 | Width, height, or size <= 0 |
| decodeFailed | -3 | Corrupt or unsupported image data |
| allocFailed | -4 | Memory allocation failed |
| encodeFailed | -5 | JPEG/PNG encoding failed |
| formatUnknown | -6 | Unknown or unsupported image format |
// Get the enum value from a native error code
final error = BicubicNativeError.fromCode(-3); // BicubicNativeError.decodeFailed// Zero std in custom normalization
try {
BicubicResizer.resizeForModel(
bytes: imageBytes,
outputWidth: 224,
outputHeight: 224,
normalization: NormalizationType.custom,
stdR: 0.0, // invalid
);
} on ArgumentError catch (e) {
print(e.name); // "stdR"
print(e.message); // "must not be zero"
}Advanced
Platform notes
Recent releases fixed a handful of platform-specific build issues worth knowing about when you upgrade.
16KB page-size alignment via -Wl,-z,max-page-size=16384 so the native library loads on Android 15 devices.
Ships Package.swift, adding Swift Package Manager support and resolving the "does not support Swift Package Manager" warning.
Adds macOS and fixes the 'Failed to lookup symbol bicubic_resize_rgb' error; Package.swift declares .macOS(10.15).
Android compileSdk raised to 36.
Let's make something together.
If you have any questions about a new project or other inquiries, feel free to contact us. We will get back to you as soon as possible.
