Compresses image with native plugin (ObjC/Kotlin) on Flutter. Works on Android, iOS, macOS, Web, OpenHarmony.
690
stars
337
commits
Dart
primary language
Jul 25, 2026
updated
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.
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_compress
Import 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.
libwebp directly would help but is not currently on the roadmap.toBlob implementation; see the compatibility link in the table above.UnsupportedError. Implemented on top of HeifWriter.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 Orientation tag is always normalized to 1 / 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.exifinterface |
| PNG | ✅ Same passthrough | ✅ ~90 tags via androidx.exifinterface |
| WebP | ❌ ImageIO cannot author WebP metadata; output is a valid WebP with no EXIF | ✅ ~90 tags via androidx.exifinterface |
| HEIC | ✅ Same passthrough | ❌ androidx.exifinterface refuses HEIF writes; output is a valid HEIC with no EXIF. HeifHandler logs a warning. |
Unsupported combinations still return valid image bytes — you simply do not get EXIF back. keepExif: true never fails the compression call as a whole.
keepExif: ImageIO cannot write metadata into a WebP container. A manual VP8X + EXIF chunk splice via SDWebImageWebPCoder would fix this but is not implemented.keepExif: androidx.exifinterface refuses HEIF writes on every version. Manually injecting ISO/IEC 23008-12 metadata boxes (~400 LoC) would fix this but is not implemented.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.
List<int> and Uint8ListYou'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.
minWidth/minHeight named that if it acts like a max?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:
minWidth: 1920, minHeight: 1920 → output ~1920×1440.minWidth: 500, minHeight: 500 → output 500×500.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:
quality has no effect on PNG in the underlying encoders; the same DEFLATE runs either way. If the source was already tightly encoded (e.g. via pngcrush), the output can be a few percent larger.minWidth/minHeight larger 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.
compute()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:
UnsupportedError otherwise. 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.
nullUsually a filesystem issue. Check that:
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:
CGImageSource → CGImageDestination. Works for JPEG, PNG, HEIC. Not WebP — ImageIO cannot author WebP metadata.androidx.exifinterface. Works for JPEG, PNG, WebP. Not HEIC — ExifInterface refuses HEIF writes and HeifHandler logs a warning.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.
UnsupportedError when 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:
10.15.Podfile to platform :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。
(top 30 of 39)
Dart
59.8%
Kotlin
19.1%
Swift
11.1%
Java
4.4%
Ruby
2.2%
Compresses image with native plugin (ObjC/Kotlin) on Flutter. Works on Android, iOS, macOS, Web, OpenHarmony.
690
stars
337
commits
Dart
primary language
Jul 25, 2026
updated
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.
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_compress
Import 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.
libwebp directly would help but is not currently on the roadmap.toBlob implementation; see the compatibility link in the table above.UnsupportedError. Implemented on top of HeifWriter.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 Orientation tag is always normalized to 1 / 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.exifinterface |
| PNG | ✅ Same passthrough | ✅ ~90 tags via androidx.exifinterface |
| WebP | ❌ ImageIO cannot author WebP metadata; output is a valid WebP with no EXIF | ✅ ~90 tags via androidx.exifinterface |
| HEIC | ✅ Same passthrough | ❌ androidx.exifinterface refuses HEIF writes; output is a valid HEIC with no EXIF. HeifHandler logs a warning. |
Unsupported combinations still return valid image bytes — you simply do not get EXIF back. keepExif: true never fails the compression call as a whole.
keepExif: ImageIO cannot write metadata into a WebP container. A manual VP8X + EXIF chunk splice via SDWebImageWebPCoder would fix this but is not implemented.keepExif: androidx.exifinterface refuses HEIF writes on every version. Manually injecting ISO/IEC 23008-12 metadata boxes (~400 LoC) would fix this but is not implemented.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.
List<int> and Uint8ListYou'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.
minWidth/minHeight named that if it acts like a max?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:
minWidth: 1920, minHeight: 1920 → output ~1920×1440.minWidth: 500, minHeight: 500 → output 500×500.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:
quality has no effect on PNG in the underlying encoders; the same DEFLATE runs either way. If the source was already tightly encoded (e.g. via pngcrush), the output can be a few percent larger.minWidth/minHeight larger 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.
compute()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:
UnsupportedError otherwise. 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.
nullUsually a filesystem issue. Check that:
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:
CGImageSource → CGImageDestination. Works for JPEG, PNG, HEIC. Not WebP — ImageIO cannot author WebP metadata.androidx.exifinterface. Works for JPEG, PNG, WebP. Not HEIC — ExifInterface refuses HEIF writes and HeifHandler logs a warning.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.
UnsupportedError when 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:
10.15.Podfile to platform :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。
(top 30 of 39)
Dart
59.8%
Kotlin
19.1%
Swift
11.1%
Java
4.4%
Ruby
2.2%