smart_image_compress 0.1.0 copy "smart_image_compress: ^0.1.0" to clipboard
smart_image_compress: ^0.1.0 copied to clipboard

Automatic image compression for Flutter apps. Intelligently compresses images to configurable size/resolution limits while preserving quality. Supports batch processing via isolates, image picker inte [...]

Smart Image Compress #

Pub Version License: MIT Flutter Dart Code Quality

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,
  ),
)

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:

  1. Check if image is already smaller than maxSizeKB

    print(originalFile.lengthSync() / 1024); // Should be > 300 KB
    
  2. Verify initialQuality is less than 100

    // Don't do this - no compression happens
    const CompressConfig(initialQuality: 100)
       
    // Do this instead
    const CompressConfig(initialQuality: 80)
    
  3. Increase maxSizeKB if 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:

  1. Check AndroidManifest.xml has 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" />
    
  2. Grant runtime permissions manually before using widget

  3. Check app settings for granted permissions

Permission Errors (iOS)

Problem: App crashes or shows blank permission dialog.

Solutions:

  1. Ensure Info.plist has required keys (see Installation section)
  2. Rebuild and reinstall app:
    flutter clean
    flutter pub get
    flutter run
    

Progress Dialog Not Showing

Problem: Progress updates don't appear.

Solutions:

  1. Ensure showProgressDialog: true (default)

    OptimizedImagePicker(
      showProgressDialog: true, // Default
    )
    
  2. For custom progress, set showProgressDialog: false and use onCompressionProgress

  3. Check widget is mounted during callback

Out of Memory (OOM) Errors

Problem: App crashes with large image batches.

Solutions:

  1. Reduce maxResolution:

    const CompressConfig(maxResolution: 1024) // Instead of 1600
    
  2. Compress images individually instead of batch:

    for (final file in files) {
      final compressed = await service.compressSingleImage(file, config);
    }
    
  3. Increase device RAM requirements

Slow Compression

Problem: Compression takes too long.

Solutions:

  1. Reduce maxResolution:

    const CompressConfig(maxResolution: 1280)
    
  2. Increase initialQuality to skip iterations:

    const CompressConfig(initialQuality: 75) // Fewer iterations
    
  3. 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:

  1. Ensure await is used:

    // โœ… Correct
    await service.cleanupTemporaryFiles();
       
    // โŒ Wrong
    service.cleanupTemporaryFiles();
    
  2. Check file isn't in use by another process

  3. 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 #

  1. Always clean up: Call cleanupTemporaryFiles() after upload
  2. Use appropriate config: Choose settings matching your use case
  3. Handle errors: Wrap calls in try-catch for production apps
  4. Monitor permissions: Log permission requests for analytics
  5. Test on devices: Compression varies by device
  6. Use isolates for batches: Don't compress many images on main thread
  7. Check file exists: Verify original file exists before compression
  8. 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 #


๐Ÿ”„ 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

3
likes
130
points
108
downloads

Publisher

unverified uploader

Weekly Downloads

Automatic image compression for Flutter apps. Intelligently compresses images to configurable size/resolution limits while preserving quality. Supports batch processing via isolates, image picker integration, and proper cleanup.

Repository (GitHub)
View/report issues

Topics

#image-compression #image-picker #flutter #compression #optimization

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_image_compress, image_picker, path_provider, permission_handler

More

Packages that depend on smart_image_compress