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
45pub 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;
57pub const MAX_IMPORT_DECODED_BYTES: u64 = 512 * 1024 * 1024;
60
61#[derive(Debug)]
62pub enum TipImageError {
63 Decode(String),
66 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
92pub 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
112pub(crate) struct DecodedImage {
115 pub width: u32,
116 pub height: u32,
117 pub format: PixelFormat,
118 pub bytes: Vec<u8>,
119}
120
121#[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#[derive(Clone, Copy)]
176pub(crate) enum TipSample {
177 Coverage,
179 Luma,
180}
181
182pub(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 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
218fn 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
253pub(crate) enum GuardedDecodeError {
255 TooLarge {
257 width: u32,
258 height: u32,
259 },
260 Decode(image::ImageError),
261}
262
263pub(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 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 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
335const MAX_PNG_EXIF_BYTES: usize = 64 * 1024;
338
339fn oversize_exif(bytes: &[u8]) -> Option<u32> {
342 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 at = at.saturating_add(12).saturating_add(len as usize);
355 }
356 None
357}
358
359fn 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 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 (_, 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
440fn 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
461fn 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 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 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
505fn 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
518struct HideMetadata<R>(R);
530
531const 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 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
605struct 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 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
630fn 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
638pub(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
660fn 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 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
690fn 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
704fn frame_coefficients(header: &[u8], frame: &ImageInfo) -> Option<u64> {
711 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 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
742fn 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
754pub 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
808pub 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 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 #[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 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 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 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 #[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 #[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 #[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}