mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-27 19:56:51 -06:00
A character the applied font lacks is drawn as that font's missing-glyph
box (its .notdef, gid 0), on screen and in PDF, and Preflight lists the
characters by font ("Missing glyphs in <family> <style>: …", an error,
at most 20 named). A .notdef without an outline is drawn as a hollow box
across its advance, so a missing character never disappears. PDF draws
.notdef glyphs as paths, which PDF/A and PDF/UA accept. Kenten marks are
set in their text's font the same way.
The document setting Draw Missing Glyphs from Fallback Fonts
(DocSettings::glyph_fallback; Preferences > Composition,
document.preferences {glyphFallback}, undoable) draws them from fallback
fonts instead (the language-aware chains, then any font; kenten marks
from a fallback font, else a bullet). It is off in new documents and
IDML imports and on in files saved before it existed, which keep
drawing as they did. A missing font's substitute always uses fallback
fonts. The composition cache's signature includes the setting. Tests
whose Arabic comes from a fallback font turn the setting on.
128 lines
5.1 KiB
Rust
128 lines
5.1 KiB
Rust
//! Ruby and kenten: small glyphs set over laid-out text (to its right in vertical frames, where
|
|
//! the frame's turn carries "above" there). They don't change line breaks or leading.
|
|
|
|
use designcraft_fonts::ScopedFonts;
|
|
use designcraft_geom::Shape as _;
|
|
|
|
use crate::{PlacedGlyph, RunStyle, upright_in_vertical};
|
|
|
|
/// Ruby and emphasis marks are half the base size.
|
|
const SCALE: f64 = 0.5;
|
|
|
|
/// A glyph of `text` set at `size` in `base`'s font from `x`, above the base text; returns them
|
|
/// and their total advance.
|
|
fn set(base: &PlacedGlyph, text: &str, size: f64, base_size: f64) -> (Vec<PlacedGlyph>, f64) {
|
|
let face = &base.face;
|
|
let k = size / face.units_per_em();
|
|
// Its baseline clears the base text's em box top.
|
|
let y = -(base_size * 0.88 + size * 0.2);
|
|
let mut x = 0.0;
|
|
let mut out = Vec::new();
|
|
for sg in designcraft_fonts::shape(face, text, &[], |c| c) {
|
|
let ch = text[sg.cluster..].chars().next().unwrap_or(' ');
|
|
let adv = sg.x_advance as f64 * k;
|
|
out.push(PlacedGlyph {
|
|
gid: sg.gid,
|
|
x: x + sg.x_offset as f64 * k,
|
|
y: y - sg.y_offset as f64 * k,
|
|
adv,
|
|
sx: k,
|
|
sy: k,
|
|
len: 0,
|
|
visible: true,
|
|
upright: upright_in_vertical(ch),
|
|
tcy: None,
|
|
rtl: false,
|
|
..base.clone()
|
|
});
|
|
x += adv;
|
|
}
|
|
(out, x)
|
|
}
|
|
|
|
/// The emphasis `character` (a sesame dot when empty) centred over `b`, in `b`'s font. A
|
|
/// character the font lacks is its missing-glyph box, unless `fallback` (fallback fonts draw
|
|
/// missing glyphs): then a fallback font draws it, or failing that a bullet.
|
|
fn kenten(db: &ScopedFonts<'_>, b: &PlacedGlyph, size: f64, character: &str, fallback: bool) -> Vec<PlacedGlyph> {
|
|
let character = if character.is_empty() { "\u{FE45}" } else { character };
|
|
let mut base = b.clone();
|
|
if fallback
|
|
&& let Some(c) = character.chars().next()
|
|
&& !base.face.covers(c)
|
|
&& let Some(face) = db.fallback_for(c, base.face.id(), None)
|
|
{
|
|
base.face = designcraft_fonts::FaceRef::of(&face);
|
|
}
|
|
let (mut dot, mut w) = set(&base, character, size * SCALE, size);
|
|
if fallback && dot.first().is_some_and(|d| d.gid == 0) {
|
|
(dot, w) = set(b, "\u{2022}", size * SCALE, size);
|
|
}
|
|
// Centre the mark's ink (its advance may hold it off centre) and sit it just over the text.
|
|
let ink = dot.first().map(|d| designcraft_fonts::FontDb::global().outline(&d.face, d.gid).bounding_box()).filter(|r| r.width() > 0.0);
|
|
for d in &mut dot {
|
|
match ink {
|
|
Some(r) => {
|
|
d.x = b.x + b.adv / 2.0 - (r.x0 + r.x1) / 2.0 * d.sx;
|
|
d.y = -(size * 0.92) - r.y1 * d.sy;
|
|
}
|
|
None => d.x += b.x + (b.adv - w) / 2.0,
|
|
}
|
|
}
|
|
dot
|
|
}
|
|
|
|
/// Add the ruby and kenten of a laid-out line's glyphs. `glyph_fallback`: the document draws
|
|
/// missing glyphs from fallback fonts (a missing font's substitute always does).
|
|
pub(crate) fn annotate(db: &ScopedFonts<'_>, styles: &[RunStyle], line: &mut Vec<PlacedGlyph>, glyph_fallback: bool) {
|
|
let style = |g: &PlacedGlyph| styles.get(g.style as usize);
|
|
if !line.iter().any(|g| style(g).is_some_and(|s| s.ruby.is_some() || s.kenten)) {
|
|
return;
|
|
}
|
|
let mut extra = Vec::new();
|
|
let mut i = 0;
|
|
while i < line.len() {
|
|
let g = &line[i];
|
|
let Some(st) = style(g).filter(|_| g.len > 0 && g.visible) else {
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if st.kenten && g.adv > 0.0 {
|
|
extra.extend(kenten(db, g, st.size, &st.kenten_character, glyph_fallback || st.missing_font));
|
|
}
|
|
let Some(text) = &st.ruby else {
|
|
i += 1;
|
|
continue;
|
|
};
|
|
// The group: following glyphs with the same ruby.
|
|
let mut j = i + 1;
|
|
while j < line.len() && (line[j].len == 0 || style(&line[j]).is_some_and(|s| s.ruby.as_ref() == Some(text))) {
|
|
j += 1;
|
|
}
|
|
let base: Vec<&PlacedGlyph> = line[i..j].iter().filter(|b| b.len > 0).collect();
|
|
let x0 = base.iter().map(|b| b.x).fold(f64::MAX, f64::min);
|
|
let x1 = base.iter().map(|b| b.x + b.adv).fold(f64::MIN, f64::max);
|
|
let (mut glyphs, w) = set(g, text, st.size * SCALE, st.size);
|
|
let n = glyphs.len().max(1) as f64;
|
|
if w < x1 - x0 {
|
|
// Shorter than the base: spread out, half a gap at each end (the 1-2-1 rule).
|
|
let gap = (x1 - x0 - w) / n;
|
|
for (k, r) in glyphs.iter_mut().enumerate() {
|
|
r.x += x0 + gap * (k as f64 + 0.5);
|
|
}
|
|
} else {
|
|
// Longer: centred over the base.
|
|
let start = (x0 + x1 - w) / 2.0;
|
|
for r in &mut glyphs {
|
|
r.x += start;
|
|
}
|
|
}
|
|
extra.extend(glyphs);
|
|
// Kenten on the rest of the group.
|
|
for b in line[i + 1..j].iter().filter(|b| st.kenten && b.len > 0 && b.adv > 0.0) {
|
|
extra.extend(kenten(db, b, st.size, &st.kenten_character, glyph_fallback || st.missing_font));
|
|
}
|
|
i = j;
|
|
}
|
|
line.extend(extra);
|
|
}
|