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
@@ -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>) {@@ -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. @@ -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 |