mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 11:52:10 +00:00
DXF import: budget dash lengths, copied text and names, and block items looked at
A drawing could make the importer keep far more than its own size: every dashed path held its own copy of its linetype, every text object and symbol instance of an array copied its text, font family or block name, and each placement of a block looked at all of its items, hidden or not, without charging the work budget. - Dash lengths kept across the import are capped; past the cap lines are drawn solid. - Text, font family and block-name bytes copied into objects share one 64 MB budget, and a font family longer than 256 bytes falls back to the default. - Each block placement charges one visit per item it looks at, and stops once the budget is spent.
This commit is contained in:
committed by
bflatastic
parent
1bd47ea30d
commit
cd1afaf1cf
@@ -23,10 +23,19 @@ const PT_PER_MM: f64 = 72.0 / 25.4;
|
||||
const THINNEST: f64 = 0.25;
|
||||
/// The most objects an import makes.
|
||||
const MAX_NODES: usize = 2_000_000;
|
||||
/// The most block visits (one per insert copy) an import makes, whatever they produce.
|
||||
const MAX_VISITS: usize = 10_000_000;
|
||||
/// The most block work an import does, whatever it produces: one per insert copy drawn, and one
|
||||
/// per item of the block looked at for it (art on hidden layers too).
|
||||
const MAX_VISITS: usize = if cfg!(test) { 1_000_000 } else { 20_000_000 };
|
||||
/// The most path elements an import makes (blocks drawn as art are copied for every placement).
|
||||
const MAX_ELEMENTS: usize = if cfg!(test) { 1_000_000 } else { 4_000_000 };
|
||||
/// The most dash lengths an import keeps (every dashed path holds its own copy of its pattern).
|
||||
const MAX_DASH_VALUES: usize = if cfg!(test) { 1_000 } else { 4_000_000 };
|
||||
/// The most bytes of text, font names and symbol names an import copies into objects (a block's
|
||||
/// text and name are copied for every placement).
|
||||
const MAX_TEXT_BYTES: usize = if cfg!(test) { 64 << 10 } else { 64 << 20 };
|
||||
/// The longest font family a text style may name; a longer one is not a font, and is set in the
|
||||
/// default.
|
||||
const MAX_FAMILY_BYTES: usize = 256;
|
||||
/// How far from the origin art may lie (points): the canvas.
|
||||
pub(crate) const MAX_EXTENT: f64 = 4.0e6;
|
||||
|
||||
@@ -34,6 +43,8 @@ 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_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";
|
||||
|
||||
/// What a block's art takes from the insert that places it: its colour, lineweight, linetype
|
||||
/// and opacity where the art says "by block", and its layer for art on layer 0.
|
||||
@@ -71,6 +82,9 @@ pub(crate) struct Build<'d, 'a> {
|
||||
visits: usize,
|
||||
/// Path elements made so far.
|
||||
elements: usize,
|
||||
/// Dash lengths and text bytes kept so far.
|
||||
dash_values: usize,
|
||||
text_bytes: usize,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -128,6 +142,8 @@ impl<'d, 'a> Build<'d, 'a> {
|
||||
nodes: 0,
|
||||
visits: 0,
|
||||
elements: 0,
|
||||
dash_values: 0,
|
||||
text_bytes: 0,
|
||||
warnings: vec![],
|
||||
}
|
||||
}
|
||||
@@ -138,6 +154,20 @@ impl<'d, 'a> Build<'d, 'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Take `bytes` of text from [`MAX_TEXT_BYTES`]; false (with a warning) when they don't fit.
|
||||
fn charge_text(&mut self, bytes: usize) -> bool {
|
||||
match self.text_bytes.checked_add(bytes).filter(|t| *t <= MAX_TEXT_BYTES) {
|
||||
Some(t) => {
|
||||
self.text_bytes = t;
|
||||
true
|
||||
}
|
||||
None => {
|
||||
self.warn(TOO_MUCH_TEXT);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Import `layout`; `natural` and `ratio` are points per drawing unit in the drawing's own
|
||||
/// unit and at the scale asked for (fitting computes its own).
|
||||
pub fn run(mut self, layout: &LayoutDef, natural: f64, ratio: f64) -> Result<(Document, Vec<String>), String> {
|
||||
@@ -484,9 +514,15 @@ 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)
|
||||
{
|
||||
s.dash = Some(dash);
|
||||
if dots {
|
||||
s.cap = LineCap::Round;
|
||||
match self.dash_values.checked_add(dash.pattern.len()).filter(|n| *n <= MAX_DASH_VALUES) {
|
||||
Some(n) => {
|
||||
self.dash_values = n;
|
||||
s.dash = Some(dash);
|
||||
if dots {
|
||||
s.cap = LineCap::Round;
|
||||
}
|
||||
}
|
||||
None => self.warn(TOO_MANY_DASHES),
|
||||
}
|
||||
}
|
||||
ap
|
||||
@@ -527,10 +563,15 @@ impl<'d, 'a> Build<'d, 'a> {
|
||||
if !(s > 1e-12 && s.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let family = t.family.clone().or_else(|| self.d.styles.get(&t.style).filter(|f| !f.is_empty()).cloned());
|
||||
let d = self.d;
|
||||
let family = t.family.as_deref().or_else(|| d.styles.get(&t.style).map(String::as_str).filter(|f| !f.is_empty()));
|
||||
let family = family.filter(|f| f.len() <= MAX_FAMILY_BYTES);
|
||||
if !self.charge_text(t.text.len().saturating_add(family.map_or(0, str::len))) {
|
||||
return None;
|
||||
}
|
||||
let default = CharStyle::default();
|
||||
let style = CharStyle {
|
||||
font_family: family.unwrap_or(default.font_family.clone()),
|
||||
font_family: family.map_or_else(|| default.font_family.clone(), str::to_string),
|
||||
size: t.height / CAP_HEIGHT * s,
|
||||
h_scale: t.width * 100.0,
|
||||
leading: t.leading.map(|l| l * s),
|
||||
@@ -558,6 +599,13 @@ impl<'d, 'a> Build<'d, 'a> {
|
||||
for cell in &ins.cells {
|
||||
let node = match &symbol {
|
||||
Some(name) => {
|
||||
if self.nodes >= MAX_NODES {
|
||||
self.warn(TOO_MANY);
|
||||
break;
|
||||
}
|
||||
if !self.charge_text(name.len()) {
|
||||
break;
|
||||
}
|
||||
let xf = map * *cell * self.symbol_map.inverse();
|
||||
self.nodes += 1;
|
||||
Some(Node::new(self.doc.alloc_id(), NodeKind::SymbolInstance { symbol: name.clone(), xf }))
|
||||
@@ -581,14 +629,19 @@ 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> {
|
||||
self.visits += 1;
|
||||
if self.visits > MAX_VISITS {
|
||||
if self.visits == MAX_VISITS + 1 {
|
||||
self.warn(TOO_MANY);
|
||||
}
|
||||
if self.visits >= MAX_VISITS {
|
||||
self.warn(TOO_MANY);
|
||||
return None;
|
||||
}
|
||||
let items = self.block(name)?;
|
||||
let items = self.block(name);
|
||||
// Each placement looks at every item of the block, drawn or not (art on a hidden layer
|
||||
// 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);
|
||||
return None;
|
||||
}
|
||||
let items = items?;
|
||||
if !self.enter(name, depth) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1177,3 +1177,100 @@ fn many_array_inserts_of_a_missing_block_stay_bounded() {
|
||||
assert!(total <= 2_000_000 + 3000, "total cells materialised: {total}");
|
||||
assert!(conv.warnings.iter().any(|w| w.contains("array inserts")), "{:?}", conv.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashed_lines_past_the_dash_budget_are_drawn_solid() {
|
||||
// 600 lines of a two-length linetype: 1,200 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 line = [(0, "LINE"), (6, "DASHED"), (10, "0"), (20, "0"), (11, "100"), (21, "0")];
|
||||
let e: Vec<(i32, &str)> = (0..600).flat_map(|_| line).collect();
|
||||
let r = open(&drawing(&[], &tables, &[], &e), &points());
|
||||
let (mut dashed, mut solid, mut kept) = (0, 0, 0);
|
||||
r.document.walk(|n| {
|
||||
if let NodeKind::Path { .. } = n.kind {
|
||||
match n.appearance.stroke().and_then(|s| s.dash.as_ref()) {
|
||||
Some(d) => {
|
||||
dashed += 1;
|
||||
kept += d.pattern.len();
|
||||
}
|
||||
None => solid += 1,
|
||||
}
|
||||
}
|
||||
});
|
||||
assert_eq!(dashed + solid, 600);
|
||||
assert!(dashed > 0 && solid > 0, "{dashed} dashed, {solid} solid");
|
||||
assert!(kept <= 1_000, "{kept} dash lengths kept");
|
||||
assert!(r.warnings.iter().any(|w| w.contains("drawn solid")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// The bytes of text in the document's text objects.
|
||||
fn text_bytes(doc: &Document) -> usize {
|
||||
let mut bytes = 0;
|
||||
doc.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
bytes += t.runs.iter().map(|r| r.text.len()).sum::<usize>();
|
||||
}
|
||||
});
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_copied_into_array_cells_stops_at_the_text_budget() {
|
||||
// 1,000 characters of text in an anonymous block placed 10 × 10: 100 KB of copies, past the
|
||||
// 64 KB tests import with.
|
||||
let long = "x".repeat(1000);
|
||||
let blocks = [(0, "BLOCK"), (2, "*U1"), (70, "1"), (10, "0"), (20, "0"), (0, "TEXT"), (10, "0"), (20, "0"), (40, "1"), (1, long.as_str()), (0, "ENDBLK")];
|
||||
let insert = [(0, "INSERT"), (2, "*U1"), (10, "0"), (20, "0"), (70, "10"), (71, "10"), (44, "1"), (45, "1")];
|
||||
let r = open(&drawing(&[], &[], &blocks, &insert), &points());
|
||||
let bytes = text_bytes(&r.document);
|
||||
assert!(bytes > 0, "some copies are kept");
|
||||
assert!(bytes <= 64 << 10, "{bytes} bytes of text");
|
||||
assert!(r.warnings.iter().any(|w| w.contains("64 MB of text")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_text_style_naming_an_overlong_font_family_takes_the_default() {
|
||||
let family = "F".repeat(10_000);
|
||||
let tables = [(0, "TABLE"), (2, "STYLE"), (0, "STYLE"), (2, "Big"), (3, "x.ttf"), (1001, "ACAD"), (1000, family.as_str()), (0, "ENDTAB")];
|
||||
let e = [(0, "TEXT"), (10, "0"), (20, "0"), (40, "1"), (1, "Hi"), (7, "Big")];
|
||||
let doc = open(&drawing(&[], &tables, &[], &e), &points()).document;
|
||||
let mut families = vec![];
|
||||
doc.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
families.extend(t.runs.iter().map(|r| r.style.font_family.clone()));
|
||||
}
|
||||
});
|
||||
assert_eq!(families, [vectorcraft_doc::CharStyle::default().font_family]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_instances_of_a_long_block_name_stop_at_the_text_budget() {
|
||||
// A 1,000-byte block name placed 10 × 10: each instance keeps its own copy of the name.
|
||||
let name = "N".repeat(1000);
|
||||
let blocks = [(0, "BLOCK"), (2, name.as_str()), (10, "0"), (20, "0"), (0, "LINE"), (10, "0"), (20, "0"), (11, "1"), (21, "0"), (0, "ENDBLK")];
|
||||
let insert = [(0, "INSERT"), (2, name.as_str()), (10, "0"), (20, "0"), (70, "10"), (71, "10"), (44, "2"), (45, "2")];
|
||||
let r = open(&drawing(&[], &[], &blocks, &insert), &points());
|
||||
let mut instances = 0;
|
||||
r.document.walk(|n| {
|
||||
if let NodeKind::SymbolInstance { .. } = n.kind {
|
||||
instances += 1;
|
||||
}
|
||||
});
|
||||
assert!(instances > 0 && instances < 100, "{instances} instances");
|
||||
assert!(r.warnings.iter().any(|w| w.contains("64 MB of text")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays_of_blocks_of_hidden_art_stop_at_the_work_budget() {
|
||||
// 500 lines on a hidden layer in an anonymous block placed 50 × 50: they make nothing, but
|
||||
// each placement looks at all of them (1,250,000 items, past the 1,000,000 tests import with).
|
||||
let tables = [(0, "TABLE"), (2, "LAYER"), (0, "LAYER"), (2, "Off"), (70, "0"), (62, "-1"), (0, "ENDTAB")];
|
||||
let mut blocks = vec![(0, "BLOCK"), (2, "*U1"), (70, "1"), (10, "0"), (20, "0")];
|
||||
for _ in 0..500 {
|
||||
blocks.extend([(0, "LINE"), (8, "Off"), (10, "0"), (20, "0"), (11, "1"), (21, "0")]);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user