257 lines
10 KiB
Rust
257 lines
10 KiB
Rust
//! Outlines of curves (`PCvD` node lists), parametric shapes (`Shpe` objects) and compounds, in
|
|
//! node space.
|
|
|
|
use crate::model::{Affine, Path, Point, Reader, SubPath};
|
|
use crate::stream::{ObjId, Tag, Value};
|
|
|
|
/// Nodes per curve object.
|
|
const MAX_NODES: usize = 4_000_000;
|
|
/// A shared-object stream is bounded, but a compound graph can expand exponentially. Count
|
|
/// each visit and each curve record / generated contour / transformed segment across the whole
|
|
/// outline, including repeated references. Match the document reader's nesting limit.
|
|
const MAX_OUTLINE_DEPTH: usize = 128;
|
|
const MAX_OUTLINE_VISITS: usize = 100_000;
|
|
|
|
struct OutlineBudget {
|
|
active: Vec<ObjId>,
|
|
visits: usize,
|
|
work: usize,
|
|
failed: bool,
|
|
}
|
|
|
|
impl OutlineBudget {
|
|
fn fail(&mut self, r: &mut Reader, warning: &'static str) -> Option<()> {
|
|
if !self.failed {
|
|
r.warn(warning);
|
|
}
|
|
self.failed = true;
|
|
None
|
|
}
|
|
|
|
fn spend(&mut self, r: &mut Reader, work: usize) -> Option<()> {
|
|
if work > self.work {
|
|
return self.fail(r, "compound outlines exceeding the geometry work limit");
|
|
}
|
|
self.work -= work;
|
|
Some(())
|
|
}
|
|
|
|
fn path(&mut self, r: &mut Reader, path: &Path) -> Option<()> {
|
|
for subpath in &path.subpaths {
|
|
self.spend(r, 1)?;
|
|
self.spend(r, subpath.segments.len())?;
|
|
}
|
|
Some(())
|
|
}
|
|
}
|
|
|
|
/// The node-space outline of a curve, shape or compound node, and whether it fills even-odd.
|
|
/// Instances (master-page or symbol copies) take the geometry of their master.
|
|
pub(crate) fn outline(r: &mut Reader, id: ObjId, class: Tag, world: Affine) -> Option<(Path, bool)> {
|
|
let mut budget = OutlineBudget { active: Vec::new(), visits: MAX_OUTLINE_VISITS, work: MAX_NODES, failed: false };
|
|
bounded_outline(r, id, class, world, &mut budget)
|
|
}
|
|
|
|
fn bounded_outline(r: &mut Reader, id: ObjId, class: Tag, world: Affine, budget: &mut OutlineBudget) -> Option<(Path, bool)> {
|
|
if budget.active.contains(&id) {
|
|
budget.fail(r, "a compound outline that contains itself")?;
|
|
}
|
|
if budget.active.len() >= MAX_OUTLINE_DEPTH {
|
|
budget.fail(r, "compound outlines nested deeper than 128 levels")?;
|
|
}
|
|
if budget.visits == 0 {
|
|
budget.fail(r, "compound outlines exceeding the operand visit limit")?;
|
|
}
|
|
budget.visits -= 1;
|
|
budget.active.push(id);
|
|
let result = outline_inner(r, id, class, world, budget);
|
|
// The popped value is our own stack marker, and is no longer needed.
|
|
let _ = budget.active.pop();
|
|
result
|
|
}
|
|
|
|
fn outline_inner(r: &mut Reader, id: ObjId, class: Tag, world: Affine, budget: &mut OutlineBudget) -> Option<(Path, bool)> {
|
|
let s = r.s;
|
|
match &class.0.to_be_bytes() {
|
|
b"PCrv" | b"TxtC" => {
|
|
let crvs = s.obj(id, b"Crvs").or_else(|| s.obj(id, b"MCrM").and_then(|m| s.obj(m, b"Crvs")))?;
|
|
let path = curves(r, crvs, budget)?;
|
|
budget.path(r, &path)?;
|
|
let multi = path.subpaths.len() > 1;
|
|
Some((path, multi))
|
|
}
|
|
b"Comp" => {
|
|
// One path of all the operands' contours in the compound's space, filled even-odd: this
|
|
// matches add and subtract operands in every public sample; a true union of overlapping
|
|
// added operands would differ.
|
|
let mut path = Path::default();
|
|
for c in s.objs(id, b"Chld") {
|
|
let local = s.floats::<6>(c, b"Xfrm").map(Affine::from_xfrm).unwrap_or(Affine::IDENTITY);
|
|
let class = s.class(c)?;
|
|
if s.enumeration(c, b"ComO").is_some_and(|(op, _)| op != 0 && op != 2) {
|
|
r.warn("compound shapes that intersect or exclude (imported as add)");
|
|
}
|
|
let child_world = local.then(world);
|
|
if !child_world.is_finite() {
|
|
budget.fail(r, "a compound operand with an invalid transform")?;
|
|
}
|
|
if let Some((mut p, _)) = bounded_outline(r, c, class, child_world, budget) {
|
|
// Charge before transforming or extending. Transform the owned path in place
|
|
// so nested compounds do not make an additional copy of every contour.
|
|
budget.path(r, &p)?;
|
|
for subpath in &mut p.subpaths {
|
|
subpath.start = local.apply(subpath.start);
|
|
for segment in &mut subpath.segments {
|
|
*segment = segment.map(|point| local.apply(point));
|
|
}
|
|
}
|
|
path.subpaths.extend(p.subpaths);
|
|
} else if budget.failed {
|
|
// Never present a partial compound as complete after a cycle or budget limit.
|
|
return None;
|
|
}
|
|
}
|
|
Some((path, true))
|
|
}
|
|
_ => {
|
|
let (shape, box_owner) = match s.obj(id, b"Shpe") {
|
|
Some(sh) => (sh, id),
|
|
None => {
|
|
let master = s.obj(id, b"ShpM")?;
|
|
(s.obj(master, b"Shpe")?, master)
|
|
}
|
|
};
|
|
let b = s.floats::<4>(box_owner, b"ShpB")?;
|
|
let path = crate::shapes::shape(r, shape, b, world)?;
|
|
budget.path(r, &path)?;
|
|
Some((path, false))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `PCvD.Data`: positional `[u8, u32 count, then per subpath: bool closed, 18-byte records]`.
|
|
/// A record is (f64 x, f64 y, u8 kind, u8 role): role 0 is an anchor, 1 the out-handle of the
|
|
/// anchor before it, 2 the in-handle of the anchor after it. A closed subpath repeats its first
|
|
/// anchor at the end. `CnrD` live corners are rounded on the stored polyline.
|
|
fn curves(r: &mut Reader, crvs: ObjId, budget: &mut OutlineBudget) -> Option<Path> {
|
|
let s = r.s;
|
|
let data = s.obj(crvs, b"Data")?;
|
|
let mut fields = s.positional(data);
|
|
fields.next()?;
|
|
let count = match fields.next()? {
|
|
Value::UInt(n) => usize::try_from(*n).ok()?,
|
|
_ => return None,
|
|
};
|
|
budget.spend(r, count)?;
|
|
let corners = s.obj(crvs, b"CnrD").and_then(|c| corner_radii(s.positional(c).collect()));
|
|
let mut path = Path::default();
|
|
let mut seen = 0usize;
|
|
for index in 0..count {
|
|
let closed = match fields.next()? {
|
|
Value::Bool(b) => *b,
|
|
_ => return None,
|
|
};
|
|
let Value::Array(records) = fields.next()? else { return None };
|
|
seen = seen.checked_add(records.len())?;
|
|
if seen > MAX_NODES {
|
|
budget.fail(r, "curves with more than four million nodes")?;
|
|
}
|
|
budget.spend(r, records.len())?;
|
|
let anchors = anchors(records)?;
|
|
let radii = if index == 0 { corners.as_ref() } else { None };
|
|
if let Some(sp) = subpath(&anchors, closed, radii) {
|
|
path.subpaths.push(sp);
|
|
}
|
|
}
|
|
if corners.is_some() && count > 1 {
|
|
r.warn("rounded corners on curves with several subpaths (only the first is rounded)");
|
|
}
|
|
Some(path)
|
|
}
|
|
|
|
struct Anchor {
|
|
p: Point,
|
|
inh: Option<Point>,
|
|
out: Option<Point>,
|
|
/// Index of the anchor's record (live corners refer to records).
|
|
record: usize,
|
|
}
|
|
|
|
fn record(v: &Value) -> Option<(Point, u8)> {
|
|
let Value::Bytes(b) = v else { return None };
|
|
if b.len() != 18 {
|
|
return None;
|
|
}
|
|
let x = f64::from_le_bytes(b.get(0..8)?.try_into().ok()?);
|
|
let y = f64::from_le_bytes(b.get(8..16)?.try_into().ok()?);
|
|
if !x.is_finite() || !y.is_finite() {
|
|
return None;
|
|
}
|
|
Some((Point { x, y }, *b.get(17)?))
|
|
}
|
|
|
|
fn anchors(records: &[Value]) -> Option<Vec<Anchor>> {
|
|
let mut out: Vec<Anchor> = Vec::with_capacity(records.len() / 3 + 1);
|
|
let mut pending_in = None;
|
|
for (i, v) in records.iter().enumerate() {
|
|
let (p, role) = record(v)?;
|
|
match role {
|
|
0 => {
|
|
out.push(Anchor { p, inh: pending_in.take(), out: None, record: i });
|
|
}
|
|
1 => {
|
|
// A leading out-handle without an anchor is edit state only.
|
|
if let Some(a) = out.last_mut() {
|
|
a.out = Some(p);
|
|
}
|
|
}
|
|
2 => pending_in = Some(p),
|
|
_ => return None,
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
fn subpath(anchors: &[Anchor], closed: bool, radii: Option<&Vec<(usize, f64)>>) -> Option<SubPath> {
|
|
let first = anchors.first()?;
|
|
if let Some(radii) = radii.filter(|r| !r.is_empty()) {
|
|
let corner = |a: &Anchor| radii.iter().filter(|(rec, _)| *rec == a.record).map(|(_, r)| *r).fold(0.0, f64::max);
|
|
let polyline = anchors.iter().all(|a| a.inh.is_none_or(|h| h == a.p) && a.out.is_none_or(|h| h == a.p));
|
|
if polyline {
|
|
let mut pts: Vec<(Point, f64)> = anchors.iter().map(|a| (a.p, corner(a))).collect();
|
|
// A closed subpath repeats its first anchor; its radius may be on either copy.
|
|
if closed
|
|
&& pts.len() > 1
|
|
&& pts.first().map(|p| p.0) == pts.last().map(|p| p.0)
|
|
&& let Some((_, r)) = pts.pop()
|
|
&& let Some(f) = pts.first_mut()
|
|
{
|
|
f.1 = f.1.max(r);
|
|
}
|
|
return Some(crate::shapes::fillet(&pts, closed));
|
|
}
|
|
}
|
|
let segments = anchors
|
|
.windows(2)
|
|
.filter_map(|w| {
|
|
let [a, b] = w else { return None };
|
|
Some([a.out.unwrap_or(a.p), b.inh.unwrap_or(b.p), b.p])
|
|
})
|
|
.collect();
|
|
Some(SubPath { start: first.p, segments, closed })
|
|
}
|
|
|
|
/// `CnrD`: positional `[u8, i32[] record indices, f64[] radii, u8[] types]`.
|
|
fn corner_radii(fields: Vec<&Value>) -> Option<Vec<(usize, f64)>> {
|
|
let (Some(Value::Array(idx)), Some(Value::Array(rad))) = (fields.get(1), fields.get(2)) else { return None };
|
|
Some(
|
|
idx.iter()
|
|
.zip(rad)
|
|
.filter_map(|(i, r)| match (i, r) {
|
|
(Value::Int(i), Value::Float(r)) if r.is_finite() && *r > 0.0 => Some((usize::try_from(*i).ok()?, *r)),
|
|
_ => None,
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|