• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

icapps / flutter-custom-image-crop / 6545506866

17 Oct 2023 09:53AM UTC coverage: 0.0%. Remained the same
6545506866

Pull #45

github

web-flow
Merge bff8ba3a1 into 75def6bbd
Pull Request #45: optimize fillcropscpace imagefit

130 of 130 new or added lines in 3 files covered. (100.0%)

0 of 485 relevant lines covered (0.0%)

0.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lib/src/widgets/custom_image_crop_widget.dart
1
import 'dart:async';
2
import 'dart:math';
3
import 'dart:ui' as ui;
4

5
import 'package:custom_image_crop/custom_image_crop.dart';
6
import 'package:custom_image_crop/src/calculators/calculate_crop_fit_params.dart';
7
import 'package:custom_image_crop/src/calculators/calculate_on_crop_params.dart';
8
import 'package:custom_image_crop/src/clippers/inverted_clipper.dart';
9
import 'package:flutter/material.dart';
10
import 'package:gesture_x_detector/gesture_x_detector.dart';
11
import 'package:vector_math/vector_math_64.dart' as vector_math;
12

13
/// An image cropper that is customizable.
14
/// You can rotate, scale and translate either
15
/// through gestures or a controller
16
class CustomImageCrop extends StatefulWidget {
17
  /// The image to crop
18
  final ImageProvider image;
19

20
  /// The controller that handles the cropping and
21
  /// changing of the cropping area
22
  final CustomImageCropController cropController;
23

24
  /// The color behind the cropping area
25
  final Color backgroundColor;
26

27
  /// The color in front of the cropped area
28
  final Color overlayColor;
29

30
  /// The shape of the cropping area.
31
  /// Possible values:
32
  /// - [CustomCropShape.Circle] Crop area will be circular.
33
  /// - [CustomCropShape.Square] Crop area will be a square.
34
  /// - [CustomCropShape.Ratio] Crop area will have a specified aspect ratio.
35
  final CustomCropShape shape;
36

37
  /// Ratio of the cropping area.
38
  /// If [shape] is set to [CustomCropShape.Ratio], this property is required.
39
  /// For example, to create a square crop area, use [Ratio(width: 1, height: 1)].
40
  /// To create a rectangular crop area with a 16:9 aspect ratio, use [Ratio(width: 16, height: 9)].
41
  final Ratio? ratio;
42

43
  /// How to fit image inside visible space
44
  final CustomImageFit imageFit;
45

46
  /// The percentage of the available area that is
47
  /// reserved for the cropping area
48
  final double cropPercentage;
49

50
  /// The path drawer of the border see [DottedCropPathPainter],
51
  /// [SolidPathPainter] for more details or how to implement a
52
  /// custom one
53
  final CustomPaint Function(Path, {Paint? pathPaint}) drawPath;
54

55
  /// Custom paint options for drawing the cropping border.
56
  ///
57
  /// If [paint] is provided, it will be used for customizing the appearance
58
  /// of the cropping border.
59
  ///
60
  /// If [paint] is not provided, default values will be used:
61
  /// - Color: [Colors.white]
62
  /// - Stle [PaintingStyle.stroke]
63
  /// - Stroke Join [StrokeJoin.round]
64
  /// - Stroke Width: 4.0
65
  final Paint? pathPaint;
66

67
  /// The radius for rounded corners of the cropping area (only applicable to rounded rectangle shapes).
68
  final double borderRadius;
69

70
  /// Whether to allow the image to be rotated.
71
  final bool canRotate;
72

73
  /// Determines whether scaling gesture is disabled.
74
  ///
75
  /// By default, scaling is enabled.
76
  /// Set [canScale] to `false` to disable scaling.
77
  final bool canScale;
78

79
  /// Determines whether moving gesture overlay is disabled.
80
  ///
81
  /// By default, moving is enabled.
82
  /// Set [canMove] to `false` to disable move.
83
  final bool canMove;
84

85
  /// The paint used when drawing an image before cropping
86
  final Paint imagePaintDuringCrop;
87

88
  /// This widget is used to specify a custom progress indicator
89
  final Widget? customProgressIndicator;
90

91
  /// Whether to clip the area outside of the path when cropping
92
  /// By default, the value is `true`
93
  final bool clipShapeOnCrop;
94

95
  /// Whether image area must cover clip path
96
  /// By default, the value is `false`
97
  /// If use CustomCropShape.circle, the cropped image may have white blank.
98
  final bool forceInsideCropArea;
99

100
  /// A custom image cropper widget
101
  ///
102
  /// Uses a `CustomImageCropController` to crop the image.
103
  /// With the controller you can rotate, translate and/or
104
  /// scale with buttons and sliders. This can also be
105
  /// achieved with gestures
106
  ///
107
  /// Use a `shape` with `CustomCropShape.Circle` or
108
  /// `CustomCropShape.Square`
109
  ///
110
  /// You can increase the cropping area using `cropPercentage`
111
  ///
112
  /// Change the cropping border by changing `drawPath`,
113
  /// we've provided two default painters as inspiration
114
  /// `DottedCropPathPainter.drawPath` and
115
  /// `SolidCropPathPainter.drawPath`
116
  CustomImageCrop({
×
117
    required this.image,
118
    required this.cropController,
119
    this.overlayColor = const Color.fromRGBO(0, 0, 0, 0.5),
120
    this.backgroundColor = Colors.white,
121
    this.shape = CustomCropShape.Circle,
122
    this.imageFit = CustomImageFit.fitCropSpace,
123
    this.cropPercentage = 0.8,
124
    this.drawPath = DottedCropPathPainter.drawPath,
125
    this.pathPaint,
126
    this.canRotate = true,
127
    this.canScale = true,
128
    this.canMove = true,
129
    this.clipShapeOnCrop = true,
130
    this.customProgressIndicator,
131
    this.ratio,
132
    this.borderRadius = 0,
133
    Paint? imagePaintDuringCrop,
134
    this.forceInsideCropArea = false,
135
    Key? key,
136
  })  : this.imagePaintDuringCrop = imagePaintDuringCrop ??
137
            (Paint()..filterQuality = FilterQuality.high),
×
138
        assert(
139
          !(shape == CustomCropShape.Ratio && ratio == null),
×
140
          "If shape is set to Ratio, ratio should not be null.",
141
        ),
142
        super(key: key);
×
143

144
  @override
×
145
  _CustomImageCropState createState() => _CustomImageCropState();
×
146
}
147

148
class _CustomImageCropState extends State<CustomImageCrop>
149
    with CustomImageCropListener {
150
  CropImageData? _dataTransitionStart;
151
  late Path _path;
152
  late double _width, _height;
153
  ui.Image? _imageAsUIImage;
154
  ImageStream? _imageStream;
155
  ImageStreamListener? _imageListener;
156

157
  @override
×
158
  void initState() {
159
    super.initState();
×
160
    widget.cropController.addListener(this);
×
161
  }
162

163
  @override
×
164
  void didChangeDependencies() {
165
    super.didChangeDependencies();
×
166
    _getImage();
×
167
  }
168

169
  @override
×
170
  void didUpdateWidget(CustomImageCrop oldWidget) {
171
    super.didUpdateWidget(oldWidget);
×
172
    if (oldWidget.image != widget.image) _getImage();
×
173
  }
174

175
  void _getImage() {
×
176
    final oldImageStream = _imageStream;
×
177
    _imageStream = widget.image.resolve(createLocalImageConfiguration(context));
×
178
    if (_imageStream?.key != oldImageStream?.key) {
×
179
      if (_imageListener != null) {
×
180
        oldImageStream?.removeListener(_imageListener!);
×
181
      }
182
      _imageListener = ImageStreamListener(_updateImage);
×
183
      _imageStream?.addListener(_imageListener!);
×
184
    }
185
  }
186

187
  void _updateImage(ImageInfo imageInfo, _) {
×
188
    setState(() {
×
189
      _imageAsUIImage = imageInfo.image;
×
190
    });
191
  }
192

193
  @override
×
194
  void dispose() {
195
    if (_imageListener != null) {
×
196
      _imageStream?.removeListener(_imageListener!);
×
197
    }
198
    widget.cropController.removeListener(this);
×
199
    super.dispose();
×
200
  }
201

202
  @override
×
203
  Widget build(BuildContext context) {
204
    final image = _imageAsUIImage;
×
205
    if (image == null) {
206
      return Center(
×
207
        child: widget.customProgressIndicator ?? CircularProgressIndicator(),
×
208
      );
209
    }
210
    return LayoutBuilder(
×
211
      builder: (context, constraints) {
×
212
        _width = constraints.maxWidth;
×
213
        _height = constraints.maxHeight;
×
214
        final cropFitParams = calculateCropFitParams(
×
215
          cropPercentage: widget.cropPercentage,
×
216
          imageFit: widget.imageFit,
×
217
          imageHeight: image.height,
×
218
          imageWidth: image.width,
×
219
          screenHeight: _height,
×
220
          screenWidth: _width,
×
221
          aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
×
222
        );
223
        final scale = data.scale * cropFitParams.additionalScale;
×
224
        _path = _getPath(
×
225
          cropWidth: cropFitParams.cropSizeWidth,
×
226
          cropHeight: cropFitParams.cropSizeHeight,
×
227
          width: _width,
×
228
          height: _height,
×
229
          borderRadius: widget.borderRadius,
×
230
        );
231
        return XGestureDetector(
×
232
          onMoveStart: onMoveStart,
×
233
          onMoveUpdate: onMoveUpdate,
×
234
          onScaleStart: onScaleStart,
×
235
          onScaleUpdate: onScaleUpdate,
×
236
          child: Container(
×
237
            width: _width,
×
238
            height: _height,
×
239
            color: widget.backgroundColor,
×
240
            child: Stack(
×
241
              children: [
×
242
                Positioned(
×
243
                  left: data.x + _width / 2,
×
244
                  top: data.y + _height / 2,
×
245
                  child: Transform(
×
246
                    transform: Matrix4.diagonal3(
×
247
                        vector_math.Vector3(scale, scale, scale))
×
248
                      ..rotateZ(data.angle)
×
249
                      ..translate(-image.width / 2, -image.height / 2),
×
250
                    child: Image(
×
251
                      image: widget.image,
×
252
                    ),
253
                  ),
254
                ),
255
                IgnorePointer(
×
256
                  child: ClipPath(
×
257
                    clipper: InvertedClipper(_path, _width, _height),
×
258
                    child: Container(
×
259
                      color: widget.overlayColor,
×
260
                    ),
261
                  ),
262
                ),
263
                widget.drawPath(_path, pathPaint: widget.pathPaint),
×
264
              ],
265
            ),
266
          ),
267
        );
268
      },
269
    );
270
  }
271

272
  void onScaleStart(_) {
×
273
    _dataTransitionStart = null; // Reset for update
×
274
  }
275

276
  void onScaleUpdate(ScaleEvent event) {
×
277
    final scale =
278
        widget.canScale ? event.scale : (_dataTransitionStart?.scale ?? 1.0);
×
279

280
    final angle = widget.canRotate ? event.rotationAngle : 0.0;
×
281

282
    if (_dataTransitionStart != null) {
×
283
      widget.cropController.addTransition(
×
284
        _dataTransitionStart! -
×
285
            CropImageData(
×
286
              scale: scale,
287
              angle: angle,
288
            ),
289
      );
290
    }
291
    _dataTransitionStart = CropImageData(
×
292
      scale: scale,
293
      angle: angle,
294
    );
295
  }
296

297
  void onMoveStart(_) {
×
298
    _dataTransitionStart = null; // Reset for update
×
299
  }
300

301
  void onMoveUpdate(MoveEvent event) {
×
302
    if (!widget.canMove) return;
×
303

304
    widget.cropController
×
305
        .addTransition(CropImageData(x: event.delta.dx, y: event.delta.dy));
×
306
  }
307

308
  Rect _getInitialImageRect() {
×
309
    assert(_imageAsUIImage != null);
×
310
    final image = _imageAsUIImage!;
×
311
    final cropFitParams = calculateCropFitParams(
×
312
      cropPercentage: widget.cropPercentage,
×
313
      imageFit: widget.imageFit,
×
314
      imageHeight: image.height,
×
315
      imageWidth: image.width,
×
316
      screenHeight: _height,
×
317
      screenWidth: _width,
×
318
      aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
×
319
    );
320
    final initialWidth = _imageAsUIImage!.width * cropFitParams.additionalScale;
×
321
    final initialHeight =
322
        _imageAsUIImage!.height * cropFitParams.additionalScale;
×
323
    return Rect.fromLTWH(
×
324
      (_width - initialWidth) / 2,
×
325
      (_height - initialHeight) / 2,
×
326
      initialWidth,
327
      initialHeight,
328
    );
329
  }
330

331
  void _correctTransition(CropImageData transition, VoidCallback callback) {
×
332
    if (!widget.forceInsideCropArea || _imageAsUIImage == null) {
×
333
      callback();
×
334
      return;
335
    }
336
    final startX = data.x;
×
337
    final startY = data.y;
×
338
    callback();
×
339
    final pathRect = _path.getBounds();
×
340
    final initialImageRect = _getInitialImageRect();
×
341
    bool isContainPath = _isContainPath(initialImageRect, pathRect, data.scale);
×
342
    bool isRotated = data.angle != 0;
×
343

344
    if (isContainPath) {
345
      return;
346
    }
347

348
    if (transition.x != 0 || transition.y != 0) {
×
349
      if (isRotated) {
350
        _addTransitionInternal(
×
351
            CropImageData(x: startX - data.x, y: startY - data.y));
×
352
      } else {
353
        final imageRect = _getImageRect(initialImageRect, data.scale);
×
354
        double deltaX = min(pathRect.left - imageRect.left, 0);
×
355
        deltaX = pathRect.right > imageRect.right
×
356
            ? pathRect.right - imageRect.right
×
357
            : deltaX;
358
        double deltaY = min(pathRect.top - imageRect.top, 0);
×
359
        deltaY = pathRect.bottom > imageRect.bottom
×
360
            ? pathRect.bottom - imageRect.bottom
×
361
            : deltaY;
362
        _addTransitionInternal(CropImageData(x: deltaX, y: deltaY));
×
363
      }
364
      return;
365
    }
366
    double minEdgeHalf =
367
        min(initialImageRect.width, initialImageRect.height) / 2;
×
368
    double adaptScale = _calculateScaleAfterRotate(
×
369
        pathRect, data.scale, initialImageRect, minEdgeHalf);
×
370
    _addTransitionInternal(CropImageData(scale: adaptScale / data.scale));
×
371
  }
372

373
  Rect _getImageRect(Rect initialImageRect, double currentScale) {
×
374
    final diffScale = (1 - currentScale) / 2;
×
375
    final left =
376
        initialImageRect.left + diffScale * initialImageRect.width + data.x;
×
377
    final top =
378
        initialImageRect.top + diffScale * initialImageRect.height + data.y;
×
379
    Rect imageRect = Rect.fromLTWH(
×
380
        left,
381
        top,
382
        currentScale * initialImageRect.width,
×
383
        currentScale * initialImageRect.height);
×
384
    return imageRect;
385
  }
386

387
  double _getDistanceBetweenPointAndLine(
×
388
      Offset point, Offset lineStart, Offset lineEnd) {
389
    if (lineEnd.dy == lineStart.dy) {
×
390
      return (point.dy - lineStart.dy).abs();
×
391
    }
392
    if (lineEnd.dx == lineStart.dx) {
×
393
      return (point.dx - lineStart.dx).abs();
×
394
    }
395
    double line1Slop =
396
        (lineEnd.dy - lineStart.dy) / (lineEnd.dx - lineStart.dx);
×
397
    double line1Delta = lineEnd.dy - lineEnd.dx * line1Slop;
×
398
    double line2Slop = -1 / line1Slop;
×
399
    double line2Delta = point.dy - point.dx * line2Slop;
×
400
    double crossPointX = (line2Delta - line1Delta) / (line1Slop - line2Slop);
×
401
    double crossPointY = line1Slop * crossPointX + line1Delta;
×
402
    return (Offset(crossPointX, crossPointY) - point).distance;
×
403
  }
404

405
  bool _isContainPath(
×
406
      Rect initialImageRect, Rect pathRect, double currentScale) {
407
    final imageRect = _getImageRect(initialImageRect, currentScale);
×
408
    Offset topLeft, topRight, bottomLeft, bottomRight;
409
    final rad = atan(imageRect.height / imageRect.width);
×
410
    final len =
411
        sqrt(pow(imageRect.width / 2, 2) + pow(imageRect.height / 2, 2));
×
412
    bool isRotated = data.angle != 0;
×
413

414
    if (isRotated) {
415
      final clockAngle = rad + data.angle;
×
416
      final counterClockAngle = rad - data.angle;
×
417
      final cosClockValue = len * cos(clockAngle);
×
418
      final sinClockValue = len * sin(clockAngle);
×
419
      final cosCounterClockValue = len * cos(counterClockAngle);
×
420
      final sinCounterClockValue = len * sin(counterClockAngle);
×
421
      bottomRight = imageRect.center.translate(cosClockValue, sinClockValue);
×
422
      topRight = imageRect.center
×
423
          .translate(cosCounterClockValue, -sinCounterClockValue);
×
424
      topLeft = imageRect.center.translate(-cosClockValue, -sinClockValue);
×
425
      bottomLeft = imageRect.center
×
426
          .translate(-cosCounterClockValue, sinCounterClockValue);
×
427
    } else {
428
      bottomRight = imageRect.bottomRight;
×
429
      topRight = imageRect.topRight;
×
430
      topLeft = imageRect.topLeft;
×
431
      bottomLeft = imageRect.bottomLeft;
×
432
    }
433

434
    if (widget.shape == CustomCropShape.Circle) {
×
435
      final anchor = max(pathRect.width, pathRect.height) / 2;
×
436
      final pathCenter = pathRect.center;
×
437
      return _getDistanceBetweenPointAndLine(pathCenter, topLeft, topRight) >=
×
438
              anchor &&
439
          _getDistanceBetweenPointAndLine(pathCenter, topRight, bottomRight) >=
×
440
              anchor &&
441
          _getDistanceBetweenPointAndLine(
×
442
                  pathCenter, bottomLeft, bottomRight) >=
×
443
              anchor &&
444
          _getDistanceBetweenPointAndLine(pathCenter, topLeft, bottomLeft) >=
×
445
              anchor;
446
    }
447

448
    if (isRotated) {
449
      Path imagePath = Path()
×
450
        ..moveTo(topLeft.dx, topLeft.dy)
×
451
        ..lineTo(topRight.dx, topRight.dy)
×
452
        ..lineTo(bottomRight.dx, bottomRight.dy)
×
453
        ..lineTo(bottomLeft.dx, bottomLeft.dy)
×
454
        ..close();
×
455
      return imagePath.contains(pathRect.topLeft) &&
×
456
          imagePath.contains(pathRect.topRight) &&
×
457
          imagePath.contains(pathRect.bottomLeft) &&
×
458
          imagePath.contains(pathRect.bottomRight);
×
459
    } else {
460
      return imageRect.contains(pathRect.topLeft) &&
×
461
          imageRect.contains(pathRect.topRight) &&
×
462
          imageRect.contains(pathRect.bottomLeft) &&
×
463
          imageRect.contains(pathRect.bottomRight);
×
464
    }
465
  }
466

467
  double _calculateScaleAfterRotate(Rect pathRect, double startScale,
×
468
      Rect initialImageRect, double minEdgeHalf) {
469
    final imageCenter = initialImageRect.center.translate(data.x, data.y);
×
470
    final topLeftDistance = (pathRect.topLeft - imageCenter).distance;
×
471
    final topRightDistance = (pathRect.topRight - imageCenter).distance;
×
472
    final bottomLeftDistance = (pathRect.bottomLeft - imageCenter).distance;
×
473
    final bottomRightDistance = (pathRect.bottomRight - imageCenter).distance;
×
474
    final maxDistance = max(
×
475
        max(max(topLeftDistance, topRightDistance), bottomLeftDistance),
×
476
        bottomRightDistance);
477
    double endScale = maxDistance / minEdgeHalf;
×
478

479
    if (startScale >= endScale) {
×
480
      return endScale;
481
    }
482

483
    ///use binary search to find best scale which just contain path.
484
    ///Also, we can use imageCenter、imageLine(longest one) and path vertex to calculate.
485
    double step = 1 / minEdgeHalf;
×
486

487
    while ((endScale - startScale).abs() > step) {
×
488
      double midScale = (endScale + startScale) / 2;
×
489

490
      if (_isContainPath(initialImageRect, pathRect, midScale)) {
×
491
        endScale = midScale;
492
      } else {
493
        startScale = midScale + step;
×
494
      }
495
    }
496
    return endScale;
497
  }
498

499
  Path _getPath({
×
500
    required double cropWidth,
501
    required double cropHeight,
502
    required double width,
503
    required double height,
504
    required double borderRadius,
505
    bool clipShape = true,
506
  }) {
507
    if (!clipShape) {
508
      return Path()
×
509
        ..addRect(
×
510
          Rect.fromCenter(
×
511
            center: Offset(width / 2, height / 2),
×
512
            width: cropWidth,
513
            height: cropHeight,
514
          ),
515
        );
516
    }
517

518
    switch (widget.shape) {
×
519
      case CustomCropShape.Circle:
×
520
        return Path()
×
521
          ..addOval(
×
522
            Rect.fromCircle(
×
523
              center: Offset(width / 2, height / 2),
×
524
              radius: cropWidth / 2,
×
525
            ),
526
          );
527
      case CustomCropShape.Ratio:
×
528
        return Path()
×
529
          ..addRRect(
×
530
            RRect.fromRectAndRadius(
×
531
              Rect.fromCenter(
×
532
                center: Offset(width / 2, height / 2),
×
533
                width: cropWidth,
534
                height: cropHeight,
535
              ),
536
              Radius.circular(borderRadius),
×
537
            ),
538
          );
539
      default:
540
        return Path()
×
541
          ..addRRect(
×
542
            RRect.fromRectAndRadius(
×
543
              Rect.fromCenter(
×
544
                center: Offset(width / 2, height / 2),
×
545
                width: cropWidth,
546
                height: cropHeight,
547
              ),
548
              Radius.circular(borderRadius),
×
549
            ),
550
          );
551
    }
552
  }
553

554
  @override
×
555
  Future<MemoryImage?> onCropImage() async {
556
    if (_imageAsUIImage == null) {
×
557
      return null;
558
    }
559
    final imageWidth = _imageAsUIImage!.width;
×
560
    final imageHeight = _imageAsUIImage!.height;
×
561
    final pictureRecorder = ui.PictureRecorder();
×
562
    final canvas = Canvas(pictureRecorder);
×
563
    final onCropParams = caclulateOnCropParams(
×
564
      cropPercentage: widget.cropPercentage,
×
565
      imageFit: widget.imageFit,
×
566
      imageHeight: imageHeight,
567
      imageWidth: imageWidth,
568
      screenHeight: _height,
×
569
      screenWidth: _width,
×
570
      dataScale: data.scale,
×
571
      aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
×
572
    );
573
    final clipPath = Path.from(_getPath(
×
574
      cropWidth: onCropParams.cropSizeWidth,
×
575
      cropHeight: onCropParams.cropSizeHeight,
×
576
      width: onCropParams.cropSizeWidth,
×
577
      height: onCropParams.cropSizeHeight,
×
578
      borderRadius: widget.borderRadius,
×
579
      clipShape: widget.clipShapeOnCrop,
×
580
    ));
581
    final matrix4Image = Matrix4.diagonal3(vector_math.Vector3.all(1))
×
582
      ..translate(
×
583
        onCropParams.translateScale * data.x + onCropParams.cropSizeWidth / 2,
×
584
        onCropParams.translateScale * data.y + onCropParams.cropSizeHeight / 2,
×
585
      )
586
      ..scale(onCropParams.scale)
×
587
      ..rotateZ(data.angle);
×
588
    final bgPaint = Paint()
×
589
      ..color = widget.backgroundColor
×
590
      ..style = PaintingStyle.fill;
×
591
    canvas.drawRect(
×
592
      Rect.fromLTWH(
×
593
        0,
594
        0,
595
        onCropParams.cropSizeWidth,
×
596
        onCropParams.cropSizeHeight,
×
597
      ),
598
      bgPaint,
599
    );
600
    canvas.save();
×
601
    canvas.clipPath(clipPath);
×
602
    canvas.transform(matrix4Image.storage);
×
603
    canvas.drawImage(
×
604
      _imageAsUIImage!,
×
605
      Offset(-imageWidth / 2, -imageHeight / 2),
×
606
      widget.imagePaintDuringCrop,
×
607
    );
608
    canvas.restore();
×
609

610
    // Optionally remove magenta from image by evaluating every pixel
611
    // See https://github.com/brendan-duncan/image/blob/master/lib/src/transform/copy_crop.dart
612

613
    // final bytes = await compute(computeToByteData, <String, dynamic>{'pictureRecorder': pictureRecorder, 'cropWidth': cropWidth});
614

615
    ui.Picture picture = pictureRecorder.endRecording();
×
616
    ui.Image image = await picture.toImage(
×
617
      onCropParams.cropSizeWidth.floor(),
×
618
      onCropParams.cropSizeHeight.floor(),
×
619
    );
620

621
    // Adding compute would be preferrable. Unfortunately we cannot pass an ui image to this.
622
    // A workaround would be to save the image and load it inside of the isolate
623
    final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
×
624
    return bytes == null ? null : MemoryImage(bytes.buffer.asUint8List());
×
625
  }
626

627
  void _addTransitionInternal(CropImageData transition) {
×
628
    setData(data + transition);
×
629
  }
630

631
  @override
×
632
  void addTransition(CropImageData transition) {
633
    _correctTransition(transition, () {
×
634
      _addTransitionInternal(transition);
×
635
    });
636
  }
637

638
  @override
×
639
  void setData(CropImageData newData) {
640
    setState(() {
×
641
      data = newData;
×
642
      // The same check should happen (once available) as in addTransition
643
      data.scale = data.scale.clamp(0.1, 10.0);
×
644
    });
645
  }
646
}
647

648
enum CustomCropShape {
649
  Circle,
650
  Square,
651
  Ratio,
652
}
653

654
enum CustomImageFit {
655
  fillCropSpace,
656
  fitCropSpace,
657
  fillCropWidth,
658
  fillCropHeight,
659
  fitVisibleSpace,
660
  fillVisibleSpace,
661
  fillVisibleHeight,
662
  fillVisibleWidth,
663
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc