import 'dart:io'; import 'dart:math' as math; import 'package:image/image.dart' as img; import 'package:path/path.dart' as p; class CropSaveError implements Exception { final String code; const CropSaveError(this.code); @override String toString() => 'CropSaveError($code)'; } /// Viewport -> tam cozunurluk kaynak eslemesi (image_canvas'tan gelir). class CropViewParams { final int w, h; // kaynak dosya boyutu final double vw, vh; // viewport boyutu final double tx, ty, sc; // pan/zoom final double angleDeg; // donus (derece) final bool flipH; // yatay aynalama (goruntunun kendisi degil, gorunum) final double cx, cy; // donus merkezi (viewport ortasi) final double k; // kaynak -> layout olcegi final double ox, oy; // layout offset const CropViewParams({ required this.w, required this.h, required this.vw, required this.vh, required this.tx, required this.ty, required this.sc, required this.angleDeg, this.flipH = false, required this.cx, required this.cy, required this.k, required this.ox, required this.oy, }); } class CropSaveService { /// Tam cozunurlukte decode + EXIF yonunu gomme (engine goruntusuyle tutarli). static Future load(String srcPath) async { final bytes = await File(srcPath).readAsBytes(); final src = img.decodeImage(bytes); if (src == null) throw const CropSaveError('decode'); return img.bakeOrientation(src); } /// Hedef yol: `resim.png` -> `resim_143025.jpg` (hep JPEG; cakismada null). static String? buildOutputPath(String srcPath) { final base = p.basenameWithoutExtension(srcPath); final dir = p.dirname(srcPath); final now = DateTime.now(); final hh = now.hour.toString().padLeft(2, '0'); final mm = now.minute.toString().padLeft(2, '0'); final ss = now.second.toString().padLeft(2, '0'); final candidate = p.join(dir, '${base}_$hh$mm$ss.jpg'); return File(candidate).existsSync() ? null : candidate; } /// Ekranda gorunen bolgeyi birebir (donus dahil) tam cozunurlukte kaydedir. /// Duzlem: viewport -> (tx,ty,sc) -> (vp ortasi etrafinda donus) -> (k,ox,oy) -> kaynak px. static Future saveCrop({ required String srcPath, required img.Image source, required CropViewParams vp, List? matrix, int jpegQuality = 93, }) async { final outPath = buildOutputPath(srcPath); if (outPath == null) throw const CropSaveError('file_exists'); final out = vp.angleDeg == 0 ? _cropAxisAligned(source, vp) : _cropRotated(source, vp); // Ekranda yatay aynalama acikken cikti da aynalanir (cikti = viewport'un birebir eslemesi). if (vp.flipH) img.flipHorizontal(out); // Çıktıda yön fiziksel — EXIF tag'ini temizle (çift dönüşü önler). out.exif.imageIfd.orientation = null; if (matrix != null) _applyMatrix(out, matrix); final jpeg = img.JpegEncoder(quality: jpegQuality).encode(out); await File(outPath).writeAsBytes(jpeg); return outPath; } /// Donus yok: gorulen dikdortgeni birebir kes (hizli yol). static img.Image _cropAxisAligned(img.Image src, CropViewParams vp) { double toX(double vx) => ((vx - vp.tx) / vp.sc - vp.ox) / vp.k; double toY(double vy) => ((vy - vp.ty) / vp.sc - vp.oy) / vp.k; final x0 = toX(0), x1 = toX(vp.vw), y0 = toY(0), y1 = toY(vp.vh); final x = x0.round().clamp(0, src.width - 1); final y = y0.round().clamp(0, src.height - 1); final xr = x1.round().clamp(x + 1, src.width); final yr = y1.round().clamp(y + 1, src.height); return img.copyCrop(src, x: x, y: y, width: xr - x, height: yr - y); } /// Donus var: ekran grid'ini kaynaga birebir ornekler (bilinear). static img.Image _cropRotated(img.Image src, CropViewParams vp) { final step = vp.sc * vp.k; // ekran px basina kaynak px if (step <= 0) throw const CropSaveError('empty_rect'); final ow = (vp.vw / step).round().clamp(1, 1 << 14); final oh = (vp.vh / step).round().clamp(1, 1 << 14); final angle = vp.angleDeg * math.pi / 180; final cos = math.cos(-angle), sin = math.sin(-angle); (double, double) toSrc(double vx, double vy) { final x = (vx - vp.tx) / vp.sc - vp.cx; final y = (vy - vp.ty) / vp.sc - vp.cy; return ((cos * x - sin * y + vp.cx - vp.ox) / vp.k, (sin * x + cos * y + vp.cy - vp.oy) / vp.k); } final out = img.Image(width: ow, height: oh); for (var y = 0; y < oh; y++) { final vy = (y + 0.5) * step; for (var x = 0; x < ow; x++) { final s = toSrc((x + 0.5) * step, vy); final c = _sampleBilinear(src, s.$1, s.$2); out.setPixelRgb(x, y, c.$1, c.$2, c.$3); } } return out; } /// Manuel bilinear ornekleme (package:image 4.x API'si). static (int, int, int) _sampleBilinear(img.Image src, double fx, double fy) { final x0 = fx.floor(), y0 = fy.floor(); final tx = (fx - x0).clamp(0.0, 1.0), ty = (fy - y0).clamp(0.0, 1.0); final c00 = src.getPixelClamped(x0, y0), c10 = src.getPixelClamped(x0 + 1, y0), c01 = src.getPixelClamped(x0, y0 + 1), c11 = src.getPixelClamped(x0 + 1, y0 + 1); int lerp(num a, num b, double t) => (a + (b - a) * t).toInt(); return ( lerp(lerp(c00.r, c10.r, tx), lerp(c01.r, c11.r, tx), ty), lerp(lerp(c00.g, c10.g, tx), lerp(c01.g, c11.g, tx), ty), lerp(lerp(c00.b, c10.b, tx), lerp(c01.b, c11.b, tx), ty), ); } /// Flutter'la ayni 0-255 alan/semantigindaki 4x5 matrisi uygular. static void _applyMatrix(img.Image image, List m) { for (final pixel in image) { final r = pixel.r.toDouble(), g = pixel.g.toDouble(), b = pixel.b.toDouble(); pixel.r = (m[0] * r + m[1] * g + m[2] * b + m[4]).clamp(0, 255).round(); pixel.g = (m[5] * r + m[6] * g + m[7] * b + m[9]).clamp(0, 255).round(); pixel.b = (m[10] * r + m[11] * g + m[12] * b + m[14]).clamp(0, 255).round(); } } }