kindle_screensaver/
lib.rs

1pub mod structs;
2pub mod dithering;
3
4use bytes::Bytes;
5use rayon::prelude::*;
6use rayon::iter::ParallelBridge;
7use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Rgba};
8use std::io::Cursor;
9use std::path::Path;
10use crate::dithering::{apply_floyd_steinberg_dithering, apply_ordered_dithering, apply_color_dithering, apply_color_ordered_dithering};
11
12pub use structs::{KindleModel, DitheringAlgorithm, ConversionOptions, KindleError, ResizingMethod};
13
14pub type Result<T> = std::result::Result<T, KindleError>;
15
16pub fn convert_to_kindle(
17    input_path: &Path,
18    options: ConversionOptions,
19    output_path: Option<&Path>,
20) -> Result<String> {
21    let img = image::open(input_path)?;
22    
23    let kindle_img = process_for_kindle(img, &options)?;
24    
25    let output = match output_path {
26        Some(path) => path.to_path_buf(),
27        None => {
28            let stem = input_path.file_stem().unwrap_or_default();
29            let extension = "png";
30            let filename = format!("{}_kindle.{}", stem.to_string_lossy(), extension);
31            input_path.with_file_name(filename)
32        }
33    };
34    
35    kindle_img.save(&output)?;
36    
37    Ok(output.to_string_lossy().to_string())
38}
39
40pub fn convert_from_bytes(
41    input_bytes: &[u8], 
42    options: ConversionOptions
43) -> Result<Bytes> {
44    // load image from bytes
45    let img = image::load_from_memory(input_bytes)?;
46    
47    // process image
48    let kindle_img = process_for_kindle(img, &options)?;
49    
50    // convert to bytes
51    let mut buffer = Cursor::new(Vec::new());
52    kindle_img.write_to(&mut buffer, ImageFormat::Png)?;
53    
54    Ok(Bytes::from(buffer.into_inner()))
55}
56
57fn process_for_kindle(img: DynamicImage, options: &ConversionOptions) -> Result<DynamicImage> {
58    
59    if !options.optimize_contrast && options.dithering == DitheringAlgorithm::None {
60        let resized = resize_maintain_aspect_ratio(&img, options);
61        if !options.model.is_color() {
62            return Ok(convert_to_grayscale(resized));
63        } else {
64            return Ok(resized);
65        }
66    }
67    
68    let processed = resize_maintain_aspect_ratio(&img, options);
69    
70    let processed = if options.optimize_contrast {
71        optimize_contrast_for_eink(processed)
72    } else {
73        processed
74    };
75    
76    if !options.model.is_color() {
77        // convert to grayscale
78        let grayscale = convert_to_grayscale(processed);
79            
80        match options.dithering {
81            DitheringAlgorithm::None => Ok(grayscale),
82            DitheringAlgorithm::FloydSteinberg => Ok(apply_floyd_steinberg_dithering(grayscale)),
83            DitheringAlgorithm::Ordered => Ok(apply_ordered_dithering(grayscale)),
84        }
85    } else {
86        match options.dithering {
87            DitheringAlgorithm::None => Ok(processed),
88            DitheringAlgorithm::FloydSteinberg => Ok(apply_color_dithering(processed)),
89            DitheringAlgorithm::Ordered => Ok(apply_color_ordered_dithering(processed)),
90        }
91    }
92}
93
94pub fn batch_convert(
95    input_path: &Path,
96    options_list: &[ConversionOptions],
97    output_dir: &Path,
98) -> Result<Vec<String>> {
99    let img = image::open(input_path)?;
100    
101    let stem = input_path.file_stem().unwrap_or_default();
102    
103    options_list.par_iter()
104        .map(|options| {
105            let kindle_img = process_for_kindle(img.clone(), options)?;
106            
107            let model_name = format!("{:?}", options.model).to_lowercase();
108            let filename = format!("{}_{}.png", stem.to_string_lossy(), model_name);
109            let output_path = output_dir.join(filename);
110            
111            kindle_img.save(&output_path)?;
112            Ok(output_path.to_string_lossy().to_string())
113        })
114        .collect()
115}
116
117fn resize_maintain_aspect_ratio(img: &DynamicImage, options: &ConversionOptions) -> DynamicImage {
118    let (target_width, target_height) = options.model.dimensions();
119    let (width, height) = img.dimensions();
120    
121    let width_scale = target_width as f32 / width as f32;
122    let height_scale = target_height as f32 / height as f32;
123    
124    let scale = width_scale.max(height_scale);
125    
126    let scaled_width = (width as f32 * scale) as u32;
127    let scaled_height = (height as f32 * scale) as u32;
128
129    let mut resized = img.resize(scaled_width, scaled_height, options.filter_type());
130    
131    let x_offset = (scaled_width - target_width) / 2;
132    let y_offset = (scaled_height - target_height) / 2;
133    
134    resized.crop(x_offset, y_offset, target_width, target_height)
135}
136
137fn convert_to_grayscale(img: DynamicImage) -> DynamicImage {
138    img.grayscale()
139}
140
141fn optimize_contrast_for_eink(img: DynamicImage) -> DynamicImage {
142    let rgba_img = img.to_rgba8();
143    let (width, height) = rgba_img.dimensions();
144    
145    let pixel_values: Vec<_> = rgba_img.pixels().collect();
146    
147    let (min_val, max_val) = pixel_values.par_iter()
148        .map(|pixel| {
149            let luma = (0.299 * pixel[0] as f32 + 0.587 * pixel[1] as f32 + 0.114 * pixel[2] as f32) as u8;
150            (luma, luma)
151        })
152        .reduce(
153            || (255, 0),
154            |(min1, max1), (min2, max2)| (min1.min(min2), max1.max(max2))
155        );
156    
157    let range = if max_val > min_val { max_val - min_val } else { 1 };
158    
159    let lut: Vec<u8> = (0..=255)
160        .map(|v| {
161            let scaled = (v as f32 - min_val as f32) / range as f32 * 255.0;
162            scaled.clamp(0.0, 255.0) as u8
163        })
164        .collect();
165    
166    let mut buffer = ImageBuffer::new(width, height);
167    
168    buffer.enumerate_pixels_mut().par_bridge().for_each(|(x, y, pixel)| {
169        let source_pixel = rgba_img.get_pixel(x, y);
170        
171        let r = lut[source_pixel[0] as usize];
172        let g = lut[source_pixel[1] as usize];
173        let b = lut[source_pixel[2] as usize];
174        
175        *pixel = Rgba([r, g, b, source_pixel[3]]);
176    });
177    
178    DynamicImage::ImageRgba8(buffer)
179}