-
Notifications
You must be signed in to change notification settings - Fork 284
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce
TIMageCropper
for users to crop their profile picture + P…
…olish UI
- Loading branch information
1 parent
5746c6d
commit d3c80a7
Showing
13 changed files
with
1,283 additions
and
110 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
enum ImageShape { | ||
rectangle, | ||
circle, | ||
} |
104 changes: 83 additions & 21 deletions
104
turms-chat-demo-flutter/lib/infra/media/image_utils.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,107 @@ | ||
import 'dart:async'; | ||
import 'dart:math'; | ||
import 'dart:typed_data'; | ||
import 'dart:ui'; | ||
|
||
import 'package:image/image.dart' as image; | ||
import 'package:image/image.dart'; | ||
|
||
import 'image_format.dart'; | ||
import 'corrupted_media_file_exception.dart'; | ||
import 'image_shape.dart'; | ||
|
||
class ImageUtils { | ||
final class ImageUtils { | ||
ImageUtils._(); | ||
|
||
static ImageFormat findFormat(Uint8List data) => findFormatForData(data); | ||
|
||
static Uint8List crop({ | ||
required image.Image original, | ||
required Image image, | ||
required ImageShape shape, | ||
required Offset topLeft, | ||
required Offset bottomRight, | ||
ImageFormat outputFormat = ImageFormat.jpeg, | ||
ImageFormat outputFormat = ImageFormat.jpg, | ||
}) { | ||
if (topLeft.dx.isNegative || | ||
topLeft.dy.isNegative || | ||
bottomRight.dx.isNegative || | ||
bottomRight.dy.isNegative || | ||
topLeft.dx.toInt() > original.width || | ||
topLeft.dy.toInt() > original.height || | ||
bottomRight.dx.toInt() > original.width || | ||
bottomRight.dy.toInt() > original.height) { | ||
topLeft.dx.toInt() > image.width || | ||
topLeft.dy.toInt() > image.height || | ||
bottomRight.dx.toInt() > image.width || | ||
bottomRight.dy.toInt() > image.height) { | ||
throw ArgumentError( | ||
'Invalid rect: (topLeft: $topLeft, bottomRight: $bottomRight)'); | ||
} | ||
if (topLeft.dx > bottomRight.dx || topLeft.dy > bottomRight.dy) { | ||
throw ArgumentError( | ||
'Invalid rect: (topLeft: $topLeft, bottomRight: $bottomRight)'); | ||
} | ||
return Uint8List.fromList( | ||
image.encodePng( | ||
image.copyCrop( | ||
original, | ||
x: topLeft.dx.toInt(), | ||
y: topLeft.dy.toInt(), | ||
width: (bottomRight.dx - topLeft.dx).toInt(), | ||
height: (bottomRight.dy - topLeft.dy).toInt(), | ||
), | ||
), | ||
final size = Size( | ||
bottomRight.dx - topLeft.dx, | ||
bottomRight.dy - topLeft.dy, | ||
); | ||
switch (shape) { | ||
case ImageShape.rectangle: | ||
return _findEncodeFuncForRect(outputFormat)( | ||
copyCrop( | ||
image, | ||
x: topLeft.dx.toInt(), | ||
y: topLeft.dy.toInt(), | ||
width: size.width.toInt(), | ||
height: size.height.toInt(), | ||
), | ||
); | ||
case ImageShape.circle: | ||
final center = Offset( | ||
topLeft.dx + size.width / 2, | ||
topLeft.dy + size.height / 2, | ||
); | ||
final radius = min(size.width, size.height) / 2; | ||
return _findEncodeFuncForCircle(outputFormat)( | ||
copyCropCircle( | ||
image.numChannels == 4 ? image : image.convert(numChannels: 4), | ||
centerX: center.dx.toInt(), | ||
centerY: center.dy.toInt(), | ||
radius: radius.toInt(), | ||
), | ||
); | ||
} | ||
} | ||
|
||
static Image parse(Uint8List data, ImageFormat? format) { | ||
final decodedImage = _decode(data, format); | ||
return switch (decodedImage?.exif.exifIfd.orientation ?? -1) { | ||
3 => copyRotate(decodedImage!, angle: 180), | ||
6 => copyRotate(decodedImage!, angle: 90), | ||
8 => copyRotate(decodedImage!, angle: -90), | ||
_ => decodedImage!, | ||
}; | ||
} | ||
} | ||
|
||
static Image? _decode(Uint8List data, ImageFormat? format) => | ||
switch (format) { | ||
ImageFormat.jpg => decodeJpg(data), | ||
ImageFormat.png => decodePng(data), | ||
ImageFormat.bmp => decodeBmp(data), | ||
ImageFormat.ico => decodeIco(data), | ||
ImageFormat.webp => decodeWebP(data), | ||
_ => throw const CorruptedMediaFileException(), | ||
}; | ||
|
||
static Uint8List Function(Image) _findEncodeFuncForRect( | ||
ImageFormat? outputFormat) => | ||
switch (outputFormat) { | ||
ImageFormat.bmp => encodeBmp, | ||
ImageFormat.ico => encodeIco, | ||
ImageFormat.jpg => encodeJpg, | ||
ImageFormat.png => encodePng, | ||
_ => throw UnsupportedError('Unsupported format: $outputFormat'), | ||
}; | ||
|
||
static Uint8List Function(Image) _findEncodeFuncForCircle( | ||
ImageFormat? outputFormat) => | ||
switch (outputFormat) { | ||
ImageFormat.bmp => encodeBmp, | ||
ImageFormat.ico => encodeIco, | ||
ImageFormat.png => encodePng, | ||
_ => throw UnsupportedError('Unsupported format: $outputFormat'), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
...demo-flutter/lib/ui/desktop/components/t_image_cropper/core/circle_crop_area_clipper.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import 'package:flutter/material.dart'; | ||
|
||
class CircleCropAreaClipper extends CustomClipper<Path> { | ||
const CircleCropAreaClipper(this.rect); | ||
|
||
final Rect rect; | ||
|
||
@override | ||
Path getClip(Size size) => Path() | ||
..addOval(Rect.fromCircle(center: rect.center, radius: rect.width / 2)) | ||
..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height)) | ||
..fillType = PathFillType.evenOdd; | ||
|
||
@override | ||
bool shouldReclip(CustomClipper<Path> oldClipper) => true; | ||
} |
53 changes: 53 additions & 0 deletions
53
turms-chat-demo-flutter/lib/ui/desktop/components/t_image_cropper/core/dot_handle.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter/services.dart'; | ||
|
||
class DotHandle extends StatelessWidget { | ||
const DotHandle({ | ||
Key? key, | ||
required this.position, | ||
required this.dimension, | ||
this.padding = 4, | ||
this.color = Colors.white, | ||
}) : assert(dimension > padding * 2), | ||
super(key: key); | ||
|
||
final DotHandlePosition position; | ||
final double dimension; | ||
final double padding; | ||
final Color color; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final dotDimension = dimension - padding * 2; | ||
return MouseRegion( | ||
cursor: position.cursor, | ||
child: SizedBox( | ||
width: dimension, | ||
height: dimension, | ||
child: Center( | ||
child: ClipRRect( | ||
borderRadius: BorderRadius.circular(dimension), | ||
child: SizedBox( | ||
width: dotDimension, | ||
height: dotDimension, | ||
child: ColoredBox( | ||
color: color, | ||
), | ||
), | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
|
||
enum DotHandlePosition { | ||
topLeft(SystemMouseCursors.resizeUpLeftDownRight), | ||
topRight(SystemMouseCursors.resizeUpRightDownLeft), | ||
bottomLeft(SystemMouseCursors.resizeUpRightDownLeft), | ||
bottomRight(SystemMouseCursors.resizeUpLeftDownRight); | ||
|
||
const DotHandlePosition(this.cursor); | ||
|
||
final SystemMouseCursor cursor; | ||
} |
Oops, something went wrong.