Skip to content

Commit

Permalink
Introduce TIMageCropper for users to crop their profile picture + P…
Browse files Browse the repository at this point in the history
…olish UI
  • Loading branch information
JamesChenX committed Jan 12, 2025
1 parent 5746c6d commit d3c80a7
Show file tree
Hide file tree
Showing 13 changed files with 1,283 additions and 110 deletions.
7 changes: 0 additions & 7 deletions turms-chat-demo-flutter/lib/infra/media/image_format.dart

This file was deleted.

4 changes: 4 additions & 0 deletions turms-chat-demo-flutter/lib/infra/media/image_shape.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
enum ImageShape {
rectangle,
circle,
}
104 changes: 83 additions & 21 deletions turms-chat-demo-flutter/lib/infra/media/image_utils.dart
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'),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export 't_floating/t_floating.dart';
export 't_focus_tracker/t_focus_tracker.dart';
export 't_form/t_form.dart';
export 't_image/t_image_broken.dart';
export 't_image_cropper/t_image_cropper.dart';
export 't_image_viewer/t_image_viewer.dart';
export 't_layout/t_responsive_layout.dart';
export 't_lazy_indexed_stack/t_lazy_indexed_stack.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,29 +78,25 @@ class TTextButton extends StatelessWidget {
disabled: disabled,
onTap: onTap,
childHovered: IntrinsicWidth(
child: Center(
child: AnimatedDefaultTextStyle(
style: textStyleHovered ??
textStyle ??
const TextStyle(color: Colors.white),
duration: const Duration(milliseconds: 200),
child: Text(
text,
textAlign: TextAlign.center,
),
child: AnimatedDefaultTextStyle(
style: textStyleHovered ??
textStyle ??
const TextStyle(color: Colors.white),
duration: const Duration(milliseconds: 200),
child: Text(
text,
textAlign: TextAlign.center,
),
),
),
prefix: prefix,
child: IntrinsicWidth(
child: Center(
child: AnimatedDefaultTextStyle(
style: textStyle ?? const TextStyle(color: Colors.white),
duration: const Duration(milliseconds: 200),
child: Text(
text,
textAlign: TextAlign.center,
),
child: AnimatedDefaultTextStyle(
style: textStyle ?? const TextStyle(color: Colors.white),
duration: const Duration(milliseconds: 200),
child: Text(
text,
textAlign: TextAlign.center,
),
),
));
Expand Down
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;
}
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;
}
Loading

0 comments on commit d3c80a7

Please sign in to comment.