PDF export: bound a colorized image's color channel

A colorized image's color channel is the paint's samples repeated once a
pixel, and nothing bounded its size: an image colorized in Registration in
a document with many spot swatches could allocate without limit, and an
overflowing length panicked inside repeat. The length is now computed with
checked arithmetic and refused above MAX_RASTER_ALLOC, the image's own
decode budget, and the export leaves such an image out with the existing
"an image couldn't be decoded" warning instead of drawing it uncolored.
This commit is contained in:
dotCooCoo
2026-10-30 23:43:57 +07:00
committed by bflatastic
parent 07188ce11b
commit 16db8b6a1a
2 changed files with 36 additions and 7 deletions
+23 -2
View File
@@ -3,6 +3,12 @@ use super::{Arc, BlendMode, Color, Exporter, Image, NodeKind, Paint, Rect, Size,
use krilla::color::device_n::{Colorant, DeviceNSpace, Process};
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
/// The byte count of a repeated color channel, within [`vectorcraft_doc::MAX_RASTER_ALLOC`].
fn tinted_color_len(samples: usize, pixels: usize) -> Option<usize> {
let bytes = samples.checked_mul(pixels)?;
(u64::try_from(bytes).ok()? <= vectorcraft_doc::MAX_RASTER_ALLOC).then_some(bytes)
}
/// A colorized image's pixels: one color in every pixel, shown as much as its soft mask says.
#[derive(Clone, Hash)]
struct Tinted {
@@ -52,8 +58,9 @@ impl Exporter<'_> {
/// every process ink and every spot ink of the document, other colors in gray, RGB or CMYK as
/// the Output settings write them. The coverage is resampled as the Compression settings ask
/// ([`crate::images::coverage_sized`]). `None` when the paint isn't a solid color or the pixels
/// can't be decoded; cached like [`Self::load_image`]. With `interpolate` (and the export's own
/// setting), viewers may smooth it.
/// can't be decoded or its color channel exceeds [`vectorcraft_doc::MAX_RASTER_ALLOC`]; cached
/// like [`Self::load_image`]. With `interpolate` (and the export's own setting), viewers may
/// smooth it.
pub(super) fn colorized_image(&mut self, im: &vectorcraft_doc::ImageObject, paint: &Paint, interpolate: bool) -> Option<Image> {
let Paint::Solid { color, swatch, tint } = paint else { return None };
let size = placed_size(im);
@@ -96,6 +103,7 @@ impl Exporter<'_> {
other => (other.to_rgb().map(q).to_vec(), ImageColorspace::Rgb),
},
};
tinted_color_len(samples.len(), n)?;
let color = Arc::new(samples.repeat(n));
let img = Image::from_custom(Tinted { color, alpha: Arc::new(alpha), width, height, space, device_n }, interpolate).ok();
self.images.insert(cache, img.clone());
@@ -245,3 +253,16 @@ pub(super) fn embed(blob: &vectorcraft_doc::ImageBlob, interpolate: bool, sixtee
Some(Image::from_rgba8(rgba.into_raw(), w, h))
})
}
#[cfg(test)]
mod tests {
use super::tinted_color_len;
#[test]
fn tinted_color_rejects_oversized_and_overflowing_buffers() {
let max = usize::try_from(vectorcraft_doc::MAX_RASTER_ALLOC).unwrap_or(usize::MAX);
assert_eq!(tinted_color_len(4, max / 4), Some(max));
assert_eq!(tinted_color_len(4, max / 4 + 1), None);
assert_eq!(tinted_color_len(4, usize::MAX), None);
}
}
+13 -5
View File
@@ -1,4 +1,4 @@
use super::exporter::{address, blend, color, constant_mask, cover, in_range, norm, rects_overlap, rule, to_path, writable, xf};
use super::exporter::{UNDECODED, address, blend, color, constant_mask, cover, in_range, norm, rects_overlap, rule, to_path, writable, xf};
use super::{
Affine, AppearanceItem, BezPath, BlendMode, Exporter, Fill, FillRule, LineCap, LineJoin, Mark, Node, NodeKind, NormalizedF32, Paint, Path, Rect,
Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke,
@@ -182,10 +182,18 @@ impl Exporter<'_> {
self.shape(s, n, &bp, *rule, page);
}
NodeKind::Text(t) => self.text(s, n, t, page),
NodeKind::Image(im) => {
let colorized = n.extras().colorize.as_deref().and_then(|p| self.colorized_image(im, p, !n.extras().pixelated));
self.image(s, im, colorized.or(alpha), n.extras().pixelated)
}
NodeKind::Image(im) => match n.extras().colorize.as_deref() {
Some(p @ Paint::Solid { .. }) => {
if writable(s, im.xf, Rect::new(0.0, 0.0, im.width.max(1).into(), im.height.max(1).into())) {
if let Some(colorized) = self.colorized_image(im, p, !n.extras().pixelated) {
self.image(s, im, Some(colorized), n.extras().pixelated);
} else {
self.warn(UNDECODED);
}
}
}
_ => self.image(s, im, alpha, n.extras().pixelated),
},
NodeKind::SymbolInstance { symbol, xf } => {
// Within the objects a frame may draw of symbol instances, as on screen: nested
// instances multiply.