Add pdf-lib dependency and implement PDF merging functionality
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces the `pdf-lib` library to facilitate PDF document manipulation. A new function, `mergePdfBuffers`, is added to handle the merging of multiple PDF buffers into a single document. Additionally, the PDF generation process is updated to support pagination, allowing for the export of each paginated page before merging. The changes enhance the PDF generation capabilities, ensuring that documents can be created with multiple pages while maintaining proper formatting and structure. Corresponding tests have been added to verify the new functionality.
This commit is contained in:
parent
bec4c8bf4c
commit
4ee5eb7d0a
@ -36,6 +36,7 @@
|
|||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"nodemailer": "*",
|
"nodemailer": "*",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"pdf-to-img": "^6.2.0",
|
"pdf-to-img": "^6.2.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"posthtml": "^0.16.7",
|
"posthtml": "^0.16.7",
|
||||||
|
|||||||
53
pnpm-lock.yaml
generated
53
pnpm-lock.yaml
generated
@ -104,6 +104,9 @@ importers:
|
|||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.1.11
|
specifier: ^3.1.11
|
||||||
version: 3.1.11
|
version: 3.1.11
|
||||||
|
pdf-lib:
|
||||||
|
specifier: ^1.17.1
|
||||||
|
version: 1.17.1
|
||||||
pdf-to-img:
|
pdf-to-img:
|
||||||
specifier: ^6.2.0
|
specifier: ^6.2.0
|
||||||
version: 6.2.0
|
version: 6.2.0
|
||||||
@ -1434,6 +1437,12 @@ packages:
|
|||||||
'@paralleldrive/cuid2@2.3.1':
|
'@paralleldrive/cuid2@2.3.1':
|
||||||
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
||||||
|
|
||||||
|
'@pdf-lib/standard-fonts@1.0.0':
|
||||||
|
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
|
||||||
|
|
||||||
|
'@pdf-lib/upng@1.0.1':
|
||||||
|
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@ -2272,8 +2281,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
chromedriver@152.0.0:
|
chromedriver@152.0.1:
|
||||||
resolution: {integrity: sha512-JBMT88W2bMGSytKcIy6A3F/0DrGJeKTO1ZSThSNICeexSY+xBAEuAvjzlsGkIMaebqNF0KkDUI203CJ+GTA12g==}
|
resolution: {integrity: sha512-8zet2yPAFOSlYxUIcQ/1yOOC1xP4ooPZiKFRdlJhFawBKvlZIHDU6qv4LDOfPEw1yXbyu+tQTzbL0Vfb+vHdQQ==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -4295,6 +4304,9 @@ packages:
|
|||||||
path-to-regexp@8.3.0:
|
path-to-regexp@8.3.0:
|
||||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||||
|
|
||||||
|
pdf-lib@1.17.1:
|
||||||
|
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
|
||||||
|
|
||||||
pdf-to-img@6.2.0:
|
pdf-to-img@6.2.0:
|
||||||
resolution: {integrity: sha512-54CaUwpTXw0SkG6U3Twx7AJNM2ZuFNpDCJ7oHmmW113gT8RwLZSbzv4/GOr+H0PFpFisuHcUirZBKgXkJXA1zg==}
|
resolution: {integrity: sha512-54CaUwpTXw0SkG6U3Twx7AJNM2ZuFNpDCJ7oHmmW113gT8RwLZSbzv4/GOr+H0PFpFisuHcUirZBKgXkJXA1zg==}
|
||||||
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||||
@ -5020,6 +5032,9 @@ packages:
|
|||||||
tsconfig-paths@3.15.0:
|
tsconfig-paths@3.15.0:
|
||||||
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
||||||
|
|
||||||
|
tslib@1.14.1:
|
||||||
|
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@ -7121,6 +7136,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
|
|
||||||
|
'@pdf-lib/standard-fonts@1.0.0':
|
||||||
|
dependencies:
|
||||||
|
pako: 1.0.11
|
||||||
|
|
||||||
|
'@pdf-lib/upng@1.0.1':
|
||||||
|
dependencies:
|
||||||
|
pako: 1.0.11
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -8146,7 +8169,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
chromedriver@152.0.0:
|
chromedriver@152.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@testim/chrome-version': 1.1.4
|
'@testim/chrome-version': 1.1.4
|
||||||
adm-zip: 0.6.0
|
adm-zip: 0.6.0
|
||||||
@ -8617,12 +8640,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.2
|
eslint: 9.39.2
|
||||||
|
|
||||||
eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1):
|
eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.37.5(eslint@9.39.2))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||||
|
|
||||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1):
|
eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@9.39.2))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
||||||
@ -9133,7 +9156,7 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
functions-have-names: 1.2.3
|
functions-have-names: 1.2.3
|
||||||
hasown: 2.0.2
|
hasown: 2.0.4
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
|
|
||||||
functions-have-names@1.2.3: {}
|
functions-have-names@1.2.3: {}
|
||||||
@ -9276,7 +9299,6 @@ snapshots:
|
|||||||
hasown@2.0.4:
|
hasown@2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
optional: true
|
|
||||||
|
|
||||||
hmac-drbg@1.0.1:
|
hmac-drbg@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9516,7 +9538,7 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
hasown: 2.0.2
|
hasown: 2.0.4
|
||||||
|
|
||||||
is-set@2.0.3: {}
|
is-set@2.0.3: {}
|
||||||
|
|
||||||
@ -10049,7 +10071,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jwk-to-pem: 2.0.7
|
jwk-to-pem: 2.0.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
chromedriver: 152.0.0
|
chromedriver: 152.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
- kerberos
|
- kerberos
|
||||||
@ -10574,6 +10596,13 @@ snapshots:
|
|||||||
|
|
||||||
path-to-regexp@8.3.0: {}
|
path-to-regexp@8.3.0: {}
|
||||||
|
|
||||||
|
pdf-lib@1.17.1:
|
||||||
|
dependencies:
|
||||||
|
'@pdf-lib/standard-fonts': 1.0.0
|
||||||
|
'@pdf-lib/upng': 1.0.1
|
||||||
|
pako: 1.0.11
|
||||||
|
tslib: 1.14.1
|
||||||
|
|
||||||
pdf-to-img@6.2.0:
|
pdf-to-img@6.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pdfjs-dist: 5.6.205
|
pdfjs-dist: 5.6.205
|
||||||
@ -11220,8 +11249,8 @@ snapshots:
|
|||||||
standard@17.1.2:
|
standard@17.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1)
|
eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@9.39.2))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1)
|
||||||
eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1)
|
eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@9.39.2))(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
||||||
eslint-plugin-n: 15.7.0(eslint@8.57.1)
|
eslint-plugin-n: 15.7.0(eslint@8.57.1)
|
||||||
eslint-plugin-promise: 6.6.0(eslint@8.57.1)
|
eslint-plugin-promise: 6.6.0(eslint@8.57.1)
|
||||||
@ -11476,6 +11505,8 @@ snapshots:
|
|||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
strip-bom: 3.0.0
|
strip-bom: 3.0.0
|
||||||
|
|
||||||
|
tslib@1.14.1: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tweetnacl@1.0.3: {}
|
tweetnacl@1.0.3: {}
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { jest } from '@jest/globals';
|
|||||||
|
|
||||||
const page = {
|
const page = {
|
||||||
setContent: jest.fn().mockResolvedValue(undefined),
|
setContent: jest.fn().mockResolvedValue(undefined),
|
||||||
evaluate: jest.fn().mockResolvedValue(120),
|
waitForFunction: jest.fn().mockResolvedValue(undefined),
|
||||||
|
evaluate: jest.fn(),
|
||||||
pdf: jest.fn().mockResolvedValue(Buffer.from('pdf-bytes')),
|
pdf: jest.fn().mockResolvedValue(Buffer.from('pdf-bytes')),
|
||||||
close: jest.fn().mockResolvedValue(undefined),
|
close: jest.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
@ -19,10 +20,28 @@ const puppeteer = {
|
|||||||
executablePath: jest.fn().mockReturnValue('/fake/puppeteer/chrome'),
|
executablePath: jest.fn().mockReturnValue('/fake/puppeteer/chrome'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copiedPage = {};
|
||||||
|
const donorPdf = {
|
||||||
|
getPageIndices: jest.fn(() => [0]),
|
||||||
|
};
|
||||||
|
const mergedPdf = {
|
||||||
|
copyPages: jest.fn(async () => [copiedPage]),
|
||||||
|
addPage: jest.fn(),
|
||||||
|
save: jest.fn(async () => new Uint8Array([9, 8, 7])),
|
||||||
|
};
|
||||||
|
const PDFDocument = {
|
||||||
|
create: jest.fn(async () => mergedPdf),
|
||||||
|
load: jest.fn(async () => donorPdf),
|
||||||
|
};
|
||||||
|
|
||||||
jest.unstable_mockModule('puppeteer', () => ({
|
jest.unstable_mockModule('puppeteer', () => ({
|
||||||
default: puppeteer,
|
default: puppeteer,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('pdf-lib', () => ({
|
||||||
|
PDFDocument,
|
||||||
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../config.js', () => ({
|
jest.unstable_mockModule('../../config.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
server: {
|
server: {
|
||||||
@ -45,12 +64,30 @@ jest.unstable_mockModule('log4js', () => ({
|
|||||||
|
|
||||||
const { generatePDF, startPDFBrowser, closePDFBrowser } = await import('../pdffactory.js');
|
const { generatePDF, startPDFBrowser, closePDFBrowser } = await import('../pdffactory.js');
|
||||||
|
|
||||||
|
function mockEvaluate({ pageCount = 1, height = 120 } = {}) {
|
||||||
|
page.evaluate.mockImplementation(async (fn) => {
|
||||||
|
const source = String(fn);
|
||||||
|
if (source.includes('getPageCount')) {
|
||||||
|
return pageCount;
|
||||||
|
}
|
||||||
|
if (source.includes('showPage')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (source.includes('measureVisiblePageHeight') || source.includes('scrollHeight')) {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
return pageCount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe('PDF Factory', () => {
|
describe('PDF Factory', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
browserInstance.connected = true;
|
browserInstance.connected = true;
|
||||||
browserInstance.newPage.mockResolvedValue(page);
|
browserInstance.newPage.mockResolvedValue(page);
|
||||||
puppeteer.launch.mockResolvedValue(browserInstance);
|
puppeteer.launch.mockResolvedValue(browserInstance);
|
||||||
|
page.pdf.mockResolvedValue(Buffer.from('pdf-bytes'));
|
||||||
|
mockEvaluate({ pageCount: 1, height: 120 });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -87,9 +124,16 @@ describe('PDF Factory', () => {
|
|||||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('measures auto height from the content element', async () => {
|
it('waits for page overflow pagination before measuring', async () => {
|
||||||
|
await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||||
|
|
||||||
|
expect(page.waitForFunction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('measures auto height from the visible page', async () => {
|
||||||
await generatePDF('<html></html>', { width: 80, height: 'auto' });
|
await generatePDF('<html></html>', { width: 80, height: 'auto' });
|
||||||
|
|
||||||
|
expect(page.waitForFunction).toHaveBeenCalled();
|
||||||
expect(page.evaluate).toHaveBeenCalled();
|
expect(page.evaluate).toHaveBeenCalled();
|
||||||
expect(page.pdf).toHaveBeenCalledWith(
|
expect(page.pdf).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -99,6 +143,23 @@ describe('PDF Factory', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('exports each paginated page then merges with pdf-lib', async () => {
|
||||||
|
mockEvaluate({ pageCount: 2, height: 120 });
|
||||||
|
page.pdf
|
||||||
|
.mockResolvedValueOnce(Buffer.from('page-one'))
|
||||||
|
.mockResolvedValueOnce(Buffer.from('page-two'));
|
||||||
|
|
||||||
|
const result = await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||||
|
|
||||||
|
expect(page.pdf).toHaveBeenCalledTimes(2);
|
||||||
|
expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function), 0);
|
||||||
|
expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function), 1);
|
||||||
|
expect(PDFDocument.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(PDFDocument.load).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mergedPdf.addPage).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result).toEqual(Buffer.from([9, 8, 7]));
|
||||||
|
});
|
||||||
|
|
||||||
it('can start the browser before the first render', async () => {
|
it('can start the browser before the first render', async () => {
|
||||||
await startPDFBrowser();
|
await startPDFBrowser();
|
||||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html
|
||||||
|
lang="en"
|
||||||
|
data-infinite-height="<%= typeof infiniteHeight !== 'undefined' && infiniteHeight ? 'true' : 'false' %>"
|
||||||
|
>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="initial-scale=1.0" />
|
<meta name="viewport" content="initial-scale=1.0" />
|
||||||
@ -17,6 +20,13 @@
|
|||||||
body {
|
body {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<% if (typeof infiniteHeight === 'undefined' || !infiniteHeight) { %>
|
||||||
|
@page {
|
||||||
|
size: <%= (pageWidth) || (width) || '50mm' %> <%= (pageHeight) || (height) || '50mm' %>;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
<% } %>
|
||||||
|
|
||||||
.previewWrapper {
|
.previewWrapper {
|
||||||
width: <%= (scaledPageWidth) || (scaledWidth) || '50mm' %>;
|
width: <%= (scaledPageWidth) || (scaledWidth) || '50mm' %>;
|
||||||
height: <%= (scaledPageHeight) || (scaledHeight) || '50mm' %>;
|
height: <%= (scaledPageHeight) || (scaledHeight) || '50mm' %>;
|
||||||
|
|||||||
@ -0,0 +1,372 @@
|
|||||||
|
(function () {
|
||||||
|
var EPSILON_PX = 1;
|
||||||
|
var MAX_PAGES = 100;
|
||||||
|
var UNSPLITTABLE = 'tr, thead, tfoot, img, svg, hr, canvas, video, picture, col, colgroup';
|
||||||
|
var IGNORED = 'script, style, link, meta, .documentPageBreak';
|
||||||
|
var PAGE_BREAK_SELECTOR = '.documentPageBreak, pagebreak';
|
||||||
|
|
||||||
|
function markPaginated() {
|
||||||
|
document.documentElement.setAttribute('data-paginated', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInfiniteHeight() {
|
||||||
|
return document.documentElement.getAttribute('data-infinite-height') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageSelector() {
|
||||||
|
if (document.querySelector('.previewDocument')) {
|
||||||
|
return '.previewDocument';
|
||||||
|
}
|
||||||
|
return '.renderDocument';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClipBottom(page) {
|
||||||
|
var rect = page.getBoundingClientRect();
|
||||||
|
var style = window.getComputedStyle(page);
|
||||||
|
return (
|
||||||
|
rect.bottom -
|
||||||
|
(parseFloat(style.borderBottomWidth) || 0) -
|
||||||
|
(parseFloat(style.paddingBottom) || 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOverflow(page) {
|
||||||
|
if (page.scrollHeight > page.clientHeight + EPSILON_PX) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var clipBottom = getClipBottom(page);
|
||||||
|
var nodes = page.querySelectorAll('*');
|
||||||
|
for (var i = 0; i < nodes.length; i += 1) {
|
||||||
|
if (isIgnored(nodes[i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isOverflowing(nodes[i], clipBottom)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIgnored(el) {
|
||||||
|
return el.matches(IGNORED);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOverflowing(el, clipBottom) {
|
||||||
|
var rect = el.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 && rect.height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return rect.bottom > clipBottom + EPSILON_PX;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPreviousContent(page, node) {
|
||||||
|
var current = node;
|
||||||
|
while (current && current !== page) {
|
||||||
|
if (current.previousElementSibling) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFirstOverflowingLeaf(page, clipBottom, locked) {
|
||||||
|
function walk(el) {
|
||||||
|
if (locked.has(el) || isIgnored(el)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isOverflowing(el, clipBottom)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!el.matches(UNSPLITTABLE) && el.children.length > 0) {
|
||||||
|
for (var i = 0; i < el.children.length; i += 1) {
|
||||||
|
var found = walk(el.children[i]);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < page.children.length; i += 1) {
|
||||||
|
var found = walk(page.children[i]);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSplitNode(page) {
|
||||||
|
var locked = new Set();
|
||||||
|
var clipBottom = getClipBottom(page);
|
||||||
|
var safety = 0;
|
||||||
|
var maxLocks = page.querySelectorAll('*').length + 10;
|
||||||
|
|
||||||
|
while (safety < maxLocks) {
|
||||||
|
safety += 1;
|
||||||
|
var splitNode = findFirstOverflowingLeaf(page, clipBottom, locked);
|
||||||
|
if (!splitNode || splitNode === page) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!hasPreviousContent(page, splitNode)) {
|
||||||
|
locked.add(splitNode);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return splitNode;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneEmpty(el) {
|
||||||
|
var clone = el.cloneNode(false);
|
||||||
|
clone.removeAttribute('id');
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveFrom(startNode, destination) {
|
||||||
|
var node = startNode;
|
||||||
|
while (node) {
|
||||||
|
var next = node.nextElementSibling;
|
||||||
|
destination.appendChild(node);
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEffectivelyEmpty(el) {
|
||||||
|
var clone = el.cloneNode(true);
|
||||||
|
var breaks = clone.querySelectorAll(PAGE_BREAK_SELECTOR);
|
||||||
|
for (var i = 0; i < breaks.length; i += 1) {
|
||||||
|
breaks[i].remove();
|
||||||
|
}
|
||||||
|
if (clone.querySelector('img, svg, canvas, video, picture, hr, table, .documentBarcode')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return String(clone.textContent || '').trim() === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveOverflowToNewPage(page, splitNode, options) {
|
||||||
|
options = options || {};
|
||||||
|
var discardSplitNode = options.discardSplitNode === true;
|
||||||
|
var newPage = cloneEmpty(page);
|
||||||
|
var ancestors = [];
|
||||||
|
var parent = splitNode.parentElement;
|
||||||
|
while (parent && parent !== page) {
|
||||||
|
ancestors.push(parent);
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clonedAncestors = [];
|
||||||
|
for (var i = 0; i < ancestors.length; i += 1) {
|
||||||
|
clonedAncestors.push(cloneEmpty(ancestors[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachPoint = newPage;
|
||||||
|
for (var i = clonedAncestors.length - 1; i >= 0; i -= 1) {
|
||||||
|
attachPoint.appendChild(clonedAncestors[i]);
|
||||||
|
attachPoint = clonedAncestors[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var inner = clonedAncestors.length > 0 ? clonedAncestors[0] : newPage;
|
||||||
|
if (discardSplitNode) {
|
||||||
|
moveFrom(splitNode.nextElementSibling, inner);
|
||||||
|
splitNode.remove();
|
||||||
|
} else {
|
||||||
|
moveFrom(splitNode, inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < ancestors.length; i += 1) {
|
||||||
|
var destination = i + 1 < clonedAncestors.length ? clonedAncestors[i + 1] : newPage;
|
||||||
|
moveFrom(ancestors[i].nextElementSibling, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEffectivelyEmpty(newPage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBeforeInDocument(a, b) {
|
||||||
|
if (!a || !b || a === b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Boolean(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitPage(page, skipOverflow) {
|
||||||
|
var pageBreak = page.querySelector(PAGE_BREAK_SELECTOR);
|
||||||
|
var overflowNode = skipOverflow ? null : findSplitNode(page);
|
||||||
|
|
||||||
|
var splitNode = null;
|
||||||
|
var discardSplitNode = false;
|
||||||
|
|
||||||
|
if (pageBreak && overflowNode) {
|
||||||
|
if (isBeforeInDocument(overflowNode, pageBreak)) {
|
||||||
|
splitNode = overflowNode;
|
||||||
|
} else {
|
||||||
|
splitNode = pageBreak;
|
||||||
|
discardSplitNode = true;
|
||||||
|
}
|
||||||
|
} else if (pageBreak) {
|
||||||
|
splitNode = pageBreak;
|
||||||
|
discardSplitNode = true;
|
||||||
|
} else if (overflowNode) {
|
||||||
|
splitNode = overflowNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!splitNode) {
|
||||||
|
if (pageBreak && pageBreak.isConnected) {
|
||||||
|
pageBreak.remove();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextPage = moveOverflowToNewPage(page, splitNode, {
|
||||||
|
discardSplitNode: discardSplitNode
|
||||||
|
});
|
||||||
|
if (!nextPage && pageBreak && pageBreak.isConnected) {
|
||||||
|
pageBreak.remove();
|
||||||
|
}
|
||||||
|
return nextPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertPageAfter(sourcePage, newPage) {
|
||||||
|
var wrapper = sourcePage.closest('.previewWrapper');
|
||||||
|
if (wrapper && wrapper.parentElement) {
|
||||||
|
var newWrapper = document.createElement('div');
|
||||||
|
newWrapper.className = wrapper.className;
|
||||||
|
newWrapper.appendChild(newPage);
|
||||||
|
wrapper.after(newWrapper);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sourcePage.after(newPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageElements() {
|
||||||
|
var renderPages = document.querySelectorAll('.renderDocument');
|
||||||
|
if (renderPages.length > 0) {
|
||||||
|
return renderPages;
|
||||||
|
}
|
||||||
|
return document.querySelectorAll('.previewDocument');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageShell(pageEl) {
|
||||||
|
return pageEl.closest('.previewWrapper') || pageEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paginationApi = {
|
||||||
|
getPageCount: function () {
|
||||||
|
return getPageElements().length || 1;
|
||||||
|
},
|
||||||
|
showPage: function (index) {
|
||||||
|
var pages = getPageElements();
|
||||||
|
for (var i = 0; i < pages.length; i += 1) {
|
||||||
|
var pageEl = pages[i];
|
||||||
|
var shell = getPageShell(pageEl);
|
||||||
|
if (i === index) {
|
||||||
|
shell.classList.remove('fcPaginationHidden');
|
||||||
|
pageEl.classList.add('fcPaginationVisible');
|
||||||
|
} else {
|
||||||
|
shell.classList.add('fcPaginationHidden');
|
||||||
|
pageEl.classList.remove('fcPaginationVisible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-active-page', String(index));
|
||||||
|
document.documentElement.setAttribute('data-page-count', String(pages.length || 1));
|
||||||
|
},
|
||||||
|
showAllPages: function () {
|
||||||
|
var pages = getPageElements();
|
||||||
|
for (var i = 0; i < pages.length; i += 1) {
|
||||||
|
var pageEl = pages[i];
|
||||||
|
var shell = getPageShell(pageEl);
|
||||||
|
shell.classList.remove('fcPaginationHidden');
|
||||||
|
pageEl.classList.remove('fcPaginationVisible');
|
||||||
|
}
|
||||||
|
document.documentElement.removeAttribute('data-active-page');
|
||||||
|
},
|
||||||
|
measureVisiblePageHeight: function () {
|
||||||
|
var pages = getPageElements();
|
||||||
|
for (var i = 0; i < pages.length; i += 1) {
|
||||||
|
var pageEl = pages[i];
|
||||||
|
var shell = getPageShell(pageEl);
|
||||||
|
if (shell.classList.contains('fcPaginationHidden')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return pageEl.scrollHeight;
|
||||||
|
}
|
||||||
|
var fallback = document.getElementById('content');
|
||||||
|
return fallback ? fallback.scrollHeight : 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__fcDocumentPagination = paginationApi;
|
||||||
|
|
||||||
|
function paginate() {
|
||||||
|
var selector = getPageSelector();
|
||||||
|
var page = document.querySelector(selector);
|
||||||
|
if (!page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var skipOverflow = isInfiniteHeight();
|
||||||
|
var guard = 0;
|
||||||
|
while (page && guard < MAX_PAGES) {
|
||||||
|
guard += 1;
|
||||||
|
var nextPage = splitPage(page, skipOverflow);
|
||||||
|
if (!nextPage) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
insertPageAfter(page, nextPage);
|
||||||
|
page = nextPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
'data-page-count',
|
||||||
|
String(paginationApi.getPageCount())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForImages() {
|
||||||
|
var images = Array.prototype.slice.call(document.images || []);
|
||||||
|
return Promise.all(
|
||||||
|
images.map(function (img) {
|
||||||
|
if (img.complete) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
img.addEventListener('load', resolve, { once: true });
|
||||||
|
img.addEventListener('error', resolve, { once: true });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
try {
|
||||||
|
paginate();
|
||||||
|
} finally {
|
||||||
|
markPaginated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitAndStart() {
|
||||||
|
var fontsReady =
|
||||||
|
document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
|
||||||
|
Promise.all([Promise.resolve(fontsReady).catch(function () {}), waitForImages()]).then(
|
||||||
|
function () {
|
||||||
|
window.requestAnimationFrame(function () {
|
||||||
|
window.requestAnimationFrame(start);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', waitAndStart);
|
||||||
|
} else {
|
||||||
|
waitAndStart();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@ -1,5 +1,7 @@
|
|||||||
<div class="previewContainer">
|
<div class="previewContainer">
|
||||||
|
<div class="previewPages" id="pages">
|
||||||
<div class="previewWrapper">
|
<div class="previewWrapper">
|
||||||
<div class="previewDocument" id="content"><%- content %></div>
|
<div class="previewDocument documentPage" id="content"><%- content %></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
<div class="renderDocument" id="content"><%- content %></div>
|
<div class="renderPages" id="pages">
|
||||||
|
<div class="renderDocument documentPage" id="content"><%- content %></div>
|
||||||
|
</div>
|
||||||
|
|||||||
@ -15,8 +15,16 @@ body {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
.previewPages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 32px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
.previewWrapper {
|
.previewWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.previewDocument {
|
.previewDocument {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@ -28,8 +36,30 @@ body {
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.renderPages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.renderDocument {
|
.renderDocument {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
overflow: hidden;
|
||||||
|
break-after: page;
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderDocument:last-child {
|
||||||
|
break-after: auto;
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fcPaginationHidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fcPaginationVisible {
|
||||||
|
break-after: auto !important;
|
||||||
|
page-break-after: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.documentText {
|
.documentText {
|
||||||
@ -66,6 +96,10 @@ h4.documentTitle {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.documentPageBreak {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.contentPlaceholder {
|
.contentPlaceholder {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
import config from '../config.js';
|
import config from '../config.js';
|
||||||
import { getPuppeteerLaunchOptions } from '../puppeteerLaunchOptions.js';
|
import { getPuppeteerLaunchOptions } from '../puppeteerLaunchOptions.js';
|
||||||
|
|
||||||
@ -100,6 +101,75 @@ async function withPage(run) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getPaginationPageCount(page) {
|
||||||
|
const pageCount = await page.evaluate(() => {
|
||||||
|
const api = window.__fcDocumentPagination;
|
||||||
|
if (!api || typeof api.getPageCount !== 'function') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return api.getPageCount() || 1;
|
||||||
|
});
|
||||||
|
return Math.max(1, Number(pageCount) || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showPaginationPage(page, index) {
|
||||||
|
await page.evaluate((pageIndex) => {
|
||||||
|
const api = window.__fcDocumentPagination;
|
||||||
|
if (api && typeof api.showPage === 'function') {
|
||||||
|
api.showPage(pageIndex);
|
||||||
|
}
|
||||||
|
}, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureVisiblePageHeight(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const api = window.__fcDocumentPagination;
|
||||||
|
if (api && typeof api.measureVisiblePageHeight === 'function') {
|
||||||
|
return api.measureVisiblePageHeight();
|
||||||
|
}
|
||||||
|
const content = document.getElementById('content');
|
||||||
|
return content ? content.scrollHeight : 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPdfOptions(options, height, isAutoHeight, isolatePage) {
|
||||||
|
return {
|
||||||
|
printBackground: true,
|
||||||
|
preferCSSPageSize: isolatePage !== true && isAutoHeight !== true,
|
||||||
|
width: options.width ? `${options.width}mm` : undefined,
|
||||||
|
height: height
|
||||||
|
? isAutoHeight
|
||||||
|
? `${height + 2}px`
|
||||||
|
: `${height}mm`
|
||||||
|
: undefined,
|
||||||
|
margin: {
|
||||||
|
top: '0mm',
|
||||||
|
right: '0mm',
|
||||||
|
bottom: '0mm',
|
||||||
|
left: '0mm',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function mergePdfBuffers(buffers = []) {
|
||||||
|
if (!Array.isArray(buffers) || buffers.length === 0) {
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
if (buffers.length === 1) {
|
||||||
|
return Buffer.isBuffer(buffers[0]) ? buffers[0] : Buffer.from(buffers[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = await PDFDocument.create();
|
||||||
|
for (const bytes of buffers) {
|
||||||
|
const donor = await PDFDocument.load(bytes);
|
||||||
|
const copiedPages = await merged.copyPages(donor, donor.getPageIndices());
|
||||||
|
for (const copiedPage of copiedPages) {
|
||||||
|
merged.addPage(copiedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Buffer.from(await merged.save());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a PDF from HTML content using a persistent Puppeteer browser
|
* Generates a PDF from HTML content using a persistent Puppeteer browser
|
||||||
* @param {string} html - The HTML content to convert to PDF
|
* @param {string} html - The HTML content to convert to PDF
|
||||||
@ -115,28 +185,34 @@ export async function generatePDF(html, options = {}) {
|
|||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
});
|
});
|
||||||
|
|
||||||
var height = options?.height || '50';
|
await page
|
||||||
|
.waitForFunction(
|
||||||
|
() => document.documentElement.getAttribute('data-paginated') === 'true',
|
||||||
|
{ timeout: 15000 }
|
||||||
|
)
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
if (options.height == 'auto') {
|
const pageCount = await getPaginationPageCount(page);
|
||||||
const calculatedHeight = await page.evaluate(() => {
|
const isAutoHeight = options.height == 'auto' || options.infiniteHeight == true;
|
||||||
return document.getElementById('content').scrollHeight;
|
const isolatePages = pageCount > 1;
|
||||||
});
|
const pdfBuffers = [];
|
||||||
|
|
||||||
height = calculatedHeight;
|
for (let index = 0; index < pageCount; index += 1) {
|
||||||
|
if (isolatePages) {
|
||||||
|
await showPaginationPage(page, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.pdf({
|
var height = options?.height || '50';
|
||||||
printBackground: true,
|
if (isAutoHeight) {
|
||||||
preferCSSPageSize: true,
|
height = await measureVisiblePageHeight(page);
|
||||||
width: options.width ? `${options.width}mm` : undefined,
|
}
|
||||||
height: height ? (options.height == 'auto' ? `${height + 2}px` : `${height}mm`) : undefined,
|
|
||||||
margin: {
|
pdfBuffers.push(
|
||||||
top: '0mm',
|
await page.pdf(buildPdfOptions(options, height, isAutoHeight, isolatePages))
|
||||||
right: '0mm',
|
);
|
||||||
bottom: '0mm',
|
}
|
||||||
left: '0mm',
|
|
||||||
},
|
return mergePdfBuffers(pdfBuffers);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error generating PDF:', error.message);
|
logger.error('Error generating PDF:', error.message);
|
||||||
|
|||||||
@ -218,6 +218,13 @@ async function transformCustomElements(content) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
(tree) =>
|
||||||
|
tree.match([{ tag: 'PageBreak' }, { tag: 'pagebreak' }], () => ({
|
||||||
|
tag: 'hr',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentPageBreak',
|
||||||
|
},
|
||||||
|
})),
|
||||||
(tree) =>
|
(tree) =>
|
||||||
tree.match({ tag: 'ProgressBar' }, (node) => {
|
tree.match({ tag: 'ProgressBar' }, (node) => {
|
||||||
return {
|
return {
|
||||||
@ -332,7 +339,7 @@ async function transformCustomElements(content) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
]).process(content);
|
]).process(content, { recognizeSelfClosing: true });
|
||||||
|
|
||||||
return result.html;
|
return result.html;
|
||||||
}
|
}
|
||||||
@ -526,7 +533,10 @@ export class TemplateManager {
|
|||||||
paddingLeft: `${padding.paddingLeft}mm`,
|
paddingLeft: `${padding.paddingLeft}mm`,
|
||||||
scale: `${scale}`,
|
scale: `${scale}`,
|
||||||
baseCSS: baseCSS,
|
baseCSS: baseCSS,
|
||||||
previewPaginationScript: preview ? previewPaginationScript : '',
|
infiniteHeight: infiniteHeight,
|
||||||
|
pageWidth: `${pageWidth}mm`,
|
||||||
|
pageHeight: infiniteHeight ? 'auto' : `${pageHeight}mm`,
|
||||||
|
previewPaginationScript: previewPaginationScript,
|
||||||
},
|
},
|
||||||
defaultOptions
|
defaultOptions
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user