1use std::fmt;
2use std::io::Cursor;
3use std::path::Path;
4
5use base64::Engine as _;
6use base64::engine::general_purpose::STANDARD;
7use image::{DynamicImage, GrayImage, RgbImage};
8
9#[derive(Debug, thiserror::Error)]
10pub enum RenderError {
11 #[error("{width}x{height} in {layout} needs {expected} bytes, but {actual} were given")]
12 UnexpectedSize {
13 width: u32,
14 height: u32,
15 layout: PixelLayout,
16 expected: usize,
17 actual: usize,
18 },
19
20 #[error("a {width}x{height} image does not fit a raster of {pixel} with a {gap} gap")]
21 RasterTooLarge {
22 width: u32,
23 height: u32,
24 pixel: u32,
25 gap: u32,
26 },
27
28 #[error("could not encode a {width}x{height} image as {format}")]
29 Encode {
30 width: u32,
31 height: u32,
32 format: ImageFormat,
33 #[source]
34 source: image::ImageError,
35 },
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ImageFormat {
40 Bmp,
41 Jpeg,
42 Png,
43}
44
45impl ImageFormat {
46 pub fn from_path(path: &Path) -> Option<Self> {
47 let extension = path.extension()?.to_str()?.to_ascii_lowercase();
48
49 match extension.as_str() {
50 "bmp" => Some(Self::Bmp),
51 "jpg" | "jpeg" => Some(Self::Jpeg),
52 "png" => Some(Self::Png),
53 _ => None,
54 }
55 }
56
57 fn encoding(self) -> image::ImageFormat {
58 match self {
59 Self::Bmp => image::ImageFormat::Bmp,
60 Self::Jpeg => image::ImageFormat::Jpeg,
61 Self::Png => image::ImageFormat::Png,
62 }
63 }
64}
65
66impl fmt::Display for ImageFormat {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::Bmp => f.write_str("bmp"),
70 Self::Jpeg => f.write_str("jpeg"),
71 Self::Png => f.write_str("png"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Raster {
78 pixel: u32,
79 gap: u32,
80}
81
82impl Raster {
83 pub fn new(pixel: u32, gap: u32) -> Option<Self> {
84 (pixel > 0).then_some(Self { pixel, gap })
85 }
86
87 pub fn pixel(self) -> u32 {
88 self.pixel
89 }
90
91 pub fn gap(self) -> u32 {
92 self.gap
93 }
94
95 fn extent(self, count: u32) -> Option<u32> {
96 let cells = count.checked_mul(self.pixel)?;
97 let gaps = count.saturating_sub(1).checked_mul(self.gap)?;
98
99 cells.checked_add(gaps)
100 }
101}
102
103impl Default for Raster {
104 fn default() -> Self {
105 Self { pixel: 3, gap: 1 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum PixelLayout {
111 Rgb888,
112 Bgr888,
113 Gray8,
114 Gray4,
115}
116
117impl PixelLayout {
118 fn byte_len(self, width: u32, height: u32) -> usize {
119 let pixels = (width as usize) * (height as usize);
120
121 match self {
122 Self::Rgb888 | Self::Bgr888 => pixels * 3,
123 Self::Gray8 => pixels,
124 Self::Gray4 => pixels.div_ceil(2),
125 }
126 }
127}
128
129impl fmt::Display for PixelLayout {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 match self {
132 Self::Rgb888 => f.write_str("rgb888"),
133 Self::Bgr888 => f.write_str("bgr888"),
134 Self::Gray8 => f.write_str("l8"),
135 Self::Gray4 => f.write_str("l4"),
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub struct RawImage {
142 width: u32,
143 height: u32,
144 image: DynamicImage,
145}
146
147impl RawImage {
148 pub fn new(
149 width: u32,
150 height: u32,
151 layout: PixelLayout,
152 pixels: &[u8],
153 ) -> Result<Self, RenderError> {
154 let expected = layout.byte_len(width, height);
155
156 if pixels.len() != expected {
157 return Err(RenderError::UnexpectedSize {
158 width,
159 height,
160 layout,
161 expected,
162 actual: pixels.len(),
163 });
164 }
165
166 let image = match layout {
167 PixelLayout::Rgb888 => DynamicImage::ImageRgb8(
168 RgbImage::from_raw(width, height, pixels.to_vec()).expect("checked above"),
169 ),
170 PixelLayout::Bgr888 => {
171 let mut pixels = pixels.to_vec();
172
173 for pixel in pixels.chunks_exact_mut(3) {
175 pixel.swap(0, 2);
176 }
177
178 DynamicImage::ImageRgb8(
179 RgbImage::from_raw(width, height, pixels).expect("checked above"),
180 )
181 }
182 PixelLayout::Gray8 => DynamicImage::ImageLuma8(
183 GrayImage::from_raw(width, height, pixels.to_vec()).expect("checked above"),
184 ),
185 PixelLayout::Gray4 => {
186 let mut expanded = Vec::with_capacity(pixels.len() * 2);
187
188 for byte in pixels {
189 expanded.push((byte >> 4) * 17);
190 expanded.push((byte & 0x0F) * 17);
191 }
192
193 expanded.truncate((width as usize) * (height as usize));
194
195 DynamicImage::ImageLuma8(
196 GrayImage::from_raw(width, height, expanded).expect("checked above"),
197 )
198 }
199 };
200
201 Ok(Self {
202 width,
203 height,
204 image,
205 })
206 }
207
208 pub fn width(&self) -> u32 {
209 self.width
210 }
211
212 pub fn height(&self) -> u32 {
213 self.height
214 }
215
216 pub fn with_raster(&self, raster: Raster) -> Result<Self, RenderError> {
217 let too_large = || RenderError::RasterTooLarge {
218 width: self.width,
219 height: self.height,
220 pixel: raster.pixel,
221 gap: raster.gap,
222 };
223
224 let width = raster.extent(self.width).ok_or_else(too_large)?;
225 let height = raster.extent(self.height).ok_or_else(too_large)?;
226
227 let step = raster.pixel + raster.gap;
228
229 let image = match &self.image {
230 DynamicImage::ImageLuma8(source) => {
231 let mut target = GrayImage::new(width, height);
232 self::paint(source, &mut target, step, raster.pixel);
233 DynamicImage::ImageLuma8(target)
234 }
235 source => {
236 let source = source.to_rgb8();
237 let mut target = RgbImage::new(width, height);
238 self::paint(&source, &mut target, step, raster.pixel);
239 DynamicImage::ImageRgb8(target)
240 }
241 };
242
243 Ok(Self {
244 width,
245 height,
246 image,
247 })
248 }
249
250 pub fn encode(&self, format: ImageFormat) -> Result<Vec<u8>, RenderError> {
251 let mut buffer = Cursor::new(Vec::new());
252
253 self.image
254 .write_to(&mut buffer, format.encoding())
255 .map_err(|source| RenderError::Encode {
256 width: self.width,
257 height: self.height,
258 format,
259 source,
260 })?;
261
262 Ok(buffer.into_inner())
263 }
264
265 pub fn encode_base64(&self, format: ImageFormat) -> Result<String, RenderError> {
266 self.encode(format).map(|bytes| STANDARD.encode(bytes))
267 }
268}
269
270fn paint<P: image::Pixel<Subpixel=u8>>(
271 source: &image::ImageBuffer<P, Vec<u8>>,
272 target: &mut image::ImageBuffer<P, Vec<u8>>,
273 step: u32,
274 size: u32,
275) {
276 for (x, y, pixel) in source.enumerate_pixels() {
277 for offset_y in 0..size {
278 for offset_x in 0..size {
279 target.put_pixel(x * step + offset_x, y * step + offset_y, *pixel);
280 }
281 }
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn picks_a_format_from_the_file_extension() {
291 assert_eq!(
292 ImageFormat::from_path(Path::new("frame.bmp")),
293 Some(ImageFormat::Bmp)
294 );
295 assert_eq!(
296 ImageFormat::from_path(Path::new("frame.JPEG")),
297 Some(ImageFormat::Jpeg)
298 );
299 assert_eq!(
300 ImageFormat::from_path(Path::new("./out/frame.png")),
301 Some(ImageFormat::Png)
302 );
303 assert_eq!(ImageFormat::from_path(Path::new("frame.raw")), None);
304 assert_eq!(ImageFormat::from_path(Path::new("frame")), None);
305 }
306
307 #[test]
308 fn reads_rgb_in_the_order_it_is_given() {
309 let image = RawImage::new(1, 1, PixelLayout::Rgb888, &[0x11, 0x22, 0x33]).unwrap();
310
311 assert_eq!(
312 image.image.as_rgb8().unwrap().get_pixel(0, 0).0,
313 [0x11, 0x22, 0x33]
314 );
315 }
316
317 #[test]
318 fn swaps_the_channels_of_a_bgr_buffer() {
319 let image = RawImage::new(1, 1, PixelLayout::Bgr888, &[0x11, 0x22, 0x33]).unwrap();
320
321 assert_eq!(
322 image.image.as_rgb8().unwrap().get_pixel(0, 0).0,
323 [0x33, 0x22, 0x11]
324 );
325 }
326
327 #[test]
328 fn expands_four_bit_grayscale_to_eight() {
329 let image = RawImage::new(2, 1, PixelLayout::Gray4, &[0xf0]).unwrap();
330 let luma = image.image.as_luma8().unwrap();
331
332 assert_eq!(luma.get_pixel(0, 0).0, [0xff]);
333 assert_eq!(luma.get_pixel(1, 0).0, [0x00]);
334 }
335
336 #[test]
337 fn rejects_a_buffer_which_does_not_match_the_geometry() {
338 let error = RawImage::new(72, 16, PixelLayout::Rgb888, &[0; 16]).unwrap_err();
339
340 assert_eq!(
341 error.to_string(),
342 "72x16 in rgb888 needs 3456 bytes, but 16 were given"
343 );
344 }
345
346 #[test]
347 fn encodes_every_format_it_supports() {
348 let image = RawImage::new(2, 2, PixelLayout::Gray8, &[0, 64, 128, 255]).unwrap();
349
350 assert!(image.encode(ImageFormat::Bmp).unwrap().starts_with(b"BM"));
351 assert!(
352 image
353 .encode(ImageFormat::Jpeg)
354 .unwrap()
355 .starts_with(&[0xff, 0xd8])
356 );
357 assert!(
358 image
359 .encode(ImageFormat::Png)
360 .unwrap()
361 .starts_with(b"\x89PNG")
362 );
363 }
364
365 #[test]
366 fn a_raster_spaces_the_pixels_out_and_leaves_black_between_them() {
367 let image = RawImage::new(
368 2,
369 2,
370 PixelLayout::Rgb888,
371 &[
372 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
373 ],
374 )
375 .unwrap();
376
377 let raster = Raster::new(3, 1).unwrap();
378 let rastered = image.with_raster(raster).unwrap();
379
380 assert_eq!((rastered.width(), rastered.height()), (7, 7));
381
382 let pixels = rastered.image.to_rgb8();
383
384 assert_eq!(pixels.get_pixel(0, 0).0, [0xff, 0x00, 0x00]);
385 assert_eq!(pixels.get_pixel(2, 2).0, [0xff, 0x00, 0x00]);
386 assert_eq!(pixels.get_pixel(3, 0).0, [0x00, 0x00, 0x00]);
387 assert_eq!(pixels.get_pixel(0, 3).0, [0x00, 0x00, 0x00]);
388 assert_eq!(pixels.get_pixel(4, 0).0, [0x00, 0xff, 0x00]);
389 assert_eq!(pixels.get_pixel(4, 4).0, [0xff, 0xff, 0xff]);
390 }
391
392 #[test]
393 fn a_raster_keeps_a_grayscale_image_grayscale() {
394 let image = RawImage::new(2, 1, PixelLayout::Gray8, &[0xff, 0x40]).unwrap();
395 let rastered = image.with_raster(Raster::default()).unwrap();
396
397 assert_eq!((rastered.width(), rastered.height()), (7, 3));
398
399 let pixels = rastered.image.as_luma8().expect("stays grayscale");
400
401 assert_eq!(pixels.get_pixel(0, 0).0, [0xff]);
402 assert_eq!(pixels.get_pixel(3, 0).0, [0x00]);
403 assert_eq!(pixels.get_pixel(4, 0).0, [0x40]);
404 }
405
406 #[test]
407 fn a_raster_needs_a_pixel_size() {
408 assert!(Raster::new(0, 1).is_none());
409 assert!(Raster::new(1, 0).is_some());
410 }
411
412 #[test]
413 fn encodes_to_base64() {
414 let image = RawImage::new(1, 1, PixelLayout::Gray8, &[0x7f]).unwrap();
415 let encoded = image.encode_base64(ImageFormat::Png).unwrap();
416
417 assert_eq!(
418 STANDARD.decode(&encoded).unwrap(),
419 image.encode(ImageFormat::Png).unwrap()
420 );
421 }
422}