mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user