mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 23:32:17 +00:00
Text: run lookup by bisection, bounded path flattening, font folders and files shared (#2991)
* 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. * address review: font walk breadth-first across roots, font file cache keyed by size and mtime and read outside the lock, first-subpath path measure with finite check, scratch dir guard * address review: font files shared through file_bytes with the open file's metadata; run lookup by bisection only for ordered runs * Pass the runs' ordered flag with them to stay under the argument limit * address review: reversed run ranges take the scan
This commit is contained in:
+55
-26
@@ -107,17 +107,29 @@ 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>) {
|
||||
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);
|
||||
/// Font files under `roots`, each real folder listed once. Breadth first from every root, so a
|
||||
/// folder reached by several routes is listed at its shallowest and keeps the most depth below it.
|
||||
fn walk(roots: &[PathBuf], out: &mut Vec<PathBuf>) {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut queue: std::collections::VecDeque<(PathBuf, u32)> = roots.iter().map(|r| (r.clone(), 0)).collect();
|
||||
while let Some((dir, depth)) = queue.pop_front() {
|
||||
// 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.clone())) {
|
||||
continue;
|
||||
}
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else { continue };
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
// Bounded, so a symlink loop ends.
|
||||
if depth < 8 {
|
||||
queue.push_back((p, depth + 1));
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
} 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,9 +137,7 @@ 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();
|
||||
for d in &self.dirs {
|
||||
walk(d, 0, &mut files);
|
||||
}
|
||||
walk(&self.dirs, &mut files);
|
||||
files.sort();
|
||||
files.dedup();
|
||||
let mut out = Vec::new();
|
||||
@@ -158,17 +168,6 @@ fn info_from(n: FaceNames, data: FaceData, origin: &'static str) -> FaceInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// A font file's bytes and its metadata from the same open file (so the metadata describes the
|
||||
/// bytes, not a later version of the file).
|
||||
fn read_with_metadata(path: &std::path::Path) -> std::io::Result<(Vec<u8>, std::fs::Metadata)> {
|
||||
use std::io::Read;
|
||||
let mut f = std::fs::File::open(path)?;
|
||||
let meta = f.metadata()?;
|
||||
let mut bytes = Vec::new();
|
||||
f.read_to_end(&mut bytes)?;
|
||||
Ok((bytes, meta))
|
||||
}
|
||||
|
||||
/// A loaded (or loadable) face.
|
||||
pub struct Face {
|
||||
pub id: FaceId,
|
||||
@@ -207,6 +206,36 @@ 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), with the metadata of the open file they were read
|
||||
/// from (so it describes the bytes, not a later version of the file).
|
||||
fn file_bytes(p: &std::path::Path) -> std::io::Result<(Arc<Vec<u8>>, std::fs::Metadata)> {
|
||||
use std::io::Read;
|
||||
type Key = (PathBuf, u64, Option<std::time::SystemTime>);
|
||||
type Entry = (std::sync::Weak<Vec<u8>>, std::fs::Metadata);
|
||||
static FILES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<Key, Entry>>> = std::sync::LazyLock::new(Default::default);
|
||||
let lock = || FILES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let hit = |files: &std::collections::HashMap<Key, Entry>, key: &Key| files.get(key).and_then(|(w, m)| Some((w.upgrade()?, m.clone())));
|
||||
let mut f = std::fs::File::open(p)?;
|
||||
// Keyed by size and modification time too: a file replaced at the same path is read afresh.
|
||||
let m = f.metadata()?;
|
||||
let key: Key = (p.to_path_buf(), m.len(), m.modified().ok());
|
||||
if let Some(found) = hit(&lock(), &key) {
|
||||
return Ok(found);
|
||||
}
|
||||
// Read outside the lock: a slow file must not hold up every other face's first load.
|
||||
let mut bytes = Vec::new();
|
||||
f.read_to_end(&mut bytes)?;
|
||||
let b = Arc::new(bytes);
|
||||
let mut files = lock();
|
||||
if let Some(found) = hit(&files, &key) {
|
||||
return Ok(found);
|
||||
}
|
||||
files.retain(|_, (w, _)| w.strong_count() > 0);
|
||||
files.insert(key, (Arc::downgrade(&b), m.clone()));
|
||||
Ok((b, m))
|
||||
}
|
||||
|
||||
impl Face {
|
||||
/// The font file bytes, loaded once on first use, or `None` when they cannot be read.
|
||||
fn bytes(&self) -> Option<&[u8]> {
|
||||
@@ -215,9 +244,9 @@ impl Face {
|
||||
FaceData::Static(b) => Some(FaceBytes::Static(b)),
|
||||
FaceData::Shared(b) => Some(FaceBytes::Shared(b.clone())),
|
||||
FaceData::File(p) => {
|
||||
let (bytes, meta) = read_with_metadata(p).ok()?;
|
||||
let (bytes, meta) = file_bytes(p).ok()?;
|
||||
let _ = self.file_meta.set(meta);
|
||||
Some(FaceBytes::Shared(Arc::new(bytes)))
|
||||
Some(FaceBytes::Shared(bytes))
|
||||
}
|
||||
})
|
||||
.as_ref()
|
||||
|
||||
@@ -567,3 +567,72 @@ fn unknown_face_id_returns_bundled_default() {
|
||||
assert!(fallback.font().is_some());
|
||||
assert!(Arc::ptr_eq(&fallback, &face(0)));
|
||||
}
|
||||
|
||||
/// A scratch folder this test created itself (never one that already exists), removed on drop.
|
||||
struct Scratch(std::path::PathBuf);
|
||||
impl Scratch {
|
||||
fn new(tag: &str) -> Self {
|
||||
let d = (0u32..)
|
||||
.map(|n| std::env::temp_dir().join(format!("effectcraft-{tag}-{}-{n}", std::process::id())))
|
||||
.find_map(|d| match std::fs::create_dir(&d) {
|
||||
Ok(()) => Some(d),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => None,
|
||||
Err(e) => panic!("create scratch dir {}: {e}", d.display()),
|
||||
})
|
||||
.unwrap();
|
||||
Scratch(d)
|
||||
}
|
||||
}
|
||||
impl Drop for Scratch {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.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 scratch = Scratch::new("font-walk");
|
||||
let d = &scratch.0;
|
||||
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 = Vec::new();
|
||||
let start = std::time::Instant::now();
|
||||
walk(std::slice::from_ref(d), &mut out);
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(5));
|
||||
assert_eq!(out.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_folder_inside_another_root_keeps_its_own_depth() {
|
||||
// root/a/b/c/d/e/f/g/h is also a root: the walk of `root` reaches it at depth 8 and could not
|
||||
// go below it, so it must be listed from the shallower visit.
|
||||
let scratch = Scratch::new("font-depth");
|
||||
let root = &scratch.0;
|
||||
let deep = root.join("a/b/c/d/e/f/g/h");
|
||||
std::fs::create_dir_all(deep.join("sub")).unwrap();
|
||||
std::fs::write(deep.join("sub/x.ttf"), b"x").unwrap();
|
||||
let mut out = Vec::new();
|
||||
walk(&[root.clone(), deep.clone()], &mut out);
|
||||
assert_eq!(out, vec![deep.join("sub/x.ttf")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_font_file_replaced_at_the_same_path_is_read_again() {
|
||||
let scratch = Scratch::new("font-bytes");
|
||||
let p = scratch.0.join("a.ttf");
|
||||
std::fs::write(&p, b"old").unwrap();
|
||||
let (old, old_meta) = file_bytes(&p).unwrap();
|
||||
// Faces of one file share its bytes, and the metadata is that of the file the bytes came from.
|
||||
let (again, _) = file_bytes(&p).unwrap();
|
||||
assert!(Arc::ptr_eq(&old, &again));
|
||||
assert_eq!(old_meta.len(), 3);
|
||||
std::fs::write(&p, b"new, longer").unwrap();
|
||||
let (new, new_meta) = file_bytes(&p).unwrap();
|
||||
assert_eq!(new.as_slice(), b"new, longer");
|
||||
assert_eq!(new_meta.len(), 11);
|
||||
assert_eq!(old.as_slice(), b"old");
|
||||
}
|
||||
|
||||
+37
-21
@@ -628,9 +628,31 @@ fn is_hanging_open(c: char) -> bool {
|
||||
matches!(c, '"' | '\'' | '\u{2018}' | '\u{201c}' | '(' | '\u{ab}')
|
||||
}
|
||||
|
||||
/// Whether no run is reversed and each starts at or after the end of the one before it (the runs
|
||||
/// of a document): the precondition of [`run_at`]'s bisection.
|
||||
fn runs_ordered(runs: &[(Range<usize>, TextStyle)]) -> bool {
|
||||
runs.iter().all(|(r, _)| r.start <= r.end) && runs.windows(2).all(|w| w[0].0.end <= w[1].0.start)
|
||||
}
|
||||
|
||||
/// Style runs with whether they are [`runs_ordered`].
|
||||
#[derive(Clone, Copy)]
|
||||
struct Runs<'a> {
|
||||
list: &'a [(Range<usize>, TextStyle)],
|
||||
ordered: 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 {
|
||||
/// paragraphs at the end). `ordered` is [`runs_ordered`] of `runs`.
|
||||
fn run_at(runs: &[(Range<usize>, TextStyle)], ordered: bool, b: usize) -> usize {
|
||||
// Ordered, non-overlapping runs: the last one starting at or before `b` is the only one that
|
||||
// can hold it, found by bisection (a scan from the first run per character was quadratic).
|
||||
// Anything else (a gap, overlapping or unordered runs) takes the scan, whose first match wins.
|
||||
if ordered {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -706,7 +728,7 @@ impl<'a> Para<'a> {
|
||||
fn new(
|
||||
text: &'a str,
|
||||
base: usize,
|
||||
runs: &'a [(Range<usize>, TextStyle)],
|
||||
runs: Runs<'a>,
|
||||
primaries: &'a [Resolved],
|
||||
variation_ids: &'a [u32],
|
||||
para: &'a ParagraphStyle,
|
||||
@@ -716,7 +738,7 @@ impl<'a> Para<'a> {
|
||||
let bidi = BidiInfo::new(text, default_level);
|
||||
let base_rtl = bidi.paragraphs[0].level.is_rtl();
|
||||
let chars = char_forms(text, base, runs, primaries, &bidi);
|
||||
let items = build_items(text, &chars, runs);
|
||||
let items = build_items(text, &chars, runs.list);
|
||||
// Prefix widths and the preceding non-whitespace boundary make candidate line widths O(1).
|
||||
// Every-line composition evaluates O(n²) candidate breaks; summing each candidate's bytes
|
||||
// or rescanning its trailing whitespace would otherwise add another factor of text length.
|
||||
@@ -739,7 +761,7 @@ impl<'a> Para<'a> {
|
||||
}
|
||||
previous_non_ws[b + c.len_utf8()] = non_ws_end;
|
||||
}
|
||||
Para { text, base, runs, primaries, variation_ids, para, width, bidi, base_rtl, chars, items, char_adv, prefix_adv, previous_non_ws }
|
||||
Para { text, base, runs: runs.list, primaries, variation_ids, para, width, bidi, base_rtl, chars, items, char_adv, prefix_adv, previous_non_ws }
|
||||
}
|
||||
|
||||
fn range_w(&self, r: Range<usize>) -> f32 {
|
||||
@@ -1021,11 +1043,11 @@ impl<'a> Para<'a> {
|
||||
}
|
||||
|
||||
/// Each char with its shaped form, face, direction and run.
|
||||
fn char_forms(text: &str, base: usize, runs: &[(Range<usize>, TextStyle)], primaries: &[Resolved], bidi: &BidiInfo) -> Vec<CharInfo> {
|
||||
fn char_forms(text: &str, base: usize, runs: Runs, primaries: &[Resolved], bidi: &BidiInfo) -> Vec<CharInfo> {
|
||||
text.char_indices()
|
||||
.map(|(b, c)| {
|
||||
let si = run_at(runs, base + b);
|
||||
let style = &runs[si].1;
|
||||
let si = run_at(runs.list, runs.ordered, base + b);
|
||||
let style = &runs.list[si].1;
|
||||
let primary = primaries[si].face;
|
||||
let face_of = |ch: char| if ch.is_whitespace() { primary } else { fonts::fallback_for(ch, primary) };
|
||||
let all_small = style.opentype.all_small_caps && style.caps != Caps::All;
|
||||
@@ -1079,17 +1101,9 @@ fn build_items(text: &str, chars: &[CharInfo], runs: &[(Range<usize>, TextStyle)
|
||||
items
|
||||
}
|
||||
|
||||
fn paragraph(
|
||||
text: &str,
|
||||
base: usize,
|
||||
runs: &[(Range<usize>, TextStyle)],
|
||||
primaries: &[Resolved],
|
||||
variation_ids: &[u32],
|
||||
para: &ParagraphStyle,
|
||||
width: Option<f32>,
|
||||
) -> Vec<ParaLine> {
|
||||
fn paragraph(text: &str, base: usize, runs: Runs, primaries: &[Resolved], variation_ids: &[u32], para: &ParagraphStyle, width: Option<f32>) -> Vec<ParaLine> {
|
||||
if text.is_empty() {
|
||||
let si = run_at(runs, base);
|
||||
let (runs, si) = (runs.list, run_at(runs.list, runs.ordered, base));
|
||||
let pm = style_metrics(primaries, runs, si);
|
||||
// No text to resolve a direction from: the explicit one.
|
||||
let rtl = para.rtl == Some(true);
|
||||
@@ -1167,18 +1181,20 @@ pub fn layout_rich_uncached(text: &str, runs: &[(Range<usize>, TextStyle)], para
|
||||
});
|
||||
let runs = cleared.as_deref().unwrap_or(runs);
|
||||
let variation_ids: Vec<u32> = runs.iter().map(|(_, s)| crate::variable::intern(&s.variations)).collect();
|
||||
let ordered = runs_ordered(runs);
|
||||
let mut lay = Layout { missing_font: primaries.iter().any(|p| p.missing), text_len: text.len(), ..Default::default() };
|
||||
let mut plines: Vec<(usize, ParaLine)> = Vec::new();
|
||||
for (pi, span) in paragraph_ranges(text).enumerate() {
|
||||
let p = text.get(span.bytes.clone()).unwrap_or_default();
|
||||
plines.extend(paragraph(p, span.bytes.start, runs, &primaries, &variation_ids, para_at(pi), width).into_iter().map(|l| (pi, l)));
|
||||
plines
|
||||
.extend(paragraph(p, span.bytes.start, Runs { list: runs, ordered }, &primaries, &variation_ids, para_at(pi), width).into_iter().map(|l| (pi, l)));
|
||||
}
|
||||
let mut baseline = 0.0f32;
|
||||
for (i, (pi, pl)) in plines.into_iter().enumerate() {
|
||||
let para = para_at(pi);
|
||||
if i == 0 {
|
||||
// Vertical type keeps its first column at the font ascent (not measured in AE).
|
||||
let vertical = runs.get(run_at(runs, pl.range.start)).is_some_and(|r| r.1.vertical)
|
||||
let vertical = runs.get(run_at(runs, ordered, pl.range.start)).is_some_and(|r| r.1.vertical)
|
||||
|| runs.iter().any(|r| r.1.vertical && r.0.start < pl.range.end && r.0.end > pl.range.start);
|
||||
baseline = if width.is_some() { if vertical { pl.ascent } else { pl.first } } else { 0.0 };
|
||||
} else {
|
||||
@@ -1198,7 +1214,7 @@ pub fn layout_rich_uncached(text: &str, runs: &[(Range<usize>, TextStyle)], para
|
||||
}));
|
||||
let x = shift + pl.left_trim;
|
||||
if let Some((u0, u1)) = pl.underline {
|
||||
let si = run_at(runs, pl.range.start);
|
||||
let si = run_at(runs, ordered, pl.range.start);
|
||||
let pm = fonts::face(primaries[si].face).metrics(runs[si].1.size);
|
||||
lay.underlines.push([u0 + shift, baseline + pm.underline_pos, u1 + shift, baseline + pm.underline_pos + pm.underline_thickness]);
|
||||
}
|
||||
|
||||
@@ -505,3 +505,29 @@ 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();
|
||||
// Overlapping runs: the first run holding a byte wins, as in the scan.
|
||||
let overlapping: Vec<(Range<usize>, TextStyle)> = vec![(0..10, st(10.0)), (5..15, st(20.0)), (12..20, st(30.0))];
|
||||
assert!(runs_ordered(&runs) && runs_ordered(&gapped) && !runs_ordered(&overlapping));
|
||||
for b in 0..220 {
|
||||
assert_eq!(run_at(&runs, true, b), scan(&runs, b), "{b}");
|
||||
assert_eq!(run_at(&gapped, true, b), scan(&gapped, b), "{b}");
|
||||
assert_eq!(run_at(&overlapping, false, b), scan(&overlapping, b), "{b}");
|
||||
}
|
||||
assert_eq!(run_at(&overlapping, false, 6), 0);
|
||||
// A reversed range (20..0) among others is not ordered either, and takes the scan.
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
let reversed: Vec<(Range<usize>, TextStyle)> = vec![(0..10, st(10.0)), (20..0, st(20.0)), (5..15, st(30.0)), (15..20, st(40.0))];
|
||||
assert!(!runs_ordered(&reversed));
|
||||
for b in 0..30 {
|
||||
assert_eq!(run_at(&reversed, runs_ordered(&reversed), b), scan(&reversed, b), "{b}");
|
||||
}
|
||||
assert_eq!(run_at(&[], true, 5), 0);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,26 @@ 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.
|
||||
// Only the first subpath is measured, so only it sets the tolerance and gets flattened.
|
||||
let mut first = BezPath::new();
|
||||
for el in path.iter() {
|
||||
if matches!(el, PathEl::MoveTo(_)) && !first.elements().is_empty() {
|
||||
break;
|
||||
}
|
||||
first.push(el);
|
||||
if matches!(el, PathEl::ClosePath) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A non-finite coordinate (an infinite control point too) measures nothing.
|
||||
if !first.is_finite() {
|
||||
return PathMeasure::default();
|
||||
}
|
||||
let bbox = kurbo::Shape::bounding_box(&first);
|
||||
let size = bbox.width().max(bbox.height());
|
||||
kurbo::flatten(first.iter(), 0.05_f64.max(size * 1e-6), |el| {
|
||||
if done {
|
||||
return;
|
||||
}
|
||||
@@ -128,6 +147,38 @@ 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);
|
||||
// Even with finite endpoints, an infinite control point is refused before flattening.
|
||||
let mut r = BezPath::new();
|
||||
r.move_to((0.0, 0.0));
|
||||
r.curve_to((f64::INFINITY, f64::INFINITY), (f64::INFINITY, 0.0), (1.0, 0.0));
|
||||
assert_eq!(PathMeasure::new(&r, false).pts.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_distant_second_subpath_does_not_coarsen_the_first() {
|
||||
let mut p = BezPath::new();
|
||||
p.move_to((0.0, 0.0));
|
||||
p.curve_to((0.0, 100.0), (100.0, 100.0), (100.0, 0.0));
|
||||
p.move_to((1e9, 1e9));
|
||||
p.line_to((1e9 + 1.0, 1e9));
|
||||
let m = PathMeasure::new(&p, false);
|
||||
assert!(m.pts.len() > 10, "{}", m.pts.len());
|
||||
assert!(m.length() > 150.0, "{}", m.length());
|
||||
}
|
||||
|
||||
#[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