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

feat: embed json certificate into pdf #241

Merged
merged 39 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7730393
add pdf-lib dependency for attachments
christophbuehler Feb 15, 2024
57c21d6
remove empty overrides
christophbuehler Feb 15, 2024
ee2224c
attaches a file to a pdf buffer
christophbuehler Feb 15, 2024
caeb5ac
extend generatePdf with functionality to attach the certificate to pdf
christophbuehler Feb 15, 2024
492d52d
update demo code to attach the certificate json
christophbuehler Feb 15, 2024
f522a92
add tests
christophbuehler Feb 15, 2024
5ef4dda
pin pdf-lib
christophbuehler Feb 15, 2024
9f12fc3
add pdf-lib to packages
christophbuehler Feb 15, 2024
c14930b
Remove lato-font dependency and update pdf-lib version
christophbuehler Feb 23, 2024
945db75
Add NotoSans-Bold.ttf, NotoSans-Italic.ttf, NotoSans-Light.ttf, NotoS…
christophbuehler Feb 23, 2024
2d3365b
Add sRGB2014.icc color profile
christophbuehler Feb 23, 2024
55a9d08
Update font references in PDF template utils
christophbuehler Feb 23, 2024
de8eee7
Remove unused dependencies and update font paths
christophbuehler Feb 23, 2024
b6e3f06
Update dependencies in generate-pdf package.json
christophbuehler Feb 23, 2024
12be178
Update font paths and remove unnecessary code
christophbuehler Feb 23, 2024
c042ba7
Remove unnecessary reference to tsconfig.spec.json
christophbuehler Feb 23, 2024
67ec976
Add readable-schema.json and update PDF certificate fixtures
christophbuehler Feb 23, 2024
a55ba11
Add attachCertificateToPdf and attachFileToPdf functions
christophbuehler Feb 23, 2024
c4c197f
Add PDF/A-3a support and validation documentation
christophbuehler Feb 23, 2024
fa5876b
Add buildModule function to generate-pdf package
christophbuehler Feb 23, 2024
b6da377
Refactor code to import PDF related interfaces from pdfmake package
christophbuehler Feb 23, 2024
c23f6bb
Refactor createPdf function to use fs/promises and update font paths
christophbuehler Feb 23, 2024
1e5ff76
Fix bug in login functionality
christophbuehler Feb 23, 2024
c8706bd
Update footer text and link in generate-pdf-template-helpers
christophbuehler Feb 23, 2024
005f3ff
Add PDF/A-3a compliance features
christophbuehler Feb 23, 2024
6e618be
Update ignorePatterns in .eslintrc.json
christophbuehler Feb 23, 2024
39fd4ac
Update semver dependency to version 6.3.1
christophbuehler Feb 23, 2024
01a4924
Update font-family in HTML templates
christophbuehler Feb 23, 2024
4f2d126
Refactor externalStandards in generateHtml function
christophbuehler Feb 23, 2024
4ca7a5a
Add lato again because we would need to update the material identity …
christophbuehler Feb 23, 2024
435df51
Delete test PDF files
christophbuehler Feb 26, 2024
b4b3778
Add references to tsconfig.json
christophbuehler Feb 26, 2024
aaae1c4
add font and color profile to assets
christophbuehler Feb 26, 2024
6fa49f5
move color profile and fonts to assets of generate-pdf
christophbuehler Feb 26, 2024
619aa9b
Add writeFileAndTest function and use it in generate-pdf.spec.ts
christophbuehler Feb 28, 2024
6a9c0ba
Update font paths in create-pdf-certificate.ts
christophbuehler Mar 1, 2024
12614b7
Update pdf2pic version to 3.1.1
christophbuehler Mar 1, 2024
1c20710
Create pdfBufferToHash and add comment to install ImageMagick and Gho…
christophbuehler Mar 1, 2024
a3592b7
Update fixtures
christophbuehler Mar 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 166 additions & 65 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"lodash.merge": "^4.6.2",
"lodash.set": "^4.3.2",
"node-cache": "^5.1.2",
"pdf-lib": "1.17.1",
christophbuehler marked this conversation as resolved.
Show resolved Hide resolved
"pdfmake": "0.2.9",
"reflect-metadata": "^0.2.1",
"semver-lite": "0.0.6"
Expand Down
12 changes: 0 additions & 12 deletions packages/generate-pdf/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,6 @@
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "node_modules"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
Expand Down
25 changes: 8 additions & 17 deletions packages/generate-pdf/example_coa.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-var-requires */
import { createWriteStream, readFileSync } from 'fs';
import { readFileSync } from 'fs';
import { writeFile } from 'fs/promises';

import { generatePdf, TDocumentDefinitions } from './src/index';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const styles = require(`${__dirname}/../generate-coa-pdf-template/utils/styles.js`);

const CoACertificate = JSON.parse(readFileSync(`${__dirname}/../../fixtures/CoA/v1.1.0/valid_cert_1.json`, 'utf-8'));
Expand Down Expand Up @@ -51,29 +52,19 @@ const generatorPath = '../generate-coa-pdf-template/dist/generateContent.cjs';

const pdfDoc = await generatePdf(CoACertificate, {
docDefinition,
outputType: 'stream',
outputType: 'buffer',
generatorPath,
fonts,
extraTranslations,
translations,
languageFontMap,
attachCertificate: true,
});

const outputFilePath = './coa-test.pdf';
const writeStream = createWriteStream(outputFilePath);
pdfDoc.pipe(writeStream);
pdfDoc.end();

await new Promise((resolve, reject) => {
writeStream
.on('finish', () => {
resolve(true);
})
.on('error', (err) => {
reject(err);
});
});
await writeFile(outputFilePath, pdfDoc);
} catch (error) {
christophbuehler marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-console
console.error(error.message);
}
})();
1 change: 1 addition & 0 deletions packages/generate-pdf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@s1seven/schema-tools-types": "0.5.6",
"@s1seven/schema-tools-utils": "0.2.9",
"pdf-lib": "1.17.1",
"lodash.clone": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2",
Expand Down
22 changes: 22 additions & 0 deletions packages/generate-pdf/src/attach-file-to-pdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AttachmentOptions, PDFDocument } from 'pdf-lib';

/**
* Attaches a file to a PDF document. The file can be viewed and downloaded in advanced PDF viewers like Adobe Acrobat Reader.
* @param pdfBuffer - The buffer of the PDF to which the file will be attached.
* @param fileContent - The content of the file to attach.
* @param fileName - The name of the file to attach.
* @param options - The options for the attachment, including mimeType, description, creationDate, and modificationDate.
* @returns A Promise that resolves with a Buffer of the updated PDF document.
*/
export const attachFileToPdf = async (
pdfBuffer: Buffer,
fileContent: string,
fileName: string,
options: AttachmentOptions,
): Promise<Buffer> => {
const pdfDoc = await PDFDocument.load(pdfBuffer);
const encodedFileContent = Buffer.from(fileContent).toString('base64');
await pdfDoc.attach(encodedFileContent, fileName, options);
const uint8Array = await pdfDoc.save();
return Buffer.from(uint8Array);
};
christophbuehler marked this conversation as resolved.
Show resolved Hide resolved
39 changes: 32 additions & 7 deletions packages/generate-pdf/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
loadExternalFile,
} from '@s1seven/schema-tools-utils';

import { attachFileToPdf } from './attach-file-to-pdf';

export { Content, StyleDictionary, TDocumentDefinitions, TFontDictionary } from 'pdfmake/interfaces';

export interface GeneratePdfOptions {
Expand All @@ -35,6 +37,7 @@ export interface GeneratePdfOptions {
translations?: Translations;
extraTranslations?: ExtraTranslations;
languageFontMap?: LanguageFontMap;
attachCertificate?: boolean;
}

export interface GeneratePdfOptionsExtended<T extends 'stream' | 'buffer'> extends GeneratePdfOptions {
Expand Down Expand Up @@ -146,13 +149,11 @@ async function getPdfMakeContentFromObject(
const schemaConfig = getSchemaConfig(refSchemaUrl);
const certificateLanguages = getCertificateLanguages(certificate);
translations ||= certificateLanguages?.length ? await getTranslations(certificateLanguages, schemaConfig) : {};

const type = getCertificateType(schemaConfig);
const externalStandards: ExternalStandards[] = schemaToExternalStandardsMap[type]
? schemaToExternalStandardsMap[type]
.map((schemaType) => get(certificate, schemaType, undefined))
.filter((externalStandards) => externalStandards) || []
: [];
const externalStandards: ExternalStandards[] =
schemaToExternalStandardsMap[type]
?.map((schemaType: keyof Schemas) => get(certificate, schemaType, undefined))
.filter(Boolean) ?? [];

extraTranslations ||=
certificateLanguages?.length && externalStandards?.length
Expand Down Expand Up @@ -203,6 +204,20 @@ async function buildPdfContent(
return pdfMakeContent;
}

async function attachCertificateToPdf(
pdfBuffer: Buffer,
certificate: Record<string, unknown> | string,
): Promise<Buffer> {
const today = new Date();
const fileContent = typeof certificate === 'string' ? certificate : JSON.stringify(certificate, null, 2);
return await attachFileToPdf(pdfBuffer, fileContent, 'certificate.json', {
mimeType: 'application/json',
description: 'The certificate in JSON format',
creationDate: today,
modificationDate: today,
});
}

export async function generatePdf(
certificateInput: Record<string, unknown> | string,
options: {
Expand All @@ -214,6 +229,7 @@ export async function generatePdf(
translations?: Translations;
extraTranslations?: ExtraTranslations;
languageFontMap?: LanguageFontMap;
attachCertificate?: boolean;
},
): Promise<Buffer>;

Expand Down Expand Up @@ -241,9 +257,15 @@ export async function generatePdf(
options: GeneratePdfOptions = {},
): Promise<Buffer | PDFKit.PDFDocument> {
const opts: GeneratePdfOptions = options ? { ...generatePdfOptions, ...options } : generatePdfOptions;

if (opts.attachCertificate && opts.outputType === 'stream') {
throw new Error('Cannot attach certificate to a stream output type. Please use buffer output type.');
}
christophbuehler marked this conversation as resolved.
Show resolved Hide resolved

if (opts.outputType !== 'stream' && opts.outputType !== 'buffer') {
throw new Error('Invalid outputType, should be one of buffer | stream');
}

const pdfMakeContent = await buildPdfContent(certificateInput, opts);
const docDefinition: TDocumentDefinitions = opts.docDefinition
? merge(baseDocDefinition(pdfMakeContent), clone(opts.docDefinition))
Expand All @@ -254,7 +276,8 @@ export async function generatePdf(
if (opts.outputType === 'stream') {
return pdfDoc;
}
return new Promise((resolve, reject) => {

const pdfBuffer = await new Promise<Buffer>((resolve, reject) => {
let buffer: Buffer = Buffer.alloc(0);
pdfDoc.on('data', (data) => {
buffer = Buffer.concat([buffer, data], buffer.length + data.length);
Expand All @@ -263,4 +286,6 @@ export async function generatePdf(
pdfDoc.on('error', reject);
pdfDoc.end();
});

return opts.attachCertificate ? await attachCertificateToPdf(pdfBuffer, certificateInput) : pdfBuffer;
}
46 changes: 43 additions & 3 deletions packages/generate-pdf/test/generate-pdf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { SchemaDirUnion, SupportedSchemas, SupportedSchemasDirMap } from '@s1sev
import { EN10168Schema, Schemas } from '@s1seven/schema-tools-types';

import { type GeneratePdfOptionsExtended, buildModule, generateInSandbox, generatePdf } from '../src';
import { attachFileToPdf } from '../src/attach-file-to-pdf';

jest.mock('../src/attach-file-to-pdf');
(attachFileToPdf as jest.Mock).mockImplementation((buf) => Promise.resolve(buf));

type PDFGenerationTestProperties = {
name: string;
Expand Down Expand Up @@ -133,11 +137,11 @@ const generatePaths = (
const expectedPdfPath = `${dirPath}/${name}.pdf`;
const translationsPath = `${dirPath}/translations.json`;
const coaCertsWithoutExtraTranslations = ['v0.0.4', 'v0.1.0'];
let generatorPath: string;
let generatorPath: string | undefined;
if (isLatestVersion) {
generatorPath = resolve(`${__dirname}/../../generate-${type.toLowerCase()}-pdf-template/dist/generateContent.cjs`);
}
let extraTranslationsPath: string;
let extraTranslationsPath: string | undefined;

if (type.toLowerCase() === SupportedSchemas.COA && !coaCertsWithoutExtraTranslations.includes(version)) {
extraTranslationsPath = `${dirPath}/extra_translations.json`;
Expand Down Expand Up @@ -205,7 +209,7 @@ const runPDFGenerationTests = (testSuite: PDFGenerationTestProperties) => {
generatorPath,
languageFontMap,
};
//

const pdfDoc = await generatePdf(validCertificate, generatePdfOptions);
const writeStream = createWriteStream(outputFilePath);
pdfDoc.pipe(writeStream);
Expand Down Expand Up @@ -248,6 +252,42 @@ const runPDFGenerationTests = (testSuite: PDFGenerationTestProperties) => {
expect(resultHash).toEqual(expectedHash);
}, 10000);

it('should attach the certificate as JSON document if `attachCertificate` is true', async () => {
const translations = JSON.parse(readFileSync(translationsPath, 'utf8'));
(attachFileToPdf as jest.Mock).mockClear();
christophbuehler marked this conversation as resolved.
Show resolved Hide resolved
await generatePdf(validCertificate, {
docDefinition: { ...docDefinition, styles },
outputType: 'buffer',
translations,
attachCertificate: true,
});
expect(attachFileToPdf).toHaveBeenCalled();
}, 10000);

it('should not attach the certificate as JSON document if `attachCertificate` is false', async () => {
const translations = JSON.parse(readFileSync(translationsPath, 'utf8'));
(attachFileToPdf as jest.Mock).mockClear();
await generatePdf(validCertificate, {
docDefinition: { ...docDefinition, styles },
outputType: 'buffer',
translations,
attachCertificate: false,
});
expect(attachFileToPdf).not.toHaveBeenCalled();
}, 10000);

it('should throw an error if `attachCertificate` is true and `outputType` is `stream`', async () => {
const translations = JSON.parse(readFileSync(translationsPath, 'utf8'));
await expect(
generatePdf(validCertificate, {
docDefinition: { ...docDefinition, styles },
outputType: 'stream',
translations,
attachCertificate: true,
} as any),
).rejects.toThrow('Cannot attach certificate to a stream output type. Please use buffer output type.');
}, 10000);

it('should render PDF certificate using certificate object and remote PDF generator script', async () => {
const outputFilePath = `./${type}-${version}-test.pdf`;
const translations = JSON.parse(readFileSync(translationsPath, 'utf8'));
Expand Down
1 change: 1 addition & 0 deletions packages/versioning/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@s1seven/schema-tools-types": "0.5.6",
"@s1seven/schema-tools-utils": "0.2.9",
"@apidevtools/json-schema-ref-parser": "^11.1.0",
"pdf-lib": "1.17.1",
"ajv": "8.12.0",
"axios": "^1.2.1",
"class-transformer": "^0.5.1",
Expand Down
Loading