mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(fonts): look up CID and width ranges by binary search
Code-to-CID and /W range lookups scanned every record in declaration order, once per glyph and once per explicit ToUnicode entry while a font was read, so a few thousand records multiplied by as many codes took billions of steps. Both tables are indexed once into stretches whose first-declared covering record wins, the precedence the scan had, and the /W ranges stop at the CMap entry limit.
This commit is contained in:
+141
-16
@@ -33,9 +33,9 @@ pub struct Metrics {
|
||||
widths: Vec<f64>,
|
||||
/// Composite fonts: CID → width, and CID ranges with one width.
|
||||
cid_widths: HashMap<u32, f64>,
|
||||
cid_ranges: Vec<(u32, u32, f64)>,
|
||||
cid_ranges: Spans<f64>,
|
||||
/// Code → CID for embedded non-identity CMaps (`cidrange` / `cidchar`).
|
||||
cid_map: Vec<(u32, u32, u32)>,
|
||||
cid_map: Spans<u32>,
|
||||
default: f64,
|
||||
/// Composite fonts with an explicit `/DW`: `/W` may leave out the glyphs that have that
|
||||
/// width (Chrome and Skia do), so a CID missing from `/W` is not a missing glyph.
|
||||
@@ -598,8 +598,8 @@ impl Metrics {
|
||||
first: 0,
|
||||
widths: Vec::new(),
|
||||
cid_widths: HashMap::new(),
|
||||
cid_ranges: Vec::new(),
|
||||
cid_map: Vec::new(),
|
||||
cid_ranges: Spans::default(),
|
||||
cid_map: Spans::default(),
|
||||
default: 500.0,
|
||||
explicit_default: false,
|
||||
std14: Some(crate::Std14::Helvetica),
|
||||
@@ -685,7 +685,7 @@ impl Metrics {
|
||||
if !spaces.is_empty() {
|
||||
m.codes = Codes::Ranges(spaces);
|
||||
}
|
||||
m.cid_map = cids;
|
||||
m.cid_map = Spans::new(cids);
|
||||
}
|
||||
let desc = font
|
||||
.get(b"DescendantFonts")
|
||||
@@ -698,6 +698,7 @@ impl Metrics {
|
||||
m.explicit_default = true;
|
||||
}
|
||||
if let Some(w) = desc.get(b"W").map(|w| doc.resolve(w)).and_then(|w| w.as_array().cloned()) {
|
||||
let mut ranges = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < w.len() {
|
||||
let Some(c0) = doc.resolve(&w[i]).as_f64().map(|v| v as u32) else { break };
|
||||
@@ -714,13 +715,16 @@ impl Metrics {
|
||||
}
|
||||
Some(c1) => {
|
||||
if let (Some(c1), Some(v)) = (c1.as_f64(), w.get(i + 2).and_then(|x| doc.resolve(x).as_f64())) {
|
||||
m.cid_ranges.push((c0, c1 as u32, v));
|
||||
if ranges.len() < MAX_CMAP_ENTRIES {
|
||||
ranges.push((c0, c1 as u32, v));
|
||||
}
|
||||
}
|
||||
i += 3;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
m.cid_ranges = Spans::new(ranges);
|
||||
}
|
||||
descriptor = dict(doc, desc.get(b"FontDescriptor"));
|
||||
} else {
|
||||
@@ -791,14 +795,14 @@ impl Metrics {
|
||||
if self.cid_map.is_empty() {
|
||||
return code;
|
||||
}
|
||||
self.cid_map.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map_or(0, |(lo, _, c)| c.saturating_add(code - lo))
|
||||
self.cid_map.find(code).map_or(0, |(lo, _, c)| c.saturating_add(code.saturating_sub(*lo)))
|
||||
}
|
||||
|
||||
/// The advance of `code` in text space per unit of font size.
|
||||
pub fn width(&self, code: u32) -> f64 {
|
||||
if self.composite {
|
||||
let cid = self.cid(code);
|
||||
let w = self.cid_widths.get(&cid).copied().or_else(|| self.cid_ranges.iter().find(|(a, b, _)| cid >= *a && cid <= *b).map(|r| r.2));
|
||||
let w = self.cid_widths.get(&cid).copied().or_else(|| self.cid_ranges.find(cid).map(|r| r.2));
|
||||
return w.unwrap_or(self.default) * self.scale;
|
||||
}
|
||||
if let Some(w) = code.checked_sub(self.first).and_then(|i| self.widths.get(i as usize)) {
|
||||
@@ -858,7 +862,7 @@ impl Metrics {
|
||||
// A CID /W leaves out takes /DW, and Skia (Chrome) leaves out every CID of that width: for
|
||||
// those, only the subset's program says whether the glyph is there.
|
||||
return self.cid_widths.contains_key(&cid)
|
||||
|| self.cid_ranges.iter().any(|(a, b, _)| cid >= *a && cid <= *b)
|
||||
|| self.cid_ranges.find(cid).is_some()
|
||||
|| !self.subset
|
||||
|| self.outlined_cids.get(cid as usize / 64).is_some_and(|w| (*w >> (cid % 64)) & 1 == 1)
|
||||
// No readable TrueType program to ask (e.g. CFF or missing): an explicit /DW
|
||||
@@ -958,16 +962,69 @@ fn add_range(out: &mut Unicode, budget: &mut usize, seq: &mut u32, lo: u32, hi:
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Code span records read by CID and width lookups, for the tests that keep a lookup logarithmic.
|
||||
static SPAN_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Counts one span record read (tests only).
|
||||
#[inline]
|
||||
fn count_span_reads() {
|
||||
#[cfg(test)]
|
||||
SPAN_READS.with(|c| c.set(c.get().saturating_add(1)));
|
||||
}
|
||||
|
||||
/// Code spans `(lo, hi, value)` in declaration order, where the first-declared span covering a code
|
||||
/// wins, indexed so that a lookup is one binary search.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
struct Spans<T> {
|
||||
spans: Vec<(u32, u32, T)>,
|
||||
/// The span covering each stretch of codes; see [`stretches`].
|
||||
cover: Vec<(u64, Option<usize>)>,
|
||||
}
|
||||
|
||||
impl<T> Spans<T> {
|
||||
fn new(spans: Vec<(u32, u32, T)>) -> Self {
|
||||
let bounds: Vec<(u32, u32)> = spans.iter().map(|s| (s.0, s.1)).collect();
|
||||
Spans { cover: stretches(&bounds, false), spans }
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.spans.is_empty()
|
||||
}
|
||||
|
||||
/// The first-declared span covering `code`.
|
||||
fn find(&self, code: u32) -> Option<&(u32, u32, T)> {
|
||||
let step = self.cover.partition_point(|&(start, _)| {
|
||||
count_span_reads();
|
||||
start <= u64::from(code)
|
||||
});
|
||||
let &(_, record) = self.cover.get(step.checked_sub(1)?)?;
|
||||
self.spans.get(record?)
|
||||
}
|
||||
}
|
||||
|
||||
/// For each stretch of codes, from its first code on, the latest-declared range covering it (`None`
|
||||
/// where no range does), sorted by code; a lookup is one binary search over it.
|
||||
fn cover_index(ranges: &[BfRange]) -> Vec<(u64, Option<usize>)> {
|
||||
let spans: Vec<(u32, u32)> = ranges.iter().map(|r| (r.lo, r.hi)).collect();
|
||||
stretches(&spans, true)
|
||||
}
|
||||
|
||||
/// For each stretch of codes, from its first code on, the span `(lo, hi)` covering it (`None` where
|
||||
/// none does), sorted by code: the latest-declared one when `later_wins`, else the first-declared.
|
||||
/// At most `u32::MAX` spans are indexed.
|
||||
fn stretches(spans: &[(u32, u32)], later_wins: bool) -> Vec<(u64, Option<usize>)> {
|
||||
// Heap keys, largest for the winning record; the mapping is its own inverse.
|
||||
let key = |record: u32| if later_wins { record } else { !record };
|
||||
// Openings (first code, record) and closings (one past the last code), each in code order. The cover
|
||||
// can only change at one of their codes, so the sweep visits those codes in order.
|
||||
let mut opens: Vec<(u32, u32)> = ranges.iter().enumerate().map(|(i, r)| (r.lo, i as u32)).collect();
|
||||
let mut opens: Vec<(u32, u32)> = spans.iter().zip(0..u32::MAX).map(|(&(lo, _), i)| (lo, i)).collect();
|
||||
opens.sort_unstable();
|
||||
let mut closes: Vec<u64> = ranges.iter().map(|r| u64::from(r.hi) + 1).collect();
|
||||
let mut closes: Vec<u64> = spans.iter().take(opens.len()).map(|&(_, hi)| u64::from(hi) + 1).collect();
|
||||
closes.sort_unstable();
|
||||
// Records that have opened and not yet closed; the top is the latest declared one.
|
||||
// Keys of the records that have opened and not yet closed; the top is the winning one.
|
||||
let mut open: BinaryHeap<u32> = BinaryHeap::new();
|
||||
let mut cover: Vec<(u64, Option<usize>)> = Vec::new();
|
||||
let (mut next_open, mut next_close) = (0, 0);
|
||||
@@ -981,16 +1038,16 @@ fn cover_index(ranges: &[BfRange]) -> Vec<(u64, Option<usize>)> {
|
||||
while let Some(&(lo, record)) = opens.get(next_open)
|
||||
&& u64::from(lo) <= p
|
||||
{
|
||||
open.push(record);
|
||||
open.push(key(record));
|
||||
next_open += 1;
|
||||
}
|
||||
while closes.get(next_close).is_some_and(|&end| end <= p) {
|
||||
next_close += 1;
|
||||
}
|
||||
while open.peek().is_some_and(|&top| ranges.get(top as usize).is_none_or(|r| u64::from(r.hi) < p)) {
|
||||
while open.peek().is_some_and(|&top| spans.get(key(top) as usize).is_none_or(|&(_, hi)| u64::from(hi) < p)) {
|
||||
open.pop();
|
||||
}
|
||||
let winner = open.peek().map(|&top| top as usize);
|
||||
let winner = open.peek().map(|&top| key(top) as usize);
|
||||
if cover.last().map(|&(_, w)| w) != Some(winner) {
|
||||
cover.push((p, winner));
|
||||
}
|
||||
@@ -1266,6 +1323,74 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Span records read while `f` runs.
|
||||
fn span_reads(f: impl FnOnce()) -> usize {
|
||||
SPAN_READS.with(|c| c.set(0));
|
||||
f();
|
||||
SPAN_READS.with(|c| c.get())
|
||||
}
|
||||
|
||||
/// A composite font with `encoding` as its CMap, `w` as its widths and `to_unicode`.
|
||||
fn composite(doc: &mut Document, encoding: &str, w: Vec<Object>, to_unicode: &str) -> Dict {
|
||||
let mut desc = Dict::new();
|
||||
desc.set(b"Subtype".to_vec(), Object::name("CIDFontType2"));
|
||||
desc.set(b"W".to_vec(), Object::Array(w));
|
||||
let enc = Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), encoding.as_bytes()))));
|
||||
let tu = Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), to_unicode.as_bytes()))));
|
||||
font(
|
||||
doc,
|
||||
vec![
|
||||
("Subtype", Object::name("Type0")),
|
||||
("Encoding", enc),
|
||||
("DescendantFonts", Object::Array(vec![Object::Dict(desc)])),
|
||||
("ToUnicode", tu),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Many `/W` ranges that leave a code out cost a glyph of it a binary search, not a scan of them all.
|
||||
#[test]
|
||||
fn widths_of_a_code_no_range_covers_take_few_steps() {
|
||||
let mut doc = Document::new_empty();
|
||||
let w: Vec<Object> = (1..=10_000i64).flat_map(|k| [Object::Int(2 * k), Object::Int(2 * k), Object::Int(500)]).collect();
|
||||
let f = composite(&mut doc, "", w, "");
|
||||
let m = Metrics::from_dict(&doc, &f);
|
||||
assert_eq!(m.width(4), 0.5);
|
||||
let reads = span_reads(|| {
|
||||
for _ in 0..1000 {
|
||||
assert_eq!(m.width(3), 1.0);
|
||||
}
|
||||
});
|
||||
assert!(reads <= 1000 * 64, "{reads} span reads");
|
||||
}
|
||||
|
||||
/// Where spans overlap, the first-declared one wins, as a scan in declaration order finds it.
|
||||
#[test]
|
||||
fn overlapping_spans_resolve_to_the_first_declared() {
|
||||
let spans = Spans::new(vec![(0, 10, 'a'), (5, 20, 'b'), (3, 3, 'c'), (30, 25, 'd'), (u32::MAX, u32::MAX, 'e')]);
|
||||
let found = |code| spans.find(code).map(|s| s.2);
|
||||
let want = [(0, Some('a')), (3, Some('a')), (10, Some('a')), (11, Some('b')), (20, Some('b')), (21, None), (27, None), (u32::MAX, Some('e'))];
|
||||
for (code, value) in want {
|
||||
assert_eq!(found(code), value, "code {code}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Checking the glyph of every explicit ToUnicode entry looks its code up once in the CID map.
|
||||
#[test]
|
||||
fn many_unicode_entries_look_up_their_cids_in_few_steps() {
|
||||
let n = 2000;
|
||||
let cidchars: String = (0..n).map(|k| format!("<{:04X}> {} ", 0x8000 + k, k + 1)).collect();
|
||||
let encoding = format!("1 begincodespacerange <0000> <FFFF> endcodespacerange {n} begincidchar {cidchars} endcidchar");
|
||||
let bfchars: String = (0..n).map(|k| format!("<{:04X}> <{:04X}> ", 0x1000 + k, 0x4E00 + k)).collect();
|
||||
let to_unicode = format!("{n} beginbfchar {bfchars} endbfchar");
|
||||
let mut doc = Document::new_empty();
|
||||
let f = composite(&mut doc, &encoding, Vec::new(), &to_unicode);
|
||||
let reads = span_reads(|| {
|
||||
Metrics::from_dict(&doc, &f);
|
||||
});
|
||||
assert!(reads <= n * 64, "{reads} span reads");
|
||||
}
|
||||
|
||||
/// Every glyph of a sequential range builds its text from the destination, so a long one is cut
|
||||
/// to the CMap limit once, when it is read.
|
||||
#[test]
|
||||
@@ -1736,7 +1861,7 @@ mod tests {
|
||||
let map = to_unicode(b"1 beginbfrange <0001> <0003> [<0041> <zz> <0043>] endbfrange");
|
||||
assert_eq!((map.get(&2), map.get(&3).map(String::as_str)), (None, Some("C")));
|
||||
let mut m = Metrics::fallback();
|
||||
(_, m.cid_map) = parse_cmap(b"1 begincidrange <0000> <FFFF> 4294967295 endcidrange");
|
||||
m.cid_map = Spans::new(parse_cmap(b"1 begincidrange <0000> <FFFF> 4294967295 endcidrange").1);
|
||||
assert_eq!(m.cid(5), u32::MAX);
|
||||
let mut doc = Document::new_empty();
|
||||
let mut enc = Dict::new();
|
||||
|
||||
Reference in New Issue
Block a user