mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-14 14:22:14 +00:00
* EPS: bound copies, storage, text outlines, group nesting and font discovery The PostScript interpreter's numeric copy grew the operand stack past its limit and charged nothing for the values copied; array and string constructors retained storage without charging it; show and charpath copied every glyph outline into the text path before any path or budget check; and the font pre-scan searched the whole prefix again for every eexec, as the bounding-box scan did for every rejected marker on a line. Clipping groups also nested without limit. Copies past the operand limit copy nothing; copies, arrays and strings count against the operation budget; each outline is charged and checked against the path limit before it is copied; headers are found in one pass and each is tried once; a rejected bounding-box comment is skipped to its line end; and the output builder drops groups past 256 open ones. * address review: EPS font look-back stops at the previous font; charge input characters * address review: count enclosing groups against the nesting limit in pattern and mask builders * address review: close a replaced soft mask before nesting the new one; skip patterns whose wrappers pass the depth limit
142 lines
5.1 KiB
Rust
142 lines
5.1 KiB
Rust
//! The output side shared by the PDF and PostScript interpreters: an open-group stack (clipping
|
|
//! groups, optional-content layers) that collects shapes into the SVG render tree.
|
|
|
|
use effectcraft_svg::{Affine, BezPath, FillRule, Geom, Group, Node, Paint, Shape, Stroke};
|
|
|
|
/// The most elements a path being built holds: an operator that would add more stops the
|
|
/// PostScript program, or the PDF content stream.
|
|
pub(crate) const MAX_PATH: usize = 1 << 20;
|
|
|
|
/// The deepest the open groups nest (the render tree and its consumers recurse through them).
|
|
const MAX_DEPTH: usize = 256;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum Kind {
|
|
Plain,
|
|
Clip,
|
|
Layer,
|
|
}
|
|
|
|
pub(crate) struct Builder {
|
|
stack: Vec<(Group, Kind)>,
|
|
/// Which of the finished top-level nodes are optional-content layers.
|
|
pub top_layers: Vec<bool>,
|
|
pub skipped: Vec<String>,
|
|
pub shapes: usize,
|
|
/// Groups the finished nodes will sit under once placed (an enclosing builder's open groups
|
|
/// and the pattern or mask groups around them), counted against [`MAX_DEPTH`].
|
|
base: usize,
|
|
}
|
|
|
|
pub(crate) fn group(name: &str) -> Group {
|
|
Group::new(name)
|
|
}
|
|
|
|
impl Builder {
|
|
pub fn new() -> Builder {
|
|
Builder { stack: vec![(group("page"), Kind::Plain)], top_layers: vec![], skipped: vec![], shapes: 0, base: 0 }
|
|
}
|
|
|
|
/// A builder for content that will be placed inside this one's open groups and `wrap` more
|
|
/// groups (a pattern's `Pattern` and `Tile`, a soft mask's two), so its nesting limit counts
|
|
/// them.
|
|
pub fn nested(&self, wrap: usize) -> Builder {
|
|
Builder { base: self.base.saturating_add(self.stack.len()).saturating_add(wrap), ..Builder::new() }
|
|
}
|
|
|
|
pub fn depth(&self) -> usize {
|
|
self.stack.len()
|
|
}
|
|
|
|
/// `wrap` more groups fit under the open ones within [`MAX_DEPTH`].
|
|
pub fn has_room(&self, wrap: usize) -> bool {
|
|
self.base.saturating_add(self.stack.len()).saturating_add(wrap) <= MAX_DEPTH
|
|
}
|
|
|
|
pub fn skip(&mut self, what: &str) {
|
|
if !self.skipped.iter().any(|s| s == what) {
|
|
self.skipped.push(what.to_string());
|
|
}
|
|
}
|
|
|
|
fn add(&mut self, n: Node, layer: bool) {
|
|
if self.stack.len() == 1 {
|
|
self.top_layers.push(layer);
|
|
}
|
|
if let Some((g, _)) = self.stack.last_mut() {
|
|
g.children.push(n);
|
|
}
|
|
}
|
|
|
|
/// Close every group opened above `depth`.
|
|
pub fn close_to(&mut self, depth: usize) {
|
|
while self.stack.len() > depth.max(1) {
|
|
let Some((g, k)) = self.stack.pop() else { break };
|
|
// Empty clipping groups draw nothing.
|
|
if g.children.is_empty() {
|
|
continue;
|
|
}
|
|
self.add(Node::Group(g), k == Kind::Layer);
|
|
}
|
|
}
|
|
|
|
/// Open `g` as the group drawn into from now on; past [`MAX_DEPTH`] the group is dropped
|
|
/// (what would have been drawn inside it goes to the open group) and noted as skipped.
|
|
fn push_group(&mut self, g: Group, kind: Kind) {
|
|
if self.base.saturating_add(self.stack.len()) >= MAX_DEPTH {
|
|
self.skip("group nesting limit");
|
|
return;
|
|
}
|
|
self.stack.push((g, kind));
|
|
}
|
|
|
|
/// Children drawn from now on are clipped by `path` (page space) until the group closes.
|
|
pub fn clip(&mut self, path: BezPath, rule: FillRule) {
|
|
let mut g = group("Clip Group");
|
|
g.clip.push((path, rule));
|
|
// Clipping is not a transparency group: blend modes inside reach the backdrop.
|
|
g.isolated = false;
|
|
self.push_group(g, Kind::Clip);
|
|
}
|
|
|
|
/// Open an optional-content layer (or a named group).
|
|
pub fn open(&mut self, name: &str, layer: bool) {
|
|
self.push_group(group(name), if layer { Kind::Layer } else { Kind::Plain });
|
|
}
|
|
|
|
/// Open a group (blend mode, soft mask) closed with the graphics state.
|
|
pub fn open_group(&mut self, g: Group) {
|
|
self.push_group(g, Kind::Plain);
|
|
}
|
|
|
|
/// Add a finished node (an image, pattern tiles).
|
|
pub fn push(&mut self, n: Node) {
|
|
self.add(n, false);
|
|
}
|
|
|
|
pub fn fill_stroke(&mut self, path: BezPath, transform: Affine, fill: Option<(Paint, f64, FillRule)>, stroke: Option<Stroke>) {
|
|
self.fill_stroke_named("Path", path, transform, fill, stroke);
|
|
}
|
|
|
|
pub fn fill_stroke_named(&mut self, name: &str, path: BezPath, transform: Affine, fill: Option<(Paint, f64, FillRule)>, stroke: Option<Stroke>) {
|
|
if path.elements().is_empty() || (fill.is_none() && stroke.is_none()) {
|
|
return;
|
|
}
|
|
self.shapes += 1;
|
|
let (f, fo, rule) = match fill {
|
|
Some((p, o, r)) => (Some(p), o, r),
|
|
None => (None, 1.0, FillRule::NonZero),
|
|
};
|
|
let s = Shape { name: name.into(), geom: Geom::Path(path), transform, fill: f, fill_opacity: fo, fill_rule: rule, stroke, opacity: 1.0 };
|
|
self.add(Node::Shape(s), false);
|
|
}
|
|
|
|
/// The finished top-level nodes and their layer flags.
|
|
pub fn finish(mut self) -> (Vec<Node>, Vec<bool>, Vec<String>) {
|
|
self.close_to(1);
|
|
let root = self.stack.pop().map(|(g, _)| g.children).unwrap_or_default();
|
|
let flags = self.top_layers;
|
|
(root, flags, self.skipped)
|
|
}
|
|
}
|