fix(dwg): read kept objects as the file's version on every save, not just the first

The bridge took the version of the kept groups from $ACADVER, but SAVEAS writes
the saved version there and the kept groups stay as they were read. After a 2018
drawing was saved as a 2004 DWG, the next save read its mental ray render
settings as 2004 (class version 2, which the ODA File Converter rejects) and a
2004 field lost its cached value. Foreign now records the $ACADVER of the file
the kept data was read from (Foreign::version), and the DWG save passes that.

The new declare_version test also moves out of the doc comment it was pasted
into. Both reported by austin62jake on #2471.
This commit is contained in:
ffishman
2026-10-30 09:42:40 -03:00
parent 1a322faa0b
commit cef9ee4b10
5 changed files with 48 additions and 17 deletions
+4
View File
@@ -75,6 +75,10 @@ pub struct Foreign {
/// surfaces, which from 2013 on those entities don't carry themselves: each `ACDSRECORD`
/// names its entity with a 320 handle (see [`Foreign::data_store_entities`]).
pub data_store: Vec<RawTag>,
/// The `$ACADVER` of the file all this was read from: its groups are in that version's form.
/// The drawing's own `$ACADVER` is the version it is saved as, which SAVEAS changes, so a
/// later save must not take it for this one (#2471).
pub version: Option<String>,
}
impl Foreign {
+15 -15
View File
@@ -1298,21 +1298,6 @@ mod tests {
}
/// A kept version not newer than the declared one, a binary DXF and a DXF without `$ACADVER`
/// The header is read as group code and value pairs up to the first `0`/`ENDSEC`: a `$ACADVER`
/// name whose value would come after it, or a file that never ends its HEADER, is no version
/// to replace, and `\r\n` lines keep the value's offset right.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn declare_version_reads_the_header_in_pairs_up_to_endsec() {
let cut = b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n 9\r\n$ACADVER\r\n 0\r\nENDSEC\r\n 1\r\nAC1015\r\n";
assert_eq!(native::declare_version(cut, acadrust::DxfVersion::AC1032), None);
let endless = vec![b'\n'; 1 << 20];
assert_eq!(native::declare_version(&endless, acadrust::DxfVersion::AC1032), None);
let crlf = b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n 9\r\n$ACADVER\r\n 1\r\n AC1015\r\n 0\r\nENDSEC\r\n";
let out = native::declare_version(crlf, acadrust::DxfVersion::AC1032).unwrap();
assert_eq!(out, b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n 9\r\n$ACADVER\r\n 1\r\n AC1032\r\n 0\r\nENDSEC\r\n");
}
/// are read as they are.
#[cfg(not(target_arch = "wasm32"))]
#[test]
@@ -1328,6 +1313,21 @@ mod tests {
assert_eq!(native::declare_version(late, acadrust::DxfVersion::AC1032), None);
}
/// The header is read as group code and value pairs up to the first `0`/`ENDSEC`: a `$ACADVER`
/// name whose value would come after it, or a file that never ends its HEADER, is no version
/// to replace, and `\r\n` lines keep the value's offset right.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn declare_version_reads_the_header_in_pairs_up_to_endsec() {
let cut = b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n 9\r\n$ACADVER\r\n 0\r\nENDSEC\r\n 1\r\nAC1015\r\n";
assert_eq!(native::declare_version(cut, acadrust::DxfVersion::AC1032), None);
let endless = vec![b'\n'; 1 << 20];
assert_eq!(native::declare_version(&endless, acadrust::DxfVersion::AC1032), None);
let crlf = b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n 9\r\n$ACADVER\r\n 1\r\n AC1015\r\n 0\r\nENDSEC\r\n";
let out = native::declare_version(crlf, acadrust::DxfVersion::AC1032).unwrap();
assert_eq!(out, b" 0\r\nSECTION\r\n 2\r\nHEADER\r\n 9\r\n$ACADVER\r\n 1\r\n AC1032\r\n 0\r\nENDSEC\r\n");
}
/// Declared as 2007 or later, a DXF is UTF-8: code page text is decoded first. Declared as an
/// older version, the code page bytes stay as they are.
#[cfg(not(target_arch = "wasm32"))]
+1
View File
@@ -452,6 +452,7 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) {
f.data_store = ds.tags.iter().take(MAX_DATA_STORE_GROUPS).map(raw).collect();
}
if !f.is_empty() {
f.version = d.header.get("ACADVER").and_then(cadcraft_doc::HVal::as_str).map(str::to_string);
d.foreign = Arc::new(f);
}
}
+3 -2
View File
@@ -102,11 +102,12 @@ pub fn write_with(d: &Drawing, name: impl AsRef<Path>, opts: &SaveOptions) -> Re
// one, at least 2004: R2000 DWG has no true colours, transparency, table styles or
// gradients, so a drawing from an R2000 DXF doesn't lose them by default. The objects
// CADCraft keeps without modelling them are still in the groups of the file they came
// from (`$ACADVER`), and the bridge reads them as that version.
// from (`Foreign::version`, not `$ACADVER`, which SAVEAS changes), and the bridge reads
// them as that version.
Some(Format::Dwg) if !opts.binary => {
let version = opts.version.unwrap_or_else(|| version.max(Version::R2004));
let bridge = dxf_write::dwg_bridge(d, version);
match d.header.get("ACADVER").and_then(cadcraft_doc::HVal::as_str) {
match d.foreign.version.as_deref() {
Some(kept) => cadcraft_dwg::dxf_to_dwg_kept_as(bridge.as_bytes(), version.code(), kept),
None => cadcraft_dwg::dxf_to_dwg_as(bridge.as_bytes(), version.code()),
}
@@ -235,6 +235,31 @@ fn kept_objects_survive(version: &str, target: Version) {
assert!(wrong.is_empty(), "{version} → {target:?}:\n{}", wrong.join("\n"));
}
/// A drawing saved once as an older DWG has that version as its `$ACADVER` (SAVEAS sets it, so a
/// later QSAVE writes it again), but its kept objects are still in the 2018 file's groups. The
/// next save must still read them as 2018: read as the saved version, the mental ray settings
/// came out a class version too high (the ODA refuses the DWG) and a 2004 field lost its cached
/// value.
#[test]
fn kept_groups_keep_their_version_across_saves() {
let dxf = synthetic("AC1032");
for (first, second) in [(Version::R2004, Version::R2004), (Version::R2010, Version::R2018), (Version::R2018, Version::R2018)] {
let mut source = acadrust::DxfReader::from_reader(Cursor::new(dxf.clone().into_bytes())).unwrap().read().unwrap();
source.version = acad(second);
let reference = kept(&acadrust::DwgReader::from_stream(Cursor::new(acadrust::DwgWriter::write_to_vec(&source).unwrap())).read().unwrap());
let mut d = read(dxf.as_bytes(), "kept.dxf").unwrap();
write_with(&d, "kept.dwg", &SaveOptions { version: Some(first), binary: false }).unwrap();
// What SAVEAS does to the drawing after writing it.
d.header.set_str("ACADVER", first.code());
let dwg = write_with(&d, "kept.dwg", &SaveOptions { version: Some(second), binary: false }).unwrap();
let written = kept(&acadrust::DwgReader::from_stream(Cursor::new(dwg)).read().unwrap());
let field = |k: &Kept| k.field.as_ref().map(|f| (f.value.clone(), f.value_string.clone()));
assert_eq!(written.mental_ray, reference.mental_ray, "{first:?} then {second:?}: mental ray render settings");
assert_eq!(field(&written), field(&reference), "{first:?} then {second:?}: field");
}
}
#[test]
fn a_2018_files_kept_objects_reach_a_2018_dwg() {
kept_objects_survive("AC1032", Version::R2018);