A Flutter plugin that compresses images using native code (Kotlin on Android, Objective-C/Swift on Apple platforms). Supported platforms: Android, iOS, macOS, Web, OpenHarmony.
Table of contents
Dart-only image libraries exist, but in practice they are too slow for typical compression workloads — even in release builds, and even when moved to an Isolate. Delegating to platform-native encoders is dramatically faster.
| Feature | Android | iOS | Web | macOS | OpenHarmony |
|---|---|---|---|---|---|
compressWithList |
✅ | ✅ | ✅ | ✅ | ✅ |
compressAssetImage |
✅ | ✅ | ✅ | ✅ | ✅ |
compressWithFile |
✅ | ✅ | ❌ | ✅ | ✅ |
compressAndGetFile |
✅ | ✅ | ❌ | ✅ | ✅ |
| Format: jpeg | ✅ | ✅ | ✅ | ✅ | ✅ |
| Format: png | ✅ | ✅ | ✅ | ✅ | ✅ |
| Format: webp | ✅ | ✅ | 🌐 | ❌ | ✅ |
| Format: heic | ✅ | ✅ | ❌ | ✅ | ✅ |
Param: quality |
✅ | ✅ | 🌐 | ✅ | ✅ |
Param: rotate |
✅ | ✅ | ❌ | ✅ | ✅ |
Param: keepExif |
✅ | ✅ | ❌ | ✅ | ❌ |
Add the latest version to your pubspec.yaml:
dependencies:
flutter_image_compress: <latest_version>Or run:
flutter pub add flutter_image_compressImport it in your Dart code:
import 'package:flutter_image_compress/flutter_image_compress.dart';The plugin exposes four entry points depending on your input and desired output. See the full example for a runnable version.
// 1. Compress a file, get bytes back.
Future<Uint8List?> testCompressFile(File file) async {
final result = await FlutterImageCompress.compressWithFile(
file.absolute.path,
minWidth: 2300,
minHeight: 1500,
quality: 94,
rotate: 90,
);
print(file.lengthSync());
if (result != null) {
print(result.length);
}
return result;
}
// 2. Compress a file, get a file back.
Future<File?> testCompressAndGetFile(File file, String targetPath) async {
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
targetPath,
quality: 88,
rotate: 180,
);
print(file.lengthSync());
if (result != null) {
print(await result.length());
return File(result.path);
}
return result;
}
// 3. Compress an asset image, get bytes back.
Future<Uint8List?> testCompressAsset(String assetName) async {
return FlutterImageCompress.compressAssetImage(
assetName,
minHeight: 1920,
minWidth: 1080,
quality: 96,
rotate: 180,
);
}
// 4. Compress bytes, get bytes back.
Future<Uint8List> testCompressList(Uint8List list) async {
final result = await FlutterImageCompress.compressWithList(
list,
minHeight: 1920,
minWidth: 1080,
quality: 96,
rotate: 135,
);
print(list.length);
print(result.length);
return result;
}minWidth and minHeight bound the output size while preserving the source aspect ratio. See the FAQ entry below for why the names are misleading.
Given a 4000×2000 image with minWidth: 1920, minHeight: 1080, the scale is computed like this:
// Illustrative Dart port of the native logic.
import 'dart:math' as math;
void main() {
final scale = calcScale(
srcWidth: 4000,
srcHeight: 2000,
minWidth: 1920,
minHeight: 1080,
);
print('scale = $scale'); // 1.8518518518518519
print('target = ${4000 / scale} × ${2000 / scale}'); // 2160.0 × 1080.0
}
double calcScale({
required double srcWidth,
required double srcHeight,
required double minWidth,
required double minHeight,
}) {
final scaleW = srcWidth / minWidth;
final scaleH = srcHeight / minHeight;
return math.max(1.0, math.min(scaleW, scaleH));
}If the source is already smaller than minWidth or minHeight, the scale is clamped to 1 — the image is never upscaled.
Rotates the output by the given number of degrees. Set to 0 to skip rotation.
Available since 0.5.0. When true (the default), the plugin reads the source's EXIF orientation and rotates so the output is upright.
If you also pass a non-zero rotate value, the two can compound. To avoid double rotation, either set rotate: 0 or autoCorrectionAngle: false.
Target quality, 0–100. Ignored for PNG on iOS (PNG is lossless there).
Chosen via the CompressFormat enum. Defaults to JPEG. JPEG and PNG work everywhere; WebP and HEIC have platform caveats — see below.
- Android: uses the system encoder, which is fast.
- iOS: encoded via SDWebImageWebPCoder. Functional, but noticeably slower than the other formats. Swapping to Google's
libwebpdirectly would help but is not currently on the roadmap. - macOS: not supported.
- Web: depends on the browser's Canvas
toBlobimplementation; see the compatibility link in the table above.
- iOS: iOS 11+ only.
- Android: API 28+ only, and requires a working hardware encoder. Not every API 28+ device qualifies — always wrap the call in try/catch and fall back to JPEG on
UnsupportedError. Implemented on top ofHeifWriter.
Android only. Passed directly to BitmapFactory.Options; see the Android documentation for its exact semantics.
When true, source EXIF metadata is copied onto the compressed output. Defaults to false.
A few caveats apply regardless of platform or format:
-
The
Orientationtag is always normalized to1/ORIENTATION_NORMAL. The pipeline bakes any rotation into the pixels, so preserving the source orientation would cause viewers to rotate a second time. -
Support depends on both the output format and the platform:
Output format iOS / macOS Android JPEG ✅ Full sub-dict passthrough (EXIF, TIFF, GPS, IPTC, PNG) ✅ ~90 tags via androidx.exifinterfacePNG ✅ Same passthrough ✅ ~90 tags via androidx.exifinterfaceWebP ❌ ImageIO cannot author WebP metadata; output is a valid WebP with no EXIF ✅ ~90 tags via androidx.exifinterfaceHEIC ✅ Same passthrough ❌ androidx.exifinterfacerefuses HEIF writes; output is a valid HEIC with no EXIF.HeifHandlerlogs a warning. -
Unsupported combinations still return valid image bytes — you simply do not get EXIF back.
keepExif: truenever fails the compression call as a whole.
Known gaps (tracked in #130)
- iOS WebP +
keepExif: ImageIO cannot write metadata into a WebP container. A manual VP8X + EXIF chunk splice viaSDWebImageWebPCoderwould fix this but is not implemented. - Android HEIC +
keepExif:androidx.exifinterfacerefuses HEIF writes on every version. Manually injecting ISO/IEC 23008-12 metadata boxes (~400 LoC) would fix this but is not implemented. - Web / OpenHarmony
keepExif: the Canvas / packing pipelines strip metadata at encode time — no in-tree fix path exists.
APIs that return a List<int> never return null — they return an empty list when compression fails.
APIs that return a file may return null. The file may also be missing on disk even when a value is returned, so verify existence before using it.
You'll often need to convert List<int> to Uint8List to display the result:
final image = Uint8List.fromList(imageList);
final ImageProvider provider = MemoryImage(image);Import dart:typed_data (or flutter/foundation.dart) to get Uint8List:
Displaying the result in an Image widget:
Future<Widget> _compressImage() async {
final List<int> image = await testCompressFile(file);
final ImageProvider provider = MemoryImage(Uint8List.fromList(image));
return Image(image: provider);
}Writing the result to disk:
Future<void> writeToFile(List<int> image, String filePath) {
return File(filePath).writeAsBytes(image, flush: true);
}Neither quality (0–100, lossy) nor minWidth/minHeight (max output dimensions) maps linearly to output bytes. If you need to hit a specific size limit, iterate:
Future<Uint8List> compressToUnder(Uint8List src, int limitBytes) async {
var quality = 88;
var out = src;
while (quality > 10) {
out = await FlutterImageCompress.compressWithList(
src,
quality: quality,
minWidth: 1920,
minHeight: 1920,
);
if (out.lengthInBytes <= limitBytes) return out;
quality -= 10;
}
return out;
}For very small targets, shrink minWidth/minHeight as well once the quality loop bottoms out — pixel count is usually the biggest lever on file size.
Historical name we're stuck with. In practice they are aspect-preserving upper bounds on the output: the pipeline scales down (never up) so both dimensions fit inside the given box while keeping the source aspect ratio. Examples:
- Source 4032×3024,
minWidth: 1920, minHeight: 1920→ output ~1920×1440. - Source 1000×1000,
minWidth: 500, minHeight: 500→ output 500×500. - Source 800×600,
minWidth: 1920, minHeight: 1080→ output 800×600 (no upscale).
The aspect ratio is always preserved (never cropped); only the scale is adjusted.
Two common causes:
- Re-encoding PNG as PNG. PNG is lossless — re-encoding rarely shrinks it unless you also downscale.
qualityhas no effect on PNG in the underlying encoders; the same DEFLATE runs either way. If the source was already tightly encoded (e.g. viapngcrush), the output can be a few percent larger. minWidth/minHeightlarger than the source. The plugin won't upscale, but re-encoding still runs and may produce a slightly larger file than the input.
If the source is already small enough, skip the call when src.lengthInBytes is below your threshold rather than always compressing.
Platform channels don't work in a background isolate unless you initialize its binary messenger first. Inside the isolate:
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
final out = await FlutterImageCompress.compressWithList(bytes, quality: 80);Obtain rootIsolateToken on the main isolate via RootIsolateToken.instance! and pass it in through the isolate's arguments.
Without this step you'll see an UnimplementedError pointing back here — that's the platform interface's UnsupportedFlutterImageCompress fallback firing.
Full matrix in Platform features. Quick reference:
- JPEG / PNG: everywhere.
- WebP: Android + iOS + macOS via SDWebImageWebPCoder, Web via the browser (quality support varies).
- HEIC / HEIF: iOS 11+ and Android API 28+ with a hardware encoder — throws
UnsupportedErrorotherwise. Not on macOS.
compressAndGetFile writes to the targetPath you pass. Everything else lands in the OS cache directory (context.cacheDir on Android, NSTemporaryDirectory() on iOS). Both are OS-managed; if you want manual cleanup, use path_provider's getTemporaryDirectory() and clear it yourself.
Format and platform support vary, so any API may throw UnsupportedError on a device that can't produce the requested format. If you rely on WebP or HEIC, catch it and fall back to JPEG:
Future<Uint8List> compressAndTryCatch(String path) async {
try {
return await FlutterImageCompress.compressWithFile(
path,
format: CompressFormat.heic,
);
} on UnsupportedError catch (e) {
print(e);
return FlutterImageCompress.compressWithFile(
path,
format: CompressFormat.jpeg,
);
}
}Requires Kotlin 1.5.21 or higher.
Usually a filesystem issue. Check that:
- Your process can read the source file and write to the target path.
- The parent directory of the target path exists.
- You've requested any permissions the platform needs (e.g. SD-card access on Android).
Use path_provider to obtain writable app directories, and a permission plugin for runtime storage permissions.
By default (keepExif: false), the compressed output carries no source EXIF — only the minimum the container encoder injects (image dimensions, color space, and so on).
With keepExif: true, the plugin copies source EXIF onto the output. The keepExif section has the full per-format, per-platform matrix. In short:
- iOS + macOS: full sub-dict passthrough (EXIF, TIFF, GPS, IPTC, PNG chunks) via
CGImageSource → CGImageDestination. Works for JPEG, PNG, HEIC. Not WebP — ImageIO cannot author WebP metadata. - Android: ~90-tag copy via
androidx.exifinterface. Works for JPEG, PNG, WebP. Not HEIC —ExifInterfacerefuses HEIF writes andHeifHandlerlogs a warning. - Web + OpenHarmony: not supported. The Canvas / packing pipelines strip metadata at encode time.
Regardless of platform, the Orientation tag is always normalized to 1 / ORIENTATION_NORMAL on the output — the pipeline bakes rotation into pixels, so preserving the source orientation would cause viewers to rotate a second time.
- JPEG / PNG: system APIs on every platform.
- WebP: system API on Android; SDWebImageWebPCoder on iOS; browser Canvas on Web.
- HEIC / HEIF: ImageIO on iOS 11+; HeifWriter on Android API 28+ with a hardware encoder (throws
UnsupportedErrorwhen the device can't encode HEIC).
The web implementation is optional — most consumers don't need it. It relies on the browser's Canvas toBlob, so format and quality support depend on the browser (see the compatibility link in the Platform features table).
Requires a minimum deployment target of macOS 10.15. To update an existing app:
- Open the Xcode project, select the Runner target, and set macOS Deployment Target to
10.15. - Update
Podfiletoplatform :osx, '10.15'.
Decoding supports JPEG, PNG, GIF, RAW, WebP, BMP, and SVG. Encoding output is currently limited to JPEG, PNG, and WebP.
当前支持的解析图片格式包括 JPEG、PNG、GIF、RAW、WebP、BMP、SVG。编码输出图片格式当前仅支持 JPEG、PNG 和 WebP。

