Files
imajviewer/docs/plan-crop-save.md
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

5.6 KiB
Raw Blame History

Plan — Crop Save (görünen alanı kaydet)

Tarih: 2026-10-11 · Versiyon: 1.4.0 · Durum: UYGULANDI (2026-10-11) — manual test + commit/push bekleniyor

Not — Uygulama Sırası

  • image v3 kullanılamadı: msix 3.18.0 image ^4.0.0 gerektiriyor → v4 async API kullanıldı (decodeImage Uint8List ile, copyCrop(x:,y:,width:,height:), JpegEncoder(quality:90), bakeOrientation, pixel kanalları r/g/b/a).
  • v4'te renk matrisi filtresi yok → matris crop_save_service.dart:_applyMatrix ile elle uygulandı (aynı 0-255 alanı).
  • Boyut okuma ayrı descriptor yerine tam decode (load()) üzerinden yapılıyor.

Gereksinim + Kararlar

  • Pencerenin görünen kısmı (image canvas viewport'u) → orijinal çözünürlükte crop
  • JPEG quality 90
  • Dosya adı: orijinal ismin sonuna saat only eki → resim.jpg → resim_143025.jpg
  • Çakışma: dosya zaten varsa yazı YAPILMAZ, uyarı gösterilir (mevcut dosya korunur)
  • Renk efektleri: kayda GÖMÜLÜR (contrast/saturation/brightness, ekrandaki gibi)
  • Rotasyon aktifken: yapılsın — görünür bölgeyi saran dikdörtgen (bounding box) kaydedilir
  • Title bar: Save butonu minimize'in SOLUNDA (sıralama: save · minimize · x)
  • Klavye: Ctrl+S de tetikler

Mimari Analiz

1) Görünen alan → kaynak piksel eşlemesi

image_canvas.dart render zinciri: Transform(translate(_tx,_ty) scale(_sc)) içinde opsiyonel Transform(rotate(_angle), vpCenter) → Image(fit: cover|contain).

Canvas lokal koordinatındaki bir nokta p için geri dönüş:

  1. q = (p - (tx,ty)) / sc
  2. _angle != 0 ise r = q'yi vpCenter ekseninde -angle kadar döndür (yoksa r = q)
  3. cover (_isFilled): k = max(vpW/iw, vpH/ih); contain: k = min(vpW/iw, vpH/ih); her ikisinde de ox=(vpW-iw*k)/2, oy=(vpH-ih*k)/2 → src = ((r.x-ox)/k, (r.y-oy)/k)
  4. Viewport'un 4 köşesi → src uzayda → min/max bounding box → [0,iw]×[0,ih]'a clamp → int
  5. Rotasyonlu durumda bounding box, ekranda görünür bölgeyi kapsayan dikdörtgendir (çıkış aspect/boyutu pencereden farklı olabilir — onaylı davranış)
  • iw,ih = gerçek dosya boyutu (descriptor ile). _imageSize downsample'lı (2048) olabilir → eşlemede kullanılmaz.
  • Görünen alan = tamamı canvas viewport (title bar 24px bindirilmiş overlay, görsel arkasında görünüyor → kesime dahil).

2) Encode zinciri (yeni bağımlılık: image ^4.x, saf Dart)

File → img.decodeImage(bytes, keepOrientation: true)
     → img.cropRect(rect)
     → aktif renk matrisi var ise img.ColorMatrixFilter(mat4x5).applyTo(crop)
     → img.encodeJpeg(crop, quality: 90)
     → hedef yol var ise → uyarı (yazılmaz); yoksa File.writeAsBytes
  • Renk matrisi: image_canvas.dart'daki mevcut _buildCombinedFilter() 4x5 matrisi (contrast×brightness×saturation) aynı zamanda ColorFilter.matrix olarak kullanılıyor. State'e List<double>? activeColorMatrix getter'ı eklenir (filtre identity ise null). Flutter ve image paketi aynı 4x5 row-major / 0-255 alanı kullanır → matris aynen taşınır, yeniden türetilmez.
  • Tuzak — EXIF yönü: Flutter engine JPEG'lerde EXIF orientation uygular; keepOrientation: true verilmezse 90° döndürülmüş fotoğraflarda crop yanlış yerden kesilir. Test listesinde zorunlu.
  • WBMP: image WBMP çözer (uygulama destekliyor); decode hatası → "desteklenmiyor" mesajı, çökme yok.

3) Bileşenler

Dosya Değişiklik
pubspec.yaml image: ^4.x; versiyon 1.4.0+8
lib/services/crop_save_service.dart YENİ — imageSize(path) (descriptor), saveCrop({srcPath, rect, matrix, outPath}), ad üretimi (_HHmmss), çakışma kontrolü (exists → uyarı), hata mesajları
lib/widgets/image_canvas.dart State → ImageCanvasState (public); Rect? cropSourceRect(int w, int h); List<double>? activeColorMatrix getter
lib/screens/viewer_screen.dart GlobalKey<ImageCanvasState>, _cropSave(), sonuç geri bildirimi (kısa overlay/Snackbar: yol veya hata)
lib/widgets/custom_title_bar.dart onSaveCrop param; save ikonu minimize'in solunda; mevcut _BarButton stili (hover kırmızı)
lib/shortcuts/app_shortcuts.dart Ctrl+S → crop save (callback param ile)
lib/l10n/app_{en,tr}.arb saveCrop (tooltip), cropSaved, cropFailed, cropFileExists, cropUnsupported + shortcutsList güncelleme; flutter gen-l10n
CHANGELOG.md [1.4.0]

4) Etkileşim

  • Buton yalnızca görsel yüklüyken aktif (yoksa null → pas geçilir).
  • Başarıda: kaydedilen tam yol gösterilir; hata/çakışmada: hata metni.
  • Tam decode kayıt anında, tek seferlik (downsample swap ile ilişkisi yok).

Sınır Durumları

  1. Görsel yok → buton pasif, Ctrl+S no-op
  2. Crop rect boş/degenerate → hata mesajı
  3. Yazılamayan klasör → hata mesajı, çökme yok
  4. Çakışma: dosya var → yazılmaz, uyarı (karar)
  5. WBMP/decode hatası → cropUnsupported
  6. Rotasyonlu bounding box (karar: kaydedilir)
  7. Büyük görsellerde tam decode RAM'i — tek seferlik, kabul edilebilir

Doğrulama

  1. flutter analyze (otomatik izinli)
  2. flutter build linux --debug + ./install.sh (otomatik izinli)
  3. Kullanıcı manuel test: fit/cover, zoom, pan, rotasyon + kaydet, renk efektleri + kaydet, EXIF 90° görsel, WBMP, çakışan isim (uyarı + dosya korunur), salt okunur dizin
  4. Commit + push: yalnızca senin komutunla

Kapsam Dışı

  • Kullanıcı alanı seçerek crop (her zaman "görünen bölge")
  • PNG/özel format seçimi (sabit JPEG q90)
  • Gitea CI release / tag (senin komutunla)