mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M7: read the page once per keystroke when measuring a text draft (#1781)
`text_edit_layout`, which the inline editor calls on every keystroke to size the frame, interpreted the page's content four times: `text_blocks` for the paragraphs, `text_lines` again for the untouched runs, once more inside `rewrite_block` on the still unchanged clone, and finally for the rewritten draft. The first three read the same, unchanged page. It now reads the page's lines once, groups the paragraphs from them, and hands them to the rewrite through `rewrite_block_from`, which `rewrite_block_with_limit` also uses after reading the lines itself. The draft is still read back once after the rewrite. On a page of 120 runs in 80 paragraphs, 50 keystrokes in a release build took 0.87 ms each, against 1.25 ms before (the fastest of three runs each). No behaviour changes. The new test checks that the measured draft frame is the extent an applied edit actually draws, for a longer row, a shorter one and an added row, so sharing the lines can't drift from a fresh read. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
c5c5d0d661
commit
72a46facb8
@@ -3713,3 +3713,32 @@ fn a_subset_program_is_not_trusted_for_new_characters_and_the_error_names_the_ch
|
||||
assert!(msg.contains("ő (U+0151)") && msg.contains("Load font") && !msg.contains("Japanese"), "{msg}");
|
||||
assert_eq!(page_content_bytes(&doc, 0), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_layout_matches_the_paragraph_an_applied_edit_draws() {
|
||||
let content = "BT /F1 12 Tf 14 TL 72 700 Td (A first paragraph of words) Tj T* (with a second row) Tj ET \
|
||||
BT /F1 12 Tf 72 600 Td (Another paragraph below) Tj ET BT /F1 12 Tf 320 700 Td (Side note) Tj ET";
|
||||
let doc = text_page(content);
|
||||
let block = text::text_blocks(&doc, 0).unwrap().into_iter().position(|b| b.text.starts_with("A first")).unwrap();
|
||||
let source = text::text_blocks(&doc, 0).unwrap()[block].clone();
|
||||
for draft in [
|
||||
source.editor_text.replacen("words", "many more words than before", 1),
|
||||
source.editor_text.replacen(" of words", "", 1),
|
||||
format!("{}\nand a third row", source.editor_text),
|
||||
] {
|
||||
let layout = text::text_edit_layout(&doc, 0, block, &draft, &text::BlockStyle::default()).unwrap();
|
||||
// Apply the same edit and measure every run that isn't one of the page's other runs.
|
||||
let mut applied = doc.clone();
|
||||
text::rewrite_block(&mut applied, 0, block, Some(&draft), &text::BlockStyle::default()).unwrap();
|
||||
let others: Vec<_> =
|
||||
text::text_lines(&doc, 0).unwrap().into_iter().enumerate().filter(|(i, _)| !source.lines.contains(i)).map(|(_, l)| l.rect).collect();
|
||||
let drawn = text::text_lines(&applied, 0)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|l| l.rect)
|
||||
.filter(|r| !others.contains(r))
|
||||
.reduce(|a, b| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]);
|
||||
assert_eq!(layout.draft_rect, drawn, "{draft:?}");
|
||||
assert_eq!(layout.source_rect, source.rect);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -140,7 +140,10 @@ pub struct TextEditLayout {
|
||||
}
|
||||
|
||||
pub fn text_edit_layout(doc: &Document, page: usize, block: usize, text: &str, style: &BlockStyle) -> Result<TextEditLayout, EditError> {
|
||||
let original = text_blocks(doc, page)?;
|
||||
// This runs on every keystroke: read the page once for the paragraphs, the untouched runs
|
||||
// and the rewrite, which starts from the same unchanged page.
|
||||
let lines = text_lines(doc, page)?;
|
||||
let original = group_blocks(&lines);
|
||||
let source = original.get(block).ok_or(EditError::Invalid("text block does not exist".into()))?;
|
||||
if text == source.editor_text && *style == BlockStyle::default() {
|
||||
return Ok(TextEditLayout { source_rect: source.rect, draft_rect: Some(source.rect), overflow: false });
|
||||
@@ -150,13 +153,13 @@ pub fn text_edit_layout(doc: &Document, page: usize, block: usize, text: &str, s
|
||||
let key =
|
||||
|line: &TextLine| (line.text.clone(), line.font.clone(), line.rect.map(f64::to_bits), line.size.to_bits(), line.color.map(f64::to_bits));
|
||||
let mut untouched = std::collections::BTreeMap::new();
|
||||
for (index, line) in text_lines(doc, page)?.iter().enumerate() {
|
||||
for (index, line) in lines.iter().enumerate() {
|
||||
if !source.lines.contains(&index) {
|
||||
*untouched.entry(key(line)).or_insert(0_usize) += 1;
|
||||
}
|
||||
}
|
||||
let mut draft = doc.clone();
|
||||
rewrite_block(&mut draft, page, block, Some(text), style)?;
|
||||
rewrite_block_from(&mut draft, page, lines, block, Some(text), style, MAX_TYPE3_GLYPHS)?;
|
||||
let mut rect: Option<[f64; 4]> = None;
|
||||
for line in text_lines(&draft, page)? {
|
||||
if let Some(count) = untouched.get_mut(&key(&line))
|
||||
@@ -1734,6 +1737,19 @@ pub(crate) fn rewrite_block_with_limit(
|
||||
max_glyphs: usize,
|
||||
) -> Result<LineEdit, EditError> {
|
||||
let lines = text_lines(doc, page)?;
|
||||
rewrite_block_from(doc, page, lines, block, text, style, max_glyphs)
|
||||
}
|
||||
|
||||
/// [`rewrite_block_with_limit`] with the page's [`text_lines`], read from `doc` as it is now.
|
||||
fn rewrite_block_from(
|
||||
doc: &mut Document,
|
||||
page: usize,
|
||||
lines: Vec<TextLine>,
|
||||
block: usize,
|
||||
text: Option<&str>,
|
||||
style: &BlockStyle,
|
||||
max_glyphs: usize,
|
||||
) -> Result<LineEdit, EditError> {
|
||||
let blocks = group_blocks(&lines);
|
||||
let b = blocks.get(block).cloned().ok_or_else(|| EditError::Invalid(format!("page {} has no paragraph {}", page + 1, block + 1)))?;
|
||||
let members: Vec<&TextLine> = b.lines.iter().map(|i| &lines[*i]).collect();
|
||||
|
||||
Reference in New Issue
Block a user