fix: keep flattened Sign PDF text at its placed position

readFormMatrix loads the appearance Matrix and uses the identity when
that entry is missing or is not six numbers. alignAppearanceToRect maps
the four BBox corners through that matrix and returns the scale and
translation that fit the transformed box to the annotation rectangle.
Those values are the cm concatenated before Do, and the original Matrix
stays on the form XObject so Do applies it once. A transformed box whose
width or height is under 0.001 is left unpainted, and that annotation is
still removed.

Fixes #647
This commit is contained in:
Matt Van Horn committed 2026-10-02 22:16:51 -07:00
1 parent c10511203e
commit 4fd0b1d3e6
2 files changed
+261 -8

No files matched your search

+61 -7
View File
@@ -33,6 +33,53 @@ function resolveStreamDict(obj: unknown): PDFDict | null {
return null;
}
const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
function readFormMatrix(dict: PDFDict | null): number[] {
if (!dict) return IDENTITY_MATRIX;
const matrixObj = dict.lookup(PDFName.of('Matrix'));
if (!(matrixObj instanceof PDFArray) || matrixObj.size() !== 6) {
return IDENTITY_MATRIX;
}
return extractNumbers(matrixObj, 6) ?? IDENTITY_MATRIX;
}
// ISO 32000-1 §12.5.5: map the matrix-transformed BBox onto Rect.
// Do still applies the form Matrix, so this cm must not include it again.
function alignAppearanceToRect(
bbox: number[],
matrix: number[],
x1: number,
y1: number,
x2: number,
y2: number
): [number, number, number, number] | null {
const [a, b, c, d, e, f] = matrix;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const x of [bbox[0], bbox[2]]) {
for (const y of [bbox[1], bbox[3]]) {
const px = a * x + c * y + e;
const py = b * x + d * y + f;
if (px < minX) minX = px;
if (py < minY) minY = py;
if (px > maxX) maxX = px;
if (py > maxY) maxY = py;
}
}
const bw = maxX - minX;
const bh = maxY - minY;
if (bw < 0.001 || bh < 0.001) return null;
const sx = (x2 - x1) / bw;
const sy = (y2 - y1) / bh;
return [sx, sy, x1 - minX * sx, y1 - minY * sy];
}
export function flattenAnnotations(pdfDoc: PDFDocument): void {
const pages = pdfDoc.getPages();
@@ -132,6 +179,20 @@ export function flattenAnnotations(pdfDoc: PDFDocument): void {
}
}
const placement = alignAppearanceToRect(
bbox,
readFormMatrix(streamDict),
x1,
y1,
x2,
y2
);
if (!placement) {
hasChanges = true;
continue;
}
const [sx, sy, tx, ty] = placement;
let appRef: PDFRef;
if (normalAppRef instanceof PDFRef) {
appRef = normalAppRef;
@@ -140,13 +201,6 @@ export function flattenAnnotations(pdfDoc: PDFDocument): void {
}
const xObjKey = pageNode.newXObject('FlatAnnot', appRef);
const bw = bbox[2] - bbox[0];
const bh = bbox[3] - bbox[1];
const sx = Math.abs(bw) > 0.001 ? (x2 - x1) / bw : 1;
const sy = Math.abs(bh) > 0.001 ? (y2 - y1) / bh : 1;
const tx = x1 - bbox[0] * sx;
const ty = y1 - bbox[1] * sy;
page.pushOperators(
PDFOperator.of(PDFOperatorNames.PushGraphicsState),
PDFOperator.of(PDFOperatorNames.ConcatTransformationMatrix, [