Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[skrifa] detect "tricky" fonts by family name #1305

Merged
merged 4 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
18 changes: 13 additions & 5 deletions fauntlet/src/font/freetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ impl FreeTypeInstance {
.new_memory_face2(data.clone(), options.index as isize)
.ok()?;
let mut load_flags = LoadFlag::NO_BITMAP;
match options.hinting {
None => load_flags |= LoadFlag::NO_HINTING,
Some(hinting) => load_flags |= hinting.freetype_load_flags(),
};
// Ignore hinting settings for tricky fonts. Let FreeType do its own
// thing
if !face.is_tricky() {
match options.hinting {
None => load_flags |= LoadFlag::NO_HINTING,
Some(hinting) => load_flags |= hinting.freetype_load_flags(),
};
}
if options.ppem != 0 {
face.set_pixel_sizes(options.ppem, options.ppem).ok()?;
} else {
load_flags |= LoadFlag::NO_SCALE | LoadFlag::NO_HINTING;
load_flags |= LoadFlag::NO_SCALE | LoadFlag::NO_HINTING | LoadFlag::NO_AUTOHINT;
}
if !options.coords.is_empty() {
let mut ft_coords = vec![];
Expand Down Expand Up @@ -66,6 +70,10 @@ impl FreeTypeInstance {
self.face.family_name()
}

pub fn is_tricky(&self) -> bool {
self.face.is_tricky()
}

pub fn is_scalable(&self) -> bool {
self.face.is_scalable()
}
Expand Down
41 changes: 32 additions & 9 deletions fauntlet/src/font/skrifa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use ::skrifa::{
raw::{FontRef, TableProvider},
GlyphId, MetadataProvider, OutlineGlyphCollection,
};
use skrifa::outline::HintingOptions;

use super::{InstanceOptions, SharedFontData};

Expand All @@ -25,16 +26,34 @@ impl<'a> SkrifaInstance<'a> {
Size::unscaled()
};
let outlines = font.outline_glyphs();
let hinter = if options.ppem != 0 && options.hinting.is_some() {
Some(
HintingInstance::new(
&outlines,
size,
options.coords,
options.hinting.unwrap().skrifa_options(),
let hinter = if options.ppem != 0 {
if options.hinting.is_some() {
Some(
HintingInstance::new(
&outlines,
size,
options.coords,
options.hinting.unwrap().skrifa_options(),
)
.ok()?,
)
.ok()?,
)
} else if outlines.require_interpreter() {
// In this case, we must use the interpreter to match FreeType
Some(
HintingInstance::new(
&outlines,
size,
options.coords,
HintingOptions {
engine: skrifa::outline::Engine::Interpreter,
target: skrifa::outline::Target::Mono,
},
)
.ok()?,
)
} else {
None
}
} else {
None
};
Expand All @@ -47,6 +66,10 @@ impl<'a> SkrifaInstance<'a> {
})
}

pub fn is_tricky(&self) -> bool {
self.font.outline_glyphs().require_interpreter()
}

pub fn glyph_count(&self) -> u16 {
self.font
.maxp()
Expand Down
22 changes: 22 additions & 0 deletions skrifa/src/outline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ mod glyf;
mod hint;
mod metrics;
mod path;
mod tricky;
mod unscaled;

#[cfg(test)]
Expand Down Expand Up @@ -608,6 +609,27 @@ impl<'a> OutlineGlyphCollection<'a> {
}
}

/// Returns true when the interpreter engine _must_ be used for hinting
/// this set of outlines to produce correct results.
///
/// This corresponds so FreeType's `FT_FACE_FLAG_TRICKY` face flag. See
/// the documentation for that [flag](https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_face_flag_xxx)
/// for more detail.
///
/// When this returns `true`, you should construct a [`HintingInstance`]
/// with [`HintingOptions::engine`] set to [`Engine::Interpreter`] and
/// [`HintingOptions::target`] set to [`Target::Mono`].
///
/// # Performance
/// This digs through the name table and potentially computes checksums
/// so it may be slow. You should cache the result of this function if
/// possible.
pub fn require_interpreter(&self) -> bool {
drott marked this conversation as resolved.
Show resolved Hide resolved
self.font()
.map(|font| tricky::is_tricky(font))
.unwrap_or_default()
}

pub(crate) fn font(&self) -> Option<&FontRef<'a>> {
match &self.kind {
OutlineCollectionKind::Glyf(glyf) => Some(&glyf.font),
Expand Down
140 changes: 140 additions & 0 deletions skrifa/src/outline/tricky.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//! Match FreeType's "tricky" font detection.
drott marked this conversation as resolved.
Show resolved Hide resolved
drott marked this conversation as resolved.
Show resolved Hide resolved
//!
//! Tricky fonts are those that have busted outlines and require the bytecode
//! interpreter to produce something that makes sense.

use crate::{string::StringId, FontRef, MetadataProvider};

pub(super) fn is_tricky(font: &FontRef) -> bool {
drott marked this conversation as resolved.
Show resolved Hide resolved
has_tricky_name(font)
}

fn has_tricky_name(font: &FontRef) -> bool {
font.localized_strings(StringId::FAMILY_NAME)
.english_or_first()
.map(|name| {
let mut buf = [0u8; MAX_TRICKY_NAME_LEN];
let mut len = 0;
let mut chars = name.chars();
for ch in chars.by_ref().take(MAX_TRICKY_NAME_LEN) {
buf[len] = ch as u8;
len += 1;
}
if chars.next().is_some() {
return false;
}
is_tricky_name(core::str::from_utf8(&buf[..len]).unwrap_or_default())
})
.unwrap_or_default()
}

/// Does this family name belong to a "tricky" font?
///
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L174>
fn is_tricky_name(name: &str) -> bool {
drott marked this conversation as resolved.
Show resolved Hide resolved
let name = skip_pdf_random_tag(name);
TRICKY_NAMES
.iter()
// FreeType uses strstr(name, tricky_name) so we use contains() to
// match behavior.
.any(|tricky_name| name.contains(*tricky_name))
}

/// Fonts embedded in PDFs add random prefixes. Strip these
/// for tricky font comparison purposes.
///
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L153>
fn skip_pdf_random_tag(name: &str) -> &str {
let bytes = name.as_bytes();
// Random tag is 6 uppercase letters followed by a +
if bytes.len() < 8 || bytes[6] != b'+' || !bytes.iter().take(6).all(|b| b.is_ascii_uppercase())
{
return name;
}
core::str::from_utf8(&bytes[7..]).unwrap_or(name)
}

/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L180>
#[rustfmt::skip]
const TRICKY_NAMES: &[&str] = &[
"cpop", /* dftt-p7.ttf; version 1.00, 1992 [DLJGyShoMedium] */
"DFGirl-W6-WIN-BF", /* dftt-h6.ttf; version 1.00, 1993 */
"DFGothic-EB", /* DynaLab Inc. 1992-1995 */
"DFGyoSho-Lt", /* DynaLab Inc. 1992-1995 */
"DFHei", /* DynaLab Inc. 1992-1995 [DFHei-Bd-WIN-HK-BF] */
/* covers "DFHei-Md-HK-BF", maybe DynaLab Inc. */

"DFHSGothic-W5", /* DynaLab Inc. 1992-1995 */
"DFHSMincho-W3", /* DynaLab Inc. 1992-1995 */
"DFHSMincho-W7", /* DynaLab Inc. 1992-1995 */
"DFKaiSho-SB", /* dfkaisb.ttf */
"DFKaiShu", /* covers "DFKaiShu-Md-HK-BF", maybe DynaLab Inc. */
"DFKai-SB", /* kaiu.ttf; version 3.00, 1998 [DFKaiShu-SB-Estd-BF] */

"DFMing", /* DynaLab Inc. 1992-1995 [DFMing-Md-WIN-HK-BF] */
/* covers "DFMing-Bd-HK-BF", maybe DynaLab Inc. */

"DLC", /* dftt-m7.ttf; version 1.00, 1993 [DLCMingBold] */
/* dftt-f5.ttf; version 1.00, 1993 [DLCFongSung] */
/* covers following */
/* "DLCHayMedium", dftt-b5.ttf; version 1.00, 1993 */
/* "DLCHayBold", dftt-b7.ttf; version 1.00, 1993 */
/* "DLCKaiMedium", dftt-k5.ttf; version 1.00, 1992 */
/* "DLCLiShu", dftt-l5.ttf; version 1.00, 1992 */
/* "DLCRoundBold", dftt-r7.ttf; version 1.00, 1993 */

"HuaTianKaiTi?", /* htkt2.ttf */
"HuaTianSongTi?", /* htst3.ttf */
"Ming(for ISO10646)", /* hkscsiic.ttf; version 0.12, 2007 [Ming] */
/* iicore.ttf; version 0.07, 2007 [Ming] */
"MingLiU", /* mingliu.ttf */
/* mingliu.ttc; version 3.21, 2001 */
"MingMedium", /* dftt-m5.ttf; version 1.00, 1993 [DLCMingMedium] */
"PMingLiU", /* mingliu.ttc; version 3.21, 2001 */
"MingLi43", /* mingli.ttf; version 1.00, 1992 */
];

const MAX_TRICKY_NAME_LEN: usize = 18;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ensure_max_tricky_name_len() {
let max_len = TRICKY_NAMES.iter().fold(0, |acc, name| acc.max(name.len()));
assert_eq!(max_len, MAX_TRICKY_NAME_LEN);
}

#[test]
fn skip_pdf_tags() {
// length must be at least 8
assert_eq!(skip_pdf_random_tag("ABCDEF+"), "ABCDEF+");
// first six chars must be ascii uppercase
assert_eq!(skip_pdf_random_tag("AbCdEF+Arial"), "AbCdEF+Arial");
// no numbers
assert_eq!(skip_pdf_random_tag("Ab12EF+Arial"), "Ab12EF+Arial");
// missing +
assert_eq!(skip_pdf_random_tag("ABCDEFArial"), "ABCDEFArial");
// too long
assert_eq!(skip_pdf_random_tag("ABCDEFG+Arial"), "ABCDEFG+Arial");
// too short
assert_eq!(skip_pdf_random_tag("ABCDE+Arial"), "ABCDE+Arial");
// just right
assert_eq!(skip_pdf_random_tag("ABCDEF+Arial"), "Arial");
}

#[test]
fn all_tricky_names() {
for name in TRICKY_NAMES {
assert!(is_tricky_name(name));
}
}

#[test]
fn non_tricky_names() {
for not_tricky in ["Roboto", "Arial", "Helvetica", "Blah", ""] {
assert!(!is_tricky_name(not_tricky));
}
}
}
Loading