# 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? 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? activeColorMatrix` getter | | `lib/screens/viewer_screen.dart` | `GlobalKey`, `_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)