Bound work: flattening of huge curves, print layout past the page limit, script-check file names

This commit is contained in:
Dylan M. Taylor
2026-11-04 08:13:07 +07:00
committed by bflatastic
parent 079f5389a6
commit 0e64aca557
7 changed files with 54 additions and 8 deletions
+2 -1
View File
@@ -236,7 +236,8 @@ pub fn tracks(bp: &BezPath, tol: f64) -> Vec<Track> {
return out;
}
let mut els = vec![];
kurbo::flatten(bp.iter(), tol.max(1e-4), |el| {
// Guarded: kurbo works out every subdivision of a curve even when the callback drops them.
vectorcraft_geom::flatten(bp.iter(), tol.max(1e-4), |el| {
if els.len() < MAX_FLATTENED {
els.push(el);
}
+1 -1
View File
@@ -103,7 +103,7 @@ fn densified(sp: &SubPath, flat_tol: f64, max_du: f64) -> Option<Vec<Point>> {
let mut bp = BezPath::new();
sp.to_bezpath_into(&mut bp);
let mut pts = vec![];
kurbo::flatten(bp.iter(), flat_tol.max(1e-4), |e| {
vectorcraft_geom::flatten(bp.iter(), flat_tol.max(1e-4), |e| {
if let PathEl::MoveTo(q) | PathEl::LineTo(q) = e {
pts.push(q);
}
+9 -1
View File
@@ -29,6 +29,11 @@ pub const EPS: f64 = 1e-9;
/// Most line segments [`flatten`] makes of one curve, whatever its size and the tolerance.
pub const MAX_FLATTEN_PER_CURVE: f64 = 4096.0;
/// A curve whose control polygon is longer than this is drawn as a line by [`flatten`]: kurbo
/// squares and cubes coordinates when it sizes its work, and with coordinates this large that
/// overflows to infinity and it reserves a buffer no machine has. (Far beyond any drawing.)
pub const MAX_FLATTEN_LENGTH: f64 = 1e60;
/// [`kurbo::flatten`], each curve's tolerance raised so that it makes at most about
/// [`MAX_FLATTEN_PER_CURVE`] segments. kurbo sizes its work and a buffer from the curve's
/// coordinates before producing anything, so a small curve with far-out handles (from a file, or
@@ -60,7 +65,7 @@ pub fn flatten(path: impl IntoIterator<Item = PathEl>, tolerance: f64, mut callb
};
last = Some(to);
let (Some(from), Some(length)) = (from, length) else { continue };
if !length.is_finite() {
if length.is_nan() || length > MAX_FLATTEN_LENGTH {
callback(PathEl::LineTo(to));
continue;
}
@@ -198,6 +203,9 @@ mod tests {
assert_eq!(ours, theirs);
let nan = [PathEl::MoveTo(Point::ZERO), PathEl::QuadTo(Point::new(f64::NAN, 0.0), Point::new(1.0, 1.0))];
assert_eq!(count(&nan, 0.25), 2);
// Finite coordinates whose squares overflow are a line to the end too.
let huge = [PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::new(1e200, 0.0), Point::new(-1e200, 1e200), Point::new(1.0, 0.0))];
assert_eq!(count(&huge, 0.25), 2);
}
#[test]
+1 -1
View File
@@ -75,7 +75,7 @@ pub(crate) fn checksum(header: &[u8]) -> u16 {
fn flatten(bp: &BezPath, tol: f64) -> Vec<(Vec<Point>, bool)> {
let mut out: Vec<(Vec<Point>, bool)> = vec![];
let mut cur: Vec<Point> = vec![];
kurbo::flatten(bp.iter(), tol, |el| match el {
vectorcraft_geom::flatten(bp.iter(), tol, |el| match el {
PathEl::MoveTo(p) => {
if cur.len() > 1 {
out.push((std::mem::take(&mut cur), false));
+11
View File
@@ -165,6 +165,10 @@ fn turns(set: &PrintSettings, (w, h): (f64, f64), flipped: bool) -> (Affine, (f6
(v, (w, h))
}
fn too_many_pages() -> PdfError {
PdfError::BadSetting(format!("the job would print more than {} pages", super::MAX_PAGES))
}
/// The pages of `doc` (whose layers are those that print) as `set` lays them out, the tiles of
/// each artboard when tiling, and warnings (art that doesn't fit the imageable area).
pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<String>) -> Result<(Vec<Layout>, Vec<TileGrid>), PdfError> {
@@ -174,6 +178,10 @@ pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<Str
let origin = set.tile_origin.placed.then_some((set.tile_origin.x, set.tile_origin.y));
let (mut pages, mut grids) = (vec![], vec![]);
for (artboard, r) in regions(doc, set)? {
// The job is refused past MAX_PAGES (copies and inks only add to it): stop making pages here.
if pages.len() > super::MAX_PAGES {
return Err(too_many_pages());
}
let what = artboard.map_or_else(|| "The art".to_string(), |i| format!("Artboard {}", i + 1));
if !(r.width() > 0.0 && r.height() > 0.0) {
warnings.push(format!("{what} has no area and was left out"));
@@ -297,6 +305,9 @@ pub(crate) fn layout(doc: &Document, set: &PrintSettings, warnings: &mut Vec<Str
let (x, y) = (grid.0 + c * step.0, grid.1 + rw * step.1);
Rect::new(x, y, x + tile.0, y + tile.1)
};
if pages.len() + pick.len() > super::MAX_PAGES {
return Err(too_many_pages());
}
for &t in &pick {
let w = window(t);
let view = turn * Affine::translate((at.0 - w.x0, at.1 - w.y0));
+8
View File
@@ -239,6 +239,14 @@ fn tiles_cover_the_artboard_with_overlap() {
assert!(preview(&d, &settings(json!({"scaling": "tileFull", "overlap": 400}))).is_err(), "overlap of half a tile");
}
#[test]
fn a_job_of_too_many_artboards_is_refused_while_laying_out() {
let mut d = doc(100.0, 100.0, &[]);
d.artboards.extend((2..=MAX_PAGES as u32 + 2).map(|i| artboard(i, 0.0)));
let e = preview(&d, &settings(json!({}))).unwrap_err().to_string();
assert!(e.contains("more than 2000 pages"), "{e}");
}
#[test]
fn artboard_range_skip_blank_and_ignore() {
let mut d =
+22 -4
View File
@@ -370,14 +370,14 @@ impl<'l> Acc<'l> {
pub fn problem(&mut self, code: &'static str, line: Option<usize>, file: Option<&str>, detail: Option<&str>) {
if self.room(self.problems.len()) {
let detail = detail.map(|d| self.echo(d));
self.problems.push(Problem { code, line, file: file.map(str::to_string), detail });
self.problems.push(Problem { code, line, file: file.map(|f| self.echo(f)), detail });
}
}
pub fn directive(&mut self, name: &'static str, value: &str, line: usize, form: &'static str, file: Option<&str>) {
if self.room(self.directives.len()) {
let value = self.echo(value);
self.directives.push(Directive { name, value, line, form, file: file.map(str::to_string) });
self.directives.push(Directive { name, value, line, form, file: file.map(|f| self.echo(f)) });
}
}
@@ -394,14 +394,14 @@ impl<'l> Acc<'l> {
version: part(t.version),
locale: part(t.locale),
line,
file: file.map(str::to_string),
file: file.map(|f| self.echo(f)),
};
self.targets.push(target);
}
pub fn include(&mut self, value: &str, line: usize, file: Option<&str>, status: IncludeStatus, path: Option<&str>) {
if self.room(self.includes.len()) {
let include = Include { value: self.echo(value), line, file: file.map(str::to_string), status, path: path.map(|p| self.echo(p)) };
let include = Include { value: self.echo(value), line, file: file.map(|f| self.echo(f)), status, path: path.map(|p| self.echo(p)) };
self.includes.push(include);
}
}
@@ -461,3 +461,21 @@ fn echo_cow(s: &str, max: usize) -> Cow<'_, str> {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_file_of_a_row_is_cut_like_the_rest_of_the_echo() {
let limits = Limits::default();
let mut acc = Acc::new(&limits);
let long = "d/".repeat(5000);
acc.problem("x", Some(1), Some(&long), None);
acc.directive("include", "v", 1, "form", Some(&long));
acc.target("illustrator", 1, Some(&long));
for file in [&acc.problems[0].file, &acc.directives[0].file, &acc.targets[0].file] {
assert_eq!(file.as_deref().map(|f| f.chars().count()), Some(limits.echo));
}
}
}