kindle_screensaver/
lib.rs1pub 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 let img = image::load_from_memory(input_bytes)?;
46
47 let kindle_img = process_for_kindle(img, &options)?;
49
50 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 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}