PDF export: look internal links up in a map, not the whole document (#937)
Found while timing US government documents against Word for Windows. Before: every internal link (#bookmark: a linked table of contents, cross-references) found its bookmark by walking every paragraph of the document, so export time grew with links x length: NIH's 67-page protocol template (297 links) took 10.6 s to export as PDF. After: the bookmark positions are gathered once per export (the first bookmark of a name still wins). The same file exports in 0.9 s with identical text and links. internal_links_export_in_linear_time guards it (the old code took 12x as long for 4x the document). Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da5da66e51
commit
912d49f38f
|
|
@ -134,6 +134,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||||
| Date | What landed |
|
| Date | What landed |
|
||||||
| 2026-10-12 | New documents follow the system region (#838): A4 and centimetres outside the US (Letter in the CLDR Letter territories, inches in the US), lengths written with the region's decimal separator (`2,54 cm`) and typed with either; an explicit Measurement units choice wins; `file.options {"region"}` for agents |
|
| 2026-10-12 | New documents follow the system region (#838): A4 and centimetres outside the US (Letter in the CLDR Letter territories, inches in the US), lengths written with the region's decimal separator (`2,54 cm`) and typed with either; an explicit Measurement units choice wins; `file.options {"region"}` for agents |
|
||||||
| 2026-10-12 | Paste from other programs keeps their formatting (#687): Ctrl+V and Home › Paste read the system clipboard's HTML (and RTF on Windows and Wayland); Merge Formatting takes the destination's font and paragraph and keeps bold, italic, underline and strikethrough; Paste Special lists what the system clipboard holds (HTML, RTF, picture, text). Linux: pictures copied in a browser or Spectacle paste on KDE and other Wayland desktops (data-control clipboard), and a paste that finds nothing usable names what the clipboard holds instead of "empty" (#833) |
|
| 2026-10-12 | Paste from other programs keeps their formatting (#687): Ctrl+V and Home › Paste read the system clipboard's HTML (and RTF on Windows and Wayland); Merge Formatting takes the destination's font and paragraph and keeps bold, italic, underline and strikethrough; Paste Special lists what the system clipboard holds (HTML, RTF, picture, text). Linux: pictures copied in a browser or Spectacle paste on KDE and other Wayland desktops (data-control clipboard), and a paste that finds nothing usable names what the clipboard holds instead of "empty" (#833) |
|
||||||
|
| 2026-10-12 | PDF export of long documents with many internal links (a linked table of contents) is linear: NIH's 67-page protocol template 10.6 s → 0.9 s |
|
||||||
| 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged |
|
| 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged |
|
||||||
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ fn write(doc: &Document, lay: &DocLayout, opts: &PdfOptions, pages: &[usize], ou
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
tag_index: HashMap::new(),
|
tag_index: HashMap::new(),
|
||||||
links: Vec::new(),
|
links: Vec::new(),
|
||||||
|
bookmarks: std::cell::OnceCell::new(),
|
||||||
};
|
};
|
||||||
if let Some(o) = ex.outline(pages) {
|
if let Some(o) = ex.outline(pages) {
|
||||||
pdf.set_outline(o);
|
pdf.set_outline(o);
|
||||||
|
|
@ -522,6 +523,10 @@ struct Exporter<'a> {
|
||||||
tag_index: HashMap<(StoryRef, DocPath), usize>,
|
tag_index: HashMap<(StoryRef, DocPath), usize>,
|
||||||
/// Link rectangles on the current page: (x0, y0, x1, y1, target).
|
/// Link rectangles on the current page: (x0, y0, x1, y1, target).
|
||||||
links: Vec<(f32, f32, f32, f32, String)>,
|
links: Vec<(f32, f32, f32, f32, String)>,
|
||||||
|
/// Bookmark positions by name (the first of a name wins), gathered once: looking each internal
|
||||||
|
/// link up by walking the whole document made a long document with a linked table of contents
|
||||||
|
/// take seconds to export.
|
||||||
|
bookmarks: std::cell::OnceCell<HashMap<String, wordcraft_doc::Pos>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What the content being drawn is, for tagging.
|
/// What the content being drawn is, for tagging.
|
||||||
|
|
@ -1210,7 +1215,14 @@ impl Exporter<'_> {
|
||||||
/// Where a bookmark is: (output page, x, y) — the page and line its start lands on, which
|
/// Where a bookmark is: (output page, x, y) — the page and line its start lands on, which
|
||||||
/// in a paragraph split across pages need not be the paragraph's first page.
|
/// in a paragraph split across pages need not be the paragraph's first page.
|
||||||
fn bookmark(&self, name: &str) -> Option<(usize, f32, f32)> {
|
fn bookmark(&self, name: &str) -> Option<(usize, f32, f32)> {
|
||||||
let (_, pos) = self.doc.bookmarks().into_iter().find(|(n, _)| n == name)?;
|
let marks = self.bookmarks.get_or_init(|| {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
for (n, pos) in self.doc.bookmarks() {
|
||||||
|
m.entry(n).or_insert(pos);
|
||||||
|
}
|
||||||
|
m
|
||||||
|
});
|
||||||
|
let pos = marks.get(name)?.clone();
|
||||||
if let Some(c) = self.lay.caret(&pos)
|
if let Some(c) = self.lay.caret(&pos)
|
||||||
&& let Some(out) = self.out_index.get(&c.page)
|
&& let Some(out) = self.out_index.get(&c.page)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -821,6 +821,7 @@ fn bookmark_destination_is_the_page_of_its_start() {
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
tag_index: HashMap::new(),
|
tag_index: HashMap::new(),
|
||||||
links: Vec::new(),
|
links: Vec::new(),
|
||||||
|
bookmarks: std::cell::OnceCell::new(),
|
||||||
};
|
};
|
||||||
let (page, x, y) = ex.bookmark("target").unwrap();
|
let (page, x, y) = ex.bookmark("target").unwrap();
|
||||||
assert_eq!(page, 1, "the bookmark is on the second page");
|
assert_eq!(page, 1, "the bookmark is on the second page");
|
||||||
|
|
@ -864,3 +865,40 @@ fn comments_print_in_balloons_beside_scaled_pages() {
|
||||||
assert!(squash(&extract_text(&bytes).concat()).contains("On the heading"));
|
assert!(squash(&extract_text(&bytes).concat()).contains("On the heading"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A long document whose every paragraph links to a bookmark (a linked table of contents, cross
|
||||||
|
/// references) exports in time proportional to its length: each link looked its bookmark up by
|
||||||
|
/// walking the whole document, so NIH's 67-page protocol template took 10.6 s (0.9 s now).
|
||||||
|
#[test]
|
||||||
|
fn internal_links_export_in_linear_time() {
|
||||||
|
let doc = |n: usize| {
|
||||||
|
let mut d = Document::new();
|
||||||
|
d.body = (0..n)
|
||||||
|
.map(|i| {
|
||||||
|
// Eight links a paragraph, like a dense table of contents or cross-references.
|
||||||
|
let mut p = wordcraft_doc::Paragraph::new();
|
||||||
|
for k in 0..8 {
|
||||||
|
let link = CharProps { link: Some(format!("#mark{}", (i * 7 + k) % n)), ..CharProps::default() };
|
||||||
|
p.insert_text(p.len(), &format!("see {k} "), &link).unwrap();
|
||||||
|
}
|
||||||
|
p.insert_object(0, InlineObject::BookmarkStart { name: format!("mark{i}") }, &CharProps::default()).unwrap();
|
||||||
|
para_block(p)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
d
|
||||||
|
};
|
||||||
|
let fastest = |d: &Document| {
|
||||||
|
(0..3)
|
||||||
|
.map(|_| {
|
||||||
|
let t = std::time::Instant::now();
|
||||||
|
let bytes = export(d, &PdfOptions::default()).unwrap();
|
||||||
|
assert!(!bytes.is_empty());
|
||||||
|
t.elapsed().as_secs_f64()
|
||||||
|
})
|
||||||
|
.fold(f64::INFINITY, f64::min)
|
||||||
|
};
|
||||||
|
let (small, big) = (doc(100), doc(400));
|
||||||
|
let _ = export(&small, &PdfOptions::default());
|
||||||
|
let ratio = fastest(&big) / fastest(&small).max(1e-9);
|
||||||
|
assert!(ratio < 9.0, "4x the paragraphs and links took {ratio:.1}x as long (quadratic would be ~16x)");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue