Files
OneCAD-Draft/tests/pdf_export_text_check.rs
T
Karim Jerbi 02c470aac7 feat(pdf): searchable text layer with per-style subset font embedding
Real BT/Tj text objects ride alongside the vector outlines (Tr 3,
zero visual change): runs preserved at layout time, TTF styles embedded
as allsorts subsets with ToUnicode, width-matched Tm, clip-culled.
WireModel literals moved to ..Default::default() tails.
2026-10-01 23:30:35 +01:00

109 lines
4.0 KiB
Rust

// Regression for #385: text and dimension text must reach the PDF / print
// export. From v0.8.2 text renders on-screen only as GPU SDF glyph quads
// (`WireModel::text_verts`); the CPU PDF exporter used to draw only wire
// stroke `points`, so all text — standalone TEXT/MTEXT and dimension text —
// vanished from exported PDFs (present in v0.7.6).
//
// The op-level placement checks live in `pdf_export`'s own unit tests, which can
// see `emit_text`'s ops. What only an integration test can cover is that the
// real entity -> scene.entity_wires() -> export_pdf path carries text at all,
// per entity kind — so that is what this asserts.
use codec::entities::{Dimension, DimensionLinear, Text};
use codec::types::Vector3;
use codec::EntityType;
use OpenCADStudio::io::pdf_export::{
export_pdf, PdfPageInput, PdfPlotOptions, PlotContent, PlotWire,
};
use OpenCADStudio::scene::Scene;
#[test]
fn text_and_dim_reach_pdf_export() {
let mut scene = Scene::new();
let t = Text::with_value("HELLO", Vector3::new(10.0, 10.0, 0.0)).with_height(5.0);
let text_h = scene.add_entity(EntityType::Text(t));
// A linear dimension — its measurement value is synthesized as SDF text too,
// and dimension text is what #385 was actually filed about.
let dim = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(40.0, 0.0, 0.0));
let dim_h = scene.add_entity(EntityType::Dimension(Dimension::Linear(dim)));
let wires = scene.entity_wires();
// Assert per entity, not "some wire somewhere has text": both asserts below
// would otherwise be satisfied by the TEXT alone, leaving a dimension-text
// regression — the actual reported bug — green.
// Tessellation names each wire after its entity handle (see the
// `w.name.parse::<u64>()` lookup in the render pipeline).
let has_text = |h: codec::types::Handle| {
let want = h.value().to_string();
wires
.iter()
.any(|w| w.name == want && !w.text_verts.is_empty())
};
assert!(has_text(text_h), "TEXT entity emitted no text glyphs");
assert!(
has_text(dim_h),
"DIMENSION entity emitted no measurement-value text glyphs"
);
// End-to-end: the same wire set with and without text. A private temp dir
// keeps concurrent runs (and other users on a shared build host) from
// colliding on a fixed name in the shared temp root.
let dir = std::env::temp_dir().join(format!("ocs385-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
let plot_wires: Vec<PlotWire> = wires
.iter()
.map(|w| PlotWire {
wire: w.clone(),
draw_depth: 0.0,
})
.collect();
let p_text = dir.join("with_text.pdf");
let mut page = PdfPageInput {
content: PlotContent {
wires: std::sync::Arc::new(plot_wires),
..Default::default()
},
paper_w: 210.0,
paper_h: 297.0,
offset_x: 0.0,
offset_y: 0.0,
rotation_deg: 0,
scale: 1.0,
clip: None,
options: PdfPlotOptions::default(),
plot_style: None,
};
export_pdf(&page, &p_text).expect("export with text");
let with_text = std::fs::read(&p_text).expect("read pdf");
assert!(with_text.starts_with(b"%PDF"), "not a PDF");
let stripped: Vec<_> = page
.content
.wires
.iter()
.cloned()
.map(|mut w| {
w.wire.text_verts.clear();
w.wire.searchable_text.clear();
w
})
.collect();
let p_bare = dir.join("no_text.pdf");
page.content.wires = std::sync::Arc::new(stripped);
export_pdf(&page, &p_bare).expect("export without text");
let no_text = std::fs::read(&p_bare).expect("read pdf");
assert!(
with_text.len() > no_text.len(),
"text added no content to the PDF: {} !> {}",
with_text.len(),
no_text.len()
);
let _ = std::fs::remove_dir_all(&dir);
}