ACAD_TABLE from other writers: keep style text height, read cell text height, lay out cell text as MTEXT (#802) (#806)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Valentin Klein 2026-10-13 11:18:23 +02:00 committed by GitHub
parent 4257674dfa
commit 966356545d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 96 additions and 30 deletions

View File

@ -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<String>,
/// 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<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]

View File

@ -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<Vec<TableCell>> = 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)) {

View File

@ -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<char> = cell.map(|x| x.text.chars().collect()).unwrap_or_default();
let chunks: Vec<String> = chars.chunks(250).map(|c| c.iter().collect()).collect();

View File

@ -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);
}

View File

@ -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, &params);
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);
}
}
}

View File

@ -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<f64>| {
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));
}