🔒 Repository is read-only – file editing is disabled.

luna/crates/libluna-render/src/wallpaper.rs main

85 lines Raw ← Back
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
//! Tapeta pulpitu: dekodowanie PNG i skalowanie „cover".
//!
//! Trzymamy to w `libluna-render`, bo tylko ta warstwa zna `tiny-skia` (a więc i PNG).
//! Kompozytor wczytuje grafikę raz i trzyma już przeskalowaną wersję, żeby co klatkę
//! szedł tani blit 1:1, a nie skalowanie w pętli renderowania.

use tiny_skia::{BlendMode, FilterQuality, Pixmap, PixmapPaint, PixmapRef, Transform};

use crate::scene::ImageData;

/// Dekoduje PNG do [`ImageData`] (premultiplied RGBA8 — spójnie z resztą obrazów sceny).
///
/// `None`, gdy bajty nie są poprawnym PNG albo brakuje pamięci.
pub fn decode_png(bytes: &[u8]) -> Option<ImageData> {
    let pixmap = Pixmap::decode_png(bytes).ok()?;
    Some(ImageData::new(
        pixmap.width(),
        pixmap.height(),
        pixmap.data().to_vec(),
    ))
}

/// Skaluje obraz tak, by **pokrył** `w`×`h`: zachowuje proporcje, przycina nadmiar
/// i centruje. Wynik ma dokładnie `w`×`h`, więc nadaje się do blitu 1:1.
pub fn cover(src: &ImageData, w: u32, h: u32) -> ImageData {
    let (w, h) = (w.max(1), h.max(1));
    let Some(sp) = PixmapRef::from_bytes(&src.rgba, src.width.max(1), src.height.max(1)) else {
        return ImageData::empty(w, h);
    };
    let Some(mut dst) = Pixmap::new(w, h) else {
        return ImageData::empty(w, h);
    };
    // Współczynnik „cover”: większy z dwóch, żeby nic nie zostało puste.
    let scale = (w as f32 / sp.width() as f32).max(h as f32 / sp.height() as f32);
    let dw = sp.width() as f32 * scale;
    let dh = sp.height() as f32 * scale;
    let tx = (w as f32 - dw) / 2.0;
    let ty = (h as f32 - dh) / 2.0;
    let paint = PixmapPaint {
        blend_mode: BlendMode::Source,
        quality: FilterQuality::Bilinear,
        opacity: 1.0,
    };
    let transform = Transform::from_scale(scale, scale).post_translate(tx, ty);
    dst.draw_pixmap(0, 0, sp, &paint, transform, None);
    // Tło pulpitu musi być nieprzezroczyste — renderer kopiuje je wprost, a przezroczyste
    // piksele dałyby czarne dziury.
    for px in dst.data_mut().chunks_exact_mut(4) {
        px[3] = 0xff;
    }
    ImageData::new(w, h, dst.data().to_vec())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cover_fills_requested_size() {
        let src = ImageData::new(4, 2, vec![128; 4 * 2 * 4]);
        let out = cover(&src, 10, 10);
        assert_eq!((out.width, out.height), (10, 10));
        assert_eq!(out.rgba.len(), 10 * 10 * 4);
    }

    #[test]
    fn cover_keeps_aspect_by_cropping() {
        // Szeroki obraz (4×2) na kwadrat: skala wg szerszego boku, więc wychodzi kwadrat.
        let src = ImageData::new(4, 2, vec![0; 4 * 2 * 4]);
        let out = cover(&src, 8, 8);
        assert_eq!((out.width, out.height), (8, 8));
    }

    #[test]
    fn decode_png_roundtrip() {
        // Zakoduj prosty obraz i sprawdź, że dekodowanie zwraca ten sam rozmiar.
        let mut pm = Pixmap::new(3, 5).unwrap();
        pm.fill(tiny_skia::Color::from_rgba8(10, 20, 30, 255));
        let png = pm.encode_png().unwrap();
        let img = decode_png(&png).unwrap();
        assert_eq!((img.width, img.height), (3, 5));
        // Niepoprawne dane → `None` (nie panikujemy).
        assert!(decode_png(b"to nie jest png").is_none());
    }
}