diff --git a/README.md b/README.md index 5527fde..91adebc 100644 --- a/README.md +++ b/README.md @@ -154,10 +154,20 @@ p, q, r, s, t #### Scale ```dart -ScaleOption(width, height, keepRatio: keepRatio); +ScaleOption(width, height, keepRatio: keepRatio, keepWidthFirst: keepWidthFirst); ``` -After specifying the width and height, it is not clipped, but stretched to the specified width and height (Does maintain the aspect ratio of the image if wanted). +`keepRatio` and `keepWidthFirst` are optional parameters used to maintain the aspect ratio of the original image to prevent image deformation. + +`keepWidthFirst` takes effect only when `keepRatio` is true. + +The following is a brief description: + +| width | height | keepRatio | keepWidthFirst | src size | dest size | +| ----- | ------ | --------- | -------------- | --------- | --------- | +| 500 | 300 | false | true/false | 1920x1920 | 500x300 | +| 500 | 300 | true | true | 1920x1920 | 500x500 | +| 500 | 300 | true | false | 1920x1920 | 300x300 | #### AddText @@ -230,8 +240,8 @@ Support next `BlendMode`, other will be ignored. You can also see [the document | iOS/macOS | android(PorterDuff.Mode) | flutter(BlendMode) | | --------------------------- | ------------------------ | ------------------ | | kCGBlendModeClear | CLEAR | clear | -|   | SRC | src | -|   | DST | dst | +| | SRC | src | +| | DST | dst | | kCGBlendModeNormal | SRC_OVER | srcOver | | kCGBlendModeDestinationOver | DST_OVER | dstOver | | kCGBlendModeSourceIn | SRC_IN | srcIn | diff --git a/android/src/main/kotlin/com/fluttercandies/image_editor/core/ImageHandler.kt b/android/src/main/kotlin/com/fluttercandies/image_editor/core/ImageHandler.kt index 5c42d7a..d1309dd 100644 --- a/android/src/main/kotlin/com/fluttercandies/image_editor/core/ImageHandler.kt +++ b/android/src/main/kotlin/com/fluttercandies/image_editor/core/ImageHandler.kt @@ -41,15 +41,14 @@ class ImageHandler(private var bitmap: Bitmap) { } private fun handleScale(option: ScaleOption): Bitmap { - val bitmapRatio = bitmap.width.toFloat() / bitmap.height.toFloat() - val optionRatio = option.width.toFloat() / option.height.toFloat() var w = option.width var h = option.height if (option.keepRatio) { - if (bitmapRatio < optionRatio) { - w = (option.height * bitmapRatio).toInt() - } else if (bitmapRatio > optionRatio) { - h = (option.width / bitmapRatio).toInt() + val srcRatio = bitmap.width.toFloat() / bitmap.height.toFloat() + if (option.keepWidthFirst) { + h = (w / srcRatio).toInt() + } else { + w = (srcRatio * h).toInt() } } val newBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) diff --git a/android/src/main/kotlin/com/fluttercandies/image_editor/option/ScaleOption.kt b/android/src/main/kotlin/com/fluttercandies/image_editor/option/ScaleOption.kt index 8aeee0f..ee7ff7a 100644 --- a/android/src/main/kotlin/com/fluttercandies/image_editor/option/ScaleOption.kt +++ b/android/src/main/kotlin/com/fluttercandies/image_editor/option/ScaleOption.kt @@ -1,3 +1,3 @@ package com.fluttercandies.image_editor.option -data class ScaleOption(val width: Int, val height: Int, val keepRatio: Boolean) : Option +data class ScaleOption(val width: Int, val height: Int, val keepRatio: Boolean, val keepWidthFirst: Boolean) : Option diff --git a/android/src/main/kotlin/com/fluttercandies/image_editor/util/ConvertUtils.kt b/android/src/main/kotlin/com/fluttercandies/image_editor/util/ConvertUtils.kt index 7a2e804..5dbed65 100644 --- a/android/src/main/kotlin/com/fluttercandies/image_editor/util/ConvertUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/image_editor/util/ConvertUtils.kt @@ -104,7 +104,8 @@ object ConvertUtils { val w = optionMap["width"] as Int val h = optionMap["height"] as Int val keepRatio = optionMap["keepRatio"] as Boolean - return ScaleOption(w, h, keepRatio) + val keepWidthFirst = optionMap["keepWidthFirst"] as Boolean + return ScaleOption(w, h, keepRatio, keepWidthFirst) } private fun getColorOption(optionMap: Any?): ColorOption { diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index c8330b7..7c065ed 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -16,6 +16,7 @@ import 'package:image_editor/image_editor.dart' Option, OutputFormat, RotateOption; +import 'package:image_size_getter/image_size_getter.dart'; import 'const/resource.dart'; import 'widget/clip_widget.dart'; @@ -137,6 +138,8 @@ class _SimpleExamplePageState extends State { final Uint8List assetImage = await getAssetImage(); + final srcSize = ImageSizeGetter.getSize(MemoryInput(assetImage)); + print(const JsonEncoder.withIndent(' ').convert(option.toJson())); final Uint8List? result = await ImageEditor.editImage( image: assetImage, @@ -148,6 +151,10 @@ class _SimpleExamplePageState extends State { return; } + final resultSize = ImageSizeGetter.getSize(MemoryInput(result)); + + print('srcSize: $srcSize, resultSize: $resultSize'); + final MemoryImage img = MemoryImage(result); setProvider(img); } diff --git a/example/lib/widget/scale_widget.dart b/example/lib/widget/scale_widget.dart index 3717246..be64784 100644 --- a/example/lib/widget/scale_widget.dart +++ b/example/lib/widget/scale_widget.dart @@ -17,10 +17,13 @@ class _ScaleWidgetState extends State { int width = 1920; int height = 1920; + bool keepRatio = false; + bool keepWidthFirst = true; + @override Widget build(BuildContext context) { return ExpandContainer( - title: 'width: $width, height: $height', + title: 'scale', child: Column( children: [ Slider( @@ -28,6 +31,7 @@ class _ScaleWidgetState extends State { value: width.toDouble(), min: 1, max: 1920, + label: 'width', ), Slider( onChanged: (double v) => setState(() => height = v.toInt()), @@ -35,11 +39,23 @@ class _ScaleWidgetState extends State { min: 1, max: 1920, ), + CheckboxListTile( + title: Text('keep ratio'), + value: keepRatio, + onChanged: (bool? v) => setState(() => keepRatio = v == true), + ), + CheckboxListTile( + title: Text( + keepWidthFirst ? 'keep width first' : 'keep height first', + ), + value: keepWidthFirst, + onChanged: (bool? v) => setState(() => keepWidthFirst = v == true), + ), SizedBox( width: double.infinity, child: TextButton( child: const Text('scale'), - onPressed: _rotate, + onPressed: _scale, ), ), ], @@ -47,8 +63,13 @@ class _ScaleWidgetState extends State { ); } - void _rotate() { - final ScaleOption opt = ScaleOption(width, height); + void _scale() { + final ScaleOption opt = ScaleOption( + width, + height, + keepRatio: keepRatio, + keepWidthFirst: keepWidthFirst, + ); widget.onTap?.call(opt); } } diff --git a/ios/Classes/EditorUIImageHandler.m b/ios/Classes/EditorUIImageHandler.m index 877101c..f5638de 100644 --- a/ios/Classes/EditorUIImageHandler.m +++ b/ios/Classes/EditorUIImageHandler.m @@ -8,6 +8,68 @@ #import "EditorUIImageHandler.h" #import + +void releaseCGContext(CGContextRef ref) { + char *bitmapData = CGBitmapContextGetData(ref); + if (bitmapData) free(bitmapData); + CGContextRelease(ref); +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" +CGContextRef createCGContext(size_t pixelsWide, size_t pixelsHigh) { + CGContextRef context = NULL; + CGColorSpaceRef colorSpace; + void *bitmapData; + size_t bitmapByteCount; + size_t bitmapBytesPerRow; + + bitmapBytesPerRow = (pixelsWide * 4);// 1 + bitmapByteCount = (bitmapBytesPerRow * pixelsHigh); + + colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);// 2 + bitmapData = calloc(bitmapByteCount, sizeof(uint8_t));// 3 + if (bitmapData == NULL) { + fprintf(stderr, "Memory not allocated!"); + return NULL; + } + context = CGBitmapContextCreate(bitmapData,// 4 + pixelsWide, + pixelsHigh, + 8, // bits per component + bitmapBytesPerRow, + colorSpace, + kCGImageAlphaPremultipliedLast); + if (context == NULL) { + free(bitmapData);// 5 + fprintf(stderr, "Context not created!"); + return NULL; + } + CGColorSpaceRelease(colorSpace);// 6 + + return context;// 7 +} + +#pragma clang diagnostic pop + +CGSize CGImageGetSize(CGImageRef ref) { + size_t w = CGImageGetWidth(ref); + size_t h = CGImageGetHeight(ref); + return CGSizeMake(w, h); +} + +UIImage *getImageFromCGContext(CGContextRef context) { + size_t h = CGBitmapContextGetHeight(context); + size_t w = CGBitmapContextGetWidth(context); + + CGImageRef pImage = CGBitmapContextCreateImage(context); + + UIImage *image = [[UIImage alloc] initWithCGImage:pImage]; + CGImageRelease(pImage); + return image; +} + + @implementation EditorUIImageHandler { UIImage *outImage; } @@ -211,29 +273,31 @@ - (void)scale:(FIScaleOption *)option { if (!outImage) { return; } - float imageRatio = outImage.size.width / outImage.size.height; - float optionRatio = option.width / option.height; - int w = option.width; - int h = option.height; + + CGImageRef srcImage = outImage.CGImage; + + double width = option.width; + double height = option.height; + if (option.keepRatio) { - if (imageRatio < optionRatio) { - w = option.height * imageRatio; - } else if (imageRatio > optionRatio) { - h = option.width / imageRatio; + CGSize srcSize = CGImageGetSize(srcImage); + + double srcRatio = srcSize.width / srcSize.height; + + if (option.keepWidthFirst) { + height = width / srcRatio; + } else { + width = srcRatio * height; } } - UIGraphicsBeginImageContext(CGSizeMake(w, h)); - CGContextRef ctx = UIGraphicsGetCurrentContext(); - if (!ctx) { - return; - } - [outImage drawInRect:CGRectMake(0, 0, w, h)]; - UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - if (!newImage) { - return; - } - outImage = newImage; + + CGContextRef context = createCGContext((size_t) width, (size_t) height); + + CGContextDrawImage(context, CGRectMake(0, 0, width, height), srcImage); + + outImage = getImageFromCGContext(context); + + releaseCGContext(context); } #pragma mark add text diff --git a/ios/Classes/FIConvertUtils.h b/ios/Classes/FIConvertUtils.h index 56faf19..5e4c8b1 100644 --- a/ios/Classes/FIConvertUtils.h +++ b/ios/Classes/FIConvertUtils.h @@ -66,6 +66,7 @@ NS_ASSUME_NONNULL_BEGIN @property(assign, nonatomic) int width; @property(assign, nonatomic) int height; @property(assign, nonatomic) BOOL keepRatio; +@property(assign, nonatomic) BOOL keepWidthFirst; @end diff --git a/ios/Classes/FIConvertUtils.m b/ios/Classes/FIConvertUtils.m index d907f8e..52f84ff 100644 --- a/ios/Classes/FIConvertUtils.m +++ b/ios/Classes/FIConvertUtils.m @@ -108,6 +108,7 @@ + (id)createFromDict:(NSDictionary *)dict { option.width = [dict[@"width"] intValue]; option.height = [dict[@"height"] intValue]; option.keepRatio = [dict[@"keepRatio"] boolValue]; + option.keepWidthFirst = [dict[@"keepWidthFirst"] boolValue]; return option; } diff --git a/lib/src/option/scale.dart b/lib/src/option/scale.dart index 076799b..6f22b0f 100644 --- a/lib/src/option/scale.dart +++ b/lib/src/option/scale.dart @@ -5,11 +5,13 @@ class ScaleOption implements Option { this.width, this.height, { this.keepRatio = false, + this.keepWidthFirst = true, }) : assert(width > 0 && height > 0); final int width; final int height; final bool keepRatio; + final bool keepWidthFirst; @override bool get canIgnore => false; @@ -23,6 +25,7 @@ class ScaleOption implements Option { 'width': width, 'height': height, 'keepRatio': keepRatio, + 'keepWidthFirst': keepWidthFirst, }; } } diff --git a/macos/Classes/FIConvertUtils.h b/macos/Classes/FIConvertUtils.h index 44a7195..2b70715 100644 --- a/macos/Classes/FIConvertUtils.h +++ b/macos/Classes/FIConvertUtils.h @@ -54,6 +54,8 @@ NS_ASSUME_NONNULL_BEGIN @interface FIScaleOption : NSObject @property(assign, nonatomic) int width; @property(assign, nonatomic) int height; +@property(assign, nonatomic) BOOL keepRatio; +@property(assign, nonatomic) BOOL keepWidthFirst; @end @interface FIAddText : NSObject diff --git a/macos/Classes/FIConvertUtils.m b/macos/Classes/FIConvertUtils.m index 10606cf..93d6eb5 100644 --- a/macos/Classes/FIConvertUtils.m +++ b/macos/Classes/FIConvertUtils.m @@ -106,6 +106,8 @@ + (id)createFromDict:(NSDictionary *)dict { FIScaleOption *option = [FIScaleOption new]; option.width = [dict[@"width"] intValue]; option.height = [dict[@"height"] intValue]; + option.keepRatio = [dict[@"keepRatio"] boolValue]; + option.keepWidthFirst = [dict[@"keepWidthFirst"] boolValue]; return option; } diff --git a/macos/Classes/FIUIImageHandler.m b/macos/Classes/FIUIImageHandler.m index 65cbb86..99066d7 100644 --- a/macos/Classes/FIUIImageHandler.m +++ b/macos/Classes/FIUIImageHandler.m @@ -52,8 +52,15 @@ CGContextRef createCGContext(size_t pixelsWide, size_t pixelsHigh) { return context;// 7 } + #pragma clang diagnostic pop +CGSize CGImageGetSize(CGImageRef ref) { + size_t w = CGImageGetWidth(ref); + size_t h = CGImageGetHeight(ref); + return CGSizeMake(w, h); +} + FIImage *getImageFromCGContext(CGContextRef context) { size_t h = CGBitmapContextGetHeight(context); size_t w = CGBitmapContextGetWidth(context); @@ -345,12 +352,26 @@ - (void)scale:(FIScaleOption *)option { return; } + CGImageRef srcImage = outImage.CGImage; + double width = option.width; double height = option.height; + if (option.keepRatio) { + CGSize srcSize = CGImageGetSize(srcImage); + + double srcRatio = srcSize.width / srcSize.height; + + if (option.keepWidthFirst) { + height = width / srcRatio; + } else { + width = srcRatio * height; + } + } + CGContextRef context = createCGContext((size_t) width, (size_t) height); - CGContextDrawImage(context, CGRectMake(0, 0, width, height), [outImage CGImage]); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), srcImage); outImage = [self getImageWith:context];