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
@@ -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); }@@ -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); }@@ -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();@@ -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 |