//! The ASCII DXF file: group codes, handles, and the sections, tables and objects around the //! entities. DXF R13 and later give every object a handle and subclass markers; R2000 and later //! add layouts and plot styles, which CAD apps expect to find. use std::fmt::Write as _; use crate::DxfVersion; /// An object handle (written in hex). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct Handle(pub u32); /// The handles of the fixed objects every drawing has. Entities and table entries count up from /// [`FIRST_FREE`]. pub(crate) mod fixed { use super::Handle; pub const LAYER_TABLE: Handle = Handle(0x1); pub const LTYPE_TABLE: Handle = Handle(0x2); pub const APPID_TABLE: Handle = Handle(0x3); pub const DIMSTYLE_TABLE: Handle = Handle(0x4); pub const STYLE_TABLE: Handle = Handle(0x5); pub const UCS_TABLE: Handle = Handle(0x6); pub const VIEW_TABLE: Handle = Handle(0x7); pub const VPORT_TABLE: Handle = Handle(0x8); pub const BLOCK_RECORD_TABLE: Handle = Handle(0x9); pub const ROOT: Handle = Handle(0xA); pub const GROUPS: Handle = Handle(0xB); pub const LAYOUTS: Handle = Handle(0xC); pub const MODEL_RECORD: Handle = Handle(0xD); pub const PAPER_RECORD: Handle = Handle(0xE); pub const MODEL_BLOCK: Handle = Handle(0xF); pub const MODEL_END: Handle = Handle(0x10); pub const PAPER_BLOCK: Handle = Handle(0x11); pub const PAPER_END: Handle = Handle(0x12); pub const MODEL_LAYOUT: Handle = Handle(0x13); pub const PAPER_LAYOUT: Handle = Handle(0x14); pub const ACTIVE_VPORT: Handle = Handle(0x15); pub const ACAD_APPID: Handle = Handle(0x16); pub const STANDARD_DIMSTYLE: Handle = Handle(0x17); pub const IMAGE_DICT: Handle = Handle(0x18); pub const IMAGE_VARS: Handle = Handle(0x19); pub const PLOT_STYLES: Handle = Handle(0x1A); pub const PLOT_NORMAL: Handle = Handle(0x1B); pub const FIRST_FREE: u32 = 0x30; } /// A number as DXF writes it: up to 6 decimals, no trailing zeros, 0 for anything not finite. pub(crate) fn num(v: f64) -> String { if !v.is_finite() { return "0".into(); } let s = format!("{v:.6}"); let s = s.trim_end_matches('0').trim_end_matches('.'); if s == "-0" || s.is_empty() { "0".into() } else { s.into() } } /// Text as a DXF string value: one line, `^` escaped, and before DXF 2007 (which reads UTF-8) /// every character outside ASCII as `\U+XXXX`. pub(crate) fn text(s: &str, utf8: bool) -> String { let mut out = String::with_capacity(s.len()); for c in s.chars() { match c { '\r' | '\n' | '\t' => out.push(' '), c if c.is_control() => {} '^' => out.push_str("^ "), c if !utf8 && !c.is_ascii() => match u16::try_from(u32::from(c)) { Ok(u) => _ = write!(out, "\\U+{u:04X}"), Err(_) => out.push('?'), }, c => out.push(c), } } out } /// Group-code/value pairs in the order they are written. pub(crate) struct Codes { pub v: DxfVersion, out: String, } impl Codes { pub fn new(v: DxfVersion) -> Self { Self { v, out: String::new() } } pub fn into_string(self) -> String { self.out } pub fn append(&mut self, other: &str) { self.out.push_str(other); } /// A value written as it is (names and keywords the writer itself chose). pub fn raw(&mut self, code: u16, value: &str) { _ = writeln!(self.out, "{code:>3}\n{value}"); } /// Text from the document, encoded for the version (see [`text`]). pub fn str(&mut self, code: u16, value: &str) { let t = text(value, self.v.utf8()); self.raw(code, &t); } pub fn int(&mut self, code: u16, value: i64) { _ = writeln!(self.out, "{code:>3}\n{value}"); } pub fn num(&mut self, code: u16, value: f64) { _ = writeln!(self.out, "{code:>3}\n{}", num(value)); } pub fn handle(&mut self, code: u16, h: Handle) { _ = writeln!(self.out, "{code:>3}\n{:X}", h.0); } /// A 2D point at `code` (x), `code + 10` (y). pub fn xy(&mut self, code: u16, x: f64, y: f64) { self.num(code, x); self.num(code + 10, y); } /// A 3D point at `code`, `code + 10`, `code + 20` (z = 0). pub fn xyz(&mut self, code: u16, x: f64, y: f64) { self.xy(code, x, y); self.num(code + 20, 0.0); } /// The extrusion direction of planar entities: the z axis. pub fn normal(&mut self) { self.num(210, 0.0); self.num(220, 0.0); self.num(230, 1.0); } /// The start of an object: its type, and from R13 its handle, owner and first subclass. fn object(&mut self, kind: &str, h: Handle, owner: Handle, subclass: &str) { self.raw(0, kind); if self.v.handles() { self.handle(if kind == "DIMSTYLE" { 105 } else { 5 }, h); self.handle(330, owner); self.raw(100, subclass); } } /// A table entry: the shared record subclass, then the table's own (from R13). fn record(&mut self, kind: &str, h: Handle, table: Handle, subclass: &str) { self.object(kind, h, table, "AcDbSymbolTableRecord"); if self.v.handles() { self.raw(100, subclass); } } fn section(&mut self, name: &str) { self.raw(0, "SECTION"); self.raw(2, name); } fn end_section(&mut self) { self.raw(0, "ENDSEC"); } fn table(&mut self, name: &str, h: Handle, count: usize) { self.raw(0, "TABLE"); self.raw(2, name); if self.v.handles() { self.handle(5, h); self.raw(330, "0"); self.raw(100, "AcDbSymbolTable"); } self.int(70, count as i64); if name == "DIMSTYLE" && self.v.r2000() { self.raw(100, "AcDbDimStyleTable"); } } fn end_table(&mut self) { self.raw(0, "ENDTAB"); } } /// How an entity is drawn: its colour index, true colour, lineweight, linetype and transparency. #[derive(Clone, Debug, Default)] pub(crate) struct EntityStyle { pub aci: u8, /// `0xRRGGBB` (DXF 2004 and later). pub rgb: Option, /// Hundredths of a millimetre (DXF 2000 and later). pub lineweight: Option, pub linetype: Option, /// Opacity 0–255 (DXF 2004 and later; opaque is left out). pub alpha: Option, } impl Codes { /// The start of an entity of `kind` owned by `owner` (model space, or a polyline for its /// vertices) on `layer` with its style, ending with `subclass` (none when empty). pub fn entity(&mut self, kind: &str, h: Handle, owner: Handle, layer: &str, style: &EntityStyle, subclass: &str) { self.object(kind, h, owner, "AcDbEntity"); self.str(8, layer); if let Some(lt) = &style.linetype { self.str(6, lt); } self.int(62, i64::from(style.aci.max(1))); if self.v.r2000() && let Some(lw) = style.lineweight { self.int(370, i64::from(lw)); } if self.v.true_color() { if let Some(rgb) = style.rgb { self.int(420, i64::from(rgb)); } if let Some(a) = style.alpha { self.int(440, 0x0200_0000 | i64::from(a)); } } if self.v.handles() && !subclass.is_empty() { self.raw(100, subclass); } } } /// A DXF layer. #[derive(Clone, Debug)] pub(crate) struct Layer { pub name: String, pub aci: u8, /// Switched off (a hidden layer). pub off: bool, pub locked: bool, /// Plotted (printable). pub plot: bool, } /// A dash pattern: dash, gap, dash… lengths in drawing units. #[derive(Clone, Debug)] pub(crate) struct LineType { pub name: String, pub pattern: Vec, } /// A placed image file and the image entities that show it. #[derive(Clone, Debug)] pub(crate) struct ImageDef { pub name: String, pub size: (u32, u32), pub handle: Handle, /// `(reactor, image entity)` per image entity. pub reactors: Vec<(Handle, Handle)>, } /// Everything around the entities, collected while they were written. pub(crate) struct Drawing { pub version: DxfVersion, pub layers: Vec, pub linetypes: Vec, /// Text styles: (style name, font family). pub styles: Vec<(String, String)>, pub images: Vec, /// The entities' group codes. pub entities: String, /// `[x0, y0, x1, y1]` of everything written (drawing units). pub extents: [f64; 4], /// The export region (drawing units). pub limits: [f64; 4], /// `$INSUNITS`. pub insunits: u8, pub metric: bool, /// The next free handle. pub next: u32, } /// The text style every drawing has. pub(crate) const STANDARD_STYLE: &str = "Standard"; impl Drawing { /// The whole file. pub fn write(self) -> String { let v = self.version; let mut c = Codes::new(v); // Table entries get the handles after the entities'. let mut next = self.next; let mut take = |n: usize| { let start = next; next = next.saturating_add(u32::try_from(n).unwrap_or(u32::MAX)); start }; let ltype_base = take(3 + self.linetypes.len()); let layer_base = take(1 + self.layers.len()); let style_base = take(1 + self.styles.len()); self.header(&mut c, next); if v.handles() { self.classes(&mut c); } c.section("TABLES"); self.vport(&mut c); self.ltypes(&mut c, ltype_base); self.layer_table(&mut c, layer_base); self.style_table(&mut c, style_base); if v.handles() { for (name, h) in [("VIEW", fixed::VIEW_TABLE), ("UCS", fixed::UCS_TABLE)] { c.table(name, h, 0); c.end_table(); } c.table("APPID", fixed::APPID_TABLE, 1); c.record("APPID", fixed::ACAD_APPID, fixed::APPID_TABLE, "AcDbRegAppTableRecord"); c.raw(2, "ACAD"); c.int(70, 0); c.end_table(); c.table("DIMSTYLE", fixed::DIMSTYLE_TABLE, 1); c.record("DIMSTYLE", fixed::STANDARD_DIMSTYLE, fixed::DIMSTYLE_TABLE, "AcDbDimStyleTableRecord"); c.raw(2, "Standard"); c.int(70, 0); c.end_table(); c.table("BLOCK_RECORD", fixed::BLOCK_RECORD_TABLE, 2); for (name, h, layout) in [("*Model_Space", fixed::MODEL_RECORD, fixed::MODEL_LAYOUT), ("*Paper_Space", fixed::PAPER_RECORD, fixed::PAPER_LAYOUT)] { c.record("BLOCK_RECORD", h, fixed::BLOCK_RECORD_TABLE, "AcDbBlockTableRecord"); c.raw(2, name); if v.r2000() { c.handle(340, layout); c.int(70, 0); c.int(280, 1); c.int(281, 0); } } c.end_table(); } c.end_section(); if v.handles() { c.section("BLOCKS"); for (name, record, begin, end) in [ ("*Model_Space", fixed::MODEL_RECORD, fixed::MODEL_BLOCK, fixed::MODEL_END), ("*Paper_Space", fixed::PAPER_RECORD, fixed::PAPER_BLOCK, fixed::PAPER_END), ] { c.object("BLOCK", begin, record, "AcDbEntity"); c.raw(8, "0"); c.raw(100, "AcDbBlockBegin"); c.raw(2, name); c.int(70, 0); c.xyz(10, 0.0, 0.0); c.raw(3, name); c.raw(1, ""); c.object("ENDBLK", end, record, "AcDbEntity"); c.raw(8, "0"); c.raw(100, "AcDbBlockEnd"); } c.end_section(); } c.section("ENTITIES"); c.append(&self.entities); c.end_section(); if v.handles() { self.objects(&mut c); } c.raw(0, "EOF"); c.into_string() } fn header(&self, c: &mut Codes, handseed: u32) { let v = self.version; let var = |c: &mut Codes, name: &str| c.raw(9, name); c.section("HEADER"); var(c, "$ACADVER"); c.raw(1, v.acadver()); var(c, "$DWGCODEPAGE"); c.raw(3, "ANSI_1252"); var(c, "$INSBASE"); c.xyz(10, 0.0, 0.0); let [x0, y0, x1, y1] = self.extents; var(c, "$EXTMIN"); c.xyz(10, x0, y0); var(c, "$EXTMAX"); c.xyz(10, x1, y1); let [lx0, ly0, lx1, ly1] = self.limits; var(c, "$LIMMIN"); c.xy(10, lx0, ly0); var(c, "$LIMMAX"); c.xy(10, lx1, ly1); if v.r2000() { var(c, "$LWDISPLAY"); c.int(290, 1); var(c, "$INSUNITS"); c.int(70, i64::from(self.insunits)); var(c, "$MEASUREMENT"); c.int(70, i64::from(self.metric)); } if v.handles() { var(c, "$HANDSEED"); c.handle(5, Handle(handseed)); } c.end_section(); } fn classes(&self, c: &mut Codes) { let mut classes: Vec<(&str, &str, &str, i64, bool)> = vec![]; if self.version.r2000() { classes.extend([ ("ACDBDICTIONARYWDFLT", "AcDbDictionaryWithDefault", "ObjectDBX Classes", 0, false), ("ACDBPLACEHOLDER", "AcDbPlaceHolder", "ObjectDBX Classes", 0, false), ("LAYOUT", "AcDbLayout", "ObjectDBX Classes", 0, false), ]); } if !self.images.is_empty() { classes.extend([ ("RASTERVARIABLES", "AcDbRasterVariables", "ISM", 0, false), ("IMAGE", "AcDbRasterImage", "ISM", 2175, true), ("IMAGEDEF", "AcDbRasterImageDef", "ISM", 0, false), ("IMAGEDEF_REACTOR", "AcDbRasterImageDefReactor", "ISM", 1, false), ]); } c.section("CLASSES"); for (name, cpp, app, flags, entity) in classes { c.raw(0, "CLASS"); c.raw(1, name); c.raw(2, cpp); c.raw(3, app); c.int(90, flags); if self.version.true_color() { c.int(91, 0); } c.int(280, 0); c.int(281, i64::from(entity)); } c.end_section(); } /// The active viewport, zoomed to the drawing's extents. fn vport(&self, c: &mut Codes) { let [x0, y0, x1, y1] = self.extents; c.table("VPORT", fixed::VPORT_TABLE, 1); c.record("VPORT", fixed::ACTIVE_VPORT, fixed::VPORT_TABLE, "AcDbViewportTableRecord"); c.raw(2, if self.version.handles() { "*Active" } else { "*ACTIVE" }); c.int(70, 0); c.xy(10, 0.0, 0.0); c.xy(11, 1.0, 1.0); c.xy(12, (x0 + x1) / 2.0, (y0 + y1) / 2.0); c.xy(13, 0.0, 0.0); c.xy(14, 1.0, 1.0); c.xy(15, 10.0, 10.0); // The view direction looks down the z axis: (0, 0, 1), each code once. c.xy(16, 0.0, 0.0); c.num(36, 1.0); c.xyz(17, 0.0, 0.0); let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs()); // The view's height, with a margin; its width follows from the aspect ratio. c.num(40, (h.max(w / 1.5) * 1.1).max(1.0)); c.num(41, 1.5); c.num(42, 50.0); c.num(43, 0.0); c.num(44, 0.0); c.num(50, 0.0); c.num(51, 0.0); for (code, value) in [(71, 0), (72, 1000), (73, 1), (74, 3), (75, 0), (76, 0), (77, 0), (78, 0)] { c.int(code, value); } if self.version.r2000() { c.int(281, 0); c.int(65, 0); c.num(146, 0.0); } c.end_table(); } fn ltypes(&self, c: &mut Codes, base: u32) { c.table("LTYPE", fixed::LTYPE_TABLE, 3 + self.linetypes.len()); let solid = [("ByBlock", ""), ("ByLayer", ""), ("Continuous", "Solid line")]; let rows = solid.iter().map(|(n, d)| (*n, *d, &[][..])).chain(self.linetypes.iter().map(|l| (l.name.as_str(), "", l.pattern.as_slice()))); for (i, (name, desc, pattern)) in rows.enumerate() { c.record("LTYPE", Handle(base.saturating_add(i as u32)), fixed::LTYPE_TABLE, "AcDbLinetypeTableRecord"); c.str(2, name); c.int(70, 0); c.str(3, if pattern.is_empty() { desc } else { "Dashed" }); c.int(72, 65); c.int(73, pattern.len() as i64); c.num(40, pattern.iter().map(|v| v.abs()).sum()); for v in pattern { c.num(49, *v); if self.version.handles() { c.int(74, 0); } } } c.end_table(); } fn layer_table(&self, c: &mut Codes, base: u32) { let zero = Layer { name: "0".into(), aci: 7, off: false, locked: false, plot: true }; c.table("LAYER", fixed::LAYER_TABLE, 1 + self.layers.len()); for (i, l) in std::iter::once(&zero).chain(&self.layers).enumerate() { c.record("LAYER", Handle(base.saturating_add(i as u32)), fixed::LAYER_TABLE, "AcDbLayerTableRecord"); c.str(2, &l.name); c.int(70, if l.locked { 4 } else { 0 }); // A negative colour switches the layer off. let aci = i64::from(l.aci.max(1)); c.int(62, if l.off { -aci } else { aci }); c.raw(6, "Continuous"); if self.version.r2000() { c.int(290, i64::from(l.plot)); c.int(370, -3); c.handle(390, fixed::PLOT_NORMAL); } } c.end_table(); } fn style_table(&self, c: &mut Codes, base: u32) { c.table("STYLE", fixed::STYLE_TABLE, 1 + self.styles.len()); let standard = (STANDARD_STYLE.to_string(), String::new()); for (i, (name, family)) in std::iter::once(&standard).chain(&self.styles).enumerate() { c.record("STYLE", Handle(base.saturating_add(i as u32)), fixed::STYLE_TABLE, "AcDbTextStyleTableRecord"); c.str(2, name); c.int(70, 0); c.num(40, 0.0); c.num(41, 1.0); c.num(50, 0.0); c.int(71, 0); c.num(42, 2.5); // The font file; from 2000 the family name too, which is how CAD apps find a TrueType font. if family.is_empty() { c.raw(3, "txt"); } else { c.str(3, &format!("{family}.ttf")); } c.raw(4, ""); if self.version.r2000() && !family.is_empty() { c.raw(1001, "ACAD"); c.str(1000, family); c.int(1071, 0); } } c.end_table(); } fn objects(&self, c: &mut Codes) { let v = self.version; let images = !self.images.is_empty(); c.section("OBJECTS"); let dict = |c: &mut Codes, h: Handle, owner: Handle, entries: &[(&str, Handle)]| { c.object("DICTIONARY", h, owner, "AcDbDictionary"); c.int(281, 1); for (name, e) in entries { c.str(3, name); c.handle(350, *e); } }; let mut root = vec![("ACAD_GROUP", fixed::GROUPS)]; if images { root.extend([("ACAD_IMAGE_DICT", fixed::IMAGE_DICT), ("ACAD_IMAGE_VARS", fixed::IMAGE_VARS)]); } if v.r2000() { root.extend([("ACAD_LAYOUT", fixed::LAYOUTS), ("ACAD_PLOTSTYLENAME", fixed::PLOT_STYLES)]); } dict(c, fixed::ROOT, Handle(0), &root); dict(c, fixed::GROUPS, fixed::ROOT, &[]); if v.r2000() { dict(c, fixed::LAYOUTS, fixed::ROOT, &[("Layout1", fixed::PAPER_LAYOUT), ("Model", fixed::MODEL_LAYOUT)]); c.object("ACDBDICTIONARYWDFLT", fixed::PLOT_STYLES, fixed::ROOT, "AcDbDictionary"); c.int(281, 1); c.raw(3, "Normal"); c.handle(350, fixed::PLOT_NORMAL); c.raw(100, "AcDbDictionaryWithDefault"); c.handle(340, fixed::PLOT_NORMAL); c.object("ACDBPLACEHOLDER", fixed::PLOT_NORMAL, fixed::PLOT_STYLES, "AcDbPlaceHolder"); self.layout(c, fixed::MODEL_LAYOUT, "Model", 0, fixed::MODEL_RECORD); self.layout(c, fixed::PAPER_LAYOUT, "Layout1", 1, fixed::PAPER_RECORD); } if images { c.object("RASTERVARIABLES", fixed::IMAGE_VARS, fixed::ROOT, "AcDbRasterVariables"); c.int(90, 0); c.int(70, 0); c.int(71, 1); c.int(72, 0); let entries: Vec<(&str, Handle)> = self.images.iter().map(|i| (i.name.as_str(), i.handle)).collect(); dict(c, fixed::IMAGE_DICT, fixed::ROOT, &entries); for img in &self.images { c.raw(0, "IMAGEDEF"); c.handle(5, img.handle); c.raw(102, "{ACAD_REACTORS"); c.handle(330, fixed::IMAGE_DICT); for (reactor, _) in &img.reactors { c.handle(330, *reactor); } c.raw(102, "}"); c.handle(330, fixed::IMAGE_DICT); c.raw(100, "AcDbRasterImageDef"); c.int(90, 0); c.str(1, &img.name); c.xy(10, f64::from(img.size.0), f64::from(img.size.1)); c.xy(11, 1.0, 1.0); c.int(280, 1); c.int(281, 0); for (reactor, image) in &img.reactors { c.object("IMAGEDEF_REACTOR", *reactor, *image, "AcDbRasterImageDefReactor"); c.int(90, 2); c.handle(330, *image); } } } c.end_section(); } /// A layout (DXF 2000 and later): its plot settings, then the layout itself. fn layout(&self, c: &mut Codes, h: Handle, name: &str, tab: i64, record: Handle) { let [x0, y0, x1, y1] = self.extents; c.object("LAYOUT", h, fixed::LAYOUTS, "AcDbPlotSettings"); c.raw(1, ""); c.raw(2, "none_device"); c.raw(4, ""); c.raw(6, ""); for code in 40..=49 { c.num(code, 0.0); } c.num(140, 0.0); c.num(141, 0.0); c.num(142, 1.0); c.num(143, 1.0); c.int(70, 688); c.int(72, 1); c.int(73, 0); c.int(74, 5); c.raw(7, ""); c.int(75, 16); c.num(147, 1.0); c.xy(148, 0.0, 0.0); c.raw(100, "AcDbLayout"); c.raw(1, name); c.int(70, 1); c.int(71, tab); let [lx0, ly0, lx1, ly1] = self.limits; c.xy(10, lx0, ly0); c.xy(11, lx1, ly1); c.xyz(12, 0.0, 0.0); c.xyz(14, x0, y0); c.xyz(15, x1, y1); c.num(146, 0.0); c.xyz(13, 0.0, 0.0); c.xyz(16, 1.0, 0.0); c.xyz(17, 0.0, 1.0); c.int(76, 1); c.handle(330, record); } } #[cfg(test)] mod tests { use super::*; #[test] fn numbers_and_text_are_written_plainly() { assert_eq!(num(1.5), "1.5"); assert_eq!(num(-0.0000001), "0"); assert_eq!(num(2.0), "2"); assert_eq!(num(f64::NAN), "0"); assert_eq!(text("a^b\nc", true), "a^ b c"); assert_eq!(text("Größe 😀", false), "Gr\\U+00F6\\U+00DFe ?"); assert_eq!(text("Größe", true), "Größe"); } }