diff --git a/crates/doc/src/entity.rs b/crates/doc/src/entity.rs index 5a8f30a..de6c8f5 100644 --- a/crates/doc/src/entity.rs +++ b/crates/doc/src/entity.rs @@ -831,6 +831,10 @@ pub struct TableCell { /// A formula (`=Sum(A1:B3)`); `text` holds its value. #[serde(default, skip_serializing_if = "Option::is_none")] pub formula: Option, + /// The cell's own text height (DXF 140 in the cell record), before the table's scale; + /// `None`: its cell style's. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub text_height: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs index 51effa6..7cfed42 100644 --- a/crates/io/src/dxf_read.rs +++ b/crates/io/src/dxf_read.rs @@ -527,7 +527,18 @@ fn acad_table(tags: &[Tag]) -> Table { (true, Some(rs), Some(cs)) if rs >= 1 && cs >= 1 && (rs, cs) != (1, 1) => Some((rs, cs)), _ => None, }; - flat.push(TableCell { text, merged, ..TableCell::default() }); + // The cell's own text height: 140 outside the value block (301 … 304), where it is a number. + let mut in_value = false; + let mut text_height = None; + for x in cell { + match x.code { + 301 => in_value = true, + 304 => in_value = false, + 140 if !in_value => text_height = Some(x.f64()).filter(|h| h.is_finite() && *h > 0.0 && *h < 1e9), + _ => {} + } + } + flat.push(TableCell { text, merged, text_height, ..TableCell::default() }); } flat.resize(rows * cols, TableCell::default()); let cells: Vec> = if cols == 0 { vec![Vec::new(); rows] } else { flat.chunks(cols).map(<[TableCell]>::to_vec).collect() }; @@ -541,7 +552,8 @@ fn acad_table(tags: &[Tag]) -> Table { (flags.first().is_some_and(|v| *v != 0), flags.get(1).is_some_and(|v| *v != 0), h, style) } // Other writers: a first row merged across every column reads as a title. - None => (cols > 1 && cells.first().and_then(|r| r.first()).and_then(|c| c.merged) == Some((1, cols as u32)), false, 0.18, "Standard".into()), + // Their text height is resolved to the table style's once the styles are read (`apply`). + None => (cols > 1 && cells.first().and_then(|r| r.first()).and_then(|c| c.merged) == Some((1, cols as u32)), false, 0.0, "Standard".into()), }; let mut t = Table { insert: T(tags).p(10), col_widths, row_heights, cells, style, text_height, title, header, ..Table::default() }; crate::dxf_table::read_table_xdata(tags, &mut t); @@ -1012,8 +1024,8 @@ fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &m rx.table_blocks.insert(b.to_ascii_uppercase()); } let ours = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP).iter().any(|t| t.code == 1000 && t.str() == "TABLE"); - if !ours && let Some(sh) = t.s(342) { - rx.table_style_fix.push((h, sh.trim().to_ascii_uppercase())); + if !ours { + rx.table_style_fix.push((h, t.s(342).map(|sh| sh.trim().to_ascii_uppercase()).unwrap_or_default())); } } ("INSERT", EntityKind::Insert(ins)) => { @@ -1894,13 +1906,24 @@ impl Objects { } } for (h, sh) in &rx.table_style_fix { - if let Some(name) = self.names.get(sh) { - let _ = d.modify_entity(*h, |e| { - if let EntityKind::Table(t) = &mut e.kind { - t.style = name.clone(); + let name = self.names.get(sh).cloned(); + // A table from another writer has no scale of its own: its text height is its + // style's, so every cell style height applies as it is. + let height = { + let style = name.as_deref().unwrap_or("Standard"); + d.table_styles + .iter() + .find(|s| s.name.eq_ignore_ascii_case(style)) + .map_or(cadcraft_doc::TableStyle::default().text_height, |s| s.text_height) + }; + let _ = d.modify_entity(*h, |e| { + if let EntityKind::Table(t) = &mut e.kind { + if let Some(name) = name { + t.style = name; } - }); - } + t.text_height = height; + } + }); } // Parametric constraints and parameters. if let Some((c, p)) = self.xrecord_text(crate::dxf_ext::CONSTRAINTS_KEY).and_then(|t| crate::dxf_ext::parse_constraints(&t)) { diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index fe4f695..aa50a62 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -856,6 +856,9 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx w.i(91, 0); w.i(178, 0); w.f(145, 0.0); + if let Some(h) = cell.and_then(|x| x.text_height).filter(|h| h.is_finite() && *h > 0.0) { + w.f(140, h); + } // Long text: 250-character code-2 chunks, the rest in code 1. let chars: Vec = cell.map(|x| x.text.chars().collect()).unwrap_or_default(); let chunks: Vec = chars.chunks(250).map(|c| c.iter().collect()).collect(); diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs index 345a456..7554cb1 100644 --- a/crates/io/src/tests.rs +++ b/crates/io/src/tests.rs @@ -1375,6 +1375,8 @@ fn hostile_extension_data_never_panics() { // Tables: enormous or negative counts, cells without bodies, spans past the edge. ent("0\nACAD_TABLE\n2\n*T1\n100\nAcDbTable\n91\n999999999\n92\n999999999\n171\n1\n0\nACAD_TABLE\n100\nAcDbTable\n91\n-4\n92\n3\n171\n"), ent("0\nACAD_TABLE\n100\nAcDbTable\n91\n2\n92\n2\n141\nnan\n142\n-1\n171\n1\n173\n1\n175\n99999\n176\n-3\n1\nx\n171\n171\n171\n171\n171\n2\nzzz\n1001\nCADCRAFT\n1000\nTABLE\n1040\n-1\n"), + // Cell text heights (140): junk, huge, unbalanced value block. + ent("0\nACAD_TABLE\n100\nAcDbTable\n91\n1\n92\n3\n171\n1\n140\nnan\n171\n1\n140\n-5\n301\nCELL_VALUE\n140\n1e308\n171\n1\n304\nACVALUE_END\n140\n1e999\n1\nx\n"), // DIMASSOC pointing at nothing, or at a non-dimension. format!( "{}{}", @@ -2136,3 +2138,23 @@ fn kept_xrecord_application_group_roundtrips_as_read() { // And no handle is written under a bool code anywhere. assert!(!out.contains("291\n2"), "no handle may be written under a bool code"); } + +#[test] +fn foreign_table_reads_cell_text_height_and_keeps_style_scale() { + // A table from another writer: no CADCraft xdata; cell 1 sets 140 before its value block + // (the cell's text height), cell 2 only has a 140 inside the value block (a number). + let dxf = "0\nSECTION\n2\nENTITIES\n0\nACAD_TABLE\n100\nAcDbTable\n91\n1\n92\n2\n141\n300\n142\n900\n142\n900\n\ + 171\n1\n140\n150\n301\nCELL_VALUE\n1\nA\n304\nACVALUE_END\n\ + 171\n1\n301\nCELL_VALUE\n140\n42\n1\nB\n304\nACVALUE_END\n0\nENDSEC\n0\nEOF\n"; + let d = read_dxf(dxf.as_bytes()).unwrap(); + let t = first(&d, |k| if let EntityKind::Table(x) = k { Some(x.clone()) } else { None }); + assert_eq!(t.cells[0][0].text_height, Some(150.0)); + assert_eq!(t.cells[0][1].text_height, None); + // No scale of its own: the table takes its style's height. + assert_eq!(t.text_height, TableStyle::default().text_height); + // Written back and read again. + let back = roundtrip(&d); + let t2 = first(&back, |k| if let EntityKind::Table(x) = k { Some(x.clone()) } else { None }); + assert_eq!(t2.cells[0][0].text_height, Some(150.0)); + assert_eq!(t2.cells[0][1].text_height, None); +} diff --git a/crates/render/src/table.rs b/crates/render/src/table.rs index abeab7c..b6a161a 100644 --- a/crates/render/src/table.rs +++ b/crates/render/src/table.rs @@ -85,6 +85,8 @@ pub fn table_style_of(d: &Drawing, t: &Table) -> TableStyle { /// The scale of a table relative to its style: SCALE changes the table's text height, and every /// cell style height and margin follows. fn style_scale(st: &TableStyle, t: &Table) -> f64 { + // A table from another writer is resolved to its style's height on reading (scale 1); a + // non-positive height here is the same "no scale of its own". let k = t.text_height / st.text_height; if k.is_finite() && k > 0.0 { k } else { 1.0 } } @@ -312,7 +314,7 @@ fn layout(d: &Drawing, t: &Table) -> TableGeom { col: a.col, at: p(x, y), attach, - height: cs.text_height, + height: cell.text_height.filter(|h| h.is_finite() && *h > 0.0).map_or(cs.text_height, |h| h * k), color: cs.text_color, style: cs.text_style.clone(), text: cell.text.clone(), @@ -352,27 +354,18 @@ pub(crate) fn draw(b: &mut Builder, ctx: &Ctx, t: &Table, rgb: Ink, lw: f32) { for tx in &g.texts { let ts = crate::text_style(ctx.d, &tx.style); let font = cadcraft_fonts::TextFont::resolve(&ts.font); - let h = match (tx.attach - 1) % 3 { - 0 => cadcraft_fonts::Align::Left, - // Middle centre is TEXT's "Middle" (centred both ways). - 1 if tx.attach == 5 => cadcraft_fonts::Align::Middle, - 1 => cadcraft_fonts::Align::Center, - _ => cadcraft_fonts::Align::Right, - }; - let v = match (tx.attach - 1) / 3 { - 0 => cadcraft_fonts::VAlign::Top, - 1 => cadcraft_fonts::VAlign::Middle, - _ => cadcraft_fonts::VAlign::Bottom, - }; - let params = cadcraft_fonts::TextParams { - align_pt: Some(tx.at), - h, - v, + // Cell content is MTEXT (`\P`, `{\Q15;…}`, `\F…;`): laid out as such, at the cell's + // alignment point (its numbering is MTEXT's attachment). + let mp = cadcraft_fonts::MTextParams { + attach: tx.attach, + font, width_factor: ts.width_factor, oblique: ts.oblique, - ..cadcraft_fonts::TextParams::new(tx.at, tx.height) + ..cadcraft_fonts::MTextParams::new(tx.at, tx.height) }; - let (sh, _) = cadcraft_fonts::place(&font, &tx.text, ¶ms); - b.shaped(ctx, style_ink(tx.color, rgb), lw, &sh); + let layout = cadcraft_fonts::layout_mtext_with(&tx.text, &mp); + for piece in &layout.pieces { + b.shaped(ctx, style_ink(tx.color, rgb), lw, &piece.shaped); + } } } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index f18dff0..036c949 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -676,3 +676,24 @@ fn mtext_exact_spacing_ignores_taller_lines() { assert!((height("A\\PBB\\PCC", 1.0, exact) - (1.0 + 2.0 * 5.0 / 3.0)).abs() < 1e-9, "uniform lines, exact {exact}"); } } + +#[test] +fn table_cell_text_height_scales_with_the_table() { + let height = |table_h: f64, own: Option| { + let d = Drawing::new_imperial(); + let st = crate::table_style_of(&d, &cadcraft_doc::Table::default()); + let cell = cadcraft_doc::TableCell { text: "a".into(), text_height: own, ..Default::default() }; + let t = cadcraft_doc::Table { + col_widths: vec![500.0], + row_heights: vec![500.0], + cells: vec![vec![cell]], + text_height: table_h * st.text_height, + ..Default::default() + }; + crate::table_geometry(&d, &t).texts.first().map(|x| x.height) + }; + let base = height(1.0, None).unwrap(); + assert_eq!(height(1.0, Some(150.0)), Some(150.0)); + assert_eq!(height(2.0, Some(150.0)), Some(300.0)); + assert_eq!(height(2.0, None), Some(base * 2.0)); +}