Open Source

Flutter package

flutter_bicubic_resize

The fastest image resize, crop and compress for Flutter — pure native C, identical results on every platform.

v1.8.223.5k / month45 likesMITAndroid · iOS · macOS3-4x faster

Advanced usage

ML preprocessing with tensor normalization, the crop system, edge modes, EXIF handling and error codes.

source: README.md

Advanced

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.

  1. 1Decode — JPEG/PNG to raw pixels
  2. 2EXIF — correct orientation (optional)
  3. 3Crop — flexible anchor and aspect ratio
  4. 4Resize — bicubic interpolation
  5. 5Normalize — scale and shift values
  6. 6Layout — HWC or CHW format
  7. 7Channel order — RGB or BGR
TypeFormulaOutput rangeUse case
nonepixel[0, 255]Raw values, backward compatible
simplepixel / 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) / stdvariesCustom 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.

ImageNet (TFLite)dart
// For TensorFlow Lite / ImageNet models
final Float32List tensor = BicubicResizer.resizeForModel(
  bytes: imageBytes,
  outputWidth: 224,
  outputHeight: 224,
  normalization: NormalizationType.imageNet,
);
Centered (MobileNet)dart
// For MobileNet (centered normalization)
final tensor = BicubicResizer.resizeForModel(
  bytes: imageBytes,
  outputWidth: 224,
  outputHeight: 224,
  normalization: NormalizationType.centered,  // [-1, 1]
);
CHW layout (PyTorch)dart
// For PyTorch (CHW layout)
final tensor = BicubicResizer.resizeForModel(
  bytes: imageBytes,
  outputWidth: 224,
  outputHeight: 224,
  normalization: NormalizationType.imageNet,
  layout: TensorLayout.chw,
);
Custom mean/stddart
// 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:

End-to-end with tflite_flutterdart
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.0 crops a 1080×1080 square; crop: 0.8 crops 864×864 (80% of the min dimension).
  • Original, for 1920×1080: crop: 1.0 uses the full 1920×1080; crop: 0.8 crops 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:

CropAnchor positions
┌─────────────────┐
│ TL    TC    TR  │
│                 │
│ CL  CENTER  CR  │
│                 │
│ BL    BC    BR  │
└─────────────────┘
Custom 3:4 portrait cropdart
// 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.

ModeDescriptionVisual effect
clampRepeat edge pixels[A B C D D D] — extends the last pixel
wrapWrap around (tile)[A B C D A B] — image repeats
reflectMirror reflection[A B C D C B] — mirror at the edge
zeroBlack / transparent[A B C D 0 0] — black pixels outside
dart
// 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.

ValueTransformation
1Normal (no transformation)
2Flip horizontal
3Rotate 180 degrees
4Flip vertical
5Transpose (rotate 90° CW + flip horizontal)
6Rotate 90 degrees clockwise
7Transverse (rotate 90° CCW + flip horizontal)
8Rotate 90 degrees counter-clockwise
dart
// 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.

dart
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-8

Advanced

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.

dart
// 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.

ExceptionCause
BicubicResizeExceptionNative resize operation failed (carries a specific error code)
ArgumentErrorInput size mismatch for raw pixel methods, or zero std values in custom normalization
UnsupportedImageFormatExceptionUnsupported image format (not JPEG or PNG) in resize() / resizeForModel()
dart
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)"
}
ErrorCodeCause
nullInput-1Null pointer passed to native function
invalidDims-2Width, height, or size <= 0
decodeFailed-3Corrupt or unsupported image data
allocFailed-4Memory allocation failed
encodeFailed-5JPEG/PNG encoding failed
formatUnknown-6Unknown or unsupported image format
dart
// Get the enum value from a native error code
final error = BicubicNativeError.fromCode(-3); // BicubicNativeError.decodeFailed
ArgumentError on invalid stddart
// 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.

Android 15 — 1.3.1

16KB page-size alignment via -Wl,-z,max-page-size=16384 so the native library loads on Android 15 devices.

iOS Swift Package Manager — 1.6.0

Ships Package.swift, adding Swift Package Manager support and resolving the "does not support Swift Package Manager" warning.

macOS support — 1.7.0

Adds macOS and fixes the 'Failed to lookup symbol bicubic_resize_rgb' error; Package.swift declares .macOS(10.15).

Android compileSdk 36 — 1.6.0

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.

Codigee
We are using cookies. Learn more