1use std::fs::File;
4use std::io::Read;
5use std::num::NonZeroU64;
6use std::path::Path;
7
8use crate::convert::{ConvertOptions, PageConsumer};
9use crate::error::{Error, Result};
10use crate::ir::{IDENTITY, Node, Page, Paint, SourceMeta, Stroke};
11
12const MAX_RASTER_PIXELS: usize = 20_000_000;
13const MAX_RASTER_DIMENSION: usize = 100_000;
14const MAX_VECTOR_SPANS: usize = 500_000;
15
16pub(crate) fn is_webp(bytes: &[u8]) -> bool {
17 bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WEBP"
18}
19
20pub(crate) fn is_gif(bytes: &[u8]) -> bool {
21 bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
22}
23
24pub(crate) fn is_pnm(bytes: &[u8]) -> bool {
25 matches!(
26 bytes.get(0..2),
27 Some(b"P1" | b"P2" | b"P3" | b"P4" | b"P5" | b"P6" | b"P7")
28 )
29}
30
31pub(crate) fn convert(
32 path: &Path,
33 options: &ConvertOptions,
34 sink: &mut dyn PageConsumer,
35) -> Result<Vec<String>> {
36 let mut bytes = Vec::new();
37 Read::take(File::open(path)?, options.max_input_bytes.saturating_add(1))
38 .read_to_end(&mut bytes)?;
39 if bytes.len() as u64 > options.max_input_bytes {
40 return Err(Error::LimitExceeded(format!(
41 "raster input exceeds maximum bytes ({})",
42 options.max_input_bytes
43 )));
44 }
45
46 let ext = path
47 .extension()
48 .and_then(|s| s.to_str())
49 .map(str::to_ascii_lowercase)
50 .unwrap_or_default();
51
52 let mut warnings = Vec::<String>::new();
53 let (width, height, grayscale_pixels) = if ext == "png"
54 || bytes.starts_with(b"\x89PNG\r\n\x1a\n")
55 {
56 decode_png(&bytes)?
57 } else if matches!(ext.as_str(), "jpg" | "jpeg" | "jpe" | "jfif")
58 || bytes.starts_with(b"\xff\xd8\xff")
59 {
60 let (width, height, pixels, jpeg_warnings) = decode_jpeg(&bytes)?;
61 warnings.extend(jpeg_warnings);
62 (width, height, pixels)
63 } else if matches!(ext.as_str(), "bmp" | "dib") || bytes.starts_with(b"BM") {
64 decode_bmp(&bytes)?
65 } else if ext == "gif" || is_gif(&bytes) {
66 let (width, height, pixels, animated, partial_first_frame) = decode_gif(&bytes)?;
67 if animated {
68 warnings.push("animated GIF was reduced to its first frame for vectorization".into());
69 }
70 if partial_first_frame {
71 warnings
72 .push("partial first GIF frame was composited over a transparent canvas".into());
73 }
74 (width, height, pixels)
75 } else if matches!(ext.as_str(), "tif" | "tiff")
76 || bytes.starts_with(b"II*\x00")
77 || bytes.starts_with(b"MM\x00*")
78 {
79 decode_tiff(&bytes)?
80 } else if ext == "webp" || is_webp(&bytes) {
81 let (width, height, pixels, animated) = decode_webp(&bytes)?;
82 if animated {
83 warnings.push("animated WebP was reduced to its first frame for vectorization".into());
84 }
85 (width, height, pixels)
86 } else if matches!(ext.as_str(), "pbm" | "pgm" | "ppm" | "pnm" | "pam") || is_pnm(&bytes) {
87 let (width, height, pixels, pnm_warnings) = decode_pnm(&bytes)?;
88 warnings.extend(pnm_warnings);
89 (width, height, pixels)
90 } else {
91 return Err(Error::Unsupported(format!(
92 "unsupported raster format: {ext}"
93 )));
94 };
95
96 let mut page = vectorize_grayscale(width, height, &grayscale_pixels, 128)?;
97 for warning in &warnings {
98 page.warn(warning.clone());
99 }
100 sink.consume(page)?;
101 Ok(warnings)
102}
103
104fn decode_pnm(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>, Vec<String>)> {
105 let mut cursor = PnmCursor { bytes, position: 0 };
106 let magic = cursor.token("magic")?;
107 if magic == b"P7" {
108 return decode_pam(bytes);
109 }
110 if magic.len() != 2 || !matches!(magic[0], b'P') || !matches!(magic[1], b'1'..=b'6') {
111 return Err(Error::InvalidInput(
112 "unsupported Netpbm magic number".into(),
113 ));
114 }
115 let format = magic[1];
116 let width = cursor.parse_usize("width")?;
117 let height = cursor.parse_usize("height")?;
118 let pixel_count = validate_dimensions(width, height)?;
119 let maxval = if matches!(format, b'1' | b'4') {
120 1u32
121 } else {
122 let value = cursor.parse_u32("maxval")?;
123 if value == 0 || value > 65_535 {
124 return Err(Error::InvalidInput(
125 "Netpbm maxval must be between 1 and 65535".into(),
126 ));
127 }
128 value
129 };
130 let mut pixels = Vec::with_capacity(pixel_count);
131 match format {
132 b'1' => {
133 for _ in 0..pixel_count {
134 let value = cursor.parse_u32("PBM sample")?;
135 if value > 1 {
136 return Err(Error::InvalidInput("PBM sample must be 0 or 1".into()));
137 }
138 pixels.push(if value == 1 { 0 } else { 255 });
139 }
140 }
141 b'2' => {
142 for _ in 0..pixel_count {
143 pixels.push(scale_pnm(cursor.parse_u32("PGM sample")?, maxval)?);
144 }
145 }
146 b'3' => {
147 for _ in 0..pixel_count {
148 let red = scale_pnm(cursor.parse_u32("PPM red sample")?, maxval)? as u32;
149 let green = scale_pnm(cursor.parse_u32("PPM green sample")?, maxval)? as u32;
150 let blue = scale_pnm(cursor.parse_u32("PPM blue sample")?, maxval)? as u32;
151 pixels.push(((red * 299 + green * 587 + blue * 114) / 1000) as u8);
152 }
153 }
154 b'4' => {
155 cursor.consume_binary_separator()?;
156 let row_bytes = width.div_ceil(8);
157 let payload_len = row_bytes
158 .checked_mul(height)
159 .ok_or_else(|| Error::LimitExceeded("PBM payload size overflowed".into()))?;
160 let payload = cursor.take_bytes(payload_len, "PBM payload")?;
161 for y in 0..height {
162 for x in 0..width {
163 let bit = (payload[y * row_bytes + x / 8] >> (7 - (x % 8))) & 1;
164 pixels.push(if bit == 1 { 0 } else { 255 });
165 }
166 }
167 }
168 b'5' => {
169 cursor.consume_binary_separator()?;
170 let bytes_per_sample = if maxval < 256 { 1 } else { 2 };
171 let payload_len = pixel_count
172 .checked_mul(bytes_per_sample)
173 .ok_or_else(|| Error::LimitExceeded("PGM payload size overflowed".into()))?;
174 let payload = cursor.take_bytes(payload_len, "PGM payload")?;
175 for chunk in payload.chunks_exact(bytes_per_sample) {
176 let sample = if bytes_per_sample == 1 {
177 u32::from(chunk[0])
178 } else {
179 u32::from(u16::from_be_bytes([chunk[0], chunk[1]]))
180 };
181 pixels.push(scale_pnm(sample, maxval)?);
182 }
183 }
184 b'6' => {
185 cursor.consume_binary_separator()?;
186 let bytes_per_sample = if maxval < 256 { 1 } else { 2 };
187 let payload_len = pixel_count
188 .checked_mul(3)
189 .and_then(|value| value.checked_mul(bytes_per_sample))
190 .ok_or_else(|| Error::LimitExceeded("PPM payload size overflowed".into()))?;
191 let payload = cursor.take_bytes(payload_len, "PPM payload")?;
192 let mut offset = 0usize;
193 for _ in 0..pixel_count {
194 let read = |payload: &[u8], offset: &mut usize| -> u32 {
195 if bytes_per_sample == 1 {
196 let value = u32::from(payload[*offset]);
197 *offset += 1;
198 value
199 } else {
200 let value =
201 u32::from(u16::from_be_bytes([payload[*offset], payload[*offset + 1]]));
202 *offset += 2;
203 value
204 }
205 };
206 let red = u32::from(scale_pnm(read(payload, &mut offset), maxval)?);
207 let green = u32::from(scale_pnm(read(payload, &mut offset), maxval)?);
208 let blue = u32::from(scale_pnm(read(payload, &mut offset), maxval)?);
209 pixels.push(((red * 299 + green * 587 + blue * 114) / 1000) as u8);
210 }
211 }
212 _ => unreachable!(),
213 }
214 Ok((width, height, pixels, Vec::new()))
215}
216
217fn decode_pam(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>, Vec<String>)> {
218 let mut cursor = PnmCursor { bytes, position: 2 };
219 cursor.consume_binary_separator()?;
220 let mut width = None;
221 let mut height = None;
222 let mut depth = None;
223 let mut maxval = None;
224 loop {
225 let line = cursor.line("PAM header")?;
226 let line = line.trim();
227 if line.is_empty() || line.starts_with('#') {
228 continue;
229 }
230 if line.eq_ignore_ascii_case("ENDHDR") {
231 break;
232 }
233 let mut fields = line.split_whitespace();
234 let key = fields.next().unwrap_or_default();
235 let value = fields
236 .next()
237 .ok_or_else(|| Error::InvalidInput(format!("PAM {key} header value is missing")))?;
238 if fields.next().is_some() {
239 return Err(Error::InvalidInput(format!(
240 "PAM {key} header contains extra fields"
241 )));
242 }
243 match key.to_ascii_uppercase().as_str() {
244 "WIDTH" => {
245 width = Some(
246 value
247 .parse::<usize>()
248 .map_err(|_| Error::InvalidInput("PAM WIDTH is invalid".into()))?,
249 )
250 }
251 "HEIGHT" => {
252 height = Some(
253 value
254 .parse::<usize>()
255 .map_err(|_| Error::InvalidInput("PAM HEIGHT is invalid".into()))?,
256 )
257 }
258 "DEPTH" => {
259 depth = Some(
260 value
261 .parse::<usize>()
262 .map_err(|_| Error::InvalidInput("PAM DEPTH is invalid".into()))?,
263 )
264 }
265 "MAXVAL" => {
266 maxval = Some(
267 value
268 .parse::<u32>()
269 .map_err(|_| Error::InvalidInput("PAM MAXVAL is invalid".into()))?,
270 )
271 }
272 "TUPLTYPE" => {}
273 _ => {
274 return Err(Error::Unsupported(format!(
275 "PAM header keyword {key:?} is unsupported"
276 )));
277 }
278 }
279 }
280 let width = width.ok_or_else(|| Error::InvalidInput("PAM WIDTH is missing".into()))?;
281 let height = height.ok_or_else(|| Error::InvalidInput("PAM HEIGHT is missing".into()))?;
282 let depth = depth.ok_or_else(|| Error::InvalidInput("PAM DEPTH is missing".into()))?;
283 let maxval = maxval.ok_or_else(|| Error::InvalidInput("PAM MAXVAL is missing".into()))?;
284 if maxval == 0 || maxval > 65_535 || !(1..=4).contains(&depth) {
285 return Err(Error::Unsupported("PAM depth/maxval is unsupported".into()));
286 }
287 let pixel_count = validate_dimensions(width, height)?;
288 let bytes_per_sample = if maxval < 256 { 1 } else { 2 };
289 let payload_len = pixel_count
290 .checked_mul(depth)
291 .and_then(|value| value.checked_mul(bytes_per_sample))
292 .ok_or_else(|| Error::LimitExceeded("PAM payload size overflowed".into()))?;
293 let payload = cursor.take_bytes(payload_len, "PAM payload")?;
294 let mut pixels = Vec::with_capacity(pixel_count);
295 let mut offset = 0usize;
296 let read = |payload: &[u8], offset: &mut usize| -> u32 {
297 if bytes_per_sample == 1 {
298 let value = u32::from(payload[*offset]);
299 *offset += 1;
300 value
301 } else {
302 let value = u32::from(u16::from_be_bytes([payload[*offset], payload[*offset + 1]]));
303 *offset += 2;
304 value
305 }
306 };
307 for _ in 0..pixel_count {
308 let first = scale_pnm(read(payload, &mut offset), maxval)?;
309 match depth {
310 1 => pixels.push(first),
311 2 => {
312 let alpha = scale_pnm(read(payload, &mut offset), maxval)?;
313 pixels.push(if alpha < 128 { 255 } else { first });
314 }
315 3 => {
316 let green = scale_pnm(read(payload, &mut offset), maxval)?;
317 let blue = scale_pnm(read(payload, &mut offset), maxval)?;
318 pixels.push(
319 ((u32::from(first) * 299 + u32::from(green) * 587 + u32::from(blue) * 114)
320 / 1000) as u8,
321 );
322 }
323 4 => {
324 let green = scale_pnm(read(payload, &mut offset), maxval)?;
325 let blue = scale_pnm(read(payload, &mut offset), maxval)?;
326 let alpha = scale_pnm(read(payload, &mut offset), maxval)?;
327 let luminance =
328 ((u32::from(first) * 299 + u32::from(green) * 587 + u32::from(blue) * 114)
329 / 1000) as u8;
330 pixels.push(if alpha < 128 { 255 } else { luminance });
331 }
332 _ => unreachable!(),
333 }
334 }
335 Ok((width, height, pixels, Vec::new()))
336}
337
338fn scale_pnm(value: u32, maxval: u32) -> Result<u8> {
339 if value > maxval {
340 return Err(Error::InvalidInput("Netpbm sample exceeds maxval".into()));
341 }
342 Ok(((value * 255 + maxval / 2) / maxval) as u8)
343}
344
345struct PnmCursor<'a> {
346 bytes: &'a [u8],
347 position: usize,
348}
349
350impl<'a> PnmCursor<'a> {
351 fn token(&mut self, context: &str) -> Result<Vec<u8>> {
352 self.skip_header_space_and_comments();
353 let start = self.position;
354 while self.position < self.bytes.len()
355 && !self.bytes[self.position].is_ascii_whitespace()
356 && self.bytes[self.position] != b'#'
357 {
358 self.position += 1;
359 }
360 if start == self.position {
361 return Err(Error::InvalidInput(format!("Netpbm {context} is missing")));
362 }
363 Ok(self.bytes[start..self.position].to_vec())
364 }
365
366 fn parse_usize(&mut self, context: &str) -> Result<usize> {
367 let token = self.token(context)?;
368 token
369 .iter()
370 .copied()
371 .map(char::from)
372 .collect::<String>()
373 .parse::<usize>()
374 .map_err(|_| Error::InvalidInput(format!("Netpbm {context} is invalid")))
375 }
376
377 fn parse_u32(&mut self, context: &str) -> Result<u32> {
378 let token = self.token(context)?;
379 token
380 .iter()
381 .copied()
382 .map(char::from)
383 .collect::<String>()
384 .parse::<u32>()
385 .map_err(|_| Error::InvalidInput(format!("Netpbm {context} is invalid")))
386 }
387
388 fn skip_header_space_and_comments(&mut self) {
389 loop {
390 while self.position < self.bytes.len()
391 && self.bytes[self.position].is_ascii_whitespace()
392 {
393 self.position += 1;
394 }
395 if self.bytes.get(self.position) != Some(&b'#') {
396 break;
397 }
398 while self.position < self.bytes.len() && self.bytes[self.position] != b'\n' {
399 self.position += 1;
400 }
401 }
402 }
403
404 fn consume_binary_separator(&mut self) -> Result<()> {
405 match self.bytes.get(self.position) {
406 Some(b'\r') if self.bytes.get(self.position + 1) == Some(&b'\n') => self.position += 2,
407 Some(byte) if byte.is_ascii_whitespace() => self.position += 1,
408 _ => {
409 return Err(Error::InvalidInput(
410 "Netpbm binary payload separator is missing".into(),
411 ));
412 }
413 }
414 Ok(())
415 }
416
417 fn take_bytes(&mut self, count: usize, context: &str) -> Result<&'a [u8]> {
418 let end = self
419 .position
420 .checked_add(count)
421 .ok_or_else(|| Error::LimitExceeded(format!("Netpbm {context} size overflowed")))?;
422 let payload = self
423 .bytes
424 .get(self.position..end)
425 .ok_or_else(|| Error::InvalidInput(format!("Netpbm {context} is truncated")))?;
426 self.position = end;
427 Ok(payload)
428 }
429
430 fn line(&mut self, context: &str) -> Result<&'a str> {
431 let start = self.position;
432 let end = self.bytes[start..]
433 .iter()
434 .position(|byte| *byte == b'\n')
435 .map(|offset| start + offset)
436 .unwrap_or(self.bytes.len());
437 self.position = if end < self.bytes.len() { end + 1 } else { end };
438 std::str::from_utf8(self.bytes.get(start..end).unwrap_or_default())
439 .map(|line| line.strip_suffix('\r').unwrap_or(line))
440 .map_err(|error| Error::InvalidInput(format!("Netpbm {context} is not UTF-8: {error}")))
441 }
442}
443
444fn decode_png(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>)> {
445 let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
446 decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
447 let mut reader = decoder
448 .read_info()
449 .map_err(|e| Error::InvalidInput(format!("PNG error: {e}")))?;
450 let (width, height) = reader.info().size();
451 let pixel_count = validate_dimensions(width as usize, height as usize)?;
452 let max_output_bytes = pixel_count
453 .checked_mul(4)
454 .ok_or_else(|| Error::LimitExceeded("PNG output size overflowed".into()))?;
455 let buf_size = reader
456 .output_buffer_size()
457 .ok_or_else(|| Error::LimitExceeded("PNG output buffer size overflowed".into()))?;
458 if buf_size > max_output_bytes {
459 return Err(Error::LimitExceeded(
460 "PNG output buffer exceeds the supported pixel limit".into(),
461 ));
462 }
463 let mut buf = vec![0; buf_size];
464 let info = reader
465 .next_frame(&mut buf)
466 .map_err(|e| Error::InvalidInput(format!("PNG frame error: {e}")))?;
467
468 let width = info.width as usize;
469 let height = info.height as usize;
470 let mut gray = Vec::with_capacity(pixel_count);
471
472 match info.color_type {
473 png::ColorType::Rgb => {
474 let limit = info.buffer_size().min(buf.len());
475 for chunk in buf[..limit].chunks_exact(3) {
476 let lum = ((chunk[0] as u32 * 299 + chunk[1] as u32 * 587 + chunk[2] as u32 * 114)
477 / 1000) as u8;
478 gray.push(lum);
479 }
480 gray.resize(width * height, 255);
481 }
482 png::ColorType::Rgba => {
483 let limit = info.buffer_size().min(buf.len());
484 for chunk in buf[..limit].chunks_exact(4) {
485 let alpha = chunk[3];
486 if alpha < 128 {
487 gray.push(255); } else {
489 let lum =
490 ((chunk[0] as u32 * 299 + chunk[1] as u32 * 587 + chunk[2] as u32 * 114)
491 / 1000) as u8;
492 gray.push(lum);
493 }
494 }
495 gray.resize(width * height, 255);
496 }
497 png::ColorType::Grayscale => {
498 let needed = (width * height).min(buf.len());
499 gray.extend_from_slice(&buf[..needed]);
500 gray.resize(width * height, 255);
501 }
502 png::ColorType::GrayscaleAlpha => {
503 let limit = info.buffer_size().min(buf.len());
504 for chunk in buf[..limit].chunks_exact(2) {
505 let alpha = chunk[1];
506 if alpha < 128 {
507 gray.push(255);
508 } else {
509 gray.push(chunk[0]);
510 }
511 }
512 gray.resize(width * height, 255);
513 }
514 _ => {
515 gray.resize(width * height, 255);
517 }
518 }
519
520 Ok((width, height, gray))
521}
522
523fn decode_jpeg(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>, Vec<String>)> {
524 let mut decoder = jpeg_decoder::Decoder::new(bytes);
525 decoder
526 .read_info()
527 .map_err(|e| Error::InvalidInput(format!("JPEG metadata error: {e}")))?;
528 let metadata = decoder
529 .info()
530 .ok_or_else(|| Error::InvalidInput("JPEG metadata missing".into()))?;
531 let width = metadata.width as usize;
532 let height = metadata.height as usize;
533 let pixel_count = validate_dimensions(width, height)?;
534 let pixel_format = metadata.pixel_format;
535 let pixels = decoder
536 .decode()
537 .map_err(|e| Error::InvalidInput(format!("JPEG error: {e}")))?;
538 let mut gray = Vec::with_capacity(pixel_count);
539 let mut warnings = Vec::new();
540
541 match pixel_format {
542 jpeg_decoder::PixelFormat::RGB24 => {
543 for chunk in pixels.chunks_exact(3) {
544 let lum = ((chunk[0] as u32 * 299 + chunk[1] as u32 * 587 + chunk[2] as u32 * 114)
545 / 1000) as u8;
546 gray.push(lum);
547 }
548 }
549 jpeg_decoder::PixelFormat::L8 => {
550 gray = pixels;
551 }
552 jpeg_decoder::PixelFormat::L16 => {
553 if pixels.len() != pixel_count.saturating_mul(2) {
554 return Err(Error::InvalidInput(
555 "16-bit JPEG output buffer size does not match its dimensions".into(),
556 ));
557 }
558 for sample in pixels.chunks_exact(2) {
559 let value = u16::from_ne_bytes([sample[0], sample[1]]);
560 gray.push((value >> 8) as u8);
561 }
562 warnings.push("16-bit lossless JPEG samples were reduced to 8-bit grayscale".into());
563 }
564 jpeg_decoder::PixelFormat::CMYK32 => {
565 if pixels.len() != pixel_count.saturating_mul(4) {
566 return Err(Error::InvalidInput(
567 "CMYK JPEG output buffer size does not match its dimensions".into(),
568 ));
569 }
570 for sample in pixels.chunks_exact(4) {
571 let cyan = sample[0] as u32;
572 let magenta = sample[1] as u32;
573 let yellow = sample[2] as u32;
574 let black = sample[3] as u32;
575 let red = (255 - cyan) * (255 - black) / 255;
576 let green = (255 - magenta) * (255 - black) / 255;
577 let blue = (255 - yellow) * (255 - black) / 255;
578 gray.push(((red * 299 + green * 587 + blue * 114) / 1000) as u8);
579 }
580 warnings.push(
581 "CMYK JPEG pixels were approximated in RGB; embedded color profiles are not applied".into(),
582 );
583 }
584 }
585
586 Ok((width, height, gray, warnings))
587}
588
589fn decode_webp(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>, bool)> {
590 let mut decoder = image_webp::WebPDecoder::new(std::io::Cursor::new(bytes))
591 .map_err(|error| Error::InvalidInput(format!("WebP decoder error: {error}")))?;
592 let (width, height) = decoder.dimensions();
593 let width = width as usize;
594 let height = height as usize;
595 let pixel_count = validate_dimensions(width, height)?;
596 decoder.set_memory_limit(MAX_RASTER_PIXELS.saturating_mul(4));
597 let animated = decoder.is_animated();
598 let bytes_per_pixel = if decoder.has_alpha() { 4 } else { 3 };
599 let expected_size = pixel_count
600 .checked_mul(bytes_per_pixel)
601 .ok_or_else(|| Error::LimitExceeded("WebP output size overflowed".into()))?;
602 let output_size = decoder
603 .output_buffer_size()
604 .ok_or_else(|| Error::LimitExceeded("WebP output buffer size overflowed".into()))?;
605 if output_size != expected_size {
606 return Err(Error::InvalidInput(
607 "WebP output buffer size does not match its declared dimensions".into(),
608 ));
609 }
610 let mut decoded = vec![0u8; output_size];
611 decoder
612 .read_image(&mut decoded)
613 .map_err(|error| Error::InvalidInput(format!("WebP decode error: {error}")))?;
614 let mut gray = Vec::with_capacity(pixel_count);
615 if bytes_per_pixel == 4 {
616 for chunk in decoded.chunks_exact(4) {
617 if chunk[3] < 128 {
618 gray.push(255);
619 } else {
620 gray.push(luminance(chunk[0], chunk[1], chunk[2]));
621 }
622 }
623 } else {
624 for chunk in decoded.chunks_exact(3) {
625 gray.push(luminance(chunk[0], chunk[1], chunk[2]));
626 }
627 }
628 Ok((width, height, gray, animated))
629}
630
631fn decode_gif(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>, bool, bool)> {
632 let memory_limit = (MAX_RASTER_PIXELS * 4) as u64;
633 let mut options = gif::DecodeOptions::new();
634 options.set_color_output(gif::ColorOutput::RGBA);
635 options.set_memory_limit(gif::MemoryLimit::Bytes(
636 NonZeroU64::new(memory_limit).expect("raster memory limit is nonzero"),
637 ));
638 options.check_frame_consistency(true);
639 let mut decoder = options
640 .read_info(std::io::Cursor::new(bytes))
641 .map_err(|error| Error::InvalidInput(format!("GIF decoder error: {error}")))?;
642 let width = decoder.width() as usize;
643 let height = decoder.height() as usize;
644 let pixel_count = validate_dimensions(width, height)?;
645 let mut rgba = vec![0u8; pixel_count * 4];
646 let (frame_left, frame_top, frame_width, frame_height) = {
647 let frame = decoder
648 .read_next_frame()
649 .map_err(|error| Error::InvalidInput(format!("GIF frame error: {error}")))?
650 .ok_or_else(|| Error::InvalidInput("GIF contains no image frames".into()))?;
651 let frame_left = frame.left as usize;
652 let frame_top = frame.top as usize;
653 let frame_width = frame.width as usize;
654 let frame_height = frame.height as usize;
655 let frame_pixels = validate_dimensions(frame_width, frame_height)?;
656 let expected_len = frame_pixels
657 .checked_mul(4)
658 .ok_or_else(|| Error::LimitExceeded("GIF frame size overflowed".into()))?;
659 if frame.buffer.len() != expected_len {
660 return Err(Error::InvalidInput(
661 "GIF RGBA frame buffer does not match its dimensions".into(),
662 ));
663 }
664 let right = frame_left
665 .checked_add(frame_width)
666 .ok_or_else(|| Error::InvalidInput("GIF frame horizontal offset overflowed".into()))?;
667 let bottom = frame_top
668 .checked_add(frame_height)
669 .ok_or_else(|| Error::InvalidInput("GIF frame vertical offset overflowed".into()))?;
670 if right > width || bottom > height {
671 return Err(Error::InvalidInput(
672 "GIF frame extends beyond its logical screen".into(),
673 ));
674 }
675 for row in 0..frame_height {
676 let source_start = row * frame_width * 4;
677 let destination_start = ((frame_top + row) * width + frame_left) * 4;
678 let bytes = frame_width * 4;
679 rgba[destination_start..destination_start + bytes]
680 .copy_from_slice(&frame.buffer[source_start..source_start + bytes]);
681 }
682 (frame_left, frame_top, frame_width, frame_height)
683 };
684 let animated = decoder
685 .next_frame_info()
686 .map_err(|error| Error::InvalidInput(format!("GIF next-frame metadata error: {error}")))?
687 .is_some();
688 let partial_first_frame =
689 frame_left != 0 || frame_top != 0 || frame_width != width || frame_height != height;
690 let mut gray = Vec::with_capacity(pixel_count);
691 for pixel in rgba.chunks_exact(4) {
692 if pixel[3] < 128 {
693 gray.push(255);
694 } else {
695 gray.push(luminance(pixel[0], pixel[1], pixel[2]));
696 }
697 }
698 Ok((width, height, gray, animated, partial_first_frame))
699}
700
701fn luminance(red: u8, green: u8, blue: u8) -> u8 {
702 ((red as u32 * 299 + green as u32 * 587 + blue as u32 * 114) / 1000) as u8
703}
704
705fn validate_dimensions(width: usize, height: usize) -> Result<usize> {
706 if width == 0 || height == 0 {
707 return Err(Error::InvalidInput(
708 "raster image dimensions must be greater than zero".into(),
709 ));
710 }
711 if width > MAX_RASTER_DIMENSION || height > MAX_RASTER_DIMENSION {
712 return Err(Error::LimitExceeded(format!(
713 "raster image dimensions exceed {MAX_RASTER_DIMENSION} pixels per side"
714 )));
715 }
716 let pixels = width
717 .checked_mul(height)
718 .ok_or_else(|| Error::LimitExceeded("raster pixel count overflowed".into()))?;
719 if pixels > MAX_RASTER_PIXELS {
720 return Err(Error::LimitExceeded(format!(
721 "raster image has {pixels} pixels; maximum is {MAX_RASTER_PIXELS}"
722 )));
723 }
724 Ok(pixels)
725}
726
727#[derive(Clone, Copy, Debug)]
728struct ActiveSpan {
729 x: usize,
730 width: usize,
731 y: usize,
732 height: usize,
733}
734
735pub fn vectorize_grayscale(
737 width: usize,
738 height: usize,
739 pixels: &[u8],
740 threshold: u8,
741) -> Result<Page> {
742 let pixel_count = validate_dimensions(width, height)?;
743 if pixels.len() < pixel_count {
744 return Err(Error::InvalidInput(format!(
745 "raster pixel buffer has {} bytes; expected at least {pixel_count}",
746 pixels.len()
747 )));
748 }
749 let mut path_d = String::new();
750 let mut active_spans: Vec<ActiveSpan> = Vec::new();
751 let mut row_spans: Vec<(usize, usize)> = Vec::new();
752 let mut next_active: Vec<ActiveSpan> = Vec::new();
753 let mut matched_row_indices: Vec<bool> = Vec::new();
754 let mut path_count = 0usize;
755
756 for y in 0..height {
758 row_spans.clear();
759 let mut in_run = false;
760 let mut run_start = 0;
761
762 for x in 0..width {
763 let is_dark = pixels.get(y * width + x).copied().unwrap_or(255) < threshold;
764 if is_dark && !in_run {
765 in_run = true;
766 run_start = x;
767 } else if !is_dark && in_run {
768 in_run = false;
769 if row_spans.len() >= MAX_VECTOR_SPANS {
770 return Err(Error::LimitExceeded(format!(
771 "raster row exceeds {MAX_VECTOR_SPANS} vector spans"
772 )));
773 }
774 row_spans.push((run_start, x - run_start));
775 }
776 }
777 if in_run {
778 if row_spans.len() >= MAX_VECTOR_SPANS {
779 return Err(Error::LimitExceeded(format!(
780 "raster row exceeds {MAX_VECTOR_SPANS} vector spans"
781 )));
782 }
783 row_spans.push((run_start, width - run_start));
784 }
785
786 next_active.clear();
787 matched_row_indices.clear();
788 matched_row_indices.resize(row_spans.len(), false);
789
790 for mut span in active_spans.drain(..) {
791 if let Ok(idx) = row_spans.binary_search(&(span.x, span.width))
792 && !matched_row_indices[idx]
793 {
794 span.height += 1;
795 next_active.push(span);
796 matched_row_indices[idx] = true;
797 } else {
798 path_count = path_count.saturating_add(1);
799 if path_count > MAX_VECTOR_SPANS {
800 return Err(Error::LimitExceeded(format!(
801 "raster vectorization exceeds {MAX_VECTOR_SPANS} path spans"
802 )));
803 }
804 path_d.push_str(&format!(
805 "M {},{} h {} v {} h -{} Z ",
806 span.x, span.y, span.width, span.height, span.width
807 ));
808 }
809 }
810
811 for (idx, &(rx, rw)) in row_spans.iter().enumerate() {
812 if !matched_row_indices[idx] {
813 next_active.push(ActiveSpan {
814 x: rx,
815 width: rw,
816 y,
817 height: 1,
818 });
819 }
820 }
821
822 std::mem::swap(&mut active_spans, &mut next_active);
823 }
824
825 for span in active_spans {
826 path_count = path_count.saturating_add(1);
827 if path_count > MAX_VECTOR_SPANS {
828 return Err(Error::LimitExceeded(format!(
829 "raster vectorization exceeds {MAX_VECTOR_SPANS} path spans"
830 )));
831 }
832 path_d.push_str(&format!(
833 "M {},{} h {} v {} h -{} Z ",
834 span.x, span.y, span.width, span.height, span.width
835 ));
836 }
837
838 let mut page = Page::new(1, width as f64, height as f64, "vectorized");
839 page.nodes.push(Node::Path {
840 id: "vectorized_path".to_string(),
841 d: path_d,
842 fill_rule: "evenodd".to_string(),
843 fill: Paint::solid("#000000"),
844 stroke: Stroke::default(),
845 transform: IDENTITY,
846 clip_id: None,
847 meta: SourceMeta::default(),
848 });
849
850 Ok(page)
851}
852
853fn decode_bmp(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>)> {
854 if bytes.len() < 54 || &bytes[0..2] != b"BM" {
855 return Err(Error::InvalidInput(
856 "invalid BMP signature or header".into(),
857 ));
858 }
859 let pixel_offset = u32::from_le_bytes(
860 bytes[10..14]
861 .try_into()
862 .map_err(|_| Error::InvalidInput("BMP header too short".into()))?,
863 ) as usize;
864 let width = i32::from_le_bytes(
865 bytes[18..22]
866 .try_into()
867 .map_err(|_| Error::InvalidInput("BMP header too short".into()))?,
868 );
869 let raw_height = i32::from_le_bytes(
870 bytes[22..26]
871 .try_into()
872 .map_err(|_| Error::InvalidInput("BMP header too short".into()))?,
873 );
874 let bpp = u16::from_le_bytes(
875 bytes[28..30]
876 .try_into()
877 .map_err(|_| Error::InvalidInput("BMP header too short".into()))?,
878 );
879 let compression = u32::from_le_bytes(
880 bytes[30..34]
881 .try_into()
882 .map_err(|_| Error::InvalidInput("BMP header too short".into()))?,
883 );
884
885 if width <= 0 || raw_height == 0 {
886 return Err(Error::InvalidInput("invalid BMP dimensions".into()));
887 }
888 if compression != 0 {
889 return Err(Error::Unsupported("compressed BMP not supported".into()));
890 }
891
892 let w = width as usize;
893 let h = raw_height.unsigned_abs() as usize;
894 let pixel_count = validate_dimensions(w, h)?;
895 let top_down = raw_height < 0;
896
897 let mut palette = Vec::new();
898 if bpp <= 8 {
899 let bi_size = u32::from_le_bytes(
900 bytes[14..18]
901 .try_into()
902 .map_err(|_| Error::InvalidInput("BMP header too short".into()))?,
903 ) as usize;
904 let clr_used = if bytes.len() >= 50 {
905 u32::from_le_bytes(bytes[46..50].try_into().unwrap_or([0, 0, 0, 0])) as usize
906 } else {
907 0
908 };
909 let max_entries = 1usize << (bpp as usize);
910 let num_colors = if clr_used > 0 && clr_used <= max_entries {
911 clr_used
912 } else {
913 max_entries
914 };
915 let palette_offset = 14 + bi_size;
916 for i in 0..num_colors {
917 let entry_offset = palette_offset + i * 4;
918 if entry_offset + 3 < bytes.len() && entry_offset + 3 < pixel_offset {
919 let b = bytes[entry_offset] as u32;
920 let g = bytes[entry_offset + 1] as u32;
921 let r = bytes[entry_offset + 2] as u32;
922 let lum = ((r * 299 + g * 587 + b * 114) / 1000) as u8;
923 palette.push(lum);
924 } else if bpp == 1 {
925 palette.push(if i == 0 { 0 } else { 255 });
926 } else {
927 palette.push((i * 255 / (max_entries - 1).max(1)) as u8);
928 }
929 }
930 }
931
932 let row_stride = match bpp {
933 32 => w * 4,
934 24 => (w * 3 + 3) & !3,
935 8 => (w + 3) & !3,
936 4 => (w.div_ceil(2) + 3) & !3,
937 1 => (w.div_ceil(8) + 3) & !3,
938 _ => return Err(Error::Unsupported(format!("unsupported BMP bpp: {bpp}"))),
939 };
940
941 if bytes.len() < pixel_offset.saturating_add(row_stride.saturating_mul(h)) {
942 return Err(Error::InvalidInput("BMP file truncated".into()));
943 }
944
945 let mut gray = vec![0u8; pixel_count];
946 for row in 0..h {
947 let src_row = if top_down { row } else { h - 1 - row };
948 let row_start = pixel_offset + src_row * row_stride;
949 let dst_start = row * w;
950 match bpp {
951 32 => {
952 for col in 0..w {
953 let b = bytes[row_start + col * 4];
954 let g = bytes[row_start + col * 4 + 1];
955 let r = bytes[row_start + col * 4 + 2];
956 let a = bytes[row_start + col * 4 + 3];
957 if a < 128 {
958 gray[dst_start + col] = 255;
959 } else {
960 let lum = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
961 gray[dst_start + col] = lum;
962 }
963 }
964 }
965 24 => {
966 for col in 0..w {
967 let b = bytes[row_start + col * 3];
968 let g = bytes[row_start + col * 3 + 1];
969 let r = bytes[row_start + col * 3 + 2];
970 let lum = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
971 gray[dst_start + col] = lum;
972 }
973 }
974 8 => {
975 for col in 0..w {
976 let idx = bytes[row_start + col] as usize;
977 gray[dst_start + col] = palette.get(idx).copied().unwrap_or(idx as u8);
978 }
979 }
980 4 => {
981 for col in 0..w {
982 let byte_idx = row_start + col / 2;
983 let nibble = if col % 2 == 0 {
984 (bytes[byte_idx] >> 4) & 0x0F
985 } else {
986 bytes[byte_idx] & 0x0F
987 } as usize;
988 gray[dst_start + col] =
989 palette.get(nibble).copied().unwrap_or((nibble * 17) as u8);
990 }
991 }
992 1 => {
993 for col in 0..w {
994 let byte_idx = row_start + col / 8;
995 let bit = ((bytes[byte_idx] >> (7 - (col % 8))) & 1) as usize;
996 gray[dst_start + col] =
997 palette
998 .get(bit)
999 .copied()
1000 .unwrap_or(if bit == 0 { 0 } else { 255 });
1001 }
1002 }
1003 _ => unreachable!(),
1004 }
1005 }
1006
1007 Ok((w, h, gray))
1008}
1009
1010pub fn decode_tiff(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>)> {
1011 let mut limits = tiff::decoder::Limits::default();
1012 limits.decoding_buffer_size = 96 * 1024 * 1024;
1013 limits.intermediate_buffer_size = 96 * 1024 * 1024;
1014 let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(bytes))
1015 .map_err(|e| Error::InvalidInput(format!("TIFF decoder error: {e}")))?
1016 .with_limits(limits);
1017 let (width, height) = decoder
1018 .dimensions()
1019 .map_err(|e| Error::InvalidInput(format!("TIFF dimensions error: {e}")))?;
1020 let w = width as usize;
1021 let h = height as usize;
1022 let pixel_count = validate_dimensions(w, h)?;
1023 let result = decoder
1024 .read_image()
1025 .map_err(|e| Error::InvalidInput(format!("TIFF read_image error: {e}")))?;
1026 let mut gray = Vec::with_capacity(pixel_count);
1027
1028 match result {
1029 tiff::decoder::DecodingResult::U8(buf) => {
1030 let colortype = decoder
1031 .colortype()
1032 .map_err(|e| Error::InvalidInput(format!("TIFF colortype error: {e}")))?;
1033 match colortype {
1034 tiff::ColorType::Gray(8) => {
1035 let needed = (w * h).min(buf.len());
1036 gray.extend_from_slice(&buf[..needed]);
1037 gray.resize(w * h, 255);
1038 }
1039 tiff::ColorType::RGB(8) => {
1040 for chunk in buf.chunks_exact(3) {
1041 let lum = ((chunk[0] as u32 * 299
1042 + chunk[1] as u32 * 587
1043 + chunk[2] as u32 * 114)
1044 / 1000) as u8;
1045 gray.push(lum);
1046 }
1047 gray.resize(w * h, 255);
1048 }
1049 tiff::ColorType::RGBA(8) => {
1050 for chunk in buf.chunks_exact(4) {
1051 if chunk[3] < 128 {
1052 gray.push(255);
1053 } else {
1054 let lum = ((chunk[0] as u32 * 299
1055 + chunk[1] as u32 * 587
1056 + chunk[2] as u32 * 114)
1057 / 1000) as u8;
1058 gray.push(lum);
1059 }
1060 }
1061 gray.resize(w * h, 255);
1062 }
1063 _ => {
1064 let needed = (w * h).min(buf.len());
1065 gray.extend_from_slice(&buf[..needed]);
1066 gray.resize(w * h, 255);
1067 }
1068 }
1069 }
1070 _ => {
1071 gray.resize(w * h, 255);
1072 }
1073 }
1074 Ok((w, h, gray))
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::vectorize_grayscale;
1080 use crate::error::Error;
1081
1082 #[test]
1083 fn rejects_oversized_raster_dimensions_before_reading_pixels() {
1084 let error = vectorize_grayscale(5_000, 5_000, &[], 128).unwrap_err();
1085 assert!(matches!(error, Error::LimitExceeded(_)));
1086 }
1087
1088 #[test]
1089 fn rejects_an_incomplete_pixel_buffer() {
1090 let error = vectorize_grayscale(2, 2, &[0, 0, 0], 128).unwrap_err();
1091 assert!(matches!(error, Error::InvalidInput(_)));
1092 }
1093
1094 #[test]
1095 fn rejects_raster_images_with_pathological_vector_complexity() {
1096 let width = 1_001usize;
1097 let height = 1_000usize;
1098 let pixels = (0..width * height)
1099 .map(|index| {
1100 let x = index % width;
1101 let y = index / width;
1102 if (x + y).is_multiple_of(2) { 0 } else { 255 }
1103 })
1104 .collect::<Vec<_>>();
1105
1106 let error = vectorize_grayscale(width, height, &pixels, 128).unwrap_err();
1107
1108 assert!(matches!(error, Error::LimitExceeded(_)));
1109 }
1110}