mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 08:51:40 +00:00
Text: run lookup by bisection, bounded path flattening, font folders and files shared
run_at scanned the style runs from the first for every character; the ordered runs are now bisected (the scan remains for gaps). Path text flattened at a fixed 0.05 tolerance, so huge coordinates made hundreds of millions of points; the tolerance grows with the path's size and non-finite paths measure nothing. Font name records aliasing one string are decoded once. The font folder walk listed folders reached through several links again along every path; each real folder is listed once. Faces of one font collection each read the whole file; they share it.
This commit is contained in:
@@ -107,14 +107,19 @@ fn extra_dirs(var: Option<&std::ffi::OsStr>) -> Vec<PathBuf> {
|
||||
var.map(|v| std::env::split_paths(v).filter(|p| !p.as_os_str().is_empty()).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn walk(dir: &std::path::Path, depth: u32, out: &mut Vec<PathBuf>) {
|
||||
fn walk(dir: &std::path::Path, depth: u32, out: &mut Vec<PathBuf>, seen: &mut std::collections::HashSet<PathBuf>) {
|
||||
// Each real folder is listed once: symlinks fanning out to the same folders would otherwise
|
||||
// be walked again along every path to them (exponentially many within the depth bound).
|
||||
if !seen.insert(std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf())) {
|
||||
return;
|
||||
}
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
// Bounded, so a symlink loop ends.
|
||||
if depth < 8 {
|
||||
walk(&p, depth + 1, out);
|
||||
walk(&p, depth + 1, out, seen);
|
||||
}
|
||||
} else if p.extension().and_then(std::ffi::OsStr::to_str).is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc")) {
|
||||
out.push(p);
|
||||
@@ -125,8 +130,9 @@ fn walk(dir: &std::path::Path, depth: u32, out: &mut Vec<PathBuf>) {
|
||||
impl FontSource for DirectorySource {
|
||||
fn faces(&self) -> Vec<FaceInfo> {
|
||||
let mut files = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for d in &self.dirs {
|
||||
walk(d, 0, &mut files);
|
||||
walk(d, 0, &mut files, &mut seen);
|
||||
}
|
||||
files.sort();
|
||||
files.dedup();
|
||||
@@ -194,13 +200,28 @@ pub struct VMetrics {
|
||||
pub underline_thickness: f32,
|
||||
}
|
||||
|
||||
/// A font file's bytes, shared by every face of it that is loaded (the faces of a collection
|
||||
/// each held their own copy of the whole file).
|
||||
fn file_bytes(p: &std::path::Path) -> Option<Arc<Vec<u8>>> {
|
||||
static FILES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<PathBuf, std::sync::Weak<Vec<u8>>>>> =
|
||||
std::sync::LazyLock::new(Default::default);
|
||||
let mut files = FILES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(b) = files.get(p).and_then(std::sync::Weak::upgrade) {
|
||||
return Some(b);
|
||||
}
|
||||
let b = Arc::new(std::fs::read(p).ok()?);
|
||||
files.retain(|_, w| w.strong_count() > 0);
|
||||
files.insert(p.to_path_buf(), Arc::downgrade(&b));
|
||||
Some(b)
|
||||
}
|
||||
|
||||
impl Face {
|
||||
fn bytes(&self) -> Option<&[u8]> {
|
||||
self.bytes
|
||||
.get_or_init(|| match &self.info.data {
|
||||
FaceData::Static(b) => Some(FaceBytes::Static(b)),
|
||||
FaceData::Shared(b) => Some(FaceBytes::Shared(b.clone())),
|
||||
FaceData::File(p) => std::fs::read(p).ok().map(|v| FaceBytes::Shared(Arc::new(v))),
|
||||
FaceData::File(p) => file_bytes(p).map(FaceBytes::Shared),
|
||||
})
|
||||
.as_ref()
|
||||
.map(FaceBytes::get)
|
||||
|
||||
@@ -567,3 +567,22 @@ fn unknown_face_id_returns_bundled_default() {
|
||||
assert!(fallback.font().is_some());
|
||||
assert!(Arc::ptr_eq(&fallback, &face(0)));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_folder_reached_through_many_links_is_listed_once() {
|
||||
// Six links back to the folder itself: 6^8 listings within the depth bound before.
|
||||
let d = std::env::temp_dir().join(format!("effectcraft-font-walk-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
std::fs::write(d.join("a.ttf"), b"x").unwrap();
|
||||
for i in 0..6 {
|
||||
std::os::unix::fs::symlink(&d, d.join(format!("l{i}"))).unwrap();
|
||||
}
|
||||
let (mut out, mut seen) = (Vec::new(), std::collections::HashSet::new());
|
||||
let start = std::time::Instant::now();
|
||||
walk(&d, 0, &mut out, &mut seen);
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(5));
|
||||
assert_eq!(out.len(), 1);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
@@ -624,6 +624,13 @@ fn is_hanging_open(c: char) -> bool {
|
||||
/// The style run index covering global byte `b` (the run before it at a boundary, for empty
|
||||
/// paragraphs at the end).
|
||||
fn run_at(runs: &[(Range<usize>, TextStyle)], b: usize) -> usize {
|
||||
// Runs from a document are ordered and contiguous: the last one starting at or before `b`
|
||||
// holds it, found by bisection (a scan from the first run per character was quadratic).
|
||||
// Anything else (a gap, unordered runs) takes the scan as before.
|
||||
let i = runs.partition_point(|(r, _)| r.start <= b).saturating_sub(1);
|
||||
if runs.get(i).is_some_and(|(r, _)| r.contains(&b)) {
|
||||
return i;
|
||||
}
|
||||
runs.iter().position(|(r, _)| b >= r.start && b < r.end).unwrap_or_else(|| runs.iter().rposition(|(r, _)| r.start <= b).unwrap_or(0))
|
||||
}
|
||||
|
||||
|
||||
@@ -494,3 +494,17 @@ fn a_later_vertical_run_keeps_the_first_line_baseline() {
|
||||
let mixed = layout_rich_uncached(text, &[(0..4, horiz), (4..text.len(), vert)], &[p]);
|
||||
assert!((mixed.lines[0].baseline - plain.lines[0].baseline).abs() < 1e-3, "{} vs {}", mixed.lines[0].baseline, plain.lines[0].baseline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_lookup_by_bisection_matches_the_scan() {
|
||||
let scan = |runs: &[(Range<usize>, TextStyle)], b: usize| {
|
||||
runs.iter().position(|(r, _)| b >= r.start && b < r.end).unwrap_or_else(|| runs.iter().rposition(|(r, _)| r.start <= b).unwrap_or(0))
|
||||
};
|
||||
let runs: Vec<(Range<usize>, TextStyle)> = (0..50).map(|i| (i * 3..i * 3 + 3, st(10.0))).collect();
|
||||
let gapped: Vec<(Range<usize>, TextStyle)> = (0..50).map(|i| (i * 4..i * 4 + 3, st(10.0))).collect();
|
||||
for b in 0..220 {
|
||||
assert_eq!(run_at(&runs, b), scan(&runs, b), "{b}");
|
||||
assert_eq!(run_at(&gapped, b), scan(&gapped, b), "{b}");
|
||||
}
|
||||
assert_eq!(run_at(&[], 5), 0);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,14 @@ impl PathMeasure {
|
||||
let mut closed = false;
|
||||
let mut started = false;
|
||||
let mut done = false;
|
||||
kurbo::flatten(path.iter(), 0.05, |el| {
|
||||
// The tolerance grows with the path's size (0.05 for ordinary layer-sized paths): a
|
||||
// path with huge coordinates would otherwise flatten into hundreds of millions of points.
|
||||
let bbox = kurbo::Shape::bounding_box(path);
|
||||
let size = bbox.width().max(bbox.height());
|
||||
if !size.is_finite() {
|
||||
done = true;
|
||||
}
|
||||
kurbo::flatten(path.iter(), 0.05_f64.max(size * 1e-6), |el| {
|
||||
if done {
|
||||
return;
|
||||
}
|
||||
@@ -128,6 +135,21 @@ pub fn arc_positions(xs: &[f64], line: [f64; 2], align: u8, length: f64, first_m
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_huge_curve_flattens_into_a_bounded_number_of_points() {
|
||||
let mut p = BezPath::new();
|
||||
p.move_to((0.0, 0.0));
|
||||
p.curve_to((1e15, 0.0), (1e15, 1e15), (0.0, 1e15));
|
||||
let m = PathMeasure::new(&p, false);
|
||||
assert!(m.pts.len() < 100_000, "{}", m.pts.len());
|
||||
assert!(m.length() > 1e15);
|
||||
// Non-finite coordinates measure nothing.
|
||||
let mut q = BezPath::new();
|
||||
q.move_to((0.0, 0.0));
|
||||
q.curve_to((f64::INFINITY, 0.0), (1.0, 1.0), (2.0, 0.0));
|
||||
assert_eq!(PathMeasure::new(&q, false).pts.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measures_a_line_and_a_circle() {
|
||||
let mut p = BezPath::new();
|
||||
|
||||
@@ -257,7 +257,12 @@ fn name_string(b: &[u8], id: u16) -> Option<String> {
|
||||
// Best rank first; a blank record falls through to the next. Decoding is lazy, so aliased
|
||||
// records are not decoded once a usable one is found.
|
||||
cands.sort_by_key(|c| c.0);
|
||||
// Records may alias one string: each (encoding, bytes) is decoded at most once.
|
||||
let mut tried = std::collections::HashSet::new();
|
||||
cands.into_iter().find_map(|(_, plat, enc, raw)| {
|
||||
if !tried.insert((plat, enc, raw.as_ptr(), raw.len())) {
|
||||
return None;
|
||||
}
|
||||
let s = match (plat, enc) {
|
||||
(3, 1 | 10) | (0, _) => {
|
||||
let u: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
|
||||
|
||||
Reference in New Issue
Block a user