Skip to content

Commit f3bf106

Browse files
authored
fix(pdf-server): import highlight/underline/strike from existing PDFs (#592)
* fix(pdf-server): import highlight/underline/strike from existing PDFs importPdfjsAnnotation expected nested quadPoints arrays, but pdf.js emits a flat Float32Array (8 numbers per quad). Iterating it yielded numbers, qp.length was undefined, rects stayed empty, and the function returned null - so every quad-based annotation in a loaded PDF was dropped from annotationMap and rendered only as unselectable canvas pixels. Shipped this way in #506; not a regression. Parse the flat array in 8-stride chunks, fall through to ann.rect if that yields nothing. Existing tests used the (wrong) nested fixture shape; switched them to Float32Array and added a 2-quad case + a rect-fallback case. * fix(pdf-server): own markup annotations in our layer, not the canvas page.render() defaulted to AnnotationMode.ENABLE, painting every annotation's appearance stream onto the canvas. We ALSO import them into annotationMap and render DOM versions in #annotation-layer, so the user saw two representations: an unclickable canvas pixel that looked right, and our clickable DOM element that looked like our styling. Clicking the 'real' one did nothing. Set annotationMode: DISABLE so the canvas is page-content-only and our layer is the single source of truth for markup. Imported annotations now behave identically to user-created ones (select/drag/delete). Form widgets are unaffected (#form-layer via AnnotationLayer.render). get_screenshot keeps ENABLE_STORAGE so screenshots still show annotations. Known trade-offs: - Stamps with image appearance streams degrade to our text-label render (no rasterization yet). - Annotation types we don't import (Ink, Polygon, Caret, ...) no longer ghost on the canvas - they're invisible. They were never editable anyway; loadBaselineAnnotations already logs them. * Revert "fix(pdf-server): own markup annotations in our layer, not the canvas" This reverts commit 2f989a7.
1 parent 4fc9513 commit f3bf106

File tree

2 files changed

+71
-29
lines changed

2 files changed

+71
-29
lines changed

examples/pdf-server/src/pdf-annotations.test.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,8 @@ describe("importPdfjsAnnotation", () => {
460460
annotationType: 9,
461461
ref: { num: 5, gen: 0 },
462462
rect: [72, 700, 272, 712],
463-
quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]],
463+
// pdf.js emits a FLAT Float32Array, not nested arrays.
464+
quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]),
464465
color: new Uint8ClampedArray([255, 255, 0]),
465466
contentsObj: { str: "Important" },
466467
};
@@ -474,12 +475,57 @@ describe("importPdfjsAnnotation", () => {
474475
expect((result as any).color).toBe("#ffff00");
475476
});
476477

478+
it("imports a multi-line highlight (multiple quads → multiple rects)", () => {
479+
// Regression: the parser iterated quadPoints as if nested; pdf.js's
480+
// flat array yielded numbers, so rects stayed empty and import bailed.
481+
const ann = {
482+
annotationType: 9,
483+
ref: { num: 8, gen: 0 },
484+
quadPoints: new Float32Array([
485+
// line 1
486+
72, 712, 272, 712, 72, 700, 272, 700,
487+
// line 2
488+
72, 698, 200, 698, 72, 686, 200, 686,
489+
]),
490+
color: new Uint8ClampedArray([255, 255, 0]),
491+
};
492+
const result = importPdfjsAnnotation(ann, 1, 0);
493+
expect(result).not.toBeNull();
494+
expect(result!.type).toBe("highlight");
495+
expect((result as any).rects).toHaveLength(2);
496+
expect((result as any).rects[0]).toEqual({
497+
x: 72,
498+
y: 700,
499+
width: 200,
500+
height: 12,
501+
});
502+
expect((result as any).rects[1]).toEqual({
503+
x: 72,
504+
y: 686,
505+
width: 128,
506+
height: 12,
507+
});
508+
});
509+
510+
it("falls back to ann.rect when quadPoints is absent", () => {
511+
const ann = {
512+
annotationType: 9,
513+
ref: { num: 9, gen: 0 },
514+
rect: [72, 700, 272, 712],
515+
color: new Uint8ClampedArray([255, 255, 0]),
516+
};
517+
const result = importPdfjsAnnotation(ann, 1, 0);
518+
expect(result).not.toBeNull();
519+
expect((result as any).rects).toHaveLength(1);
520+
});
521+
477522
it("imports an underline annotation", () => {
478523
const ann = {
479524
annotationType: 10,
480525
ref: { num: 6, gen: 0 },
481526
rect: [72, 700, 272, 712],
482-
quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]],
527+
// pdf.js emits a FLAT Float32Array, not nested arrays.
528+
quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]),
483529
color: new Uint8ClampedArray([255, 0, 0]),
484530
};
485531
const result = importPdfjsAnnotation(ann, 1, 0);
@@ -493,7 +539,8 @@ describe("importPdfjsAnnotation", () => {
493539
annotationType: 12,
494540
ref: { num: 7, gen: 0 },
495541
rect: [72, 700, 272, 712],
496-
quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]],
542+
// pdf.js emits a FLAT Float32Array, not nested arrays.
543+
quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]),
497544
color: new Uint8ClampedArray([255, 0, 0]),
498545
};
499546
const result = importPdfjsAnnotation(ann, 2, 0);

examples/pdf-server/src/pdf-annotations.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -948,33 +948,28 @@ export function importPdfjsAnnotation(
948948
case "highlight":
949949
case "underline":
950950
case "strikethrough": {
951-
// PDF.js provides quadPoints as array of arrays [[x1,y1,x2,y2,...], ...]
952-
// or rect as [x1,y1,x2,y2]
953-
let rects: Rect[];
954-
if (ann.quadPoints && ann.quadPoints.length > 0) {
955-
rects = [];
956-
for (const qp of ann.quadPoints) {
957-
// Each quadPoint is [x1,y1,x2,y2,x3,y3,x4,y4]
958-
// We need the bounding box
959-
if (qp.length >= 8) {
960-
const xs = [qp[0], qp[2], qp[4], qp[6]];
961-
const ys = [qp[1], qp[3], qp[5], qp[7]];
962-
const minX = Math.min(...xs);
963-
const minY = Math.min(...ys);
964-
const maxX = Math.max(...xs);
965-
const maxY = Math.max(...ys);
966-
rects.push({
967-
x: minX,
968-
y: minY,
969-
width: maxX - minX,
970-
height: maxY - minY,
971-
});
972-
}
951+
// pdf.js emits quadPoints as a FLAT Float32Array [x1,y1,...,x4,y4, …]
952+
// (8 numbers per quad), NOT as nested arrays. Iterating it yields
953+
// numbers, so the old `for (const qp of …) if (qp.length>=8)` never
954+
// matched and every quad-based annotation was dropped (#506).
955+
const rects: Rect[] = [];
956+
const qp = ann.quadPoints as ArrayLike<number> | undefined;
957+
if (qp && qp.length >= 8) {
958+
for (let i = 0; i + 8 <= qp.length; i += 8) {
959+
const xs = [qp[i], qp[i + 2], qp[i + 4], qp[i + 6]];
960+
const ys = [qp[i + 1], qp[i + 3], qp[i + 5], qp[i + 7]];
961+
const minX = Math.min(...xs);
962+
const minY = Math.min(...ys);
963+
rects.push({
964+
x: minX,
965+
y: minY,
966+
width: Math.max(...xs) - minX,
967+
height: Math.max(...ys) - minY,
968+
});
973969
}
974-
} else if (ann.rect) {
975-
rects = [pdfjsRectToRect(ann.rect)];
976-
} else {
977-
return null;
970+
}
971+
if (rects.length === 0 && ann.rect) {
972+
rects.push(pdfjsRectToRect(ann.rect));
978973
}
979974
if (rects.length === 0) return null;
980975

0 commit comments

Comments
 (0)