diff --git a/example/lib/my_cropper.dart b/example/lib/my_cropper.dart new file mode 100644 index 0000000..230834f --- /dev/null +++ b/example/lib/my_cropper.dart @@ -0,0 +1,54 @@ +import 'dart:ui'; + +import 'package:crop_your_image/crop_your_image.dart'; +import 'package:image/image.dart' as image hide ImageFormat; + +class MyImageCropper extends ImageCropper { + const MyImageCropper(); + @override + RectValidator get rectValidator => defaultRectValidator; + + @override + RectCropper get rectCropper => _rectCropper; + + @override + CircleCropper get circleCropper => _circleCropper; +} + +final RectCropper _rectCropper = ( + image.Image original, { + required Offset topLeft, + required Size size, + required ImageFormat? outputFormat, +}) { + /// crop image with low quality + return image.encodeJpg( + quality: 10, + image.copyCrop( + original, + x: topLeft.dx.toInt(), + y: topLeft.dy.toInt(), + width: size.width.toInt(), + height: size.height.toInt(), + ), + ); +}; + +final CircleCropper _circleCropper = ( + image.Image original, { + required Offset center, + required double radius, + required ImageFormat? outputFormat, +}) { + /// crop image with low quality + /// note: jpg can't cropped circle + return image.encodeJpg( + quality: 10, + image.copyCropCircle( + original, + centerX: center.dx.toInt(), + centerY: center.dy.toInt(), + radius: radius.toInt(), + ), + ); +}; diff --git a/lib/crop_your_image.dart b/lib/crop_your_image.dart index eb98346..5c91ca8 100644 --- a/lib/crop_your_image.dart +++ b/lib/crop_your_image.dart @@ -1,13 +1,12 @@ import 'package:crop_your_image/src/logic/cropper/image_image_cropper.dart'; -import 'package:crop_your_image/src/logic/format_detector/format_detector.dart'; +import 'package:crop_your_image/src/logic/cropper/legacy_image_image_cropper.dart'; +import 'package:crop_your_image/src/logic/format_detector/default_format_detector.dart'; import 'package:crop_your_image/src/logic/parser/image_image_parser.dart'; export 'src/widget/widget.dart'; export 'src/logic/logic.dart'; final defaultImageParser = imageImageParser; - -// TODO(chooyan-eng): implement format detector if possible -const FormatDetector? defaultFormatDetector = null; - +final defaultFormatDetector = imageFormatDetector; const defaultImageCropper = ImageImageCropper(); +const legacyImageCropper = LegacyImageImageCropper(); diff --git a/lib/src/logic/cropper/default_rect_validator.dart b/lib/src/logic/cropper/default_rect_validator.dart new file mode 100644 index 0000000..8c83a69 --- /dev/null +++ b/lib/src/logic/cropper/default_rect_validator.dart @@ -0,0 +1,25 @@ +import 'dart:ui'; + +import 'package:crop_your_image/crop_your_image.dart'; +import 'package:crop_your_image/src/logic/cropper/errors.dart'; +import 'package:image/image.dart'; + +/// default implementation of [RectValidator] +/// this checks if the rect is inside the image, not negative, and not negative size +final RectValidator defaultRectValidator = + (Image original, Offset topLeft, Offset bottomRight) { + if (topLeft.dx.toInt().isNegative || + topLeft.dy.toInt().isNegative || + bottomRight.dx.toInt().isNegative || + bottomRight.dy.toInt().isNegative || + topLeft.dx.toInt() > original.width || + topLeft.dy.toInt() > original.height || + bottomRight.dx.toInt() > original.width || + bottomRight.dy.toInt() > original.height) { + return InvalidRectError(topLeft: topLeft, bottomRight: bottomRight); + } + if (topLeft.dx > bottomRight.dx || topLeft.dy > bottomRight.dy) { + return NegativeSizeError(topLeft: topLeft, bottomRight: bottomRight); + } + return null; +}; diff --git a/lib/src/logic/cropper/image_cropper.dart b/lib/src/logic/cropper/image_cropper.dart index e474cf6..1b5ec0b 100644 --- a/lib/src/logic/cropper/image_cropper.dart +++ b/lib/src/logic/cropper/image_cropper.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'dart:typed_data'; import 'dart:ui'; @@ -13,7 +14,55 @@ abstract class ImageCropper { required T original, required Offset topLeft, required Offset bottomRight, - ImageFormat outputFormat, - ImageShape shape, - }); + ImageFormat? outputFormat, + ImageShape shape = ImageShape.rectangle, + }) async { + final error = rectValidator(original, topLeft, bottomRight); + if (error != null) { + throw error; + } + + final size = Size( + bottomRight.dx - topLeft.dx, + bottomRight.dy - topLeft.dy, + ); + + return switch (shape) { + ImageShape.rectangle => rectCropper( + original, + topLeft: topLeft, + size: size, + outputFormat: outputFormat, + ), + ImageShape.circle => circleCropper( + original, + center: Offset( + topLeft.dx + size.width / 2, + topLeft.dy + size.height / 2, + ), + radius: min(size.width, size.height) / 2, + outputFormat: outputFormat, + ), + }; + } + + RectValidator get rectValidator; + RectCropper get rectCropper; + CircleCropper get circleCropper; } + +typedef RectValidator = Error? Function( + T original, Offset topLeft, Offset bottomRight); +typedef RectCropper = Uint8List Function( + T original, { + required Offset topLeft, + required Size size, + required ImageFormat? outputFormat, +}); + +typedef CircleCropper = Uint8List Function( + T original, { + required Offset center, + required double radius, + required ImageFormat? outputFormat, +}); diff --git a/lib/src/logic/cropper/image_image_cropper.dart b/lib/src/logic/cropper/image_image_cropper.dart index 25900f2..bff8f79 100644 --- a/lib/src/logic/cropper/image_image_cropper.dart +++ b/lib/src/logic/cropper/image_image_cropper.dart @@ -1,12 +1,7 @@ -import 'dart:async'; -import 'dart:math'; import 'dart:typed_data'; import 'dart:ui'; -import 'package:crop_your_image/src/logic/cropper/errors.dart'; -import 'package:crop_your_image/src/logic/cropper/image_cropper.dart'; -import 'package:crop_your_image/src/logic/format_detector/format.dart'; -import 'package:crop_your_image/src/logic/shape.dart'; +import 'package:crop_your_image/crop_your_image.dart'; import 'package:image/image.dart' hide ImageFormat; @@ -15,82 +10,58 @@ class ImageImageCropper extends ImageCropper { const ImageImageCropper(); @override - FutureOr call({ - required Image original, - required Offset topLeft, - required Offset bottomRight, - ImageFormat outputFormat = ImageFormat.jpeg, - ImageShape shape = ImageShape.rectangle, - }) { - if (topLeft.dx.toInt().isNegative || - topLeft.dy.toInt().isNegative || - bottomRight.dx.toInt().isNegative || - bottomRight.dy.toInt().isNegative || - topLeft.dx.toInt() > original.width || - topLeft.dy.toInt() > original.height || - bottomRight.dx.toInt() > original.width || - bottomRight.dy.toInt() > original.height) { - throw InvalidRectError(topLeft: topLeft, bottomRight: bottomRight); - } - if (topLeft.dx > bottomRight.dx || topLeft.dy > bottomRight.dy) { - throw NegativeSizeError(topLeft: topLeft, bottomRight: bottomRight); - } + RectCropper get rectCropper => defaultRectCropper; - final function = switch (shape) { - ImageShape.rectangle => _doCrop, - ImageShape.circle => _doCropCircle, - }; + @override + CircleCropper get circleCropper => defaultCircleCropper; - return function( - original, - topLeft: topLeft, - size: Size( - bottomRight.dx - topLeft.dx, - bottomRight.dy - topLeft.dy, - ), - ); - } + @override + RectValidator get rectValidator => defaultRectValidator; } /// process cropping image. /// this method is supposed to be called only via compute() -Uint8List _doCrop( +final RectCropper defaultRectCropper = ( Image original, { required Offset topLeft, required Size size, + required ImageFormat? outputFormat, }) { - return Uint8List.fromList( - encodePng( - copyCrop( - original, - x: topLeft.dx.toInt(), - y: topLeft.dy.toInt(), - width: size.width.toInt(), - height: size.height.toInt(), - ), + return _findCropFunc(outputFormat)( + copyCrop( + original, + x: topLeft.dx.toInt(), + y: topLeft.dy.toInt(), + width: size.width.toInt(), + height: size.height.toInt(), ), ); -} +}; /// process cropping image with circle shape. /// this method is supposed to be called only via compute() -Uint8List _doCropCircle( +final CircleCropper defaultCircleCropper = ( Image original, { - required Offset topLeft, - required Size size, + required Offset center, + required double radius, + required ImageFormat? outputFormat, }) { - final center = Point( - topLeft.dx + size.width / 2, - topLeft.dy + size.height / 2, - ); - return Uint8List.fromList( - encodePng( - copyCropCircle( - original, - centerX: center.xi, - centerY: center.yi, - radius: min(size.width, size.height) ~/ 2, - ), + return _findCropFunc(outputFormat)( + copyCropCircle( + original, + centerX: center.dx.toInt(), + centerY: center.dy.toInt(), + radius: radius.toInt(), ), ); +}; + +Uint8List Function(Image) _findCropFunc(ImageFormat? outputFormat) { + return switch (outputFormat) { + ImageFormat.bmp => encodeBmp, + ImageFormat.ico => encodeIco, + ImageFormat.jpeg => encodeJpg, + ImageFormat.png => encodePng, + _ => encodePng, + }; } diff --git a/lib/src/logic/cropper/legacy_image_image_cropper.dart b/lib/src/logic/cropper/legacy_image_image_cropper.dart new file mode 100644 index 0000000..10cd035 --- /dev/null +++ b/lib/src/logic/cropper/legacy_image_image_cropper.dart @@ -0,0 +1,58 @@ +import 'dart:ui'; + +import 'package:crop_your_image/crop_your_image.dart'; + +import 'package:image/image.dart' hide ImageFormat; + +/// an implementation of [ImageCropper] using image package +/// this implementation is legacy that behaves the same as the version 1.1.0 or earlier +/// meaning that it doesn't respect the outputFormat and always encode result as png +class LegacyImageImageCropper extends ImageCropper { + const LegacyImageImageCropper(); + + @override + RectCropper get rectCropper => legacyRectCropper; + + @override + CircleCropper get circleCropper => legacyCircleCropper; + + @override + RectValidator get rectValidator => defaultRectValidator; +} + +/// process cropping image. +/// this method is supposed to be called only via compute() +final RectCropper legacyRectCropper = ( + Image original, { + required Offset topLeft, + required Size size, + required ImageFormat? outputFormat, +}) { + return encodePng( + copyCrop( + original, + x: topLeft.dx.toInt(), + y: topLeft.dy.toInt(), + width: size.width.toInt(), + height: size.height.toInt(), + ), + ); +}; + +/// process cropping image with circle shape. +/// this method is supposed to be called only via compute() +final CircleCropper legacyCircleCropper = ( + Image original, { + required Offset center, + required double radius, + required ImageFormat? outputFormat, +}) { + return encodePng( + copyCropCircle( + original, + centerX: center.dx.toInt(), + centerY: center.dy.toInt(), + radius: radius.toInt(), + ), + ); +}; diff --git a/lib/src/logic/format_detector/default_format_detector.dart b/lib/src/logic/format_detector/default_format_detector.dart new file mode 100644 index 0000000..3640fbb --- /dev/null +++ b/lib/src/logic/format_detector/default_format_detector.dart @@ -0,0 +1,17 @@ +import 'dart:typed_data'; + +import 'package:crop_your_image/crop_your_image.dart'; +import 'package:image/image.dart' as img; + +final FormatDetector imageFormatDetector = (Uint8List data) { + final format = img.findFormatForData(data); + + return switch (format) { + img.ImageFormat.png => ImageFormat.png, + img.ImageFormat.jpg => ImageFormat.jpeg, + img.ImageFormat.webp => ImageFormat.webp, + img.ImageFormat.bmp => ImageFormat.bmp, + img.ImageFormat.ico => ImageFormat.ico, + _ => ImageFormat.png, + }; +}; diff --git a/lib/src/logic/logic.dart b/lib/src/logic/logic.dart index 9e564fe..33b023a 100644 --- a/lib/src/logic/logic.dart +++ b/lib/src/logic/logic.dart @@ -1,4 +1,5 @@ export 'cropper/image_cropper.dart'; +export 'cropper/default_rect_validator.dart'; export 'format_detector/format_detector.dart'; export 'format_detector/format.dart'; export 'parser/image_parser.dart'; diff --git a/lib/src/widget/crop.dart b/lib/src/widget/crop.dart index df44a10..bdf1e32 100644 --- a/lib/src/widget/crop.dart +++ b/lib/src/widget/crop.dart @@ -172,13 +172,14 @@ class Crop extends StatelessWidget { this.interactive = false, this.willUpdateScale, this.onHistoryChanged, - this.formatDetector = defaultFormatDetector, + FormatDetector? formatDetector, this.imageCropper = defaultImageCropper, ImageParser? imageParser, this.scrollZoomSensitivity = 0.05, this.overlayBuilder, this.filterQuality = FilterQuality.medium, - }) : this.imageParser = imageParser ?? defaultImageParser; + }) : this.imageParser = imageParser ?? defaultImageParser, + this.formatDetector = formatDetector ?? defaultFormatDetector; @override Widget build(BuildContext context) { @@ -761,14 +762,13 @@ FutureOr _cropFunc(List args) { final originalImage = args[1]; final rect = args[2] as Rect; final withCircleShape = args[3] as bool; - - // TODO(chooyan-eng): currently always PNG - // final outputFormat = args[4] as ImageFormat?; + final outputFormat = args[4] as ImageFormat; return cropper.call( original: originalImage, topLeft: Offset(rect.left, rect.top), bottomRight: Offset(rect.right, rect.bottom), shape: withCircleShape ? ImageShape.circle : ImageShape.rectangle, + outputFormat: outputFormat, ); } diff --git a/test/widget/helper.dart b/test/widget/helper.dart index 8203708..d2c4391 100644 --- a/test/widget/helper.dart +++ b/test/widget/helper.dart @@ -1,10 +1,4 @@ -import 'dart:async'; - -import 'dart:typed_data'; - import 'package:crop_your_image/src/logic/cropper/image_cropper.dart'; -import 'package:crop_your_image/src/logic/format_detector/format.dart'; -import 'package:crop_your_image/src/logic/shape.dart'; import 'package:flutter/material.dart'; Widget withMaterial(Widget widget) { @@ -20,13 +14,11 @@ Widget withMaterial(Widget widget) { /// [ImageCropper] that always fails class FailureCropper extends ImageCropper { @override - Future call({ - required dynamic original, - required Offset topLeft, - required Offset bottomRight, - ImageFormat outputFormat = ImageFormat.jpeg, - ImageShape shape = ImageShape.rectangle, - }) async { - throw Error(); - } -} \ No newline at end of file + CircleCropper get circleCropper => throw UnimplementedError(); + + @override + RectCropper get rectCropper => throw UnimplementedError(); + + @override + RectValidator get rectValidator => throw Error(); +}