| 1 | //! Background image loading and processing for the greeter |
| 2 | //! |
| 3 | //! Loads background images, scales to fit screen, applies blur and brightness adjustments. |
| 4 | |
| 5 | use anyhow::{Context, Result}; |
| 6 | use image::{imageops, RgbaImage}; |
| 7 | |
| 8 | /// Load and process a background image for the greeter |
| 9 | pub fn load_blurred_background( |
| 10 | path: &str, |
| 11 | width: u32, |
| 12 | height: u32, |
| 13 | blur_radius: f32, |
| 14 | brightness: f32, |
| 15 | ) -> Result<RgbaImage> { |
| 16 | let img = image::open(path) |
| 17 | .with_context(|| format!("Failed to open background image: {}", path))? |
| 18 | .to_rgba8(); |
| 19 | |
| 20 | // Scale to screen size (cover mode - fills screen, may crop) |
| 21 | let scaled = scale_to_cover(&img, width, height); |
| 22 | |
| 23 | // Apply gaussian blur |
| 24 | let blurred = imageops::blur(&scaled, blur_radius); |
| 25 | |
| 26 | // Adjust brightness (typically darken for better text contrast) |
| 27 | let adjusted = adjust_brightness(&blurred, brightness); |
| 28 | |
| 29 | Ok(adjusted) |
| 30 | } |
| 31 | |
| 32 | /// Generate a solid color fallback background |
| 33 | pub fn solid_background(width: u32, height: u32, r: u8, g: u8, b: u8) -> RgbaImage { |
| 34 | RgbaImage::from_fn(width, height, |_, _| image::Rgba([r, g, b, 255])) |
| 35 | } |
| 36 | |
| 37 | /// Scale image to cover the target dimensions (may crop) |
| 38 | fn scale_to_cover(img: &RgbaImage, target_w: u32, target_h: u32) -> RgbaImage { |
| 39 | let (src_w, src_h) = img.dimensions(); |
| 40 | |
| 41 | // Calculate scale to cover entire target |
| 42 | let scale = (target_w as f32 / src_w as f32).max(target_h as f32 / src_h as f32); |
| 43 | |
| 44 | let new_w = (src_w as f32 * scale).ceil() as u32; |
| 45 | let new_h = (src_h as f32 * scale).ceil() as u32; |
| 46 | |
| 47 | let resized = imageops::resize(img, new_w, new_h, imageops::FilterType::Lanczos3); |
| 48 | |
| 49 | // Crop to center |
| 50 | let x = (new_w.saturating_sub(target_w)) / 2; |
| 51 | let y = (new_h.saturating_sub(target_h)) / 2; |
| 52 | |
| 53 | imageops::crop_imm(&resized, x, y, target_w, target_h).to_image() |
| 54 | } |
| 55 | |
| 56 | /// Adjust image brightness by a factor (0.0-1.0 darkens, >1.0 brightens) |
| 57 | fn adjust_brightness(img: &RgbaImage, factor: f32) -> RgbaImage { |
| 58 | let mut result = img.clone(); |
| 59 | for pixel in result.pixels_mut() { |
| 60 | pixel[0] = (pixel[0] as f32 * factor).min(255.0) as u8; |
| 61 | pixel[1] = (pixel[1] as f32 * factor).min(255.0) as u8; |
| 62 | pixel[2] = (pixel[2] as f32 * factor).min(255.0) as u8; |
| 63 | // Alpha unchanged |
| 64 | } |
| 65 | result |
| 66 | } |
| 67 | |
| 68 | /// Convert RGBA image to BGRA for Cairo/X11 (little-endian ARGB32) |
| 69 | pub fn rgba_to_bgra(img: &RgbaImage) -> Vec<u8> { |
| 70 | let (width, height) = img.dimensions(); |
| 71 | let stride = (width * 4) as usize; |
| 72 | let mut data = vec![0u8; stride * height as usize]; |
| 73 | |
| 74 | for (y, row) in img.rows().enumerate() { |
| 75 | for (x, pixel) in row.enumerate() { |
| 76 | let offset = y * stride + x * 4; |
| 77 | // RGBA -> BGRA |
| 78 | data[offset] = pixel[2]; // B |
| 79 | data[offset + 1] = pixel[1]; // G |
| 80 | data[offset + 2] = pixel[0]; // R |
| 81 | data[offset + 3] = pixel[3]; // A |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | data |
| 86 | } |
| 87 | |
| 88 | /// Render background image to Cairo context |
| 89 | pub fn render_to_cairo( |
| 90 | ctx: &cairo::Context, |
| 91 | img: &RgbaImage, |
| 92 | ) -> Result<()> { |
| 93 | let (width, height) = img.dimensions(); |
| 94 | let bgra_data = rgba_to_bgra(img); |
| 95 | |
| 96 | let surface = cairo::ImageSurface::create_for_data( |
| 97 | bgra_data, |
| 98 | cairo::Format::ARgb32, |
| 99 | width as i32, |
| 100 | height as i32, |
| 101 | (width * 4) as i32, |
| 102 | ) |
| 103 | .context("Failed to create Cairo surface from background")?; |
| 104 | |
| 105 | ctx.set_source_surface(&surface, 0.0, 0.0)?; |
| 106 | ctx.paint()?; |
| 107 | |
| 108 | Ok(()) |
| 109 | } |
| 110 |