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

99 lines
5.6 KiB
Markdown
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.
# 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)