diff --git a/README.md b/README.md index c9ac5db..361536b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Future compressImage(Uint8List imageData) async { quality: 60, targetWidth: 400, imageQuality: ImageQuality.low, + imageFormat: ImageFormat.jpeg, ); return compressedImage; } @@ -76,6 +77,7 @@ Future> compressMultipleImages(List imageList) async targetWidth: 800, batchSize: 3, imageQuality: ImageQuality.low, + imageFormat: ImageFormat.jpeg, ); return result; } @@ -88,21 +90,13 @@ Future _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 @@ -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. @@ -125,6 +120,13 @@ 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: @@ -132,6 +134,12 @@ The `imageQuality` parameter controls the quality of the processed images. It ha - 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. diff --git a/android/src/main/kotlin/com/example/fast_image_compress/Constants.kt b/android/src/main/kotlin/com/example/fast_image_compress/Constants.kt index af97e75..146ec10 100644 --- a/android/src/main/kotlin/com/example/fast_image_compress/Constants.kt +++ b/android/src/main/kotlin/com/example/fast_image_compress/Constants.kt @@ -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" } diff --git a/android/src/main/kotlin/com/example/fast_image_compress/FastImageCompressPlugin.kt b/android/src/main/kotlin/com/example/fast_image_compress/FastImageCompressPlugin.kt index 8068d2d..f818bf7 100644 --- a/android/src/main/kotlin/com/example/fast_image_compress/FastImageCompressPlugin.kt +++ b/android/src/main/kotlin/com/example/fast_image_compress/FastImageCompressPlugin.kt @@ -68,6 +68,8 @@ class FastImageCompressPlugin: FlutterPlugin, MethodCallHandler { val targetWidth = call.argument(Constants.TARGET_WIDTH) val compressionQuality = call.argument(Constants.COMPRESSION_QUALITY) val imageQualityInString = call.argument(Constants.IMAGE_QUALITY) + // Get the desired output image format, default to JPEG + val imageFormat = call.argument(Constants.IMAGE_FORMAT) ?: Constants.FORMAT_JPEG // Determine the image quality based on the input string val imageQuality = @@ -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 @@ -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() } diff --git a/ios/Classes/FastImageCompressPlugin.swift b/ios/Classes/FastImageCompressPlugin.swift index a693b20..580dd78 100644 --- a/ios/Classes/FastImageCompressPlugin.swift +++ b/ios/Classes/FastImageCompressPlugin.swift @@ -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 @@ -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 = { @@ -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 @@ -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)) @@ -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 @@ -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 diff --git a/lib/src/fast_image_compress.dart b/lib/src/fast_image_compress.dart index c04b71f..10ea0b8 100644 --- a/lib/src/fast_image_compress.dart +++ b/lib/src/fast_image_compress.dart @@ -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` that resolves to the compressed image data as a `Uint8List`, @@ -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, @@ -95,6 +98,7 @@ class FastImageCompress { quality, targetWidth, imageQuality, + imageFormat, ); } @@ -105,7 +109,7 @@ 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. @@ -113,6 +117,8 @@ class FastImageCompress { /// 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` containing the compressed images. @@ -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, @@ -155,6 +162,7 @@ class FastImageCompress { quality, targetWidth, imageQuality, + imageFormat, ); if (compressedImage != null) { compressedImages.add(compressedImage); diff --git a/lib/src/image_compression_service.dart b/lib/src/image_compression_service.dart index 2e2335a..7ee2f14 100644 --- a/lib/src/image_compression_service.dart +++ b/lib/src/image_compression_service.dart @@ -46,7 +46,7 @@ class ImageCompressionService { int quality, int? targetWidth, ImageQuality imageQuality, - ) async { + [ImageFormat imageFormat = ImageFormat.jpeg]) async { if (!isInitialized) await initializeIsolate(); final responsePort = ReceivePort(); _isolateSendPort!.send({ @@ -54,7 +54,8 @@ class ImageCompressionService { 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?; } @@ -75,6 +76,8 @@ 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( @@ -82,6 +85,7 @@ class ImageCompressionService { targetWidth, quality, imageQuality.name, + imageFormat, ); responsePort.send(processedImage); @@ -97,6 +101,7 @@ class ImageCompressionService { int? targetWidth, int compressionQuality, String imageQuality, + String imageFormat, ) async { try { final message = { @@ -104,6 +109,7 @@ class ImageCompressionService { Constants.targetWidth: targetWidth, Constants.compressionQuality: compressionQuality, Constants.imageQuality: imageQuality, + Constants.imageFormat: imageFormat, }; final resizedImage = await platform.invokeMethod( Constants.compressImageMethodName, @@ -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; +} diff --git a/lib/values/constants.dart b/lib/values/constants.dart index df8c0cb..b05f368 100644 --- a/lib/values/constants.dart +++ b/lib/values/constants.dart @@ -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'; }