Skip to content

Commit 3dcefe4

Browse files
committed
fix(pdf-server): imported stamp canvases cropped/2x on retina
renderPage applied devicePixelRatio via ctx.scale(dpr,dpr) instead of page.render's transform parameter. pdf.js sizes annotationCanvasMap backing buffers as rectW * outputScaleX * viewport.scale, and outputScaleX is read from transform[0] (defaults 1). So on retina the per-annotation canvas got a 1x backing while its internal setTransform (from the SVD of the already-dpr-scaled ctx) was 2x - the appearance rendered at 2x into a half-sized buffer, showing only the top-left quarter. Pass dpr via transform: [dpr,0,0,dpr,0,0] so outputScaleX matches. Also filter AnnotationLayer.render() to Widget annotations only so it stops creating empty pointer-events:auto sections for stamps in #form-layer that could steal clicks from our overlays.
1 parent 46df2d0 commit 3dcefe4

File tree

1 file changed

+18
-3
lines changed

1 file changed

+18
-3
lines changed

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3225,8 +3225,15 @@ async function renderPage() {
32253225
// resize so the size handoff is atomic.
32263226
pageWrapperEl.style.transform = "";
32273227

3228-
// Scale context for retina
3229-
ctx.scale(dpr, dpr);
3228+
// Retina: pass dpr via page.render's `transform` (NOT ctx.scale).
3229+
// pdf.js sizes per-annotation canvases as
3230+
// width = rectW * outputScaleX * viewport.scale
3231+
// and outputScaleX is read from transform[0] (defaults to 1). A bare
3232+
// ctx.scale(dpr,dpr) leaves outputScaleX at 1, so the
3233+
// annotationCanvasMap canvases get a half-sized backing store on
3234+
// retina while their internal setTransform IS dpr-aware → the
3235+
// appearance renders 2× too big into a 1× buffer → cropped/shifted.
3236+
const dprTransform = dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined;
32303237

32313238
// Clear and setup text layer
32323239
textLayerEl.innerHTML = "";
@@ -3249,6 +3256,7 @@ async function renderPage() {
32493256
const renderTask = (page.render as any)({
32503257
canvasContext: ctx,
32513258
viewport,
3259+
transform: dprTransform,
32523260
annotationCanvasMap,
32533261
// isEditing forces hasOwnCanvas=true for stamps regardless of /F
32543262
// NoRotate (StampAnnotation.mustBeViewedWhenEditing in pdf.worker).
@@ -3337,8 +3345,15 @@ async function renderPage() {
33373345
commentManager: null,
33383346
} as any);
33393347
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3348+
// Only feed Widgets (form fields) here. Markup annotations are
3349+
// owned by #annotation-layer; letting AnnotationLayer create
3350+
// <section> elements for them in #form-layer adds invisible
3351+
// pointer-events:auto boxes that steal clicks from our overlays.
3352+
const widgetAnns = annotations.filter(
3353+
(a: { subtype?: string }) => a.subtype === "Widget",
3354+
);
33403355
await annotationLayer.render({
3341-
annotations,
3356+
annotations: widgetAnns,
33423357
div: formLayerEl,
33433358
page,
33443359
viewport,

0 commit comments

Comments
 (0)