refactoring, numbering plan

This commit is contained in:
rehlert committed 2026-10-28 16:09:53 +01:00
1 parent b5279434b2
commit 3b04039aa9
47 files changed
+3485 -953

No files matched your search

+107 -7
View File
@@ -123,19 +123,88 @@ export const getPageContentText = (document: PDFDocument, pageIndex: number) =>
.join('\n');
};
const PDF_STRING_ESCAPES: Record<string, string> = {
n: '\n',
r: '\r',
t: '\t',
b: '\b',
f: '\f',
'(': '(',
')': ')',
'\\': '\\'
};
/** Decodes a single PDF string token (`<hex>` or `(literal)`). */
const decodePdfString = (token: string): string => {
if (token.startsWith('<')) {
return Buffer.from(token.slice(1, -1).replace(/\s/g, ''), 'hex').toString('latin1');
}
const raw = token.slice(1, -1);
let decoded = '';
for (let index = 0; index < raw.length; index += 1) {
const char = raw[index];
if (char !== '\\') {
decoded += char;
continue;
}
const escaped = raw[index + 1];
index += 1;
if (escaped === undefined) break;
if (escaped >= '0' && escaped <= '7') {
let octal = escaped;
while (octal.length < 3 && raw[index + 1] >= '0' && raw[index + 1] <= '7') {
octal += raw[(index += 1)];
}
decoded += String.fromCharCode(Number.parseInt(octal, 8));
continue;
}
decoded += PDF_STRING_ESCAPES[escaped] ?? escaped;
}
return decoded;
};
const PDF_STRING_TOKEN = String.raw`<[0-9a-fA-F\s]*>|\((?:\\.|[^()\\])*\)`;
const TEXT_SHOWING_PATTERN = new RegExp(
String.raw`\[((?:[^\[\]\\]|\\.)*)\]\s*TJ|(${PDF_STRING_TOKEN})\s*(?:Tj|'|")`,
'g'
);
const STRING_TOKEN_PATTERN = new RegExp(PDF_STRING_TOKEN, 'g');
/**
* Decoded operands of all `Tj` operators. pdf-lib writes drawn text as hex
* strings (`<426C2E2031> Tj`), so the raw content stream is not readable.
* Decoded operands of every text-showing operator (`Tj`, `TJ`, `'`, `"`).
* pdf-lib writes drawn text as hex strings (`<426C2E2031> Tj`), so the raw
* content stream is not readable without this decoding.
*
* Limitations: other text operators (`Td`, `Tf`, …) and non-text strings are
* ignored. A page that shows text through an operator this helper does not know
* returns `[]`, so `not.toContain(...)` assertions on it prove nothing — assert
* on a positive match instead.
*/
export const getPageTextValues = (document: PDFDocument, pageIndex: number) => {
const content = getPageContentText(document, pageIndex);
const values: string[] = [];
for (const match of content.matchAll(/<([0-9a-fA-F]+)>\s*Tj/g)) {
values.push(Buffer.from(match[1], 'hex').toString('latin1'));
}
for (const match of content.matchAll(/\(((?:\\.|[^()\\])*)\)\s*Tj/g)) {
values.push(match[1]);
for (const match of content.matchAll(TEXT_SHOWING_PATTERN)) {
const [, arrayBody, singleToken] = match;
if (singleToken !== undefined) {
values.push(decodePdfString(singleToken));
continue;
}
for (const token of arrayBody.matchAll(STRING_TOKEN_PATTERN)) {
values.push(decodePdfString(token[0]));
}
}
return values;
@@ -144,6 +213,37 @@ export const getPageTextValues = (document: PDFDocument, pageIndex: number) => {
export const getPageText = (document: PDFDocument, pageIndex: number) =>
getPageTextValues(document, pageIndex).join('\n');
export type PageTextMatrix = { a: number; b: number; c: number; d: number; x: number; y: number };
/**
* Translation and rotation of every text matrix (`Tm`) in a page's content
* stream, in drawing order. Lets tests assert on the drawing geometry without
* hard-coding the hex-encoded text operands.
*/
export const getPageTextMatrices = (document: PDFDocument, pageIndex: number): PageTextMatrix[] => {
const content = getPageContentText(document, pageIndex);
const matrices: PageTextMatrix[] = [];
for (const match of content.matchAll(
/([-\d.]+) ([-\d.]+) ([-\d.]+) ([-\d.]+) ([-\d.]+) ([-\d.]+) Tm/g
)) {
const [a, b, c, d, x, y] = match.slice(1).map(Number);
matrices.push({ a, b, c, d, x, y });
}
if (matrices.length === 0) throw new Error(`Page ${pageIndex + 1} contains no text matrix.`);
return matrices;
};
/** First text matrix of a page (the only one in a single-mark document). */
export const getPageTextMatrix = (document: PDFDocument, pageIndex: number): PageTextMatrix =>
getPageTextMatrices(document, pageIndex)[0];
/** Angle of a text matrix in degrees, normalised to `0 … 359`. */
export const getPageTextAngle = (matrix: PageTextMatrix) =>
((Math.atan2(matrix.b, matrix.a) * 180) / Math.PI + 360) % 360;
export const createEncryptedPdf = async (pageCount: number, label: string, password: string) => {
const document = await EncryptablePdfDocument.load(await createPdf(pageCount, label));
document.encrypt({