Smart Image Compress
Automatic image compression for Flutter apps. Intelligently compress images to configurable size/resolution limits while preserving quality. Perfect for reducing upload times, storage usage, and improving app performance.
๐ Features
โจ Smart Compression
- Iterative Quality Reduction: Progressively reduces quality until target size is met
- EXIF Auto-Correction: Automatically fixes image orientation for consistent display across devices
- Format Support: WebP (recommended) and JPEG formats
- Smart Fallback: Returns original file if compression fails or isn't beneficial
- Best-of Approach: Returns the best compression result, not just the last attempt
๐ฏ Flexible Image Picker Widget
- Single & Multi-Select: Pick one or multiple images at once
- Camera & Gallery: Choose from camera capture or device gallery
- Real-Time Progress: Visual feedback during compression with
LinearProgressIndicator - Custom Trigger: Use your own widget to trigger the picker
- Responsive: Works seamlessly on tablets and phones
โก Performance Features
- Isolate-Based Processing: Batch compression runs in background isolate (doesn't block UI)
- Minimal Dependencies: Only 4 core packages (flutter_image_compress, image_picker, permission_handler, path_provider)
- Configurable Limits: Control file size, resolution, quality, and format
- Smart Threading: Single images compress on main thread, batches use isolates
๐งน Memory Management
- Auto Tracking: All temporary files automatically tracked
- Manual Cleanup: Delete files with
cleanupTemporaryFiles() - Age-Based Cleanup: Remove files older than 24 hours with
cleanupOldTemporaryFiles() - Proper Lifecycle: Clean up on app pause or before app termination
๐ Security & Reliability
- Error Handling: Graceful fallback to original on any error
- Permission Management: Platform-aware iOS/Android permission handling
- No Data Leaks: Temporary files are properly deleted
- Null Safety: 100% null-safe, works with Dart 3.5+
๐ฆ Installation
Step 1: Add dependency
Add to your pubspec.yaml:
dependencies:
smart_image_compress: ^0.1.0
Or use command line:
flutter pub add smart_image_compress
Step 2: Platform Configuration
Android
Add to android/app/build.gradle:
android {
compileSdk 34
defaultConfig {
minSdkVersion 21
}
}
No additional permissions needed โ permission_handler manages them.
iOS
Add to ios/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to compress and share them</string>
<key>NSCameraUsageDescription</key>
<string>We need camera access to take and compress photos</string>
<key>NSPhotoLibraryAddOnlyUsageDescription</key>
<string>We need permission to save compressed images</string>
macOS
Add to macos/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos</string>
Linux & Windows
Image picking not available on desktop platforms. The widget will raise an exception if used.
๐ Quick Start
Basic Usage (Widget)
The simplest way to compress images:
import 'package:smart_image_compress/smart_image_compress.dart';
OptimizedImagePicker(
onImagesPicked: (files) {
print('Compressed images: $files');
// Save to database or upload
for (final file in files) {
print('File: ${file.path}, Size: ${file.lengthSync() / 1024} KB');
}
},
)
Service-Based Approach (Advanced)
For more control:
import 'package:smart_image_compress/smart_image_compress.dart';
final service = CompressionService();
// Compress single image
final compressed = await service.compressSingleImage(
imageFile,
const CompressConfig(),
onProgress: (progress) {
print('Progress: ${(progress * 100).toInt()}%');
},
);
// Compress multiple images
final results = await service.compressBatchInIsolate(
imageFiles,
const CompressConfig(),
onProgress: (completed, total) {
print('Compressed $completed of $total images');
},
);
// Clean up
await service.cleanupTemporaryFiles();
๐ Comprehensive API Reference
OptimizedImagePicker Widget
The main widget for picking and compressing images.
OptimizedImagePicker({
required Function(List<File>) onImagesPicked,
CompressConfig? config,
int? maxImages,
bool allowCamera = true,
bool multiSelect = true,
Widget? triggerWidget,
bool showProgressDialog = true,
CompressionProgressCallback? onCompressionProgress,
})
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| onImagesPicked | Function(List<File>) |
Required | Callback when images are compressed and ready |
| config | CompressConfig? |
null |
Custom compression settings; uses defaults if null |
| maxImages | int? |
10 | Maximum images for multi-select (null = no limit) |
| allowCamera | bool |
true |
Show camera option in single-select mode |
| multiSelect | bool |
true |
Allow multiple image selection |
| triggerWidget | Widget? |
null |
Custom widget to trigger picker (uses default button if null) |
| showProgressDialog | bool |
true |
Show built-in progress dialog during compression |
| onCompressionProgress | CompressionProgressCallback? |
null |
Custom progress callback; called even with progress dialog |
Example: Widget with All Options
OptimizedImagePicker(
onImagesPicked: (files) {
setState(() => _images = files);
},
config: const CompressConfig(
maxSizeKB: 200,
maxResolution: 1280,
initialQuality: 75,
format: CompressFormat.jpeg,
),
maxImages: 5,
allowCamera: true,
multiSelect: true,
showProgressDialog: true,
onCompressionProgress: (completed, total) {
print('Compressed $completed/$total');
},
triggerWidget: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add_a_photo, color: Colors.white),
SizedBox(width: 8),
Text('Add Photos', style: TextStyle(color: Colors.white)),
],
),
),
)
CompressionService
Singleton service for direct image compression control.
Creating an Instance
final service = CompressionService();
// Singleton: all calls return the same instance
Methods
1. compressSingleImage()
Compress a single image file.
Future<File> compressSingleImage(
File originalFile,
CompressConfig config, {
ValueChanged<double>? onProgress,
})
- originalFile: Image to compress
- config: Compression settings
- onProgress: Progress callback (0.0 to 1.0)
- Returns: Compressed file or original if compression fails
Example:
final file = File('/path/to/image.jpg');
final compressed = await service.compressSingleImage(
file,
const CompressConfig(maxSizeKB: 300),
onProgress: (progress) {
print('Compressed: ${(progress * 100).toInt()}%');
},
);
2. compressBatchInIsolate()
Compress multiple images in a background isolate (non-blocking).
Future<List<File>> compressBatchInIsolate(
List<File> files,
CompressConfig config, {
ValueChanged<(int, int)>? onProgress,
})
- files: List of image files
- config: Applied to all files
- onProgress: Callback with (completed, total) count
- Returns: List of compressed files in same order as input
Example:
final files = [File('/path1'), File('/path2'), File('/path3')];
final compressed = await service.compressBatchInIsolate(
files,
const CompressConfig(),
onProgress: (completed, total) {
print('Progress: $completed/$total');
},
);
3. cleanupTemporaryFiles()
Delete all temporary files created during compression.
Future<int> cleanupTemporaryFiles()
- Returns: Number of files deleted
Example:
final deleted = await service.cleanupTemporaryFiles();
print('Deleted $deleted temp files');
4. cleanupOldTemporaryFiles()
Delete temporary files older than a threshold.
Future<int> cleanupOldTemporaryFiles({
Duration olderThan = const Duration(hours: 24),
})
- olderThan: Age threshold (default: 24 hours)
- Returns: Number of files deleted
Example:
// Delete files older than 7 days
final deleted = await service.cleanupOldTemporaryFiles(
olderThan: const Duration(days: 7),
);
print('Cleaned $deleted old files');
CompressConfig
Configuration object for compression parameters.
const CompressConfig({
int maxSizeKB = 300,
int maxResolution = 1600,
int initialQuality = 80,
CompressFormat format = CompressFormat.webp,
bool keepExif = true,
bool autoCorrectOrientation = true,
})
Configuration Parameters
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
| maxSizeKB | int |
300 | > 0 | Target file size in KB |
| maxResolution | int |
1600 | > 0 | Max longest dimension (px) |
| initialQuality | int |
80 | 1-100 | Starting quality % |
| format | CompressFormat |
webp |
webp, jpeg | Output format |
| keepExif | bool |
true |
- | Preserve EXIF data |
| autoCorrectOrientation | bool |
true |
- | Fix image rotation |
Predefined Configurations
// Recommended for most apps
const CompressConfig()
// Aggressive compression (small file size priority)
const CompressConfig(
maxSizeKB: 150,
maxResolution: 1024,
initialQuality: 65,
)
// High quality (quality priority)
const CompressConfig(
maxSizeKB: 500,
maxResolution: 2048,
initialQuality: 90,
)
// JPEG format
const CompressConfig(
format: CompressFormat.jpeg,
keepExif: true,
)
// For thumbnails
const CompressConfig(
maxSizeKB: 50,
maxResolution: 512,
initialQuality: 70,
)
// Lossless (minimal compression)
const CompressConfig(
maxSizeKB: 1000,
maxResolution: 2048,
initialQuality: 100,
)
๐ก Use Cases & Examples
Use Case 1: Social Media App
Post images with consistent quality and size:
// User posts single photo
OptimizedImagePicker(
onImagesPicked: (files) async {
final file = files.first;
final fileSize = file.lengthSync() / 1024 / 1024; // MB
// Upload to server
final request = http.MultipartRequest('POST', Uri.parse(uploadUrl))
..files.add(await http.MultipartFile.fromPath('image', file.path));
final response = await request.send();
print('Uploaded ${fileSize.toStringAsFixed(2)} MB');
},
config: const CompressConfig(
maxSizeKB: 500,
maxResolution: 1920,
),
)
Use Case 2: Bulk Image Upload (Gallery)
Compress multiple images efficiently:
OptimizedImagePicker(
onImagesPicked: (files) async {
print('Compressing ${files.length} images...');
// Save to local database
for (final file in files) {
await _saveToDatabase(file);
}
print('Saved ${files.length} images');
},
multiSelect: true,
maxImages: 20,
config: const CompressConfig(maxSizeKB: 300),
showProgressDialog: true,
)
Use Case 3: Profile Picture Upload
Single image with high quality:
OptimizedImagePicker(
onImagesPicked: (files) {
final profileImage = files.first;
_updateProfilePicture(profileImage);
},
multiSelect: false,
config: const CompressConfig(
maxSizeKB: 400,
maxResolution: 1024,
initialQuality: 85,
),
)
Use Case 4: Chat Application
Quick image sharing with minimal size:
OptimizedImagePicker(
onImagesPicked: (files) {
for (final file in files) {
_sendImageToChat(file);
}
},
multiSelect: true,
config: const CompressConfig(
maxSizeKB: 200,
maxResolution: 1024,
initialQuality: 70,
),
)
Use Case 5: Direct Service Access
Full control without widget:
void _compressAndSave() async {
final service = CompressionService();
final picker = ImagePicker();
// Pick image
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile == null) return;
// Compress with custom progress
final compressed = await service.compressSingleImage(
File(pickedFile.path),
const CompressConfig(maxSizeKB: 250),
onProgress: (progress) {
// Update custom progress UI
setState(() => _compressionProgress = progress);
},
);
// Use compressed file
print('Original: ${File(pickedFile.path).lengthSync() / 1024} KB');
print('Compressed: ${compressed.lengthSync() / 1024} KB');
// Clean up
await service.cleanupTemporaryFiles();
}
โ๏ธ Advanced Configuration & Optimization
Choosing Compression Parameters
For Upload-Heavy Applications
const CompressConfig(
maxSizeKB: 250, // Quick uploads
maxResolution: 1280,
initialQuality: 75,
format: CompressFormat.webp, // Best compression ratio
)
For High-Quality Requirements
const CompressConfig(
maxSizeKB: 600, // Larger but higher quality
maxResolution: 2048,
initialQuality: 88,
format: CompressFormat.jpeg, // Better quality perception
)
For Storage-Constrained Devices
const CompressConfig(
maxSizeKB: 150, // Very small files
maxResolution: 800,
initialQuality: 60,
format: CompressFormat.webp,
)
App Lifecycle Cleanup
Clean up old files on app pause:
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
final _compressionService = CompressionService();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// Clean up files older than 24 hours
_compressionService.cleanupOldTemporaryFiles();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
Custom Progress UI
Use your own progress dialog instead of built-in:
OptimizedImagePicker(
onImagesPicked: _handleCompressedImages,
showProgressDialog: false, // Disable built-in dialog
onCompressionProgress: (completed, total) {
// Update custom progress
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: const Text('Compressing...'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(
value: completed / total,
),
const SizedBox(height: 16),
Text('$completed of $total'),
],
),
),
);
},
)
Custom Trigger Widget
OptimizedImagePicker(
onImagesPicked: _handleImages,
triggerWidget: Padding(
padding: const EdgeInsets.all(16),
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: () {}, // Handled by widget
borderRadius: BorderRadius.circular(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.cloud_upload, size: 40, color: Colors.blue),
const SizedBox(height: 12),
const Text(
'Tap to Add Photos',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
const Text(
'Select from gallery or camera',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
),
),
)
๐ง Troubleshooting
Common Issues
Images Not Being Compressed
Problem: Images are returned unchanged.
Solutions:
-
Check if image is already smaller than
maxSizeKBprint(originalFile.lengthSync() / 1024); // Should be > 300 KB -
Verify
initialQualityis less than 100// Don't do this - no compression happens const CompressConfig(initialQuality: 100) // Do this instead const CompressConfig(initialQuality: 80) -
Increase
maxSizeKBif it's too strict// If current maxSizeKB is too small const CompressConfig(maxSizeKB: 500) // Instead of 300
Permission Errors (Android)
Problem: "Permission Denied" error when picking images.
Solutions:
-
Check
AndroidManifest.xmlhas required permissions:<!-- android/app/src/main/AndroidManifest.xml --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
Grant runtime permissions manually before using widget
-
Check app settings for granted permissions
Permission Errors (iOS)
Problem: App crashes or shows blank permission dialog.
Solutions:
- Ensure
Info.plisthas required keys (see Installation section) - Rebuild and reinstall app:
flutter clean flutter pub get flutter run
Progress Dialog Not Showing
Problem: Progress updates don't appear.
Solutions:
-
Ensure
showProgressDialog: true(default)OptimizedImagePicker( showProgressDialog: true, // Default ) -
For custom progress, set
showProgressDialog: falseand useonCompressionProgress -
Check widget is mounted during callback
Out of Memory (OOM) Errors
Problem: App crashes with large image batches.
Solutions:
-
Reduce
maxResolution:const CompressConfig(maxResolution: 1024) // Instead of 1600 -
Compress images individually instead of batch:
for (final file in files) { final compressed = await service.compressSingleImage(file, config); } -
Increase device RAM requirements
Slow Compression
Problem: Compression takes too long.
Solutions:
-
Reduce
maxResolution:const CompressConfig(maxResolution: 1280) -
Increase
initialQualityto skip iterations:const CompressConfig(initialQuality: 75) // Fewer iterations -
Use WebP format (faster than JPEG):
const CompressConfig(format: CompressFormat.webp)
Files Not Deleted After Cleanup
Problem: Temp files still exist after calling cleanupTemporaryFiles().
Solutions:
-
Ensure
awaitis used:// โ Correct await service.cleanupTemporaryFiles(); // โ Wrong service.cleanupTemporaryFiles(); -
Check file isn't in use by another process
-
Verify temp file path is tracked (should be automatic)
๐ Performance Metrics
Compression Speed
| Image Size | Resolution | Time | Format |
|---|---|---|---|
| 5 MB | 4000ร3000 | ~2-3s | WebP |
| 3 MB | 3000ร2250 | ~1-2s | WebP |
| 1 MB | 1600ร1200 | ~0.5s | WebP |
| 10 MB (batch 5) | 4000ร3000 | ~8-10s | WebP |
File Size Reduction
| Config | Input Size | Output Size | Reduction |
|---|---|---|---|
| Medium (300KB) | 5 MB | 280 KB | 94% |
| Aggressive (150KB) | 5 MB | 145 KB | 97% |
| High Quality (500KB) | 5 MB | 480 KB | 90% |
Note: Actual results vary based on image content and quality.
โ FAQ
Q: Does compression reduce quality noticeably? A: No. Default (80% quality) looks identical to users. Compression mostly removes metadata and duplicates.
Q: Can I use WebP on old devices? A: Yes. WebP is supported on Android 4.2+ and iOS 11+.
Q: Do I need to manage permissions myself?
A: No. The widget handles it automatically. Just ensure Info.plist has required keys (iOS).
Q: Can I compress and upload simultaneously?
A: Yes. Compress first, then upload: final file = await service.compressSingleImage(...)
Q: Does this work on web? A: Not yet. Web support is planned for v0.2.0.
Q: How often should I call cleanupTemporaryFiles()?
A: After uploading images, or use app lifecycle cleanup (see advanced section).
Q: Can I use multiple CompressionService instances?
A: No, it's a singleton. CompressionService() always returns same instance.
Q: What happens if compression fails?
A: Widget returns original file. Check logs via debugPrint().
๐ค Integration Examples
With Firebase Storage
Future<void> uploadImage(File imageFile) async {
final service = CompressionService();
// Compress
final compressed = await service.compressSingleImage(
imageFile,
const CompressConfig(),
);
// Upload to Firebase
final ref = FirebaseStorage.instance
.ref()
.child('images/${DateTime.now().millisecondsSinceEpoch}');
await ref.putFile(compressed);
// Cleanup
await service.cleanupTemporaryFiles();
}
With HTTP Upload
Future<void> uploadWithHttp(File imageFile) async {
final service = CompressionService();
final compressed = await service.compressSingleImage(
imageFile,
const CompressConfig(maxSizeKB: 300),
);
final request = http.MultipartRequest('POST', Uri.parse(apiUrl))
..files.add(await http.MultipartFile.fromPath('image', compressed.path));
await request.send();
await service.cleanupTemporaryFiles();
}
With Riverpod State Management
final compressionServiceProvider = Provider((ref) {
return CompressionService();
});
final imageCompressorProvider = FutureProvider.family<File, (File, CompressConfig)>(
(ref, params) async {
final service = ref.watch(compressionServiceProvider);
return service.compressSingleImage(params.$1, params.$2);
},
);
With GetX State Management
class ImageController extends GetxController {
final compressionService = CompressionService();
final _isCompressing = false.obs;
void pickAndCompress() async {
_isCompressing.value = true;
try {
// Use widget or manual service
final compressed = await compressionService.compressSingleImage(...);
// Handle compressed file
} finally {
_isCompressing.value = false;
}
}
}
๐ Best Practices
- Always clean up: Call
cleanupTemporaryFiles()after upload - Use appropriate config: Choose settings matching your use case
- Handle errors: Wrap calls in try-catch for production apps
- Monitor permissions: Log permission requests for analytics
- Test on devices: Compression varies by device
- Use isolates for batches: Don't compress many images on main thread
- Check file exists: Verify original file exists before compression
- Profile memory: Monitor memory usage in large batches
๐ Comparison with Alternatives
| Feature | smart_image_compress | image_picker | flutter_image_compress | Others |
|---|---|---|---|---|
| Image picking | โ | โ | โ | Varies |
| Compression | โ | โ | โ | Varies |
| Batch processing | โ | โ | โ | โ |
| Progress tracking | โ | โ | โ | โ |
| Temp file cleanup | โ | โ | โ | โ |
| Isolate support | โ | โ | โ | โ |
| EXIF auto-fix | โ | โ | โ | Varies |
| Custom widget | โ | โ | โ | โ |
| Documentation | โ โ | โ | โ | Varies |
๐ Support
- ๐ Issues: GitHub Issues
- ๐ฌ Discussions: GitHub Discussions
- ๐ง Email: your-email@example.com
- ๐ Docs: Check CHANGELOG.md and example/ folder
๐ Version Support
| Version | Status | Dart | Flutter |
|---|---|---|---|
| 0.1.x | Current | 3.5+ | 3.24+ |
| 0.2.0 | Planned | 3.5+ | 3.24+ |
๐ License
MIT License - See LICENSE file for details
Made with โค๏ธ for Flutter developers
Last Updated: February 20, 2026
Libraries
- main
- smart_image_compress
- Automatic image compression for Flutter apps.