154 lines
5.8 KiB
Dart
154 lines
5.8 KiB
Dart
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<img.Image> 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<String> saveCrop({
|
||
required String srcPath,
|
||
required img.Image source,
|
||
required CropViewParams vp,
|
||
List<double>? matrix,
|
||
int jpegQuality = 95,
|
||
}) 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<double> 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();
|
||
}
|
||
}
|
||
}
|