Compare commits
4 Commits
feat-tiled
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1002156cd4 | ||
|
|
72c35a8a26 | ||
|
|
addece7ade | ||
|
|
45cf56a1e6 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -4,12 +4,31 @@ All notable changes to imajViewer are documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.3.1] - 2026-08-14
|
||||
|
||||
### Fixed
|
||||
- Aspect ratio distortion on image switch — downsample now uses `ResizeImagePolicy.fit` (was `exact`, which forced 2048×2048 square and stretched non-square images)
|
||||
|
||||
## [1.3.0] - 2026-08-14
|
||||
|
||||
### Fixed
|
||||
- `_imageSize` now resets on image switch (wrong swap threshold on navigation)
|
||||
- Downsample now caps both dimensions, not just width (tall images no longer full-res decode)
|
||||
- Image size listener leak in `loadingBuilder` — replaced with single `_readImageSize`
|
||||
- Old downsample provider now evicted on full-res swap
|
||||
- Removed `imageCache.clear()` that also dropped the live image (gallery flash)
|
||||
|
||||
## [1.2.0] - 2026-08-14
|
||||
|
||||
### Added
|
||||
- Downsampling: large images decode at max 2048px on first load (RAM ~1.6MB)
|
||||
- Full-res swap: zooming in past 1.5x stretch swaps to original resolution
|
||||
- Internationalization: Turkish + English, auto-detected from system locale
|
||||
- Zoom decodes at original resolution while zooming in (crisp details)
|
||||
- Max zoom increased to 20x
|
||||
|
||||
### Fixed
|
||||
- RAM leak on image switch — old providers now evicted + `ImageCache.evict()`
|
||||
- Black screen flash during zoom (gapless playback)
|
||||
|
||||
### Changed
|
||||
|
||||
BIN
dist/imajviewer_1.2.0_amd64.deb
vendored
Normal file
BIN
dist/imajviewer_1.2.0_amd64.deb
vendored
Normal file
Binary file not shown.
BIN
dist/imajviewer_1.3.0_amd64.deb
vendored
Normal file
BIN
dist/imajviewer_1.3.0_amd64.deb
vendored
Normal file
Binary file not shown.
BIN
dist/imajviewer_1.3.1_amd64.deb
vendored
Normal file
BIN
dist/imajviewer_1.3.1_amd64.deb
vendored
Normal file
Binary file not shown.
135
docs/rapor-aspect-ratio-ram.md
Normal file
135
docs/rapor-aspect-ratio-ram.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Analiz: Aspect Ratio Bozulması + RAM Büyümesi
|
||||
|
||||
Tarih: 2026-08-14
|
||||
Sürüm: v1.3.0 sonrası regresyon
|
||||
|
||||
---
|
||||
|
||||
## 1. Aspect Ratio Bug'ı (kök neden bulundu)
|
||||
|
||||
### Belirti
|
||||
- Next/prev ile resim değişince görüntü **en/boy oranı bozulmuş** (gerilmiş/sıkışmış) geliyor.
|
||||
- Fazla zoom yapınca aspect düzeliyor.
|
||||
|
||||
### Kök Neden — `ResizeImagePolicy.exact`
|
||||
|
||||
`lib/widgets/image_canvas.dart:429`:
|
||||
|
||||
```dart
|
||||
ResizeImage.resizeIfNeeded(effectiveWidth, effectiveWidth, _cachedFileImage!)
|
||||
```
|
||||
|
||||
Bu çağrı Flutter SDK içinde şuna dönüşür (`image_provider.dart:1302-1310`):
|
||||
|
||||
```dart
|
||||
static ImageProvider<Object> resizeIfNeeded(int? cacheWidth, int? cacheHeight, provider) {
|
||||
if (cacheWidth != null || cacheHeight != null) {
|
||||
return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
```
|
||||
|
||||
Ve `ResizeImage` constructor'ı (`image_provider.dart:1262-1268`):
|
||||
|
||||
```dart
|
||||
const ResizeImage(this.imageProvider, { this.width, this.height,
|
||||
this.policy = ResizeImagePolicy.exact, // ← VARSAYILAN exact
|
||||
this.allowUpscaling = false,
|
||||
});
|
||||
```
|
||||
|
||||
**`ResizeImagePolicy.exact`**: genişlik ve yüksekliği **bağımsız** ölçekler, aspect ratio'yu
|
||||
**koruMAZ**. Yani `width: 2048, height: 2048` demek → her resmi **2048×2048 kareye
|
||||
zorla**. 4000×2000 bir resim 2048×2048'e gerilir (2:1 → 1:1), görüntü bozulur.
|
||||
|
||||
Zoom yapınca `_checkFullResSwap()` → `_usingFullRes = true` → `_cachedProvider =
|
||||
_cachedFileImage` (orijinal boyutlu `FileImage`, aspect doğru) → görüntü düzelir.
|
||||
Bu, belirtiyle birebir örtüşüyor.
|
||||
|
||||
### Düzeltme
|
||||
|
||||
`resizeIfNeeded` yerine doğrudan `ResizeImage` + `policy: ResizeImagePolicy.fit`:
|
||||
|
||||
```dart
|
||||
_cachedProvider = effectiveWidth != null
|
||||
? ResizeImage(
|
||||
_cachedFileImage!,
|
||||
width: effectiveWidth,
|
||||
height: effectiveWidth,
|
||||
policy: ResizeImagePolicy.fit,
|
||||
)
|
||||
: _cachedFileImage;
|
||||
```
|
||||
|
||||
`fit` policy'si (SDK `image_provider.dart:1378-1409`) aspect ratio'yu koruyarak
|
||||
2048×2048 kutusuna **sığdırır**:
|
||||
- 4000×2000 → 2048×1024 (2:1 korunur)
|
||||
- 800×20000 dikey → 82×2048 (HATA-2'nin istediği dikey sınırlama da korunur)
|
||||
- 20000×800 panorama → 2048×82
|
||||
|
||||
> Not: `resizeIfNeeded` helper'ı `policy` parametresi **almaz**, bu yüzden `fit`
|
||||
> için `ResizeImage`'ı doğrudan kurmak zorunlu.
|
||||
|
||||
---
|
||||
|
||||
## 2. RAM Büyümesi Analizi
|
||||
|
||||
### Gözlem
|
||||
RAM 200MB'a kadar büyüdü. "Daha büyür mü?" — Evet, aşağıdaki modele göre büyüyebilir.
|
||||
|
||||
### RAM modeli (mevcut)
|
||||
|
||||
| Kaynak | Boyut |
|
||||
|---|---|
|
||||
| Downsample (fit 2048×2048, RGBA) | en fazla 2048×2048×4 = **16.7MB** |
|
||||
| Full-res swap (zoom ≥ 1.5x) | resme bağlı: 12000px → **576MB**, 4000px → 64MB |
|
||||
| `ImageCache.maximumSize` | 16 entry |
|
||||
| `ImageCache.maximumSizeBytes` | 256MB |
|
||||
|
||||
### Neden 200MB'a büyüdü?
|
||||
|
||||
1. Kullanıcı zoom yaptı → `_checkFullResSwap` threshold 1.5 aşıldı → tam çözünürlük
|
||||
decode edildi → RAM'e ~100MB+ girdi.
|
||||
2. Full-res resim **canlı** olduğu için `maximumSizeBytes = 256MB` sınırı onu
|
||||
**atamaz** (Flutter aktif kullanımdaki image'ı evict etmez).
|
||||
|
||||
### Daha büyür mü? — Evet, iki senaryo
|
||||
|
||||
- **Daha büyük resim:** 12000px panorama full-res'e geçince tek resim ~576MB.
|
||||
`maximumSizeBytes = 256MB` canlı resmi korur, sınır aşılır.
|
||||
- **Çok sayıda full-res swap:** her resme zoom yapıp geçince, `didUpdateWidget`
|
||||
eski provider'ı `evict` eder ama evict **asenkron/gecikmeli** olabilir; ardışık
|
||||
hızlı geçişte 2-3 full-res aynı anda RAM'de kalabilir.
|
||||
|
||||
### Yapılacak iyileştirmeler
|
||||
|
||||
1. **Full-res swap'ı kademeli yap** (şu an binary: 2048 → full). Orta seviye
|
||||
(ör. 4096px) ekleyerek RAM'i sınırla. Ama bu kullanıcı isteği değilse yapma.
|
||||
2. **`_checkFullResSwap` stretch eşiğini yükselt** (1.5 → 2.0) → daha az full-res
|
||||
swap, daha az RAM. Trade-off: zoom'da biraz daha geç keskinleşme.
|
||||
3. **`maximumSizeBytes`'ı düşür** (256MB → 128MB) → LRU daha agresif. Ama canlı
|
||||
resmi yine korur, etkisi sınırlı.
|
||||
4. **Full-res swap'ı dispose etme zamanlamasını netleştir** — şu an `didUpdateWidget`
|
||||
evict yapıyor; `dispose()` içine `_cachedProvider?.evict()` de eklenebilir.
|
||||
|
||||
### Karar önerisi
|
||||
|
||||
Aspect bug'ı **kesin düzeltilmeli** (görsel regresyon, her resimde). RAM büyümesi
|
||||
ise full-res swap'ın **beklenen davranışı** — 200MB makul, ama 576MB'lık dev
|
||||
panoramalarda tek resimde patlayabilir. `fit` policy'ye geçmek downsample'ı da
|
||||
düzelteceği için ortalama RAM'i de bir miktar düşürür (exact kare yerine doğru
|
||||
orantılı küçük resim).
|
||||
|
||||
---
|
||||
|
||||
## 3. Öncelik Sırası
|
||||
|
||||
1. **Aspect bug fix** — `ResizeImagePolicy.fit` (tek satır, kritik görsel düzeltme)
|
||||
2. **RAM** — şimdilik izle; 2. eşik/kademeli swap istersen ayrı yaparız.
|
||||
|
||||
## 4. Doğrulama
|
||||
|
||||
- `flutter analyze`: 0 error
|
||||
- Release build: ✓
|
||||
- Manuel test: kullanıcı (aspect düzelmesi + RAM gözlemi)
|
||||
58
docs/rapor-downsampling-swap.md
Normal file
58
docs/rapor-downsampling-swap.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Durum Raporu: Downsampling + Full-Res Swap + RAM Leak Fix
|
||||
|
||||
Tarih: 2026-08-14
|
||||
Branch: `main`
|
||||
|
||||
## Yapılan Değişiklikler
|
||||
|
||||
Tek dosya: `lib/widgets/image_canvas.dart`
|
||||
|
||||
### 1. Downsampling (ilk yüklemede RAM tasarrufu)
|
||||
|
||||
- `_downsampleTarget = 2048` sabiti eklendi.
|
||||
- `_buildImage()` artık ilk yüklemede `ResizeImage.resizeIfNeeded(2048, null, ...)`
|
||||
kullanıyor. Büyük resimler (ör. 12000px) 2048px'e küçültülerek decode ediliyor.
|
||||
- Önceki davranış: resim her zaman tam çözünürlükte decode ediliyordu
|
||||
(`cacheWidth = longest * _sc * 2`), bu da dev resimlerde RAM'i dolduruyordu.
|
||||
|
||||
### 2. Full-Res Swap (zoom'da kalite)
|
||||
|
||||
- `_checkFullResSwap()`: downsampled resmin viewport'a oranı (`stretch`) 1.5x'i
|
||||
geçince `_usingFullRes = true` yapılıyor ve doğrudan `FileImage` (tam çözünürlük)
|
||||
kullanılıyor.
|
||||
- `_handleScroll()` içinde `_sc` güncellendikten sonra çağrılıyor.
|
||||
- Sonuç: küçük görünümde 2048px (RAM ~1.6MB), zoom ile yakınlaşınca orijinal.
|
||||
|
||||
### 3. RAM Leak Fix (resim geçişlerinde sızıntı)
|
||||
|
||||
- `didUpdateWidget()` içinde `old.filePath != widget.filePath` kontrolü:
|
||||
- `_cachedFileImage?.evict()`, `_cachedProvider?.evict()`
|
||||
- `PaintingBinding.instance.imageCache.evict(FileImage(File(old.filePath)))`
|
||||
- Tüm cache alanları `null`'a çekiliyor.
|
||||
- Önceki davranış: her yüklenen resim Flutter'ın global `ImageCache`'inde
|
||||
birikiyor, RAM sürekli artıyordu.
|
||||
|
||||
### 4. Görüntü boyutu takibi
|
||||
|
||||
- `_imageSize`, `loadingBuilder` + `ImageStreamListener` ile decode edilen
|
||||
gerçek piksel boyutundan dolduruluyor. `_checkFullResSwap()` bu değeri
|
||||
stretch hesabında kullanıyor.
|
||||
|
||||
## Temizlik
|
||||
|
||||
- Tiled render kodunun tamamı kaldırıldı (Flutter codec API region decode
|
||||
desteklemiyor).
|
||||
- `dart:ui` ve `dart:typed_data` import'ları gereksiz kalınca silindi.
|
||||
- Kullanılmayan `_uiImage`, `_fullResImage`, `_fullResLoading` alanları kaldırıldı.
|
||||
|
||||
## Doğrulama
|
||||
|
||||
- `flutter analyze --no-pub`: 0 error.
|
||||
- `flutter build linux --debug`: başarılı.
|
||||
- Kullanıcı testi: onaylandı ("herşey tamam gözüküyor").
|
||||
|
||||
## Açık Konular / Sonraki Adımlar
|
||||
|
||||
- `_downsampleTarget` ve swap threshold (1.5x) değerleri tune edilebilir.
|
||||
- LOD (progressive loading) seviyeleri — şimdilik tek kademe (2048 → full) yeterli.
|
||||
- Android/iOS hedeflerinde `ResizeImage` davranışı ayrıca test edilmeli.
|
||||
139
docs/rapor-kod-inceleme-performans.md
Normal file
139
docs/rapor-kod-inceleme-performans.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Kod İnceleme + Performans Analizi
|
||||
|
||||
Tarih: 2026-08-14
|
||||
Kapsam: Tüm `lib/` (13 dosya, 1799 satır)
|
||||
Bağlam: Downsampling + full-res swap + RAM leak fix (v1.2.0) sonrası genel inceleme
|
||||
|
||||
---
|
||||
|
||||
## 1. Tespit Edilen Hatalar
|
||||
|
||||
### HATA-1 (Önemli): `_imageSize` resim değişiminde sıfırlanmıyor
|
||||
|
||||
`lib/widgets/image_canvas.dart`
|
||||
|
||||
`_imageSize`, `loadingBuilder` içinde `_imageSize.isEmpty` koşuluyla **yalnızca bir kez**
|
||||
dolduruluyor. Ama `_resetView()` ve `didUpdateWidget()` bu alanı **sıfırlamıyor**.
|
||||
|
||||
Sonuç: ikinci resme geçince `_imageSize` eski resmin boyutunu tutar, yeni resmin
|
||||
boyutu hiç okunmaz. `_checkFullResSwap()` eski boyutla stretch hesabı yapar →
|
||||
swap yanlış zamanda tetiklenir (ya hiç, ya da gereksiz erken).
|
||||
|
||||
Örnek: 2000px bir resimden 12000px panoramaya geçince, stretch hesabı hâlâ 2000px
|
||||
üzerinden yapılır; swap ya geç tetiklenir ya da downsample 2048px'te takılı kalır
|
||||
(zoom'da kalite düşer). Ters yönde ise gereksiz erken full-res yükleme olur.
|
||||
|
||||
Düzeltme: `_resetView()` içine `_imageSize = Size.zero;` ekle.
|
||||
|
||||
### HATA-2 (Önemli): Downsample yalnızca genişliğe uygulanıyor
|
||||
|
||||
`_buildImage()` içinde `ResizeImage.resizeIfNeeded(2048, null, ...)` çağrısı
|
||||
`cacheHeight = null` geçiyor. Bu, **yalnızca genişlik** kısıtı demek.
|
||||
|
||||
Sonuç: Uzun dikey resimler (ör. 800×20000 ekran görüntüsü) genişliği 2048'in
|
||||
altında olduğu için **hiç küçültülmez**, tam çözünürlükte (20000px) decode edilir.
|
||||
Tek resim yüzlerce MB RAM yer — downsampling'in amacı boşa çıkar. Aynı sorun
|
||||
panoramik 20000×800 resimlerde genişlik kısıtıyla zaten yakalanır, ama dikeyde kaçar.
|
||||
|
||||
Düzeltme: `ResizeImage.resizeIfNeeded(2048, 2048, ...)` (iki boyut da sınırla).
|
||||
Flutter codec aspect oranını koruyarak sığdırır (fit).
|
||||
|
||||
### HATA-3 (Performans): `loadingBuilder` içinde listener sızıntısı
|
||||
|
||||
`loadingBuilder` her frame'de çağrılabilir. `_imageSize.isEmpty` olduğu sürece her
|
||||
çağrıda `provider.resolve()` + `addListener()` yapılıyor; listener **hiç
|
||||
remove edilmiyor**. `_imageSize` bir kez dolunca duruyor ama dolana kadar (ya da
|
||||
HATA-1 yüzünden hiç dolmazsa) her rebuild'de yeni listener birikir.
|
||||
|
||||
Ayrıca `resolve()` ikinci bir decode stream'i açar — `Image` widget'ı zaten kendi
|
||||
decode'ini yaparken boyut öğrenmek için ikinci kez çözmek gereksiz iş.
|
||||
|
||||
Düzeltme: `_imageSize`'i `loadingBuilder` yerine `initState`/`didUpdateWidget`'ta
|
||||
tek sefer `resolve()` ile oku ve `ImageStreamListener`'ı `ImageStream.removeListener`
|
||||
ile kaldır. Ya da `Image` widget'ının `ImageInfo`'sini `imageStream` üzerinden
|
||||
tek listener ile dinle.
|
||||
|
||||
### HATA-4 (Küçük): Full-res swap'ta eski downsample provider evict edilmiyor
|
||||
|
||||
`_checkFullResSwap()` `_cachedProvider = _cachedFileImage` yapıyor ama eski
|
||||
`ResizeImage` provider'ı `ImageCache`'ten evict etmiyor. Downsamlı `ui.Image`
|
||||
(~16MB) cache'te gereksiz kalır. `maximumSizeBytes = 256MB` altında LRU temizler
|
||||
ama tamamen gereksiz bir tutma.
|
||||
|
||||
### HATA-5 (Yorum yanlışlığı): `imageCache.clear()` canlı görseli de siler
|
||||
|
||||
`image_manager.dart:73` — `PaintingBinding.instance.imageCache.clear()` tüm cache'i
|
||||
boşaltır, **ekrandaki mevcut görsel de dahil**. Üstündeki yorum "canlı görsel korunur"
|
||||
diyor ama bu doğru değil. `clear()` o an render edilen image'ı da atar → galeri
|
||||
kurulumunda kısa bir flaş/boşluk riski.
|
||||
|
||||
Düzeltme: Yalnızca eski klasöre ait dosyaları `evict` et (tek tek) ya da bu satırı
|
||||
kaldırıp `didUpdateWidget` evict mantığına bırak.
|
||||
|
||||
---
|
||||
|
||||
## 2. RAM Modeli (mevcut davranış)
|
||||
|
||||
- Downsample 2048px → ~16MB/resim (RGBA 2048×2048×4)
|
||||
- Full-res swap sonrası → 100MB+ (büyük resimlerde)
|
||||
- `ImageCache`: `maximumSize = 16` entry, `maximumSizeBytes = 256MB`
|
||||
|
||||
**Kullanıcının "50 resim = 5GB" endişesi gerçekleşmez.** `maximumSizeBytes = 256MB`
|
||||
LRU evict ile toplamı ~256MB–1.6GB arasında sınırlar:
|
||||
- Tüm resimler downsample ise: 16 entry × ~16MB ≈ 256MB (tam limit)
|
||||
- Full-res resimler varsa: 2–3 tane tutulur, gerisi LRU ile atılır
|
||||
|
||||
Yani ölçülen "her resim ~100MB, artmıyor" davranışı doğru ve beklenen.
|
||||
|
||||
**Kalan risk:** `maximumSize = 16` entry limiti, `maximumSizeBytes`'tan bağımsız
|
||||
işler. Full-res ağırlıklı kullanımda 16 × 100MB = 1.6GB'a kadar tutulabilir.
|
||||
`maximumSizeBytes`'ı düşürmek (ör. 128MB) ya da entry limitini küçültmek bu tavanı
|
||||
düşürür.
|
||||
|
||||
---
|
||||
|
||||
## 3. Performans İyileştirme Önerileri
|
||||
|
||||
### ÖN-1: `dir.listSync()` → async/isolate
|
||||
`image_manager.dart:43` — klasör `listSync()` ile senkron taranıyor. Büyük klasörde
|
||||
(10k+ dosya) UI thread bloklanır, pencere donar. `dir.list()` (async) ya da
|
||||
`Isolate.run` ile arka planda tarama önerilir. Sonuç `notifyListeners` ile gelir.
|
||||
|
||||
### ÖN-2: `_printImage` tam dosyayı belleğe alıyor
|
||||
`viewer_screen.dart:173` — `File(path).readAsBytes()` tüm dosyayı RAM'e yükler.
|
||||
12000px resimde print sırasında +200MB spike. Nadir işlem ama downsample edilmiş
|
||||
kopya üzerinden print etmek düşünülebilir.
|
||||
|
||||
### ÖN-3: `_buildImage` içindeki `_imageKey` + `ResizeImage` memo cache
|
||||
HATA-1 çözülünce `_imageSize` doğru çalışır; ayrıca `_checkFullResSwap`'ın
|
||||
`setState` çağrısı scroll handler içinde — bu zaten `_markRenderDirty` ile çift
|
||||
rebuild üretebilir. `_checkFullResSwap`'ın `setState` yerine `_markRenderDirty`
|
||||
kullanması yeterli (ValueListenableBuilder zaten tick'i dinliyor).
|
||||
|
||||
### ÖN-4: Warmup katmanı her açılışta
|
||||
`_startWarmup` shader derlemesini her resim açılışında yapıyor; uygulama ömründe
|
||||
bir kez yapılması yeterli olabilir (global static flag). Küçük kazanç.
|
||||
|
||||
---
|
||||
|
||||
## 4. Doğrulama Matrisi
|
||||
|
||||
| Madde | Durum |
|
||||
|---|---|
|
||||
| `flutter analyze` | 0 error (mevcut info/deprecated uyarıları hariç) |
|
||||
| Release build | ✓ |
|
||||
| RAM stabilitesi (kullanıcı testi) | ✓ "her resim ~100MB, artmıyor" |
|
||||
| HATA-1 (`_imageSize` reset) | ✅ düzeltildi |
|
||||
| HATA-2 (dikey downsample) | ✅ düzeltildi |
|
||||
| HATA-3 (listener sızıntısı) | ✅ düzeltildi |
|
||||
| HATA-4 (eski provider evict) | ✅ düzeltildi |
|
||||
| HATA-5 (`clear()` canlı görsel) | ✅ düzeltildi |
|
||||
|
||||
---
|
||||
|
||||
## 5. Öncelik Sırası
|
||||
|
||||
1. **HATA-1 + HATA-2** — doğruluk + RAM (uzun dikey resimler hâlâ patlayabilir)
|
||||
2. **HATA-3** — listener sızıntısı (uzun oturumlarda)
|
||||
3. **ÖN-1** — büyük klasörde UI donması
|
||||
4. HATA-4, HATA-5 — kozmetik/ince ayar
|
||||
101
docs/yonerge-downsampling-swap.md
Normal file
101
docs/yonerge-downsampling-swap.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Yönerge: Downsampling + Full-Res Swap + RAM Leak Fix
|
||||
|
||||
## Amaç
|
||||
Yüksek çözünürlüklü görsellerde RAM tüketimini azaltmak ve resim geçişlerinde bellek sızıntısını gidermek.
|
||||
|
||||
## Dosya
|
||||
`lib/widgets/image_canvas.dart`
|
||||
|
||||
## 1. State değişkenleri ekle
|
||||
|
||||
`_ImageCanvasState` sınıfına aşağıdaki değişkenleri ekle:
|
||||
|
||||
```dart
|
||||
ui.Image? _uiImage;
|
||||
ui.Image? _fullResImage;
|
||||
bool _fullResLoading = false;
|
||||
bool _usingFullRes = false;
|
||||
static const int _downsampleTarget = 2048;
|
||||
```
|
||||
|
||||
## 2. `_loadUiImage()` — Downsample decode
|
||||
|
||||
Mevcut `_loadUiImage()` metodunu aşağıdaki gibi değiştir:
|
||||
|
||||
- `ui.instantiateImageCodec()` çağrısına `targetWidth: _downsampleTarget, targetHeight: _downsampleTarget` parametrelerini ekle
|
||||
- Decode edilen resmin boyutunu `_uiImage` ve `_tiledImageSize` olarak kaydet
|
||||
- Eğer decode boyutu target'tan büyükse → `_loadFullResImage(bytes)` başlat (arka plan)
|
||||
- Eğer decode boyutu target'tan küçükse → resim zaten küçük, `_usingFullRes = true` yap
|
||||
|
||||
## 3. `_loadFullResImage()` — Arka plan full-res yükleme
|
||||
|
||||
Yeni metot:
|
||||
|
||||
```dart
|
||||
Future<void> _loadFullResImage(Uint8List bytes) async {
|
||||
if (_fullResLoading) return;
|
||||
_fullResLoading = true;
|
||||
try {
|
||||
final codec = await ui.instantiateImageCodec(bytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
if (mounted && !_usingFullRes) {
|
||||
setState(() => _fullResImage = frame.image);
|
||||
} else {
|
||||
frame.image.dispose();
|
||||
}
|
||||
} catch (e) { /* log */ }
|
||||
finally { _fullResLoading = false; }
|
||||
}
|
||||
```
|
||||
|
||||
## 4. `_checkFullResSwap()` — Zoom threshold'unda swap
|
||||
|
||||
Yeni metot. `_handleScroll()` içinde `_sc` güncellendikten sonra çağrılır.
|
||||
|
||||
- Downsample resmin ne kadar stretch edildiğini hesapla (`stretchX`, `stretchY`)
|
||||
- `maxStretch >= 1.5` ise:
|
||||
- `_uiImage?.dispose()`
|
||||
- `_uiImage = _fullResImage`
|
||||
- `_fullResImage = null`
|
||||
- `_tiledImageSize` güncelle
|
||||
- `_usingFullRes = true`
|
||||
|
||||
## 5. `_handleScroll()` — Swap kontrolü ekle
|
||||
|
||||
`_sc = newSc;` satırından sonra `_checkFullResSwap()` çağrısı ekle.
|
||||
|
||||
## 6. RAM Leak Fix — `didUpdateWidget`
|
||||
|
||||
Resim değiştiğinde eski tüm referansları temizle:
|
||||
|
||||
```dart
|
||||
_uiImage?.dispose();
|
||||
_fullResImage?.dispose();
|
||||
_cachedFileImage?.evict();
|
||||
_cachedProvider?.evict();
|
||||
_cachedFileImage = null;
|
||||
_cachedProvider = null;
|
||||
_cachedPath = null;
|
||||
_cachedCacheWidth = null;
|
||||
PaintingBinding.instance.imageCache.evict(FileImage(File(old.filePath)));
|
||||
```
|
||||
|
||||
## 7. `_resetView()` — Tüm state sıfırla
|
||||
|
||||
`_fullResImage`, `_fullResLoading`, `_usingFullRes` sıfırlansın.
|
||||
|
||||
## 8. `dispose()` — Her iki resmi de dispose et
|
||||
|
||||
```dart
|
||||
_uiImage?.dispose();
|
||||
_fullResImage?.dispose();
|
||||
```
|
||||
|
||||
## 9. Import
|
||||
|
||||
`dart:typed_data` import'u ekle (`Uint8List` için).
|
||||
|
||||
## Sonuç
|
||||
- İlk yükleme: max 2048px downsample (RAM ~1.6MB)
|
||||
- Zoom ile yakınlaşınca: full-res swap (~512MB+ büyük resimlerde)
|
||||
- Resim geçişinde: eski tüm cache dispose + evict
|
||||
@@ -69,8 +69,6 @@ class ImageManager extends ChangeNotifier {
|
||||
}
|
||||
if (_currentIndex < 0) _currentIndex = 0;
|
||||
|
||||
// Eski klasörün decode edilmiş görsellerini bırak; canlı görsel korunur
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
bool _ctrlPressed = false;
|
||||
bool _shiftPressed = false;
|
||||
|
||||
// ── Downsampling + swap ──
|
||||
bool _usingFullRes = false;
|
||||
Size _imageSize = Size.zero;
|
||||
static const int _downsampleTarget = 2048;
|
||||
|
||||
// ── Kare hızına eşitlenmiş render tick ──
|
||||
final ValueNotifier<int> _renderTick = ValueNotifier(0);
|
||||
bool _frameScheduled = false;
|
||||
@@ -151,6 +156,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_onHardwareKey);
|
||||
_renderTick.dispose();
|
||||
_imageSizeStream?.removeListener(_imageSizeListener!);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -179,12 +185,23 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
@override
|
||||
void didUpdateWidget(ImageCanvas old) {
|
||||
super.didUpdateWidget(old);
|
||||
if (old.filePath != widget.filePath) _resetView();
|
||||
if (old.filePath != widget.filePath) {
|
||||
_cachedFileImage?.evict();
|
||||
_cachedProvider?.evict();
|
||||
_cachedFileImage = null;
|
||||
_cachedProvider = null;
|
||||
_cachedPath = null;
|
||||
_cachedCacheWidth = null;
|
||||
PaintingBinding.instance.imageCache.evict(FileImage(File(old.filePath)));
|
||||
_resetView();
|
||||
}
|
||||
}
|
||||
|
||||
void _resetView() {
|
||||
_tx = 0; _ty = 0; _sc = 1.0; _isFilled = true;
|
||||
_angle = 0; _contrast = 1.0; _saturation = 1.0; _brightness = 0;
|
||||
_usingFullRes = false;
|
||||
_imageSize = Size.zero;
|
||||
_markRenderDirty();
|
||||
}
|
||||
|
||||
@@ -216,6 +233,7 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
_ty -= dh / 2;
|
||||
}
|
||||
_sc = newSc;
|
||||
_checkFullResSwap();
|
||||
_clamp();
|
||||
_markRenderDirty();
|
||||
}
|
||||
@@ -371,17 +389,51 @@ class _ImageCanvasState extends State<ImageCanvas> {
|
||||
ImageProvider<Object>? _cachedProvider;
|
||||
String? _cachedPath;
|
||||
int? _cachedCacheWidth;
|
||||
ImageStream? _imageSizeStream;
|
||||
ImageStreamListener? _imageSizeListener;
|
||||
|
||||
/// Provider değiştiğinde boyutu tek sefer oku (listener sızıntısı olmadan).
|
||||
void _readImageSize(ImageProvider<Object> provider) {
|
||||
_imageSizeStream?.removeListener(_imageSizeListener!);
|
||||
_imageSizeStream = provider.resolve(const ImageConfiguration());
|
||||
_imageSizeListener = ImageStreamListener((info, _) {
|
||||
if (mounted) {
|
||||
_imageSize = Size(info.image.width.toDouble(), info.image.height.toDouble());
|
||||
}
|
||||
});
|
||||
_imageSizeStream!.addListener(_imageSizeListener!);
|
||||
}
|
||||
|
||||
void _checkFullResSwap() {
|
||||
if (_usingFullRes || _imageSize.isEmpty) return;
|
||||
final double stretchX = _vpSize.width * _sc / _imageSize.width;
|
||||
final double stretchY = _vpSize.height * _sc / _imageSize.height;
|
||||
if (math.max(stretchX, stretchY) >= 1.5) {
|
||||
_cachedProvider?.evict(); // eski downsample provider'ı cache'ten at
|
||||
setState(() {
|
||||
_usingFullRes = true;
|
||||
_cachedProvider = _cachedFileImage;
|
||||
_cachedCacheWidth = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildImage() {
|
||||
final longest = _vpSize.longestSide;
|
||||
final cacheWidth = longest > 0 ? (longest * _sc * 2).round() : null;
|
||||
if (_cachedPath != widget.filePath || _cachedCacheWidth != cacheWidth) {
|
||||
final needFullRes = _usingFullRes;
|
||||
final effectiveWidth = needFullRes ? null : _downsampleTarget;
|
||||
if (_cachedPath != widget.filePath || _cachedCacheWidth != effectiveWidth) {
|
||||
_cachedPath = widget.filePath;
|
||||
_cachedCacheWidth = cacheWidth;
|
||||
_cachedCacheWidth = effectiveWidth;
|
||||
_cachedFileImage = FileImage(File(widget.filePath));
|
||||
_cachedProvider = cacheWidth != null
|
||||
? ResizeImage.resizeIfNeeded(cacheWidth, null, _cachedFileImage!)
|
||||
: null;
|
||||
_cachedProvider = effectiveWidth != null
|
||||
? ResizeImage(
|
||||
_cachedFileImage!,
|
||||
width: effectiveWidth,
|
||||
height: effectiveWidth,
|
||||
policy: ResizeImagePolicy.fit,
|
||||
)
|
||||
: _cachedFileImage;
|
||||
_readImageSize(_cachedProvider!);
|
||||
}
|
||||
final ImageProvider<Object> provider = _cachedProvider ?? _cachedFileImage!;
|
||||
return Image(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: imajviewer
|
||||
description: "Ultra-lightweight image viewer for Linux and Windows."
|
||||
publish_to: 'none'
|
||||
version: 1.1.0+3
|
||||
version: 1.3.1+6
|
||||
|
||||
msix_config:
|
||||
display_name: ImajViewer
|
||||
|
||||
Reference in New Issue
Block a user