- 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
76 lines
2.8 KiB
Dart
76 lines
2.8 KiB
Dart
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();
|
||
}
|
||
}
|
||
}
|