diff --git a/crates/cad/src/import/build.rs b/crates/cad/src/import/build.rs index fcc87a71f..c65862e67 100644 --- a/crates/cad/src/import/build.rs +++ b/crates/cad/src/import/build.rs @@ -377,7 +377,8 @@ impl<'d, 'a> Build<'d, 'a> { let k = self.d.header.ltscale * p.ltscale * scale; // Dashes (positive), gaps (negative) and dots (0), merged into dash, gap, dash, gap… let mut out: Vec = vec![]; - for v in pattern { + // (A linetype has a few elements; every path keeps its own copy of the dashes.) + for v in pattern.iter().take(64) { let dash = *v >= 0.0; let len = v.abs() * k; match out.len().is_multiple_of(2) { diff --git a/crates/cad/src/scene.rs b/crates/cad/src/scene.rs index 8d90c2bf1..42c25b9a3 100644 --- a/crates/cad/src/scene.rs +++ b/crates/cad/src/scene.rs @@ -168,12 +168,29 @@ fn subpaths(bp: &BezPath) -> Vec { out } +/// The most pixels of an image written (it is held decoded, as RGBA and encoded at once). +const MAX_IMAGE_PIXELS: u64 = 64 << 20; + +/// The most entities (handles) a drawing is written with. +const MAX_ENTITIES: u32 = 10_000_000; + +/// The largest x or y a path may have to be flattened, and the most points its polygons keep. +const MAX_COORD: f64 = 1e9; +const MAX_POINTS: usize = 4_000_000; + /// `bp` flattened into polygons (each closing implicitly), consecutive repeats dropped. fn polygons(bp: &BezPath) -> Vec> { + // Far outside any drawing, a curve flattens to millions of points at this tolerance. + let b = bp.control_box(); + if !(b.x0.abs().max(b.x1.abs()).max(b.y0.abs()).max(b.y1.abs()) <= MAX_COORD) { + return vec![]; + } let mut out: Vec> = vec![]; let mut cur: Vec = vec![]; - let push = |p: Point, cur: &mut Vec| { - if cur.last().is_none_or(|q| q.distance(p) > 1e-9) { + let mut points = 0usize; + let mut push = |p: Point, cur: &mut Vec| { + if points < MAX_POINTS && cur.last().is_none_or(|q| q.distance(p) > 1e-9) { + points += 1; cur.push(p); } }; @@ -208,21 +225,37 @@ fn symbol_name(name: &str, long: bool) -> String { s.trim().to_string() } -/// `base` made unique among `taken` (any case) by `_2`, `_3`…, within `max` characters. -fn unique(base: &str, taken: &[&str], max: usize) -> String { - let base = if base.is_empty() { "Layer".to_string() } else { base.to_string() }; - let free = |n: &str| !taken.iter().any(|t| t.eq_ignore_ascii_case(n)); - if free(&base) { - return base; +/// The names a DXF table holds (compared in any case), and the next suffix to try for each base, +/// so identical names don't each search the suffixes before. +#[derive(Default)] +struct Names { + taken: std::collections::HashSet, + next: HashMap, +} + +impl Names { + /// Names that are there already. + fn with(names: &[&str]) -> Self { + Self { taken: names.iter().map(|n| n.to_lowercase()).collect(), next: HashMap::new() } } - (2..) - .map(|i| { - let suffix = format!("_{i}"); + + /// `base` made unique by `_2`, `_3`…, within `max` characters; the name is taken from now on. + fn unique(&mut self, base: &str, max: usize) -> String { + let base = if base.is_empty() { "Layer".to_string() } else { base.to_string() }; + if self.taken.insert(base.to_lowercase()) { + return base; + } + let next = self.next.entry(base.to_lowercase()).or_insert(2); + loop { + let suffix = format!("_{next}"); + *next += 1; let keep: String = base.chars().take(max.saturating_sub(suffix.len())).collect(); - format!("{keep}{suffix}") - }) - .find(|n| free(n)) - .unwrap_or(base) + let name = format!("{keep}{suffix}"); + if self.taken.insert(name.to_lowercase()) { + return name; + } + } + } } fn overlaps(a: Rect, b: Rect) -> bool { @@ -251,12 +284,14 @@ pub(crate) struct Scene<'a> { e: Codes, next: u32, layers: Vec, + layer_names: Names, /// The DXF layer art goes on, and its flags. layer: String, flags: LayerFlags, linetypes: Vec, /// (style name, font family). styles: Vec<(String, String)>, + style_names: Names, images: Vec, files: Vec, /// Image keys → index into `images` (`None`: left out). @@ -283,10 +318,12 @@ impl<'a> Scene<'a> { e: Codes::new(o.version), next: fixed::FIRST_FREE, layers: vec![], + layer_names: Names::with(&["0", "Defpoints"]), layer: "0".into(), flags: LayerFlags { off: false, locked: false, plot: true }, linetypes: vec![], styles: vec![], + style_names: Names::with(&[crate::writer::STANDARD_STYLE]), images: vec![], files: vec![], by_key: HashMap::new(), @@ -350,6 +387,12 @@ impl<'a> Scene<'a> { if let NodeKind::Layer { template: true, .. } = n.kind { return; } + // Symbols that hold symbols are written in full for each instance: depth alone doesn't + // bound how many entities that is. + if self.next.saturating_sub(fixed::FIRST_FREE) > MAX_ENTITIES { + self.warn("the drawing is too large for DXF: what follows 10 million entities was left out"); + return; + } // Hidden layers are written, switched off. let layer = n.is_layer(); if !force && !n.visible && !layer { @@ -452,9 +495,7 @@ impl<'a> Scene<'a> { fn add_layer(&mut self, n: &Node) -> String { let long = self.o.version.r2000(); let base = symbol_name(n.name.as_deref().unwrap_or("Layer"), long); - let mut taken: Vec<&str> = self.layers.iter().map(|l| l.name.as_str()).collect(); - taken.extend(["0", "Defpoints"]); - let name = unique(&base, &taken, if long { 255 } else { 31 }); + let name = self.layer_names.unique(&base, if long { 255 } else { 31 }); let aci = match n.kind { NodeKind::Layer { color, .. } => nearest_aci(color.rgb(), ColorDepth::Aci256), _ => 7, @@ -904,9 +945,7 @@ impl<'a> Scene<'a> { return name.clone(); } let long = self.o.version.r2000(); - let mut taken: Vec<&str> = self.styles.iter().map(|(n, _)| n.as_str()).collect(); - taken.push(crate::writer::STANDARD_STYLE); - let name = unique(&symbol_name(family, long), &taken, if long { 255 } else { 31 }); + let name = self.style_names.unique(&symbol_name(family, long), if long { 255 } else { 31 }); self.styles.push((name.clone(), family.to_string())); name } @@ -985,6 +1024,15 @@ impl<'a> Scene<'a> { /// Image `im` as a file in the chosen format, and its size in pixels. fn encode_image(&self, im: &ImageObject) -> Result<(DxfImage, (u32, u32)), String> { let blob = self.doc.image_blob(im).filter(|b| !b.bytes.is_empty()).ok_or_else(|| "an image has no pixels and was left out".to_string())?; + // The pixels it converts to (RGBA, 4 bytes each) are read from its header before decoding. + let (w, h) = image::ImageReader::new(Cursor::new(blob.bytes.as_slice())) + .with_guessed_format() + .ok() + .and_then(|r| r.into_dimensions().ok()) + .ok_or_else(|| UNDECODED.to_string())?; + if u64::from(w) * u64::from(h) > MAX_IMAGE_PIXELS { + return Err("an image is too large for DXF and was left out".to_string()); + } let img = image::load_from_memory(&blob.bytes).map_err(|_| UNDECODED.to_string())?; let size = (img.width(), img.height()); let mut bytes = vec![];