DWG: write AutoCAD's drawing variable dictionary (AC1018 output) (#1872)

* DWG: write AutoCAD's drawing variable dictionary (fixes #1813)

A 2004 (AC1018) DWG of a drawing without an AcDbVariableDictionary is read as
empty by Vectric Aspire 10.5, though the same drawing as DXF imports. AutoCAD
always writes the dictionary; CADCraft's and ezdxf's DXF writers don't.

The DWG bridge now adds it, with AutoCAD's usual variables, when the drawing
has none, for DWG versions before 2018 (AC1032 output reads fine without it).
A dictionary the drawing already has keeps its entries and values.

* docs: document the functions touched by #1813

* DWG: keep the drawing's DIMASSOC and HIDETEXT in the variable dictionary

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Nicholas Gower
2026-10-24 01:59:40 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent 5cfe1a4149
commit 7aa952b8da
2 changed files with 215 additions and 0 deletions
+68
View File
@@ -541,12 +541,80 @@ mod native {
dimension_flags(&mut doc);
drop_unwritable(&mut doc);
current_dimstyle(&mut doc);
add_variable_dictionary(&mut doc);
current_text_style(&mut doc);
std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DwgWriter::write_to_vec(&doc)))
.map_err(|_| "the DWG writer failed on this drawing".to_string())?
.map_err(|e| format!("DWG write: {e}"))
}
/// AutoCAD's entry name for the drawing variables dictionary (the DXF Reference calls it the
/// ACDBVARIABLEDICTIONARY; dictionary keys are case-insensitive).
const VARIABLE_DICTIONARY: &str = "AcDbVariableDictionary";
/// A style or scale name, or `default` when the drawing has none.
fn name_or_default(name: &str, default: &str) -> String {
let name = name.trim();
if name.is_empty() { default.to_string() } else { name.to_string() }
}
/// A drawing variable's value as text, or `default` when the header holds a value the
/// variable doesn't accept (a hostile file can put any integer in it).
fn variable_value(value: i16, valid: &[i16], default: i16) -> String {
if valid.contains(&value) { value.to_string() } else { default.to_string() }
}
/// Add the drawing variables dictionary AutoCAD always writes into the named object
/// dictionary, with the variables it keeps there, when the drawing has none (#1813).
/// Vectric Aspire 10.5 reads a 2004 (AC1018) DWG without it as empty ("No data was imported
/// from the DXF / DWG file"), though it reads the same drawing as DXF — which has no such
/// dictionary either, as CADCraft's and ezdxf's writers don't make one. AutoCAD's AC1032 DWGs
/// read there without it, so newer output is left as it was, and a dictionary the drawing
/// does have keeps its own entries and values.
fn add_variable_dictionary(doc: &mut acadrust::CadDocument) {
use acadrust::objects::{Dictionary, DictionaryVariable, ObjectType};
if doc.version >= acadrust::DxfVersion::AC1032 {
return;
}
let root_handle = doc.header.named_objects_dict_handle;
match doc.objects.get(&root_handle) {
Some(ObjectType::Dictionary(root)) => {
if root.get(VARIABLE_DICTIONARY).is_some() {
return;
}
}
_ => return,
}
// The variables AutoCAD writes for a drawing with default styles and a 1:1 annotation
// scale. DIMASSOC and HIDETEXT keep the drawing's own header values; LAYEREVAL and
// LAYERNOTIFY have no DXF header variable.
let variables = [
("CMLEADERSTYLE", name_or_default(&doc.header.current_mleader_style_name, "Standard")),
("CTABLESTYLE", name_or_default(&doc.header.current_table_style_name, "Standard")),
("CANNOSCALE", name_or_default(&doc.header.current_annotation_scale, "1:1")),
("DIMASSOC", variable_value(doc.header.dimension_associativity, &[0, 1, 2], 2)),
("HIDETEXT", variable_value(doc.header.hide_text, &[0, 1], 1)),
("LAYEREVAL", "0".to_string()),
("LAYERNOTIFY", "0".to_string()),
];
let dictionary_handle = doc.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = dictionary_handle;
dictionary.owner = root_handle;
for (name, value) in variables {
let handle = doc.allocate_handle();
let mut variable = DictionaryVariable::new(name, &value);
variable.handle = handle;
variable.owner_handle = dictionary_handle;
dictionary.add_entry(name, handle);
doc.objects.insert(handle, ObjectType::DictionaryVariable(variable));
}
doc.objects.insert(dictionary_handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root)) = doc.objects.get_mut(&root_handle) {
root.add_entry(VARIABLE_DICTIONARY, dictionary_handle);
}
}
/// Entities of a layout other than the one in `*Paper_Space` are written owned by their block
/// (entity mode 0, with its handle). acadrust writes every paper-space entity with mode 1,
/// "paper space", which AutoCAD reads as the `*Paper_Space` block's: everything drawn on a
@@ -0,0 +1,147 @@
//! #1813: AutoCAD always writes an `AcDbVariableDictionary` in the named object dictionary;
//! ezdxf, and CADCraft's own DXF writer, do not. Vectric Aspire 10.5 reads an AC1018 DWG without
//! it as empty ("No data was imported..."), though it reads the same drawing as DXF (and as an
//! AC1032 DWG) fine. The DWG bridge adds the dictionary AutoCAD writes, with its usual variables,
//! when the DXF has none.
#![cfg(not(target_arch = "wasm32"))]
use std::io::Cursor;
use cadcraft_doc::{Common, Drawing, EntityKind, LwPolyline, Space};
use cadcraft_geom::{PolyVertex, Vec2};
use cadcraft_io::{SaveOptions, Version, write_with};
/// ezdxf's rectangle: one closed LWPOLYLINE in model space.
fn rectangle() -> Drawing {
let mut d = Drawing::new_metric();
let corners = [(0.0, 0.0), (4.0, 0.0), (4.0, 2.0), (0.0, 2.0)];
let pl = LwPolyline {
vertices: corners.iter().map(|(x, y)| PolyVertex::new(Vec2::new(*x, *y))).collect(),
closed: true,
const_width: 0.0,
elevation: 0.0,
plinegen: false,
};
d.add(&Space::Model, Common::default(), EntityKind::LwPolyline(pl)).expect("add the rectangle");
d
}
/// The drawing variables in the named object dictionary, with their values, or nothing when the
/// dictionary is missing. Dictionary keys are case-insensitive in AutoCAD; its own spelling is
/// `AcDbVariableDictionary` (the DXF Reference calls it the ACDBVARIABLEDICTIONARY).
fn variable_dictionary(doc: &acadrust::CadDocument) -> Option<Vec<(String, String)>> {
let acadrust::objects::ObjectType::Dictionary(root) = doc.objects.get(&doc.header.named_objects_dict_handle)? else {
return None;
};
let acadrust::objects::ObjectType::Dictionary(vars) = doc.objects.get(&root.get("AcDbVariableDictionary")?)? else {
return None;
};
Some(
vars.entries
.iter()
.filter_map(|(name, handle)| match doc.objects.get(handle) {
Some(acadrust::objects::ObjectType::DictionaryVariable(v)) => Some((name.clone(), v.value.clone())),
_ => None,
})
.collect(),
)
}
/// Read a DWG back with acadrust's reader, so the assertions inspect the file as another
/// application would rather than through CADCraft's own importer.
fn read_dwg(dwg: &[u8]) -> acadrust::CadDocument {
acadrust::DwgReader::from_stream(Cursor::new(dwg)).read().expect("read the DWG back")
}
/// The value of the variable called `name`, matched case-insensitively as AutoCAD matches
/// dictionary keys, or nothing when the dictionary has no such entry.
fn value<'a>(vars: &'a [(String, String)], name: &str) -> Option<&'a str> {
vars.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(_, v)| v.as_str())
}
/// #1813's regression: a DXF written without an `AcDbVariableDictionary` (as ezdxf and CADCraft
/// write them) must gain AutoCAD's dictionary when exported as an AC1018 DWG, so readers that
/// require it see the drawing's contents; the added objects must not break CADCraft's own read.
#[test]
fn an_ac1018_dwg_gets_the_variable_dictionary_autocad_always_writes() {
let d = rectangle();
let dxf = write_with(&d, "rect.dxf", &SaveOptions { version: Some(Version::R2000), binary: false }).expect("write the DXF");
let text = String::from_utf8_lossy(&dxf).to_ascii_lowercase();
assert!(!text.contains("acdbvariabledictionary"), "this DXF is one without the dictionary, like ezdxf's");
let dwg = write_with(&d, "rect.dwg", &SaveOptions { version: Some(Version::R2004), binary: false }).expect("write the DWG");
assert!(dwg.starts_with(b"AC1018"), "the DWG is AutoCAD 2004");
let doc = read_dwg(&dwg);
let vars = variable_dictionary(&doc).expect("AC1018 DWG: the named object dictionary has an AcDbVariableDictionary");
// The variables AutoCAD keeps there, by name (Aspire needs at least the dictionary; these
// are the ones the reference application writes).
for (name, want) in [("CMLEADERSTYLE", "Standard"), ("CTABLESTYLE", "Standard"), ("CANNOSCALE", "1:1"), ("DIMASSOC", "2"), ("HIDETEXT", "1")] {
assert_eq!(value(&vars, name), Some(want), "{name} in {vars:?}");
}
assert!(value(&vars, "LAYEREVAL").is_some() && value(&vars, "LAYERNOTIFY").is_some(), "{vars:?}");
// The added objects don't disturb CADCraft's own read of the file: the rectangle is there.
let back = cadcraft_io::read(&dwg, "rect.dwg").expect("CADCraft reads its own 2004 DWG");
assert_eq!(back.model.len(), 1);
}
/// The dictionary must carry the drawing's own `$DIMASSOC` and `$HIDETEXT`, not AutoCAD's
/// defaults: a drawing that turned text hiding off or stores dimensions non-associatively keeps
/// those settings for every consumer that reads them from the dictionary.
#[test]
fn the_dictionary_keeps_the_drawings_dimassoc_and_hidetext() {
let dxf = "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DIMASSOC\n70\n1\n9\n$HIDETEXT\n70\n0\n0\nENDSEC\n0\nEOF\n";
let dwg = cadcraft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1018").expect("write the DWG");
let doc = read_dwg(&dwg);
let vars = variable_dictionary(&doc).expect("AC1018 DWG: the named object dictionary has an AcDbVariableDictionary");
assert_eq!(value(&vars, "DIMASSOC"), Some("1"), "DIMASSOC in {vars:?}");
assert_eq!(value(&vars, "HIDETEXT"), Some("0"), "HIDETEXT in {vars:?}");
// The DWG's own header settings match the dictionary, so both kinds of consumer agree.
assert_eq!(doc.header.dimension_associativity, 1);
assert_eq!(doc.header.hide_text, 0);
}
/// A header variable holding a value the variable doesn't accept is written with AutoCAD's
/// default, as a hostile file can put any integer there.
#[test]
fn impossible_header_values_fall_back_to_the_defaults() {
let dxf = "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DIMASSOC\n70\n9\n9\n$HIDETEXT\n70\n7\n0\nENDSEC\n0\nEOF\n";
let dwg = cadcraft_dwg::dxf_to_dwg_as(dxf.as_bytes(), "AC1018").expect("write the DWG");
let doc = read_dwg(&dwg);
let vars = variable_dictionary(&doc).expect("AC1018 DWG: the named object dictionary has an AcDbVariableDictionary");
assert_eq!(value(&vars, "DIMASSOC"), Some("2"), "DIMASSOC in {vars:?}");
assert_eq!(value(&vars, "HIDETEXT"), Some("1"), "HIDETEXT in {vars:?}");
}
/// The AC1032 (AutoCAD 2018) path is deliberately unchanged: those DWGs read in Vectric Aspire
/// without the dictionary, so no dictionary is injected and the existing output stays as it was.
#[test]
fn an_ac1032_dwg_stays_as_it_was() {
// 2018 DWGs read fine in Aspire without the dictionary, so the latest format is unchanged.
let d = rectangle();
let dwg = write_with(&d, "rect.dwg", &SaveOptions { version: Some(Version::R2018), binary: false }).expect("write the DWG");
assert!(dwg.starts_with(b"AC1032"));
let doc = read_dwg(&dwg);
assert_eq!(variable_dictionary(&doc), None, "no dictionary is added to a 2018 DWG");
}
/// A drawing that already has the dictionary must keep its own entries and values: conversion
/// only supplies AutoCAD's dictionary when it is absent, rather than merging defaults into one
/// that a producer such as ezdxf's `add_dict_var` already wrote.
#[test]
fn a_variable_dictionary_the_drawing_has_is_left_alone() {
// A drawing that already has the dictionary keeps its own values and entries (here only
// CMLEADERSTYLE, as ezdxf's add_dict_var("CMLEADERSTYLE", "Standard") writes it): a DWG
// export must not add defaults next to them.
let mut source = acadrust::CadDocument::new();
source.version = acadrust::DxfVersion::AC1015;
assert!(source.set_drawing_variable("CMLEADERSTYLE", "Notes"));
let dxf = acadrust::DxfWriter::new(&source).write_to_vec().expect("write a DXF with the dictionary");
let dwg = cadcraft_dwg::dxf_to_dwg_as(&dxf, "AC1018").expect("write the DWG");
let doc = read_dwg(&dwg);
let vars = variable_dictionary(&doc).expect("the drawing's dictionary is kept");
assert_eq!(vars.len(), 1, "{vars:?}");
assert_eq!(value(&vars, "CMLEADERSTYLE"), Some("Notes"), "{vars:?}");
}