Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Future<Uint8List> compressImage(Uint8List imageData) async {
quality: 60,
targetWidth: 400,
imageQuality: ImageQuality.low,
imageFormat: ImageFormat.jpeg,
);
return compressedImage;
}
Expand All @@ -76,6 +77,7 @@ Future<List<Uint8List>> compressMultipleImages(List<Uint8List> imageList) async
targetWidth: 800,
batchSize: 3,
imageQuality: ImageQuality.low,
imageFormat: ImageFormat.jpeg,
);
return result;
}
Expand All @@ -88,21 +90,13 @@ Future<void> _cancelCompression() async {
```
## Parameters of `compressImage` function:

| Parameter Name | Data type | Default Value | Description |
|----------------|--------------|---------------------|-----------------------------------------------------|
| imageData | Uint8List | - | The image to be compressed |
| quality | int | 60 | The compression quality percentage |
| targetWidth | int? | null | The desired width for the compressed image |
| imageQuality | ImageQuality | ImageQuality.medium | The quality of the image to store after compression |

## Parameters of `compressImage` function:

| Parameter Name | Data type | Default Value | Description |
|----------------|--------------|---------------------|-----------------------------------------------------------|
| imageData | Uint8List | - | The image to be compressed |
| quality | int | 60 | The compression quality percentage |
| targetWidth | int? | null | The desired width for the compressed image |
| imageQuality | ImageQuality | ImageQuality.medium | The quality of the image to store after compression |
| imageFormat | ImageFormat | ImageFormat.jpeg | The format of the output image (jpeg or png) |
| batchSize | int | 3 | The number of images to process simultaneously in a batch |

## About params
Expand All @@ -113,6 +107,7 @@ The `targetWidth` parameter allows you to resize the images to a specific width.

Use this parameter when you need all images to have a uniform width. It is particularly useful for optimizing image processing performance.
Recommended to use this parameter when image size or image width is large.

### batchSize

The `batchSize` parameter determines how many images are processed in a single batch.
Expand All @@ -125,13 +120,26 @@ To dynamically adjust batchSize based on the number of CPU threads available on
final maxAvailableCPUThreads = Platform.numberOfProcessors;
final batchSize = maxAvailableCPUThreads > 0 ? maxAvailableCPUThreads ~/ 2 : 1;
```

### quality

Specifies the quality of the target image.

Note: On iOS, quality is ignored for PNG because it's a lossless format with no adjustable compression.

### imageQuality

The `imageQuality` parameter controls the quality of the processed images. It has three predefined values:
- low
- medium (default)
- high

### imageFormat

The `imageFormat` parameter determines the output format of the compressed image:
- `ImageFormat.jpeg` (default): Creates JPEG images with lossy compression. Better for photos and images without transparency.
- `ImageFormat.png`: Creates PNG images with lossless compression. Preserves transparency but may result in larger file sizes.

## Android

You may need to update Kotlin to version `1.5.20` or higher.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ object Constants {
const val TARGET_WIDTH: String = "targetWidth"
const val COMPRESSION_QUALITY: String = "compressionQuality"
const val IMAGE_QUALITY: String = "imageQuality"
const val IMAGE_FORMAT: String = "imageFormat"
const val IMAGE_QUALITY_HIGH: String = "high"
const val IMAGE_QUALITY_LOW: String = "low"
const val IMAGE_QUALITY_MEDIUM: String = "medium"
const val FORMAT_JPEG: String = "jpeg"
const val FORMAT_PNG: String = "png"
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class FastImageCompressPlugin: FlutterPlugin, MethodCallHandler {
val targetWidth = call.argument<Int?>(Constants.TARGET_WIDTH)
val compressionQuality = call.argument<Int?>(Constants.COMPRESSION_QUALITY)
val imageQualityInString = call.argument<String>(Constants.IMAGE_QUALITY)
// Get the desired output image format, default to JPEG
val imageFormat = call.argument<String>(Constants.IMAGE_FORMAT) ?: Constants.FORMAT_JPEG

// Determine the image quality based on the input string
val imageQuality =
Expand Down Expand Up @@ -117,7 +119,7 @@ class FastImageCompressPlugin: FlutterPlugin, MethodCallHandler {
val resizedBitmap = resizeBitmap(correctedBitmap, targetWidth)

// Compress the image and convert it to a byte array
val compressedBytes = bitmapToByteArray(resizedBitmap, compressionQuality)
val compressedBytes = bitmapToByteArray(resizedBitmap, compressionQuality, imageFormat)
if (compressedBytes == null) {
// Return the original image if compression fails or when compressed image
// is larger than original image
Expand Down Expand Up @@ -183,26 +185,35 @@ class FastImageCompressPlugin: FlutterPlugin, MethodCallHandler {
}

// Compress a Bitmap into a byte array
private fun bitmapToByteArray(bitmap: Bitmap, compressionQuality: Int? = 80): ByteArray? {
private fun bitmapToByteArray(bitmap: Bitmap, compressionQuality: Int? = 80, imageFormat: String = Constants.FORMAT_JPEG): ByteArray? {
val quality = compressionQuality ?: 80
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)

// Select compression format based on imageFormat parameter
val compressFormat = if (imageFormat == Constants.FORMAT_PNG) {
Bitmap.CompressFormat.PNG
} else {
Bitmap.CompressFormat.JPEG
}

bitmap.compress(compressFormat, quality, stream)
var outputImageSize = stream.toByteArray().size

// To avoid increasing the size of an already compressed image, compare
// input and output sizes, and use lower compression quality if needed.
if (outputImageSize > inputImageSize) {
var updatedCompQuality = quality;
while (outputImageSize > inputImageSize && updatedCompQuality >= 10) {
bitmap.compress(Bitmap.CompressFormat.JPEG, updatedCompQuality, stream)
updatedCompQuality = updatedCompQuality - 10;
outputImageSize = stream.toByteArray().size;
}
if (updatedCompQuality >= 10) {
return stream.toByteArray()
} else {
return null
}
var updatedCompQuality = quality
while (outputImageSize > inputImageSize && updatedCompQuality >= 10) {
stream.reset() // Clear the stream
bitmap.compress(Bitmap.CompressFormat.JPEG, updatedCompQuality, stream)
updatedCompQuality -= 10
outputImageSize = stream.toByteArray().size
}
if (updatedCompQuality >= 10) {
return stream.toByteArray()
} else {
return null
}
}
return stream.toByteArray()
}
Expand Down
37 changes: 24 additions & 13 deletions ios/Classes/FastImageCompressPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ enum Constants{
static let targetWidth = "targetWidth"
static let compressionQuality = "compressionQuality"
static let imageQuality = "imageQuality"
static let imageFormat = "imageFormat"
static let high = "high"
static let low = "low"
static let medium = "medium"
static let formatJPEG = "jpeg"
static let formatPNG = "png"
}

// Enum to represent image quality levels and their associated downscaling factors
Expand Down Expand Up @@ -88,6 +91,8 @@ public class FastImageCompressPlugin: NSObject, FlutterPlugin {
// Optional arguments for compression quality and target width
let compressionQuality = args[Constants.compressionQuality] as? Int
let targetWidth = args[Constants.targetWidth] as? Int
// Get the desired output image format, default to JPEG
let imageFormat = args[Constants.imageFormat] as? String ?? Constants.formatJPEG

// Determine the image quality based on the provided string
let imageQuality: ImageQuality = {
Expand All @@ -100,7 +105,7 @@ public class FastImageCompressPlugin: NSObject, FlutterPlugin {

isCancelled = false // Reset cancellation flag
// Start resizing and compressing the image
resizeImage(imageData: imageData.data, targetWidth: targetWidth, compressionQuality: compressionQuality, imageQuality: imageQuality, result: result)
resizeImage(imageData: imageData.data, targetWidth: targetWidth, compressionQuality: compressionQuality, imageQuality: imageQuality, imageFormat: imageFormat, result: result)
}

// Handles the cancellation of image compression
Expand All @@ -112,7 +117,7 @@ public class FastImageCompressPlugin: NSObject, FlutterPlugin {
// MARK: - Image Processing

// Resizes the image and compresses it based on the provided parameters
private func resizeImage(imageData: Data, targetWidth: Int?, compressionQuality: Int? = 80, imageQuality: ImageQuality, result: FlutterResult) {
private func resizeImage(imageData: Data, targetWidth: Int?, compressionQuality: Int? = 80, imageQuality: ImageQuality, imageFormat: String, result: @escaping FlutterResult) {
// Decode the image data into a UIImage
guard let image = UIImage(data: imageData) else {
result(FlutterError(code: "INVALID_IMAGE", message: "Unable to decode image data", details: nil))
Expand All @@ -127,7 +132,7 @@ public class FastImageCompressPlugin: NSObject, FlutterPlugin {
}

// Compress the resized image
compressImage(finalImage: finalImage, imageData: imageData, compressionQuality: compressionQuality, result: result)
compressImage(finalImage: finalImage, imageData: imageData, compressionQuality: compressionQuality, imageFormat: imageFormat, result: result)
}

// Resizes the image to the target width and quality
Expand All @@ -154,17 +159,23 @@ public class FastImageCompressPlugin: NSObject, FlutterPlugin {
}

// Compresses the resized image and returns the compressed data
private func compressImage(finalImage: UIImage, imageData: Data, compressionQuality: Int?, result: FlutterResult) {
var quality = Double(compressionQuality ?? 80) / 100.0 // Default quality is 80%
var compressedData = finalImage.jpegData(compressionQuality: quality)

private func compressImage(finalImage: UIImage, imageData: Data, compressionQuality: Int?, imageFormat: String, result: FlutterResult) {
let quality = Double(compressionQuality ?? 60) / 100.0 // Default quality is 80%
let inputImageSize = imageData.count
var outputImageSize = compressedData?.count ?? Int.max

// To avoid increasing the size of an already compressed image, compare
// input and output sizes, and use lower compression quality if needed.
if outputImageSize > inputImageSize {
compressedData = performIterativeCompression(finalImage: finalImage, inputImageSize: inputImageSize, initialQuality: quality)
var compressedData: Data?

// Generate compressed data based on the requested format
if imageFormat == Constants.formatPNG {
compressedData = finalImage.pngData()
} else {
// Default to JPEG
compressedData = finalImage.jpegData(compressionQuality: quality)

// For JPEG, we can try iterative compression if the output size is larger than input
let outputImageSize = compressedData?.count ?? Int.max
if outputImageSize > inputImageSize {
compressedData = performIterativeCompression(finalImage: finalImage, inputImageSize: inputImageSize, initialQuality: quality)
}
}

// Return the compressed data or an error if compression failed
Expand Down
12 changes: 10 additions & 2 deletions lib/src/fast_image_compress.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ class FastImageCompress {
/// data or `null` if the compression is cancelled.
///
/// - [imageData]: The image data as a `Uint8List` to be compressed.
/// - [quality]: The compression quality percentage (default is 80).
/// - [quality]: The compression quality percentage (default is 60).
/// Must be a value between 0 and 100, where 100 represents the highest quality.
/// - [targetWidth]: The desired width for the compressed image (default is 500).
/// The height will be scaled proportionally to maintain the aspect ratio.
/// - [imageQuality]: The quality of the image to undergo compression. This can be
/// `ImageQuality.high`, `ImageQuality.medium`, or `ImageQuality.low` (default is `ImageQuality.medium`).
/// - [imageFormat]: The format of the output image. This can be `ImageFormat.jpeg` (default)
/// or `ImageFormat.png`. PNG format preserves transparency but may result in larger file sizes.
///
/// ### Returns:
/// A `Future<Uint8List?>` that resolves to the compressed image data as a `Uint8List`,
Expand All @@ -77,6 +79,7 @@ class FastImageCompress {
int quality = 60,
int? targetWidth = 500,
ImageQuality imageQuality = ImageQuality.medium,
ImageFormat imageFormat = ImageFormat.jpeg,
}) {
assert(
quality > 0 || quality < 100,
Expand All @@ -95,6 +98,7 @@ class FastImageCompress {
quality,
targetWidth,
imageQuality,
imageFormat,
);
}

Expand All @@ -105,14 +109,16 @@ class FastImageCompress {
/// processed in parallel batches to optimize performance.
///
/// - [images]: A list of `Uint8List` objects representing the images to be compressed.
/// - [quality]: The compression quality percentage (default is 80).
/// - [quality]: The compression quality percentage (default is 60).
/// Must be a value between 0 and 100, where 100 represents the highest quality.
/// - [targetWidth]: The desired width for the compressed images (default is 500).
/// The height will be scaled proportionally to maintain the aspect ratio.
/// - [batchSize]: The number of images to process simultaneously in a batch (default is 3).
/// Must be at least 1.
/// - [imageQuality]: The quality of the image to undergo compression. This can be
/// `ImageQuality.high`, `ImageQuality.medium`, or `ImageQuality.low` (default is `ImageQuality.medium`).
/// - [imageFormat]: The format of the output image. This can be `ImageFormat.jpeg` (default)
/// or `ImageFormat.png`. PNG format preserves transparency but may result in larger file sizes.
///
/// ### Returns:
/// A `Future` that resolves to a `List<Uint8List>` containing the compressed images.
Expand All @@ -133,6 +139,7 @@ class FastImageCompress {
int? targetWidth,
int batchSize = 3,
ImageQuality imageQuality = ImageQuality.medium,
ImageFormat imageFormat = ImageFormat.jpeg,
}) async {
assert(
quality > 0 || quality < 100,
Expand All @@ -155,6 +162,7 @@ class FastImageCompress {
quality,
targetWidth,
imageQuality,
imageFormat,
);
if (compressedImage != null) {
compressedImages.add(compressedImage);
Expand Down
19 changes: 17 additions & 2 deletions lib/src/image_compression_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@ class ImageCompressionService {
int quality,
int? targetWidth,
ImageQuality imageQuality,
) async {
[ImageFormat imageFormat = ImageFormat.jpeg]) async {
if (!isInitialized) await initializeIsolate();
final responsePort = ReceivePort();
_isolateSendPort!.send({
Constants.imageData: imageData,
Constants.quality: quality,
Constants.targetWidth: targetWidth,
Constants.port: responsePort.sendPort,
Constants.imageQuality: imageQuality
Constants.imageQuality: imageQuality,
Constants.imageFormat: imageFormat.name,
});
return await responsePort.first as Uint8List?;
}
Expand All @@ -75,13 +76,16 @@ class ImageCompressionService {
final targetWidth = message[Constants.targetWidth] as int?;
final responsePort = message[Constants.port] as SendPort;
final imageQuality = message[Constants.imageQuality] as ImageQuality;
final imageFormat = message[Constants.imageFormat] as String? ??
ImageFormat.jpeg.name;

Uint8List? processedImage;
processedImage = await imageCompress(
imageData,
targetWidth,
quality,
imageQuality.name,
imageFormat,
);

responsePort.send(processedImage);
Expand All @@ -97,13 +101,15 @@ class ImageCompressionService {
int? targetWidth,
int compressionQuality,
String imageQuality,
String imageFormat,
) async {
try {
final message = {
Constants.imageData: imageData,
Constants.targetWidth: targetWidth,
Constants.compressionQuality: compressionQuality,
Constants.imageQuality: imageQuality,
Constants.imageFormat: imageFormat,
};
final resizedImage = await platform.invokeMethod<Uint8List?>(
Constants.compressImageMethodName,
Expand All @@ -130,3 +136,12 @@ enum ImageQuality {
medium,
high;
}

/// Specifies the format of the compressed image output
enum ImageFormat {
/// JPEG format (default) - supports lossy compression with quality settings
jpeg,

/// PNG format - lossless compression that preserves transparency
png;
}
1 change: 1 addition & 0 deletions lib/values/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ class Constants {
static const String compressionQuality = 'compressionQuality';
static const String targetWidth = 'targetWidth';
static const String imageQuality = 'imageQuality';
static const String imageFormat = 'imageFormat';
static const String port = 'port';
}