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 = (self.uvs_subtable_offset != 0)
.then(|| font_data.get(self.uvs_subtable_offset as usize..))
.flatten()
.and_then(|data| Cmap14::read(FontData::new(data)).ok());
Some(Charmap {
subtable,
uvs_subtable,
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: Option<Cmap14<'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> {
self.uvs_subtable
.as_ref()?
.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: 80 additions & 16 deletions parley/src/shape/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,43 +348,107 @@ 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.
// Within each font, an exact cmap format 14 mapping beats the
// color-table heuristic. Fonts with the matching glyph style come
// first, and mismatched fonts stay as a fallback so the base
// character still renders.
// See <https://www.unicode.org/reports/tr51/#Presentation_Style>.
let variation_sequences: SmallVec<[(char, char); 2]> = cluster
.chars()
.windows(2)
.filter(|pair| matches!(pair[1].ch, '\u{FE0E}' | '\u{FE0F}'))
.map(|pair| (pair[0].ch, pair[1].ch))
.collect();
let requested_color = variation_sequences
.first()
.map(|(_, selector)| *selector == '\u{FE0F}');

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 =
requested_color.is_none_or(|color| font.has_color_tables == color);
let mut exact_variant = !variation_sequences.is_empty();
let mut variant_bases: SmallVec<[char; 2]> = SmallVec::new();
for &(base, selector) in &variation_sequences {
match charmap.map_variant(base, selector) {
Some(fontique::MapVariant::Variant(glyph)) if glyph.to_u32() != 0 => {
matches_presentation = true;
variant_bases.push(base);
}
// The nominal glyph is declared correct for this sequence.
Some(fontique::MapVariant::UseDefault) => matches_presentation = true,
_ => exact_variant = false,
}
}

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()
// A variant mapping covers its base character even when
// the nominal character map does not encode it.
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 {
// Exact mappings win only when the font also covers the rest of
// the cluster, so a partial font cannot shadow a complete one.
if exact_variant && coverage.is_complete() {
selected_font = Some(SelectedFont { font: font.clone() });
best_coverage = coverage;

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

// A zero-coverage font must not shadow one that can render the base
// character, whichever bucket each landed in.
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
1 change: 1 addition & 0 deletions parley/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

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