Compare commits

..
4 changed files with 50 additions and 92 deletions
+11 -37
View File
@@ -11,10 +11,6 @@ use crate::geom::{Op, P};
/// Line segments per Bézier when a crossing shape is flattened.
const CURVE_STEPS: usize = 16;
/// Point-against-edge tests one clipped item may cost (all its subpaths together); an item over
/// it is dropped, as a single polygon over it already was.
const MAX_CLIP_WORK: usize = 1_000_000;
/// The bounding box `[l, t, r, b]` of the points in `ops` (control points included).
pub(crate) fn ops_bbox(ops: &[Op]) -> Option<[f64; 4]> {
let mut b: Option<[f64; 4]> = None;
@@ -104,20 +100,14 @@ fn is_rect(clip: &[P]) -> bool {
/// Sutherland–Hodgman: `poly` cut by convex `clip`.
pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec<P> {
clip_polygon_within(poly, clip, &mut { MAX_CLIP_WORK }).unwrap_or_default()
}
/// [`clip_polygon`] charging each point visited against each clip edge to `budget`: the polygon
/// can gain a vertex at every edge, so its cost isn't known from its input size alone. `None`
/// once the budget is spent.
fn clip_polygon_within(poly: &[P], clip: &[P], budget: &mut usize) -> Option<Vec<P>> {
if poly.len().saturating_mul(clip.len()) > 1_000_000 {
return Vec::new();
}
let mut poly = poly.to_vec();
for i in 0..clip.len() {
// The work actually done: this edge visits the polygon as it now stands.
if poly.len() > *budget {
return None;
if poly.len().saturating_mul(clip.len() - i) > 1_000_000 {
return Vec::new();
}
*budget -= poly.len();
let (a, b) = (clip[i], clip[(i + 1) % clip.len()]);
let keep = |p: P| cross(a, b, p) >= 0.0;
let cut = |p: P, q: P| {
@@ -143,7 +133,7 @@ fn clip_polygon_within(poly: &[P], clip: &[P], budget: &mut usize) -> Option<Vec
break;
}
}
Some(poly)
poly
}
fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool {
@@ -168,12 +158,9 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec<Raw> {
return Vec::new();
}
let subs = flatten(&ops);
if too_much_work(&subs, clip) {
return Vec::new();
}
let mut out = Vec::new();
if fill.is_some() {
let Some(f) = clip_fill(&subs, clip) else { return Vec::new() };
let f = clip_fill(&subs, clip);
if !f.is_empty() {
out.push(Raw::Path { ops: f, fill, stroke: None, even_odd, region: region.clone() });
}
@@ -194,11 +181,7 @@ pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec<Raw> {
if disjoint(b, cb) {
return Vec::new();
}
let subs = flatten(&ops);
if too_much_work(&subs, clip) {
return Vec::new();
}
let f = clip_fill(&subs, clip).unwrap_or_default();
let f = clip_fill(&flatten(&ops), clip);
if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] }
}
Raw::Bitmap { rect, img, region } => crop(rect, img, clip).map(|(rect, img)| vec![Raw::Bitmap { rect, img, region }]).unwrap_or_default(),
@@ -247,26 +230,17 @@ pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec<P>, bool)> {
subs
}
/// Whether cutting every subpath of one item by `clip` would exceed [`MAX_CLIP_WORK`]; the
/// per-polygon check alone lets many small subpaths each spend the whole allowance.
fn too_much_work(subs: &[(Vec<P>, bool)], clip: &[P]) -> bool {
subs.iter().map(|s| s.0.len() + 1).sum::<usize>().saturating_mul(clip.len()) > MAX_CLIP_WORK
}
/// The clipped fill of every subpath, its clipping work shared by all of them: `None` (the whole
/// item goes) once [`MAX_CLIP_WORK`] is spent.
fn clip_fill(subs: &[(Vec<P>, bool)], clip: &[P]) -> Option<Vec<Op>> {
fn clip_fill(subs: &[(Vec<P>, bool)], clip: &[P]) -> Vec<Op> {
let mut ops = Vec::new();
let mut budget = MAX_CLIP_WORK;
for (pts, _) in subs {
let poly = clip_polygon_within(pts, clip, &mut budget)?;
let poly = clip_polygon(pts, clip);
if poly.len() >= 3 {
ops.push(Op::M(poly[0]));
ops.extend(poly[1..].iter().map(|p| Op::L(*p)));
ops.push(Op::Z);
}
}
Some(ops)
ops
}
fn clip_stroke(subs: &[(Vec<P>, bool)], clip: &[P]) -> Vec<Op> {
-43
View File
@@ -2074,46 +2074,3 @@ fn clip_polygon_work_bound() {
let clipped = crate::cliprect::clip_polygon(&poly, &clip);
assert!(clipped.is_empty(), "excessive polygon clip product rejected");
}
#[test]
fn clip_polygon_charges_actual_work_not_projected_growth() {
// Two convex 1,000-gons: the product is exactly the budget, and a cut that adds a vertex must
// not make a later step look over budget.
let gon = |cx: f64| -> Vec<(f64, f64)> {
(0..1000).map(|i| (i as f64 / 1000.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (cx + 100.0 * c, 100.0 * s)).collect()
};
let clipped = crate::cliprect::clip_polygon(&gon(0.0), &gon(30.0));
assert!(clipped.len() > 100, "the intersection is kept: {}", clipped.len());
}
#[test]
fn clip_work_is_bounded_across_subpaths() {
use crate::canvas::Raw;
use crate::geom::Op;
// 512-gon clip; 4,000 small triangles crossing its edge: each under the per-polygon cap, together
// far over it, for both the fill and the stroke.
let clip: Vec<(f64, f64)> =
(0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect();
let tri = |x: f64| [Op::M((x, 95.0)), Op::L((x + 1.0, 95.0)), Op::L((x, 300.0)), Op::Z];
let many: Vec<Op> = (0..4000).flat_map(|i| tri(i as f64 % 200.0)).collect();
let raw = |ops| Raw::Path { ops, fill: Some([0, 0, 0, 255]), stroke: Some(([0, 0, 0, 255], 1.0)), even_odd: false, region: None };
assert!(crate::cliprect::clip_raw(raw(many), &clip).is_empty(), "the whole item's clipping work is bounded");
assert_eq!(crate::cliprect::clip_raw(raw(tri(100.0).to_vec()), &clip).len(), 2, "a small crossing shape is still clipped");
}
/// Clipping a fill can add a vertex at every clip edge, so a polygon's work isn't its input size:
/// shapes that pass the up-front estimate but grow while clipped are charged as they grow, against
/// one budget for the whole item.
#[test]
fn growing_fill_clips_share_one_budget() {
use crate::canvas::Raw;
use crate::geom::Op;
let clip: Vec<(f64, f64)> =
(0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect();
// A triangle around the whole circle: clipped, it becomes the 512-gon, about 130,000 steps.
let tri = [Op::M((-1000.0, -1000.0)), Op::L((1000.0, -1000.0)), Op::L((100.0, 1000.0)), Op::Z];
let raw = |n: usize| Raw::Path { ops: tri.repeat(n), fill: Some([0, 0, 0, 255]), stroke: None, even_odd: false, region: None };
// 400 of them pass the estimate (400 x 4 x 512 is under a million) but cost about 52 million.
assert!(crate::cliprect::clip_raw(raw(400), &clip).is_empty(), "the growing work is charged");
assert_eq!(crate::cliprect::clip_raw(raw(2), &clip).len(), 1, "a couple of them are still clipped");
}
+29 -12
View File
@@ -260,12 +260,15 @@ pub(crate) struct Tab {
align: TabAlign,
}
/// The paragraph's tab stops in position order (a stop at no finite position is left out), so
/// [`tab_advance`] finds the next one by binary search instead of a scan per tab.
pub(crate) fn tabs_of(props: &ParaProps) -> Vec<Tab> {
props
let mut tabs: Vec<Tab> = props
.tabs
.as_ref()
.map(|t| {
t.iter()
.filter(|s| s.pos.is_finite())
.map(|s| Tab {
pos: s.pos,
align: match s.align.as_str() {
@@ -277,7 +280,9 @@ pub(crate) fn tabs_of(props: &ParaProps) -> Vec<Tab> {
})
.collect()
})
.unwrap_or_default()
.unwrap_or_default();
tabs.sort_by(|a, b| a.pos.total_cmp(&b.pos));
tabs
}
/// Width of the text after the tab at cell `i` up to the next tab or line break, and the width of
@@ -301,18 +306,30 @@ pub(crate) fn tab_segment(cells: &[Cell], i: usize) -> (f64, f64) {
/// (`seg`: its width, and its width before the decimal point) as the stop aligns it but never
/// moving back, as PowerPoint does; past the last stop, to the next default stop.
pub(crate) fn tab_advance(x: f64, stops: &[Tab], default_tab: f64, seg: (f64, f64)) -> f64 {
for s in stops {
if s.pos > x + 0.5 {
let to = s.pos - x;
return match s.align {
TabAlign::Left => to,
TabAlign::Right => (to - seg.0).max(0.0),
TabAlign::Center => (to - seg.0 / 2.0).max(0.0),
TabAlign::Decimal => (to - seg.1).max(0.0),
};
}
// `stops` is in position order (`tabs_of`): the next stop is the first past x.
// A NaN `x` (a NaN spacing upstream) skips the lookup and takes the finite default-stop path.
if let Some(s) = stops.get(stops.partition_point(|s| s.pos <= x + 0.5)).filter(|_| !x.is_nan()) {
let to = s.pos - x;
return match s.align {
TabAlign::Left => to,
TabAlign::Right => (to - seg.0).max(0.0),
TabAlign::Center => (to - seg.0 / 2.0).max(0.0),
TabAlign::Decimal => (to - seg.1).max(0.0),
};
}
let d = if default_tab > 1.0 { default_tab } else { 72.0 };
let next = ((x / d).floor() + 1.0) * d;
(next - x).max(1.0)
}
#[cfg(test)]
mod tab_tests {
use super::*;
#[test]
fn a_nan_position_takes_the_finite_default_stop() {
let stops = [Tab { pos: 100.0, align: TabAlign::Left }];
assert!(tab_advance(f64::NAN, &stops, 72.0, (0.0, 0.0)).is_finite());
assert_eq!(tab_advance(10.0, &stops, 72.0, (0.0, 0.0)), 90.0);
}
}
+10
View File
@@ -1416,6 +1416,16 @@ fn tab_stops_align_the_text_after_them() {
assert!((l.lines[0].edges[4].0 - x0(&l) - 72.0).abs() < 0.01);
}
#[test]
fn many_tabs_over_many_stops_lay_out_by_position() {
// 20,000 stops given out of order, and 2,000 tabs: each tab finds the next stop by position.
let mut stops: Vec<(f64, &str)> = (1..=20_000).rev().map(|k| (k as f64 * 0.05 + 30.0, "l")).collect();
stops.push((f64::NAN, "l"));
let text = "a\t".repeat(2_000);
let l = tabbed(&text, &stops, 1.0e6);
assert!((l.lines[0].edges[2].0 - l.inner.x0 - 30.05).abs() < 0.01, "{:?}", &l.lines[0].edges[..3]);
}
#[test]
fn a_tab_past_the_end_moves_to_the_next_line() {
// PowerPoint: "abcdefghij\tk\tl\tm…" in a narrow box puts the tab that doesn't fit on the next