use std::path::Path;
use std::sync::{ Arc, Mutex };
pub type ImageData = ( Arc<Vec<u8>>, u32, u32 );
pub struct WallpaperBundle
{
landscape: ImageData,
cache: Mutex<Option<( ( u32, u32 ), ImageData )>>,
}
impl WallpaperBundle
{
pub fn from_decoded( landscape: ImageData ) -> Self
{
Self
{
landscape,
cache: Mutex::new( None ),
}
}
pub fn from_bytes( bytes: &[u8] ) -> Result<Self, image::ImageError>
{
Ok( Self::from_decoded( decode_bytes( bytes )? ) )
}
pub fn from_path( path: &Path ) -> Result<Self, image::ImageError>
{
Ok( Self::from_decoded( decode_path( path )? ) )
}
pub fn from_path_or_bytes( path: Option<&Path>, bundled_fallback: &[u8] ) -> Self
{
if let Some( p ) = path
{
match decode_path( p )
{
Ok( img ) => return Self::from_decoded( img ),
Err( e ) => eprintln!(
"ltk: failed to load wallpaper {}: {} — falling back to bundled",
p.display(), e,
),
}
}
Self::from_bytes( bundled_fallback ).expect( "bundled wallpaper must decode" )
}
pub fn from_path_or_solid( path: Option<&Path>, r: u8, g: u8, b: u8 ) -> Self
{
if let Some( p ) = path
{
match decode_path( p )
{
Ok( img ) => return Self::from_decoded( img ),
Err( e ) => eprintln!(
"ltk: failed to load wallpaper {}: {} — falling back to solid colour",
p.display(), e,
),
}
}
let rgba = Arc::new( vec![ r, g, b, 255u8 ] );
Self::from_decoded( ( rgba, 1, 1 ) )
}
pub fn landscape( &self ) -> ImageData
{
( Arc::clone( &self.landscape.0 ), self.landscape.1, self.landscape.2 )
}
pub fn blurred( &self, divisor: u32, sigma: f32 ) -> Self
{
let ( ref rgba, w, h ) = self.landscape;
let divisor = divisor.max( 1 );
let nw = ( w / divisor ).max( 1 );
let nh = ( h / divisor ).max( 1 );
let Some( view ) = image::ImageBuffer::<image::Rgba<u8>, &[u8]>::from_raw( w, h, rgba.as_slice() ) else
{
return Self::from_decoded( self.landscape() );
};
let small = image::imageops::resize( &view, nw, nh, image::imageops::FilterType::Triangle );
let blurred = image::imageops::fast_blur( &small, sigma );
Self::from_decoded( ( Arc::new( blurred.into_raw() ), nw, nh ) )
}
pub fn for_size( &self, sw: u32, sh: u32 ) -> ImageData
{
if sw == 0 || sh == 0
{
return self.landscape();
}
let key = ( sw, sh );
let mut guard = self.cache.lock().expect( "wallpaper cache poisoned" );
if let Some( ( cached_key, data ) ) = guard.as_ref()
{
if *cached_key == key
{
return ( Arc::clone( &data.0 ), data.1, data.2 );
}
}
let base = if sw >= sh { self.landscape() } else { crop_left_to_aspect( &self.landscape, sw, sh ) };
let cropped = fit_to_surface( base, sw, sh );
let result = ( Arc::clone( &cropped.0 ), cropped.1, cropped.2 );
*guard = Some( ( key, cropped ) );
result
}
}
fn fit_to_surface( src: ImageData, sw: u32, sh: u32 ) -> ImageData
{
let ( ref rgba, w, h ) = src;
if w <= sw && h <= sh { return src; }
let scale = ( sw as f32 / w as f32 ).max( sh as f32 / h as f32 );
let nw = ( ( w as f32 * scale ).ceil() as u32 ).max( 1 );
let nh = ( ( h as f32 * scale ).ceil() as u32 ).max( 1 );
let Some( view ) = image::ImageBuffer::<image::Rgba<u8>, &[u8]>::from_raw( w, h, rgba.as_slice() ) else { return src };
let resized = image::imageops::resize( &view, nw, nh, image::imageops::FilterType::Triangle );
( Arc::new( resized.into_raw() ), nw, nh )
}
fn decode_bytes( bytes: &[u8] ) -> Result<ImageData, image::ImageError>
{
use image::GenericImageView as _;
let img = image::load_from_memory( bytes )?;
let ( w, h ) = img.dimensions();
Ok( ( Arc::new( img.to_rgba8().into_raw() ), w, h ) )
}
fn decode_path( path: &Path ) -> Result<ImageData, image::ImageError>
{
decode_bytes( &crate::vfs::read( path )? )
}
fn crop_left_to_aspect( src: &ImageData, target_w: u32, target_h: u32 ) -> ImageData
{
let ( ref rgba, sw, sh ) = *src;
if sw == 0 || sh == 0 || target_w == 0 || target_h == 0
{
return ( Arc::clone( rgba ), sw, sh );
}
let target_aspect = target_w as f32 / target_h as f32;
let new_w_f = ( sh as f32 * target_aspect ).round();
let new_w = ( new_w_f as u32 ).clamp( 1, sw );
if new_w >= sw
{
return ( Arc::clone( rgba ), sw, sh );
}
let mut out = Vec::with_capacity( ( new_w as usize ) * ( sh as usize ) * 4 );
let row_bytes_src = ( sw as usize ) * 4;
let row_bytes_new = ( new_w as usize ) * 4;
for y in 0..( sh as usize )
{
let start = y * row_bytes_src;
out.extend_from_slice( &rgba[ start .. start + row_bytes_new ] );
}
( Arc::new( out ), new_w, sh )
}
#[ cfg( test ) ]
mod tests
{
use super::*;
fn xgrad( w: u32, h: u32 ) -> ImageData
{
let mut buf = Vec::with_capacity( ( w * h * 4 ) as usize );
for _y in 0..h
{
for x in 0..w
{
buf.extend_from_slice( &[ x as u8, 0, 0, 255 ] );
}
}
( Arc::new( buf ), w, h )
}
#[ test ]
fn landscape_surface_returns_full_image()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 16, 8 ) );
let ( _, w, h ) = bundle.for_size( 32, 16 );
assert_eq!( ( w, h ), ( 16, 8 ) );
}
#[ test ]
fn portrait_surface_left_crops()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 100, 10 ) );
let ( bytes, w, h ) = bundle.for_size( 9, 16 );
assert_eq!( h, 10 );
assert_eq!( w, 6 );
assert_eq!( bytes[0], 0 );
let last = ( w as usize - 1 ) * 4;
assert_eq!( bytes[ last ], 5 );
}
#[ test ]
fn portrait_crop_is_cached()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 100, 10 ) );
let a = bundle.for_size( 9, 16 );
let b = bundle.for_size( 9, 16 );
assert!( Arc::ptr_eq( &a.0, &b.0 ) );
}
#[ test ]
fn portrait_narrower_than_target_returns_source()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 1, 10 ) );
let ( _, w, h ) = bundle.for_size( 9, 16 );
assert_eq!( ( w, h ), ( 1, 10 ) );
}
#[ test ]
fn oversized_source_is_downscaled_to_surface()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 64, 64 ) );
let ( _, w, h ) = bundle.for_size( 16, 16 );
assert_eq!( ( w, h ), ( 16, 16 ) );
let ( _, w, h ) = bundle.for_size( 8, 16 );
assert_eq!( ( w, h ), ( 8, 16 ) );
}
#[ test ]
fn zero_size_returns_landscape()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 4, 4 ) );
let ( _, w, h ) = bundle.for_size( 0, 0 );
assert_eq!( ( w, h ), ( 4, 4 ) );
}
#[ test ]
fn blurred_is_divided_and_never_empty()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 64, 32 ) );
let ( rgba, w, h ) = bundle.blurred( 4, 1.0 ).landscape();
assert_eq!( ( w, h ), ( 16, 8 ) );
assert_eq!( rgba.len(), 16 * 8 * 4 );
let ( _, w, h ) = WallpaperBundle::from_decoded( xgrad( 2, 2 ) ).blurred( 8, 1.0 ).landscape();
assert_eq!( ( w, h ), ( 1, 1 ) );
}
#[ test ]
fn blurred_keeps_a_flat_colour()
{
let flat = ( Arc::new( [ 10u8, 20, 30, 255 ].repeat( 8 * 8 ) ), 8, 8 );
let ( rgba, _, _ ) = WallpaperBundle::from_decoded( flat ).blurred( 2, 2.0 ).landscape();
assert!( rgba.chunks( 4 ).all( |px| px == [ 10, 20, 30, 255 ] ) );
}
}