Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ATTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `assets/icons/x.svg` | Lucide icon "x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/zoom-in.svg` | Lucide icon "zoom-in" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-in.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/zoom-out.svg` | Lucide icon "zoom-out" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-out.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/fonts/Inter-Regular.ttf` | Inter Regular | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels |
| `assets/fonts/Inter-Regular.ttf` | Inter Regular | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels; slanted fallback glyphs in Fill & Sign typed signatures for characters Dancing Script lacks (Cyrillic, Greek), drawn as filled paths |
| `assets/fonts/Inter-Medium.ttf` | Inter Medium | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels |
| `assets/fonts/Inter-SemiBold.ttf` | Inter SemiBold | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels |
| `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono Regular | The JetBrains Mono Project Authors | OFL-1.1 | https://github.com/JetBrains/JetBrainsMono | UI monospace font; demo PDF code |
Expand Down
2 changes: 1 addition & 1 deletion ATTRIBUTION.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2119,7 +2119,7 @@ source = "https://github.com/rsms/inter"
licence = "OFL-1.1"
licence_file = "assets/fonts/OFL-Inter.txt"
kind = "font"
usage = "UI font, embedded in the app; demo PDF labels"
usage = "UI font, embedded in the app; demo PDF labels; slanted fallback glyphs in Fill & Sign typed signatures for characters Dancing Script lacks (Cyrillic, Greek), drawn as filled paths"
sha256 = "40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82"

[[asset]]
Expand Down
2 changes: 1 addition & 1 deletion crates/fonts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub use craft::{
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_japanese_font, document_japanese_font_for_style,
document_japanese_fonts_for_style, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_japanese_fonts, ui_telugu_fonts,
};
pub use script::{GlyphError, GlyphOutline, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_outline};
pub use script::{GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_outline};

/// Approximate advance of `s` in Helvetica (or Arial) at `size` points.
pub fn helvetica_width(s: &str, size: f64) -> f64 {
Expand Down
75 changes: 66 additions & 9 deletions crates/fonts/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ use skrifa::{FontRef, GlyphId, MetadataProvider};

static FONT: &[u8] = include_bytes!("../../../assets/fonts/DancingScript.ttf");

/// Inter Regular, the interface font. Typed signatures fall back to it, slanted, for characters
/// the script font lacks (Dancing Script is Latin and Vietnamese only: no Cyrillic or Greek).
pub static INTER_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");

/// Forward slant of fallback glyphs in a typed signature (about 12°), so they lean like the script.
const FALLBACK_SLANT: f64 = 0.21;

/// Bound signature work while allowing long personal names.
pub const MAX_SIGNATURE_CHARS: usize = 256;

Expand Down Expand Up @@ -51,6 +58,8 @@ struct Flatten {
contours: Vec<Vec<[f64; 2]>>,
cur: Vec<[f64; 2]>,
scale: f64,
/// Horizontal shear applied to every point (x += slant × y), for slanted fallback glyphs.
slant: f64,
dx: f64,
points: usize,
too_complex: bool,
Expand All @@ -59,11 +68,12 @@ struct Flatten {

impl Flatten {
fn new(scale: f64) -> Self {
Self { contours: Vec::new(), cur: Vec::new(), scale, dx: 0.0, points: 0, too_complex: false, max_points: 4096 }
Self { contours: Vec::new(), cur: Vec::new(), scale, slant: 0.0, dx: 0.0, points: 0, too_complex: false, max_points: 4096 }
}

fn pt(&self, x: f32, y: f32) -> [f64; 2] {
[self.dx + x as f64 * self.scale, y as f64 * self.scale]
let (x, y) = (x as f64 * self.scale, y as f64 * self.scale);
[self.dx + x + self.slant * y, y]
}

fn last(&self) -> [f64; 2] {
Expand Down Expand Up @@ -178,8 +188,9 @@ pub(crate) fn bounded_outline(font: &FontRef, gid: GlyphId, width: f64) -> Resul
Ok(GlyphOutline { contours: pen.contours, width, bbox })
}

/// The outlines of `text` in the script font (characters it lacks are skipped).
/// Over-limit input returns an empty outline instead of a silently truncated signature.
/// The outlines of `text` in the script font. Characters it lacks (Cyrillic, Greek, …) are drawn
/// from Inter, slanted and scaled to the script's capital height; characters neither font has are
/// skipped. Over-limit input returns an empty outline instead of a silently truncated signature.
pub fn script_outline(text: &str) -> ScriptOutline {
if text.chars().take(MAX_SIGNATURE_CHARS + 1).count() > MAX_SIGNATURE_CHARS {
return ScriptOutline::default();
Expand All @@ -191,17 +202,39 @@ pub fn script_outline(text: &str) -> ScriptOutline {
let charmap = font.charmap();
let glyphs = font.outline_glyphs();
let advances = font.glyph_metrics(Size::unscaled(), loc);
// The fallback face, scaled so its capitals are as tall as the script's.
let fallback = FontRef::new(INTER_REGULAR).ok().map(|f| {
let m = f.metrics(Size::unscaled(), loc);
let em = 1.0 / m.units_per_em.max(1) as f64;
let ratio = match (metrics.cap_height, m.cap_height) {
(Some(ours), Some(theirs)) if ours > 0.0 && theirs > 0.0 => (ours as f64 * scale) / (theirs as f64 * em),
_ => 1.0,
};
let ratio = if ratio.is_finite() { ratio.clamp(0.5, 2.0) } else { 1.0 };
(f.charmap(), f.outline_glyphs(), f.glyph_metrics(Size::unscaled(), loc), em * ratio)
});
let mut pen = Flatten::new(scale);
// The 4096-point budget is for one untrusted document glyph (japanese_glyph), not a
// whole signature in our bundled font. Keep a separate bounded budget for the line.
pen.max_points = 262_144;
for ch in text.chars() {
let Some(gid) = charmap.map(ch) else { continue };
if let Some(g) = glyphs.get(gid) {
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), loc), &mut pen);
pen.close();
if let Some(gid) = charmap.map(ch) {
(pen.scale, pen.slant) = (scale, 0.0);
if let Some(g) = glyphs.get(gid) {
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), loc), &mut pen);
pen.close();
}
pen.dx += advances.advance_width(gid).unwrap_or(0.0) as f64 * scale;
} else if let Some((map, outlines, adv, em)) = &fallback
&& let Some(gid) = map.map(ch)
{
(pen.scale, pen.slant) = (*em, FALLBACK_SLANT);
if let Some(g) = outlines.get(gid) {
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), loc), &mut pen);
pen.close();
}
pen.dx += adv.advance_width(gid).unwrap_or(0.0) as f64 * em;
}
pen.dx += advances.advance_width(gid).unwrap_or(0.0) as f64 * scale;
}
pen.close();
if pen.too_complex {
Expand All @@ -224,6 +257,30 @@ mod tests {
assert!(super::script_outline(&"W".repeat(super::MAX_SIGNATURE_CHARS + 1)).contours.is_empty());
}

/// Issue #846: Cyrillic (and Greek) names come out of the fallback face instead of vanishing.
#[test]
fn cyrillic_and_greek_names_are_drawn_from_the_fallback_face() {
for name in ["Саша Дермановић", "Љубица Ђорђевић", "Ђурђа Џаковић", "Νίκος"] {
let o = super::script_outline(name);
let letters = name.chars().filter(|c| !c.is_whitespace()).count();
assert!(o.contours.len() >= letters, "{name}: {} contours for {letters} letters", o.contours.len());
for ch in name.chars().filter(|c| !c.is_whitespace()) {
let one = super::script_outline(&ch.to_string());
assert!(!one.contours.is_empty() && one.width > 0.0, "{name}: {ch} has no outline");
}
let [_, bottom, right, top] = o.bounds();
assert!(right <= o.width + 0.5 && top <= o.ascent + 0.2 && bottom >= o.descent - 0.2, "{name}: {:?}", o.bounds());
}
// Capitals from the fallback are about as tall as the script's.
let cap = |c: &str| super::script_outline(c).contours.iter().flatten().map(|p| p[1]).fold(0.0, f64::max);
let (latin, cyrillic) = (cap("H"), cap("Н"));
assert!((cyrillic / latin - 1.0).abs() < 0.2, "Н {cyrillic} vs H {latin}");
// Mixed names keep every letter, in order: the Cyrillic follows the Latin.
let mixed = super::script_outline("Ana Ана");
let latin_only = super::script_outline("Ana ");
assert!(mixed.width > latin_only.width + 0.5, "{} vs {}", mixed.width, latin_only.width);
}

#[test]
fn a_name_becomes_outlines() {
let o = super::script_outline("Ada L.");
Expand Down
3 changes: 2 additions & 1 deletion crates/ui-egui/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ pub fn font_definitions_for(prefer_hans: bool) -> FontDefinitions {
let add = |fonts: &mut FontDefinitions, name: &str, bytes: &'static [u8]| {
fonts.font_data.insert(name.to_owned(), Arc::new(FontData::from_static(bytes)));
};
add(&mut fonts, "Inter", include_bytes!("../../../assets/fonts/Inter-Regular.ttf"));
// Shared with typed signatures' fallback glyphs, so the font is embedded once.
add(&mut fonts, "Inter", pdfcraft_fonts::INTER_REGULAR);
add(&mut fonts, "Inter-Medium", include_bytes!("../../../assets/fonts/Inter-Medium.ttf"));
add(&mut fonts, "Inter-SemiBold", include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"));
add(&mut fonts, "JetBrainsMono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"));
Expand Down
Loading