From f8cb50eaa3ec60c2e156678ed87d66b240b6197c Mon Sep 17 00:00:00 2001 From: CaseyKleinman <106724583+CaseyKleinman@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:11:09 -0600 Subject: [PATCH] DXF: write kept application data groups as read, not as handle lists (#681) Squash of PR #681 by @CaseyKleinman. Co-Authored-By: Claude Opus 5.5 --- crates/io/src/dxf_raw.rs | 13 +++++++++++++ crates/io/src/tests.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/io/src/dxf_raw.rs b/crates/io/src/dxf_raw.rs index 8b7e71c9..00b09420 100644 --- a/crates/io/src/dxf_raw.rs +++ b/crates/io/src/dxf_raw.rs @@ -598,6 +598,19 @@ impl Map<'_> { while let Some(r) = o.tags.get(i) { i += 1; if r.code == 102 && r.value.trim().starts_with('{') { + // A `{NAME` group is a handle list (`{ACAD_REACTORS 330 … }`) only when every + // group inside it is a reference. Application data groups also use 102 brackets + // with mixed values and their own closing form — the AcDs thumbnail schema's + // `{ATTRRECORD 341 h 2 name 280 1 291 1 ATTRRECORD}` — and collapsing those into + // a handle list rewrote the handle under the LAST code read (291, a bool) and + // dropped the rest, which strict DXF readers reject. Those are written as read, + // references remapped, by the plain path below. + let end = (i..o.tags.len()).find(|&k| o.tags[k].code == 102).unwrap_or(o.tags.len()); + let inner = &o.tags[i..end]; + if !inner.is_empty() && !inner.iter().all(|n| is_pointer(n.code)) { + out.push(tag(r)); + continue; + } let name = r.value.trim().trim_start_matches('{').to_string(); let mut hs = Vec::new(); let mut code = 330; diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs index d099d92c..e9d337e7 100644 --- a/crates/io/src/tests.rs +++ b/crates/io/src/tests.rs @@ -1985,3 +1985,30 @@ fn svg_no_geometry_yields_an_empty_drawing() { let d = read(svg.as_bytes(), "empty.svg").unwrap(); assert!(kinds(&d).is_empty()); } + +/// An application data group inside a kept XRECORD — the AcDs thumbnail schema's +/// `{ATTRRECORD 341 h 2 name 280 1 291 1 ATTRRECORD}` — is written back as read. It used to be +/// collapsed into a handle list: the handle came out under group 291 (a bool) and the name and +/// flags were dropped, which strict DXF readers (ezdxf) reject outright. +#[test] +fn kept_xrecord_application_group_roundtrips_as_read() { + let dxf = "0\nSECTION\n2\nOBJECTS\n\ +0\nDICTIONARY\n5\nC\n330\n0\n100\nAcDbDictionary\n3\nTD_SCHEMA\n350\n20\n\ +0\nXRECORD\n5\n20\n330\nC\n100\nAcDbXrecord\n280\n1\n1\nAcDb_Thumbnail_Schema\n\ +102\n{ATTRRECORD\n341\n21\n2\nAcDbDs::TreatedAsObjectData\n280\n1\n291\n1\n102\nATTRRECORD}\n\ +0\nXRECORD\n5\n21\n330\n20\n100\nAcDbXrecord\n280\n1\n1\nTarget\n\ +0\nENDSEC\n0\nEOF\n"; + let d = read_dxf(dxf.as_bytes()).expect("read"); + let out = write_dxf(&d); + let lines: Vec<&str> = out.lines().map(str::trim).collect(); + let at = lines.iter().position(|l| *l == "{ATTRRECORD").expect("group kept"); + // The group keeps its own shape: a reference under 341, then the name, flag and bool. + assert_eq!(lines[at + 1], "341", "reference group code: {:?}", &lines[at..at + 12]); + assert!(lines[at + 2] != "0" && !lines[at + 2].is_empty()); + assert_eq!(&lines[at + 3..at + 5], &["2", "AcDbDs::TreatedAsObjectData"]); + assert_eq!(&lines[at + 5..at + 7], &["280", "1"]); + assert_eq!(&lines[at + 7..at + 9], &["291", "1"]); + assert_eq!(&lines[at + 9..at + 11], &["102", "ATTRRECORD}"]); + // And no handle is written under a bool code anywhere. + assert!(!out.contains("291\n2"), "no handle may be written under a bool code"); +}