Skip to main content

document_svg/vectorize/
mod.rs

1//! Bounded PNG/JPEG/BMP/TIFF/WebP/Netpbm grayscale raster vectorization.
2
3use 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); // Treat transparent as white
488                } 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            // Fallback: fill with 255
516            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
735/// Converts a 2D grayscale bitmap into clean 2D merged vector rectangles.
736pub 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    // 2D greedy vertical span merging
757    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}