printmaker
Version:
Generate PDF documents and from JavaScript objects
347 lines (291 loc) • 9.87 kB
text/typescript
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { PDFArray, PDFContext, PDFDict, PDFFont, PDFName, PDFRef, rgb } from 'pdf-lib';
import { Frame } from '../src/layout.js';
import { renderFrame, renderPage } from '../src/page.js';
import { fakePdfFont } from './test-utils.js';
const { objectContaining } = expect;
describe('page', () => {
describe('renderPage', () => {
let size, pdfPage, doc;
beforeEach(() => {
size = { width: 300, height: 400 };
pdfPage = { drawRectangle: jest.fn() };
doc = { pdfDoc: { addPage: jest.fn().mockReturnValue(pdfPage) } as any };
});
it('renders content', () => {
const content: Frame = {
...{ x: 50, y: 50, width: 280, height: 300 },
objects: [{ type: 'rect', x: 0, y: 0, width: 280, height: 300 }],
};
const page = { size, content };
renderPage(page, doc);
expect(pdfPage.drawRectangle).toHaveBeenCalledWith(
objectContaining({ x: 50, y: 50, width: 280, height: 300 })
);
});
it('renders header', () => {
const content = { x: 50, y: 50, width: 280, height: 300 };
const header: Frame = {
...{ x: 50, y: 20, width: 280, height: 30 },
objects: [{ type: 'rect', x: 0, y: 0, width: 280, height: 30 }],
};
const page = { size, content, header };
renderPage(page, doc);
expect(pdfPage.drawRectangle).toHaveBeenCalledWith(
objectContaining({ x: 50, y: 350, width: 280, height: 30 })
);
});
it('renders footer', () => {
const content = { x: 50, y: 50, width: 280, height: 300 };
const footer: Frame = {
...{ x: 50, y: 350, width: 280, height: 30 },
objects: [{ type: 'rect', x: 0, y: 0, width: 280, height: 30 }],
};
const page = { size, content, footer };
renderPage(page, doc);
expect(pdfPage.drawRectangle).toHaveBeenCalledWith(
objectContaining({ x: 50, y: 20, width: 280, height: 30 })
);
});
});
describe('renderFrame', () => {
let page, size, pdfPage, font: PDFFont;
beforeEach(() => {
size = { width: 500, height: 800 };
const context = PDFContext.create();
pdfPage = {
doc: { context, catalog: context.obj({}) },
ref: PDFRef.of(1),
node: context.obj({}),
drawText: jest.fn(),
drawLine: jest.fn(),
drawRectangle: jest.fn(),
drawSvgPath: jest.fn(),
};
page = { size, pdfPage };
font = fakePdfFont('Test');
});
it('renders text objects', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'text', x: 1, y: 2, text: 'Test text', fontSize: 12, font }],
};
renderFrame(frame, page);
expect(pdfPage.drawText).toHaveBeenCalledWith('Test text', {
x: 10 + 1,
y: 800 - 20 - 2 - 30,
size: 12,
font,
});
});
it('renders text objects with style attrs', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [
{
...{ type: 'text', x: 1, y: 2, text: 'Test text', fontSize: 12, font },
color: rgb(1, 0, 0),
},
],
};
renderFrame(frame, page);
expect(pdfPage.drawText).toHaveBeenCalledWith(
'Test text',
objectContaining({ color: rgb(1, 0, 0) })
);
});
it('renders children relative to parent frame', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 100 },
children: [
{
...{ x: 10, y: 20, width: 80, height: 30 },
objects: [{ type: 'text', x: 1, y: 2, text: 'Test text', fontSize: 12, font }],
},
],
};
renderFrame(frame, page);
expect(pdfPage.drawText).toHaveBeenCalledWith('Test text', {
x: 10 + 10 + 1,
y: 800 - 20 - 20 - 2 - 30,
size: 12,
font,
});
});
it('renders named destinations', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'anchor', x: 1, y: 2, name: 'test-dest' }],
};
renderFrame(frame, page);
const names = pdfPage.doc.catalog
.get(PDFName.of('Names'))
.get(PDFName.of('Dests'))
.get(PDFName.of('Names'));
expect(names).toBeInstanceOf(PDFArray);
expect(names.toString()).toEqual('[ (test-dest) [ 1 0 R /XYZ 11 778 null ] ]');
});
it('renders link objects', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'link', x: 1, y: 2, width: 80, height: 20, url: 'test-url' }],
};
renderFrame(frame, page);
const pageAnnotations = pdfPage.node.get(PDFName.of('Annots'));
expect(pageAnnotations).toBeInstanceOf(PDFArray);
expect(pageAnnotations.size()).toBe(1);
const ref = pageAnnotations.get(0);
expect(ref).toBeInstanceOf(PDFRef);
const annotation = pdfPage.doc.context.indirectObjects.get(ref);
expect(annotation.toString()).toEqual(
[
'<<',
'/Type /Annot',
'/Subtype /Link',
'/Rect [ 11 748 91 768 ]', // [10 + 1, 800 - 20 - 2 - 30, 11 + 80, 748 + 20]
'/A <<',
'/Type /Action',
'/S /URI',
'/URI (test-url)',
'>>',
'/C [ ]',
'/F 0',
'>>',
].join('\n')
);
});
it('renders internal link objects', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'link', x: 1, y: 2, width: 80, height: 20, url: '#test-id' }],
};
renderFrame(frame, page);
const pageAnnotations = pdfPage.node.get(PDFName.of('Annots'));
expect(pageAnnotations).toBeInstanceOf(PDFArray);
expect(pageAnnotations.size()).toBe(1);
const annotation = pdfPage.doc.context.lookup(pageAnnotations.get(0)) as PDFDict;
expect(annotation).toBeInstanceOf(PDFDict);
expect(annotation.toString()).toEqual(
[
'<<',
'/Type /Annot',
'/Subtype /Link',
'/Rect [ 11 748 91 768 ]', // [10 + 1, 800 - 20 - 2 - 30, 11 + 80, 748 + 20]
'/A <<',
'/Type /Action',
'/S /GoTo',
'/D (test-id)',
'>>',
'/C [ ]',
'/F 0',
'>>',
].join('\n')
);
});
it('renders rect objects', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'rect', x: 1, y: 2, width: 3, height: 4 }],
};
renderFrame(frame, page);
expect(pdfPage.drawRectangle).toHaveBeenCalledWith({
x: 10 + 1,
y: 800 - 20 - 2 - 4,
width: 3,
height: 4,
});
});
it('renders rect objects with style attrs', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [
{
...{ type: 'rect', x: 1, y: 2, width: 3, height: 4 },
strokeColor: rgb(1, 0, 0),
strokeWidth: 1,
fillColor: rgb(0, 0, 1),
},
],
};
renderFrame(frame, page);
expect(pdfPage.drawRectangle).toHaveBeenCalledWith(
objectContaining({
borderColor: rgb(1, 0, 0),
borderWidth: 1,
color: rgb(0, 0, 1),
})
);
});
it('renders line objects', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'line', x1: 1, y1: 2, x2: 3, y2: 4 }],
};
renderFrame(frame, page);
expect(pdfPage.drawLine).toHaveBeenCalledWith({
start: { x: 10 + 1, y: 800 - 20 - 2 },
end: { x: 10 + 3, y: 800 - 20 - 4 },
});
});
it('renders line objects with style attrs', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [
{
...{ type: 'line', x1: 1, y1: 2, x2: 3, y2: 4 },
strokeWidth: 1,
strokeColor: rgb(1, 0, 0),
},
],
};
renderFrame(frame, page);
expect(pdfPage.drawLine).toHaveBeenCalledWith(
objectContaining({
thickness: 1,
color: rgb(1, 0, 0),
})
);
});
it('renders polyline objects', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'polyline', points: [p(1, 2), p(3, 4)] }],
};
renderFrame(frame, page);
expect(pdfPage.drawSvgPath).toHaveBeenCalledWith('M1,2 L3,4', { x: 10, y: 800 - 20 });
});
it('renders polyline objects with closePath', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [{ type: 'polyline', points: [p(1, 2), p(3, 4)], closePath: true }],
};
renderFrame(frame, page);
expect(pdfPage.drawSvgPath).toHaveBeenCalledWith('M1,2 L3,4 Z', expect.anything());
});
it('renders polyline objects with style attrs', () => {
const frame: Frame = {
...{ x: 10, y: 20, width: 200, height: 30 },
objects: [
{
...{ type: 'polyline', points: [p(1, 2), p(3, 4)] },
strokeColor: rgb(1, 0, 0),
strokeWidth: 1,
fillColor: rgb(0, 0, 1),
},
],
};
renderFrame(frame, page);
expect(pdfPage.drawSvgPath).toHaveBeenCalledWith(
expect.anything(),
objectContaining({
borderColor: rgb(1, 0, 0),
borderWidth: 1,
color: rgb(0, 0, 1),
})
);
});
});
});
function p(x, y) {
return { x, y };
}