forked from 1b-agent/opencadstudio-upstream
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.
This commit is contained in:
1 parent
443b935a3f
commit
02c470aac7
44 files changed
+1534
-127
No files matched your search
@@ -147,6 +147,7 @@ pub fn apply_along(
|
||||
// Embedded linetype text (LtSeg::Text) renders as SDF glyph quads collected
|
||||
// here; emitted as one extra text-carrying WireModel at the end.
|
||||
let mut text_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||||
let mut searchable: Vec<crate::scene::model::wire_model::SearchableTextRun> = Vec::new();
|
||||
|
||||
let mut elem_idx: usize = 0;
|
||||
let mut elem_consumed: f32 = 0.0;
|
||||
@@ -269,7 +270,7 @@ pub fn apply_along(
|
||||
// SDF: glyph quads at the insert point (rotation baked in by
|
||||
// layout_glyph_quads), collected for the text wire.
|
||||
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
||||
let quads = crate::scene::text::glyph_quads::layout_glyph_quads(
|
||||
let (quads, pen_adv) = crate::scene::text::glyph_quads::layout_glyph_quads(
|
||||
&mut atlas,
|
||||
*tx_scale,
|
||||
fwd_angle,
|
||||
@@ -291,6 +292,28 @@ pub fn apply_along(
|
||||
color,
|
||||
0.0,
|
||||
);
|
||||
{
|
||||
use crate::scene::model::wire_model::{
|
||||
clean_searchable_text, SearchableTextRun,
|
||||
};
|
||||
let visible = clean_searchable_text(&resolved);
|
||||
if !visible.is_empty() {
|
||||
searchable.push(SearchableTextRun {
|
||||
text: visible,
|
||||
origin: [
|
||||
insert[0] as f64,
|
||||
insert[1] as f64,
|
||||
insert[2] as f64,
|
||||
],
|
||||
height: *tx_scale,
|
||||
rotation: fwd_angle,
|
||||
color,
|
||||
bold: false,
|
||||
font: style.to_string(),
|
||||
adv_width: pen_adv,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elem_idx += 1;
|
||||
@@ -342,7 +365,9 @@ pub fn apply_along(
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Embedded linetype text (SDF): one extra wire carrying the glyph quads,
|
||||
@@ -376,6 +401,7 @@ pub fn apply_along(
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts,
|
||||
searchable_text: searchable,
|
||||
name: name.to_string(),
|
||||
points: Vec::new(),
|
||||
points_low: Vec::new(),
|
||||
|
||||
@@ -34,6 +34,13 @@ pub struct GlyphQuad {
|
||||
/// Lay `text` out as per-glyph quads. Mirrors `tessellate_text_run`'s transform
|
||||
/// and per-character advance so the quads land exactly where the strokes would.
|
||||
/// Whitespace and glyphs with no ink advance the pen but emit no quad.
|
||||
///
|
||||
/// Returns the quads plus the final pen advance in run-local units
|
||||
/// (`cursor × scale × |width_factor|`, always ≥ 0): the exact laid-out width
|
||||
/// including spacing/tracking, so exporters can width-match (`Tz`) and cull
|
||||
/// runs without re-deriving advances from a second layout pass. Mirrored
|
||||
/// (negative width factor) runs collapse to a positive extent — the search
|
||||
/// layer reads LTR while the outlines keep the true mirror.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn layout_glyph_quads(
|
||||
atlas: &mut GlyphAtlas,
|
||||
@@ -45,9 +52,9 @@ pub fn layout_glyph_quads(
|
||||
font_name: &str,
|
||||
bold: bool,
|
||||
text: &str,
|
||||
) -> Vec<GlyphQuad> {
|
||||
) -> (Vec<GlyphQuad>, f32) {
|
||||
if text.is_empty() || height <= 0.0 {
|
||||
return vec![];
|
||||
return (vec![], 0.0);
|
||||
}
|
||||
|
||||
let scale = height / 9.0;
|
||||
@@ -155,6 +162,7 @@ pub fn layout_glyph_quads(
|
||||
}
|
||||
}
|
||||
|
||||
let pen_advance = (cursor_x * scale * wf).abs();
|
||||
// Any decoration still open at the end runs to the final pen position.
|
||||
for (slot, y) in [(under, UNDER_Y), (over, OVER_Y), (strike, STRIKE_Y)] {
|
||||
if let Some(s) = slot {
|
||||
@@ -176,7 +184,7 @@ pub fn layout_glyph_quads(
|
||||
});
|
||||
}
|
||||
|
||||
quads
|
||||
(quads, pen_advance)
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────
|
||||
@@ -198,7 +206,8 @@ mod tests {
|
||||
#[test]
|
||||
fn glyphs_advance_rightward() {
|
||||
let mut atlas = GlyphAtlas::new(512, 512);
|
||||
let quads = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AA");
|
||||
let (quads, _) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AA");
|
||||
assert_eq!(quads.len(), 2, "two inked glyphs -> two quads");
|
||||
assert!(
|
||||
center(&quads[1])[0] > center(&quads[0])[0],
|
||||
@@ -216,8 +225,10 @@ mod tests {
|
||||
#[test]
|
||||
fn space_emits_no_quad_but_advances() {
|
||||
let mut atlas = GlyphAtlas::new(512, 512);
|
||||
let ab = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AA");
|
||||
let a_sp_a = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "A A");
|
||||
let (ab, _) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AA");
|
||||
let (a_sp_a, _) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "A A");
|
||||
assert_eq!(a_sp_a.len(), 2, "space produces no quad");
|
||||
assert!(
|
||||
center(&a_sp_a[1])[0] > center(&ab[1])[0],
|
||||
@@ -231,8 +242,9 @@ mod tests {
|
||||
// Use the baseline advance direction (glyph0 -> glyph1), which is +x when
|
||||
// flat and swings to +y after a 90° turn — independent of where a single
|
||||
// glyph's box centre sits within the cap height.
|
||||
let flat = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AA");
|
||||
let turned = layout_glyph_quads(
|
||||
let (flat, _) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AA");
|
||||
let (turned, _) = layout_glyph_quads(
|
||||
&mut atlas,
|
||||
10.0,
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
@@ -256,8 +268,10 @@ mod tests {
|
||||
#[test]
|
||||
fn taller_text_makes_bigger_quads() {
|
||||
let mut atlas = GlyphAtlas::new(512, 512);
|
||||
let small = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "A");
|
||||
let big = layout_glyph_quads(&mut atlas, 20.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "A");
|
||||
let (small, _) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "A");
|
||||
let (big, _) =
|
||||
layout_glyph_quads(&mut atlas, 20.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "A");
|
||||
let extent = |q: &GlyphQuad| {
|
||||
let (mut lo, mut hi) = ([f32::MAX; 2], [f32::MIN; 2]);
|
||||
for p in &q.corners {
|
||||
@@ -278,13 +292,44 @@ mod tests {
|
||||
#[test]
|
||||
fn underline_emits_an_extra_solid_quad() {
|
||||
let mut atlas = GlyphAtlas::new(512, 512);
|
||||
let plain = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AB");
|
||||
let (plain, plain_adv) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AB");
|
||||
// `\L…\l` underlines the run — same glyph quads plus one decoration quad.
|
||||
let under = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "\\LAB\\l");
|
||||
let (under, under_adv) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "\\LAB\\l");
|
||||
assert_eq!(under.len(), plain.len() + 1, "underline adds one quad");
|
||||
// The decoration quad samples the atlas's solid texel (degenerate UV).
|
||||
let deco = under.last().unwrap();
|
||||
assert_eq!(deco.uv_min, deco.uv_max, "decoration quad uses the solid UV");
|
||||
assert_eq!(deco.uv_min, atlas.solid_uv());
|
||||
// Decorations don't move the pen: same advance either way, and the
|
||||
// advance covers both glyphs (positive, wider than one glyph).
|
||||
assert!(
|
||||
(under_adv - plain_adv).abs() < 1e-6,
|
||||
"decoration must not move the pen: {under_adv} vs {plain_adv}"
|
||||
);
|
||||
assert!(plain_adv > 0.0, "two glyphs advance the pen");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_advance_scales_with_height_and_width_factor() {
|
||||
let mut atlas = GlyphAtlas::new(512, 512);
|
||||
let (_, base) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AB");
|
||||
let (_, tall) =
|
||||
layout_glyph_quads(&mut atlas, 20.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "AB");
|
||||
assert!(
|
||||
(tall / base - 2.0).abs() < 0.05,
|
||||
"double height ~ double advance: {tall} vs {base}"
|
||||
);
|
||||
let (_, wide) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 2.0, 0.0, 1.0, "txt", false, "AB");
|
||||
assert!(
|
||||
(wide / base - 2.0).abs() < 0.05,
|
||||
"double width factor ~ double advance: {wide} vs {base}"
|
||||
);
|
||||
let (_, empty) =
|
||||
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, "");
|
||||
assert_eq!(empty, 0.0, "empty text advances nothing");
|
||||
}
|
||||
}
|
||||
@@ -139,10 +139,18 @@ fn canonical_family_name_uncached(family: &str) -> Option<String> {
|
||||
|
||||
/// Resolve a family name to a concrete face id (regular weight/style).
|
||||
fn face_id(family: &str) -> Option<fontdb::ID> {
|
||||
face_id_weighted(family, fontdb::Weight::NORMAL)
|
||||
}
|
||||
|
||||
/// Resolve a family name to a concrete face id at the requested weight
|
||||
/// (e.g. bold runs). Falls back through fontdb's fuzzy matching, so a
|
||||
/// family without a bold face still resolves — to its regular face.
|
||||
fn face_id_weighted(family: &str, weight: fontdb::Weight) -> Option<fontdb::ID> {
|
||||
let db = &fonts().db;
|
||||
let canonical = canonical_family_name(family)?;
|
||||
let query = fontdb::Query {
|
||||
families: &[fontdb::Family::Name(&canonical)],
|
||||
weight,
|
||||
..Default::default()
|
||||
};
|
||||
db.query(&query)
|
||||
@@ -157,6 +165,18 @@ pub fn with_face_data<T>(family: &str, f: impl FnOnce(&[u8], u32) -> T) -> Optio
|
||||
fonts().db.with_face_data(id, f)
|
||||
}
|
||||
|
||||
/// Borrow the raw face bytes for `family` at `weight` (see
|
||||
/// [`face_id_weighted`]). The PDF exporter uses this to embed the bold face
|
||||
/// for bold runs instead of faux-stroking the regular one.
|
||||
pub fn with_face_data_weighted<T>(
|
||||
family: &str,
|
||||
weight: fontdb::Weight,
|
||||
f: impl FnOnce(&[u8], u32) -> T,
|
||||
) -> Option<T> {
|
||||
let id = face_id_weighted(family, weight)?;
|
||||
fonts().db.with_face_data(id, f)
|
||||
}
|
||||
|
||||
/// The family of the installed face whose file is named `name` — a bare file
|
||||
/// name or any path to one, matched case-insensitively.
|
||||
fn family_for_file(name: &str) -> Option<String> {
|
||||
|
||||
Reference in new issue
Block a user