mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 07:17:08 +00:00
Merge pull request #6677 from storytold/fix/sec-text-fallback-scans
Text: bounded font fallback file searches, COLR layer outlines, frame baseline search and glyph outline size
This commit is contained in:
+13
-4
@@ -169,6 +169,17 @@ impl<'a> Tree<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Glyph `gid`'s outline in the current transform; empty once the glyph is over budget, so
|
||||
/// the layers that follow cost nothing (a compact layer list can name one big outline often).
|
||||
fn layer_outline(&mut self, gid: GlyphId) -> BezPath {
|
||||
if self.too_large {
|
||||
return BezPath::new();
|
||||
}
|
||||
let mut path = outline(self.font, self.location, gid);
|
||||
path.apply_affine(self.transform());
|
||||
path
|
||||
}
|
||||
|
||||
fn transform(&self) -> Affine {
|
||||
self.transforms.last().copied().unwrap_or(FLIP)
|
||||
}
|
||||
@@ -359,8 +370,7 @@ impl ColorPainter for Tree<'_> {
|
||||
}
|
||||
|
||||
fn push_clip_glyph(&mut self, glyph_id: GlyphId) {
|
||||
let mut clip = outline(self.font, self.location, glyph_id);
|
||||
clip.apply_affine(self.transform());
|
||||
let clip = self.layer_outline(glyph_id);
|
||||
self.charge(&clip);
|
||||
self.open(Open { clip: Some(clip), blend: None, children: vec![], flooded: false });
|
||||
}
|
||||
@@ -392,8 +402,7 @@ impl ColorPainter for Tree<'_> {
|
||||
|
||||
/// The glyph's outline filled, the brush moved by `brush_transform` (the glyph stays).
|
||||
fn fill_glyph(&mut self, glyph_id: GlyphId, brush_transform: Option<Transform>, brush: Brush<'_>) {
|
||||
let mut path = outline(self.font, self.location, glyph_id);
|
||||
path.apply_affine(self.transform());
|
||||
let path = self.layer_outline(glyph_id);
|
||||
let brush_xf = self.transform() * brush_transform.as_ref().map_or(Affine::IDENTITY, affine);
|
||||
self.transforms.push(brush_xf);
|
||||
self.fill_path(&path, &brush);
|
||||
|
||||
@@ -192,6 +192,19 @@ mod alias_tests {
|
||||
font
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn the_file_search_for_uncovered_characters_stops_at_its_budget() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
// The first search scans the font folders, which starts the count again.
|
||||
db.system_fallback('\u{E001}');
|
||||
db.sys.lock().unwrap().file_searches = names::MAX_FILE_SEARCHES;
|
||||
assert!(!db.system_fallback('\u{E000}'));
|
||||
let sys = db.sys.lock().unwrap();
|
||||
assert_eq!(sys.file_searches, names::MAX_FILE_SEARCHES);
|
||||
assert!(!sys.misses.contains(&'\u{E000}'), "an unsearched character is not a remembered miss");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_collection_lists_at_most_max_faces() {
|
||||
let mut font = name_font(&[(3, 0x0409, 1, "Fam"), (3, 0x0409, 2, "Regular")]);
|
||||
|
||||
@@ -456,6 +456,7 @@ impl FontDb {
|
||||
let mut sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
|
||||
sys.scans = sys.scans.wrapping_add(1);
|
||||
sys.misses.clear();
|
||||
sys.file_searches = 0;
|
||||
}
|
||||
self.changed();
|
||||
n
|
||||
|
||||
@@ -7,23 +7,63 @@ pub(super) fn svg_drawn(face: &FontFace, font: &skrifa::FontRef<'_>, gid: u32) -
|
||||
face.svg_glyphs && !crate::colr::has_colour_glyph(font, gid)
|
||||
}
|
||||
|
||||
/// The most path elements one glyph outline keeps. Real outlines have hundreds; a variable
|
||||
/// composite glyph can repeat a big base outline into millions. Elements past it are dropped.
|
||||
pub(crate) const MAX_OUTLINE_ELEMENTS: usize = 1 << 20;
|
||||
|
||||
/// A pen drawing outlines y down, as text space has them.
|
||||
pub(crate) struct FlipPen(pub(crate) BezPath);
|
||||
|
||||
impl FlipPen {
|
||||
fn full(&self) -> bool {
|
||||
self.0.elements().len() >= MAX_OUTLINE_ELEMENTS
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlinePen for FlipPen {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
if self.full() {
|
||||
return;
|
||||
}
|
||||
self.0.move_to((x as f64, -y as f64));
|
||||
}
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
if self.full() {
|
||||
return;
|
||||
}
|
||||
self.0.line_to((x as f64, -y as f64));
|
||||
}
|
||||
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
|
||||
if self.full() {
|
||||
return;
|
||||
}
|
||||
self.0.quad_to((cx0 as f64, -cy0 as f64), (x as f64, -y as f64));
|
||||
}
|
||||
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
|
||||
if self.full() {
|
||||
return;
|
||||
}
|
||||
self.0.curve_to((cx0 as f64, -cy0 as f64), (cx1 as f64, -cy1 as f64), (x as f64, -y as f64));
|
||||
}
|
||||
fn close(&mut self) {
|
||||
if self.full() {
|
||||
return;
|
||||
}
|
||||
self.0.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_pen_keeps_at_most_the_element_cap() {
|
||||
let mut pen = FlipPen(BezPath::new());
|
||||
pen.move_to(0.0, 0.0);
|
||||
for i in 0..MAX_OUTLINE_ELEMENTS + 10 {
|
||||
pen.line_to(i as f32, 0.0);
|
||||
}
|
||||
assert_eq!(pen.0.elements().len(), MAX_OUTLINE_ELEMENTS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ impl FontDb {
|
||||
/// Load a system font covering `c` (preferred fallback families first, then any cataloged
|
||||
/// file under 40 MB). Returns true if one was loaded. Misses are remembered.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn system_fallback(&self, c: char) -> bool {
|
||||
pub(super) fn system_fallback(&self, c: char) -> bool {
|
||||
if c.is_control() || c.is_whitespace() {
|
||||
return false;
|
||||
}
|
||||
@@ -501,6 +501,13 @@ impl FontDb {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut sys = self.sys.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if sys.file_searches >= super::names::MAX_FILE_SEARCHES {
|
||||
return false;
|
||||
}
|
||||
sys.file_searches += 1;
|
||||
}
|
||||
let mut paths: Vec<PathBuf> = self.read_catalog().values().flat_map(|c| c.faces.iter().map(|cf| cf.path.clone())).collect();
|
||||
paths.sort();
|
||||
paths.dedup();
|
||||
|
||||
@@ -7,8 +7,16 @@ pub(super) struct SysFallback {
|
||||
pub(super) misses: std::collections::HashSet<char>,
|
||||
/// Counts the scans of the font folders: a search that a scan overtook doesn't record its miss.
|
||||
pub(super) scans: u64,
|
||||
/// Characters that went on to read every cataloged font file (and found nothing, or a face).
|
||||
/// Each such search rereads the catalog, so only [`MAX_FILE_SEARCHES`] are made per scan.
|
||||
pub(super) file_searches: u32,
|
||||
}
|
||||
|
||||
/// The most characters searched for in the font files themselves between two scans of the font
|
||||
/// folders. Real text has a few uncovered characters; a document of thousands of distinct
|
||||
/// private-use code points would reread the whole catalog for each.
|
||||
pub(super) const MAX_FILE_SEARCHES: u32 = 1024;
|
||||
|
||||
/// Fonts made for Thai, tried first (when installed) for Thai the font in use lacks: Windows
|
||||
/// (Leelawadee UI; Tahoma before Windows 8), macOS (Thonburi, Sukhumvit Set) and Linux (Noto,
|
||||
/// TLWG).
|
||||
|
||||
@@ -63,12 +63,19 @@ pub(super) struct Pen<'r> {
|
||||
pub(super) next_top: Option<f64>,
|
||||
/// The narrowest word no line from here down is wide enough for (see [`Pen::place`]).
|
||||
pub(super) no_room_for: f64,
|
||||
/// Baseline steps still allowed over all the frame's cells and placements (shared by this pen's
|
||||
/// copies): see [`MAX_TOTAL_STEPS`].
|
||||
pub(super) steps_left: std::rc::Rc<std::cell::Cell<usize>>,
|
||||
}
|
||||
|
||||
/// The largest number of steps layout takes to move a line down a frame cell to find room for it:
|
||||
/// in a cell taller than this many leads, a step is the cell's height divided by this.
|
||||
const MAX_STEPS: usize = 100_000;
|
||||
|
||||
/// The most baseline steps one frame's layout takes in all, over every cell: a cell may take
|
||||
/// [`MAX_STEPS`], but rows and columns of tall, narrow-shaped cells would multiply that.
|
||||
pub(super) const MAX_TOTAL_STEPS: usize = 10_000_000;
|
||||
|
||||
impl Pen<'_> {
|
||||
/// Baseline and horizontal span for a line with estimated metrics `est` and indents that starts
|
||||
/// with a word `word` wide; `None` = the frame is full (overflow).
|
||||
@@ -110,6 +117,12 @@ impl Pen<'_> {
|
||||
// height when that is more, in at most `MAX_STEPS` steps.
|
||||
let step = est.lead.max(1.0).max(r.cell.height() / MAX_STEPS as f64);
|
||||
for _ in 0..=MAX_STEPS {
|
||||
let left = self.steps_left.get();
|
||||
if left == 0 {
|
||||
// Out of steps: the frame is full as far as this text goes.
|
||||
return None;
|
||||
}
|
||||
self.steps_left.set(left - 1);
|
||||
// A baseline a step no longer moves (past a gutter of 1e300, say) won't reach a line.
|
||||
if baseline + est.desc > r.bottom() + 0.01 || baseline + step <= baseline || baseline.is_nan() {
|
||||
break;
|
||||
@@ -732,3 +745,26 @@ pub(super) fn candidates(text: &str, g: &[SGlyph], hyphenate: Option<&Hyphenatio
|
||||
v.dedup_by_key(|b| b.end);
|
||||
v
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn a_pen_out_of_baseline_steps_finds_no_room() {
|
||||
let frame = kurbo::Shape::to_path(&kurbo::Rect::new(0.0, 0.0, 400.0, 400.0), 0.1);
|
||||
let regions = Region::cells(&frame, &super::LayoutOptions::default(), &[]);
|
||||
let est = Metrics { asc: 10.0, desc: 3.0, lead: 14.0, cap: 7.0, xh: 5.0, top: 10.0 };
|
||||
let pen = |steps| Pen {
|
||||
regions: Some(®ions),
|
||||
ri: 0,
|
||||
prev: None,
|
||||
pending: 0.0,
|
||||
fb: FirstBaseline::Ascent,
|
||||
fb_min: 0.0,
|
||||
queued: vec![],
|
||||
model: LeadingModel::default(),
|
||||
next_top: None,
|
||||
no_room_for: f64::INFINITY,
|
||||
steps_left: std::rc::Rc::new(std::cell::Cell::new(steps)),
|
||||
};
|
||||
assert!(pen(MAX_TOTAL_STEPS).place(est, 0.0, 0.0, 0.0).is_some());
|
||||
assert!(pen(0).place(est, 0.0, 0.0, 0.0).is_none());
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ pub(super) fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, reg
|
||||
model: t.para_at(0).leading_model,
|
||||
next_top: None,
|
||||
no_room_for: f64::INFINITY,
|
||||
steps_left: std::rc::Rc::new(std::cell::Cell::new(super::breaking::MAX_TOTAL_STEPS)),
|
||||
};
|
||||
let markers = list_markers(t, paras);
|
||||
for (pi, pr) in paras.iter().enumerate() {
|
||||
|
||||
Reference in New Issue
Block a user