Files
imajviewer/lib/services/crop_save_service.dart
T
Alhan 168e087c8b
Build & Release / build-linux (push) Has been cancelled
Build & Release / build-windows (push) Has been cancelled
Build & Release / create-release (push) Has been cancelled
feat: crop save (save visible area at original resolution) v1.4.0
- Save visible area at original resolution as JPEG q90 (title bar button or Ctrl+S)
- Output name: original name + _HHmmss suffix; no overwrite, warning shown
- Active color effects baked in; rotation saves bounding box
- New: crop_save_service, l10n strings TR/EN, docs/plan-crop-save.md
2026-10-11 21:36:20 +03:00

76 lines
2.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:ui' show Rect;
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)';
}
class CropSaveService {
/// Tam çözünürlükte decode + EXIF yönünü gömme (engine görüntüsüyle tutarlı).
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.jpg` -> `resim_143025.jpg` (çakışmada null).
static String? buildOutputPath(String srcPath) {
final ext = p.extension(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$ext');
return File(candidate).existsSync() ? null : candidate;
}
/// Crop + aktif renk matrisi + JPEG q90.
/// [matrix] 4x5 row-major renk matrisi (image_canvas ile aynı form, 0-255 alanı);
/// null = gömme yok.
static Future<String> saveCrop({
required String srcPath,
required img.Image source,
required Rect rect,
List<double>? matrix,
}) async {
final outPath = buildOutputPath(srcPath);
if (outPath == null) throw const CropSaveError('file_exists');
final iw = source.width, ih = source.height;
final x = rect.left.round().clamp(0, iw - 1);
final y = rect.top.round().clamp(0, ih - 1);
final w = rect.right.round().clamp(x + 1, iw) - x;
final h = rect.bottom.round().clamp(y + 1, ih) - y;
if (w <= 0 || h <= 0) throw const CropSaveError('empty_rect');
final crop = img.copyCrop(source, x: x, y: y, width: w, height: h);
// Çıktıda yön fiziksel — EXIF tag'ini temizle (çift dönüşü önler).
crop.exif.imageIfd.orientation = null;
if (matrix != null) _applyMatrix(crop, matrix);
final jpeg = img.JpegEncoder(quality: 90).encode(crop);
await File(outPath).writeAsBytes(jpeg);
return outPath;
}
/// Flutter'la aynı 0-255 alan/semantiğindeki 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[13]).clamp(0, 255).round();
}
}
}