Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New basePdf type and Support adding new pages to template #394

Merged
merged 20 commits into from Jan 3, 2024
Merged
6 changes: 5 additions & 1 deletion packages/common/src/helper.ts
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { Buffer } from 'buffer';
import { Schema, Template, Font, BasePdf, Plugins } from './types';
import { Schema, Template, Font, BasePdf, Plugins, BlankPdf } from './types';
import {
Inputs as InputsSchema,
UIOptions as UIOptionsSchema,
Expand All @@ -9,6 +9,7 @@ import {
DesignerProps as DesignerPropsSchema,
GenerateProps as GeneratePropsSchema,
UIProps as UIPropsSchema,
BlankPdf as BlankPdfSchema,
} from './schema.js';
import {
MM_TO_PT_RATIO,
Expand Down Expand Up @@ -98,6 +99,9 @@ export const getB64BasePdf = (basePdf: BasePdf) => {
return basePdf as string;
};

export const isBlankPdf = (basePdf: BasePdf): basePdf is BlankPdf =>
BlankPdfSchema.safeParse(basePdf).success;

const getByteString = (base64: string) => Buffer.from(base64, 'base64').toString('binary');

export const b64toUint8Array = (base64: string) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/common/src/index.ts
Expand Up @@ -48,7 +48,8 @@ import {
pt2mm,
pt2px,
isHexValid,
getInputFromTemplate
getInputFromTemplate,
isBlankPdf,
} from './helper.js';

export {
Expand All @@ -67,6 +68,7 @@ export {
pt2px,
isHexValid,
getInputFromTemplate,
isBlankPdf,
checkFont,
checkInputs,
checkUIOptions,
Expand Down
28 changes: 18 additions & 10 deletions packages/common/src/schema.ts
@@ -1,6 +1,6 @@
import { z } from 'zod';

const langs = ['en', 'ja', 'ar', 'th', 'pl', 'it', 'de'] as const;
const langs = ['en', 'ja', 'ar', 'th', 'it', 'pl', 'de'] as const;

export const Lang = z.enum(langs);
export const Dict = z.object({
Expand All @@ -26,6 +26,9 @@ export const Dict = z.object({
errorBulkUpdateFieldName: z.string(),
commitBulkUpdateFieldName: z.string(),
bulkUpdateFieldName: z.string(),
addPageAfter: z.string(),
removePage: z.string(),
removePageConfirm: z.string(),
hexColorPrompt: z.string(),
// -----------------used in schemas-----------------
'schemas.color': z.string(),
Expand Down Expand Up @@ -81,16 +84,13 @@ export const SchemaForUI = Schema.merge(SchemaForUIAdditionalInfo);

const ArrayBufferSchema: z.ZodSchema<ArrayBuffer> = z.any().refine((v) => v instanceof ArrayBuffer);
const Uint8ArraySchema: z.ZodSchema<Uint8Array> = z.any().refine((v) => v instanceof Uint8Array);
export const BlankPdf = z.object({
width: z.number(),
height: z.number(),
padding: z.array(z.number()).length(4),
});

export const Font = z.record(
z.object({
data: z.union([z.string(), ArrayBufferSchema, Uint8ArraySchema]),
fallback: z.boolean().optional(),
subset: z.boolean().optional(),
})
);

export const BasePdf = z.union([z.string(), ArrayBufferSchema, Uint8ArraySchema]);
export const BasePdf = z.union([z.string(), ArrayBufferSchema, Uint8ArraySchema, BlankPdf]);

export const Template = z
.object({
Expand All @@ -101,6 +101,14 @@ export const Template = z

export const Inputs = z.array(z.record(z.string())).min(1);

export const Font = z.record(
z.object({
data: z.union([z.string(), ArrayBufferSchema, Uint8ArraySchema]),
fallback: z.boolean().optional(),
subset: z.boolean().optional(),
})
);

const CommonOptions = z.object({ font: Font.optional() }).passthrough();

const CommonProps = z.object({
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/types.ts
Expand Up @@ -11,6 +11,7 @@ import {
Font,
SchemaForUI,
BasePdf,
BlankPdf,
Template,
GeneratorOptions,
GenerateProps,
Expand Down Expand Up @@ -157,6 +158,7 @@ export type SchemaForUI = z.infer<typeof SchemaForUI>;
*/
export type Font = z.infer<typeof Font>;
export type BasePdf = z.infer<typeof BasePdf>;
export type BlankPdf = z.infer<typeof BlankPdf>;
export type Template = z.infer<typeof Template>;
export type GeneratorOptions = z.infer<typeof GeneratorOptions>;
export type GenerateProps = z.infer<typeof GenerateProps> & { plugins?: Plugins };
Expand Down
Binary file modified packages/generator/__tests__/assets/pdfs/assert/pet.pdf
Binary file not shown.
15 changes: 12 additions & 3 deletions packages/generator/__tests__/assets/templates/pet.json
Expand Up @@ -394,8 +394,8 @@
"badge": {
"type": "readOnlySvg",
"position": {
"x": 178.06,
"y": 9
"x": 180,
"y": 10
},
"width": 20,
"height": 20,
Expand All @@ -404,7 +404,16 @@
}
}
],
"basePdf": "data:application/pdf;base64,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",
"basePdf": {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main part of what I did in this PR is that I made it so that we can set it with a new type called BlankPdf instead of PDF data in basePdf.

"width": 210,
"height": 297,
"padding": [
10,
10,
10,
10
]
},
"columns": [
"name",
"subtitle",
Expand Down
14 changes: 5 additions & 9 deletions packages/generator/src/generate.ts
@@ -1,7 +1,7 @@
import * as pdfLib from '@pdfme/pdf-lib';
import type { GenerateProps } from '@pdfme/common';
import { checkGenerateProps } from '@pdfme/common';
import { drawEmbeddedPage, preprocessing, postProcessing } from './helper.js';
import { insertPage, preprocessing, postProcessing } from './helper.js';

const generate = async (props: GenerateProps) => {
checkGenerateProps(props);
Expand All @@ -11,7 +11,7 @@ const generate = async (props: GenerateProps) => {
throw new Error('inputs should not be empty');
}

const { pdfDoc, embeddedPages, embedPdfBoxes, renderObj } = await preprocessing({
const { pdfDoc, basePages, embedPdfBoxes, renderObj } = await preprocessing({
template,
userPlugins,
});
Expand All @@ -21,14 +21,10 @@ const generate = async (props: GenerateProps) => {
const _cache = new Map();
for (let i = 0; i < inputs.length; i += 1) {
const inputObj = inputs[i];
for (let j = 0; j < embeddedPages.length; j += 1) {
const embeddedPage = embeddedPages[j];
const { width: pageWidth, height: pageHeight } = embeddedPage;
for (let j = 0; j < basePages.length; j += 1) {
const basePage = basePages[j];
const embedPdfBox = embedPdfBoxes[j];

const page = pdfDoc.addPage([pageWidth, pageHeight]);

drawEmbeddedPage({ page, embeddedPage, embedPdfBox });
const page = insertPage({ basePage, embedPdfBox, pdfDoc });
for (let l = 0; l < keys.length; l += 1) {
const key = keys[l];
const schemaObj = template.schemas[j];
Expand Down
128 changes: 78 additions & 50 deletions packages/generator/src/helper.ts
@@ -1,68 +1,79 @@
import * as fontkit from 'fontkit';
import * as pdfLib from '@pdfme/pdf-lib';
import type { GeneratorOptions, Template, PDFRenderProps, Plugin } from '@pdfme/common';
import type { Schema, Plugins } from '@pdfme/common';
import {
Schema,
Plugins,
GeneratorOptions,
Template,
PDFRenderProps,
Plugin,
getB64BasePdf,
BasePdf,
isBlankPdf,
mm2pt,
} from '@pdfme/common';
import { builtInPlugins } from '@pdfme/schemas';
import { PDFPage, PDFDocument, PDFEmbeddedPage, TransformationMatrix } from '@pdfme/pdf-lib';
import { getB64BasePdf, BasePdf } from '@pdfme/common';
import { TOOL_NAME } from './constants.js';
import type { EmbedPdfBox } from './types';

export const getEmbeddedPagesAndEmbedPdfBoxes = async (arg: {
const getBasePagesAndEmbedPdfBoxes = async (arg: {
template: Template;
pdfDoc: PDFDocument;
basePdf: BasePdf;
}) => {
const { pdfDoc, basePdf } = arg;
let embeddedPages: PDFEmbeddedPage[] = [];
const {
template: { schemas },
pdfDoc,
basePdf,
} = arg;
let basePages: (PDFEmbeddedPage | PDFPage)[] = [];
let embedPdfBoxes: EmbedPdfBox[] = [];
const willLoadPdf = typeof basePdf === 'string' ? await getB64BasePdf(basePdf) : basePdf;
const embedPdf = await PDFDocument.load(willLoadPdf);
const embedPdfPages = embedPdf.getPages();

embedPdfBoxes = embedPdfPages.map((p) => ({
mediaBox: p.getMediaBox(),
bleedBox: p.getBleedBox(),
trimBox: p.getTrimBox(),
}));

const boundingBoxes = embedPdfPages.map((p) => {
const { x, y, width, height } = p.getMediaBox();

return { left: x, bottom: y, right: width, top: height + y };
});

const transformationMatrices = embedPdfPages.map(
() => [1, 0, 0, 1, 0, 0] as TransformationMatrix
);

embeddedPages = await pdfDoc.embedPages(embedPdfPages, boundingBoxes, transformationMatrices);

return { embeddedPages, embedPdfBoxes };
};

export const drawEmbeddedPage = (arg: {
page: PDFPage;
embeddedPage: PDFEmbeddedPage;
embedPdfBox: EmbedPdfBox;
}) => {
const { page, embeddedPage, embedPdfBox } = arg;
page.drawPage(embeddedPage);
const { mediaBox: mb, bleedBox: bb, trimBox: tb } = embedPdfBox;
page.setMediaBox(mb.x, mb.y, mb.width, mb.height);
page.setBleedBox(bb.x, bb.y, bb.width, bb.height);
page.setTrimBox(tb.x, tb.y, tb.width, tb.height);
if (isBlankPdf(basePdf)) {
const { width: _width, height: _height } = basePdf;
const width = mm2pt(_width);
const height = mm2pt(_height);
basePages = schemas.map(() => {
const page = PDFPage.create(pdfDoc);
page.setSize(width, height);
return page;
});
embedPdfBoxes = schemas.map(() => ({
mediaBox: { x: 0, y: 0, width, height },
bleedBox: { x: 0, y: 0, width, height },
trimBox: { x: 0, y: 0, width, height },
}));
} else {
const willLoadPdf = typeof basePdf === 'string' ? await getB64BasePdf(basePdf) : basePdf;
const embedPdf = await PDFDocument.load(willLoadPdf as ArrayBuffer | Uint8Array | string);
const embedPdfPages = embedPdf.getPages();
embedPdfBoxes = embedPdfPages.map((p) => ({
mediaBox: p.getMediaBox(),
bleedBox: p.getBleedBox(),
trimBox: p.getTrimBox(),
}));
const boundingBoxes = embedPdfPages.map((p) => {
const { x, y, width, height } = p.getMediaBox();
return { left: x, bottom: y, right: width, top: height + y };
});
const transformationMatrices = embedPdfPages.map(
() => [1, 0, 0, 1, 0, 0] as TransformationMatrix
);
basePages = await pdfDoc.embedPages(embedPdfPages, boundingBoxes, transformationMatrices);
}
return { basePages, embedPdfBoxes };
};

export const preprocessing = async (arg: { template: Template; userPlugins: Plugins }) => {
const { template, userPlugins } = arg;
const { basePdf, schemas } = template;

const pdfDoc = await pdfLib.PDFDocument.create();
const pdfDoc = await PDFDocument.create();
// @ts-ignore
pdfDoc.registerFontkit(fontkit);

const pagesAndBoxes = await getEmbeddedPagesAndEmbedPdfBoxes({ pdfDoc, basePdf });
const { embeddedPages, embedPdfBoxes } = pagesAndBoxes;
const basePagesAndBoxes = await getBasePagesAndEmbedPdfBoxes({ template, pdfDoc, basePdf });
const { basePages, embedPdfBoxes } = basePagesAndBoxes;

const pluginValues = (
Object.values(userPlugins).length > 0
Expand All @@ -84,13 +95,10 @@ Check this document: https://pdfme.com/docs/custom-schemas`);
return { ...acc, [type]: render.pdf };
}, {} as Record<string, (arg: PDFRenderProps<Schema>) => Promise<void> | void>);

return { pdfDoc, embeddedPages, embedPdfBoxes, renderObj };
return { pdfDoc, basePages, embedPdfBoxes, renderObj };
};

export const postProcessing = (props: {
pdfDoc: pdfLib.PDFDocument;
options: GeneratorOptions;
}) => {
export const postProcessing = (props: { pdfDoc: PDFDocument; options: GeneratorOptions }) => {
const { pdfDoc, options } = props;
const {
author = TOOL_NAME,
Expand All @@ -113,3 +121,23 @@ export const postProcessing = (props: {
pdfDoc.setSubject(subject);
pdfDoc.setTitle(title);
};

export const insertPage = (arg: {
basePage: PDFEmbeddedPage | PDFPage;
embedPdfBox: EmbedPdfBox;
pdfDoc: PDFDocument;
}) => {
const { basePage, embedPdfBox, pdfDoc } = arg;
const size = basePage instanceof PDFEmbeddedPage ? basePage.size() : basePage.getSize();
const insertedPage = pdfDoc.addPage([size.width, size.height]);

if (basePage instanceof PDFEmbeddedPage) {
insertedPage.drawPage(basePage);
const { mediaBox, bleedBox, trimBox } = embedPdfBox;
insertedPage.setMediaBox(mediaBox.x, mediaBox.y, mediaBox.width, mediaBox.height);
insertedPage.setBleedBox(bleedBox.x, bleedBox.y, bleedBox.width, bleedBox.height);
insertedPage.setTrimBox(trimBox.x, trimBox.y, trimBox.width, trimBox.height);
}

return insertedPage;
};
8 changes: 7 additions & 1 deletion packages/ui/__tests__/assets/helper.ts
Expand Up @@ -6,7 +6,13 @@ export const setupUIMock = () => {
const backgrounds = ['data:image/png;base64,a...'];
const pageSizes = [{ height: 297, width: 210 }];
const mock = jest.spyOn(hooks, 'useUIPreProcessor');
mock.mockImplementation(() => ({ backgrounds, pageSizes, scale: 1, error: null }));
mock.mockImplementation(() => ({
backgrounds,
pageSizes,
scale: 1,
error: null,
refresh: () => Promise.resolve(),
}));
(getPdfPageSizes as jest.Mock) = jest.fn().mockReturnValue(Promise.resolve(pageSizes));
(pdf2Pngs as jest.Mock) = jest.fn().mockReturnValue(Promise.resolve(backgrounds));
(uuid as jest.Mock) = jest
Expand Down
Expand Up @@ -11,7 +11,7 @@ exports[`Designer snapshot 1`] = `
style="position: absolute; bottom: 6%; width: 850px;"
>
<div
style="display: flex; align-items: center; justify-content: center; position: relative; z-index: 1; left: calc(50% - 75px); width: 150px; height: 40px; box-sizing: border-box; padding: 12px; border-radius: 6px; background-color: rgba(0, 0, 0, 0.45);"
style="display: flex; align-items: center; justify-content: space-evenly; position: relative; z-index: 1; left: calc(50% - 75px); width: 150px; height: 40px; box-sizing: border-box; padding: 12px; border-radius: 6px; background-color: rgba(0, 0, 0, 0.45);"
>
<div
style="display: flex; align-items: center;"
Expand Down Expand Up @@ -312,7 +312,6 @@ exports[`Designer snapshot 1`] = `
style="transform: scale(1); transform-origin: top left; height: 1px; width: 1px;"
>
<div
id="@pdfme/ui-paper0"
style="font-family: 'Roboto'; top: 30px; left: 28.149606294500018px; position: relative; background-image: url(data:image/png;base64,a...); background-size: 793.700787411px 1122.5196850527px; width: 793.700787411px; height: 1122.5196850527px;"
>
<div
Expand Down