DXF import: charge every dash copy and symbol name, name the block-work limit

Follow-up to #5288. A dashed compound path keeps its dash pattern on the compound node and on each loop, so the dash budget is charged for every copy. A symbol's name is charged against the text budget before the symbol is kept. Running out of block work now says so instead of reporting more than 2,000,000 objects.
This commit is contained in:
Dylan M. Taylor
2026-11-03 22:52:28 +07:00
committed by bflatastic
parent 30ce0e3a42
commit ac391b0f85
2 changed files with 87 additions and 8 deletions
+13 -6
View File
@@ -43,6 +43,7 @@ const NESTED: &str = "blocks nested more than 8 deep, or inside themselves, are
const XREFS: &str = "blocks referencing other drawings (external references) are left out";
const TOO_MANY: &str = "the drawing has more than 2,000,000 objects: the rest are left out";
const TOO_MANY_POINTS: &str = "the drawing has more than 4,000,000 path points: the rest are left out";
const TOO_MUCH_BLOCK_WORK: &str = "the drawing's blocks take too much work to place: the rest are left out";
const TOO_MANY_DASHES: &str = "the drawing has more dashed lines than can be kept: the rest are drawn solid";
const TOO_MUCH_TEXT: &str = "the drawing has more than 64 MB of text and block names: the rest are left out";
@@ -499,6 +500,9 @@ impl<'d, 'a> Build<'d, 'a> {
super::curve::bounds(&bp)?;
let paint = Paint::solid(rgb_color(self.rgb(p, inh)));
let scale = map.determinant().abs().sqrt();
let path = PathData::from_bezpath(&bp);
// A compound path's loops each keep a copy of its appearance, beside its own.
let copies = if path.subpaths.len() > 1 { path.subpaths.len() + 1 } else { 1 };
let appearance = if fill {
Appearance::basic(paint, Paint::None, 0.0)
} else {
@@ -514,7 +518,7 @@ impl<'d, 'a> Build<'d, 'a> {
if let Some(AppearanceItem::Stroke(s)) = ap.items.last_mut()
&& let Some((dash, dots)) = self.dash(&self.linetype(p, inh), p, scale)
{
match self.dash_values.checked_add(dash.pattern.len()).filter(|n| *n <= MAX_DASH_VALUES) {
match dash.pattern.len().checked_mul(copies).and_then(|v| self.dash_values.checked_add(v)).filter(|n| *n <= MAX_DASH_VALUES) {
Some(n) => {
self.dash_values = n;
s.dash = Some(dash);
@@ -528,7 +532,6 @@ impl<'d, 'a> Build<'d, 'a> {
ap
};
let rule = if fill { FillRule::EvenOdd } else { FillRule::NonZero };
let path = PathData::from_bezpath(&bp);
let kind = |path: PathData| NodeKind::Path { path, rule, live: None, clipping: false, guide: false };
let mut n = if path.subpaths.len() > 1 {
// Several loops (a hatch with islands) paint as one compound path. Each loop is an
@@ -630,7 +633,7 @@ impl<'d, 'a> Build<'d, 'a> {
/// The art of block `name` drawn by `map`, as one object.
fn art(&mut self, name: &str, map: Affine, inh: &Inherit, depth: u32) -> Option<Node> {
if self.visits >= MAX_VISITS {
self.warn(TOO_MANY);
self.warn(TOO_MUCH_BLOCK_WORK);
return None;
}
let items = self.block(name);
@@ -638,7 +641,7 @@ impl<'d, 'a> Build<'d, 'a> {
// makes nothing, so the node budget doesn't bound it).
self.visits = self.visits.saturating_add(1 + items.as_ref().map_or(0, |i| i.len()));
if self.visits > MAX_VISITS {
self.warn(TOO_MANY);
self.warn(TOO_MUCH_BLOCK_WORK);
return None;
}
let items = items?;
@@ -671,12 +674,16 @@ impl<'d, 'a> Build<'d, 'a> {
let base = self.d.blocks.get(name).map_or(Point::ZERO, |b| b.base);
let map = self.symbol_map * Affine::translate(-base.to_vec2());
let art = self.art(name, map, &key.1, depth);
let made = art.map(|art| {
let made = art.and_then(|art| {
let label = self.d.blocks.get(name).map_or(name, |b| b.name.as_str());
// The symbol keeps its name: text, as its instances' copies of it are.
if !self.charge_text(label.len()) {
return None;
}
let symbol = unique(label, &mut self.next_suffix, &self.symbol_names);
self.symbol_names.insert(symbol.clone());
self.doc.symbols.push(Symbol { dynamic: false, name: symbol.clone(), art: Arc::new(art) });
symbol
Some(symbol)
});
self.symbols.insert(key, made.clone());
made
+74 -2
View File
@@ -814,7 +814,7 @@ fn nested_array_inserts_of_nothing_stop_at_the_work_budget() {
blocks.extend(block("*C", None));
let top = [(0, "INSERT"), (2, "*A"), (10, "0"), (20, "0"), (70, "300"), (44, "1")];
let r = open(&drawing(&[], &[], &blocks, &top), &points());
assert!(r.warnings.iter().any(|w| w.contains("more than")), "{:?}", r.warnings);
assert!(r.warnings.iter().any(|w| w.contains("too much work")), "{:?}", r.warnings);
}
#[test]
@@ -1273,5 +1273,77 @@ fn arrays_of_blocks_of_hidden_art_stop_at_the_work_budget() {
blocks.push((0, "ENDBLK"));
let insert = [(0, "INSERT"), (2, "*U1"), (10, "0"), (20, "0"), (70, "50"), (71, "50"), (44, "1"), (45, "1")];
let r = open(&drawing(&[], &tables, &blocks, &insert), &points());
assert!(r.warnings.iter().any(|w| w.contains("more than")), "{:?}", r.warnings);
assert!(r.warnings.iter().any(|w| w.contains("too much work")), "{:?}", r.warnings);
}
#[test]
fn dashed_compound_paths_charge_every_copy_of_their_pattern() {
// 100 pattern hatches of 9 loops in a two-length linetype: each keeps its dashes on the
// compound path and on each loop, 2,000 dash lengths, past the 1,000 tests import with.
let tables = [(0, "TABLE"), (2, "LTYPE"), (0, "LTYPE"), (2, "DASHED"), (73, "2"), (40, "15"), (49, "10"), (49, "-5"), (0, "ENDTAB")];
let corners = ["0", "10", "20", "30", "40", "50", "60", "70", "80", "90"];
let mut hatch = vec![(0, "HATCH"), (6, "DASHED"), (10, "0"), (20, "0"), (30, "0"), (2, "ANSI31"), (70, "0"), (71, "0"), (91, "9")];
for i in 0..9 {
let (a, b) = (corners[i], corners[i + 1]);
hatch.extend([
(92, "2"),
(72, "0"),
(73, "1"),
(93, "4"),
(10, a),
(20, "0"),
(10, b),
(20, "0"),
(10, b),
(20, "5"),
(10, a),
(20, "5"),
(97, "0"),
]);
}
hatch.extend([(75, "0"), (76, "1"), (98, "0")]);
let e: Vec<(i32, &str)> = (0..100).flat_map(|_| hatch.iter().copied()).collect();
let r = open(&drawing(&[], &tables, &[], &e), &points());
let (mut compounds, mut kept) = (0, 0);
r.document.walk(|n| {
compounds += usize::from(matches!(n.kind, NodeKind::Compound { .. }));
kept += n.appearance.stroke().and_then(|s| s.dash.as_ref()).map_or(0, |d| d.pattern.len());
});
assert_eq!(compounds, 100, "{:?}", r.warnings);
assert!(kept > 0 && kept <= 1_000, "{kept} dash lengths kept");
assert!(r.warnings.iter().any(|w| w.contains("drawn solid")), "{:?}", r.warnings);
}
#[test]
fn symbols_of_a_long_block_name_stop_at_the_text_budget() {
// A 1,000-byte block name inserted in 100 colours its art takes: a symbol per colour, each
// keeping its own copy of the name, past the 64 KB tests import with.
let name = "N".repeat(1000);
let blocks = [
(0, "BLOCK"),
(2, name.as_str()),
(10, "0"),
(20, "0"),
(0, "LINE"),
(8, "0"),
(62, "0"),
(10, "0"),
(20, "0"),
(11, "1"),
(21, "0"),
(0, "ENDBLK"),
];
let colours: Vec<String> = (1..=100).map(|c| c.to_string()).collect();
let inserts: Vec<(i32, &str)> =
colours.iter().flat_map(|c| [(0, "INSERT"), (2, name.as_str()), (62, c.as_str()), (10, "0"), (20, "0")]).collect();
let r = open(&drawing(&[], &[], &blocks, &inserts), &points());
let mut bytes: usize = r.document.symbols.iter().map(|s| s.name.len()).sum();
r.document.walk(|n| {
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind {
bytes += symbol.len();
}
});
assert!(!r.document.symbols.is_empty());
assert!(bytes <= 64 << 10, "{bytes} bytes of symbol names");
assert!(r.warnings.iter().any(|w| w.contains("64 MB of text")), "{:?}", r.warnings);
}