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

icapps / flutter-custom-image-crop / 12549161986

30 Dec 2024 04:24PM UTC coverage: 0.0%. Remained the same
12549161986

push

github

web-flow
Merge pull request #61 from icapps/release/0.1.1

Release/0.1.1

0 of 84 new or added lines in 5 files covered. (0.0%)

9 existing lines in 3 files now uncovered.

0 of 517 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
/// A method that draws a path with a given paint, outline color and stroke width
14
typedef DrawPathMethod = CustomPaint Function(
15
  Path path, {
16
  Paint? pathPaint,
17
  Color outlineColor,
18
  double outlineStrokeWidth,
19
});
20

21
/// An image cropper that is customizable.
22
/// You can rotate, scale and translate either
23
/// through gestures or a controller
24
class CustomImageCrop extends StatefulWidget {
25
  /// The image to crop
26
  final ImageProvider image;
27

28
  /// The controller that handles the cropping and
29
  /// changing of the cropping area
30
  final CustomImageCropController cropController;
31

32
  /// The color behind the cropping area
33
  final Color backgroundColor;
34

35
  /// The color in front of the cropped area
36
  final Color overlayColor;
37

38
  /// The shape of the cropping area.
39
  /// Possible values:
40
  /// - [CustomCropShape.Circle] Crop area will be circular.
41
  /// - [CustomCropShape.Square] Crop area will be a square.
42
  /// - [CustomCropShape.Ratio] Crop area will have a specified aspect ratio.
43
  final CustomCropShape shape;
44

45
  /// The shape of the mask area.
46
  /// Possible values:
47
  /// - [CustomCropShape.Circle] Mask area will be circular.
48
  /// - [CustomCropShape.Square] Mask area will be a square.
49
  /// - [CustomCropShape.Ratio] Mask area will have a specified aspect ratio.
50
  final CustomCropShape? maskShape;
51

52
  /// Ratio of the cropping area.
53
  /// If [shape] is set to [CustomCropShape.Ratio], this property is required.
54
  /// For example, to create a square crop area, use [Ratio(width: 1, height: 1)].
55
  /// To create a rectangular crop area with a 16:9 aspect ratio, use [Ratio(width: 16, height: 9)].
56
  final Ratio? ratio;
57

58
  /// How to fit image inside visible space
59
  final CustomImageFit imageFit;
60

61
  /// The percentage of the available area that is
62
  /// reserved for the cropping area
63
  final double cropPercentage;
64

65
  /// The path drawer of the border see [DottedCropPathPainter],
66
  /// [SolidPathPainter] for more details or how to implement a
67
  /// custom one
68
  final DrawPathMethod drawPath;
69

70
  /// Custom paint options for drawing the cropping border.
71
  ///
72
  /// If [paint] is provided, it will be used for customizing the appearance
73
  /// of the cropping border.
74
  ///
75
  /// If [paint] is not provided, default values will be used:
76
  /// - Color: [Colors.white]
77
  /// - Stle [PaintingStyle.stroke]
78
  /// - Stroke Join [StrokeJoin.round]
79
  /// - Stroke Width: 4.0
80
  final Paint? pathPaint;
81

82
  /// The radius for rounded corners of the cropping area (only applicable to rounded rectangle shapes).
83
  final double borderRadius;
84

85
  /// Whether to allow the image to be rotated.
86
  final bool canRotate;
87

88
  /// Determines whether scaling gesture is disabled.
89
  ///
90
  /// By default, scaling is enabled.
91
  /// Set [canScale] to `false` to disable scaling.
92
  final bool canScale;
93

94
  /// Determines whether moving gesture overlay is disabled.
95
  ///
96
  /// By default, moving is enabled.
97
  /// Set [canMove] to `false` to disable move.
98
  final bool canMove;
99

100
  /// The paint used when drawing an image before cropping
101
  final Paint imagePaintDuringCrop;
102

103
  /// This widget is used to specify a custom progress indicator
104
  final Widget? customProgressIndicator;
105

106
  /// Whether to clip the area outside of the path when cropping
107
  /// By default, the value is `true`
108
  final bool clipShapeOnCrop;
109

110
  /// Whether image area must cover clip path
111
  /// By default, the value is `false`
112
  /// If use CustomCropShape.circle, the cropped image may have white blank.
113
  final bool forceInsideCropArea;
114

115
  /// Sets the color of the outline of the crop selection area
116
  /// This is provided to the [drawPath] method
117
  /// Default is [Colors.white]
118
  final Color outlineColor;
119

120
  /// Sets the stroke width of the outline of the crop selection area
121
  /// This is provided to the [drawPath] method
122
  /// Default is 4.0
123
  final double outlineStrokeWidth;
124

125
  /// Adds a filter to overlay.
126
  /// For example, consider using [ImageFilter.blur] to create a backdrop blur effect.
127
  final ui.ImageFilter? imageFilter;
128

129
  /// The blend mode of the image filter
130
  /// Default is [BlendMode.srcOver]
131
  final BlendMode imageFilterBlendMode;
132

133
  /// A custom image cropper widget
134
  ///
135
  /// Uses a `CustomImageCropController` to crop the image.
136
  /// With the controller you can rotate, translate and/or
137
  /// scale with buttons and sliders. This can also be
138
  /// achieved with gestures
139
  ///
140
  /// Use a `shape` with `CustomCropShape.Circle` or
141
  /// `CustomCropShape.Square`
142
  ///
143
  /// You can increase the cropping area using `cropPercentage`
144
  ///
145
  /// Change the cropping border by changing `drawPath`,
146
  /// we've provided two default painters as inspiration
147
  /// `DottedCropPathPainter.drawPath` and
148
  /// `SolidCropPathPainter.drawPath`
149
  CustomImageCrop({
×
150
    required this.image,
151
    required this.cropController,
152
    this.overlayColor = const Color.fromRGBO(0, 0, 0, 0.5),
153
    this.backgroundColor = Colors.white,
154
    this.shape = CustomCropShape.Circle,
155
    this.maskShape,
156
    this.imageFit = CustomImageFit.fitCropSpace,
157
    this.cropPercentage = 0.8,
158
    this.drawPath = DottedCropPathPainter.drawPath,
159
    this.pathPaint,
160
    this.canRotate = true,
161
    this.canScale = true,
162
    this.canMove = true,
163
    this.clipShapeOnCrop = true,
164
    this.customProgressIndicator,
165
    this.ratio,
166
    this.borderRadius = 0,
167
    Paint? imagePaintDuringCrop,
168
    this.forceInsideCropArea = false,
169
    this.outlineColor = Colors.white,
170
    this.outlineStrokeWidth = 4.0,
171
    this.imageFilter,
172
    this.imageFilterBlendMode = BlendMode.srcOver,
173
    Key? key,
174
  })  : this.imagePaintDuringCrop = imagePaintDuringCrop ??
NEW
175
            (Paint()..filterQuality = FilterQuality.high),
×
176
        assert(
177
          !(shape == CustomCropShape.Ratio && ratio == null),
×
178
          "If shape is set to Ratio, ratio should not be null.",
179
        ),
180
        super(key: key);
×
181

182
  @override
×
183
  _CustomImageCropState createState() => _CustomImageCropState();
×
184
}
185

186
class _CustomImageCropState extends State<CustomImageCrop>
187
    with CustomImageCropListener {
188
  CropImageData? _dataTransitionStart;
189
  late Path _path;
190
  late Path _maskPath;
191
  late double _width, _height;
192
  ui.Image? _imageAsUIImage;
193
  ImageStream? _imageStream;
194
  ImageStreamListener? _imageListener;
195

196
  @override
×
197
  void initState() {
198
    super.initState();
×
199
    widget.cropController.addListener(this);
×
200
  }
201

202
  @override
×
203
  void didChangeDependencies() {
204
    super.didChangeDependencies();
×
205
    _getImage();
×
206
  }
207

208
  @override
×
209
  void didUpdateWidget(CustomImageCrop oldWidget) {
210
    super.didUpdateWidget(oldWidget);
×
211
    if (oldWidget.image != widget.image) _getImage();
×
212
  }
213

214
  void _getImage() {
×
215
    final oldImageStream = _imageStream;
×
216
    _imageStream = widget.image.resolve(createLocalImageConfiguration(context));
×
217
    if (_imageStream?.key != oldImageStream?.key) {
×
218
      if (_imageListener != null) {
×
219
        oldImageStream?.removeListener(_imageListener!);
×
220
      }
221
      _imageListener = ImageStreamListener(_updateImage);
×
222
      _imageStream?.addListener(_imageListener!);
×
223
    }
224
  }
225

226
  void _updateImage(ImageInfo imageInfo, _) {
×
227
    setState(() {
×
228
      _imageAsUIImage = imageInfo.image;
×
229
    });
230
  }
231

232
  @override
×
233
  void dispose() {
234
    if (_imageListener != null) {
×
235
      _imageStream?.removeListener(_imageListener!);
×
236
    }
237
    widget.cropController.removeListener(this);
×
238
    super.dispose();
×
239
  }
240

241
  @override
×
242
  Widget build(BuildContext context) {
243
    final image = _imageAsUIImage;
×
244
    if (image == null) {
245
      return Center(
×
246
        child: widget.customProgressIndicator ?? CircularProgressIndicator(),
×
247
      );
248
    }
249
    return LayoutBuilder(
×
250
      builder: (context, constraints) {
×
251
        _width = constraints.maxWidth;
×
252
        _height = constraints.maxHeight;
×
253
        final cropFitParams = calculateCropFitParams(
×
254
          cropPercentage: widget.cropPercentage,
×
255
          imageFit: widget.imageFit,
×
256
          imageHeight: image.height,
×
257
          imageWidth: image.width,
×
258
          screenHeight: _height,
×
259
          screenWidth: _width,
×
260
          aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
×
NEW
261
          forceInsideCropArea: widget.forceInsideCropArea,
×
262
        );
263
        final scale = data.scale * cropFitParams.additionalScale;
×
264
        _path = _getPath(
×
265
          cropWidth: cropFitParams.cropSizeWidth,
×
266
          cropHeight: cropFitParams.cropSizeHeight,
×
267
          width: _width,
×
268
          height: _height,
×
269
          borderRadius: widget.borderRadius,
×
270
          shape: widget.shape,
×
271
        );
272

273
        _maskPath = widget.maskShape == null
×
274
            ? _path
×
275
            : _getPath(
×
276
                cropWidth: cropFitParams.cropSizeWidth,
×
277
                cropHeight: cropFitParams.cropSizeHeight,
×
278
                width: _width,
×
279
                height: _height,
×
280
                borderRadius: widget.borderRadius,
×
281
                shape: widget.maskShape!,
×
282
              );
283

NEW
284
        Widget overlay = Container(
×
NEW
285
          color: widget.overlayColor,
×
286
        );
NEW
287
        final filter = widget.imageFilter;
×
288
        if (filter != null) {
NEW
289
          overlay = BackdropFilter(
×
290
            filter: filter,
NEW
291
            blendMode: widget.imageFilterBlendMode,
×
292
            child: overlay,
293
          );
294
        }
NEW
295
        overlay = IgnorePointer(
×
NEW
296
          child: ClipPath(
×
NEW
297
            clipper: InvertedClipper(_maskPath, _width, _height),
×
298
            child: overlay,
299
          ),
300
        );
301

302
        return XGestureDetector(
×
303
          onMoveStart: onMoveStart,
×
304
          onMoveUpdate: onMoveUpdate,
×
305
          onScaleStart: onScaleStart,
×
306
          onScaleUpdate: onScaleUpdate,
×
307
          child: Container(
×
308
            width: _width,
×
309
            height: _height,
×
310
            color: widget.backgroundColor,
×
311
            child: Stack(
×
312
              children: [
×
313
                Positioned(
×
314
                  left: data.x + _width / 2,
×
315
                  top: data.y + _height / 2,
×
316
                  child: Transform(
×
NEW
317
                    transform: Matrix4.diagonal3(
×
NEW
318
                        vector_math.Vector3(scale, scale, scale))
×
319
                      ..rotateZ(data.angle)
×
320
                      ..translate(-image.width / 2, -image.height / 2),
×
321
                    child: Image(
×
322
                      image: widget.image,
×
323
                    ),
324
                  ),
325
                ),
326
                overlay,
NEW
327
                widget.drawPath(
×
NEW
328
                  _maskPath,
×
NEW
329
                  pathPaint: widget.pathPaint,
×
NEW
330
                  outlineColor: widget.outlineColor,
×
NEW
331
                  outlineStrokeWidth: widget.outlineStrokeWidth,
×
332
                ),
333
              ],
334
            ),
335
          ),
336
        );
337
      },
338
    );
339
  }
340

341
  void onScaleStart(_) {
×
342
    _dataTransitionStart = null; // Reset for update
×
343
  }
344

345
  void onScaleUpdate(ScaleEvent event) {
×
346
    final scale =
NEW
347
        widget.canScale ? event.scale : (_dataTransitionStart?.scale ?? 1.0);
×
348

349
    final angle = widget.canRotate ? event.rotationAngle : 0.0;
×
350

351
    if (_dataTransitionStart != null) {
×
352
      widget.cropController.addTransition(
×
353
        _dataTransitionStart! -
×
354
            CropImageData(
×
355
              scale: scale,
356
              angle: angle,
357
            ),
358
      );
359
    }
360
    _dataTransitionStart = CropImageData(
×
361
      scale: scale,
362
      angle: angle,
363
    );
364
  }
365

366
  void onMoveStart(_) {
×
367
    _dataTransitionStart = null; // Reset for update
×
368
  }
369

370
  void onMoveUpdate(MoveEvent event) {
×
371
    if (!widget.canMove) return;
×
372

NEW
373
    widget.cropController
×
NEW
374
        .addTransition(CropImageData(x: event.delta.dx, y: event.delta.dy));
×
375
  }
376

377
  Rect _getInitialImageRect() {
×
378
    assert(_imageAsUIImage != null);
×
379
    final image = _imageAsUIImage!;
×
380
    final cropFitParams = calculateCropFitParams(
×
381
      cropPercentage: widget.cropPercentage,
×
382
      imageFit: widget.imageFit,
×
383
      imageHeight: image.height,
×
384
      imageWidth: image.width,
×
385
      screenHeight: _height,
×
386
      screenWidth: _width,
×
387
      aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
×
NEW
388
      forceInsideCropArea: widget.forceInsideCropArea,
×
389
    );
NEW
390
    final initialWidth = image.width * cropFitParams.additionalScale;
×
NEW
391
    final initialHeight = image.height * cropFitParams.additionalScale;
×
392
    return Rect.fromLTWH(
×
393
      (_width - initialWidth) / 2,
×
394
      (_height - initialHeight) / 2,
×
395
      initialWidth,
396
      initialHeight,
397
    );
398
  }
399

400
  void _correctTransition(CropImageData transition, VoidCallback callback) {
×
401
    if (!widget.forceInsideCropArea || _imageAsUIImage == null) {
×
402
      callback();
×
403
      return;
404
    }
405
    final startX = data.x;
×
406
    final startY = data.y;
×
407
    callback();
×
408
    final pathRect = _path.getBounds();
×
409
    final initialImageRect = _getInitialImageRect();
×
410
    bool isContainPath = _isContainPath(initialImageRect, pathRect, data.scale);
×
411
    bool isRotated = data.angle != 0;
×
412

413
    if (isContainPath) {
414
      return;
415
    }
416

417
    if (transition.x != 0 || transition.y != 0) {
×
418
      if (isRotated) {
NEW
419
        _addTransitionInternal(
×
NEW
420
            CropImageData(x: startX - data.x, y: startY - data.y));
×
421
      } else {
422
        final imageRect = _getImageRect(initialImageRect, data.scale);
×
423
        double deltaX = min(pathRect.left - imageRect.left, 0);
×
NEW
424
        deltaX = pathRect.right > imageRect.right
×
NEW
425
            ? pathRect.right - imageRect.right
×
426
            : deltaX;
427
        double deltaY = min(pathRect.top - imageRect.top, 0);
×
NEW
428
        deltaY = pathRect.bottom > imageRect.bottom
×
NEW
429
            ? pathRect.bottom - imageRect.bottom
×
430
            : deltaY;
UNCOV
431
        _addTransitionInternal(CropImageData(x: deltaX, y: deltaY));
×
432
      }
433
      return;
434
    }
435
    double minEdgeHalf =
NEW
436
        min(initialImageRect.width, initialImageRect.height) / 2;
×
NEW
437
    double adaptScale = _calculateScaleAfterRotate(
×
NEW
438
        pathRect, data.scale, initialImageRect, minEdgeHalf);
×
UNCOV
439
    _addTransitionInternal(CropImageData(scale: adaptScale / data.scale));
×
440
  }
441

442
  Rect _getImageRect(Rect initialImageRect, double currentScale) {
×
443
    final diffScale = (1 - currentScale) / 2;
×
444
    final left =
NEW
445
        initialImageRect.left + diffScale * initialImageRect.width + data.x;
×
446
    final top =
NEW
447
        initialImageRect.top + diffScale * initialImageRect.height + data.y;
×
NEW
448
    Rect imageRect = Rect.fromLTWH(
×
449
        left,
450
        top,
NEW
451
        currentScale * initialImageRect.width,
×
NEW
452
        currentScale * initialImageRect.height);
×
453
    return imageRect;
454
  }
455

NEW
456
  double _getDistanceBetweenPointAndLine(
×
457
      Offset point, Offset lineStart, Offset lineEnd) {
458
    if (lineEnd.dy == lineStart.dy) {
×
459
      return (point.dy - lineStart.dy).abs();
×
460
    }
461
    if (lineEnd.dx == lineStart.dx) {
×
462
      return (point.dx - lineStart.dx).abs();
×
463
    }
464
    double line1Slop =
NEW
465
        (lineEnd.dy - lineStart.dy) / (lineEnd.dx - lineStart.dx);
×
466
    double line1Delta = lineEnd.dy - lineEnd.dx * line1Slop;
×
467
    double line2Slop = -1 / line1Slop;
×
468
    double line2Delta = point.dy - point.dx * line2Slop;
×
469
    double crossPointX = (line2Delta - line1Delta) / (line1Slop - line2Slop);
×
470
    double crossPointY = line1Slop * crossPointX + line1Delta;
×
471
    return (Offset(crossPointX, crossPointY) - point).distance;
×
472
  }
473

NEW
474
  bool _isContainPath(
×
475
      Rect initialImageRect, Rect pathRect, double currentScale) {
UNCOV
476
    final imageRect = _getImageRect(initialImageRect, currentScale);
×
477
    Offset topLeft, topRight, bottomLeft, bottomRight;
478
    final rad = atan(imageRect.height / imageRect.width);
×
479
    final len =
NEW
480
        sqrt(pow(imageRect.width / 2, 2) + pow(imageRect.height / 2, 2));
×
UNCOV
481
    bool isRotated = data.angle != 0;
×
482

483
    if (isRotated) {
484
      final clockAngle = rad + data.angle;
×
485
      final counterClockAngle = rad - data.angle;
×
486
      final cosClockValue = len * cos(clockAngle);
×
487
      final sinClockValue = len * sin(clockAngle);
×
488
      final cosCounterClockValue = len * cos(counterClockAngle);
×
489
      final sinCounterClockValue = len * sin(counterClockAngle);
×
490
      bottomRight = imageRect.center.translate(cosClockValue, sinClockValue);
×
NEW
491
      topRight = imageRect.center
×
NEW
492
          .translate(cosCounterClockValue, -sinCounterClockValue);
×
493
      topLeft = imageRect.center.translate(-cosClockValue, -sinClockValue);
×
NEW
494
      bottomLeft = imageRect.center
×
NEW
495
          .translate(-cosCounterClockValue, sinCounterClockValue);
×
496
    } else {
497
      bottomRight = imageRect.bottomRight;
×
498
      topRight = imageRect.topRight;
×
499
      topLeft = imageRect.topLeft;
×
500
      bottomLeft = imageRect.bottomLeft;
×
501
    }
502

503
    if (widget.shape == CustomCropShape.Circle) {
×
504
      final anchor = max(pathRect.width, pathRect.height) / 2;
×
505
      final pathCenter = pathRect.center;
×
NEW
506
      return _getDistanceBetweenPointAndLine(pathCenter, topLeft, topRight) >=
×
507
              anchor &&
NEW
508
          _getDistanceBetweenPointAndLine(pathCenter, topRight, bottomRight) >=
×
509
              anchor &&
NEW
510
          _getDistanceBetweenPointAndLine(
×
NEW
511
                  pathCenter, bottomLeft, bottomRight) >=
×
512
              anchor &&
NEW
513
          _getDistanceBetweenPointAndLine(pathCenter, topLeft, bottomLeft) >=
×
514
              anchor;
515
    }
516

517
    if (isRotated) {
518
      Path imagePath = Path()
×
519
        ..moveTo(topLeft.dx, topLeft.dy)
×
520
        ..lineTo(topRight.dx, topRight.dy)
×
521
        ..lineTo(bottomRight.dx, bottomRight.dy)
×
522
        ..lineTo(bottomLeft.dx, bottomLeft.dy)
×
523
        ..close();
×
NEW
524
      return imagePath.contains(pathRect.topLeft) &&
×
NEW
525
          imagePath.contains(pathRect.topRight) &&
×
NEW
526
          imagePath.contains(pathRect.bottomLeft) &&
×
NEW
527
          imagePath.contains(pathRect.bottomRight);
×
528
    } else {
NEW
529
      return imageRect.contains(pathRect.topLeft) &&
×
NEW
530
          imageRect.contains(pathRect.topRight) &&
×
NEW
531
          imageRect.contains(pathRect.bottomLeft) &&
×
NEW
532
          imageRect.contains(pathRect.bottomRight);
×
533
    }
534
  }
535

NEW
536
  double _calculateScaleAfterRotate(Rect pathRect, double startScale,
×
537
      Rect initialImageRect, double minEdgeHalf) {
538
    final imageCenter = initialImageRect.center.translate(data.x, data.y);
×
539
    final topLeftDistance = (pathRect.topLeft - imageCenter).distance;
×
540
    final topRightDistance = (pathRect.topRight - imageCenter).distance;
×
541
    final bottomLeftDistance = (pathRect.bottomLeft - imageCenter).distance;
×
542
    final bottomRightDistance = (pathRect.bottomRight - imageCenter).distance;
×
NEW
543
    final maxDistance = max(
×
NEW
544
        max(max(topLeftDistance, topRightDistance), bottomLeftDistance),
×
545
        bottomRightDistance);
UNCOV
546
    double endScale = maxDistance / minEdgeHalf;
×
547

548
    if (startScale >= endScale) {
×
549
      return endScale;
550
    }
551

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

556
    while ((endScale - startScale).abs() > step) {
×
557
      double midScale = (endScale + startScale) / 2;
×
558

559
      if (_isContainPath(initialImageRect, pathRect, midScale)) {
×
560
        endScale = midScale;
561
      } else {
562
        startScale = midScale + step;
×
563
      }
564
    }
565
    return endScale;
566
  }
567

568
  Path _getPath({
×
569
    required double cropWidth,
570
    required double cropHeight,
571
    required double width,
572
    required double height,
573
    required double borderRadius,
574
    required CustomCropShape shape,
575
    bool clipShape = true,
576
  }) {
577
    if (!clipShape) {
578
      return Path()
×
579
        ..addRect(
×
580
          Rect.fromCenter(
×
581
            center: Offset(width / 2, height / 2),
×
582
            width: cropWidth,
583
            height: cropHeight,
584
          ),
585
        );
586
    }
587

588
    switch (shape) {
589
      case CustomCropShape.Circle:
×
590
        return Path()
×
591
          ..addOval(
×
592
            Rect.fromCircle(
×
593
              center: Offset(width / 2, height / 2),
×
594
              radius: cropWidth / 2,
×
595
            ),
596
          );
597
      case CustomCropShape.Ratio:
×
598
        return Path()
×
599
          ..addRRect(
×
600
            RRect.fromRectAndRadius(
×
601
              Rect.fromCenter(
×
602
                center: Offset(width / 2, height / 2),
×
603
                width: cropWidth,
604
                height: cropHeight,
605
              ),
606
              Radius.circular(borderRadius),
×
607
            ),
608
          );
609
      default:
610
        return Path()
×
611
          ..addRRect(
×
612
            RRect.fromRectAndRadius(
×
613
              Rect.fromCenter(
×
614
                center: Offset(width / 2, height / 2),
×
615
                width: cropWidth,
616
                height: cropHeight,
617
              ),
618
              Radius.circular(borderRadius),
×
619
            ),
620
          );
621
    }
622
  }
623

624
  @override
×
625
  Future<MemoryImage?> onCropImage() async {
626
    if (_imageAsUIImage == null) {
×
627
      return null;
628
    }
629
    final imageWidth = _imageAsUIImage!.width;
×
630
    final imageHeight = _imageAsUIImage!.height;
×
631
    final pictureRecorder = ui.PictureRecorder();
×
632
    final canvas = Canvas(pictureRecorder);
×
NEW
633
    final onCropParams = calculateOnCropParams(
×
634
      cropPercentage: widget.cropPercentage,
×
635
      imageFit: widget.imageFit,
×
636
      imageHeight: imageHeight,
637
      imageWidth: imageWidth,
638
      screenHeight: _height,
×
639
      screenWidth: _width,
×
640
      dataScale: data.scale,
×
641
      aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
×
NEW
642
      forceInsideCropArea: widget.forceInsideCropArea,
×
643
    );
644
    final clipPath = Path.from(_getPath(
×
645
      cropWidth: onCropParams.cropSizeWidth,
×
646
      cropHeight: onCropParams.cropSizeHeight,
×
647
      width: onCropParams.cropSizeWidth,
×
648
      height: onCropParams.cropSizeHeight,
×
649
      borderRadius: widget.borderRadius,
×
650
      clipShape: widget.clipShapeOnCrop,
×
651
      shape: widget.shape,
×
652
    ));
653
    final matrix4Image = Matrix4.diagonal3(vector_math.Vector3.all(1))
×
654
      ..translate(
×
655
        onCropParams.translateScale * data.x + onCropParams.cropSizeWidth / 2,
×
656
        onCropParams.translateScale * data.y + onCropParams.cropSizeHeight / 2,
×
657
      )
658
      ..scale(onCropParams.scale)
×
659
      ..rotateZ(data.angle);
×
660
    final bgPaint = Paint()
×
661
      ..color = widget.backgroundColor
×
662
      ..style = PaintingStyle.fill;
×
663
    canvas.drawRect(
×
664
      Rect.fromLTWH(
×
665
        0,
666
        0,
667
        onCropParams.cropSizeWidth,
×
668
        onCropParams.cropSizeHeight,
×
669
      ),
670
      bgPaint,
671
    );
672
    canvas.save();
×
673
    canvas.clipPath(clipPath);
×
674
    canvas.transform(matrix4Image.storage);
×
675
    canvas.drawImage(
×
676
      _imageAsUIImage!,
×
677
      Offset(-imageWidth / 2, -imageHeight / 2),
×
678
      widget.imagePaintDuringCrop,
×
679
    );
680
    canvas.restore();
×
681

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

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

687
    ui.Picture picture = pictureRecorder.endRecording();
×
688
    ui.Image image = await picture.toImage(
×
689
      onCropParams.cropSizeWidth.floor(),
×
690
      onCropParams.cropSizeHeight.floor(),
×
691
    );
692

693
    // Adding compute would be preferrable. Unfortunately we cannot pass an ui image to this.
694
    // A workaround would be to save the image and load it inside of the isolate
695
    final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
×
696
    return bytes == null ? null : MemoryImage(bytes.buffer.asUint8List());
×
697
  }
698

699
  void _addTransitionInternal(CropImageData transition) {
×
700
    setData(data + transition);
×
701
  }
702

703
  @override
×
704
  void addTransition(CropImageData transition) {
705
    _correctTransition(transition, () {
×
706
      _addTransitionInternal(transition);
×
707
    });
708
  }
709

710
  @override
×
711
  void setData(CropImageData newData) {
712
    setState(() {
×
713
      data = newData;
×
714
      // The same check should happen (once available) as in addTransition
715
      data.scale = data.scale.clamp(0.1, 10.0);
×
716
    });
717
  }
718
}
719

720
enum CustomCropShape {
721
  Circle,
722
  Square,
723
  Ratio,
724
}
725

726
enum CustomImageFit {
727
  fillCropSpace,
728
  fitCropSpace,
729
  fillCropWidth,
730
  fillCropHeight,
731
  fitVisibleSpace,
732
  fillVisibleSpace,
733
  fillVisibleHeight,
734
  fillVisibleWidth,
735
}
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