From b4509d2368d5b85fc728885b8562b41d116eb2f8 Mon Sep 17 00:00:00 2001 From: Gurparvez Date: Mon, 19 Oct 2026 12:27:16 +0530 Subject: [PATCH] DWG: leave out orphaned DGN line style components, so DGN-bloated drawings open (fixes #1482) (#1501) * DWG: leave out orphaned DGN line style components, so DGN-bloated drawings open (fixes #1482) A DWG that has been through MicroStation can carry hundreds of thousands of DGN line style components no line style definition uses. In an 82.8 MB site plan they were 888,483 of 889,053 and made the DXF rendition 1.2 GB, over the 1 GB limit, so the file didn't open. After the failsafe read, components no LSDEFINITION reaches (through compound and point components) are left out with their dictionary entries, and a read note says how many. Co-Authored-By: Claude Opus 5.5 * contributors: add my real name for the About window credits Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- contributors/people.toml | 3 + crates/dwg/src/lib.rs | 197 ++++++++++++++++++++++++++++++++++++- docs/file-format-parity.md | 8 +- 3 files changed, 205 insertions(+), 3 deletions(-) diff --git a/contributors/people.toml b/contributors/people.toml index 7000a27a..18c34481 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -66,3 +66,6 @@ sync_github_name = true real_name = "Antoine Boxho" display_name = "Antoine Boxho" sync_github_name = true + +[people.gurparvez] +real_name = "Gurparvez" diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index 7c7da02a..bae5c6c0 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -83,12 +83,17 @@ mod native { let why = outcome.stats.diagnostics.first().map(|d| d.message.trim_start_matches("Failsafe: ").to_string()); return Err(format!("DWG: {}", why.unwrap_or_else(|| "the file's objects couldn't be read".to_string()))); } - let notes = read_notes(&outcome.stats); + let mut notes = read_notes(&outcome.stats); let mut doc = outcome.document; current_style_names(&mut doc); table_merges(&mut doc); table_style_rows(&mut doc, false); dimension_flags(&mut doc, false); + let orphans = drop_orphaned_dgn_components(&mut doc); + if orphans > 0 { + let (s, were) = if orphans == 1 { ("", "was") } else { ("s", "were") }; + notes.push(format!("DWG: {orphans} DGN line style component{s} that no line style definition uses {were} left out")); + } let mut out = CappedWriter { buf: Vec::new(), limit: limits.max_dxf_bytes, exceeded: false }; let written = std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DxfWriter::new(&doc).write_to_writer(&mut out))) .map_err(|_| "the DWG→DXF conversion failed on this file".to_string())?; @@ -288,6 +293,62 @@ mod native { } } + /// Leave out DGN line style components (`LS*COMPONENT`) that no line style definition uses + /// (#1482). A drawing that once came from MicroStation can carry hundreds of thousands of + /// them, orphaned copies piled up by earlier DGN imports and copy-pastes (AutoCAD's PURGE + /// removes them as orphaned data): in a real 83 MB site plan they were 888,483 of 889,053 + /// components and made its DXF rendition 1.2 GB, over [`Limits::DEFAULT`], so the file + /// didn't open at all. No definition that was read refers to them (a definition the reader + /// skipped can't use them either) and CADCraft doesn't draw DGN line styles, so nothing + /// visible is lost. + /// Every component a definition reaches (directly, or through compound and point + /// components) is kept, as are the definitions; dictionary entries that named a dropped + /// component go with it. Returns how many were left out. + fn drop_orphaned_dgn_components(doc: &mut acadrust::CadDocument) -> usize { + use acadrust::objects::{DgnLineStyleData, ObjectType}; + use std::collections::{HashMap, HashSet}; + // Each component's references, and the definitions' root components to start from. + let mut refs: HashMap> = HashMap::new(); + let mut stack: Vec = Vec::new(); + for (h, o) in &doc.objects { + let ObjectType::DgnLineStyle(ls) = o else { continue }; + match &ls.data { + DgnLineStyleData::Definition { root_component, .. } => stack.push(*root_component), + DgnLineStyleData::Component { component, .. } => { + refs.insert(*h, component.references()); + } + DgnLineStyleData::Registered { .. } => {} + } + } + if refs.is_empty() { + return 0; + } + // Each component is visited once, so cycles and shared subtrees end. + let mut used: HashSet = HashSet::new(); + while let Some(h) = stack.pop() { + if let Some(r) = refs.get(&h) + && used.insert(h) + { + stack.extend(r.iter().copied()); + } + } + let orphans: HashSet = refs.into_keys().filter(|h| !used.contains(h)).collect(); + if orphans.is_empty() { + return 0; + } + doc.objects.retain(|h, _| !orphans.contains(h)); + doc.dgn_ls_components.retain(|h, _| !orphans.contains(h)); + for o in doc.objects.values_mut() { + let ObjectType::Dictionary(d) = o else { continue }; + if d.entries.iter().any(|(_, e)| orphans.contains(e)) { + d.entries.retain(|(_, e)| !orphans.contains(e)); + let keys: HashSet<&str> = d.entries.iter().map(|(k, _)| k.as_str()).collect(); + d.hard_owner_entries.retain(|k| keys.contains(k.as_str())); + } + } + orphans.len() + } + /// Leave out what acadrust can't write as this DWG version: other programs' custom entities /// and objects without raw DWG data of that version. acadrust 0.6.4 refuses the whole file /// for one of them (0.6.3 left them out); CADCraft already tells the user which entity types @@ -593,4 +654,138 @@ mod tests { assert_eq!(flags.len(), 8, "{flags:?}"); assert!(flags.iter().all(|(l, f)| (*f == 128) == (*l == "MOVED")), "{flags:?}"); } + + /// The handles a DXF text refers to or gives its records (groups 5, 105 and 330–369). + #[cfg(not(target_arch = "wasm32"))] + fn dxf_handles(dxf: &str) -> std::collections::HashSet { + let lines: Vec<&str> = dxf.lines().collect(); + lines + .chunks(2) + .filter_map(|p| match p { + [c, v] if matches!(c.trim().parse::(), Ok(5 | 105 | 330..=369)) => u64::from_str_radix(v.trim(), 16).ok(), + _ => None, + }) + .collect() + } + + /// Issue #1482: a DWG that once came from MicroStation can carry hundreds of thousands of DGN + /// line style components no definition uses (a real 83 MB site plan: 888,483 of 889,053, a + /// 1.2 GB DXF rendition over the limit, so it didn't open). They're left out with a note, and + /// their dictionary entries with them; the definition and every component it reaches — + /// through compound and point components, around a cycle — are kept. + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn orphaned_dgn_line_style_components_are_left_out() { + use acadrust::objects::*; + let mut doc = acadrust::CadDocument::new(); + let container = doc.allocate_handle(); + let nod = doc.header.named_objects_dict_handle; + let Some(ObjectType::Dictionary(root)) = doc.objects.get_mut(&nod) else { panic!("no named object dictionary") }; + root.entries.push(("ACAD_DGNLINESTYLECOMP".into(), container)); + let pattern = || DgnLsStrokePattern { + has_iteration_limit: false, + is_single_segment: false, + iteration_limit: 0, + auto_phase: 0.0, + phase: 0.0, + phase_mode: DgnLsPhaseMode::Distance, + strokes: vec![DgnLsStroke { + is_dash: true, + bypass_corner: false, + can_be_scaled: true, + invert_at_origin: false, + invert_at_end: false, + length: 2.5, + start_width: 0.0, + end_width: 0.0, + width_mode: 0, + cap_mode: 0, + }], + }; + let kind = |c: &DgnLsComponentData| match c { + DgnLsComponentData::Symbol(_) => DgnLsComponentType::Symbol, + DgnLsComponentData::Compound(_) => DgnLsComponentType::Compound, + DgnLsComponentData::Stroke(_) => DgnLsComponentType::Stroke, + DgnLsComponentData::Point(_) => DgnLsComponentType::Point, + DgnLsComponentData::Internal(_) => DgnLsComponentType::Internal, + }; + let compound = |of: &[acadrust::Handle]| { + DgnLsComponentData::Compound(DgnLsCompoundComponent { + entries: of.iter().map(|&component| DgnLsCompoundEntry { component, offset: 0.0 }).collect(), + }) + }; + let mut entries = Vec::new(); + let mut put = |doc: &mut acadrust::CadDocument, h: acadrust::Handle, data: DgnLineStyleData| { + entries.push((format!("{:X}", h.value()), h)); + let reactors = vec![container]; + doc.objects + .insert(h, ObjectType::DgnLineStyle(DgnLineStyleObject { handle: h, owner: container, reactors, xdictionary_handle: None, data })); + }; + let component = |c: DgnLsComponentData| DgnLineStyleData::Component { + kind: kind(&c), + description: "dash".into(), + version: 1, + component_uid: [7; 16], + scale: 1.0, + property_flags: 0, + component: c, + properties: vec![], + }; + let h: Vec = (0..9).map(|_| doc.allocate_handle()).collect(); + let [def, root, stroke, point, back, orphan, orphan_cycle, internal, shared] = h[..] else { panic!("nine handles") }; + put( + &mut doc, + def, + DgnLineStyleData::Definition { + description: "{ dash }".into(), + version: 1, + style_number: 1, + component_uid: [1; 16], + is_continuous: false, + unit_definition: 1.0, + unit_scale: 1.0, + units_type: 0, + is_element: false, + is_physical: false, + is_scale_independent: false, + is_snappable: false, + root_component: root, + properties: vec![], + }, + ); + // Used: def → root → {stroke, point → shared, back → root (a cycle)}. + put(&mut doc, root, component(compound(&[stroke, point, back]))); + put(&mut doc, stroke, component(DgnLsComponentData::Stroke(pattern()))); + put(&mut doc, point, component(DgnLsComponentData::Point(DgnLsPointComponent { stroke_component: shared, symbols: vec![] }))); + put(&mut doc, back, component(compound(&[root]))); + put(&mut doc, shared, component(DgnLsComponentData::Stroke(pattern()))); + // Orphans: an internal component, and two compounds that refer to each other and to a + // used component (which stays). + let pattern_internal = + DgnLsInternalComponent { pattern: pattern(), internal_version: 0, hardware_style: 0, is_hardware_style: false, line_code: 0 }; + put(&mut doc, internal, component(DgnLsComponentData::Internal(pattern_internal))); + put(&mut doc, orphan, component(compound(&[orphan_cycle, shared]))); + put(&mut doc, orphan_cycle, component(compound(&[orphan]))); + let mut dict = Dictionary::new(); + dict.handle = container; + dict.owner = nod; + dict.entries = entries; + doc.objects.insert(container, ObjectType::Dictionary(dict)); + let dwg = acadrust::DwgWriter::write_to_vec(&doc).unwrap(); + + let (dxf, notes) = dwg_to_dxf_noted(&dwg, Limits::DEFAULT).unwrap(); + let dxf = String::from_utf8(dxf).unwrap(); + let handles = dxf_handles(&dxf); + for kept in [def, root, stroke, point, back, shared] { + assert!(handles.contains(&kept.value()), "{:X} was left out", kept.value()); + } + for gone in [internal, orphan, orphan_cycle] { + assert!(!handles.contains(&gone.value()), "{:X} was kept or is still referred to", gone.value()); + } + let lines: Vec<&str> = dxf.lines().collect(); + let ls: Vec<&str> = lines.chunks(2).filter_map(|p| p.get(1).filter(|_| p[0].trim() == "0" && p[1].starts_with("LS")).copied()).collect(); + assert_eq!(ls.iter().filter(|t| t.ends_with("COMPONENT")).count(), 5, "{ls:?}"); + assert!(ls.contains(&"LSDEFINITION") && !ls.contains(&"LSINTERNALCOMPONENT"), "{ls:?}"); + assert_eq!(notes, ["DWG: 3 DGN line style components that no line style definition uses were left out"]); + } } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index bb9b7dc0..c598193d 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (multiline attributes read, drawn and written) · **Target:** Autodesk AutoCAD 2027 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (DWG read leaves out orphaned DGN line style components) · **Target:** Autodesk AutoCAD 2027 Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work items are in [gaps.md](gaps.md#file-formats); the weighted score feeds @@ -53,7 +53,10 @@ title/header suppression. Everything lost in the DXF read/write paths below is also lost through DWG, plus whatever `acadrust`'s own DXF output omits (proxy objects, ACIS data, custom objects: not measured). User report #57: a 10 MB DWG failed after about two minutes; limits now stop runaway conversions -early (#175), but the throughput problem remains. **DWG ≈ 30% read, ≈ 15% write.** +early (#175), but the throughput problem remains. DGN line style components that no line style +definition uses (leftovers of DGN imports, hundreds of thousands in some drawings) are left out +on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG ≈ 30% read, +≈ 15% write.** ## DXF, version by version @@ -135,6 +138,7 @@ our output in AutoCAD black-box. | Date | Change | Summary | |---|---|---| +| 2026-10-17 | minor | DWG read: DGN line style components no line style definition reaches are left out before the DXF bridge, with a read note; an 83 MB site plan with 888,483 of them (1.2 GB of DXF, over the limit) now opens (#1482) | | 2026-10-12 | minor | PDF output: object transparency plotted as PDF alpha (`/ExtGState` `/ca` `/CA`) when the page setup's or the plot's "Plot transparency" is on; PNG plots blend it, SVG uses `opacity`; plots without it are unchanged byte for byte | | 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space | | 2026-10-12 | minor | VIEWPORT frozen layers also written and read as AutoCAD's group 341 layer handles, so third-party viewers honour per-viewport freezing in DXF (DWG converter gap noted) |