Skip to content

Commit 436850c

Browse files
committed
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.
1 parent 4fc9513 commit 436850c

2 files changed

Lines changed: 71 additions & 29 deletions

File tree

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)