From 42503a8ecd9edd09b6e33c92bef87f7d0c2fc470 Mon Sep 17 00:00:00 2001 From: ffishman <20211677+ffishman@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:46:19 -0300 Subject: [PATCH] Keep a file's own paper size and plot rotation when saving DXF/DWG A media named wider than tall is stored two ways: AutoCAD writes a portrait size and a quarter turn (ISO_A4_(297.00_x_210.00_MM) as 210 x 297, 73 = 1), ZWCAD the media's own size and no turn (420 x 297, 73 = 0). Deriving the groups from the name alone turned the AutoCAD form into the ZWCAD one. The reader now keeps groups 44, 45 and 73 as read (PageSetup::file_paper, not serialized), and the writer puts them back while they still describe the page setup's size and orientation; page setups made or changed in CADCraft are derived from the media name as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/doc/src/tables.rs | 8 +++ crates/io/src/dxf_read/objects.rs | 1 + crates/io/src/dxf_write/sections.rs | 17 ++++-- crates/io/src/tests.rs | 52 +++++++++++++++++++ .../io/tests/it/tables_objects_roundtrip.rs | 4 +- 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/crates/doc/src/tables.rs b/crates/doc/src/tables.rs index a2c63ca6a..692dd2029 100644 --- a/crates/doc/src/tables.rs +++ b/crates/doc/src/tables.rs @@ -799,6 +799,13 @@ pub struct PageSetup { /// The layout's paper units are millimetres (plot paper units 1 in DXF) whatever the /// drawing's MEASUREMENT says; otherwise paper units follow MEASUREMENT (#1058). pub paper_mm: bool, + /// Paper size (DXF groups 44, 45) and plot rotation (group 73) as the file stored them. + /// Saving writes them back while they still describe `width_mm`, `height_mm` and + /// `landscape`, so each program's convention for media defined wider than tall survives a + /// round trip: AutoCAD stores a portrait size and a quarter turn, ZWCAD the media's own size + /// and no turn (#1463). `None` for page setups made in CADCraft. + #[serde(skip)] + pub file_paper: Option<(f64, f64, i64)>, } impl Default for PageSetup { @@ -820,6 +827,7 @@ impl Default for PageSetup { show_plot_styles: false, plot_transparency: false, paper_mm: false, + file_paper: None, } } } diff --git a/crates/io/src/dxf_read/objects.rs b/crates/io/src/dxf_read/objects.rs index 4d692ccee..3c36960ff 100644 --- a/crates/io/src/dxf_read/objects.rs +++ b/crates/io/src/dxf_read/objects.rs @@ -241,6 +241,7 @@ pub(super) fn page_setup(t: &T) -> PageSetup { let rotated = matches!(t.i(73), Some(1 | 3)); if w > 0.0 && h > 0.0 { p.landscape = rotated != (w > h); + p.file_paper = Some((w, h, t.i(73).unwrap_or(0))); } else if t.i(73).is_some() { p.landscape = rotated; } diff --git a/crates/io/src/dxf_write/sections.rs b/crates/io/src/dxf_write/sections.rs index 9fed1cdcc..f8f199758 100644 --- a/crates/io/src/dxf_write/sections.rs +++ b/crates/io/src/dxf_write/sections.rs @@ -1124,11 +1124,20 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec (f64, f64, i64) { + if let Some((w, h, rotation)) = page.file_paper { + let same = |a: f64, b: f64| (a - b).abs() < 1e-6; + let landscape = matches!(rotation, 1 | 3) != (w > h); + if same(w.min(h), page.width_mm) && same(w.max(h), page.height_mm) && landscape == page.landscape { + return (w, h, rotation); + } + } if media_is_landscape(&page.paper) { let (short, long) = (page.width_mm.min(page.height_mm), page.width_mm.max(page.height_mm)); (long, short, i64::from(!page.landscape)) diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs index 8bfb4980b..0d603be76 100644 --- a/crates/io/src/tests.rs +++ b/crates/io/src/tests.rs @@ -576,6 +576,58 @@ fn landscape_media_keeps_its_size_order_and_rotation() { } } +/// A file's own groups 44, 45 and 73 are written back unchanged, in DXF and DWG, whether they +/// follow ZWCAD's convention (the media's size, no turn) or AutoCAD's (a portrait size and a +/// quarter turn) for a media named wider than tall. Once the page setup changes, they no +/// longer describe it and the size and rotation are derived again. +#[test] +fn a_files_own_paper_groups_are_written_back() { + let set = |dxf: &str, (w, h, r): (f64, f64, i64)| -> String { + let mut lines: Vec = dxf.lines().map(String::from).collect(); + let name = (0..lines.len() - 3) + .find(|&i| { + lines[i].trim() == "100" && lines[i + 1].trim() == "AcDbLayout" && lines[i + 2].trim() == "1" && lines[i + 3].trim() == "Layout1" + }) + .unwrap(); + let start = (0..name).rev().find(|&i| lines[i].trim() == "100" && lines[i + 1].trim() == "AcDbPlotSettings").unwrap(); + for i in (start..name).step_by(2) { + match lines[i].trim() { + "44" => lines[i + 1] = w.to_string(), + "45" => lines[i + 1] = h.to_string(), + "73" => lines[i + 1] = r.to_string(), + _ => {} + } + } + lines.join("\n") + }; + // (media, groups as the program wrote them, portrait size and landscape CADCraft reads) + let cases = [ + ("ISO full bleed A3 (420.00 x 297.00 MM)", (420.0, 297.0, 0), (297.0, 420.0, true)), + ("ISO A4 (297.00 x 210.00 MM)", (210.0, 297.0, 1), (210.0, 297.0, true)), + ("ISO A4 (297.00 x 210.00 MM)", (210.0, 297.0, 0), (210.0, 297.0, false)), + ]; + for (media, groups, (pw, ph, landscape)) in cases { + let mut d = sample(); + d.layouts[0].page.paper = media.into(); + let file = set(&write_dxf(&d), groups); + let r = read_dxf(file.as_bytes()).unwrap(); + let p = &r.layout("Layout1").unwrap().page; + assert_eq!((p.width_mm, p.height_mm, p.landscape), (pw, ph, landscape), "{media} {groups:?} read"); + assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), groups, "{media} {groups:?} DXF"); + let dwg = write(&r, "t.dwg").unwrap(); + let back = String::from_utf8_lossy(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).into_owned(); + assert_eq!(plot_paper_groups(&back, "Layout1"), groups, "{media} {groups:?} DWG"); + let r = read(&dwg, "t.dwg").unwrap(); + assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), groups, "{media} {groups:?} DWG read and saved again"); + } + // Turned in CADCraft after reading: the stored groups no longer apply. + let mut d = sample(); + d.layouts[0].page.paper = "ISO full bleed A3 (420.00 x 297.00 MM)".into(); + let mut r = read_dxf(set(&write_dxf(&d), (420.0, 297.0, 0)).as_bytes()).unwrap(); + r.layouts.iter_mut().find(|l| l.name == "Layout1").unwrap().page.landscape = false; + assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), (420.0, 297.0, 1), "portrait after the change"); +} + #[test] fn portrait_media_is_written_as_before() { let mut d = sample(); diff --git a/crates/io/tests/it/tables_objects_roundtrip.rs b/crates/io/tests/it/tables_objects_roundtrip.rs index 0f75e4cd3..5a6ad5a4a 100644 --- a/crates/io/tests/it/tables_objects_roundtrip.rs +++ b/crates/io/tests/it/tables_objects_roundtrip.rs @@ -100,7 +100,9 @@ fn layout_page_setup_and_paper_view_round_trip() { let r = round_trip(&d); for (a, b) in d.layouts.iter().zip(&r.layouts) { assert_eq!(a.name, b.name); - assert_eq!(a.page, b.page, "{}", a.name); + // The groups 44, 45 and 73 the file stored are provenance, not a setting. + let read = cadcraft_doc::PageSetup { file_paper: None, ..b.page.clone() }; + assert_eq!(a.page, read, "{}", a.name); assert_eq!(a.view, b.view, "{}", a.name); } assert_eq!(r.layouts.len(), d.layouts.len());