From 2d7004f972fc3e97feda7775853026c63c922331 Mon Sep 17 00:00:00 2001 From: dwetscher <24957205+dwetscher@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:40:44 +0200 Subject: [PATCH] AF.5: cap dash patterns at 64 lengths A line style's `Patn` was copied twice (filtered, then scaled) whatever its length, before any budget saw it. Patterns longer than 64 lengths now import as solid lines, with a warning. Addresses CodeRabbit's third review on #3548. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/affinity/src/paint.rs | 7 +++++++ crates/engine/tests/affinity_features.rs | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/crates/affinity/src/paint.rs b/crates/affinity/src/paint.rs index ab235cac7..3eea67795 100644 --- a/crates/affinity/src/paint.rs +++ b/crates/affinity/src/paint.rs @@ -126,6 +126,9 @@ pub struct Stroke { /// Gradient stops per gradient. const MAX_STOPS: usize = 1024; +/// Dash and gap lengths per dash pattern (a pattern has a few; strokes and text runs copy theirs). +const MAX_DASHES: usize = 64; + fn floats(v: &Value) -> Option> { match v { Value::Floats(f) => Some(f.clone()), @@ -319,6 +322,10 @@ pub(crate) fn stroke(r: &mut Reader, pen: ObjId, ldsc: ObjId, local: Affine, beh _ => Align::Center, }; let dash = match s.field(lsty, b"Patn") { + Some(Value::Array(a)) if a.len() > MAX_DASHES => { + r.warn("dash patterns of more than 64 lengths (imported as solid lines)"); + None + } Some(Value::Array(a)) if data.get(10) == Some(&2) || !a.is_empty() => { let mut v: Vec = a.iter().filter_map(|x| if let Value::Float(f) = x { Some(*f) } else { None }).collect(); while v.len() >= 2 && v.iter().rev().take(2).all(|x| *x == 0.0) { diff --git a/crates/engine/tests/affinity_features.rs b/crates/engine/tests/affinity_features.rs index bb323387d..dbc10fcb8 100644 --- a/crates/engine/tests/affinity_features.rs +++ b/crates/engine/tests/affinity_features.rs @@ -391,6 +391,19 @@ fn master_pages_of_embedded_documents_are_left_out() { named(&loaded.doc, "Page box"); } +/// A dash pattern has a few lengths; a crafted one of thousands would be copied with every stroke. +#[test] +fn dash_patterns_of_more_than_64_lengths_import_as_solid_lines() { + use vectorcraft_affinity::model::Kind; + for (lengths, dashed) in [(64, true), (65, false)] { + let dash: Vec = (0..lengths).map(|i| 1.0 + f64::from(i)).collect(); + let d = vectorcraft_affinity::read(&canvas_with(vec![stroked(10, 2, &dash)], None), vectorcraft_affinity::Limits::default()).unwrap(); + let Kind::Shape { strokes, .. } = &d.spreads[0].nodes[0].kind else { panic!("{:?}", d.spreads[0].nodes[0].kind) }; + assert_eq!(strokes[0].dash.as_ref().map(|(d, _)| d.len()), dashed.then_some(64)); + assert_eq!(d.warnings.iter().any(|w| w.contains("dash patterns of more than 64")), !dashed, "{:?}", d.warnings); + } +} + #[test] fn master_pages_show_on_the_pages_that_use_them() { let loaded = open("master.afpub", &with_master(1));