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
This commit is contained in:
1 parent
73c1ff51d6
commit
168e087c8b
16 files changed
+399
-14
No files matched your search
@@ -0,0 +1,98 @@
|
||||
# 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)
|
||||
Reference in new issue
Block a user