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