mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 19:19:10 -06:00
fix(engine): read long underscore runs in one pass
Word assembly checked whether the whole word so far was underscores for every glyph it added, so a long run of underscores on one line took quadratic time. The underscore state of the current word is kept as it grows.
This commit is contained in:
+36
-5
@@ -289,6 +289,12 @@ pub(crate) fn page_words(text: &pdfcraft_render::PageText, info: &pdfcraft_rende
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Characters read to tell underscore runs from other words.
|
||||
static UNDERSCORE_CHARS_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// A page's words in reading order, their boxes mapped from view space by `map` (and ordered
|
||||
/// again, so that [x0, y0, x1, y1] holds whatever way `map` turns them).
|
||||
pub(crate) fn words_with(text: &pdfcraft_render::PageText, map: impl Fn(f32, f32) -> [f32; 2]) -> Vec<(String, [f32; 4])> {
|
||||
@@ -296,6 +302,8 @@ pub(crate) fn words_with(text: &pdfcraft_render::PageText, map: impl Fn(f32, f32
|
||||
let mut last_line = u32::MAX;
|
||||
// A space glyph ends the word before it.
|
||||
let mut gap = false;
|
||||
// Whether the last word is an underscore run (a form's blank), which other glyphs don't join.
|
||||
let mut last_under = false;
|
||||
for (i, g) in text.glyphs.iter().enumerate() {
|
||||
let line = text.line_of.get(i).copied().unwrap_or(0);
|
||||
if g.text.trim().is_empty() {
|
||||
@@ -303,10 +311,9 @@ pub(crate) fn words_with(text: &pdfcraft_render::PageText, map: impl Fn(f32, f32
|
||||
continue;
|
||||
}
|
||||
let under = g.text.chars().all(|c| c == '_');
|
||||
let breaks = match words.last() {
|
||||
None => true,
|
||||
Some((w, _)) => gap || line != last_line || text.space_before.get(i).copied().unwrap_or(false) || w.chars().all(|c| c == '_') != under,
|
||||
};
|
||||
#[cfg(test)]
|
||||
UNDERSCORE_CHARS_READ.with(|n| n.set(n.get() + g.text.len()));
|
||||
let breaks = words.is_empty() || gap || line != last_line || text.space_before.get(i).copied().unwrap_or(false) || last_under != under;
|
||||
gap = false;
|
||||
last_line = line;
|
||||
match words.last_mut() {
|
||||
@@ -317,7 +324,10 @@ pub(crate) fn words_with(text: &pdfcraft_render::PageText, map: impl Fn(f32, f32
|
||||
r[2] = r[2].max(g.rect[2]);
|
||||
r[3] = r[3].max(g.rect[3]);
|
||||
}
|
||||
_ => words.push((g.text.clone(), g.rect)),
|
||||
_ => {
|
||||
words.push((g.text.clone(), g.rect));
|
||||
last_under = under;
|
||||
}
|
||||
}
|
||||
}
|
||||
words
|
||||
@@ -409,6 +419,27 @@ impl Session {
|
||||
mod review_tests {
|
||||
use super::*;
|
||||
|
||||
/// A long run of underscores (a form's blank) is read in one pass: each glyph is compared with
|
||||
/// the word so far without reading the word again.
|
||||
#[test]
|
||||
fn a_long_underscore_run_is_read_in_one_pass() {
|
||||
let n = 10_000;
|
||||
let glyph = |i: usize, t: &str| pdfcraft_render::TextGlyph {
|
||||
text: t.into(),
|
||||
rect: [i as f32, 0.0, i as f32 + 1.0, 10.0],
|
||||
quad: [[0.0; 2]; 4],
|
||||
direction: [1.0, 0.0],
|
||||
};
|
||||
let mut glyphs: Vec<_> = (0..n).map(|i| glyph(i, "_")).collect();
|
||||
glyphs.push(glyph(n, "x"));
|
||||
let text = pdfcraft_render::PageText { line_of: vec![0; glyphs.len()], space_before: vec![false; glyphs.len()], glyphs };
|
||||
UNDERSCORE_CHARS_READ.with(|c| c.set(0));
|
||||
let words = words_with(&text, |x, y| [x, y]);
|
||||
assert_eq!(words.iter().map(|(w, _)| w.len()).collect::<Vec<_>>(), [n, 1]);
|
||||
let read = UNDERSCORE_CHARS_READ.with(std::cell::Cell::get);
|
||||
assert!(read <= 4 * n, "{read} characters read for {n} glyphs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_field_names_diff_against_first_original() {
|
||||
let first = pdfcraft_js::FieldState::new("duplicate", pdfcraft_js::FieldType::Text, vec!["first".into()]);
|
||||
|
||||
Reference in New Issue
Block a user