Skip to main content

brushkit_preview/
bitmap.rs

1use brushkit_abr::TipBitmap;
2use image::{ColorType, ImageBuffer, ImageDecoder, Rgba, RgbaImage};
3use std::io::Cursor;
4use zune_core::bytestream::{ZByteIoError, ZByteReaderTrait, ZCursor, ZSeekFrom};
5use zune_core::colorspace::ColorSpace;
6use zune_core::options::DecoderOptions;
7use zune_jpeg::ImageInfo;
8
9#[derive(Debug, Clone)]
10pub struct GrayscaleBitmap {
11    pub width: u32,
12    pub height: u32,
13    pub data: Vec<u8>,
14}
15
16pub fn to_grayscale(tip: &TipBitmap) -> GrayscaleBitmap {
17    match tip.depth {
18        8 => GrayscaleBitmap {
19            width: tip.width,
20            height: tip.height,
21            data: tip.data.clone(),
22        },
23        16 => {
24            let data: Vec<u8> = tip
25                .data
26                .as_chunks::<2>()
27                .0
28                .iter()
29                .map(|pair| pair[0])
30                .collect();
31            GrayscaleBitmap {
32                width: tip.width,
33                height: tip.height,
34                data,
35            }
36        }
37        _ => GrayscaleBitmap {
38            width: tip.width,
39            height: tip.height,
40            data: tip.data.clone(),
41        },
42    }
43}
44
45/// Both formats use the same polarity — a `Shape.png` is white=stamp coverage
46/// and a `TipBitmap` is 255=full ink — so the pixels are copied unchanged.
47pub fn tip_bitmap_of(bitmap: &GrayscaleBitmap) -> TipBitmap {
48    TipBitmap {
49        width: bitmap.width,
50        height: bitmap.height,
51        depth: 8,
52        data: bitmap.data.clone(),
53    }
54}
55
56pub const MAX_IMPORT_DIMENSION: u32 = 16384;
57/// The decode budget of [`decode_tip_image`]. [`TipImageError::TooLarge`] lists
58/// what it reserves.
59pub const MAX_IMPORT_DECODED_BYTES: u64 = 512 * 1024 * 1024;
60
61#[derive(Debug)]
62pub enum TipImageError {
63    /// The bytes did not decode, or a PNG carries an eXIf chunk over 64 KiB
64    /// before the image data.
65    Decode(String),
66    /// A side over [`MAX_IMPORT_DIMENSION`], or a decode over
67    /// [`MAX_IMPORT_DECODED_BYTES`]: the image in its own pixel format, plus
68    /// the DCT coefficients of a progressive JPEG or of a baseline JPEG whose
69    /// first scan leaves out a component. The decoder's other buffers are not
70    /// counted: up to a few hundred KiB at any width, more for wide images,
71    /// whose row buffers grow with the width. Neither is a PNG's eXIf chunk of
72    /// at most 64 KiB, which the decoder holds twice, up to 128 KiB. A JPEG's
73    /// metadata segments cost nothing: zune-jpeg skips them without a copy.
74    TooLarge { width: u32, height: u32 },
75}
76
77impl std::fmt::Display for TipImageError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            TipImageError::Decode(msg) => f.write_str(msg),
81            TipImageError::TooLarge { width, height } => write!(
82                f,
83                "image is {width}x{height}px; a brush tip must be at most {MAX_IMPORT_DIMENSION}px per side and {} MiB to decode",
84                MAX_IMPORT_DECODED_BYTES / (1024 * 1024)
85            ),
86        }
87    }
88}
89
90impl std::error::Error for TipImageError {}
91
92/// Decode an imported brush-tip image (PNG/JPEG bytes) into a grayscale tip
93/// buffer. Alpha becomes intensity when the image has an alpha channel;
94/// otherwise luminance is inverted (dark = opaque), matching Photoshop's
95/// convention.
96pub fn decode_tip_image(bytes: &[u8]) -> Result<GrayscaleBitmap, TipImageError> {
97    let img = decode_guarded(bytes, MAX_IMPORT_DIMENSION, MAX_IMPORT_DECODED_BYTES).map_err(
98        |e| match e {
99            GuardedDecodeError::TooLarge { width, height } => {
100                TipImageError::TooLarge { width, height }
101            }
102            GuardedDecodeError::Decode(e) => TipImageError::Decode(e.to_string()),
103        },
104    )?;
105    Ok(GrayscaleBitmap {
106        width: img.width,
107        height: img.height,
108        data: tip_plane(img, TipSample::Coverage),
109    })
110}
111
112/// A decoded image in its own pixel format. 16-bit and float samples are in
113/// native byte order, as `ImageDecoder::read_image` writes them.
114pub(crate) struct DecodedImage {
115    pub width: u32,
116    pub height: u32,
117    pub format: PixelFormat,
118    pub bytes: Vec<u8>,
119}
120
121/// Every pixel format `image` 0.25 decodes to. The float formats come from
122/// decoders a dependent crate may enable on the shared `image` dependency.
123#[derive(Clone, Copy)]
124pub(crate) enum PixelFormat {
125    L8,
126    La8,
127    Rgb8,
128    Rgba8,
129    L16,
130    La16,
131    Rgb16,
132    Rgba16,
133    Rgb32F,
134    Rgba32F,
135}
136
137impl PixelFormat {
138    fn of(color: ColorType) -> Option<Self> {
139        Some(match color {
140            ColorType::L8 => Self::L8,
141            ColorType::La8 => Self::La8,
142            ColorType::Rgb8 => Self::Rgb8,
143            ColorType::Rgba8 => Self::Rgba8,
144            ColorType::L16 => Self::L16,
145            ColorType::La16 => Self::La16,
146            ColorType::Rgb16 => Self::Rgb16,
147            ColorType::Rgba16 => Self::Rgba16,
148            ColorType::Rgb32F => Self::Rgb32F,
149            ColorType::Rgba32F => Self::Rgba32F,
150            _ => return None,
151        })
152    }
153
154    fn color_type(self) -> ColorType {
155        match self {
156            Self::L8 => ColorType::L8,
157            Self::La8 => ColorType::La8,
158            Self::Rgb8 => ColorType::Rgb8,
159            Self::Rgba8 => ColorType::Rgba8,
160            Self::L16 => ColorType::L16,
161            Self::La16 => ColorType::La16,
162            Self::Rgb16 => ColorType::Rgb16,
163            Self::Rgba16 => ColorType::Rgba16,
164            Self::Rgb32F => ColorType::Rgb32F,
165            Self::Rgba32F => ColorType::Rgba32F,
166        }
167    }
168
169    fn bytes_per_pixel(self) -> usize {
170        usize::from(self.color_type().bytes_per_pixel())
171    }
172}
173
174/// What a tip keeps of each pixel.
175#[derive(Clone, Copy)]
176pub(crate) enum TipSample {
177    /// Alpha when the image has it, otherwise inverted luma, so dark is opaque.
178    Coverage,
179    Luma,
180}
181
182/// One byte per pixel, written over the front of the decoded buffer so the tip
183/// allocates no second buffer. Values are `image`'s own `into_luma8` and
184/// `into_luma_alpha8` conversions. The final shrink is a `realloc`, measured in
185/// place from 16 to 512 MiB on glibc and both wasm32 targets, and on macOS
186/// except a 16 MiB block shrunk to 2 MiB. An allocator that moves the block
187/// adds the tip's size to the peak.
188pub(crate) fn tip_plane(image: DecodedImage, sample: TipSample) -> Vec<u8> {
189    const CHUNK: usize = 4096;
190    let format = image.format;
191    let bytes_per_pixel = format.bytes_per_pixel();
192    let coverage = matches!(sample, TipSample::Coverage);
193    let alpha = coverage && format.color_type().has_alpha();
194    let mut raw = image.bytes;
195    let pixels = raw.len() / bytes_per_pixel;
196    if alpha && matches!(format, PixelFormat::La8 | PixelFormat::Rgba8) {
197        for i in 0..pixels {
198            raw[i] = raw[i * bytes_per_pixel + bytes_per_pixel - 1];
199        }
200    } else if bytes_per_pixel > 1 {
201        // Chunk `start..end` is written to bytes `start..end`, which precede
202        // its own source bytes, so no unread pixel is overwritten.
203        for start in (0..pixels).step_by(CHUNK) {
204            let end = (start + CHUNK).min(pixels);
205            let chunk = &raw[start * bytes_per_pixel..end * bytes_per_pixel];
206            let plane = convert_chunk(format, alpha, chunk);
207            raw[start..end].copy_from_slice(&plane);
208        }
209    }
210    raw.truncate(pixels);
211    raw.shrink_to_fit();
212    if coverage && !alpha {
213        raw.iter_mut().for_each(|v| *v = 255 - *v);
214    }
215    raw
216}
217
218/// One byte per pixel of `chunk`, alpha when `alpha` and luma otherwise,
219/// through a one-row image so `image` does the conversion.
220fn convert_chunk(format: PixelFormat, alpha: bool, chunk: &[u8]) -> Vec<u8> {
221    use image::DynamicImage::*;
222    let width = (chunk.len() / format.bytes_per_pixel()) as u32;
223    let u8s = || chunk.to_vec();
224    let u16s = || {
225        let samples = chunk.as_chunks::<2>().0.iter();
226        samples.map(|s| u16::from_ne_bytes(*s)).collect()
227    };
228    let f32s = || {
229        let samples = chunk.as_chunks::<4>().0.iter();
230        samples.map(|s| f32::from_ne_bytes(*s)).collect()
231    };
232    let pixels = match format {
233        PixelFormat::L8 => ImageBuffer::from_raw(width, 1, u8s()).map(ImageLuma8),
234        PixelFormat::La8 => ImageBuffer::from_raw(width, 1, u8s()).map(ImageLumaA8),
235        PixelFormat::Rgb8 => ImageBuffer::from_raw(width, 1, u8s()).map(ImageRgb8),
236        PixelFormat::Rgba8 => ImageBuffer::from_raw(width, 1, u8s()).map(ImageRgba8),
237        PixelFormat::L16 => ImageBuffer::from_raw(width, 1, u16s()).map(ImageLuma16),
238        PixelFormat::La16 => ImageBuffer::from_raw(width, 1, u16s()).map(ImageLumaA16),
239        PixelFormat::Rgb16 => ImageBuffer::from_raw(width, 1, u16s()).map(ImageRgb16),
240        PixelFormat::Rgba16 => ImageBuffer::from_raw(width, 1, u16s()).map(ImageRgba16),
241        PixelFormat::Rgb32F => ImageBuffer::from_raw(width, 1, f32s()).map(ImageRgb32F),
242        PixelFormat::Rgba32F => ImageBuffer::from_raw(width, 1, f32s()).map(ImageRgba32F),
243    }
244    .expect("a chunk holds whole pixels");
245    if alpha {
246        let luma_alpha = pixels.into_luma_alpha8().into_raw();
247        luma_alpha.into_iter().skip(1).step_by(2).collect()
248    } else {
249        pixels.into_luma8().into_raw()
250    }
251}
252
253/// Why `decode_guarded` returned no image.
254pub(crate) enum GuardedDecodeError {
255    /// A side over `max_side`, or a decode over `max_bytes`.
256    TooLarge {
257        width: u32,
258        height: u32,
259    },
260    Decode(image::ImageError),
261}
262
263/// Decode an image of at most `max_side` px per side and `max_bytes` decoded:
264/// the image in its own pixel format, plus the DCT coefficients zune-jpeg holds
265/// for the whole image, counted by `coefficient_bytes`. The decoder's other
266/// buffers are not counted, as [`TipImageError::TooLarge`] describes. Oversize
267/// is decided from the header before any pixels are decoded. A JPEG is decoded
268/// from `bytes`, and its metadata segments are skipped. A PNG's iCCP
269/// profile and text chunks are skipped, and one with an eXIf chunk over
270/// [`MAX_PNG_EXIF_BYTES`] before the image data does not decode.
271pub(crate) fn decode_guarded(
272    bytes: &[u8],
273    max_side: u32,
274    max_bytes: u64,
275) -> Result<DecodedImage, GuardedDecodeError> {
276    match image::guess_format(bytes).ok() {
277        Some(image::ImageFormat::Jpeg) => return decode_jpeg(bytes, max_side, max_bytes),
278        Some(image::ImageFormat::Png) => return decode_png(bytes, max_side, max_bytes),
279        _ => {}
280    }
281    if let Some((width, height)) = header_dimensions(bytes) {
282        if width > max_side || height > max_side {
283            return Err(GuardedDecodeError::TooLarge { width, height });
284        }
285    }
286    let mut reader = image::ImageReader::new(Cursor::new(bytes))
287        .with_guessed_format()
288        .map_err(|e| GuardedDecodeError::Decode(image::ImageError::IoError(e)))?;
289    let mut limits = image::Limits::default();
290    limits.max_image_width = Some(max_side);
291    limits.max_image_height = Some(max_side);
292    limits.max_alloc = Some(max_bytes);
293    reader.limits(limits.clone());
294    let mut decoder = reader.into_decoder().map_err(GuardedDecodeError::Decode)?;
295    // What `ImageReader::decode` does, with the budget failure reported as
296    // `TooLarge`: the output buffer is reserved and the decoder keeps the rest.
297    if limits.reserve(decoder.total_bytes()).is_err() {
298        let (width, height) = decoder.dimensions();
299        return Err(GuardedDecodeError::TooLarge { width, height });
300    }
301    decoder
302        .set_limits(limits)
303        .map_err(GuardedDecodeError::Decode)?;
304    let (width, height) = decoder.dimensions();
305    let color = decoder.color_type();
306    let format = PixelFormat::of(color)
307        .ok_or_else(|| unsupported_color(image::error::ImageFormatHint::Unknown, color.into()))?;
308    // One buffer in the decoder's own format, so a converting tip reuses it.
309    let total_bytes = usize::try_from(decoder.total_bytes())
310        .map_err(|_| GuardedDecodeError::TooLarge { width, height })?;
311    let mut bytes = vec![0; total_bytes];
312    decoder
313        .read_image(&mut bytes)
314        .map_err(GuardedDecodeError::Decode)?;
315    Ok(DecodedImage {
316        width,
317        height,
318        format,
319        bytes,
320    })
321}
322
323fn unsupported_color(
324    format: image::error::ImageFormatHint,
325    color: image::ExtendedColorType,
326) -> GuardedDecodeError {
327    GuardedDecodeError::Decode(image::ImageError::Unsupported(
328        image::error::UnsupportedError::from_format_and_kind(
329            format,
330            image::error::UnsupportedErrorKind::Color(color),
331        ),
332    ))
333}
334
335/// The largest eXIf chunk a PNG may carry for its tip to decode: what a JPEG's
336/// APP1 segment holds.
337const MAX_PNG_EXIF_BYTES: usize = 64 * 1024;
338
339/// The length of the first eXIf chunk before the image data that is over
340/// [`MAX_PNG_EXIF_BYTES`]. `png` buffers a chunk's declared length.
341fn oversize_exif(bytes: &[u8]) -> Option<u32> {
342    // Chunks follow the 8-byte signature.
343    let mut at = 8usize;
344    while let Some(&[l0, l1, l2, l3, ref kind @ ..]) =
345        bytes.get(at..).and_then(|rest| rest.get(..8))
346    {
347        let len = u32::from_be_bytes([l0, l1, l2, l3]);
348        match kind {
349            b"IDAT" => return None,
350            b"eXIf" if len as usize > MAX_PNG_EXIF_BYTES => return Some(len),
351            _ => {}
352        }
353        // Length, type, data and CRC.
354        at = at.saturating_add(12).saturating_add(len as usize);
355    }
356    None
357}
358
359/// `decode_guarded` for a PNG. Through `image`'s PNG decoder, `png` inflates
360/// an iCCP profile up to the whole budget before the output buffer is
361/// reserved, and keeps it while the image decodes. Text chunks are kept the
362/// same way. The tip reads neither, so `png` decodes here with both skipped.
363/// `png` 0.18.1 keeps an eXIf chunk whatever its options, in its chunk buffer
364/// and in a copy, so a PNG with one over [`MAX_PNG_EXIF_BYTES`] fails before
365/// `png` reads it. The transformation, output formats and errors are those of
366/// `image` 0.25.
367fn decode_png(
368    bytes: &[u8],
369    max_side: u32,
370    max_bytes: u64,
371) -> Result<DecodedImage, GuardedDecodeError> {
372    use png::{BitDepth, ColorType as Png};
373    let limits = png::Limits {
374        bytes: usize::try_from(max_bytes).unwrap_or(usize::MAX),
375    };
376    let mut decoder = png::Decoder::new_with_limits(Cursor::new(bytes), limits);
377    decoder.set_ignore_iccp_chunk(true);
378    decoder.set_ignore_text_chunk(true);
379    decoder.set_transformations(png::Transformations::EXPAND);
380    let info = decoder.read_header_info().map_err(png_error)?;
381    let (width, height) = (info.width, info.height);
382    if width > max_side || height > max_side {
383        return Err(GuardedDecodeError::TooLarge { width, height });
384    }
385    // On 32-bit targets `png` rejects an output buffer over `isize::MAX` in
386    // `read_info`, so an over-budget PNG is sized from IHDR first. IHDR
387    // undercounts indexed color, which EXPAND turns into RGB or RGBA.
388    let min_decoded = u64::from(width) * u64::from(height) * info.bytes_per_pixel() as u64;
389    if min_decoded > max_bytes {
390        return Err(GuardedDecodeError::TooLarge { width, height });
391    }
392    if let Some(len) = oversize_exif(bytes) {
393        return Err(GuardedDecodeError::Decode(image::ImageError::Decoding(
394            image::error::DecodingError::new(
395                image::ImageFormat::Png.into(),
396                format!("eXIf chunk is {len} bytes, over the {MAX_PNG_EXIF_BYTES} a tip reads"),
397            ),
398        )));
399    }
400    let mut reader = decoder.read_info().map_err(png_error)?;
401    let (color, depth) = reader.output_color_type();
402    let format = match (color, depth) {
403        (Png::Grayscale, BitDepth::Eight) => PixelFormat::L8,
404        (Png::GrayscaleAlpha, BitDepth::Eight) => PixelFormat::La8,
405        (Png::Rgb, BitDepth::Eight) => PixelFormat::Rgb8,
406        (Png::Rgba, BitDepth::Eight) => PixelFormat::Rgba8,
407        (Png::Grayscale, BitDepth::Sixteen) => PixelFormat::L16,
408        (Png::GrayscaleAlpha, BitDepth::Sixteen) => PixelFormat::La16,
409        (Png::Rgb, BitDepth::Sixteen) => PixelFormat::Rgb16,
410        (Png::Rgba, BitDepth::Sixteen) => PixelFormat::Rgba16,
411        // EXPAND widens every other pair to one of the above.
412        (_, bits) => {
413            return Err(unsupported_color(
414                image::ImageFormat::Png.into(),
415                image::ExtendedColorType::Unknown(bits as u8),
416            ))
417        }
418    };
419    let total_bytes = u64::from(width) * u64::from(height) * format.bytes_per_pixel() as u64;
420    if total_bytes > max_bytes {
421        return Err(GuardedDecodeError::TooLarge { width, height });
422    }
423    let total_bytes =
424        usize::try_from(total_bytes).map_err(|_| GuardedDecodeError::TooLarge { width, height })?;
425    let mut bytes = vec![0; total_bytes];
426    reader.next_frame(&mut bytes).map_err(png_error)?;
427    if depth == BitDepth::Sixteen {
428        for sample in bytes.as_chunks_mut::<2>().0 {
429            *sample = u16::from_be_bytes(*sample).to_ne_bytes();
430        }
431    }
432    Ok(DecodedImage {
433        width,
434        height,
435        format,
436        bytes,
437    })
438}
439
440/// `image`'s own mapping of a `png` error.
441fn png_error(err: png::DecodingError) -> GuardedDecodeError {
442    use image::error::{
443        DecodingError, ImageFormatHint, LimitError, LimitErrorKind, ParameterError,
444        ParameterErrorKind,
445    };
446    GuardedDecodeError::Decode(match err {
447        png::DecodingError::IoError(err) => image::ImageError::IoError(err),
448        err @ png::DecodingError::Format(_) => image::ImageError::Decoding(DecodingError::new(
449            ImageFormatHint::Exact(image::ImageFormat::Png),
450            err,
451        )),
452        err @ png::DecodingError::Parameter(_) => image::ImageError::Parameter(
453            ParameterError::from_kind(ParameterErrorKind::Generic(err.to_string())),
454        ),
455        png::DecodingError::LimitsExceeded => {
456            image::ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
457        }
458    })
459}
460
461/// `decode_guarded` for a JPEG. `image`'s JPEG decoder copies its whole input
462/// before it decodes, so zune-jpeg decodes the borrowed bytes here, with the
463/// options and output color `image` uses.
464fn decode_jpeg(
465    bytes: &[u8],
466    max_side: u32,
467    max_bytes: u64,
468) -> Result<DecodedImage, GuardedDecodeError> {
469    let JpegHeader {
470        width,
471        height,
472        input_color,
473        coefficient_bytes,
474    } = jpeg_header(bytes).map_err(jpeg_error)?;
475    if width > max_side || height > max_side {
476        return Err(GuardedDecodeError::TooLarge { width, height });
477    }
478    // `image` decodes a color space it has no pixel format for to RGB.
479    let (output_color, format) = match input_color {
480        ColorSpace::Luma => (ColorSpace::Luma, PixelFormat::L8),
481        ColorSpace::LumaA => (ColorSpace::LumaA, PixelFormat::La8),
482        ColorSpace::RGBA => (ColorSpace::RGBA, PixelFormat::Rgba8),
483        _ => (ColorSpace::RGB, PixelFormat::Rgb8),
484    };
485    let total_bytes = u64::from(width) * u64::from(height) * output_color.num_components() as u64;
486    if total_bytes.saturating_add(coefficient_bytes) > max_bytes {
487        return Err(GuardedDecodeError::TooLarge { width, height });
488    }
489    let total_bytes =
490        usize::try_from(total_bytes).map_err(|_| GuardedDecodeError::TooLarge { width, height })?;
491    let mut pixels = vec![0; total_bytes];
492    // A second decoder, because zune-jpeg picks its color conversion while it
493    // reads the headers.
494    jpeg_decoder(ZCursor::new(bytes), output_color)
495        .decode_into(&mut pixels)
496        .map_err(jpeg_error)?;
497    Ok(DecodedImage {
498        width,
499        height,
500        format,
501        bytes: pixels,
502    })
503}
504
505/// zune-jpeg over `reader`, with the options `image` decodes with.
506fn jpeg_decoder<R: ZByteReaderTrait>(
507    reader: R,
508    output_color: ColorSpace,
509) -> zune_jpeg::JpegDecoder<HideMetadata<R>> {
510    let options = DecoderOptions::default()
511        .jpeg_set_out_colorspace(output_color)
512        .set_strict_mode(false)
513        .set_max_width(usize::MAX)
514        .set_max_height(usize::MAX);
515    zune_jpeg::JpegDecoder::new_with_options(HideMetadata(reader), options)
516}
517
518/// `reader` with every metadata segment zune-jpeg keeps a copy of hidden from
519/// it: EXIF, XMP, extended XMP, ICC, gain map, MPF and IPTC. The tip reads
520/// none of them. zune-jpeg keeps a list entry per ICC and gain-map segment,
521/// which reaches five times the input for many small ones, and before the
522/// first scan it sorts and walks the extended XMP parts it holds after every
523/// marker, so parts that never complete make the header parse quadratic.
524/// zune-jpeg recognizes each segment by peeking its identifier, so this reader
525/// changes that peek and zune-jpeg skips the segment as an unknown one. A
526/// segment zune-jpeg would reject, too short for an extended XMP part's
527/// 40-byte header or running past the end of the input, stays visible, so
528/// zune-jpeg rejects it as `image` does.
529struct HideMetadata<R>(R);
530
531/// The identifier of each segment zune-jpeg keeps, and the bytes it requires
532/// after the identifier. zune-jpeg peeks each identifier length in one parser
533/// only, so matching the identifier alone is exact.
534const KEPT_SEGMENTS: &[(&[u8], u64)] = &[
535    (b"Exif\0\0", 0),
536    (b"http://ns.adobe.com/xap/1.0/\0", 0),
537    (b"http://ns.adobe.com/xmp/extension/\0", 40),
538    (b"ICC_PROFILE\0", 0),
539    (b"urn:iso:std:iso:ts:21496:-1\0", 0),
540    (b"MPF\0", 0),
541    (b"Photoshop 3.0\0", 0),
542];
543
544impl<R: ZByteReaderTrait> HideMetadata<R> {
545    /// Whether the segment whose length field ends at the cursor holds `bytes`
546    /// after that field and ends within the input.
547    fn holds(&mut self, bytes: u64) -> Result<bool, ZByteIoError> {
548        let position = self.0.z_seek(ZSeekFrom::Current(-2))?;
549        let mut length = [0; 2];
550        self.0.read_exact_bytes(&mut length)?;
551        let length = u64::from(u16::from_be_bytes(length));
552        let end = self.0.z_seek(ZSeekFrom::End(0))?;
553        self.0.z_seek(ZSeekFrom::Start(position + 2))?;
554        Ok(length >= 2 + bytes && position + length <= end)
555    }
556}
557
558impl<R: ZByteReaderTrait> ZByteReaderTrait for HideMetadata<R> {
559    fn read_byte_no_error(&mut self) -> u8 {
560        self.0.read_byte_no_error()
561    }
562
563    fn read_exact_bytes(&mut self, buf: &mut [u8]) -> Result<(), ZByteIoError> {
564        self.0.read_exact_bytes(buf)
565    }
566
567    fn read_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ZByteIoError> {
568        self.0.read_bytes(buf)
569    }
570
571    fn peek_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ZByteIoError> {
572        self.0.peek_bytes(buf)
573    }
574
575    fn peek_exact_bytes(&mut self, buf: &mut [u8]) -> Result<(), ZByteIoError> {
576        self.0.peek_exact_bytes(buf)?;
577        let kept = KEPT_SEGMENTS
578            .iter()
579            .find(|(identifier, _)| buf == *identifier);
580        if let Some(&(identifier, header)) = kept {
581            if self.holds(identifier.len() as u64 + header)? {
582                buf[0] = 0;
583            }
584        }
585        Ok(())
586    }
587
588    fn z_seek(&mut self, from: ZSeekFrom) -> Result<u64, ZByteIoError> {
589        self.0.z_seek(from)
590    }
591
592    fn is_eof(&mut self) -> Result<bool, ZByteIoError> {
593        self.0.is_eof()
594    }
595
596    fn z_position(&mut self) -> Result<u64, ZByteIoError> {
597        self.0.z_position()
598    }
599
600    fn read_remaining(&mut self, sink: &mut Vec<u8>) -> Result<usize, ZByteIoError> {
601        self.0.read_remaining(sink)
602    }
603}
604
605/// What `decode_jpeg` needs from a JPEG's headers.
606struct JpegHeader {
607    width: u32,
608    height: u32,
609    input_color: ColorSpace,
610    coefficient_bytes: u64,
611}
612
613fn jpeg_header(bytes: &[u8]) -> Result<JpegHeader, zune_jpeg::errors::DecodeErrors> {
614    // `decode_headers` stops right after the first scan header, so the
615    // cursor's position is where that header ends.
616    let mut cursor = Cursor::new(bytes);
617    let mut decoder = jpeg_decoder(&mut cursor, ColorSpace::RGB);
618    decoder.decode_headers()?;
619    let frame = decoder.info().expect("headers were decoded");
620    let input_color = decoder.input_colorspace().expect("headers were decoded");
621    let scan_end = usize::try_from(cursor.position()).expect("within the input");
622    Ok(JpegHeader {
623        width: frame.width.into(),
624        height: frame.height.into(),
625        input_color,
626        coefficient_bytes: coefficient_bytes(&bytes[..scan_end], &frame),
627    })
628}
629
630/// The `ImageError` `image` reports for a zune-jpeg error. `image` maps two
631/// more variants, which zune-jpeg 0.5.15 never returns.
632fn jpeg_error(err: zune_jpeg::errors::DecodeErrors) -> GuardedDecodeError {
633    GuardedDecodeError::Decode(image::ImageError::Decoding(
634        image::error::DecodingError::new(image::ImageFormat::Jpeg.into(), err),
635    ))
636}
637
638/// Reads header dimensions without allocating pixels or applying decode limits.
639/// PNG goes through the `png` header alone: `image` sizes the output buffer
640/// before it reports dimensions, which fails on 32-bit targets for large ones.
641/// JPEG goes through zune-jpeg, which reads the borrowed bytes.
642pub(crate) fn header_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
643    match image::guess_format(bytes).ok()? {
644        image::ImageFormat::Png => {
645            let info = png_header(bytes)?;
646            Some((info.width, info.height))
647        }
648        image::ImageFormat::Jpeg => {
649            let header = jpeg_header(bytes).ok()?;
650            Some((header.width, header.height))
651        }
652        _ => image::ImageReader::new(Cursor::new(bytes))
653            .with_guessed_format()
654            .ok()?
655            .into_dimensions()
656            .ok(),
657    }
658}
659
660/// The DCT coefficients zune-jpeg holds for the whole image during the
661/// decode: those of a progressive JPEG, and those of a baseline JPEG whose
662/// first scan, the one whose header ends `to_first_scan`, leaves out a
663/// component.
664///
665/// `frame` is what zune-jpeg parsed, which gives the frame type and the
666/// component count. Its `SampleRatios` is too coarse for the count, so the
667/// sampling factors are read from the bytes. Every frame header of that type
668/// in `to_first_scan` that zune-jpeg would accept at the parsed size and
669/// component count is read, and the largest count wins. zune-jpeg parses one
670/// of them, so the count is never below zune-jpeg's. It can be above it. The
671/// largest of several headers wins, and a header is counted even if the decode
672/// would fail on it, such as one that names a quantization table no DQT
673/// segment before the first scan defines. zune-jpeg checks the tables only
674/// when the decode starts, before it allocates the coefficients.
675fn coefficient_bytes(to_first_scan: &[u8], frame: &ImageInfo) -> u64 {
676    let bytes = to_first_scan;
677    let progressive = frame.sof.is_progressive();
678    if !progressive && scan_components(bytes) >= frame.components {
679        return 0;
680    }
681    // zune-jpeg parses 0xFFC0 and 0xFFC1 as baseline, and 0xFFC2 as progressive.
682    let markers: &[u8] = if progressive { &[0xC2] } else { &[0xC0, 0xC1] };
683    (1..bytes.len())
684        .filter(|&i| bytes[i - 1] == 0xFF && markers.contains(&bytes[i]))
685        .filter_map(|i| frame_coefficients(&bytes[i + 1..], frame))
686        .max()
687        .unwrap_or(0)
688}
689
690/// The component count of the scan header that ends `bytes`: 0xFFDA, its
691/// length, the count, two bytes per component and three more. The smallest
692/// count that fits wins, and 1 if none does, so a doubt counts coefficients.
693fn scan_components(bytes: &[u8]) -> u8 {
694    (1..=4)
695        .find(|&components| {
696            let length = 6 + 2 * usize::from(components);
697            bytes.len().checked_sub(length + 2).is_some_and(|marker| {
698                bytes[marker..marker + 5] == [0xFF, 0xDA, 0, length as u8, components]
699            })
700        })
701        .unwrap_or(1)
702}
703
704/// The coefficients of a frame header that matches `frame` and that zune-jpeg
705/// would accept: 64 two-byte coefficients for each block of every MCU, with
706/// each side padded to whole MCUs. The blocks per MCU are the sum of each
707/// component's sampling factors multiplied. The quantization tables the header
708/// names are not checked against the DQT segments before the first scan, so
709/// the count includes a header zune-jpeg would parse and then fail to decode.
710fn frame_coefficients(header: &[u8], frame: &ImageInfo) -> Option<u64> {
711    // Length, precision, height, width, component count, then an id, the
712    // sampling factors and a table per component.
713    let fixed = header.get(..8)?;
714    let field = |at: usize| u16::from_be_bytes([fixed[at], fixed[at + 1]]);
715    let components = fixed[7];
716    let length = 8 + 3 * usize::from(components);
717    if components != frame.components
718        || usize::from(field(0)) != length
719        || fixed[2] != 8
720        || field(3) != frame.height
721        || field(5) != frame.width
722    {
723        return None;
724    }
725    let (mut h_max, mut v_max, mut blocks) = (1, 1, 0);
726    for &[_, factors, table] in header.get(8..length)?.as_chunks::<3>().0 {
727        let (h, v) = (u64::from(factors >> 4), u64::from(factors & 0xF));
728        // zune-jpeg rejects a horizontal factor other than 1, 2 or 4, a
729        // vertical factor outside 1..=4 and a quantization table above 3.
730        if !matches!(h, 1 | 2 | 4) || !(1..=4).contains(&v) || table > 3 {
731            return None;
732        }
733        h_max = h_max.max(h);
734        v_max = v_max.max(v);
735        blocks += h * v;
736    }
737    let mcus =
738        u64::from(frame.width).div_ceil(8 * h_max) * u64::from(frame.height).div_ceil(8 * v_max);
739    Some(2 * 64 * mcus * blocks)
740}
741
742/// A PNG's IHDR, parsed without allocating pixels. `None` for other formats
743/// and for a PNG whose header does not parse.
744fn png_header(bytes: &[u8]) -> Option<png::Info<'static>> {
745    if image::guess_format(bytes).ok()? != image::ImageFormat::Png {
746        return None;
747    }
748    png::Decoder::new(Cursor::new(bytes))
749        .read_header_info()
750        .ok()
751        .cloned()
752}
753
754/// Box-filter `bitmap` so its larger side is at most `max_side` (>= 1). Returns
755/// a clone when it already fits. Average of the covered source pixels per
756/// destination pixel, the same filter `generate_preview_png` uses.
757pub fn downsample(bitmap: &GrayscaleBitmap, max_side: u32) -> GrayscaleBitmap {
758    let max_side = max_side.max(1);
759
760    let (tw, th) = if bitmap.width > max_side || bitmap.height > max_side {
761        let scale = f64::min(
762            max_side as f64 / bitmap.width as f64,
763            max_side as f64 / bitmap.height as f64,
764        );
765        let w = ((bitmap.width as f64 * scale).round() as u32).max(1);
766        let h = ((bitmap.height as f64 * scale).round() as u32).max(1);
767        (w, h)
768    } else {
769        return bitmap.clone();
770    };
771
772    let src_w = bitmap.width as f64;
773    let src_h = bitmap.height as f64;
774    let tw_f = tw as f64;
775    let th_f = th as f64;
776
777    let mut data = Vec::with_capacity((tw as usize) * (th as usize));
778    for y in 0..th {
779        let sy0 = (y as f64 * src_h / th_f) as u32;
780        let sy1 = (((y + 1) as f64 * src_h / th_f) as u32).min(bitmap.height);
781        for x in 0..tw {
782            let sx0 = (x as f64 * src_w / tw_f) as u32;
783            let sx1 = (((x + 1) as f64 * src_w / tw_f) as u32).min(bitmap.width);
784
785            let mut sum = 0u32;
786            let mut count = 0u32;
787            for sy in sy0..sy1 {
788                let row_offset = (sy * bitmap.width) as usize;
789                for sx in sx0..sx1 {
790                    sum += bitmap.data[row_offset + sx as usize] as u32;
791                    count += 1;
792                }
793            }
794
795            #[allow(clippy::manual_checked_ops)]
796            let value = if count > 0 { (sum / count) as u8 } else { 0 };
797            data.push(value);
798        }
799    }
800
801    GrayscaleBitmap {
802        width: tw,
803        height: th,
804        data,
805    }
806}
807
808/// A 200 px RGBA thumbnail: black ink, alpha from the tip's gray value.
809pub fn generate_preview_png(bitmap: &GrayscaleBitmap) -> Result<Vec<u8>, String> {
810    let small = downsample(bitmap, 200);
811
812    let img: RgbaImage = ImageBuffer::from_fn(small.width, small.height, |x, y| {
813        let alpha = small.data[(y * small.width + x) as usize];
814        Rgba([0, 0, 0, alpha])
815    });
816
817    let mut buf = std::io::Cursor::new(Vec::new());
818    img.write_to(&mut buf, image::ImageFormat::Png)
819        .map_err(|e| format!("thumbnail encoding failed: {e}"))?;
820
821    Ok(buf.into_inner())
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827
828    #[test]
829    fn to_grayscale_16bit_takes_high_byte() {
830        let tip = TipBitmap {
831            width: 2,
832            height: 1,
833            depth: 16,
834            data: vec![0xAB, 0x12, 0xCD, 0x34],
835        };
836        let result = to_grayscale(&tip);
837        assert_eq!(result.width, 2);
838        assert_eq!(result.height, 1);
839        assert_eq!(result.data.len(), 2);
840        assert_eq!(result.data, vec![0xAB, 0xCD], "must keep the high byte");
841    }
842
843    #[test]
844    fn downsample_averages_covered_pixels() {
845        let bitmap = GrayscaleBitmap {
846            width: 4,
847            height: 2,
848            data: vec![0, 255, 0, 255, 0, 255, 0, 255],
849        };
850        let small = downsample(&bitmap, 2);
851        assert_eq!((small.width, small.height), (2, 1));
852        assert_eq!(small.data, vec![127, 127]);
853    }
854
855    #[test]
856    fn downsample_keeps_a_fitting_bitmap() {
857        let bitmap = GrayscaleBitmap {
858            width: 3,
859            height: 2,
860            data: vec![1, 2, 3, 4, 5, 6],
861        };
862        let same = downsample(&bitmap, 8);
863        assert_eq!((same.width, same.height), (3, 2));
864        assert_eq!(same.data, bitmap.data);
865    }
866
867    #[test]
868    fn preview_png_is_200_wide_with_alpha_from_gray() {
869        let bitmap = GrayscaleBitmap {
870            width: 400,
871            height: 100,
872            data: vec![200u8; 400 * 100],
873        };
874        let png = generate_preview_png(&bitmap).unwrap();
875        let img = image::load_from_memory(&png).unwrap().to_rgba8();
876        assert_eq!((img.width(), img.height()), (200, 50));
877        assert!(img.pixels().all(|p| p.0 == [0, 0, 0, 200]));
878    }
879}
880
881#[cfg(test)]
882mod tip_image_tests {
883    use super::*;
884
885    fn rgba_png(width: u32, height: u32, pixels: &[[u8; 4]]) -> Vec<u8> {
886        let img: RgbaImage =
887            ImageBuffer::from_fn(width, height, |x, y| Rgba(pixels[(y * width + x) as usize]));
888        let mut buf = std::io::Cursor::new(Vec::new());
889        img.write_to(&mut buf, image::ImageFormat::Png).unwrap();
890        buf.into_inner()
891    }
892
893    fn luma_png(width: u32, height: u32, luma: &[u8]) -> Vec<u8> {
894        let mut buf = Vec::new();
895        {
896            let mut encoder = png::Encoder::new(&mut buf, width, height);
897            encoder.set_color(png::ColorType::Grayscale);
898            encoder.set_depth(png::BitDepth::Eight);
899            let mut writer = encoder.write_header().unwrap();
900            writer.write_image_data(luma).unwrap();
901        }
902        buf
903    }
904
905    #[test]
906    fn alpha_channel_becomes_intensity() {
907        let png = rgba_png(3, 1, &[[255, 0, 0, 255], [255, 0, 0, 128], [255, 0, 0, 0]]);
908        let bitmap = decode_tip_image(&png).unwrap();
909        assert_eq!((bitmap.width, bitmap.height), (3, 1));
910        assert_eq!(bitmap.data, vec![255, 128, 0]);
911    }
912
913    #[test]
914    fn opaque_image_inverts_luminance() {
915        let png = luma_png(3, 1, &[0, 40, 255]);
916        let bitmap = decode_tip_image(&png).unwrap();
917        assert_eq!((bitmap.width, bitmap.height), (3, 1));
918        assert_eq!(bitmap.data, vec![255, 215, 0]);
919    }
920
921    #[test]
922    fn oversized_image_is_rejected_with_the_ceiling_message() {
923        let over = MAX_IMPORT_DIMENSION + 1;
924        let png = luma_png(over, 1, &vec![0u8; over as usize]);
925        let err = decode_tip_image(&png).expect_err("over-ceiling image must be rejected");
926        assert!(
927            matches!(err, TipImageError::TooLarge { width, height } if width == over && height == 1),
928            "expected TooLarge, got {err:?}"
929        );
930        assert!(
931            err.to_string().starts_with("image is "),
932            "unexpected message: {err}"
933        );
934    }
935
936    /// Every decodable pixel format, filled with varied samples, over several
937    /// conversion chunks.
938    fn every_format() -> [(PixelFormat, image::DynamicImage); 10] {
939        let base = image::DynamicImage::ImageRgba16(ImageBuffer::from_fn(100, 100, |x, y| {
940            let i = (y * 100 + x) * 4;
941            image::Rgba([0, 1, 2, 3].map(|c| ((i + c).wrapping_mul(0x9E37_79B9) >> 16) as u16))
942        }));
943        [
944            (PixelFormat::L8, base.to_luma8().into()),
945            (PixelFormat::La8, base.to_luma_alpha8().into()),
946            (PixelFormat::Rgb8, base.to_rgb8().into()),
947            (PixelFormat::Rgba8, base.to_rgba8().into()),
948            (PixelFormat::L16, base.to_luma16().into()),
949            (PixelFormat::La16, base.to_luma_alpha16().into()),
950            (PixelFormat::Rgb16, base.to_rgb16().into()),
951            (PixelFormat::Rgb32F, base.to_rgb32f().into()),
952            (PixelFormat::Rgba32F, base.to_rgba32f().into()),
953            (PixelFormat::Rgba16, base),
954        ]
955    }
956
957    #[test]
958    fn tips_match_image_conversions_for_every_format() {
959        for (format, image) in every_format() {
960            let name = format!("{:?}", image.color());
961            let coverage = if image.color().has_alpha() {
962                image.to_luma_alpha8().pixels().map(|p| p.0[1]).collect()
963            } else {
964                let luma = image.to_luma8().into_raw();
965                luma.into_iter().map(|v| 255 - v).collect::<Vec<_>>()
966            };
967            let luma = image.to_luma8().into_raw();
968            let decoded = |image: &image::DynamicImage| DecodedImage {
969                width: image.width(),
970                height: image.height(),
971                format,
972                bytes: image.as_bytes().to_vec(),
973            };
974            assert_eq!(
975                tip_plane(decoded(&image), TipSample::Coverage),
976                coverage,
977                "coverage {name}"
978            );
979            assert_eq!(
980                tip_plane(decoded(&image), TipSample::Luma),
981                luma,
982                "luma {name}"
983            );
984            if !matches!(format, PixelFormat::Rgb32F | PixelFormat::Rgba32F) {
985                let mut png = std::io::Cursor::new(Vec::new());
986                image.write_to(&mut png, image::ImageFormat::Png).unwrap();
987                let png = png.into_inner();
988                assert_eq!(
989                    decode_tip_image(&png).unwrap().data,
990                    coverage,
991                    "decode_tip_image {name}"
992                );
993                assert_eq!(
994                    crate::procreate::decode_tip_png(&png).unwrap().data,
995                    luma,
996                    "decode_tip_png {name}"
997                );
998            }
999        }
1000    }
1001
1002    /// `decode_guarded` decodes JPEG with zune-jpeg directly, so it must
1003    /// return the pixels and errors `image` returns for the same bytes.
1004    #[test]
1005    fn jpeg_decodes_as_image_does() {
1006        let base = image::DynamicImage::ImageRgb8(ImageBuffer::from_fn(37, 23, |x, y| {
1007            let i = y * 37 + x;
1008            image::Rgb([0, 1, 2].map(|c| ((i * 3 + c).wrapping_mul(0x9E37_79B9) >> 24) as u8))
1009        }));
1010        let mut gray = std::io::Cursor::new(Vec::new());
1011        base.to_luma8()
1012            .write_to(&mut gray, image::ImageFormat::Jpeg)
1013            .unwrap();
1014        for image in [base.to_luma8().into(), base] {
1015            let mut jpeg = std::io::Cursor::new(Vec::new());
1016            image.write_to(&mut jpeg, image::ImageFormat::Jpeg).unwrap();
1017            let jpeg = jpeg.into_inner();
1018            let expected = image::load_from_memory(&jpeg).unwrap();
1019            let Ok(decoded) = decode_guarded(&jpeg, 64, u64::MAX) else {
1020                panic!("{:?} JPEG must decode", image.color());
1021            };
1022            assert_eq!((decoded.width, decoded.height), (37, 23));
1023            assert_eq!(decoded.bytes, expected.as_bytes(), "{:?}", image.color());
1024        }
1025
1026        let truncated = [0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x43, 0x00];
1027        let expected = image::load_from_memory(&truncated).unwrap_err();
1028        let Err(GuardedDecodeError::Decode(err)) = decode_guarded(&truncated, 64, u64::MAX) else {
1029            panic!("a truncated JPEG must not decode");
1030        };
1031        assert_eq!(err.to_string(), expected.to_string());
1032
1033        // zune-jpeg rejects a kept segment that runs past the end of the input
1034        // and an extended XMP part with a 39-byte header, as `image` does.
1035        let mut cases = Vec::new();
1036        for (name, marker, body) in kept_segments() {
1037            cases.push((name.to_string(), segment(marker, &body, None), true));
1038            let past_end = segment(marker, &body, Some(u16::MAX));
1039            cases.push((format!("{name} past the end"), past_end, false));
1040        }
1041        let mut short_part = b"http://ns.adobe.com/xmp/extension/\0".to_vec();
1042        short_part.resize(short_part.len() + 39, 0);
1043        let short_part = segment(0xE1, &short_part, None);
1044        cases.push(("extended XMP with a short header".into(), short_part, false));
1045        for (name, segment, decodes) in cases {
1046            let mut jpeg = gray.get_ref().clone();
1047            jpeg.splice(2..2, segment);
1048            match (
1049                image::load_from_memory(&jpeg),
1050                decode_guarded(&jpeg, 64, u64::MAX),
1051            ) {
1052                (Ok(expected), Ok(decoded)) if decodes => {
1053                    assert_eq!(decoded.bytes, expected.as_bytes(), "{name}");
1054                }
1055                (Err(expected), Err(GuardedDecodeError::Decode(err))) if !decodes => {
1056                    assert_eq!(err.to_string(), expected.to_string(), "{name}");
1057                }
1058                (expected, decoded) => panic!(
1059                    "{name}: image {:?}, decode_guarded {:?}",
1060                    expected.is_ok(),
1061                    decoded.is_ok()
1062                ),
1063            }
1064        }
1065    }
1066
1067    /// A JPEG segment: `marker`, a length, `declared` or the real one, and
1068    /// `body`.
1069    fn segment(marker: u8, body: &[u8], declared: Option<u16>) -> Vec<u8> {
1070        let length = declared.unwrap_or_else(|| u16::try_from(2 + body.len()).unwrap());
1071        let mut segment = vec![0xFF, marker];
1072        segment.extend_from_slice(&length.to_be_bytes());
1073        segment.extend_from_slice(body);
1074        segment
1075    }
1076
1077    /// One segment of each kind zune-jpeg 0.5.15 keeps, with the identifiers
1078    /// copied from its `headers.rs` and 5 bytes of data, enough for every
1079    /// kind to be kept.
1080    fn kept_segments() -> [(&'static str, u8, Vec<u8>); 7] {
1081        let with_data = |identifier: &[u8]| [identifier, b"12345"].concat();
1082        let mut extended_xmp = b"http://ns.adobe.com/xmp/extension/\0".to_vec();
1083        extended_xmp.extend_from_slice(&[b'G'; 32]);
1084        extended_xmp.extend_from_slice(&5u32.to_be_bytes());
1085        extended_xmp.extend_from_slice(&0u32.to_be_bytes());
1086        [
1087            ("EXIF", 0xE1, with_data(b"Exif\0\0")),
1088            ("XMP", 0xE1, with_data(b"http://ns.adobe.com/xap/1.0/\0")),
1089            ("extended XMP", 0xE1, with_data(&extended_xmp)),
1090            ("ICC", 0xE2, with_data(b"ICC_PROFILE\0\x01\x01")),
1091            (
1092                "gain map",
1093                0xE2,
1094                with_data(b"urn:iso:std:iso:ts:21496:-1\0"),
1095            ),
1096            ("MPF", 0xE2, with_data(b"MPF\0")),
1097            ("IPTC", 0xED, with_data(b"Photoshop 3.0\0")),
1098        ]
1099    }
1100
1101    /// zune-jpeg keeps none of the metadata segments behind `jpeg_decoder`,
1102    /// and all of them without it.
1103    #[test]
1104    fn jpeg_decoder_keeps_no_metadata() {
1105        let mut jpeg = std::io::Cursor::new(Vec::new());
1106        image::DynamicImage::ImageLuma8(ImageBuffer::from_pixel(8, 8, image::Luma([0x40])))
1107            .write_to(&mut jpeg, image::ImageFormat::Jpeg)
1108            .unwrap();
1109        let mut jpeg = jpeg.into_inner();
1110        let metadata: Vec<u8> = kept_segments()
1111            .iter()
1112            .flat_map(|(_, marker, body)| segment(*marker, body, None))
1113            .collect();
1114        jpeg.splice(2..2, metadata);
1115
1116        let kept = |info: &zune_jpeg::ImageInfo, icc: Option<Vec<u8>>| {
1117            [
1118                ("EXIF", info.exif_data.is_some()),
1119                ("XMP", info.xmp_data.is_some()),
1120                ("extended XMP", info.extended_xmp.is_some()),
1121                ("ICC", icc.is_some()),
1122                ("gain map", !info.gain_map_info.is_empty()),
1123                ("MPF", info.multi_picture_information.is_some()),
1124                ("IPTC", info.iptc_data.is_some()),
1125            ]
1126        };
1127        let options = DecoderOptions::default().set_strict_mode(false);
1128        let mut plain = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(&jpeg), options);
1129        plain.decode_headers().unwrap();
1130        for (name, kept) in kept(&plain.info().unwrap(), plain.icc_profile()) {
1131            assert!(kept, "zune-jpeg alone keeps {name}");
1132        }
1133        let mut hidden = jpeg_decoder(ZCursor::new(&jpeg), ColorSpace::Luma);
1134        hidden.decode_headers().unwrap();
1135        for (name, kept) in kept(&hidden.info().unwrap(), hidden.icc_profile()) {
1136            assert!(!kept, "jpeg_decoder must hide {name}");
1137        }
1138    }
1139
1140    /// `decode_guarded` decodes PNG with `png` directly, so it must return the
1141    /// pixels and errors `image` returns for the same bytes, including the
1142    /// color types and bit depths EXPAND widens.
1143    #[test]
1144    fn png_decodes_as_image_does() {
1145        use png::{BitDepth, ColorType};
1146        let (width, height) = (37, 23);
1147        let cases: [(ColorType, BitDepth, u32, &[u8]); 5] = [
1148            (ColorType::Grayscale, BitDepth::One, 1, &[]),
1149            (ColorType::Grayscale, BitDepth::Four, 4, &[0, 3]),
1150            (ColorType::Indexed, BitDepth::Eight, 8, &[0, 64, 128, 192]),
1151            (ColorType::Rgb, BitDepth::Sixteen, 48, &[0, 7, 0, 7, 0, 7]),
1152            (ColorType::Rgba, BitDepth::Sixteen, 64, &[]),
1153        ];
1154        let mut last = Vec::new();
1155        for (color, depth, bits_per_pixel, trns) in cases {
1156            let len = (width * bits_per_pixel).div_ceil(8) * height;
1157            let data: Vec<u8> = (0..len)
1158                .map(|i| (i.wrapping_mul(0x9E37_79B9) >> 24) as u8)
1159                .collect();
1160            let mut png = Vec::new();
1161            let mut encoder = png::Encoder::new(&mut png, width, height);
1162            encoder.set_color(color);
1163            encoder.set_depth(depth);
1164            if color == ColorType::Indexed {
1165                encoder.set_palette((0..=255u8).flat_map(|i| [i, !i, i / 2]).collect::<Vec<_>>());
1166            }
1167            if !trns.is_empty() {
1168                encoder.set_trns(trns);
1169            }
1170            let mut writer = encoder.write_header().unwrap();
1171            writer.write_image_data(&data).unwrap();
1172            writer.finish().unwrap();
1173
1174            let name = format!("{color:?} {depth:?}");
1175            let expected = image::load_from_memory(&png).unwrap();
1176            let Ok(decoded) = decode_guarded(&png, 64, u64::MAX) else {
1177                panic!("{name} PNG must decode");
1178            };
1179            assert_eq!((decoded.width, decoded.height), (width, height), "{name}");
1180            assert_eq!(decoded.format.color_type(), expected.color(), "{name}");
1181            assert_eq!(decoded.bytes, expected.as_bytes(), "{name}");
1182            last = png;
1183        }
1184
1185        let mut bad_crc = last.clone();
1186        bad_crc[29] ^= 1;
1187        for (name, broken) in [("truncated", &last[..20]), ("bad IHDR CRC", &bad_crc[..])] {
1188            let expected = image::load_from_memory(broken).unwrap_err();
1189            let Err(GuardedDecodeError::Decode(err)) = decode_guarded(broken, 64, u64::MAX) else {
1190                panic!("a PNG with {name} must not decode");
1191            };
1192            assert_eq!(err.to_string(), expected.to_string(), "{name}");
1193        }
1194    }
1195
1196    /// An eXIf chunk before the image data decodes up to
1197    /// `MAX_PNG_EXIF_BYTES`, whatever the pixel format. One after the image
1198    /// data is never read.
1199    #[test]
1200    fn png_exif_decodes_up_to_the_limit() {
1201        let png = |exif_len: usize, after_image: bool| {
1202            let mut png = Vec::new();
1203            let mut encoder = png::Encoder::new(&mut png, 3, 2);
1204            encoder.set_color(png::ColorType::Rgba);
1205            encoder.set_depth(png::BitDepth::Sixteen);
1206            let mut writer = encoder.write_header().unwrap();
1207            let exif = png::chunk::ChunkType(*b"eXIf");
1208            if !after_image {
1209                writer.write_chunk(exif, &vec![0; exif_len]).unwrap();
1210            }
1211            writer.write_image_data(&[0x40; 3 * 2 * 8]).unwrap();
1212            if after_image {
1213                writer.write_chunk(exif, &vec![0; exif_len]).unwrap();
1214            }
1215            writer.finish().unwrap();
1216            png
1217        };
1218        for (exif_len, after_image) in [(MAX_PNG_EXIF_BYTES, false), (MAX_PNG_EXIF_BYTES + 1, true)]
1219        {
1220            assert!(
1221                decode_guarded(&png(exif_len, after_image), 64, u64::MAX).is_ok(),
1222                "a {exif_len}-byte eXIf, after the image data: {after_image}, must decode"
1223            );
1224        }
1225        let Err(GuardedDecodeError::Decode(err)) =
1226            decode_guarded(&png(MAX_PNG_EXIF_BYTES + 1, false), 64, u64::MAX)
1227        else {
1228            panic!("an eXIf over the limit must not decode");
1229        };
1230        assert_eq!(
1231            err.to_string(),
1232            "Format error decoding Png: eXIf chunk is 65537 bytes, over the 65536 a tip reads"
1233        );
1234    }
1235
1236    #[test]
1237    fn garbage_bytes_report_a_decode_error() {
1238        let err = decode_tip_image(b"not an image").expect_err("garbage must not decode");
1239        assert!(matches!(err, TipImageError::Decode(_)), "got {err:?}");
1240    }
1241}
1242
1243#[cfg(test)]
1244mod tip_bitmap_of_tests {
1245    use super::*;
1246
1247    #[test]
1248    fn tip_bitmap_of_copies_pixels_and_declares_depth_8() {
1249        let bitmap = GrayscaleBitmap {
1250            width: 3,
1251            height: 2,
1252            data: vec![0, 17, 255, 128, 1, 0],
1253        };
1254        let tip = tip_bitmap_of(&bitmap);
1255        assert_eq!((tip.width, tip.height), (3, 2));
1256        assert_eq!(tip.depth, 8, "GrayscaleBitmap is 8-bit by construction");
1257        assert_eq!(tip.data, bitmap.data, "polarity matches, so no conversion");
1258    }
1259
1260    #[test]
1261    fn tip_bitmap_of_round_trips_an_8_bit_tip() {
1262        let tip = TipBitmap {
1263            width: 2,
1264            height: 2,
1265            depth: 8,
1266            data: vec![9, 200, 0, 255],
1267        };
1268        let back = tip_bitmap_of(&to_grayscale(&tip));
1269        assert_eq!((back.width, back.height, back.depth), (2, 2, 8));
1270        assert_eq!(back.data, tip.data);
1271    }
1272}