mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
AF.5: bound hostile Affinity files; fuzz the new import paths
- One budget for the whole read (layers, pixels, characters, table cells), shared with embedded documents and charged also when their reading fails; the first reading is dropped before the second. - Tables: at most 65 536 cells; lines and cell formats that don't match the grid are left out instead of shifted. - Paragraph formats matched to runs by binary search; font names longer than 256 bytes are dropped. - Overlays: one work budget per import; encoded originals decoded within their layer's size; table cell outlines recoloured too. - Engine: master page copies capped, each page's master found in linear time, line gaps looked up once per font, the working CMYK table built once, objects past MAX_COORD left out. - Embedded content stays with its node; pixel masks under artistic text are read whole and include stroke reach; gradient overlays on hidden layers are kept. - Render: contexts and raster effect images stay within MAX_RASTER_SIDE per side (vello overflowed near u16::MAX; found by the new fuzz target). - Tests: affinity tests/import_limits.rs, feature samples in import_fuzz.rs, shared builders in engine/tests/support/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
fd50bd6f84
commit
f73a3df974
+125
-58
@@ -271,9 +271,11 @@ pub struct Table {
|
||||
pub rows: Vec<f64>,
|
||||
/// Row by row.
|
||||
pub cells: Vec<TableCell>,
|
||||
/// Vertical edges row by row, one more per row than the columns; `None` draws no line.
|
||||
/// Vertical edges row by row, one more per row than the columns; `None` draws no line, and
|
||||
/// the list is empty when the file's didn't match the grid.
|
||||
pub vertical: Vec<Option<Stroke>>,
|
||||
/// Horizontal edges boundary by boundary (one more than the rows), one per column.
|
||||
/// Horizontal edges boundary by boundary (one more than the rows), one per column (empty
|
||||
/// as `vertical` can be).
|
||||
pub horizontal: Vec<Option<Stroke>>,
|
||||
/// Node space to document pixels.
|
||||
pub transform: Affine,
|
||||
@@ -472,6 +474,29 @@ const MAX_LAYER_PIXELS: u64 = 512 << 20;
|
||||
|
||||
/// Embedded documents inside embedded documents are followed this deep.
|
||||
const MAX_EMBED_DEPTH: usize = 4;
|
||||
/// Characters of all stories together (a story may be shared by many text layers).
|
||||
const MAX_DOCUMENT_CHARS: usize = 16 << 20;
|
||||
/// Table cells of all tables together (each becomes a text object with its edge lines).
|
||||
const MAX_DOCUMENT_CELLS: usize = 1 << 18;
|
||||
|
||||
/// What a whole read may still use. Shared objects can be linked from many layers, and embedded
|
||||
/// documents are read with what their embedding document left, so linking or embedding the
|
||||
/// same content many times can't multiply the work.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Budget {
|
||||
/// Layer nodes ([`MAX_NODES`] in all).
|
||||
pub(crate) nodes: usize,
|
||||
/// Pixels the pixel layers may decode ([`MAX_LAYER_PIXELS`] in all).
|
||||
pub(crate) pixels: u64,
|
||||
/// Characters of stories ([`MAX_DOCUMENT_CHARS`] in all).
|
||||
pub(crate) chars: usize,
|
||||
/// Table cells ([`MAX_DOCUMENT_CELLS`] in all).
|
||||
pub(crate) cells: usize,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
pub(crate) const FULL: Budget = Budget { nodes: MAX_NODES, pixels: MAX_LAYER_PIXELS, chars: MAX_DOCUMENT_CHARS, cells: MAX_DOCUMENT_CELLS };
|
||||
}
|
||||
|
||||
/// Read a document: archive, `doc.dat` and the model.
|
||||
pub fn read(bytes: &[u8], limits: Limits) -> Result<Document, Error> {
|
||||
@@ -480,28 +505,35 @@ pub fn read(bytes: &[u8], limits: Limits) -> Result<Document, Error> {
|
||||
|
||||
/// [`read`], its pixel layers holding at most `pixels` pixels together.
|
||||
pub(crate) fn read_within(bytes: &[u8], limits: Limits, pixels: u64) -> Result<Document, Error> {
|
||||
read_at(bytes, limits, pixels, Affine::IDENTITY, 0).map(|r| r.document)
|
||||
read_at(bytes, limits, &mut Budget { pixels, ..Budget::FULL }, Affine::IDENTITY, 0).document
|
||||
}
|
||||
|
||||
/// A document read by [`read_at`], with what it used of its budgets.
|
||||
/// A document read by [`read_at`] (or why it couldn't be), with the bytes its archive extracted
|
||||
/// either way.
|
||||
struct ReadAt {
|
||||
document: Document,
|
||||
/// Bytes its archive extracted.
|
||||
document: Result<Document, Error>,
|
||||
extracted: usize,
|
||||
/// Pixels its pixel layers left of the allowance.
|
||||
pixels_left: u64,
|
||||
}
|
||||
|
||||
/// Read a document whose spreads are placed by `base` (for embedded documents, nested `depth`
|
||||
/// deep), its pixel layers holding at most `pixels` pixels together.
|
||||
fn read_at(bytes: &[u8], limits: Limits, pixels: u64, base: Affine, depth: usize) -> Result<ReadAt, Error> {
|
||||
let mut archive = Archive::open(bytes, limits)?;
|
||||
/// deep). What it uses is taken from `budget`, also when it fails part of the way.
|
||||
fn read_at(bytes: &[u8], limits: Limits, budget: &mut Budget, base: Affine, depth: usize) -> ReadAt {
|
||||
let mut archive = match Archive::open(bytes, limits) {
|
||||
Ok(a) => a,
|
||||
Err(e) => return ReadAt { document: Err(e), extracted: 0 },
|
||||
};
|
||||
let document = read_archive(&mut archive, budget, base, depth);
|
||||
ReadAt { document, extracted: archive.extracted() }
|
||||
}
|
||||
|
||||
fn read_archive(archive: &mut Archive, budget: &mut Budget, base: Affine, depth: usize) -> Result<Document, Error> {
|
||||
let doc = archive.read("doc.dat")?;
|
||||
let s = stream::parse(&doc)?;
|
||||
let mut reader = Reader::new(&s, &mut archive, base, depth);
|
||||
reader.pixels_left = pixels;
|
||||
let (document, pixels_left) = reader.document()?;
|
||||
Ok(ReadAt { document, extracted: archive.extracted(), pixels_left })
|
||||
let mut reader = Reader::new(&s, archive, base, depth);
|
||||
reader.budget = *budget;
|
||||
let document = reader.document();
|
||||
*budget = reader.budget;
|
||||
document
|
||||
}
|
||||
|
||||
pub(crate) struct Reader<'s, 'a, 'b> {
|
||||
@@ -509,7 +541,6 @@ pub(crate) struct Reader<'s, 'a, 'b> {
|
||||
pub(crate) archive: &'b mut Archive<'a>,
|
||||
pub(crate) dpi: f64,
|
||||
pub(crate) warnings: BTreeMap<String, usize>,
|
||||
nodes: usize,
|
||||
active: HashSet<ObjId>,
|
||||
/// Where the spreads go: the identity, or an embedding document's placement.
|
||||
base: Affine,
|
||||
@@ -521,8 +552,8 @@ pub(crate) struct Reader<'s, 'a, 'b> {
|
||||
pending: Option<Vec<Node>>,
|
||||
/// The master spreads, in the order of [`Document::masters`].
|
||||
master_ids: Vec<ObjId>,
|
||||
/// Pixels the pixel layers still to come may decode ([`MAX_LAYER_PIXELS`] in all).
|
||||
pub(crate) pixels_left: u64,
|
||||
/// What the rest of the read may use.
|
||||
pub(crate) budget: Budget,
|
||||
}
|
||||
|
||||
impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
@@ -533,14 +564,13 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
archive,
|
||||
dpi: 72.0,
|
||||
warnings: BTreeMap::new(),
|
||||
nodes: 0,
|
||||
active: HashSet::new(),
|
||||
base,
|
||||
type_base: Affine([a, b, c, d, 0.0, 0.0]),
|
||||
depth,
|
||||
pending: None,
|
||||
master_ids: Vec::new(),
|
||||
pixels_left: MAX_LAYER_PIXELS,
|
||||
budget: Budget::FULL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,8 +578,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
*self.warnings.entry(what.into()).or_default() += 1;
|
||||
}
|
||||
|
||||
/// The document, and the pixels its pixel layers left of the allowance.
|
||||
fn document(mut self) -> Result<(Document, u64), Error> {
|
||||
/// The document.
|
||||
fn document(&mut self) -> Result<Document, Error> {
|
||||
let s = self.s;
|
||||
let root = s.root;
|
||||
self.dpi = s.obj(root, b"UVCn").and_then(|u| s.f64(u, b"UPPI")).filter(|d| *d > 0.0 && *d < 100_000.0).unwrap_or(72.0);
|
||||
@@ -589,8 +619,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
.and_then(|p| s.bytes(p, b"Data"))
|
||||
.filter(|b| b.len() <= MAX_PROFILE_BYTES && b.get(16..20) == Some(b"CMYK".as_slice()))
|
||||
.map(<[u8]>::to_vec);
|
||||
let warnings = self.warnings.into_iter().map(|(w, n)| if n > 1 { format!("{w} ({n}×)") } else { w }).collect();
|
||||
Ok((Document { dpi: self.dpi, spreads, masters, saved_by, warnings, cmyk_profile }, self.pixels_left))
|
||||
let warnings = std::mem::take(&mut self.warnings).into_iter().map(|(w, n)| if n > 1 { format!("{w} ({n}×)") } else { w }).collect();
|
||||
Ok(Document { dpi: self.dpi, spreads, masters, saved_by, warnings, cmyk_profile })
|
||||
}
|
||||
|
||||
/// A spread (`Sprd`) of the document `doc`, or of its master spreads.
|
||||
@@ -650,10 +680,10 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
self.warn("layers nested deeper than 128 levels");
|
||||
return Ok(None);
|
||||
}
|
||||
self.nodes += 1;
|
||||
if self.nodes > MAX_NODES {
|
||||
let Some(left) = self.budget.nodes.checked_sub(1) else {
|
||||
return Err(Error::Limit("too many layers"));
|
||||
}
|
||||
};
|
||||
self.budget.nodes = left;
|
||||
// A node that contains itself (a shared object linked back into its own subtree).
|
||||
if !self.active.insert(id) {
|
||||
self.warn("a layer that contains itself");
|
||||
@@ -680,17 +710,16 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
} else {
|
||||
self.kind(id, class, world, parent)?
|
||||
};
|
||||
// This node's embedded content, before its children's `node` calls can take it.
|
||||
let embedded = self.pending.take();
|
||||
// A compound's children are its operands, already merged into its outline.
|
||||
let (mut children, child_mask) = if class == Tag::of(b"Comp") { (Vec::new(), None) } else { self.children(id, world, depth)? };
|
||||
if let Some(embedded) = self.pending.take() {
|
||||
if let Some(embedded) = embedded {
|
||||
children.extend(embedded);
|
||||
}
|
||||
// Pixel masks are read only over what they mask (they can span a whole spread at print
|
||||
// resolution, a gigapixel).
|
||||
let over = match (kind_bounds(&kind), nodes_bounds(&children)) {
|
||||
(Some(a), Some(b)) => Some(union(a, b)),
|
||||
(a, b) => a.or(b),
|
||||
};
|
||||
let over = mask_extent(&kind, &children);
|
||||
let child_mask = match child_mask {
|
||||
Some((c, at)) => crate::raster::mask(self, c, at, over)?,
|
||||
None => None,
|
||||
@@ -699,8 +728,8 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
let pixel_mask = attached_mask.or(child_mask);
|
||||
self.active.remove(&id);
|
||||
let mut node = Node { class, name, visible, locked, opacity, blend, kind, mask, pixel_mask, effects: Vec::new(), children };
|
||||
// Gradient overlays are laid out over the node's bounds.
|
||||
let bounds = nodes_bounds(std::slice::from_ref(&node));
|
||||
// Gradient overlays are laid out over the node's bounds, also while it is hidden.
|
||||
let bounds = tree_bounds(&node);
|
||||
node.effects = self.effects(id, world, bounds);
|
||||
Ok(Some(node))
|
||||
}
|
||||
@@ -925,20 +954,30 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
return None;
|
||||
}
|
||||
// A first reading finds its page and resolution, the second places it.
|
||||
let probe = match read_at(inner, self.archive.remaining(), self.pixels_left, Affine::IDENTITY, self.depth + 1) {
|
||||
Ok(r) => r,
|
||||
// Both readings are charged to the budget and the archive's limits, also when they fail:
|
||||
// each does the work.
|
||||
let mut budget = self.budget;
|
||||
let probe = read_at(inner, self.archive.remaining(), &mut budget, Affine::IDENTITY, self.depth + 1);
|
||||
self.budget = budget;
|
||||
if self.archive.charge(probe.extracted).is_err() {
|
||||
self.warn("embedded documents beyond the size limit (left out)");
|
||||
return None;
|
||||
}
|
||||
let probe = match probe.document {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
self.warn(format!("embedded documents that could not be read ({e})"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let used = probe.extracted;
|
||||
let spread = probe.document.spreads.first()?;
|
||||
let spread = probe.spreads.first()?;
|
||||
// `PBBx` 2 places the content's own bounds (as with a cached picture); otherwise its page.
|
||||
let content = s.obj(id, b"Bitm").and_then(|b| s.enumeration(b, b"PBBx")).is_some_and(|(k, _)| k == 2);
|
||||
let first_board = spread.nodes.iter().find_map(|n| if let Kind::Artboard { rect, .. } = &n.kind { Some(*rect) } else { None });
|
||||
let page = if content { None } else { spread.pages.first().copied().or(first_board) };
|
||||
let page = page.or_else(|| nodes_bounds(&spread.nodes)).unwrap_or(spread.bounds);
|
||||
// Its pixels aren't needed any more: never hold both readings' at once.
|
||||
drop(probe);
|
||||
// Its document pixels are the node's units, whatever either document's resolution.
|
||||
let k = 1.0;
|
||||
let (cx, cy) = ((page.x0 + page.x1) / 2.0, (page.y0 + page.y1) / 2.0);
|
||||
@@ -947,18 +986,14 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
self.warn("embedded documents with an invalid placement");
|
||||
return None;
|
||||
}
|
||||
if self.archive.charge(used).is_err() {
|
||||
self.warn("embedded documents beyond the size limit (left out)");
|
||||
return None;
|
||||
}
|
||||
let placed = read_at(inner, self.archive.remaining(), self.pixels_left, base, self.depth + 1).ok()?;
|
||||
// The probe's pixels were only looked at; the placed content's stay.
|
||||
self.pixels_left = placed.pixels_left;
|
||||
let doc = placed.document;
|
||||
let mut budget = self.budget;
|
||||
let placed = read_at(inner, self.archive.remaining(), &mut budget, base, self.depth + 1);
|
||||
self.budget = budget;
|
||||
if self.archive.charge(placed.extracted).is_err() {
|
||||
self.warn("embedded documents beyond the size limit (left out)");
|
||||
return None;
|
||||
}
|
||||
let doc = placed.document.ok()?;
|
||||
for w in doc.warnings {
|
||||
self.warn(format!("{w} [in embedded documents]"));
|
||||
}
|
||||
@@ -1005,22 +1040,54 @@ impl<'s, 'a, 'b> Reader<'s, 'a, 'b> {
|
||||
/// The bounds of what visible `nodes` draw (control points of outlines, corners of images, frames
|
||||
/// and tables), in document pixels.
|
||||
pub fn nodes_bounds(nodes: &[Node]) -> Option<Rect> {
|
||||
fn walk(nodes: &[Node], depth: usize, out: &mut Option<Rect>) {
|
||||
if depth > MAX_TREE_DEPTH {
|
||||
return;
|
||||
}
|
||||
for n in nodes.iter().filter(|n| n.visible) {
|
||||
kind_points(&n.kind, &mut |p| add_point(out, p));
|
||||
walk(&n.children, depth + 1, out);
|
||||
}
|
||||
}
|
||||
let mut out = None;
|
||||
walk(nodes, 0, &mut out);
|
||||
walk_bounds(nodes, true, 0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// The bounds of what one node's own kind draws (not its children).
|
||||
pub(crate) fn kind_bounds(kind: &Kind) -> Option<Rect> {
|
||||
/// The bounds of `node` and everything in it, hidden or not.
|
||||
fn tree_bounds(node: &Node) -> Option<Rect> {
|
||||
let mut out = None;
|
||||
walk_bounds(std::slice::from_ref(node), false, 0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn walk_bounds(nodes: &[Node], visible_only: bool, depth: usize, out: &mut Option<Rect>) {
|
||||
if depth > MAX_TREE_DEPTH {
|
||||
return;
|
||||
}
|
||||
for n in nodes.iter().filter(|n| n.visible || !visible_only) {
|
||||
kind_points(&n.kind, &mut |p| add_point(out, p));
|
||||
walk_bounds(&n.children, visible_only, depth + 1, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// What a pixel mask over a node of `kind` with `children` is read over: the bounds of what they
|
||||
/// draw, strokes included, or `None` (all of the mask) when some of it has no bounds known here:
|
||||
/// artistic text, whose glyphs only the layout places.
|
||||
fn mask_extent(kind: &Kind, children: &[Node]) -> Option<Rect> {
|
||||
fn visit(kind: &Kind, children: &[Node], depth: usize, out: &mut Option<Rect>) -> bool {
|
||||
if depth > MAX_TREE_DEPTH || matches!(kind, Kind::Text(t) if t.frame.is_none()) {
|
||||
return false;
|
||||
}
|
||||
let mut own = kind_bounds(kind);
|
||||
if let (Some(r), Kind::Shape { strokes, .. } | Kind::Artboard { strokes, .. }) = (own, kind) {
|
||||
// As far as a stroke can reach out of its path, miter joins included.
|
||||
let reach = strokes.iter().map(|s| s.width * s.miter_limit.clamp(1.0, 10.0)).filter(|w| w.is_finite()).fold(0.0, f64::max);
|
||||
own = Some(Rect { x0: r.x0 - reach, y0: r.y0 - reach, x1: r.x1 + reach, y1: r.y1 + reach });
|
||||
}
|
||||
if let Some(r) = own {
|
||||
*out = Some(out.map_or(r, |o| union(o, r)));
|
||||
}
|
||||
children.iter().filter(|n| n.visible).all(|n| visit(&n.kind, &n.children, depth + 1, out))
|
||||
}
|
||||
let mut out = None;
|
||||
if visit(kind, children, 0, &mut out) { out } else { None }
|
||||
}
|
||||
|
||||
/// The bounds of what one node's own kind draws (not its children): its outline's points, its
|
||||
/// image's or frame's corners, or artistic text's anchor.
|
||||
pub fn kind_bounds(kind: &Kind) -> Option<Rect> {
|
||||
let mut out = None;
|
||||
kind_points(kind, &mut |p| add_point(&mut out, p));
|
||||
out
|
||||
|
||||
@@ -17,6 +17,11 @@ const MAX_DEPTH: usize = 256;
|
||||
/// Most pixels an overlay is baked into per image.
|
||||
const MAX_PIXELS: usize = 64 << 20;
|
||||
|
||||
/// Work the overlays of one import may take together: each overlay counts the nodes and pixels
|
||||
/// of the subtree it is baked into (which is copied once more for them all). Nested overlays
|
||||
/// and shared content would otherwise multiply it; past it, overlays are left out and said.
|
||||
pub const MAX_WORK: u64 = 1 << 29;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Source<'a> {
|
||||
Solid(Color),
|
||||
@@ -30,9 +35,10 @@ struct Overlay<'a> {
|
||||
blend: Blend,
|
||||
}
|
||||
|
||||
/// `node` with its colour and gradient overlays baked in (borrowed unchanged when it has none).
|
||||
/// `warn` hears what could only be approximated.
|
||||
pub fn apply<'a>(node: &'a Node, warn: &mut dyn FnMut(&str)) -> Cow<'a, Node> {
|
||||
/// `node` with its colour and gradient overlays baked in (borrowed unchanged when it has none,
|
||||
/// or when what is left of `work`, see [`MAX_WORK`], can't pay for them). `warn` hears what
|
||||
/// could only be approximated or was left out.
|
||||
pub fn apply<'a>(node: &'a Node, work: &mut u64, warn: &mut dyn FnMut(&str)) -> Cow<'a, Node> {
|
||||
let overlays: Vec<Overlay> = node
|
||||
.effects
|
||||
.iter()
|
||||
@@ -48,6 +54,13 @@ pub fn apply<'a>(node: &'a Node, warn: &mut dyn FnMut(&str)) -> Cow<'a, Node> {
|
||||
if overlays.is_empty() {
|
||||
return Cow::Borrowed(node);
|
||||
}
|
||||
let copies = u64::try_from(overlays.len()).unwrap_or(u64::MAX).saturating_add(1);
|
||||
let cost = subtree_cost(node, *work / copies, 0).saturating_mul(copies);
|
||||
let Some(left) = work.checked_sub(cost) else {
|
||||
warn("overlay effects beyond the work one import may take (left out)");
|
||||
return Cow::Borrowed(node);
|
||||
};
|
||||
*work = left;
|
||||
let mut out = node.clone();
|
||||
out.effects.retain(|e| !matches!(e, Effect::ColorOverlay { .. } | Effect::GradientOverlay { .. }));
|
||||
for o in &overlays {
|
||||
@@ -59,6 +72,26 @@ pub fn apply<'a>(node: &'a Node, warn: &mut dyn FnMut(&str)) -> Cow<'a, Node> {
|
||||
Cow::Owned(out)
|
||||
}
|
||||
|
||||
/// Nodes and pixels in `node`'s subtree, counted only until they pass `limit`.
|
||||
fn subtree_cost(node: &Node, limit: u64, depth: usize) -> u64 {
|
||||
if depth > MAX_DEPTH {
|
||||
return 0;
|
||||
}
|
||||
let mut cost: u64 = match &node.kind {
|
||||
Kind::Image(img) => (u64::from(img.width) * u64::from(img.height)).saturating_add(1),
|
||||
Kind::Text(t) => u64::try_from(t.runs.len()).unwrap_or(u64::MAX).saturating_add(1),
|
||||
Kind::Table(t) => u64::try_from(t.cells.len()).unwrap_or(u64::MAX).saturating_add(1),
|
||||
_ => 1,
|
||||
};
|
||||
for c in &node.children {
|
||||
if cost > limit {
|
||||
break;
|
||||
}
|
||||
cost = cost.saturating_add(subtree_cost(c, limit - cost, depth + 1));
|
||||
}
|
||||
cost
|
||||
}
|
||||
|
||||
fn bake_node(node: &mut Node, o: &Overlay, depth: usize, warn: &mut dyn FnMut(&str)) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
@@ -88,6 +121,9 @@ fn bake_node(node: &mut Node, o: &Overlay, depth: usize, warn: &mut dyn FnMut(&s
|
||||
for c in &mut t.cells {
|
||||
for r in &mut c.runs {
|
||||
r.fill = paint(&r.fill, o, warn);
|
||||
if let Some(s) = &mut r.stroke {
|
||||
stroke(s, o, warn);
|
||||
}
|
||||
}
|
||||
}
|
||||
for s in t.vertical.iter_mut().chain(t.horizontal.iter_mut()).flatten() {
|
||||
@@ -236,9 +272,9 @@ fn pixels(img: &mut Image, o: &Overlay) -> bool {
|
||||
Some(p) => p.to_vec(),
|
||||
None => return false,
|
||||
},
|
||||
Pixels::Encoded(bytes) => match image::load_from_memory(bytes) {
|
||||
Ok(d) if d.width() == img.width && d.height() == img.height => d.to_rgba8().into_raw(),
|
||||
_ => return false,
|
||||
Pixels::Encoded(bytes) => match decode(bytes, img.width, img.height) {
|
||||
Some(p) => p,
|
||||
None => return false,
|
||||
},
|
||||
// An overlay is blended in RGB: CMYK pixels take the profile-free formula here.
|
||||
Pixels::Cmyka8(p) => match p.get(..n.saturating_mul(5)) {
|
||||
@@ -292,6 +328,21 @@ fn pixels(img: &mut Image, o: &Overlay) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// An encoded original's RGBA pixels, decoded only if it is `width` × `height` as its layer says.
|
||||
fn decode(bytes: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
|
||||
let probe = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
||||
if probe.into_dimensions().ok()? != (width, height) {
|
||||
return None;
|
||||
}
|
||||
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_image_width = Some(width);
|
||||
limits.max_image_height = Some(height);
|
||||
limits.max_alloc = u64::try_from(MAX_PIXELS).ok().map(|n| n * 8);
|
||||
reader.limits(limits);
|
||||
Some(reader.decode().ok()?.into_rgba8().into_raw())
|
||||
}
|
||||
|
||||
/// The gradient's colour at unit-space point `q`.
|
||||
fn sample(g: &Gradient, q: Point) -> Color {
|
||||
let t = match g.kind {
|
||||
@@ -495,11 +546,11 @@ mod tests {
|
||||
#[test]
|
||||
fn a_full_colour_overlay_replaces_the_fill_and_keeps_its_alpha() {
|
||||
let white = Color::Rgb { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
|
||||
let n = apply(&solid(white, 1.0, Blend::Normal), &mut |_| {}).into_owned();
|
||||
let n = apply(&solid(white, 1.0, Blend::Normal), &mut { MAX_WORK }, &mut |_| {}).into_owned();
|
||||
assert_eq!(fill(&n), white);
|
||||
assert!(n.effects.is_empty(), "the overlay is baked, not kept");
|
||||
// Half way, mixed in RGB.
|
||||
let n = apply(&solid(white, 0.5, Blend::Normal), &mut |_| {}).into_owned();
|
||||
let n = apply(&solid(white, 0.5, Blend::Normal), &mut { MAX_WORK }, &mut |_| {}).into_owned();
|
||||
assert_eq!(fill(&n), Color::Rgb { r: 1.0, g: 0.75, b: 0.5, a: 1.0 });
|
||||
}
|
||||
|
||||
@@ -519,7 +570,7 @@ mod tests {
|
||||
fn overlays_recolour_pixels_inside_their_alpha() {
|
||||
let mut n = solid(Color::Rgb { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, 1.0, Blend::Normal);
|
||||
n.kind = Kind::Image(Image { width: 2, height: 1, pixels: Pixels::Rgba8(vec![255, 0, 0, 255, 255, 0, 0, 0]), transform: Affine::IDENTITY });
|
||||
let out = apply(&n, &mut |_| {}).into_owned();
|
||||
let out = apply(&n, &mut { MAX_WORK }, &mut |_| {}).into_owned();
|
||||
let Kind::Image(img) = &out.kind else { panic!() };
|
||||
assert_eq!(img.pixels, Pixels::Rgba8(vec![0, 0, 255, 255, 255, 0, 0, 0]));
|
||||
}
|
||||
@@ -536,8 +587,22 @@ mod tests {
|
||||
};
|
||||
let mut n = solid(Color::Rgb { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 1.0, Blend::Normal);
|
||||
n.effects = vec![Effect::GradientOverlay { gradient: g.clone(), opacity: 1.0, blend: Blend::Normal }];
|
||||
let out = apply(&n, &mut |_| {}).into_owned();
|
||||
let out = apply(&n, &mut { MAX_WORK }, &mut |_| {}).into_owned();
|
||||
let Kind::Shape { fills, .. } = &out.kind else { panic!() };
|
||||
assert_eq!(fills.first(), Some(&Paint::Gradient(g)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlays_stop_at_the_work_one_import_may_take() {
|
||||
let mut n = solid(Color::Rgb { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, 1.0, Blend::Normal);
|
||||
n.kind = Kind::Image(Image { width: 4, height: 4, pixels: Pixels::Rgba8(vec![255; 64]), transform: Affine::IDENTITY });
|
||||
// 16 pixels and the node, for the copy and for the overlay: 34.
|
||||
let mut work = 40;
|
||||
let mut warnings = Vec::new();
|
||||
assert!(matches!(apply(&n, &mut work, &mut |w| warnings.push(w.to_string())), Cow::Owned(_)));
|
||||
assert_eq!((work, warnings.len()), (6, 0));
|
||||
assert!(matches!(apply(&n, &mut work, &mut |w| warnings.push(w.to_string())), Cow::Borrowed(_)));
|
||||
assert_eq!(work, 6);
|
||||
assert!(warnings.iter().any(|w| w.contains("beyond the work")), "{warnings:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,11 +168,11 @@ fn decode_within(r: &mut Reader, bitmap: ObjId, crop: (u32, u32, u32, u32), worl
|
||||
}
|
||||
// Before anything is allocated: a layer's tiles can be flat colour, with nothing to extract.
|
||||
let pixels = u64::from(w.div_ceil(step)) * u64::from(h.div_ceil(step));
|
||||
let Some(left) = r.pixels_left.checked_sub(pixels) else {
|
||||
let Some(left) = r.budget.pixels.checked_sub(pixels) else {
|
||||
r.warn("pixel layers beyond the pixels one document may hold");
|
||||
return Ok(None);
|
||||
};
|
||||
r.pixels_left = left;
|
||||
r.budget.pixels = left;
|
||||
let format = s.enumeration(bitmap, b"Frmt").map(|(f, _)| f);
|
||||
let (channels, bps) = match format {
|
||||
Some(0) => (4, 1),
|
||||
|
||||
+48
-12
@@ -48,6 +48,8 @@ pub(crate) fn read(r: &mut Reader, id: ObjId, world: Affine) -> Option<Text> {
|
||||
|
||||
/// Rows and columns a table may have.
|
||||
const MAX_TABLE_LINES: usize = 4096;
|
||||
/// Cells a table may have (rows × columns): each becomes a text object with its edge lines.
|
||||
const MAX_TABLE_CELLS: usize = 1 << 16;
|
||||
|
||||
/// A table node (`TxtT`): the grid, the cells' text and the lines on the cell edges.
|
||||
pub(crate) fn table(r: &mut Reader, id: ObjId, world: Affine) -> Option<Table> {
|
||||
@@ -63,10 +65,24 @@ pub(crate) fn table(r: &mut Reader, id: ObjId, world: Affine) -> Option<Table> {
|
||||
let rows = bounds(s.obj(tabl, b"RPos"))?;
|
||||
let (ncols, nrows) = (columns.len() - 1, rows.len() - 1);
|
||||
let count = ncols.checked_mul(nrows)?;
|
||||
let cell_objs = s.obj(tabl, b"Cell").map(|c| s.objs(c, b"Cell")).unwrap_or_default();
|
||||
if cell_objs.len() != count {
|
||||
r.warn("tables whose cells don't match their grid (cell formatting left out)");
|
||||
if count > MAX_TABLE_CELLS {
|
||||
r.warn("tables with more than 65536 cells (left out)");
|
||||
return None;
|
||||
}
|
||||
let Some(left) = r.budget.cells.checked_sub(count) else {
|
||||
r.warn("tables beyond the cells one document may hold (left out)");
|
||||
return None;
|
||||
};
|
||||
r.budget.cells = left;
|
||||
let cell_objs = s.obj(tabl, b"Cell").map(|c| s.objs(c, b"Cell")).unwrap_or_default();
|
||||
// Formatting that doesn't match the grid would land on the wrong cells (and merging them,
|
||||
// move text between cells): only text then.
|
||||
let cell_objs = if cell_objs.len() == count {
|
||||
cell_objs
|
||||
} else {
|
||||
r.warn("tables whose cells don't match their grid (cell formatting left out)");
|
||||
Vec::new()
|
||||
};
|
||||
let blank = || TableCell { runs: Vec::new(), inset: [0.0; 4], valign: VAlign::Top, merged_left: false };
|
||||
let mut cells: Vec<TableCell> = (0..count).map(|_| blank()).collect();
|
||||
for (i, (cell, obj)) in cells.iter_mut().zip(&cell_objs).enumerate() {
|
||||
@@ -109,7 +125,7 @@ pub(crate) fn table(r: &mut Reader, id: ObjId, world: Affine) -> Option<Table> {
|
||||
let objs = s.obj(tabl, tag).map(|e| s.objs(e, b"Edge")).unwrap_or_default();
|
||||
if objs.len() != expected {
|
||||
r.warn("tables whose lines don't match their grid (lines left out)");
|
||||
return vec![None; expected];
|
||||
return Vec::new();
|
||||
}
|
||||
objs.into_iter()
|
||||
.map(|e| match (s.obj(e, b"Fill"), s.obj(e, b"Line")) {
|
||||
@@ -145,8 +161,9 @@ pub(crate) fn story_runs(r: &mut Reader, story: ObjId, world: Affine) -> (Vec<Te
|
||||
r.warn("a text block without readable characters (left out)");
|
||||
continue;
|
||||
};
|
||||
// Bound collection itself, including placeholders, before allocating the character buffer.
|
||||
let budget = MAX_CHARS - total;
|
||||
// Bound collection itself, including placeholders, before allocating the character buffer:
|
||||
// by the story's limit and by what the document's other stories left.
|
||||
let budget = (MAX_CHARS - total).min(r.budget.chars.saturating_sub(total));
|
||||
|
||||
let mut chars: Vec<char> = match s.str(glyphs, b"Utf8") {
|
||||
Some(t) => t.chars().take(budget + 1).collect(),
|
||||
@@ -182,7 +199,11 @@ pub(crate) fn story_runs(r: &mut Reader, story: ObjId, world: Affine) -> (Vec<Te
|
||||
let truncated = chars.len() > budget;
|
||||
if truncated {
|
||||
chars.truncate(budget);
|
||||
r.warn("text longer than a million characters (truncated)");
|
||||
if budget < MAX_CHARS - total {
|
||||
r.warn("text beyond the characters one document may hold (truncated)");
|
||||
} else {
|
||||
r.warn("text longer than a million characters (truncated)");
|
||||
}
|
||||
}
|
||||
total += chars.len();
|
||||
// Paragraph formats by end index; a run without an index covers the rest of the block.
|
||||
@@ -201,14 +222,17 @@ pub(crate) fn story_runs(r: &mut Reader, story: ObjId, world: Affine) -> (Vec<Te
|
||||
},
|
||||
}
|
||||
}
|
||||
let align_at = |i: usize| paragraphs.iter().find(|(end, _)| i < *end).map(|(_, a)| *a).or(open).unwrap_or_default();
|
||||
// Sorted by end (they are in order in every file), so both lookups below are binary
|
||||
// searches: a block can have as many paragraphs and runs as characters.
|
||||
paragraphs.sort_by_key(|(end, _)| *end);
|
||||
let align_at = |i: usize| paragraphs.get(paragraphs.partition_point(|(end, _)| *end <= i)).map(|(_, a)| *a).or(open).unwrap_or_default();
|
||||
let mut cuts: Vec<usize> = paragraphs.iter().map(|(end, _)| *end).filter(|e| *e < chars.len()).collect();
|
||||
cuts.sort_unstable();
|
||||
cuts.dedup();
|
||||
let push = |r: &mut Reader, runs: &mut Vec<TextRun>, attrs: Option<ObjId>, from: usize, to: usize, budget: &mut Option<usize>| {
|
||||
// Split at paragraph ends so every run has one paragraph format.
|
||||
let inside = cuts.get(cuts.partition_point(|c| *c <= from)..cuts.partition_point(|c| *c < to)).unwrap_or_default();
|
||||
let mut a = from;
|
||||
for &c in cuts.iter().filter(|c| **c > from && **c < to).chain(std::iter::once(&to)) {
|
||||
for &c in inside.iter().chain(std::iter::once(&to)) {
|
||||
push_run(r, runs, attrs, characters(&chars, a, c), world, budget, align_at(a));
|
||||
a = c;
|
||||
}
|
||||
@@ -236,6 +260,7 @@ pub(crate) fn story_runs(r: &mut Reader, story: ObjId, world: Affine) -> (Vec<Te
|
||||
break;
|
||||
}
|
||||
}
|
||||
r.budget.chars = r.budget.chars.saturating_sub(total);
|
||||
if placeholders {
|
||||
r.warn("text fields such as page numbers (left out of the text)");
|
||||
}
|
||||
@@ -370,8 +395,8 @@ fn run_attrs(r: &mut Reader, attrs: ObjId, text: String, world: Affine, attribut
|
||||
if font.and_then(|f| s.int(f, b"Widh")).is_some_and(|w| w != 5) {
|
||||
r.warn("font width variants (exact face depends on the installed PostScript font)");
|
||||
}
|
||||
let postscript = metadata_string(r, attribute_budget, font.and_then(|f| s.str(f, b"Post")).unwrap_or_default());
|
||||
let family = metadata_string(r, attribute_budget, font.and_then(|f| s.str(f, b"Famy")).unwrap_or_default());
|
||||
let postscript = font_name(r, attribute_budget, font.and_then(|f| s.str(f, b"Post")).unwrap_or_default());
|
||||
let family = font_name(r, attribute_budget, font.and_then(|f| s.str(f, b"Famy")).unwrap_or_default());
|
||||
let (features, all_caps) = font_features(r, objects.get(7).copied().flatten(), attribute_budget);
|
||||
let weight = font.and_then(|f| s.int(f, b"Wegt")).unwrap_or(400);
|
||||
let weight = if (1..=1000).contains(&weight) {
|
||||
@@ -415,6 +440,17 @@ fn charge_attributes(r: &mut Reader, budget: &mut Option<usize>, bytes: usize) -
|
||||
}
|
||||
}
|
||||
|
||||
/// Longest font name kept (OpenType names are far shorter); every run and paragraph carries it.
|
||||
const MAX_FONT_NAME: usize = 256;
|
||||
|
||||
fn font_name(r: &mut Reader, budget: &mut Option<usize>, value: &str) -> String {
|
||||
if value.len() > MAX_FONT_NAME {
|
||||
r.warn("font names longer than 256 bytes (the default font is used)");
|
||||
return String::new();
|
||||
}
|
||||
metadata_string(r, budget, value)
|
||||
}
|
||||
|
||||
fn metadata_string(r: &mut Reader, budget: &mut Option<usize>, value: &str) -> String {
|
||||
let bytes = value.len().saturating_add(std::mem::size_of::<String>());
|
||||
if value.is_empty() || !charge_attributes(r, budget, bytes) { String::new() } else { value.to_owned() }
|
||||
|
||||
Reference in New Issue
Block a user