refactoring, numbering plan
This commit is contained in:
1 parent
b5279434b2
commit
3b04039aa9
47 files changed
+3485
-953
No files matched your search
+107
-7
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user