Skip to content

Commit 9d55050

Browse files
authored
fix(pdf-server): render page before O(numPages) annotation scans (#581)
PR #506 added loadBaselineAnnotations() and buildFieldNameMap() between showViewer() and renderPage(). Both iterate every page calling getPage()+getAnnotations(), so the viewer sat with an unsized empty canvas for ~400ms (15-page doc, scales linearly) before painting. Neither scan blocks the canvas — page.render() only needs canvasContext + viewport. renderAnnotationsForPage() reads annotationMap (graceful empty), AnnotationLayer.render() takes cachedFieldObjects (graceful null). So: render first, scan after, re-render to overlay annotations + form values. Same fix at the reload path.
1 parent 4b3f625 commit 9d55050

1 file changed

Lines changed: 22 additions & 10 deletions

File tree

examples/pdf-server/src/mcp-app.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3896,11 +3896,16 @@ async function reloadPdf(): Promise<void> {
38963896
log.info("PDF reloaded:", totalPages, "pages,", totalBytes, "bytes");
38973897

38983898
showViewer();
3899+
// Render immediately — annotation/form scans below are O(numPages) and
3900+
// do NOT block the canvas. See same pattern in the initial-load path.
3901+
await renderPage();
3902+
38993903
await loadBaselineAnnotations(document);
39003904
await buildFieldNameMap(document);
39013905
syncFormValuesToStorage();
39023906
updateAnnotationsBadge();
39033907
renderAnnotationPanel();
3908+
39043909
renderPage();
39053910
startPreloading();
39063911
} catch (err) {
@@ -4103,9 +4108,18 @@ app.ontoolresult = async (result: CallToolResult) => {
41034108
downloadBtn.style.display = app.getHostCapabilities()?.downloadFile
41044109
? ""
41054110
: "none";
4106-
// Save button visibility driven by setDirty()/updateSaveBtn();
4107-
// restoreAnnotations() above may have already shown it via setDirty(true).
4108-
updateSaveBtn();
4111+
4112+
// Compute fit + render IMMEDIATELY for fast first paint. The canvas is
4113+
// unsized until renderPage() runs — anything async between showViewer()
4114+
// and here makes the empty viewer visible. The annotation/form scans
4115+
// below are O(numPages) and do NOT block the canvas (page.render only
4116+
// needs canvasContext+viewport), so they run after.
4117+
const fitScale = await computeFitToWidthScale();
4118+
if (fitScale !== null) {
4119+
scale = fitScale;
4120+
log.info("Fit-to-width scale:", scale);
4121+
}
4122+
await renderPage();
41094123

41104124
// Import annotations from the PDF to establish baseline
41114125
await loadBaselineAnnotations(document);
@@ -4118,14 +4132,12 @@ app.ontoolresult = async (result: CallToolResult) => {
41184132
syncFormValuesToStorage();
41194133

41204134
updateAnnotationsBadge();
4135+
// Save button visibility driven by setDirty()/updateSaveBtn();
4136+
// restoreAnnotations() may have just flipped it via setDirty(true).
4137+
updateSaveBtn();
41214138

4122-
// Compute fit-to-width scale for narrow containers (e.g. mobile)
4123-
const fitScale = await computeFitToWidthScale();
4124-
if (fitScale !== null) {
4125-
scale = fitScale;
4126-
log.info("Fit-to-width scale:", scale);
4127-
}
4128-
4139+
// Re-render to overlay PDF-baseline annotations + restored form values.
4140+
// For PDFs with neither, the canvas is identical → no flicker.
41294141
renderPage();
41304142
// Start background preloading of all pages for text extraction
41314143
startPreloading();

0 commit comments

Comments
 (0)