Files
bea-edit/tests/unit/pdf-text-overlay.spec.ts
2026-10-28 16:09:53 +01:00

134 lines
3.9 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { degrees, PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import { TextOverlayError } from '../../src/lib/pdf-errors';
import {
TEXT_OVERLAY_MARGIN,
drawTextOverlay,
getRotatedBoxPlacement
} from '../../src/lib/pdf-text-overlay';
import { getPageText, getPageTextAngle, getPageTextMatrix } from '../e2e/helpers';
const PAGE_WIDTH = 420;
const PAGE_HEIGHT = 594;
const MARGIN = 28;
/**
* Maps a point from default user space to the displayed page. Viewers rotate the
* content clockwise by `/Rotate`, so this is the forward transformation.
*/
const toVisualPoint = (
point: { x: number; y: number },
rotation: number,
width: number,
height: number
) => {
switch (rotation) {
case 90:
return { x: point.y, y: width - point.x };
case 180:
return { x: width - point.x, y: height - point.y };
case 270:
return { x: height - point.y, y: point.x };
default:
return point;
}
};
const createPageWithMark = async (rotation: number) => {
const document = await PDFDocument.create();
const font = await document.embedFont(StandardFonts.Helvetica);
const page = document.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
page.setRotation(degrees(rotation));
drawTextOverlay(page, {
font,
text: 'Bl. 1',
size: 11,
color: rgb(0.1, 0.1, 0.1),
position: 'bottom-center',
margin: MARGIN,
compensatePageRotation: true
});
return PDFDocument.load(await document.save());
};
for (const rotation of [0, 90, 180, 270]) {
test(`bottom-center mark stays at the visual bottom on a /Rotate ${rotation} page`, async () => {
const output = await createPageWithMark(rotation);
const matrix = getPageTextMatrix(output, 0);
const visualWidth = rotation % 180 === 0 ? PAGE_WIDTH : PAGE_HEIGHT;
const origin = toVisualPoint({ x: matrix.x, y: matrix.y }, rotation, PAGE_WIDTH, PAGE_HEIGHT);
// Horizontally centred on the displayed page.
expect(Math.abs(origin.x - visualWidth / 2)).toBeLessThan(30);
// Close to the bottom margin (the baseline sits a few points above it).
expect(origin.y).toBeGreaterThan(MARGIN - 2);
expect(origin.y).toBeLessThan(MARGIN + 20);
// The glyphs read horizontally: the drawn angle cancels the page rotation.
expect(Math.round(getPageTextAngle(matrix))).toBe(rotation);
expect(getPageText(output, 0)).toBe('Bl. 1');
});
}
test('a mark that does not fit raises a TextOverlayError instead of shrinking forever', async () => {
const document = await PDFDocument.create();
const font = await document.embedFont(StandardFonts.Helvetica);
const page = document.addPage([120, 160]);
expect(() =>
drawTextOverlay(page, {
font,
text: 'Ein sehr langer Stempeltext, der nicht auf die Seite passt',
size: 18,
color: rgb(0, 0, 0),
position: 'bottom-center',
margin: MARGIN,
framed: true
})
).toThrow(TextOverlayError);
});
test('characters the standard font cannot encode are reported as such', async () => {
const document = await PDFDocument.create();
const font = await document.embedFont(StandardFonts.Helvetica);
const page = document.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
let thrown: unknown;
try {
drawTextOverlay(page, {
font,
text: 'Beschluss \u{1F600}',
size: 11,
color: rgb(0, 0, 0),
position: 'top-right',
margin: MARGIN
});
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(TextOverlayError);
expect((thrown as Error).message).toContain('nicht dargestellt werden');
});
test('a rotated mark keeps its bounding box inside the page', async () => {
const boxWidth = 200;
const boxHeight = 40;
for (const angle of [0, 45, 90, 135]) {
const placement = getRotatedBoxPlacement(
PAGE_WIDTH,
PAGE_HEIGHT,
boxWidth,
boxHeight,
'bottom-center',
angle,
TEXT_OVERLAY_MARGIN
);
expect(placement.x).toBeGreaterThanOrEqual(TEXT_OVERLAY_MARGIN - 0.001);
expect(placement.y).toBeGreaterThanOrEqual(TEXT_OVERLAY_MARGIN - 0.001);
expect(placement).not.toHaveProperty('cosine');
}
});