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 for_size( &self, sw: u32, sh: u32 ) -> ImageData
{
if sw == 0 || sh == 0 || sw >= sh
{
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 cropped = crop_left_to_aspect( &self.landscape, sw, sh );
let result = ( Arc::clone( &cropped.0 ), cropped.1, cropped.2 );
*guard = Some( ( key, cropped ) );
result
}
}
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>
{
use image::GenericImageView as _;
let img = image::open( path )?;
let ( w, h ) = img.dimensions();
Ok( ( Arc::new( img.to_rgba8().into_raw() ), w, h ) )
}
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 ) as usize;
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 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 ) );
}
}