collective

Browse / github.com/storytold/cadcraft / patch

DXF: an attribute's justification is read from its text groups, not its multiline header (#2255)

DXF: an attribute's justification is read from its text groups, not its multiline header (#2255) <!-- claude-projects-attribution --> _Requested by **Francesco BV**_ **Before:** an ATTRIB or ATTDEF with a multiline (embedded MTEXT) part took the `AcDbAttribute` subclass's multiline header (71, 72 and the point 11) and its field length (73) as the text's justification, alignment point and vertical justification. A left-justified multiline attribute was read as Center with an alignment point; its next save recomputed the single-line fallback TEXT's insertion from that justification, and an R2000 file saved after an R2018 save placed the attribute differently from one saved directly. A defi

Status
open
Score
0.00
Adopters
0 other operators
Author
francescoBV (imported from pull request #2313, open)
Base
d864a08b013353143086bd8bb568c26cb6256796
Size
+55 −6 in 4 files
Review
pending
Shared
2026-10-28
Id
ea5185b8d734d65c9a12fa17

How it went

No other operator has reported using it yet.

Diff

raw · crates/io/src/dxf_read/entities.rs crates/io/src/dxf_read/text.rs crates/io/tests/it/multiline_attributes.rs docs/file-format-parity.md

diff --git a/crates/io/src/dxf_read/entities.rs b/crates/io/src/dxf_read/entities.rs--- a/crates/io/src/dxf_read/entities.rs+++ b/crates/io/src/dxf_read/entities.rs@@ -1,6 +1,6 @@ use super::common::{T, common, ocs}; use super::hatch::hatch;-use super::text::{attdef, attrib_modes, attrib_props, mtext_from, multiline_attrib, split_embedded, text_from};+use super::text::{attdef, attrib_modes, attrib_props, attrib_text_groups, mtext_from, multiline_attrib, split_embedded, text_from}; use super::*;  /// Parse one entity record. POLYLINE/INSERT followers are handled by the caller.@@ -526,8 +526,8 @@ pub(super) fn insert_attribs(recs: &[(String, Vec<Tag>)], i: &mut usize, ins_lay             // A multiline attribute has its text as embedded MTEXT after its own groups.             let (own, embedded) = split_embedded(t2);             let multiline = multiline_attrib(embedded, &common(&tt));+            let mut text = text_from(&T(&attrib_text_groups(own)));             let own = T(own);-            let mut text = text_from(&own);             if let Some(m) = &multiline {                 text.value.clone_from(&m.read_as);             }diff --git a/crates/io/src/dxf_read/text.rs b/crates/io/src/dxf_read/text.rs--- a/crates/io/src/dxf_read/text.rs+++ b/crates/io/src/dxf_read/text.rs@@ -33,6 +33,18 @@ pub(super) fn split_embedded(tags: &[Tag]) -> (&[Tag], Option<&[Tag]>) {     } } +/// The groups of an ATTRIB's or ATTDEF's own groups that describe its text: all of them up to+/// its `AcDbAttribute`/`AcDbAttributeDefinition` subclass, and from there on all but the field+/// length (73) and the multiline header (71, 72 and the point 11): there 72 isn't the+/// justification nor 11 the alignment point, and a left-justified attribute writes no 72 of its+/// own, so the multiline one was read as centred. The vertical justification is that+/// subclass's 74.+pub(super) fn attrib_text_groups(own: &[Tag]) -> Vec<Tag> {+    let at = own.iter().position(|t| t.code == 100 && t.str().trim().starts_with("AcDbAttribute")).unwrap_or(own.len());+    let (text, attrib) = own.split_at(at);+    text.iter().chain(attrib.iter().filter(|t| !matches!(t.code, 71 | 72 | 73 | 11 | 21 | 31))).cloned().collect()+}+ /// A multiline attribute's text, from the MTEXT groups embedded in it. pub(super) fn multiline_attrib(embedded: Option<&[Tag]>, c: &Common) -> Option<MultilineAttrib> {     let mtext = mtext_from(embedded.filter(|e| !e.is_empty())?, c);@@ -157,8 +169,8 @@ pub(super) fn attdef(tags: &[Tag], c: &Common) -> Attrib {     // A multiline definition has its text as embedded MTEXT after its own groups.     let (own, embedded) = split_embedded(tags);     let multiline = multiline_attrib(embedded, c);+    let mut text = text_from(&T(&attrib_text_groups(own)));     let own = T(own);-    let mut text = text_from(&own);     if let Some(m) = &multiline {         text.value.clone_from(&m.read_as);     }diff --git a/crates/io/tests/it/multiline_attributes.rs b/crates/io/tests/it/multiline_attributes.rs--- a/crates/io/tests/it/multiline_attributes.rs+++ b/crates/io/tests/it/multiline_attributes.rs@@ -5,7 +5,7 @@ //! The fixture is synthetic, laid out as the DXF Reference describes these attributes: block TAG //! with a multiline definition LABEL, and a reference whose LABEL attribute has its own text. -use cadcraft_doc::{Drawing, EntityKind};+use cadcraft_doc::{Drawing, EntityKind, HAlign, VAlign}; use cadcraft_io::{SaveOptions, Version, read_dxf};  /// DXF text from lines of `code value` (the value may be empty).@@ -309,6 +309,42 @@ fn an_edited_value_replaces_the_multiline_text() {     assert_eq!(reference(&again).attribs[0].text.value, "SOUTH {A}\nWELL 8"); } +/// The fixture with the reference's attribute left-justified on its baseline: its text groups+/// carry no 72, 11 or 74, only its multiline header (71 2, 72 1, 11) does.+fn left_justified() -> String {+    dxf(&FIXTURE.replacen("72 1\n11 110\n21 53\n31 0\n100 AcDbAttribute", "100 AcDbAttribute", 1).replacen("70 0\n74 1\n", "70 0\n", 1))+}++/// An attribute's justification comes from its text groups (#2255). In its attribute subclass+/// 71, 72 and 11 are the multiline header and 73 the field length, not the justification,+/// alignment point and vertical justification (that is 74): a left-justified multiline attribute+/// was read as centred, and its next save moved its single-line text.+#[test]+fn justification_is_read_from_the_text_groups() {+    let d = read_dxf(left_justified().as_bytes()).unwrap();+    let left = |d: &Drawing| {+        let t = reference(d).attribs[0].text.clone();+        assert_eq!((t.halign, t.valign, t.align_pt), (HAlign::Left, VAlign::Baseline, None));+        assert_eq!((t.insert.x, t.insert.y), (102.0, 52.0));+    };+    left(&d);+    // The definition's field length (73 0) isn't its vertical justification (74 1).+    let ad = definition(&d);+    assert_eq!((ad.text.halign, ad.text.valign), (HAlign::Center, VAlign::Bottom));+    // Saved twice, it stays where it was, and so does the definition.+    let once = read_dxf(save(&d).as_bytes()).unwrap();+    left(&once);+    let twice = read_dxf(save(&once).as_bytes()).unwrap();+    left(&twice);+    assert_eq!(definition(&twice), definition(&d));+    // An older version written after a 2018 save places the text as one written directly.+    let r2000 = |d: &Drawing| {+        let out = cadcraft_io::write_with(d, "x.dxf", &SaveOptions { version: Some(Version::R2000), binary: false }).unwrap();+        reference(&read_dxf(&out).unwrap()).attribs[0].text.clone()+    };+    assert_eq!(r2000(&twice), r2000(&d));+}+ #[test] fn files_before_2007_get_single_line_attributes() {     let d = read_dxf(dxf(FIXTURE).as_bytes()).unwrap();diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md--- 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-22 · **Change:** minor (DIMASSOC objects for angular, diametric and radial dimensions) · **Target:** Autodesk AutoCAD 2027+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (a multiline attribute's justification is read from its text groups) · **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@@ -75,7 +75,7 @@ on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG |---|---|---|---| | LINE, POINT, CIRCLE, ARC, ELLIPSE, LWPOLYLINE (widths, bulges, elevation), SPLINE, RAY, XLINE | yes | yes | round-trip tests | | POLYLINE 2D/3D | yes | yes (as LWPOLYLINE / 3D POLYLINE) | polyface and polygon meshes flattened: lossy |-| TEXT, MTEXT, ATTDEF, INSERT + ATTRIB | yes; multiline attributes (embedded MTEXT) read and drawn | yes; multiline attributes written as embedded MTEXT (DXF 2007 and later; older versions: one line) | ATTDEF prompt kept (#137) |+| TEXT, MTEXT, ATTDEF, INSERT + ATTRIB | yes; multiline attributes (embedded MTEXT) read and drawn | yes; multiline attributes written as embedded MTEXT (DXF 2007 and later; older versions: one line) | ATTDEF prompt kept (#137); an attribute's justification comes from its text groups, not its multiline header or field length (#2255) | | DIMENSION (all 7 kinds) | yes | yes, with anonymous `*D` blocks | a reopened dimension's `*D` block is regenerated under its name, unreferenced ones dropped (#495); DIMASSOC objects written for every associated kind — linear/aligned, angular, radial/diametric, ordinate and arc length — and read back without CADCraft's xdata (#1651) | | DIMENSION (all 7 kinds) | yes | yes, with anonymous `*D` blocks | a reopened dimension's `*D` block is regenerated under its name, unreferenced ones dropped (#495); DIMASSOC objects written; angular block text honors DIMDSEP, including per-dimension overrides (#1646) | | DIMENSION (all 7 kinds) | yes | yes, with anonymous `*D` blocks; their text keeps the diameter sign as `%%c`, so SHX styles plot it (#1645) | a reopened dimension's `*D` block is regenerated under its name, unreferenced ones dropped (#495); DIMASSOC objects written |@@ -140,6 +140,7 @@ our output in AutoCAD black-box.  | Date | Change | Summary | |---|---|---|+| 2026-10-27 | minor | DXF read: an ATTRIB's or ATTDEF's justification and alignment point come from its text groups; the multiline header (71, 72, 11) and field length (73) of its attribute subclass were read as them, so a left-justified multiline attribute became centred and its next save moved its single-line text (#2255) | | 2026-10-22 | minor | DXF/DWG save: angular, diametric and radial dimensions get standard DIMASSOC objects (one centre reference to the circle for radial/diametric, the definition points for angular), so AutoCAD follows their geometry; the standard objects of those kinds are read back too, without CADCraft's xdata (#1651) | | 2026-10-19 | minor | DXF/DWG save: angular dimension block text honors DIMDSEP, including per-dimension overrides (#1646) | | 2026-10-19 | minor | A file's own arrowhead blocks with standard names (AutoCAD's `_Dot`, `_DotSmall`), named only by a dimension style, are kept and saved as they were instead of being replaced by CADCraft's solids; PURGE keeps arrowhead blocks dimension styles name |