Skip to content
Open
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
33 changes: 32 additions & 1 deletion fontique/src/charmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@

use read_fonts::{
FontData, FontRead, FontRef, TableProvider, TopLevelTable,
tables::cmap::{Cmap, CmapSubtable},
tables::cmap::{Cmap, Cmap14, CmapSubtable},
types::GlyphId,
};

pub use read_fonts::tables::cmap::MapVariant;

/// Metadata for constructing a character map from font data.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct CharmapIndex {
subtable_offset: u32,
/// Offset of the format 14 subtable, or 0 when the font has none.
uvs_subtable_offset: u32,
is_symbol: bool,
is_mac_roman: bool,
}
Expand All @@ -31,8 +35,14 @@ impl CharmapIndex {
.map(|rec| rec.offset())?;
let (_, rec, _) = cmap.best_subtable()?;
let subtable_offset = cmap_offset.checked_add(rec.subtable_offset().to_u32())?;
let uvs_subtable_offset = cmap
.uvs_subtable()
.and_then(|(index, _)| cmap.encoding_records().get(index as usize))
.and_then(|rec| cmap_offset.checked_add(rec.subtable_offset().to_u32()))
.unwrap_or_default();
Some(Self {
subtable_offset,
uvs_subtable_offset,
is_symbol: rec.is_symbol(),
is_mac_roman: rec.is_mac_roman(),
})
Expand All @@ -42,8 +52,13 @@ impl CharmapIndex {
pub fn charmap<'a>(&self, font_data: &'a [u8]) -> Option<Charmap<'a>> {
let subtable_data = font_data.get(self.subtable_offset as usize..)?;
let subtable = CmapSubtable::read(FontData::new(subtable_data)).ok()?;
let uvs_subtable_data = (self.uvs_subtable_offset != 0)
.then(|| font_data.get(self.uvs_subtable_offset as usize..))
.flatten()
.map(FontData::new);
Some(Charmap {
subtable,
uvs_subtable_data,
is_symbol: self.is_symbol,
is_mac_roman: self.is_mac_roman,
})
Expand All @@ -54,6 +69,7 @@ impl CharmapIndex {
#[derive(Clone)]
pub struct Charmap<'a> {
subtable: CmapSubtable<'a>,
uvs_subtable_data: Option<FontData<'a>>,
is_symbol: bool,
is_mac_roman: bool,
}
Expand Down Expand Up @@ -96,6 +112,21 @@ impl Charmap<'_> {
}
result.map(|gid| gid.to_u32())
}

/// Maps a [variation sequence] to a glyph identifier using the format 14 subtable.
///
/// [`MapVariant::UseDefault`] means the nominal glyph for `codepoint` is the right one.
///
/// [variation sequence]: https://www.unicode.org/reports/tr51/#Emoji_Variation_Sequences
pub fn map_variant(
&self,
codepoint: impl Into<u32>,
selector: impl Into<u32>,
) -> Option<MapVariant> {
Cmap14::read(self.uvs_subtable_data?)
.ok()?
.map_variant(codepoint.into(), selector.into())
}
}

#[rustfmt::skip]
Expand Down
4 changes: 4 additions & 0 deletions fontique/src/collection/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ pub struct QueryFont {
pub synthesis: Synthesis,
/// Data used for constructing a character map for this font.
pub charmap_index: CharmapIndex,
/// Whether the font has a color glyph table.
pub has_color_tables: bool,
}

impl QueryFont {
Expand Down Expand Up @@ -280,6 +282,7 @@ fn load_bucket<'a>(
attributes.weight,
),
charmap_index: font_info.charmap_index(),
has_color_tables: font_info.has_color_tables(),
});
}
if fonts.is_empty() {
Expand Down Expand Up @@ -319,6 +322,7 @@ fn load_font<'a>(
index: blob_index,
synthesis,
charmap_index: font_info.charmap_index(),
has_color_tables: font_info.has_color_tables(),
});
if let Entry::Ok(font) = status {
Some(font)
Expand Down
20 changes: 20 additions & 0 deletions fontique/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ use smallvec::SmallVec;

type AxisVec = SmallVec<[AxisInfo; 1]>;

const SBIX: Tag = Tag::new(b"sbix");
const COLR: Tag = Tag::new(b"COLR");
const CPAL: Tag = Tag::new(b"CPAL");
const CBDT: Tag = Tag::new(b"CBDT");
const CBLC: Tag = Tag::new(b"CBLC");

/// Color tables in the pairs Blink's `ColorTableLookup` accepts.
fn has_color_tables(font: &FontRef<'_>) -> bool {
font.table_data(SBIX).is_some()
|| (font.table_data(COLR).is_some() && font.table_data(CPAL).is_some())
|| (font.table_data(CBDT).is_some() && font.table_data(CBLC).is_some())
}

/// Representation of a single font in a family.
#[derive(Clone, Debug)]
pub struct FontInfo {
Expand All @@ -25,6 +38,7 @@ pub struct FontInfo {
axes: AxisVec,
attr_axes: u8,
charmap_index: CharmapIndex,
has_color_tables: bool,
}

impl FontInfo {
Expand Down Expand Up @@ -204,6 +218,11 @@ impl FontInfo {
self.attr_axes & OPTICAL_SIZE_AXIS != 0
}

/// Returns `true` if the font has a color glyph table.
pub fn has_color_tables(&self) -> bool {
self.has_color_tables
}

/// Returns the index used for constructing a [Charmap] for this font.
///
/// [Charmap]: crate::Charmap
Expand Down Expand Up @@ -255,6 +274,7 @@ impl FontInfo {
axes,
attr_axes,
charmap_index,
has_color_tables: has_color_tables(font),
})
}

Expand Down
2 changes: 1 addition & 1 deletion fontique/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub use linebender_resource_handle::Blob;
pub use script::ScriptExt;

pub use attributes::Attributes;
pub use charmap::{Charmap, CharmapIndex};
pub use charmap::{Charmap, CharmapIndex, MapVariant};
pub use collection::{Collection, CollectionOptions, Query, QueryFamily, QueryFont, QueryStatus};
pub use fallback::FallbackKey;
pub use family::{FamilyId, FamilyInfo};
Expand Down
96 changes: 77 additions & 19 deletions parley/src/shape/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,43 +347,92 @@ impl<'a, 'b, B: Brush> parley_engine::FontSelector for FontSelector<'a, 'b, B> {
self.features = self.rcx.features(style.font_features).unwrap_or(&[]);
}

// `U+FE0F` requests a color glyph and `U+FE0E` a monochrome one.
// See <https://www.unicode.org/reports/tr51/#Presentation_Style>.
let mut variation_sequences: SmallVec<[VariationSequence; 2]> = SmallVec::new();
if cluster.has_presentation_selector() {
let chars = cluster.chars();
variation_sequences.extend(
chars
.windows(2)
.filter(|pair| matches!(pair[1].ch, '\u{FE0E}' | '\u{FE0F}'))
.map(|pair| VariationSequence {
base: pair[0].ch,
selector: pair[1].ch,
base_is_unique: chars.iter().filter(|c| c.ch == pair[0].ch).count() == 1,
}),
);
}

let mut selected_font = None;
let mut best_coverage = Coverage::NONE;
let mut mismatched_font = None;
let mut mismatched_coverage = Coverage::NONE;

self.query.matches_with(|font| {
let Some(charmap) = font.charmap() else {
return fontique::QueryStatus::Continue;
};

let mut matches_presentation = true;
let mut variant_bases: SmallVec<[char; 2]> = SmallVec::new();
for sequence in &variation_sequences {
match charmap.map_variant(sequence.base, sequence.selector) {
Some(fontique::MapVariant::Variant(glyph)) if glyph.to_u32() != 0 => {
if sequence.base_is_unique {
variant_bases.push(sequence.base);
}
}
// The nominal glyph is declared correct for this sequence.
Some(fontique::MapVariant::UseDefault) => {}
_ => {
matches_presentation &=
font.has_color_tables == (sequence.selector == '\u{FE0F}');
}
}
}

let coverage = cluster.calculate_coverage(
|ch| {
charmap
.map(ch)
.map(|g| {
// Any non-zero value indicates the existence of a glyph.
g != 0
})
.unwrap_or_default()
variant_bases.contains(&ch)
|| charmap
.map(ch)
.map(|g| {
// Any non-zero value indicates the existence of a glyph.
g != 0
})
.unwrap_or_default()
},
self.analysis_data_sources,
);
if coverage > best_coverage {
selected_font = Some(SelectedFont { font: font.clone() });
best_coverage = coverage;

if coverage.is_complete() {
fontique::QueryStatus::Stop
} else {
fontique::QueryStatus::Continue
}
} else {
if selected_font.is_none() {
if matches_presentation {
if coverage > best_coverage {
selected_font = Some(SelectedFont { font: font.clone() });
best_coverage = coverage;
if coverage.is_complete() {
return fontique::QueryStatus::Stop;
}
} else if selected_font.is_none() {
selected_font = Some(SelectedFont { font: font.clone() });
}
fontique::QueryStatus::Continue
} else if coverage > mismatched_coverage {
mismatched_font = Some(SelectedFont { font: font.clone() });
mismatched_coverage = coverage;
} else if mismatched_font.is_none() {
mismatched_font = Some(SelectedFont { font: font.clone() });
}
fontique::QueryStatus::Continue
});

// Presentation only outranks coverage while there is any coverage to compare.
let selected_font = if best_coverage > Coverage::NONE {
selected_font
} else if mismatched_coverage > Coverage::NONE {
mismatched_font
} else {
selected_font.or(mismatched_font)
};

selected_font
.map(|selected_font| selected_font.font)
.map(|font| FontInstance {
Expand Down Expand Up @@ -451,6 +500,15 @@ fn any_font(query: &mut Query<'_>) -> Option<QueryFont> {
found
}

/// A base character followed by an emoji variation selector.
struct VariationSequence {
base: char,
selector: char,
/// Coverage is matched by character value, so a variant mapping only proves
/// coverage of an unrepeated base.
base_is_unique: bool,
}

struct SelectedFont {
font: QueryFont,
}
Expand Down
1 change: 1 addition & 0 deletions parley/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@

mod test_analysis;
mod test_builders;
mod test_font_selection;
mod test_layout;
mod utils;
Loading
Loading