1#![doc = include_str!("../README.md")]
9
10use std::io::Cursor;
11
12use image::{
13 DynamicImage, ExtendedColorType, GenericImageView, ImageDecoder, ImageEncoder, Limits,
14 codecs::png::{PngDecoder, PngEncoder},
15};
16use letratag_protocol::{HEAD_ROWS, MAX_COLUMNS, Raster};
17
18pub const MAX_PNG_BYTES: usize = 8 * 1024 * 1024;
19pub const MAX_SOURCE_DIMENSION: usize = 8192;
20pub const MAX_SOURCE_PIXELS: usize = 16_000_000;
21pub const MAX_DECODE_BYTES: u64 = 64 * 1024 * 1024;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct RenderOptions {
25 pub width: usize,
26 pub active_rows: usize,
27 pub threshold: u8,
29 pub dither: bool,
31}
32
33impl Default for RenderOptions {
34 fn default() -> Self {
35 Self {
36 width: 400,
37 active_rows: 30,
38 threshold: 128,
39 dither: false,
40 }
41 }
42}
43
44#[derive(Debug, thiserror::Error)]
45pub enum Error {
46 #[error("output width {width} is outside 1..={MAX_COLUMNS}")]
47 InvalidOutputWidth { width: usize },
48 #[error("active row count {active_rows} is outside 1..={HEAD_ROWS}")]
49 InvalidActiveRows { active_rows: usize },
50 #[error("source dimensions {width}x{height} are outside 1..={MAX_SOURCE_DIMENSION}")]
51 InvalidSourceDimensions { width: usize, height: usize },
52 #[error("source has {pixels} pixels, exceeding {MAX_SOURCE_PIXELS}")]
53 TooManyPixels { pixels: usize },
54 #[error("grayscale data has {actual} bytes, expected {expected}")]
55 GrayscaleLength { expected: usize, actual: usize },
56 #[error("PNG has {bytes} bytes, exceeding {MAX_PNG_BYTES}")]
57 FileTooLarge { bytes: usize },
58 #[error("missing or invalid PNG signature or IHDR header")]
59 InvalidPngHeader,
60 #[error("decoded image needs {bytes} bytes, exceeding {MAX_DECODE_BYTES}")]
61 AllocationLimit { bytes: u64 },
62 #[error("image size arithmetic overflow")]
63 ArithmeticOverflow,
64 #[error(transparent)]
65 Image(#[from] image::ImageError),
66 #[error("invalid protocol raster: {0}")]
67 Raster(#[source] letratag_protocol::Error),
68}
69
70pub fn render_png(data: &[u8], options: RenderOptions) -> Result<Raster, Error> {
75 validate_options(options)?;
76 if data.len() > MAX_PNG_BYTES {
77 return Err(Error::FileTooLarge { bytes: data.len() });
78 }
79 let (width, height) = validate_png_header(data)?;
80 let mut limits = Limits::default();
81 limits.max_image_width = Some(MAX_SOURCE_DIMENSION as u32);
82 limits.max_image_height = Some(MAX_SOURCE_DIMENSION as u32);
83 limits.max_alloc = Some(MAX_DECODE_BYTES);
84 let mut decoder = PngDecoder::with_limits(Cursor::new(data), limits.clone())?;
85 let bytes = decoder.total_bytes();
86 if bytes > MAX_DECODE_BYTES {
87 return Err(Error::AllocationLimit { bytes });
88 }
89 limits.reserve(bytes)?;
92 decoder.set_limits(limits)?;
93 let image = DynamicImage::from_decoder(decoder)?;
94 render_samples(width, height, options, |x, y| {
95 let [r, g, b, a] = image.get_pixel(x as u32, y as u32).0;
96 let composite =
97 |c: u8| (u32::from(c) * u32::from(a) + 255 * (255 - u32::from(a)) + 127) / 255;
98 ((2126 * composite(r) + 7152 * composite(g) + 722 * composite(b) + 5000) / 10_000) as u8
99 })
100}
101
102pub fn render_gray(
104 width: usize,
105 height: usize,
106 pixels: &[u8],
107 options: RenderOptions,
108) -> Result<Raster, Error> {
109 validate_options(options)?;
110 let expected = validate_source(width, height)?;
111 if pixels.len() != expected {
112 return Err(Error::GrayscaleLength {
113 expected,
114 actual: pixels.len(),
115 });
116 }
117 render_samples(width, height, options, |x, y| pixels[y * width + x])
118}
119
120pub fn preview_png(raster: &Raster) -> Result<Vec<u8>, Error> {
122 let pixels: Vec<u8> = raster
123 .pixels()
124 .iter()
125 .map(|&pixel| if pixel == 0 { 255 } else { 0 })
126 .collect();
127 let mut output = Vec::new();
128 PngEncoder::new(&mut output).write_image(
129 &pixels,
130 raster.width() as u32,
131 HEAD_ROWS as u32,
132 ExtendedColorType::L8,
133 )?;
134 Ok(output)
135}
136
137fn validate_options(options: RenderOptions) -> Result<(), Error> {
138 if !(1..=MAX_COLUMNS).contains(&options.width) {
139 return Err(Error::InvalidOutputWidth {
140 width: options.width,
141 });
142 }
143 if !(1..=HEAD_ROWS).contains(&options.active_rows) {
144 return Err(Error::InvalidActiveRows {
145 active_rows: options.active_rows,
146 });
147 }
148 Ok(())
149}
150
151fn validate_source(width: usize, height: usize) -> Result<usize, Error> {
152 if !(1..=MAX_SOURCE_DIMENSION).contains(&width) || !(1..=MAX_SOURCE_DIMENSION).contains(&height)
153 {
154 return Err(Error::InvalidSourceDimensions { width, height });
155 }
156 let pixels = width.checked_mul(height).ok_or(Error::ArithmeticOverflow)?;
157 if pixels > MAX_SOURCE_PIXELS {
158 return Err(Error::TooManyPixels { pixels });
159 }
160 Ok(pixels)
161}
162
163fn validate_png_header(data: &[u8]) -> Result<(usize, usize), Error> {
164 if data.len() < 33
165 || &data[..8] != b"\x89PNG\r\n\x1a\n"
166 || data[8..12] != 13_u32.to_be_bytes()
167 || &data[12..16] != b"IHDR"
168 {
169 return Err(Error::InvalidPngHeader);
170 }
171 let width = u32::from_be_bytes(data[16..20].try_into().unwrap()) as usize;
172 let height = u32::from_be_bytes(data[20..24].try_into().unwrap()) as usize;
173 let pixels = validate_source(width, height)?;
174 let channels = match data[25] {
177 0 => 1_u64,
178 2 => 3,
179 3 | 6 => 4,
180 4 => 2,
181 _ => return Err(Error::InvalidPngHeader),
182 };
183 let sample_bytes = match data[24] {
184 1 | 2 | 4 | 8 => 1,
185 16 => 2,
186 _ => return Err(Error::InvalidPngHeader),
187 };
188 let bytes = (pixels as u64)
189 .checked_mul(channels)
190 .and_then(|bytes| bytes.checked_mul(sample_bytes))
191 .ok_or(Error::ArithmeticOverflow)?;
192 if bytes > MAX_DECODE_BYTES {
193 return Err(Error::AllocationLimit { bytes });
194 }
195 Ok((width, height))
196}
197
198fn render_samples(
199 source_width: usize,
200 source_height: usize,
201 options: RenderOptions,
202 sample: impl Fn(usize, usize) -> u8,
203) -> Result<Raster, Error> {
204 let width_fit = source_width
205 .checked_mul(options.active_rows)
206 .ok_or(Error::ArithmeticOverflow)?;
207 let height_fit = source_height
208 .checked_mul(options.width)
209 .ok_or(Error::ArithmeticOverflow)?;
210 let (scaled_width, scaled_height) = if width_fit >= height_fit {
211 (options.width, (height_fit / source_width).max(1))
212 } else {
213 ((width_fit / source_height).max(1), options.active_rows)
214 };
215 let offset_x = (options.width - scaled_width) / 2;
216 let offset_y = (HEAD_ROWS - scaled_height) / 2;
217 let length = options
218 .width
219 .checked_mul(HEAD_ROWS)
220 .ok_or(Error::ArithmeticOverflow)?;
221 let mut pixels = vec![0; length];
222 for y in 0..scaled_height {
225 let source_y = (2 * y + 1) * source_height / (2 * scaled_height);
226 for x in 0..scaled_width {
227 let source_x = (2 * x + 1) * source_width / (2 * scaled_width);
228 let canvas_x = x + offset_x;
229 let canvas_y = y + offset_y;
230 let threshold = if options.dither {
231 const BAYER: [[i16; 4]; 4] =
232 [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
233 (i16::from(options.threshold) + BAYER[canvas_y % 4][canvas_x % 4] * 16 + 8 - 128)
234 .clamp(0, 255) as u8
235 } else {
236 options.threshold
237 };
238 pixels[canvas_y * options.width + canvas_x] =
239 u8::from(sample(source_x, source_y) < threshold);
240 }
241 }
242 Raster::new(options.width, pixels).map_err(Error::Raster)
243}