1use cosmic::widget;
2use image::ImageReader;
3use std::collections::{HashMap, HashSet};
4use std::path::{Path, PathBuf};
5
6pub const RGBA_BYTES_PER_PIXEL: u64 = 4;
8
9#[allow(dead_code)]
13const SYSTEM_MEMORY_RESERVE_MB: u64 = 500;
14
15const GALLERY_MEMORY_LIMIT_MB: u64 = 2000;
19
20pub const ATLAS_FRAGMENT_SIZE: u32 = 4096;
24
25pub const MB_TO_BYTES: u64 = 1024 * 1024;
27
28pub const DECIMAL_MB_TO_BYTES: u64 = 1000 * 1000;
31
32const DISPLAY_SCALE_FACTOR: f32 = 1.5;
36
37pub fn calculate_target_dimensions(
43 image_width: u32,
44 image_height: u32,
45 display_width: u32,
46 display_height: u32,
47) -> Option<(u32, u32)> {
48 let target_width = (display_width as f32 * DISPLAY_SCALE_FACTOR) as u32;
49 let target_height = (display_height as f32 * DISPLAY_SCALE_FACTOR) as u32;
50
51 if image_width <= target_width && image_height <= target_height {
52 return None;
53 }
54
55 let image_aspect = image_width as f32 / image_height as f32;
56 let target_aspect = target_width as f32 / target_height as f32;
57
58 let (new_width, new_height) = if image_aspect > target_aspect {
59 let w = target_width;
60 let h = (target_width as f32 / image_aspect) as u32;
61 (w, h)
62 } else {
63 let h = target_height;
64 let w = (target_height as f32 * image_aspect) as u32;
65 (w, h)
66 };
67
68 let new_width = new_width.max(1);
69 let new_height = new_height.max(1);
70
71 log::info!(
72 "Calculated target dimensions: {}x{} -> {}x{} (display: {}x{}, scale: {}x)",
73 image_width,
74 image_height,
75 new_width,
76 new_height,
77 display_width,
78 display_height,
79 DISPLAY_SCALE_FACTOR
80 );
81
82 Some((new_width, new_height))
83}
84
85pub fn exceeds_memory_limit(width: u32, height: u32, memory_limit_mb: u64) -> bool {
88 let Some(bytes_needed) = calculate_image_memory(width, height) else {
89 return true;
91 };
92
93 let max_bytes = memory_limit_mb * MB_TO_BYTES;
94 bytes_needed > max_bytes
95}
96
97pub fn should_use_tiling(width: u32, height: u32) -> bool {
100 width > ATLAS_FRAGMENT_SIZE || height > ATLAS_FRAGMENT_SIZE
101}
102
103pub fn should_use_dedicated_worker(
109 width: u32,
110 height: u32,
111 total_budget_mb: u64,
112 parallel_workers: usize,
113) -> (bool, u64, usize) {
114 if width == 0 || height == 0 {
115 log::warn!(
116 "Invalid image dimensions {}x{}, using normal queue",
117 width,
118 height
119 );
120 return (false, total_budget_mb, parallel_workers);
121 }
122
123 let Some(bytes_needed) = calculate_image_memory(width, height) else {
124 log::warn!(
125 "Image dimensions {}x{} overflow memory calculation, using normal queue",
126 width,
127 height
128 );
129 return (false, total_budget_mb, parallel_workers);
130 };
131
132 let mb_needed = bytes_needed / MB_TO_BYTES;
133 let per_worker_budget_mb = total_budget_mb / parallel_workers as u64;
134
135 if mb_needed > per_worker_budget_mb {
136 log::info!(
137 "Large image {}x{} needs {}MB (exceeds per-worker {}MB), using dedicated worker",
138 width,
139 height,
140 mb_needed,
141 per_worker_budget_mb
142 );
143 (true, total_budget_mb, 1)
145 } else {
146 log::debug!(
147 "Normal image {}x{} needs {}MB (within per-worker {}MB), using parallel workers",
148 width,
149 height,
150 mb_needed,
151 per_worker_budget_mb
152 );
153 (false, total_budget_mb, parallel_workers)
155 }
156}
157
158pub fn get_image_dimensions(path: &Path) -> Option<(u32, u32)> {
160 match ImageReader::open(path) {
161 Ok(reader) => match reader.into_dimensions() {
162 Ok((width, height)) => {
163 log::debug!(
164 "Image dimensions: {}x{} for {}",
165 width,
166 height,
167 path.display()
168 );
169 Some((width, height))
170 }
171 Err(e) => {
172 log::warn!("Failed to get dimensions for {}: {}", path.display(), e);
173 None
174 }
175 },
176 Err(e) => {
177 log::warn!("Failed to open image reader for {}: {}", path.display(), e);
178 None
179 }
180 }
181}
182
183fn calculate_image_memory(width: u32, height: u32) -> Option<u64> {
186 let pixels = (width as u64).checked_mul(height as u64)?;
187 pixels.checked_mul(RGBA_BYTES_PER_PIXEL)
188}
189
190#[cfg(target_os = "linux")]
193fn check_ram_available(width: u32, height: u32) -> (bool, Option<String>) {
194 use procfs::Current;
195
196 let Some(bytes_needed) = calculate_image_memory(width, height) else {
197 let error_msg = format!(
198 "Image dimensions too large: {}x{} causes overflow in memory calculation",
199 width, height
200 );
201 log::error!("{}", error_msg);
202 return (false, Some(error_msg));
203 };
204
205 let mb_needed = bytes_needed / MB_TO_BYTES;
206
207 match procfs::Meminfo::current() {
208 Ok(meminfo) => {
209 let available_kb = meminfo.mem_available.unwrap_or(0);
212 let available_bytes = available_kb * 1024;
213
214 let min_reserve_bytes = SYSTEM_MEMORY_RESERVE_MB * MB_TO_BYTES;
216 let usable_bytes = available_bytes.saturating_sub(min_reserve_bytes);
217
218 if bytes_needed > usable_bytes {
219 let available_mb = available_bytes / MB_TO_BYTES;
220 let error_msg = format!(
221 "Insufficient memory: need {}MB, available {}MB. Try closing other applications.",
222 mb_needed, available_mb
223 );
224 log::warn!("{}", error_msg);
225 return (false, Some(error_msg));
226 }
227
228 (true, None)
229 }
230 Err(e) => {
231 log::warn!("Failed to read /proc/meminfo: {}. Skipping RAM check.", e);
232 (true, None)
234 }
235 }
236}
237
238#[cfg(not(target_os = "linux"))]
239fn check_ram_available(_width: u32, _height: u32) -> (bool, Option<String>) {
240 (true, None)
242}
243
244pub fn check_memory_available(width: u32, height: u32) -> (bool, Option<String>) {
245 if width == 0 || height == 0 {
246 let error_msg = format!(
247 "Invalid image dimensions: {}x{} (zero dimension)",
248 width, height
249 );
250 log::error!("{}", error_msg);
251 return (false, Some(error_msg));
252 }
253
254 check_ram_available(width, height)
256}
257
258pub async fn decode_large_image(
264 path: PathBuf,
265 target_dimensions: Option<(u32, u32)>,
266) -> Option<(PathBuf, u32, u32, Vec<u8>)> {
267 tokio::task::spawn_blocking(move || {
269 log::info!("Starting async decode of {}", path.display());
270
271 match image::ImageReader::open(&path) {
274 Ok(reader) => {
275 match reader.with_guessed_format() {
276 Ok(mut reader) => {
277 let mut limits = image::Limits::default();
279 limits.max_alloc = Some(GALLERY_MEMORY_LIMIT_MB * DECIMAL_MB_TO_BYTES);
280 reader.limits(limits);
281
282 match reader.decode() {
283 Ok(img) => {
284 let rgba = img.into_rgba8();
285 let orig_width = rgba.width();
286 let orig_height = rgba.height();
287
288 let (final_img, width, height) = if let Some((target_w, target_h)) = target_dimensions {
290 log::info!(
291 "Resizing {}x{} -> {}x{} for memory optimization: {}",
292 orig_width, orig_height, target_w, target_h,
293 path.display()
294 );
295
296 let resized = image::imageops::resize(
298 &rgba,
299 target_w,
300 target_h,
301 image::imageops::FilterType::Lanczos3,
302 );
303
304 let resized_w = resized.width();
305 let resized_h = resized.height();
306
307 log::info!(
308 "Resize complete: {}x{} image now uses ~{} MB instead of ~{} MB",
309 resized_w, resized_h,
310 (resized_w as u64 * resized_h as u64 * 4) / MB_TO_BYTES,
311 (orig_width as u64 * orig_height as u64 * 4) / MB_TO_BYTES
312 );
313
314 (resized, resized_w, resized_h)
315 } else {
316 log::info!(
317 "Decoded {}x{} image at full resolution: {}",
318 orig_width, orig_height,
319 path.display()
320 );
321 (rgba, orig_width, orig_height)
322 };
323
324 let pixels = final_img.into_raw();
325 Some((path, width, height, pixels))
326 }
327 Err(e) => {
328 log::warn!("Failed to decode {}: {}", path.display(), e);
329 None
330 }
331 }
332 }
333 Err(e) => {
334 log::warn!("Failed to guess format for {}: {}", path.display(), e);
335 None
336 }
337 }
338 }
339 Err(e) => {
340 log::warn!("Failed to open {}: {}", path.display(), e);
341 None
342 }
343 }
344 })
345 .await
346 .ok()
347 .flatten()
348}
349
350#[derive(Debug, Default)]
352pub struct LargeImageManager {
353 decoding_images: HashSet<PathBuf>,
355 decoded_images: HashMap<PathBuf, widget::image::Handle>,
357 decoded_display_sizes: HashMap<PathBuf, (u32, u32)>,
359 decode_errors: HashMap<PathBuf, String>,
361 decode_generations: HashMap<PathBuf, u64>,
365}
366
367impl LargeImageManager {
368 pub fn new() -> Self {
369 Self::default()
370 }
371
372 pub fn is_decoding(&self, path: &Path) -> bool {
373 self.decoding_images.contains(path)
374 }
375
376 pub fn get_decoded(&self, path: &Path) -> Option<&widget::image::Handle> {
377 self.decoded_images.get(path)
378 }
379
380 pub fn get_error(&self, path: &Path) -> Option<&String> {
381 self.decode_errors.get(path)
382 }
383
384 pub fn store_decoded_with_generation(
387 &mut self,
388 path: PathBuf,
389 handle: widget::image::Handle,
390 display_size: Option<(u32, u32)>,
391 generation: u64,
392 ) -> bool {
393 if let Some(¤t_gen) = self.decode_generations.get(&path)
395 && generation != current_gen
396 {
397 log::info!(
398 "Discarding outdated decode for {} (generation {} != current {})",
399 path.display(),
400 generation,
401 current_gen
402 );
403 return false;
404 }
405
406 log::info!(
407 "Storing decoded image for {} (generation {})",
408 path.display(),
409 generation
410 );
411
412 self.decoded_images.insert(path.clone(), handle);
413 if let Some(size) = display_size {
414 self.decoded_display_sizes.insert(path.clone(), size);
415 }
416 self.decoding_images.remove(&path);
417 true
418 }
419
420 pub fn store_error(&mut self, path: PathBuf, error: String) {
421 self.decode_errors.insert(path.clone(), error);
422 self.decoding_images.remove(&path);
423 }
424
425 pub fn clear_error(&mut self, path: &Path) {
426 self.decode_errors.remove(path);
427 }
428
429 pub fn clear_cache(&mut self) {
430 log::info!(
431 "Clearing {} cached images from large image manager",
432 self.decoded_images.len()
433 );
434 self.decoded_images.clear();
435 }
436
437 pub fn cache_size(&self) -> usize {
438 self.decoded_images.len()
439 }
440
441 pub fn cache_is_empty(&self) -> bool {
442 self.decoded_images.is_empty()
443 }
444
445 pub fn needs_redecode_for_size(
449 &self,
450 path: &Path,
451 new_display_size: Option<(u32, u32)>,
452 ) -> bool {
453 let Some(new_size) = new_display_size else {
454 return false;
455 };
456
457 let Some(&old_size) = self.decoded_display_sizes.get(path) else {
458 return false;
459 };
460
461 const REDECODE_THRESHOLD: f32 = 0.2;
462
463 let width_increase = (new_size.0 as f32 / old_size.0 as f32) - 1.0;
464 let height_increase = (new_size.1 as f32 / old_size.1 as f32) - 1.0;
465
466 let needs_redecode =
467 width_increase > REDECODE_THRESHOLD || height_increase > REDECODE_THRESHOLD;
468
469 if needs_redecode {
470 log::info!(
471 "Display size increased significantly for {}: {}x{} -> {}x{} (increase: {:.1}% width, {:.1}% height) - re-decoding at higher resolution",
472 path.display(),
473 old_size.0,
474 old_size.1,
475 new_size.0,
476 new_size.1,
477 width_increase * 100.0,
478 height_increase * 100.0
479 );
480 } else if width_increase < -REDECODE_THRESHOLD || height_increase < -REDECODE_THRESHOLD {
481 log::debug!(
482 "Display size decreased for {}: {}x{} -> {}x{} (decrease: {:.1}% width, {:.1}% height) - keeping existing higher resolution",
483 path.display(),
484 old_size.0,
485 old_size.1,
486 new_size.0,
487 new_size.1,
488 width_increase * 100.0,
489 height_increase * 100.0
490 );
491 }
492
493 needs_redecode
494 }
495
496 pub fn try_decode(
499 &mut self,
500 path: &PathBuf,
501 display_dimensions: Option<(u32, u32)>,
502 ) -> (bool, Option<(u32, u32)>, u64) {
503 self.clear_error(path);
504 let is_currently_decoding = self.is_decoding(path);
505 let needs_redecode = self.needs_redecode_for_size(path, display_dimensions);
506
507 if is_currently_decoding && !needs_redecode {
508 let generation = self.decode_generations.get(path).copied().unwrap_or(0);
510 return (false, None, generation);
511 }
512
513 if self.get_decoded(path).is_some() && !needs_redecode && !is_currently_decoding {
514 let generation = self.decode_generations.get(path).copied().unwrap_or(0);
515 return (false, None, generation);
516 }
517
518 let Some((width, height)) = get_image_dimensions(path) else {
519 self.store_error(path.clone(), "Failed to read image dimensions".to_string());
520 return (false, None, 0);
521 };
522
523 let target_dimensions = if let Some((display_w, display_h)) = display_dimensions {
524 calculate_target_dimensions(width, height, display_w, display_h)
525 } else {
526 None
527 };
528
529 let (check_w, check_h) = target_dimensions.unwrap_or((width, height));
531 if !self.ensure_memory_available(path, check_w, check_h) {
532 return (false, None, 0);
533 }
534
535 let generation = self
537 .decode_generations
538 .entry(path.clone())
539 .and_modify(|g| *g += 1)
540 .or_insert(1);
541 let generation = *generation;
542
543 if is_currently_decoding {
544 log::info!(
545 "Cancelling previous decode for {} and starting new one (generation {})",
546 path.display(),
547 generation
548 );
549 }
550
551 self.decoding_images.insert(path.clone());
553 (true, target_dimensions, generation)
554 }
555
556 fn ensure_memory_available(&mut self, path: &Path, width: u32, height: u32) -> bool {
559 let (has_memory, error_opt) = check_memory_available(width, height);
560
561 if has_memory {
562 return true;
563 }
564
565 if self.cache_is_empty() {
566 if let Some(error_msg) = error_opt {
567 self.store_error(path.to_path_buf(), error_msg);
568 log::warn!(
569 "Cannot load {}: insufficient memory and cache is empty",
570 path.display()
571 );
572 }
573 return false;
574 }
575
576 log::info!(
577 "Insufficient memory, clearing {} cached images",
578 self.cache_size()
579 );
580 self.clear_cache();
581
582 let (has_memory_after_clear, error_opt_after) = check_memory_available(width, height);
583
584 if has_memory_after_clear {
585 log::info!("Memory available after cache clear, proceeding with decode");
586 return true;
587 }
588
589 if let Some(error_msg) = error_opt_after {
590 self.store_error(path.to_path_buf(), error_msg);
591 log::warn!(
592 "Cannot load {}: insufficient memory even after cache clear",
593 path.display()
594 );
595 }
596 false
597 }
598}