Merge pull request #4716 from dotCooCoo/fix/pdf-image-repeat-bound

PDF export: bound a colorized image's color channel
This commit is contained in:
BFlat
2026-10-31 00:48:17 +07:00
committed by GitHub
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.