import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui'; import 'package:image/image.dart' as img; import 'coin_detector.dart'; class CropResult { const CropResult({ required this.bytes, required this.width, required this.height, required this.usedDetection, }); final Uint8List bytes; final int width; final int height; final bool usedDetection; } /// Circular crop of the detected coin (square output with transparent-ish bg). class CoinCropper { static Future cropAndCorrect({ required String imagePath, CoinBox? box, double padding = 0.12, }) async { final raw = await File(imagePath).readAsBytes(); var decoded = img.decodeImage(raw); if (decoded == null) { throw StateError('Failed to decode image: $imagePath'); } decoded = img.bakeOrientation(decoded); final usedDetection = box != null; final center = box?.effectiveCenter ?? Offset(decoded.width / 2, decoded.height / 2); var radius = box?.effectiveRadius ?? math.min(decoded.width, decoded.height) * 0.35; radius *= (1 + padding); final size = (radius * 2).round().clamp(32, 2048); final out = img.Image(width: size, height: size, numChannels: 4); img.fill(out, color: img.ColorRgba8(0, 0, 0, 0)); final r2 = radius * radius; for (var y = 0; y < size; y++) { for (var x = 0; x < size; x++) { final dx = x + 0.5 - size / 2; final dy = y + 0.5 - size / 2; if (dx * dx + dy * dy > r2) continue; final sx = (center.dx + dx).round(); final sy = (center.dy + dy).round(); if (sx < 0 || sy < 0 || sx >= decoded.width || sy >= decoded.height) { continue; } final p = decoded.getPixel(sx, sy); out.setPixelRgba(x, y, p.r.toInt(), p.g.toInt(), p.b.toInt(), 255); } } const outSize = 320; final resized = img.copyResize( out, width: outSize, height: outSize, interpolation: img.Interpolation.cubic, ); final bytes = Uint8List.fromList(img.encodePng(resized)); return CropResult( bytes: bytes, width: resized.width, height: resized.height, usedDetection: usedDetection, ); } }