collective

Browse / github.com/storytold/cadcraft / patch

Keep a landscape media's size order and rotation when saving DXF/DWG

Keep a landscape media's size order and rotation when saving DXF/DWG <!-- claude-projects-attribution: {"github_login":"ffishman"} --> _Requested by **Rodrigo**_ Before: a layout whose media is defined wider than tall, such as `ISO_full_bleed_A3_(420.00_x_297.00_MM)` with 44 = 420, 45 = 297 and 73 = 0, was saved as 44 = 297, 45 = 420 and 73 = 1. ZWCAD 2025 takes the media's size from its name and applies the quarter turn on top, so the sheet plotted as a portrait page with the drawing shrunk to about 70% (reported in #1463). After: that layout is saved with the paper size in the media's own order and the plot rotation relative to it (420 × 297, rotation 0, or

Status
open
Score
0.00
Adopters
0 other operators
Author
ffishman (imported from pull request #2028, open)
Base
0ff1b03537f885bc1d592f2acbc1b440f25964ee
Size
+85 −4 in 3 files
Review
pending
Shared
2026-10-28
Id
2b90102724ee73f36f6a8d4d

How it went

No other operator has reported using it yet.

Diff

raw · crates/io/src/dxf_write/sections.rs crates/io/src/tests.rs docs/file-format-parity.md

diff --git a/crates/io/src/dxf_write/sections.rs b/crates/io/src/dxf_write/sections.rs--- a/crates/io/src/dxf_write/sections.rs+++ b/crates/io/src/dxf_write/sections.rs@@ -834,13 +834,14 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Ta         w.s(2, if device.is_empty() || device.eq_ignore_ascii_case("None") { "none_device" } else { device });         w.s(4, page.paper.replace(' ', "_"));         w.s(6, "");+        let (paper_w, paper_h, rotation) = plot_paper(page);         for (c, v) in [             (40, page.margins_mm[0]),             (41, page.margins_mm[1]),             (42, page.margins_mm[2]),             (43, page.margins_mm[3]),-            (44, page.width_mm),-            (45, page.height_mm),+            (44, paper_w),+            (45, paper_h),             (46, 0.0),             (47, 0.0),             (48, page.plot_window[0]),@@ -855,7 +856,7 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Ta         w.i(70, plot_flags);         // Plot paper units: 1 millimetres, 0 inches (#1058).         w.i(72, i64::from(cadcraft_render::paper::page_unit_mm(d, page) == 1.0));-        w.i(73, i64::from(page.landscape));+        w.i(73, rotation);         w.i(74, plot_type);         w.s(7, &page.plot_style_table);         w.i(75, if page.scale_to_fit { 0 } else { 16 });@@ -912,6 +913,33 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Ta     w.t } +/// Paper size (groups 44, 45) and plot rotation (group 73) for a LAYOUT object. Page setups+/// keep a portrait size and a landscape flag. A media whose name makes it wider than tall, such+/// as `ISO_full_bleed_A3_(420.00_x_297.00_MM)`, gets its size in that order and the rotation+/// relative to it, so readers that take the media's size from its name agree with groups 44 and+/// 45 (#1463). Other media are written as before.+fn plot_paper(page: &PageSetup) -> (f64, f64, i64) {+    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))+    } else {+        (page.width_mm, page.height_mm, i64::from(page.landscape))+    }+}++/// Whether a media name ends in a size wider than tall, like `… (420.00 x 297.00 MM)`.+fn media_is_landscape(paper: &str) -> bool {+    let name = paper.replace('_', " ");+    let Some(open) = name.rfind('(') else { return false };+    let mut sides = name[open + 1..].splitn(2, ['x', 'X', '×']);+    let num = |s: Option<&str>| {+        let s = s?.trim_start();+        let end = s.find(|c: char| !(c.is_ascii_digit() || c == '.')).unwrap_or(s.len());+        s[..end].parse::<f64>().ok()+    };+    matches!((num(sides.next()), num(sides.next())), (Some(w), Some(h)) if w > h)+}+ /// Write DXF Reference group 91 on every CLASS record. Count records after `dxf_raw::finish` /// restores foreign classes and objects so every class gets its actual instance count. fn add_class_instance_counts(tags: &mut Vec<Tag>) {diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs--- a/crates/io/src/tests.rs+++ b/crates/io/src/tests.rs@@ -537,6 +537,58 @@ fn dxf_roundtrips_page_setup() {     assert_eq!(p.paper, a3.name); } +/// Groups 44, 45 and 73 of a layout's AcDbPlotSettings in DXF text.+fn plot_paper_groups(dxf: &str, layout: &str) -> (f64, f64, i64) {+    let lines: Vec<&str> = dxf.lines().map(str::trim).collect();+    let pairs: Vec<(&str, &str)> = lines.chunks(2).filter(|c| c.len() == 2).map(|c| (c[0], c[1])).collect();+    let name = pairs.windows(2).position(|w| w[0] == ("100", "AcDbLayout") && w[1] == ("1", layout)).unwrap_or_else(|| panic!("no LAYOUT {layout}"));+    let start = pairs[..name].iter().rposition(|p| *p == ("100", "AcDbPlotSettings")).unwrap();+    let get = |code: &str| pairs[start..name].iter().find(|p| p.0 == code).map(|p| p.1).unwrap();+    (get("44").parse().unwrap(), get("45").parse().unwrap(), get("73").parse().unwrap())+}++#[test]+fn landscape_media_keeps_its_size_order_and_rotation() {+    // A media defined wider than tall, as ZWCAD and AutoCAD name it (#1463).+    let media = "ISO full bleed A3 (420.00 x 297.00 MM)";+    for (landscape, rotation) in [(true, 0), (false, 1)] {+        let mut d = sample();+        {+            let p = &mut d.layouts[0].page;+            p.paper = media.into();+            p.width_mm = 297.0;+            p.height_mm = 420.0;+            p.landscape = landscape;+        }+        let dxf = write_dxf(&d);+        assert_eq!(plot_paper_groups(&dxf, "Layout1"), (420.0, 297.0, rotation), "DXF, landscape {landscape}");+        let r = read_dxf(dxf.as_bytes()).unwrap();+        let p = &r.layout("Layout1").unwrap().page;+        assert_eq!((p.width_mm, p.height_mm, p.landscape), (297.0, 420.0, landscape), "DXF read back");+        assert_eq!(p.paper, media);++        let dwg = write(&d, "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"), (420.0, 297.0, rotation), "DWG, landscape {landscape}");+        let r = read(&dwg, "t.dwg").unwrap();+        let p = &r.layout("Layout1").unwrap().page;+        assert_eq!((p.width_mm, p.height_mm, p.landscape), (297.0, 420.0, landscape), "DWG read back");+    }+}++#[test]+fn portrait_media_is_written_as_before() {+    let mut d = sample();+    {+        let p = &mut d.layouts[0].page;+        p.paper = "ISO A3 (297.00 x 420.00 MM)".into();+        p.width_mm = 297.0;+        p.height_mm = 420.0;+        p.landscape = true;+    }+    assert_eq!(plot_paper_groups(&write_dxf(&d), "Layout1"), (297.0, 420.0, 1));+}+ // --------------------------------------------------------------------------------------------- // Round trips of styles, overrides, associativity, constraints and tables. 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-23 · **Change:** minor (DXF/DWG save keeps a landscape media's paper size order and plot rotation) · **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@@ -140,6 +140,7 @@ our output in AutoCAD black-box.  | Date | Change | Summary | |---|---|---|+| 2026-10-23 | minor | DXF/DWG save: a layout whose media is named wider than tall (`ISO_full_bleed_A3_(420.00_x_297.00_MM)`) keeps its paper size in that order (44 = 420, 45 = 297) and its plot rotation relative to the media, instead of a portrait size with rotation 1 that ZWCAD plotted as a shrunk portrait page (#1463); other media are written as before | | 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 |