134 lines
3.9 KiB
TypeScript
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');
|
|
}
|
|
});
|