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

[RFC] Deep-check for monospacity #317

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ smol_str = { version = "0.2.2", default-features = false }
swash = { version = "0.1.17", optional = true }
syntect = { version = "5.1.0", optional = true }
sys-locale = { version = "0.3.1", optional = true }
ttf-parser = { version = "0.21", default-features = false }
ttf-parser = { version = "0.25", default-features = false, features = [ "opentype-layout" ] }
unicode-linebreak = "0.1.5"
unicode-script = "0.5.5"
unicode-segmentation = "1.10.1"
Expand All @@ -40,7 +40,7 @@ features = ["hardcoded-data"]
default = ["std", "swash", "fontconfig"]
fontconfig = ["fontdb/fontconfig", "std"]
monospace_fallback = []
no_std = ["rustybuzz/libm", "hashbrown", "dep:libm"]
no_std = ["rustybuzz/libm", "ttf-parser/no-std-float", "hashbrown", "dep:libm"]
shape-run-cache = []
std = [
"fontdb/memmap",
Expand Down Expand Up @@ -73,3 +73,6 @@ opt-level = 1

[package.metadata.docs.rs]
features = ["vi"]

[patch.crates-io]
ttf-parser = { git = "https://github.com/MoSal/ttf-parser", branch = "codepoints_iter" }
76 changes: 65 additions & 11 deletions src/font/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,24 +90,78 @@ impl Font {
}

impl Font {
pub fn new(db: &fontdb::Database, id: fontdb::ID) -> Option<Self> {
#[cfg(feature = "monospace_fallback")]
fn proportional_monospaced(face: &ttf_parser::Face) -> Option<bool> {
use ttf_parser::cmap::{Format, Subtable};
use ttf_parser::Face;

// Pick a unicode cmap subtable to check against its glyphs
let cmap = face.tables().cmap.as_ref()?;
let subtable12 = cmap.subtables.into_iter().find(|subtable| {
subtable.is_unicode() && matches!(subtable.format, Format::SegmentedCoverage(_))
});
let subtable4_fn = || {
cmap.subtables.into_iter().find(|subtable| {
subtable.is_unicode()
&& matches!(subtable.format, Format::SegmentMappingToDeltaValues(_))
})
};
let unicode_subtable = subtable12.or_else(subtable4_fn)?;

fn is_proportional(
face: &Face,
unicode_subtable: Subtable,
code_point_iter: impl Iterator<Item = u32>,
) -> Option<bool> {
// Fonts like "Noto Sans Mono" have single, double, AND triple width glyphs.
// So we check proportionality up to 3x width, and assume non-proportionality
// once a forth non-zero advance value is encountered.
const MAX_ADVANCES: usize = 3;

let mut advances = Vec::with_capacity(MAX_ADVANCES);

for code_point in code_point_iter {
if let Some(glyph_id) = unicode_subtable.glyph_index(code_point) {
match face.glyph_hor_advance(glyph_id) {
Some(advance) if advance != 0 => match advances.binary_search(&advance) {
Err(_) if advances.len() == MAX_ADVANCES => return Some(false),
Err(i) => advances.insert(i, advance),
Ok(_) => (),
},
_ => (),
}
}
}

let mut advances = advances.into_iter();
let smallest = advances.next()?;
Some(advances.find(|advance| advance % smallest > 0).is_none())
}

match unicode_subtable.format {
Format::SegmentedCoverage(subtable12) => {
is_proportional(face, unicode_subtable, subtable12.codepoints_iter())
}
Format::SegmentMappingToDeltaValues(subtable4) => {
is_proportional(face, unicode_subtable, subtable4.codepoints_iter())
}
_ => unreachable!(),
}
}

pub fn new(db: &fontdb::Database, id: fontdb::ID, is_monospace: bool) -> Option<Self> {
let info = db.face(id)?;

let monospace_fallback = if cfg!(feature = "monospace_fallback") {
let monospace_fallback = if cfg!(feature = "monospace_fallback") && is_monospace {
db.with_face_data(id, |font_data, face_index| {
let face = ttf_parser::Face::parse(font_data, face_index).ok()?;
let monospace_em_width = info
.monospaced
.then(|| {
let monospace_em_width = {
|| {
let hor_advance = face.glyph_hor_advance(face.glyph_index(' ')?)? as f32;
let upem = face.units_per_em() as f32;
Some(hor_advance / upem)
})
.flatten();

if info.monospaced && monospace_em_width.is_none() {
None?;
}
}
}();

let scripts = face
.tables()
Expand Down
64 changes: 48 additions & 16 deletions src/font/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,25 @@ impl FontSystem {

/// Create a new [`FontSystem`] with a pre-specified locale and font database.
pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self {
let mut monospace_font_ids = db
.faces()
.filter(|face_info| {
face_info.monospaced && !face_info.post_script_name.contains("Emoji")
})
#[cfg(feature = "std")]
use rayon::iter::{IntoParallelIterator, ParallelIterator};

let faces = db.faces();
#[cfg(feature = "std")]
let faces = faces.collect::<Vec<_>>();
#[cfg(feature = "std")]
let faces = faces.into_par_iter();

let mono_filter_fn = |face_info: &&crate::fontdb::FaceInfo| {
let monospaced = face_info.monospaced;
let proportional_monospaced =
|| Self::proportional_monospaced(&db, face_info.id).unwrap_or(false);
(monospaced || proportional_monospaced())
&& !face_info.post_script_name.contains("Emoji")
};

let mut monospace_font_ids = faces
.filter(mono_filter_fn)
.map(|face_info| face_info.id)
.collect::<Vec<_>>();
monospace_font_ids.sort();
Expand Down Expand Up @@ -197,6 +211,21 @@ impl FontSystem {
ret
}

fn proportional_monospaced(db: &fontdb::Database, id: fontdb::ID) -> Option<bool> {
#[cfg(feature = "monospace_fallback")]
{
db.with_face_data(id, |font_data, face_index| {
let face = ttf_parser::Face::parse(font_data, face_index).ok()?;
Font::proportional_monospaced(&face)
})?
}
#[cfg(not(feature = "monospace_fallback"))]
{
let (_, _) = (db, id);
None
}
}

/// Get the locale.
pub fn locale(&self) -> &str {
&self.locale
Expand Down Expand Up @@ -244,16 +273,18 @@ impl FontSystem {
let fonts = ids.iter();

fonts
.map(|id| match Font::new(&self.db, *id) {
Some(font) => Some(Arc::new(font)),
None => {
log::warn!(
"failed to load font '{}'",
self.db.face(*id)?.post_script_name
);
None
}
})
.map(
|id| match Font::new(&self.db, *id, self.is_monospace(*id)) {
Some(font) => Some(Arc::new(font)),
None => {
log::warn!(
"failed to load font '{}'",
self.db.face(*id)?.post_script_name
);
None
}
},
)
.collect::<Vec<Option<Arc<Font>>>>()
.into_iter()
.flatten()
Expand All @@ -264,14 +295,15 @@ impl FontSystem {

/// Get a font by its ID.
pub fn get_font(&mut self, id: fontdb::ID) -> Option<Arc<Font>> {
let is_monospace = self.is_monospace(id);
self.font_cache
.entry(id)
.or_insert_with(|| {
#[cfg(feature = "std")]
unsafe {
self.db.make_shared_face_data(id);
}
match Font::new(&self.db, id) {
match Font::new(&self.db, id, is_monospace) {
Some(font) => Some(Arc::new(font)),
None => {
log::warn!(
Expand Down
53 changes: 25 additions & 28 deletions src/shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,34 +456,31 @@ fn shape_skip(
let ascent = metrics.ascent / f32::from(metrics.units_per_em);
let descent = metrics.descent / f32::from(metrics.units_per_em);

glyphs.extend(
line[start_run..end_run]
.char_indices()
.enumerate()
.map(|(i, (chr_idx, codepoint))| {
let glyph_id = charmap.map(codepoint);
let x_advance = glyph_metrics.advance_width(glyph_id);
let attrs = attrs_list.get_span(start_run + chr_idx);

ShapeGlyph {
start: i,
end: i + 1,
x_advance,
y_advance: 0.0,
x_offset: 0.0,
y_offset: 0.0,
ascent,
descent,
font_monospace_em_width,
font_id,
glyph_id,
color_opt: attrs.color_opt,
metadata: attrs.metadata,
cache_key_flags: attrs.cache_key_flags,
metrics_opt: attrs.metrics_opt.map(|x| x.into()),
}
}),
);
glyphs.extend(line[start_run..end_run].char_indices().enumerate().map(
|(i, (chr_idx, codepoint))| {
let glyph_id = charmap.map(codepoint);
let x_advance = glyph_metrics.advance_width(glyph_id);
let attrs = attrs_list.get_span(start_run + chr_idx);

ShapeGlyph {
start: i,
end: i + 1,
x_advance,
y_advance: 0.0,
x_offset: 0.0,
y_offset: 0.0,
ascent,
descent,
font_monospace_em_width,
font_id,
glyph_id,
color_opt: attrs.color_opt,
metadata: attrs.metadata,
cache_key_flags: attrs.cache_key_flags,
metrics_opt: attrs.metrics_opt.map(|x| x.into()),
}
},
));
}

/// A shaped glyph
Expand Down
Loading