Files
imajviewer/lib/services/crop_save_service.dart
T
Alhan bad48167c8 feat: horizontal flip with R key, baked into crop save (v1.6.0)
- R mirrors the view about the viewport centre and clears the active effects
  (same reset as double right click); pressing R again restores the original
  orientation. Pan/zoom are kept.
- Image switch, fill/fit toggle and right double click also clear the flip.
- Crop save (Ctrl+S) mirrors the output too, so the saved JPEG matches the
  on-screen view (verified in combination with rotation).
- Shortcut lists (tr/en), README and CHANGELOG updated; deb rebuilt as 1.6.0.
2026-10-14 02:28:24 +03:00

154 lines
5.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = 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<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();
}
}
}