From 4f74e9c7bcd00f0aeb034cf2846cca6aa086e773 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:24:45 -0400 Subject: [PATCH 1/6] =?UTF-8?q?feat(objects):=20decode=20VIEWPORT=20ENTITY?= =?UTF-8?q?=20HEADER=20from=20=C2=A720.4.70=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header the older releases keep for each viewport entity is the name and flags a symbol-table entry opens with plus one more flag. It had no decoder, so all 6 records of the corpus (R14 and R2000) were skipped; all 6 close on their data-stream boundary, and so does the spec's own R14 example (§20.4.70.1). AutoCAD's DXF exports of the corpus hold no record for the header, so the evidence is the boundary alone. --- ARCHITECTURE.md | 30 ++ docs/data/spec-coverage.tsv | 2 +- docs/spec-coverage.md | 6 +- examples/probe_vp_ent_hdr.rs | 52 +++ examples/support/twin.rs | 391 +++++++++++++++++++ src/entities/dispatch.rs | 9 + src/objects/mod.rs | 2 + src/objects/vp_ent_hdr.rs | 60 +++ tests/fixtures/spec-examples/r14-objects.tsv | 2 +- tools/corpus/baseline.tsv | 4 +- 10 files changed, 551 insertions(+), 7 deletions(-) create mode 100644 examples/probe_vp_ent_hdr.rs create mode 100644 examples/support/twin.rs create mode 100644 src/objects/vp_ent_hdr.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8fba91cb..25d953b1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1828,6 +1828,36 @@ cargo run --release --example probe_cross_release -- arc_R14.dwg arc_2004.dwg LA --ignore=shade_plot_mode,shade_plot_resolution_level,shade_plot_custom_dpi,viewport_count,trailing_strings ``` +## 7x. VIEWPORT ENTITY HEADER (2026-10-26, #146) + +§20.4.70 prints the header the older releases keep for each viewport entity +(type `0x47`): the name and flags a symbol-table entry opens with, as +`TV` name, `B` 64-flag, `BS` xrefindex + 1 and `B` xdep, then one more `B`, +"1 flag". Only R14 and R2000 drawings hold one. The handle references (the +control object, the extension dictionary, the external reference block, the +viewport entity it describes and the previous header of the chain) are in the +handle stream and are not read. The record had no decoder, so its type +stayed `Unhandled`. + +All 6 records of the corpus close on their data-stream boundary, 3 on +R14 and 3 on R2000 (the `RL` inside the common object data on R14, the +prologue's on R2000): the names read `""`, `"1"` and `""` on each, the +64-flag and the 1 flag are set and the xref index is 0, the shape of a +header with no xref behind it. This is boundary evidence alone. AutoCAD's DXF +exports of the corpus hold no record for the header (R14's is R12 DXF, which +has no OBJECTS section, and R2000's has no viewport entity table), so no value +is compared. The spec's own example (§20.4.70.1, an R14 record) now runs +through the decoder as well, and `spec_examples` records its outcome. + +```sh +cargo run --release --example probe_vp_ent_hdr -- ../dwg-corpus +``` + +| Release | Records | Close | Values compared with a DXF | +|---|---|---|---| +| R14 | 3 | 3 | no twin | +| R2000 | 3 | 3 | the DXF holds no such record | + ## 8. Write pipeline (current scope) The inverse pipeline is partially shipped. Stage-1 (per-section diff --git a/docs/data/spec-coverage.tsv b/docs/data/spec-coverage.tsv index 9f477225..d5bc9c7f 100644 --- a/docs/data/spec-coverage.tsv +++ b/docs/data/spec-coverage.tsv @@ -94,7 +94,7 @@ 20.4.67 DIMSTYLE CONTROL (68) 177 closes src/objects/control.rs #148 DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (84 equal, 0 by the DXF default, 0 with no DXF group); 135 DXF groups not decoded. The spec's own example (§20.4.67.1) closes on the record's boundary (#107). 20.4.68 DIMSTYLE (69) 178 partial src/tables/dimstyle.rs #148 R13/R14 block order unmeasured (#74); 3 records each on ACadSharp's R2007 and R2010 saves error (#228). DXF oracle (examples/dxf_oracle): 102 records R2000-R2018, every decoded value equals AutoCAD's (667 equal, 1067 by the DXF default, 204 with no DXF group); 1085 DXF groups not decoded; 6 more paired records not decoded. The spec's own example (§20.4.68.1) has no decoder to run (#107). 20.4.69 VIEWPORT ENTITY CONTROL (70) 182 decoder src/objects/control.rs #146 The spec's own example (§20.4.69.1) closes on the record's boundary (#107). -20.4.70 VIEWPORT ENTITY HEADER (71) 183 missing #146 The spec's own example (§20.4.70.1) has no decoder to run (#107). +20.4.70 VIEWPORT ENTITY HEADER (71) 183 closes src/objects/vp_ent_hdr.rs #146 Closes on all 6 VIEWPORT ENTITY HEADER records of the corpus (3 on R14, 3 on R2000; later releases hold none), and on the spec's own example (§20.4.70.1, an R14 record), which closed here for the first time (#107). Boundary evidence only: AutoCAD's DXF exports of the corpus hold no record for the header, so no value is compared (examples/probe_vp_ent_hdr). No R13 file is in the corpus. 20.4.71 AcDbAnnotScaleObjectContextData 184 missing #149 20.4.72 GROUP (72) 184 closes src/objects/acad_group.rs #149 The spec's own example (§20.4.72.1) closes on the record's boundary (#107). 20.4.73 MLINESTYLE (73) 185 closes src/objects/acad_mlinestyle.rs #147 All 10 corpus records, four release bands (ARCHITECTURE §7d.3). DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (462 equal, 0 by the DXF default, 84 with no DXF group); 168 DXF groups not decoded. The spec's own example (§20.4.73.1) closes on the record's boundary (#107). diff --git a/docs/spec-coverage.md b/docs/spec-coverage.md index f806ce9b..ee60becb 100644 --- a/docs/spec-coverage.md +++ b/docs/spec-coverage.md @@ -18,8 +18,8 @@ One row for every section of the *Open Design Specification for .dwg files* v5.4 ## Summary -- All sections: 0 `complete`, 79 `closes`, 6 `decoder`, 19 `partial`, 34 `missing`. -- The 104 object prescriptions of §20.4: 0 `complete`, 60 `closes`, 6 `decoder`, 16 `partial`, 22 `missing`. +- All sections: 0 `complete`, 80 `closes`, 6 `decoder`, 19 `partial`, 33 `missing`. +- The 104 object prescriptions of §20.4: 0 `complete`, 61 `closes`, 6 `decoder`, 16 `partial`, 21 `missing`. ## Sections @@ -120,7 +120,7 @@ One row for every section of the *Open Design Specification for .dwg files* v5.4 | §20.4.67 | DIMSTYLE CONTROL (68) | 177 | `closes` | src/objects/control.rs | #148 | DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (84 equal, 0 by the DXF default, 0 with no DXF group); 135 DXF groups not decoded. The spec's own example (§20.4.67.1) closes on the record's boundary (#107). | | §20.4.68 | DIMSTYLE (69) | 178 | `partial` | src/tables/dimstyle.rs | #148 | R13/R14 block order unmeasured (#74); 3 records each on ACadSharp's R2007 and R2010 saves error (#228). DXF oracle (examples/dxf_oracle): 102 records R2000-R2018, every decoded value equals AutoCAD's (667 equal, 1067 by the DXF default, 204 with no DXF group); 1085 DXF groups not decoded; 6 more paired records not decoded. The spec's own example (§20.4.68.1) has no decoder to run (#107). | | §20.4.69 | VIEWPORT ENTITY CONTROL (70) | 182 | `decoder` | src/objects/control.rs | #146 | The spec's own example (§20.4.69.1) closes on the record's boundary (#107). | -| §20.4.70 | VIEWPORT ENTITY HEADER (71) | 183 | `missing` | | #146 | The spec's own example (§20.4.70.1) has no decoder to run (#107). | +| §20.4.70 | VIEWPORT ENTITY HEADER (71) | 183 | `closes` | src/objects/vp_ent_hdr.rs | #146 | Closes on all 6 VIEWPORT ENTITY HEADER records of the corpus (3 on R14, 3 on R2000; later releases hold none), and on the spec's own example (§20.4.70.1, an R14 record), which closed here for the first time (#107). Boundary evidence only: AutoCAD's DXF exports of the corpus hold no record for the header, so no value is compared (examples/probe_vp_ent_hdr). No R13 file is in the corpus. | | §20.4.71 | AcDbAnnotScaleObjectContextData | 184 | `missing` | | #149 | | | §20.4.72 | GROUP (72) | 184 | `closes` | src/objects/acad_group.rs | #149 | The spec's own example (§20.4.72.1) closes on the record's boundary (#107). | | §20.4.73 | MLINESTYLE (73) | 185 | `closes` | src/objects/acad_mlinestyle.rs | #147 | All 10 corpus records, four release bands (ARCHITECTURE §7d.3). DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (462 equal, 0 by the DXF default, 84 with no DXF group); 168 DXF groups not decoded. The spec's own example (§20.4.73.1) closes on the record's boundary (#107). | diff --git a/examples/probe_vp_ent_hdr.rs b/examples/probe_vp_ent_hdr.rs new file mode 100644 index 00000000..89c94291 --- /dev/null +++ b/examples/probe_vp_ent_hdr.rs @@ -0,0 +1,52 @@ +//! Does the §20.4.70 VIEWPORT ENTITY HEADER field list close on every corpus +//! record? +//! +//! # What this proves +//! +//! For every VP_ENT_HDR (type 0x47) of every drawing under the corpus +//! directory this reports whether the crate's decoder closed exactly on the +//! record's data-stream boundary (ARCHITECTURE.md §7i), and prints the entry +//! name and flags it read. Only R14 and R2000 drawings hold one, and +//! AutoCAD's DXF exports of the corpus (R12 for R14, R2000's own) hold no +//! record for it, so there is nothing to compare values with: the evidence is +//! the boundary. +//! +//! ```sh +//! python3 -I tools/corpus/fetch.py --dest ../dwg-corpus +//! cargo run --release --example probe_vp_ent_hdr -- ../dwg-corpus +//! ``` +//! +//! Exit status is non-zero when a record errors or is not decoded. + +#[path = "support/twin.rs"] +mod twin; + +use dwg::ObjectType; +use dwg::entities::DecodedEntity; +use std::process::ExitCode; +use twin::{Probe, Record, Tally}; + +fn compare(decoded: &DecodedEntity, _rec: Option<&Record>, _t: &mut Tally) -> bool { + let DecodedEntity::VpEntHdr(ours) = decoded else { + return false; + }; + println!( + " name {:?}, 64-flag {}, xref index + 1 {}, xdep {}, 1 flag {}", + ours.name, ours.flag_64, ours.xref_index_plus_1, ours.is_xref_dependent, ours.flag_1 + ); + true +} + +fn main() -> ExitCode { + twin::run( + &Probe { + tag: "VP_ENT_HDR", + class: None, + kind: Some(ObjectType::VpEntHdr), + dxf_kind: "", + decodes_on: |_| true, + note: "; the DXF exports hold no VIEWPORT ENTITY HEADER, so only the boundary is checked", + }, + compare, + ) +} diff --git a/examples/support/twin.rs b/examples/support/twin.rs new file mode 100644 index 00000000..41ef6093 --- /dev/null +++ b/examples/support/twin.rs @@ -0,0 +1,391 @@ +//! Walking a corpus directory for the object probes: every drawing opened, +//! its object records decoded as `DwgFile::decoded_entities` does and kept +//! next to their handles, and AutoCAD's DXF export of the drawing beside it +//! when the corpus holds one. +//! +//! The DXF side is read as plain group pairs. A record is a `0` group and +//! the groups up to the next `0`; its handle is its first group 5 (or 105); +//! the groups inside a `102 {...}` / `102 }` bracket (reactors, extension +//! dictionary) are dropped, as `dwg::dxf_reader` treats them. +//! +//! Each probe includes this file and uses part of it, so the unused part is +//! not a warning. + +#![allow(dead_code)] + +use dwg::classes::ClassMap; +use dwg::entities::{DecodedEntity, decode_from_raw, decode_from_raw_with_class_map}; +use dwg::object::RawObject; +use dwg::{DwgFile, ObjectType, Version}; +use std::collections::{BTreeMap, HashMap}; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +pub struct Group { + pub code: u16, + pub value: String, +} + +impl Group { + pub fn int(&self) -> Option { + self.value.trim().parse().ok() + } + + pub fn real(&self) -> Option { + self.value.trim().parse().ok() + } +} + +pub struct Record { + pub kind: String, + pub handle: Option, + /// How many groups 330 the `{ACAD_REACTORS` bracket holds. + pub reactors: usize, + pub groups: Vec, +} + +impl Record { + pub fn first(&self, code: u16) -> Option<&Group> { + self.groups.iter().find(|g| g.code == code) + } +} + +pub struct Twin { + records: Vec, + by_handle: HashMap, +} + +impl Twin { + fn parse(bytes: &[u8]) -> Twin { + let text = String::from_utf8_lossy(bytes); + let mut lines = text.lines(); + let mut records: Vec = Vec::new(); + let mut depth = 0usize; + let mut in_reactors = false; + while let (Some(code), Some(value)) = (lines.next(), lines.next()) { + let code: u16 = code.trim().parse().unwrap_or(u16::MAX); + if code == 0 { + depth = 0; + in_reactors = false; + records.push(Record { + kind: value.trim().to_string(), + handle: None, + reactors: 0, + groups: Vec::new(), + }); + continue; + } + let Some(rec) = records.last_mut() else { + continue; + }; + if code == 102 { + if value.trim_start().starts_with('{') { + depth += 1; + in_reactors = value.trim_start().starts_with("{ACAD_REACTORS"); + } else { + depth = depth.saturating_sub(1); + in_reactors = false; + } + continue; + } + if depth > 0 { + if in_reactors && code == 330 { + rec.reactors += 1; + } + continue; + } + if (code == 5 || code == 105) && rec.handle.is_none() { + rec.handle = u64::from_str_radix(value.trim(), 16).ok(); + } + rec.groups.push(Group { + code, + value: value.to_string(), + }); + } + let mut by_handle = HashMap::new(); + for (i, r) in records.iter().enumerate() { + if let Some(h) = r.handle { + by_handle.entry(h).or_insert(i); + } + } + Twin { records, by_handle } + } + + /// The record whose handle is `handle`. + pub fn record(&self, handle: u64) -> Option<&Record> { + self.by_handle.get(&handle).map(|&i| &self.records[i]) + } +} + +pub struct Drawing { + pub name: String, + pub version: Version, + pub twin: Option, + objects: Vec, + classes: Option, +} + +fn collect(dir: &Path, depth: usize, out: &mut Vec) -> std::io::Result<()> { + for entry in std::fs::read_dir(dir)? { + let path = entry?.path(); + if path.is_dir() && depth < 4 { + collect(&path, depth + 1, out)?; + } else if path + .extension() + .is_some_and(|x| x.eq_ignore_ascii_case("dwg")) + { + out.push(path); + } + } + Ok(()) +} + +/// Every `.dwg` under `dir`, sorted, with its DXF twin: `.dxf` or the +/// ACadSharp samples' `_ascii.dxf`. A drawing with no twin (R14: the +/// nextgis saves were exported only to R12 DXF) is returned without one. +pub fn drawings(dir: &Path) -> Result, String> { + let mut paths = Vec::new(); + collect(dir, 0, &mut paths).map_err(|e| format!("{}: {e}", dir.display()))?; + paths.sort(); + let mut out = Vec::new(); + for path in paths { + let name = path + .file_name() + .map_or_else(String::new, |n| n.to_string_lossy().into_owned()); + let file = DwgFile::open(&path).map_err(|e| format!("{name}: open: {e}"))?; + let objects = match file.all_objects() { + Some(Ok(o)) => o, + Some(Err(e)) => return Err(format!("{name}: object walk: {e}")), + None => return Err(format!("{name}: no object stream")), + }; + let stem = path + .file_stem() + .map_or_else(String::new, |s| s.to_string_lossy().into_owned()); + let parent = path.parent().map_or_else(PathBuf::new, Path::to_path_buf); + let twin = [format!("{stem}.dxf"), format!("{stem}_ascii.dxf")] + .iter() + .map(|n| parent.join(n)) + .find(|p| p.exists()) + .map(|p| { + std::fs::read(&p) + .map(|bytes| Twin::parse(&bytes)) + .map_err(|e| format!("{name}: read DXF: {e}")) + }) + .transpose()?; + out.push(Drawing { + name, + version: file.version(), + twin, + objects, + classes: file.class_map().and_then(Result::ok), + }); + } + Ok(out) +} + +impl Drawing { + /// The decode of every record of the custom class `class`, by handle. + pub fn class_records(&self, class: &str) -> Vec<(u64, DecodedEntity)> { + let Some(map) = self.classes.as_ref() else { + return Vec::new(); + }; + self.objects + .iter() + .filter_map(|raw| { + let ObjectType::Custom(code) = raw.kind else { + return None; + }; + let def = map.by_type_code(code)?; + (def.dxf_class_name == class).then(|| { + ( + raw.handle.value, + decode_from_raw_with_class_map(raw, self.version, map, code), + ) + }) + }) + .collect() + } + + /// The decode of every record of the fixed type `kind`, by handle. + pub fn kind_records(&self, kind: ObjectType) -> Vec<(u64, DecodedEntity)> { + self.objects + .iter() + .filter(|raw| raw.kind == kind) + .map(|raw| (raw.handle.value, decode_from_raw(raw, self.version))) + .collect() + } +} + +/// Counts of how a decoder's values stood against the DXF export. +#[derive(Default)] +pub struct Tally { + pub compared: usize, + pub differ: usize, + pub omitted: usize, +} + +impl Tally { + /// Compare an integer; `None` means the DXF omits the group. + pub fn int(&mut self, what: &str, ours: i64, theirs: Option) { + match theirs { + Some(t) if t == ours => self.compared += 1, + Some(t) => { + self.compared += 1; + self.differ += 1; + println!(" {what}: dwg {ours} dxf {t}"); + } + None => self.omitted += 1, + } + } + + /// Compare a real to the 1e-12 relative tolerance of a 16-digit DXF. + pub fn real(&mut self, what: &str, ours: f64, theirs: Option) { + match theirs { + Some(t) if (ours - t).abs() <= 1e-12 * ours.abs().max(t.abs()).max(1.0) => { + self.compared += 1; + } + Some(t) => { + self.compared += 1; + self.differ += 1; + println!(" {what}: dwg {ours} dxf {t}"); + } + None => self.omitted += 1, + } + } + + /// Compare text. + pub fn text(&mut self, what: &str, ours: &str, theirs: Option<&str>) { + match theirs { + Some(t) if t == ours => self.compared += 1, + Some(t) => { + self.compared += 1; + self.differ += 1; + println!(" {what}: dwg {ours:?} dxf {t:?}"); + } + None => self.omitted += 1, + } + } +} + +/// What one probe measures: the records of `class` (a custom class) and of +/// `kind` (a fixed type), the DXF record type they pair with (empty when the +/// DXF exports hold none, so that only the boundary is checked), and the +/// releases that must decode. +pub struct Probe<'a> { + pub tag: &'a str, + pub class: Option<&'a str>, + pub kind: Option, + pub dxf_kind: &'a str, + pub decodes_on: fn(Version) -> bool, + pub note: &'a str, +} + +/// Run `p` over the corpus directory given as the first argument and print +/// each record, a table by release and a total. `compare` gets the decode +/// and the DXF record (`None` on a drawing with no twin), tallies the +/// values and says whether the decode is the variant the probe expects. +/// The exit status is non-zero when a record that must decode errors or is +/// unhandled, a twin lacks the record, a value differs, or none was compared. +pub fn run( + p: &Probe, + compare: impl Fn(&DecodedEntity, Option<&Record>, &mut Tally) -> bool, +) -> ExitCode { + let Some(dir) = std::env::args().nth(1) else { + eprintln!( + "usage: {} ", + std::env::args().next().unwrap_or_default() + ); + return ExitCode::FAILURE; + }; + let drawings = match drawings(Path::new(&dir)) { + Ok(d) => d, + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + }; + let tag = p.tag; + let mut per_release: BTreeMap = BTreeMap::new(); + let mut total = Tally::default(); + let mut failed = false; + for d in &drawings { + let release = format!("{:?}", d.version); + let mut records = p.class.map_or_else(Vec::new, |c| d.class_records(c)); + records.extend(p.kind.map_or_else(Vec::new, |k| d.kind_records(k))); + for (handle, decoded) in records { + let row = per_release.entry(release.clone()).or_default(); + row[0] += 1; + let name = &d.name; + match &decoded { + DecodedEntity::Error { message, .. } => { + failed = true; + println!("{name} ({release}) {tag} 0x{handle:X} ERROR {message}"); + } + DecodedEntity::Unhandled { .. } if !(p.decodes_on)(d.version) => { + row[2] += 1; + println!("{name} ({release}) {tag} 0x{handle:X} not decoded on this release"); + } + DecodedEntity::Unhandled { .. } => { + failed = true; + println!("{name} ({release}) {tag} 0x{handle:X} UNHANDLED"); + } + _ => { + let mut tally = Tally::default(); + let rec = if p.dxf_kind.is_empty() { + None + } else { + d.twin + .as_ref() + .map(|t| t.record(handle).filter(|r| r.kind == p.dxf_kind)) + }; + if matches!(rec, Some(None)) { + failed = true; + println!( + "{name} ({release}) {tag} 0x{handle:X} closes; the DXF has no {} \ + with this handle", + p.dxf_kind + ); + continue; + } + if !compare(&decoded, rec.flatten(), &mut tally) { + failed = true; + println!("{name} ({release}) {tag} 0x{handle:X} decoded as another type"); + continue; + } + row[1] += 1; + if rec.is_some() { + println!( + "{name} ({release}) {tag} 0x{handle:X} closes; DXF: {} compared, \ + {} differ, {} omitted", + tally.compared, tally.differ, tally.omitted + ); + } else { + println!("{name} ({release}) {tag} 0x{handle:X} closes; no DXF twin"); + } + row[3] += tally.compared; + row[4] += tally.differ; + total.compared += tally.compared; + total.differ += tally.differ; + total.omitted += tally.omitted; + } + } + } + } + println!(); + println!("release records close not decoded compared differ"); + for (release, r) in &per_release { + println!( + "{release:<8} {:>7} {:>5} {:>11} {:>8} {:>6}", + r[0], r[1], r[2], r[3], r[4] + ); + } + println!( + "total: {} values compared, {} differ, {} omitted by the DXF{}", + total.compared, total.differ, total.omitted, p.note + ); + if failed || total.differ > 0 || (total.compared == 0 && !p.dxf_kind.is_empty()) { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +} diff --git a/src/entities/dispatch.rs b/src/entities/dispatch.rs index c4cec2d5..a0f428e5 100644 --- a/src/entities/dispatch.rs +++ b/src/entities/dispatch.rs @@ -132,6 +132,7 @@ pub enum DecodedEntity { XRecord(crate::objects::xrecord::XRecord), Placeholder(crate::objects::placeholder::Placeholder), Group(crate::objects::acad_group::AcadGroup), + VpEntHdr(crate::objects::vp_ent_hdr::VpEntHdr), Scale(crate::objects::acad_scale::AcadScale), VisualStyle(crate::objects::acad_visual_style::AcadVisualStyle), Layout(Box), @@ -245,6 +246,7 @@ impl DecodedEntity { Self::XRecord(_) => OBJECT_TYPE_XRECORD, Self::Placeholder(_) => OBJECT_TYPE_ACDB_PLACEHOLDER, Self::Group(_) => OBJECT_TYPE_GROUP, + Self::VpEntHdr(_) => OBJECT_TYPE_VP_ENT_HDR, Self::Layout(_) => OBJECT_TYPE_LAYOUT, Self::MLineStyle(_) => OBJECT_TYPE_MLINESTYLE, // DICTIONARYVAR and SCALE are custom classes — their codes @@ -343,6 +345,7 @@ const PROXY_OBJECT_ITEM_CLASS_ID: u16 = 0x1F3; // through `dispatch_object`, not through the entity path. const OBJECT_TYPE_DICTIONARY: u16 = 0x2A; // 42 const OBJECT_TYPE_GROUP: u16 = 0x48; // 72 +const OBJECT_TYPE_VP_ENT_HDR: u16 = 0x47; // 71 const OBJECT_TYPE_XRECORD: u16 = 0x4F; // 79 const OBJECT_TYPE_ACDB_PLACEHOLDER: u16 = 0x50; // 80 const OBJECT_TYPE_LAYOUT: u16 = 0x52; // 82 @@ -581,6 +584,7 @@ fn dispatch_object( | ObjectType::XRecord | ObjectType::AcDbPlaceholder | ObjectType::Group + | ObjectType::VpEntHdr | ObjectType::Layout | ObjectType::MLineStyle ) || kind.is_control(); @@ -619,6 +623,11 @@ fn dispatch_object( .map(DecodedEntity::Group) .map_err(|e| e.to_string()) } + ObjectType::VpEntHdr => { + crate::objects::vp_ent_hdr::decode_object(payload, body_start, inline_end, version) + .map(DecodedEntity::VpEntHdr) + .map_err(|e| e.to_string()) + } ObjectType::Layout => { match crate::objects::acad_layout::decode_object( payload, body_start, inline_end, version, diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 645213f4..887ebe6b 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -31,6 +31,7 @@ //! | DICTIONARYVAR | [`dictionary_var`] | §19.6.x | //! | proxy entity | [`proxy_entity`] | §19.4.91 (L7-04) | //! | proxy object | [`proxy_object`] | §19.4.91 (L7-05) | +//! | VP_ENT_HDR | [`vp_ent_hdr`] | §20.4.70 | //! | XData | [`xdata`] | §3.5 (L7-01) | //! | XRECORD | [`xrecord`] | §19.6.5 (L7-02) | //! @@ -97,5 +98,6 @@ pub(crate) mod modern; pub mod placeholder; pub mod proxy_entity; pub mod proxy_object; +pub mod vp_ent_hdr; pub mod xdata; pub mod xrecord; diff --git a/src/objects/vp_ent_hdr.rs b/src/objects/vp_ent_hdr.rs new file mode 100644 index 00000000..6d601703 --- /dev/null +++ b/src/objects/vp_ent_hdr.rs @@ -0,0 +1,60 @@ +//! VIEWPORT ENTITY HEADER (§20.4.70, type 0x47) — the header the older +//! releases keep for each viewport entity of a paper space layout. +//! +//! Only R14 and R2000 drawings hold one; later releases do not. It is a +//! non-entity object whose data stream is the name and flags a symbol-table +//! entry opens with (§20.4.54), then one more flag: +//! +//! ```text +//! TV entry name DXF 2 +//! B 64-flag DXF 70, the 64 bit +//! BS xrefindex + 1 DXF 70; subtract one, -1 means not from an xref +//! B xdep DXF 70, the 16 bit; dependent on an xref +//! B 1 flag DXF 70, the 1 bit +//! ``` +//! +//! Its handle references (the viewport entity control, the extension +//! dictionary, the external reference block, the viewport entity it +//! describes and the previous header of the chain) are in the handle stream +//! and are not read here. The corpus's AutoCAD DXF exports hold no VIEWPORT +//! ENTITY HEADER, so its records are checked against their data-stream +//! boundary alone. + +use crate::error::Result; +use crate::objects::modern; +use crate::version::Version; + +/// A decoded VIEWPORT ENTITY HEADER. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct VpEntHdr { + /// `TV` entry name, DXF 2. + pub name: String, + /// `B` the 64 bit of DXF group 70. + pub flag_64: bool, + /// `BS` xrefindex + 1. + pub xref_index_plus_1: i16, + /// `B` xdep: dependent on an external reference. + pub is_xref_dependent: bool, + /// `B` the 1 bit of DXF group 70. + pub flag_1: bool, +} + +/// Decode a VIEWPORT ENTITY HEADER (§20.4.70) from its raw object payload, +/// checking the data fields end exactly on the data-stream boundary. +pub(crate) fn decode_object( + payload: &[u8], + body_start: usize, + inline_data_end: Option, + version: Version, +) -> Result { + let mut split = modern::open(payload, body_start, inline_data_end, version)?; + let out = VpEntHdr { + name: modern::read_tv(&mut split.data, &mut split.strings, version)?, + flag_64: split.data.read_b()?, + xref_index_plus_1: split.data.read_bs()?, + is_xref_dependent: split.data.read_b()?, + flag_1: split.data.read_b()?, + }; + split.finish("VP_ENT_HDR")?; + Ok(out) +} diff --git a/tests/fixtures/spec-examples/r14-objects.tsv b/tests/fixtures/spec-examples/r14-objects.tsv index d0e22291..e8b53735 100644 --- a/tests/fixtures/spec-examples/r14-objects.tsv +++ b/tests/fixtures/spec-examples/r14-objects.tsv @@ -66,7 +66,7 @@ section pages label type len handle crc expect record 20.4.67 177-178 dimstyle ctrl 44 16 0A BA14 closes 1000510042A480000009034030211D214D20BA14 20.4.68 182 dimstyle 45 112 1D CC33 unhandled 70005140476490300009085354414E44415244C3000400008001800000001029044110240902B5E8DC0F42B1CFC000000000000B03F14AE07A17AD4760FC0AD7A3703D0AC73FAA02B5E8DC0F42B1CFC0AD7A3703D0AB73F86666666666663940640AD7A3703D0AB73FAAAA20851828288800CC33 20.4.69 183 vpent ctrl 46 23 0B 9E84 closes 1700518042E48000000906403021512152215421562158215A9E84 -20.4.70 184 vpent hdr 47 17 58 2F9E unhandled 110051C056245000000ACA085982820ACA8AB42F9E +20.4.70 184 vpent hdr 47 17 58 2F9E closes 110051C056245000000ACA085982820ACA8AB42F9E 20.4.72 185 group 48 39 7B 3569 closes 270052005EEDE0000004050F74686973206973206D7967726F757090140D04350434C145E945B545A13569 20.4.73 186-187 mlstyle 49 85 74 8FAA closes 550052405D27C020000405094D594D4C5354594C45449B5E481B5D5B1D1A5B1A5B99481CDD1E5B1968182D4454FB21F93F060B51153EC87E4FC0C000000000000E03FD01904408000000000000E0BF40903410E410E3008FAA 20.4.74 187-188 proxy 1F9 18 01EA ACDA walk-only 12003E4040807AA700000004040101334041A230ACDA diff --git a/tools/corpus/baseline.tsv b/tools/corpus/baseline.tsv index 6e67f10f..a56e1f1d 100644 --- a/tools/corpus/baseline.tsv +++ b/tools/corpus/baseline.tsv @@ -52,8 +52,8 @@ polyline2d_line_2007.dwg nextgis AC1021 181 5 0 0 0 CELLSTYLEMAP 3>1;DICTIONARYV polyline2d_line_2010.dwg nextgis AC1024 177 4 0 0 0 CELLSTYLEMAP 1>0;DICTIONARYVAR 10>5 0 491 0 0 0 polyline2d_line_2013.dwg nextgis AC1027 128 4 0 0 0 - 0 369 0 0 0 polyline2d_line_R14.dwg nextgis AC1014 283 9 0 0 0 - 0 0 0 0 292 -sample_AC1014.dwg acadsharp AC1014 1050 115 77 0 0 - 0 0 0 0 1242 -sample_AC1015.dwg acadsharp AC1015 922 78 47 0 0 - 0 3160 6 0 0 +sample_AC1014.dwg acadsharp AC1014 1053 112 77 0 0 - 0 0 0 0 1242 +sample_AC1015.dwg acadsharp AC1015 925 75 47 0 0 - 0 3160 6 0 0 sample_AC1018.dwg acadsharp AC1018 854 74 86 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;CELLSTYLEMAP 2>1;DICTIONARYVAR 15>14;TABLECONTENT 9>2;TABLEGEOMETRY 9>2 0 3135 6 0 0 sample_AC1021.dwg acadsharp AC1021 838 72 15 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;TABLECONTENT 8>2;TABLEGEOMETRY 8>2 0 2960 6 0 0 sample_AC1024.dwg acadsharp AC1024 851 57 15 0 0 TABLECONTENT 7>2;TABLEGEOMETRY 7>2 0 3050 6 0 0 From 2ee1ad008b27da7308c42f74c3b2f1810b003bc9 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:36:07 -0400 Subject: [PATCH 2/6] refactor(objects): name the module control_vp_ent_hdr so its entry in the list sits apart from its siblings' (rustfmt sorts the list) --- docs/data/spec-coverage.tsv | 2 +- docs/spec-coverage.md | 2 +- src/entities/dispatch.rs | 12 ++++++------ src/objects/{vp_ent_hdr.rs => control_vp_ent_hdr.rs} | 0 src/objects/mod.rs | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename src/objects/{vp_ent_hdr.rs => control_vp_ent_hdr.rs} (100%) diff --git a/docs/data/spec-coverage.tsv b/docs/data/spec-coverage.tsv index d5bc9c7f..7b6e033f 100644 --- a/docs/data/spec-coverage.tsv +++ b/docs/data/spec-coverage.tsv @@ -94,7 +94,7 @@ 20.4.67 DIMSTYLE CONTROL (68) 177 closes src/objects/control.rs #148 DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (84 equal, 0 by the DXF default, 0 with no DXF group); 135 DXF groups not decoded. The spec's own example (§20.4.67.1) closes on the record's boundary (#107). 20.4.68 DIMSTYLE (69) 178 partial src/tables/dimstyle.rs #148 R13/R14 block order unmeasured (#74); 3 records each on ACadSharp's R2007 and R2010 saves error (#228). DXF oracle (examples/dxf_oracle): 102 records R2000-R2018, every decoded value equals AutoCAD's (667 equal, 1067 by the DXF default, 204 with no DXF group); 1085 DXF groups not decoded; 6 more paired records not decoded. The spec's own example (§20.4.68.1) has no decoder to run (#107). 20.4.69 VIEWPORT ENTITY CONTROL (70) 182 decoder src/objects/control.rs #146 The spec's own example (§20.4.69.1) closes on the record's boundary (#107). -20.4.70 VIEWPORT ENTITY HEADER (71) 183 closes src/objects/vp_ent_hdr.rs #146 Closes on all 6 VIEWPORT ENTITY HEADER records of the corpus (3 on R14, 3 on R2000; later releases hold none), and on the spec's own example (§20.4.70.1, an R14 record), which closed here for the first time (#107). Boundary evidence only: AutoCAD's DXF exports of the corpus hold no record for the header, so no value is compared (examples/probe_vp_ent_hdr). No R13 file is in the corpus. +20.4.70 VIEWPORT ENTITY HEADER (71) 183 closes src/objects/control_vp_ent_hdr.rs #146 Closes on all 6 VIEWPORT ENTITY HEADER records of the corpus (3 on R14, 3 on R2000; later releases hold none), and on the spec's own example (§20.4.70.1, an R14 record), which closed here for the first time (#107). Boundary evidence only: AutoCAD's DXF exports of the corpus hold no record for the header, so no value is compared (examples/probe_vp_ent_hdr). No R13 file is in the corpus. 20.4.71 AcDbAnnotScaleObjectContextData 184 missing #149 20.4.72 GROUP (72) 184 closes src/objects/acad_group.rs #149 The spec's own example (§20.4.72.1) closes on the record's boundary (#107). 20.4.73 MLINESTYLE (73) 185 closes src/objects/acad_mlinestyle.rs #147 All 10 corpus records, four release bands (ARCHITECTURE §7d.3). DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (462 equal, 0 by the DXF default, 84 with no DXF group); 168 DXF groups not decoded. The spec's own example (§20.4.73.1) closes on the record's boundary (#107). diff --git a/docs/spec-coverage.md b/docs/spec-coverage.md index ee60becb..4629f44a 100644 --- a/docs/spec-coverage.md +++ b/docs/spec-coverage.md @@ -120,7 +120,7 @@ One row for every section of the *Open Design Specification for .dwg files* v5.4 | §20.4.67 | DIMSTYLE CONTROL (68) | 177 | `closes` | src/objects/control.rs | #148 | DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (84 equal, 0 by the DXF default, 0 with no DXF group); 135 DXF groups not decoded. The spec's own example (§20.4.67.1) closes on the record's boundary (#107). | | §20.4.68 | DIMSTYLE (69) | 178 | `partial` | src/tables/dimstyle.rs | #148 | R13/R14 block order unmeasured (#74); 3 records each on ACadSharp's R2007 and R2010 saves error (#228). DXF oracle (examples/dxf_oracle): 102 records R2000-R2018, every decoded value equals AutoCAD's (667 equal, 1067 by the DXF default, 204 with no DXF group); 1085 DXF groups not decoded; 6 more paired records not decoded. The spec's own example (§20.4.68.1) has no decoder to run (#107). | | §20.4.69 | VIEWPORT ENTITY CONTROL (70) | 182 | `decoder` | src/objects/control.rs | #146 | The spec's own example (§20.4.69.1) closes on the record's boundary (#107). | -| §20.4.70 | VIEWPORT ENTITY HEADER (71) | 183 | `closes` | src/objects/vp_ent_hdr.rs | #146 | Closes on all 6 VIEWPORT ENTITY HEADER records of the corpus (3 on R14, 3 on R2000; later releases hold none), and on the spec's own example (§20.4.70.1, an R14 record), which closed here for the first time (#107). Boundary evidence only: AutoCAD's DXF exports of the corpus hold no record for the header, so no value is compared (examples/probe_vp_ent_hdr). No R13 file is in the corpus. | +| §20.4.70 | VIEWPORT ENTITY HEADER (71) | 183 | `closes` | src/objects/control_vp_ent_hdr.rs | #146 | Closes on all 6 VIEWPORT ENTITY HEADER records of the corpus (3 on R14, 3 on R2000; later releases hold none), and on the spec's own example (§20.4.70.1, an R14 record), which closed here for the first time (#107). Boundary evidence only: AutoCAD's DXF exports of the corpus hold no record for the header, so no value is compared (examples/probe_vp_ent_hdr). No R13 file is in the corpus. | | §20.4.71 | AcDbAnnotScaleObjectContextData | 184 | `missing` | | #149 | | | §20.4.72 | GROUP (72) | 184 | `closes` | src/objects/acad_group.rs | #149 | The spec's own example (§20.4.72.1) closes on the record's boundary (#107). | | §20.4.73 | MLINESTYLE (73) | 185 | `closes` | src/objects/acad_mlinestyle.rs | #147 | All 10 corpus records, four release bands (ARCHITECTURE §7d.3). DXF oracle (examples/dxf_oracle): 42 records R2000-R2018, every decoded value equals AutoCAD's (462 equal, 0 by the DXF default, 84 with no DXF group); 168 DXF groups not decoded. The spec's own example (§20.4.73.1) closes on the record's boundary (#107). | diff --git a/src/entities/dispatch.rs b/src/entities/dispatch.rs index a0f428e5..451c6d8f 100644 --- a/src/entities/dispatch.rs +++ b/src/entities/dispatch.rs @@ -132,7 +132,7 @@ pub enum DecodedEntity { XRecord(crate::objects::xrecord::XRecord), Placeholder(crate::objects::placeholder::Placeholder), Group(crate::objects::acad_group::AcadGroup), - VpEntHdr(crate::objects::vp_ent_hdr::VpEntHdr), + VpEntHdr(crate::objects::control_vp_ent_hdr::VpEntHdr), Scale(crate::objects::acad_scale::AcadScale), VisualStyle(crate::objects::acad_visual_style::AcadVisualStyle), Layout(Box), @@ -623,11 +623,11 @@ fn dispatch_object( .map(DecodedEntity::Group) .map_err(|e| e.to_string()) } - ObjectType::VpEntHdr => { - crate::objects::vp_ent_hdr::decode_object(payload, body_start, inline_end, version) - .map(DecodedEntity::VpEntHdr) - .map_err(|e| e.to_string()) - } + ObjectType::VpEntHdr => crate::objects::control_vp_ent_hdr::decode_object( + payload, body_start, inline_end, version, + ) + .map(DecodedEntity::VpEntHdr) + .map_err(|e| e.to_string()), ObjectType::Layout => { match crate::objects::acad_layout::decode_object( payload, body_start, inline_end, version, diff --git a/src/objects/vp_ent_hdr.rs b/src/objects/control_vp_ent_hdr.rs similarity index 100% rename from src/objects/vp_ent_hdr.rs rename to src/objects/control_vp_ent_hdr.rs diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 887ebe6b..7e08da84 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -30,8 +30,8 @@ //! | DICTIONARY | [`dictionary`] | §19.5.19 | //! | DICTIONARYVAR | [`dictionary_var`] | §19.6.x | //! | proxy entity | [`proxy_entity`] | §19.4.91 (L7-04) | +//! | VP_ENT_HDR | [`control_vp_ent_hdr`] | §20.4.70 | //! | proxy object | [`proxy_object`] | §19.4.91 (L7-05) | -//! | VP_ENT_HDR | [`vp_ent_hdr`] | §20.4.70 | //! | XData | [`xdata`] | §3.5 (L7-01) | //! | XRECORD | [`xrecord`] | §19.6.5 (L7-02) | //! @@ -91,6 +91,7 @@ pub mod acad_visual_style; pub mod class_map_extension; pub mod color; pub mod control; +pub mod control_vp_ent_hdr; pub mod custom_dict_entry; pub mod dictionary; pub mod dictionary_var; @@ -98,6 +99,5 @@ pub(crate) mod modern; pub mod placeholder; pub mod proxy_entity; pub mod proxy_object; -pub mod vp_ent_hdr; pub mod xdata; pub mod xrecord; From 78863ed3f4bf97d6c4ba8577d089fc9de5c5d2f3 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:36:51 -0400 Subject: [PATCH 3/6] fix(examples): carry the one harness file every probe of this lane shares, so the copies merge in any order --- examples/support/twin.rs | 66 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/examples/support/twin.rs b/examples/support/twin.rs index 41ef6093..eaae66ca 100644 --- a/examples/support/twin.rs +++ b/examples/support/twin.rs @@ -115,6 +115,27 @@ impl Twin { pub fn record(&self, handle: u64) -> Option<&Record> { self.by_handle.get(&handle).map(|&i| &self.records[i]) } + + /// Every record of DXF type `kind`, in file order. + pub fn records_of(&self, kind: &str) -> Vec<&Record> { + self.records.iter().filter(|r| r.kind == kind).collect() + } +} + +/// How a DWG record finds its DXF partner. +pub enum Pairing { + /// The DXF record with the same handle. + Handle, + /// The DXF record with the same handle; failing that, the DXF records of + /// the type that no DWG record shares a handle with, taken in file order + /// against the DWG records that find none. For the objects of a table + /// AutoCAD created while saving, whose handles differ between the DWG and + /// the DXF of the same drawing. + HandleThenOrder, + /// The one record of the DXF type in the twin, for an object AutoCAD + /// writes afresh on every save, so that the two files never share a + /// handle. A twin with none or several has no partner for it. + OnlyOfKind, } pub struct Drawing { @@ -289,6 +310,17 @@ pub struct Probe<'a> { pub fn run( p: &Probe, compare: impl Fn(&DecodedEntity, Option<&Record>, &mut Tally) -> bool, +) -> ExitCode { + run_with_twin(p, Pairing::Handle, |d, r, _, t| compare(d, r, t)) +} + +/// [`run`], with the record's partner found by `pairing` and the whole twin +/// handed to `compare`, for a record whose values also live in a second DXF +/// object. +pub fn run_with_twin( + p: &Probe, + pairing: Pairing, + compare: impl Fn(&DecodedEntity, Option<&Record>, Option<&Twin>, &mut Tally) -> bool, ) -> ExitCode { let Some(dir) = std::env::args().nth(1) else { eprintln!( @@ -312,6 +344,22 @@ pub fn run( let release = format!("{:?}", d.version); let mut records = p.class.map_or_else(Vec::new, |c| d.class_records(c)); records.extend(p.kind.map_or_else(Vec::new, |k| d.kind_records(k))); + let mut by_order: HashMap = HashMap::new(); + if let (Pairing::HandleThenOrder, Some(t), false) = + (&pairing, &d.twin, p.dxf_kind.is_empty()) + { + let mut spare = t + .records_of(p.dxf_kind) + .into_iter() + .filter(|r| !records.iter().any(|(h, _)| r.handle == Some(*h))); + for (h, _) in &records { + if t.record(*h).is_none_or(|r| r.kind != p.dxf_kind) { + if let Some(r) = spare.next() { + by_order.insert(*h, r); + } + } + } + } for (handle, decoded) in records { let row = per_release.entry(release.clone()).or_default(); row[0] += 1; @@ -334,20 +382,28 @@ pub fn run( let rec = if p.dxf_kind.is_empty() { None } else { - d.twin - .as_ref() - .map(|t| t.record(handle).filter(|r| r.kind == p.dxf_kind)) + d.twin.as_ref().map(|t| match pairing { + Pairing::Handle => t.record(handle).filter(|r| r.kind == p.dxf_kind), + Pairing::HandleThenOrder => t + .record(handle) + .filter(|r| r.kind == p.dxf_kind) + .or_else(|| by_order.get(&handle).copied()), + Pairing::OnlyOfKind => { + let all = t.records_of(p.dxf_kind); + (all.len() == 1).then(|| all[0]) + } + }) }; if matches!(rec, Some(None)) { failed = true; println!( "{name} ({release}) {tag} 0x{handle:X} closes; the DXF has no {} \ - with this handle", + to pair it with", p.dxf_kind ); continue; } - if !compare(&decoded, rec.flatten(), &mut tally) { + if !compare(&decoded, rec.flatten(), d.twin.as_ref(), &mut tally) { failed = true; println!("{name} ({release}) {tag} 0x{handle:X} decoded as another type"); continue; From 51ae0f580224016c2e0c69ac83354141bf0ba85b Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:26:08 -0400 Subject: [PATCH 4/6] chore(corpus): record the baseline the Corpus job measured for VIEWPORT ENTITY HEADER (#146) Regenerated with measure.py baseline from the corpus-measurement artifact of the Corpus (public DWG) job on 9338d80. Against main: decoded +6, skipped -6, every other column unchanged (the corpus's 6 VIEWPORT ENTITY HEADER records). --- tools/corpus/baseline.tsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/corpus/baseline.tsv b/tools/corpus/baseline.tsv index c5550111..02575a80 100644 --- a/tools/corpus/baseline.tsv +++ b/tools/corpus/baseline.tsv @@ -52,8 +52,8 @@ polyline2d_line_2007.dwg nextgis AC1021 182 4 0 0 0 CELLSTYLEMAP 3>1;DICTIONARYV polyline2d_line_2010.dwg nextgis AC1024 177 4 0 0 0 CELLSTYLEMAP 1>0;DICTIONARYVAR 10>5 0 491 0 0 0 polyline2d_line_2013.dwg nextgis AC1027 128 4 0 0 0 - 0 369 0 0 0 polyline2d_line_R14.dwg nextgis AC1014 288 4 0 0 0 - 0 0 0 0 292 -sample_AC1014.dwg acadsharp AC1014 1189 53 0 0 0 - 0 0 0 0 1242 -sample_AC1015.dwg acadsharp AC1015 994 53 0 0 0 - 0 3160 6 0 0 +sample_AC1014.dwg acadsharp AC1014 1192 50 0 0 0 - 0 0 0 0 1242 +sample_AC1015.dwg acadsharp AC1015 997 50 0 0 0 - 0 3160 6 0 0 sample_AC1018.dwg acadsharp AC1018 964 50 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;CELLSTYLEMAP 2>1;DICTIONARYVAR 15>14;TABLECONTENT 9>2;TABLEGEOMETRY 9>2 0 3135 6 0 0 sample_AC1021.dwg acadsharp AC1021 877 48 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;TABLECONTENT 8>2;TABLEGEOMETRY 8>2 0 2960 6 0 0 sample_AC1024.dwg acadsharp AC1024 874 49 0 0 0 TABLECONTENT 7>2;TABLEGEOMETRY 7>2 0 3050 6 0 0 From a2b06c82df617bd63589bf269b050b6e6889db2d Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:48:05 -0400 Subject: [PATCH 5/6] docs(architecture): renumber the new section (7au->7bu) after the sections main added --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5e3c8968..309a7d2a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -3669,7 +3669,7 @@ end; characters above the BMP, two escapes each as before and never measured; an strings encoder for the entities, which the DWG writer does not write at all (its only authored strings are the class table's). -## 7au. VIEWPORT ENTITY HEADER (2026-10-26, #146) +## 7bu. VIEWPORT ENTITY HEADER (2026-10-26, #146) §20.4.70 prints the header the older releases keep for each viewport entity (type `0x47`): the name and flags a symbol-table entry opens with, as From 3ab4a090d8afc53ab6668e733ad183ec5f293f85 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:50:30 -0400 Subject: [PATCH 6/6] chore(corpus): commit the baselines CI run 40739158933 measured --- README.md | 14 +++++++------- STATUS.md | 4 ++-- tools/corpus/baseline.tsv | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fe5e9d95..10c2a0f1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This is **0.1.0-alpha.1**. Do not use it in production. Do not benchmark it against the ODA SDK. Do not tell your CAD team dwg-rs solves their interop problem today. Entity and object decode coverage, measured in CI by the `Corpus (public DWG)` -job on `main` ([run 40736084515](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40736084515) on 2026-10-28) with +job on `main` ([run 40739158933](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40739158933) on 2026-10-28) with [`examples/coverage_report.rs`](./examples/coverage_report.rs), on the public drawings fetched by pinned sha256 ([`tools/corpus/pins.tsv`](./tools/corpus/pins.tsv); none is committed): @@ -25,14 +25,14 @@ drawings fetched by pinned sha256 | Version | Files tested | Decoded | Skipped | Errored | Success rate | Median drawing | |---------|--------------|---------|---------|---------|--------------|----------------| -| R14 (AC1014) | 12 | 4734 | 22 | 0 | **99.5 %** | 100.0 % | -| R2000 (AC1015) | 9 | 2782 | 3 | 0 | **99.9 %** | 100.0 % | +| R14 (AC1014) | 12 | 4737 | 19 | 0 | **99.6 %** | 100.0 % | +| R2000 (AC1015) | 9 | 2785 | 0 | 0 | **100.0 %** | 100.0 % | | R2004 (AC1018) | 12 | 106994 | 0 | 0 | **100.0 %** | 100.0 % | | R2007 (AC1021) | 8 | 2237 | 0 | 0 | **100.0 %** | 100.0 % | | R2010 (AC1024) | 8 | 2188 | 0 | 0 | **100.0 %** | 100.0 % | | R2013 (AC1027) | 8 | 1787 | 0 | 0 | **100.0 %** | 100.0 % | | R2018 (AC1032) | 4 | 1615 | 0 | 0 | **100.0 %** | 100.0 % | -| **Aggregate** | **61** | **122337** | **25** | **0** | **> 99.9 %** | **100.0 %** | +| **Aggregate** | **61** | **122343** | **19** | **0** | **> 99.9 %** | **100.0 %** | A success rate counts records, and the sources differ in size by orders of @@ -46,14 +46,14 @@ a change came from decoding or from the corpus: | Source | Files tested | Decoded | Skipped | Errored | Success rate | Median drawing | |--------|--------------|---------|---------|---------|--------------|----------------| | nextgis | 42 | 8443 | 0 | 0 | **100.0 %** | 100.0 % | -| acadsharp | 10 | 7625 | 6 | 0 | **99.9 %** | 100.0 % | +| acadsharp | 10 | 7631 | 0 | 0 | **100.0 %** | 100.0 % | | bookviewerapp3 | 4 | 1440 | 19 | 0 | **98.7 %** | 99.4 % | | oshw-jetson | 4 | 104577 | 0 | 0 | **100.0 %** | 100.0 % | | planar-algorithm | 1 | 252 | 0 | 0 | **100.0 %** | 100.0 % | -| **All sources** | **61** | **122337** | **25** | **0** | **> 99.9 %** | **100.0 %** | +| **All sources** | **61** | **122343** | **19** | **0** | **> 99.9 %** | **100.0 %** | -Totals: 122,337 decoded / 25 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 99.9 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.5 %, R2000 99.9 %, R2004 100.0 %, R2007 100.0 %, R2010 100.0 %, R2013 100.0 %, R2018 100.0 %. +Totals: 122,343 decoded / 19 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 100.0 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.6 %, R2000 100.0 %, R2004 100.0 %, R2007 100.0 %, R2010 100.0 %, R2013 100.0 %, R2018 100.0 %. [`tools/corpus/baseline.tsv`](./tools/corpus/baseline.tsv) pins each drawing's decoded, skipped and errored counts, its class-census shortfalls diff --git a/STATUS.md b/STATUS.md index 69d2ef79..c22145af 100644 --- a/STATUS.md +++ b/STATUS.md @@ -15,8 +15,8 @@ the current capability matrix. examples (`tools/ci/run_test_targets.sh`), and the Corpus job runs the corpus-reading targets on the 19 pinned drawings; clippy and fmt clean. - **Current real-file decode coverage, measured in CI** (the - `Corpus (public DWG)` job, [run 40736084515](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40736084515) on 2026-10-28, on the public drawings - pinned in `tools/corpus/pins.tsv`): 122,337 decoded / 25 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 99.9 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.5 %, R2000 99.9 %, R2004 100.0 %, R2007 100.0 %, R2010 100.0 %, R2013 100.0 %, R2018 100.0 %. + `Corpus (public DWG)` job, [run 40739158933](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40739158933) on 2026-10-28, on the public drawings + pinned in `tools/corpus/pins.tsv`): 122,343 decoded / 19 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 100.0 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.6 %, R2000 100.0 %, R2004 100.0 %, R2007 100.0 %, R2010 100.0 %, R2013 100.0 %, R2018 100.0 %. The 308 errors of the 2026-10-26 measurement were closed by #226, #227, #228 and #71; `sample_AC1032.dwg` comes from ACadSharp's MIT `samples/`, saved by AutoCAD 2025 (#96, CLEANROOM.md). diff --git a/tools/corpus/baseline.tsv b/tools/corpus/baseline.tsv index 270ef7e3..3351776d 100644 --- a/tools/corpus/baseline.tsv +++ b/tools/corpus/baseline.tsv @@ -60,8 +60,8 @@ polyline2d_line_2007.dwg nextgis AC1021 186 0 0 0 0 CELLSTYLEMAP 3>1;DICTIONARYV polyline2d_line_2010.dwg nextgis AC1024 181 0 0 0 0 CELLSTYLEMAP 1>0;DICTIONARYVAR 10>5 0 491 0 0 0 polyline2d_line_2013.dwg nextgis AC1027 132 0 0 0 0 - 0 369 0 0 0 polyline2d_line_R14.dwg nextgis AC1014 292 0 0 0 0 - 0 0 0 0 292 -sample_AC1014.dwg acadsharp AC1014 1239 3 0 0 0 - 0 0 0 0 1242 -sample_AC1015.dwg acadsharp AC1015 1044 3 0 0 0 - 0 3160 6 0 0 +sample_AC1014.dwg acadsharp AC1014 1242 0 0 0 0 - 0 0 0 0 1242 +sample_AC1015.dwg acadsharp AC1015 1047 0 0 0 0 - 0 3160 6 0 0 sample_AC1018.dwg acadsharp AC1018 1014 0 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;CELLSTYLEMAP 2>1;DICTIONARYVAR 15>14;TABLECONTENT 9>2;TABLEGEOMETRY 9>2 0 3135 6 0 0 sample_AC1021.dwg acadsharp AC1021 925 0 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;TABLECONTENT 8>2;TABLEGEOMETRY 8>2 0 2960 6 0 0 sample_AC1024.dwg acadsharp AC1024 923 0 0 0 0 TABLECONTENT 7>2;TABLEGEOMETRY 7>2 0 3050 6 0 0