Effective Image Compression in Flutter: A Step-by-Step Guide

ByteBitz Solutions
4 min readOct 16, 2024

--

Optimizing image size can considerably improve efficiency when transmitting photos or displaying them in galleries. This post explains how to add image compression to a Flutter project.

dependencies:
flutter:
sdk: flutter
image: ^4.2.0
image_picker: ^1.1.2
path_provider: ^2.1.4
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart';
  1. Isolate for Image Processing

To keep our app snappy, we use Dart’s Isolate to process images on a separate thread. This prevents the user interface from freezing during compression.

2. Compressing and Resizing Images

When a user picks an image, the following steps are executed:

  • Picking the Image: We utilize the image_picker package to allow users to select an image from their device.
  • Resizing and Compressing: The selected image is passed to a function that spawns an isolate. Inside the isolate, we decode the image, resize it (keeping the aspect ratio), and compress it to a JPEG format with a specified quality.
  • Here’s how the compression function is structured:
 Future<void> pickAndProcessImage() async {
setState(() {
_isLoading = true;
});

final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);

if (pickedFile != null) {
File imageFile = File(pickedFile.path);
String compressedFilePath = await callReceiver(imageFile);
setState(() {
_compressedImagePaths.add(compressedFilePath);
});
if (kDebugMode) {
print('Compressed image saved at: $compressedFilePath');
}
} else {
if (kDebugMode) {
print('No image selected.');
}
}

setState(() {
_isLoading = false;
});
}
Future<dynamic> sendReceive(SendPort sendPort, String filePath) {
final response = ReceivePort();
sendPort.send([filePath, response.sendPort]);
return response.first;
}
Future<String> callReceiver(File file) async {
final receivePort = ReceivePort();

// Spawn an isolate
await Isolate.spawn(_processImage, receivePort.sendPort);

final sendPort = await receivePort.first as SendPort;
final result = await sendReceive(sendPort, file.path);
return result; // Return the path of the compressed file
}
  void _processImage(SendPort sendPort) async {
final port = ReceivePort();
sendPort.send(port.sendPort);

await for (var message in port) {
final filePath = message[0] as String;
final send = message[1] as SendPort;

// Process the image
File file = File(filePath);
img.Image? images = img.decodeImage(await file.readAsBytes());

if (images != null) {
int width, height;
if (images.width > images.height) {
width = 800;
height = (images.height / images.width * 800).round();
} else {
height = 800;
width = (images.width / images.height * 800).round();
}

img.Image resizeImage = img.copyResize(images, width: width, height: height);
// you can adjust image quality as per your needs
List<int> compressBytes = img.encodeJpg(resizeImage, quality: 85);

// Save the compressed image
File compressFile = File(file.path.replaceFirst('.jpg', 'compress.jpg'));
compressFile.writeAsBytesSync(compressBytes);
// Send the path back
send.send(compressFile.path);
} else {
send.send('Error: Image could not be decoded.');
}
}
}

3. Saving the Compressed Image

The compressed image is kept in the device’s application for easy access later.


File compressFile = File(file.path.replaceFirst('.jpg', 'compress.jpg'));
compressFile.writeAsBytesSync(compressBytes);

4. User Interface

import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Image Compression',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
bool _isLoading = false;
final List<String> _compressedImagePaths = [];

Future<void> pickAndProcessImage() async {
setState(() {
_isLoading = true;
});

final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);

if (pickedFile != null) {
File imageFile = File(pickedFile.path);
String compressedFilePath = await callReceiver(imageFile);
setState(() {
_compressedImagePaths.add(compressedFilePath);
});
if (kDebugMode) {
print('Compressed image saved at: $compressedFilePath');
}
} else {
if (kDebugMode) {
print('No image selected.');
}
}

setState(() {
_isLoading = false;
});
}

Future<String> callReceiver(File file) async {
final receivePort = ReceivePort();

// Spawn an isolate
await Isolate.spawn(_processImage, receivePort.sendPort);

final sendPort = await receivePort.first as SendPort;
final result = await sendReceive(sendPort, file.path);
return result; // Return the path of the compressed file
}

Future<dynamic> sendReceive(SendPort sendPort, String filePath) {
final response = ReceivePort();
sendPort.send([filePath, response.sendPort]);
return response.first;
}

static void _processImage(SendPort sendPort) async {
final port = ReceivePort();
sendPort.send(port.sendPort);

await for (var message in port) {
final filePath = message[0] as String;
final send = message[1] as SendPort;

// Process the image
File file = File(filePath);
img.Image? images = img.decodeImage(await file.readAsBytes());

if (images != null) {
int width, height;
if (images.width > images.height) {
width = 800;
height = (images.height / images.width * 800).round();
} else {
height = 800;
width = (images.width / images.height * 800).round();
}

img.Image resizeImage = img.copyResize(images, width: width, height: height);
// you can adjust image quality as per your needs
List<int> compressBytes = img.encodeJpg(resizeImage, quality: 85);

// Save the compressed image
File compressFile = File(file.path.replaceFirst('.jpg', 'compress.jpg'));
compressFile.writeAsBytesSync(compressBytes);
// Send the path back
send.send(compressFile.path);
} else {
send.send('Error: Image could not be decoded.');
}
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Image Picker Example')),
body: Center(
child: _isLoading
? const CircularProgressIndicator()
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: pickAndProcessImage,
child: const Text('Pick and Compress Image'),
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: _compressedImagePaths.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Image ${index + 1}'),
subtitle: Text(_compressedImagePaths[index]),
);
},
),
),
],
),
),
);
}
}

Conclusion:

By utilizing Dart isolates for image compression, we not only assure a seamless user experience but also properly control image sizes, which can lead to improved app performance and lower storage requirements.

--

--

ByteBitz Solutions
ByteBitz Solutions

Written by ByteBitz Solutions

"Bytebitz Solutions" a tech company specializing in precise, innovative digital and tech solutions #StartupLife # SoftwareSolutions

No responses yet