import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/utils/matrix.utils.dart'; import 'package:openapi/api.dart' hide AssetEditAction; Rect convertCropParametersToRect(CropParameters parameters, int originalWidth, int originalHeight) { return Rect.fromLTWH( parameters.x.toDouble() / originalWidth, parameters.y.toDouble() / originalHeight, parameters.width.toDouble() / originalWidth, parameters.height.toDouble() / originalHeight, ); } CropParameters convertRectToCropParameters(Rect rect, int originalWidth, int originalHeight) { final x = (rect.left * originalWidth).round(); final y = (rect.top * originalHeight).round(); final width = (rect.width * originalWidth).round(); final height = (rect.height * originalHeight).round(); return CropParameters( x: max(x, 0).clamp(0, originalWidth), y: max(y, 0).clamp(0, originalHeight), width: max(width, 0).clamp(0, originalWidth - x), height: max(height, 0).clamp(0, originalHeight - y), ); } AffineMatrix buildAffineFromEdits(List edits) { return AffineMatrix.compose( edits.map((edit) { switch (edit.action) { case AssetEditAction.rotate: final angleInDegrees = edit.parameters["angle"] as num; final angleInRadians = angleInDegrees * pi / 180; return AffineMatrix.rotate(angleInRadians); case AssetEditAction.mirror: final axis = edit.parameters["axis"] as String; return axis == "horizontal" ? AffineMatrix.flipY() : AffineMatrix.flipX(); default: return AffineMatrix.identity(); } }).toList(), ); } (double, bool, bool) normalizeTransformEdits(List edits) { double rotation = 0; bool flipX = false; bool flipY = false; final matrix = buildAffineFromEdits(edits); // round to avoid floating point precision issues int a = matrix.a.round(); int b = matrix.b.round(); int c = matrix.c.round(); int d = matrix.d.round(); // [ +/-1, 0, 0, +/-1 ] indicates a 0° or 180° rotation with possible mirrors // [ 0, +/-1, +/-1, 0 ] indicates a 90° or 270° rotation with possible mirrors if (a.abs() == 1 && b.abs() == 0 && c.abs() == 0 && d.abs() == 1) { rotation = a > 0 ? 0 : 180; flipX = rotation == 0 ? a < 0 : a > 0; flipY = rotation == 0 ? d < 0 : d > 0; } else if (a.abs() == 0 && b.abs() == 1 && c.abs() == 1 && d.abs() == 0) { rotation = c > 0 ? 90 : 270; flipX = rotation == 90 ? c < 0 : c > 0; flipY = rotation == 90 ? b > 0 : b < 0; } return (rotation, flipX, flipY); } class MatrixAdjustmentPainter extends CustomPainter { final ui.Image image; final ColorFilter? filter; const MatrixAdjustmentPainter({required this.image, this.filter}); @override void paint(Canvas canvas, Size size) { final paint = Paint()..colorFilter = filter; final srcRect = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); final dstRect = Rect.fromLTWH(0, 0, size.width, size.height); canvas.drawImageRect(image, srcRect, dstRect, paint); } @override bool shouldRepaint(covariant MatrixAdjustmentPainter oldDelegate) { return oldDelegate.image != image || oldDelegate.filter != filter; } } /// Helper to resolve an ImageProvider to a ui.Image Future resolveImage(ImageProvider provider) { final completer = Completer(); final stream = provider.resolve(const ImageConfiguration()); late final ImageStreamListener listener; listener = ImageStreamListener( (ImageInfo info, bool sync) { if (!completer.isCompleted) { completer.complete(info.image); } stream.removeListener(listener); }, onError: (error, stackTrace) { if (!completer.isCompleted) { completer.completeError(error, stackTrace); } stream.removeListener(listener); }, ); stream.addListener(listener); return completer.future; }