Add PDF generation and document template management features
This update introduces a comprehensive PDF generation system using Puppeteer, allowing for the rendering and downloading of document templates in various formats. Key features include the addition of preview and download routes for document templates, integration of EJS for template rendering, and utilities for converting PDFs to images. The new `TemplateManager` class manages template loading and rendering, while the `pdffactory.js` handles PDF generation. Additionally, several assets and styles for document templates have been added to enhance the visual presentation. This enhancement significantly improves the document management capabilities of the application.
This commit is contained in:
parent
5a4770820a
commit
0fe1714e1a
@ -13,8 +13,10 @@
|
||||
"body-parser": "^2.2.0",
|
||||
"canonical-json": "^0.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.23",
|
||||
"diff": "^9.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"ejs": "^3.1.10",
|
||||
"exceljs": "^4.4.0",
|
||||
"exifr": "^7.1.3",
|
||||
"express": "^5.1.0",
|
||||
@ -33,7 +35,9 @@
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "*",
|
||||
"nodemon": "^3.1.11",
|
||||
"pdf-to-img": "^6.2.0",
|
||||
"pg": "^8.16.3",
|
||||
"posthtml": "^0.16.7",
|
||||
"puppeteer": "^24.37.5",
|
||||
"sequelize": "^6.37.7",
|
||||
"sharp": "^0.34.3",
|
||||
|
||||
321
pnpm-lock.yaml
generated
321
pnpm-lock.yaml
generated
@ -35,12 +35,18 @@ importers:
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.6
|
||||
dayjs:
|
||||
specifier: ^1.11.23
|
||||
version: 1.11.23
|
||||
diff:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
ejs:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.10
|
||||
exceljs:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@ -95,9 +101,15 @@ importers:
|
||||
nodemon:
|
||||
specifier: ^3.1.11
|
||||
version: 3.1.11
|
||||
pdf-to-img:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0
|
||||
pg:
|
||||
specifier: ^8.16.3
|
||||
version: 8.18.0
|
||||
posthtml:
|
||||
specifier: ^0.16.7
|
||||
version: 0.16.7
|
||||
puppeteer:
|
||||
specifier: ^24.37.5
|
||||
version: 24.37.5
|
||||
@ -1303,6 +1315,76 @@ packages:
|
||||
'@mongodb-js/saslprep@1.4.5':
|
||||
resolution: {integrity: sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@ -1781,9 +1863,9 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
adm-zip@0.5.18:
|
||||
resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==}
|
||||
engines: {node: '>=12.0'}
|
||||
adm-zip@0.6.0:
|
||||
resolution: {integrity: sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
@ -1916,8 +1998,8 @@ packages:
|
||||
axios@1.13.4:
|
||||
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
|
||||
|
||||
axios@1.18.1:
|
||||
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
|
||||
axios@1.19.0:
|
||||
resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==}
|
||||
|
||||
b4a@1.8.0:
|
||||
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
|
||||
@ -2172,8 +2254,8 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
chromedriver@151.0.2:
|
||||
resolution: {integrity: sha512-p2NI0qrBSpoksa9S4lxqpML28I1U1oMtNPyvbjiMlBB+JOn9qRQC/cuiAWPf36lAdsYgrqhQczb12UH2he04MQ==}
|
||||
chromedriver@152.0.0:
|
||||
resolution: {integrity: sha512-JBMT88W2bMGSytKcIy6A3F/0DrGJeKTO1ZSThSNICeexSY+xBAEuAvjzlsGkIMaebqNF0KkDUI203CJ+GTA12g==}
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
|
||||
@ -2346,8 +2428,8 @@ packages:
|
||||
resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
dayjs@1.11.19:
|
||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||
dayjs@1.11.23:
|
||||
resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
@ -2454,6 +2536,19 @@ packages:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domhandler@4.3.1:
|
||||
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domutils@2.8.0:
|
||||
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
||||
|
||||
dotenv@17.2.3:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
@ -2482,6 +2577,11 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
ejs@3.1.10:
|
||||
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
hasBin: true
|
||||
|
||||
electron-to-chromium@1.5.283:
|
||||
resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==}
|
||||
|
||||
@ -2505,6 +2605,13 @@ packages:
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
entities@2.2.0:
|
||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||
|
||||
entities@3.0.1:
|
||||
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2821,6 +2928,9 @@ packages:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
filelist@1.0.6:
|
||||
resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
@ -2886,6 +2996,10 @@ packages:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
form-data@4.0.6:
|
||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formidable@3.5.4:
|
||||
resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -3055,6 +3169,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.4:
|
||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
|
||||
|
||||
@ -3065,6 +3183,9 @@ packages:
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
htmlparser2@7.2.0:
|
||||
resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -3228,6 +3349,9 @@ packages:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-json@2.0.1:
|
||||
resolution: {integrity: sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==}
|
||||
|
||||
is-map@2.0.3:
|
||||
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3342,6 +3466,11 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jake@10.9.4:
|
||||
resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
jest-changed-files@30.2.0:
|
||||
resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@ -3924,6 +4053,9 @@ packages:
|
||||
node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
@ -4102,6 +4234,15 @@ packages:
|
||||
path-to-regexp@8.3.0:
|
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||
|
||||
pdf-to-img@6.2.0:
|
||||
resolution: {integrity: sha512-54CaUwpTXw0SkG6U3Twx7AJNM2ZuFNpDCJ7oHmmW113gT8RwLZSbzv4/GOr+H0PFpFisuHcUirZBKgXkJXA1zg==}
|
||||
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||
hasBin: true
|
||||
|
||||
pdfjs-dist@5.6.205:
|
||||
resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==}
|
||||
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
@ -4190,6 +4331,18 @@ packages:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
posthtml-parser@0.11.0:
|
||||
resolution: {integrity: sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
posthtml-render@3.0.0:
|
||||
resolution: {integrity: sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
posthtml@0.16.7:
|
||||
resolution: {integrity: sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -6810,6 +6963,54 @@ snapshots:
|
||||
dependencies:
|
||||
sparse-bitfield: 3.0.3
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.100
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.100
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.100
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.100
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.8.1
|
||||
@ -7387,7 +7588,7 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
adm-zip@0.5.18:
|
||||
adm-zip@0.6.0:
|
||||
optional: true
|
||||
|
||||
agent-base@6.0.2:
|
||||
@ -7585,10 +7786,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axios@1.18.1:
|
||||
axios@1.19.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.5
|
||||
form-data: 4.0.6
|
||||
https-proxy-agent: 5.0.1
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@ -7874,11 +8075,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
chromedriver@151.0.2:
|
||||
chromedriver@152.0.0:
|
||||
dependencies:
|
||||
'@testim/chrome-version': 1.1.4
|
||||
adm-zip: 0.5.18
|
||||
axios: 1.18.1
|
||||
adm-zip: 0.6.0
|
||||
axios: 1.19.0
|
||||
compare-versions: 6.1.1
|
||||
proxy-agent: 8.0.2
|
||||
proxy-from-env: 2.1.0
|
||||
@ -8048,7 +8249,7 @@ snapshots:
|
||||
|
||||
date-format@4.0.14: {}
|
||||
|
||||
dayjs@1.11.19: {}
|
||||
dayjs@1.11.23: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
@ -8128,6 +8329,24 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
entities: 2.2.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domhandler@4.3.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domutils@2.8.0:
|
||||
dependencies:
|
||||
dom-serializer: 1.4.1
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
dottie@2.0.6: {}
|
||||
@ -8157,6 +8376,10 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
ejs@3.1.10:
|
||||
dependencies:
|
||||
jake: 10.9.4
|
||||
|
||||
electron-to-chromium@1.5.283: {}
|
||||
|
||||
elliptic@6.6.1:
|
||||
@ -8181,6 +8404,10 @@ snapshots:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
entities@2.2.0: {}
|
||||
|
||||
entities@3.0.1: {}
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
@ -8565,7 +8792,7 @@ snapshots:
|
||||
exceljs@4.4.0:
|
||||
dependencies:
|
||||
archiver: 5.3.2
|
||||
dayjs: 1.11.19
|
||||
dayjs: 1.11.23
|
||||
fast-csv: 4.3.6
|
||||
jszip: 3.10.1
|
||||
readable-stream: 3.6.2
|
||||
@ -8696,6 +8923,10 @@ snapshots:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
filelist@1.0.6:
|
||||
dependencies:
|
||||
minimatch: 5.1.9
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
@ -8766,6 +8997,15 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@4.0.6:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.4
|
||||
mime-types: 2.1.35
|
||||
optional: true
|
||||
|
||||
formidable@3.5.4:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2': 2.3.1
|
||||
@ -8947,6 +9187,11 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hasown@2.0.4:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
optional: true
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
dependencies:
|
||||
hash.js: 1.1.7
|
||||
@ -8959,6 +9204,13 @@ snapshots:
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
htmlparser2@7.2.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
domutils: 2.8.0
|
||||
entities: 3.0.1
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@ -9152,6 +9404,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-json@2.0.1: {}
|
||||
|
||||
is-map@2.0.3: {}
|
||||
|
||||
is-negative-zero@2.0.3: {}
|
||||
@ -9276,6 +9530,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jake@10.9.4:
|
||||
dependencies:
|
||||
async: 3.2.6
|
||||
filelist: 1.0.6
|
||||
picocolors: 1.1.1
|
||||
|
||||
jest-changed-files@30.2.0:
|
||||
dependencies:
|
||||
execa: 5.1.1
|
||||
@ -9693,7 +9953,7 @@ snapshots:
|
||||
dependencies:
|
||||
jwk-to-pem: 2.0.7
|
||||
optionalDependencies:
|
||||
chromedriver: 151.0.2
|
||||
chromedriver: 152.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- kerberos
|
||||
@ -9987,6 +10247,9 @@ snapshots:
|
||||
|
||||
node-int64@0.4.0: {}
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
optional: true
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
nodemailer@7.0.13: {}
|
||||
@ -10198,6 +10461,15 @@ snapshots:
|
||||
|
||||
path-to-regexp@8.3.0: {}
|
||||
|
||||
pdf-to-img@6.2.0:
|
||||
dependencies:
|
||||
pdfjs-dist: 5.6.205
|
||||
|
||||
pdfjs-dist@5.6.205:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.100
|
||||
node-readable-to-web-readable-stream: 0.4.2
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
@ -10270,6 +10542,19 @@ snapshots:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
posthtml-parser@0.11.0:
|
||||
dependencies:
|
||||
htmlparser2: 7.2.0
|
||||
|
||||
posthtml-render@3.0.0:
|
||||
dependencies:
|
||||
is-json: 2.0.1
|
||||
|
||||
posthtml@0.16.7:
|
||||
dependencies:
|
||||
posthtml-parser: 0.11.0
|
||||
posthtml-render: 3.0.0
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-linter-helpers@1.0.1:
|
||||
|
||||
@ -72,6 +72,7 @@ import { populateUserMiddleware } from './services/misc/auth.js';
|
||||
import { natsServer } from './database/nats.js';
|
||||
import { initializeBuckets } from './database/ceph.js';
|
||||
import { getEnvironment } from './config.js';
|
||||
import { startPDFBrowser } from './templates/pdffactory.js';
|
||||
|
||||
const PORT = config.server.port;
|
||||
const app = express();
|
||||
@ -125,6 +126,12 @@ async function initializeApp() {
|
||||
// Don't throw error - allow app to start without Ceph for development
|
||||
}
|
||||
|
||||
try {
|
||||
await startPDFBrowser();
|
||||
} catch (err) {
|
||||
logger.error('Failed to start persistent Puppeteer browser:', err);
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => logger.info(`Server listening to port ${PORT}`));
|
||||
logger.info(`Allowed origins: ${allowedOrigins.join(', ')}`);
|
||||
|
||||
@ -41,6 +41,8 @@ import {
|
||||
getDocumentTemplatePropertyValuesRouteHandler,
|
||||
searchDocumentTemplatesRouteHandler,
|
||||
getDocumentTemplateNeighborsRouteHandler,
|
||||
previewDocumentTemplateRouteHandler,
|
||||
downloadDocumentTemplateRouteHandler,
|
||||
} from '../../services/management/documenttemplates.js';
|
||||
|
||||
// list of document templates
|
||||
@ -90,6 +92,14 @@ router.get('/neighbors', isAuthenticated, async (req, res) => {
|
||||
getDocumentTemplateNeighborsRouteHandler(req, res, property, filter, search, getSort(sort, listAllowedSorters), order, id);
|
||||
});
|
||||
|
||||
router.post('/:id/preview', isAuthenticated, async (req, res) => {
|
||||
previewDocumentTemplateRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.post('/:id/download', isAuthenticated, async (req, res) => {
|
||||
downloadDocumentTemplateRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.get('/:id', isAuthenticated, async (req, res) => {
|
||||
getDocumentTemplateRouteHandler(req, res);
|
||||
});
|
||||
|
||||
@ -18,6 +18,13 @@ jest.unstable_mockModule('../../../database/schemas/management/documenttemplate.
|
||||
documentTemplateModel: { modelName: 'DocumentTemplate' },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../../templates/templatemanager.js', () => ({
|
||||
templateManager: {
|
||||
renderTemplate: jest.fn(),
|
||||
renderDownload: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('log4js', () => ({
|
||||
default: {
|
||||
getLogger: () => ({
|
||||
@ -35,6 +42,8 @@ const {
|
||||
getDocumentTemplateRouteHandler,
|
||||
newDocumentTemplateRouteHandler,
|
||||
editDocumentTemplateRouteHandler,
|
||||
previewDocumentTemplateRouteHandler,
|
||||
downloadDocumentTemplateRouteHandler,
|
||||
} = await import('../documenttemplates.js');
|
||||
|
||||
const { listObjects, getObject, editObject, newObject } = await import(
|
||||
@ -43,6 +52,7 @@ const { listObjects, getObject, editObject, newObject } = await import(
|
||||
const { documentTemplateModel } = await import(
|
||||
'../../../database/schemas/management/documenttemplate.schema.js'
|
||||
);
|
||||
const { templateManager } = await import('../../../templates/templatemanager.js');
|
||||
|
||||
describe('Document Template Service Route Handlers', () => {
|
||||
let req, res;
|
||||
@ -57,6 +67,7 @@ describe('Document Template Service Route Handlers', () => {
|
||||
res = {
|
||||
send: jest.fn(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
set: jest.fn(),
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@ -87,5 +98,91 @@ describe('Document Template Service Route Handlers', () => {
|
||||
expect(res.send).toHaveBeenCalledWith(mockTemplate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('previewDocumentTemplateRouteHandler', () => {
|
||||
it('should preview a document template', async () => {
|
||||
req.params.id = 'template-1';
|
||||
req.body = { content: '<Text>Hello</Text>', testObject: { name: 'Test' }, scale: 1 };
|
||||
const mockPreview = { html: '<html></html>', width: 100, height: 50 };
|
||||
templateManager.renderTemplate.mockResolvedValue(mockPreview);
|
||||
|
||||
await previewDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'template-1',
|
||||
'<Text>Hello</Text>',
|
||||
{ name: 'Test' },
|
||||
1,
|
||||
{},
|
||||
true
|
||||
);
|
||||
expect(res.send).toHaveBeenCalledWith(mockPreview);
|
||||
});
|
||||
|
||||
it('should return an error when preview fails', async () => {
|
||||
req.params.id = 'template-1';
|
||||
templateManager.renderTemplate.mockResolvedValue({
|
||||
error: 'Document template not found.',
|
||||
code: 404,
|
||||
});
|
||||
|
||||
await previewDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ error: 'Document template not found.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadDocumentTemplateRouteHandler', () => {
|
||||
it('should download a single PDF', async () => {
|
||||
req.params.id = 'template-1';
|
||||
req.query = { type: 'pdf' };
|
||||
req.body = { content: '<Text>Hello</Text>', object: { name: 'Test' } };
|
||||
const pdf = Buffer.from('pdf-data');
|
||||
templateManager.renderDownload.mockResolvedValue({
|
||||
type: 'pdf',
|
||||
mime: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
buffers: [pdf],
|
||||
});
|
||||
|
||||
await downloadDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(templateManager.renderDownload).toHaveBeenCalledWith(
|
||||
'template-1',
|
||||
'<Text>Hello</Text>',
|
||||
{ name: 'Test' },
|
||||
'pdf',
|
||||
{}
|
||||
);
|
||||
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/pdf');
|
||||
expect(res.send).toHaveBeenCalledWith(pdf);
|
||||
});
|
||||
|
||||
it('should return base64 images when multiple pages are rendered', async () => {
|
||||
req.params.id = 'template-1';
|
||||
req.query = { type: 'png' };
|
||||
req.body = { object: { name: 'Test' } };
|
||||
const pageOne = Buffer.from('page-1');
|
||||
const pageTwo = Buffer.from('page-2');
|
||||
templateManager.renderDownload.mockResolvedValue({
|
||||
type: 'png',
|
||||
mime: 'image/png',
|
||||
extension: 'png',
|
||||
buffers: [pageOne, pageTwo],
|
||||
});
|
||||
|
||||
await downloadDocumentTemplateRouteHandler(req, res);
|
||||
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
type: 'png',
|
||||
mime: 'image/png',
|
||||
extension: 'png',
|
||||
images: [pageOne.toString('base64'), pageTwo.toString('base64')],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
getPropertyValues,
|
||||
getObjectNeighbors,
|
||||
} from '../../database/database.js';
|
||||
import { templateManager } from '../../templates/templatemanager.js';
|
||||
const logger = log4js.getLogger('Document Templates');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
@ -262,3 +263,73 @@ export const getDocumentTemplateNeighborsRouteHandler = async (
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const previewDocumentTemplateRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const content = req.body?.content;
|
||||
const testObject = req.body?.testObject || req.body?.object || {};
|
||||
const scale = req.body?.scale ?? 1;
|
||||
|
||||
logger.debug(`Previewing document template with ID: ${id}`);
|
||||
|
||||
const result = await templateManager.renderTemplate(
|
||||
id,
|
||||
content,
|
||||
testObject,
|
||||
scale,
|
||||
{},
|
||||
true
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
logger.warn('Error previewing document template:', result.error);
|
||||
return res.status(result.code || 400).send(result);
|
||||
}
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const downloadDocumentTemplateRouteHandler = async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const type = req.query.type || req.body?.type || 'pdf';
|
||||
const content = req.body?.content;
|
||||
const data = req.body?.object || req.body?.testObject || {};
|
||||
const width = req.body?.width || req.query.width;
|
||||
const filename = req.body?.filename || req.query.filename || 'document';
|
||||
const options = {};
|
||||
if (width) {
|
||||
options.width = Number(width);
|
||||
}
|
||||
|
||||
logger.debug(`Downloading document template ${id} as ${type}`);
|
||||
|
||||
const result = await templateManager.renderDownload(id, content, data, type, options);
|
||||
|
||||
if (result?.error) {
|
||||
logger.warn('Error downloading document template:', result.error);
|
||||
return res.status(result.code || 400).send(result);
|
||||
}
|
||||
|
||||
const buffers = result.buffers || [];
|
||||
if (buffers.length === 0) {
|
||||
return res.status(500).send({ error: 'Failed to render document template.' });
|
||||
}
|
||||
|
||||
if (buffers.length === 1) {
|
||||
const buffer = buffers[0];
|
||||
res.set('Content-Type', result.mime);
|
||||
res.set(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${filename}.${result.extension}"`
|
||||
);
|
||||
res.set('Content-Length', String(buffer.length));
|
||||
return res.send(buffer);
|
||||
}
|
||||
|
||||
res.send({
|
||||
type: result.type,
|
||||
mime: result.mime,
|
||||
extension: result.extension,
|
||||
images: buffers.map((buffer) => Buffer.from(buffer).toString('base64')),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
108
src/templates/__tests__/pdffactory.test.js
Normal file
108
src/templates/__tests__/pdffactory.test.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
const page = {
|
||||
setContent: jest.fn().mockResolvedValue(undefined),
|
||||
evaluate: jest.fn().mockResolvedValue(120),
|
||||
pdf: jest.fn().mockResolvedValue(Buffer.from('pdf-bytes')),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const browserInstance = {
|
||||
connected: true,
|
||||
newPage: jest.fn().mockResolvedValue(page),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
on: jest.fn(),
|
||||
};
|
||||
|
||||
const puppeteer = {
|
||||
launch: jest.fn().mockResolvedValue(browserInstance),
|
||||
};
|
||||
|
||||
jest.unstable_mockModule('puppeteer', () => ({
|
||||
default: puppeteer,
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../config.js', () => ({
|
||||
default: {
|
||||
server: {
|
||||
logLevel: 'info',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('log4js', () => ({
|
||||
default: {
|
||||
getLogger: () => ({
|
||||
level: 'info',
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const { generatePDF, startPDFBrowser, closePDFBrowser } = await import('../pdffactory.js');
|
||||
|
||||
describe('PDF Factory', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
browserInstance.connected = true;
|
||||
browserInstance.newPage.mockResolvedValue(page);
|
||||
puppeteer.launch.mockResolvedValue(browserInstance);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePDFBrowser();
|
||||
});
|
||||
|
||||
it('launches puppeteer once and reuses it across renders', async () => {
|
||||
await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
expect(browserInstance.newPage).toHaveBeenCalledTimes(2);
|
||||
expect(page.close).toHaveBeenCalledTimes(2);
|
||||
expect(browserInstance.close).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('shares a single in-flight launch across concurrent renders', async () => {
|
||||
let resolveLaunch;
|
||||
puppeteer.launch.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveLaunch = () => resolve(browserInstance);
|
||||
})
|
||||
);
|
||||
|
||||
const first = generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
const second = generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
|
||||
await Promise.resolve();
|
||||
resolveLaunch();
|
||||
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('measures auto height from the content element', async () => {
|
||||
await generatePDF('<html></html>', { width: 80, height: 'auto' });
|
||||
|
||||
expect(page.evaluate).toHaveBeenCalled();
|
||||
expect(page.pdf).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
width: '80mm',
|
||||
height: '120px',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('can start the browser before the first render', async () => {
|
||||
await startPDFBrowser();
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
|
||||
await generatePDF('<html></html>', { width: 100, height: 50 });
|
||||
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
193
src/templates/__tests__/templatemanager.test.js
Normal file
193
src/templates/__tests__/templatemanager.test.js
Normal file
@ -0,0 +1,193 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
jest.unstable_mockModule('fs', () => ({
|
||||
default: {
|
||||
readFileSync: jest.fn((filePath) => {
|
||||
if (filePath.endsWith('basetemplate.ejs')) return '<html><%- content %></html>';
|
||||
if (filePath.endsWith('styles.css')) return 'body { color: red; }';
|
||||
if (filePath.endsWith('previewtemplate.ejs'))
|
||||
return '<div class="preview"><%- content %></div>';
|
||||
if (filePath.endsWith('rendertemplate.ejs'))
|
||||
return '<div class="render"><%- content %></div>';
|
||||
if (filePath.endsWith('contentplaceholder.ejs'))
|
||||
return '<div class="placeholder"></div>';
|
||||
if (filePath.endsWith('previewpagination.js')) return 'console.log("pagination");';
|
||||
return '';
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('ejs', () => ({
|
||||
default: {
|
||||
render: jest.fn(async (content) => `rendered: ${content}`),
|
||||
compile: jest.fn(() => jest.fn()),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('posthtml', () => ({
|
||||
default: jest.fn(() => ({
|
||||
process: jest.fn(async (content) => ({ html: `transformed: ${content}` })),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../database/database.js', () => ({
|
||||
getObject: jest.fn(),
|
||||
listObjects: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../database/schemas/management/documenttemplate.schema.js', () => ({
|
||||
documentTemplateModel: { modelName: 'DocumentTemplate' },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../database/schemas/management/documentsize.schema.js', () => ({}));
|
||||
|
||||
jest.unstable_mockModule('../../services/misc/model.js', () => ({
|
||||
getModelByName: jest.fn(() => ({
|
||||
model: {
|
||||
schema: {
|
||||
obj: { name: {}, status: {} },
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../pdffactory.js', () => ({
|
||||
generatePDF: jest.fn().mockResolvedValue(Buffer.from('pdf-data')),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../pdfUtils.js', () => ({
|
||||
convertPDFToImage: jest.fn().mockResolvedValue([Buffer.from('image-data')]),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('sharp', () => {
|
||||
const sharpInstance = {
|
||||
png: jest.fn().mockReturnThis(),
|
||||
jpeg: jest.fn().mockReturnThis(),
|
||||
toBuffer: jest.fn().mockResolvedValue(Buffer.from('converted-image')),
|
||||
metadata: jest.fn().mockResolvedValue({ width: 100, height: 50 }),
|
||||
};
|
||||
const sharp = jest.fn(() => sharpInstance);
|
||||
return { default: sharp };
|
||||
});
|
||||
|
||||
jest.unstable_mockModule('log4js', () => ({
|
||||
default: {
|
||||
getLogger: () => ({
|
||||
level: 'info',
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
info: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../../config.js', () => ({
|
||||
default: {
|
||||
server: {
|
||||
logLevel: 'info',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const { TemplateManager } = await import('../templatemanager.js');
|
||||
const { getObject } = await import('../../database/database.js');
|
||||
const ejs = (await import('ejs')).default;
|
||||
const { generatePDF } = await import('../pdffactory.js');
|
||||
|
||||
describe('TemplateManager', () => {
|
||||
let templateManager;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
templateManager = new TemplateManager();
|
||||
});
|
||||
|
||||
describe('renderTemplate', () => {
|
||||
it('should render a template successfully', async () => {
|
||||
const mockTemplate = {
|
||||
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||
global: false,
|
||||
objectType: 'printer',
|
||||
};
|
||||
|
||||
getObject.mockResolvedValue(mockTemplate);
|
||||
|
||||
const result = await templateManager.renderTemplate('temp-id', 'some content', {
|
||||
name: 'Test',
|
||||
});
|
||||
|
||||
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: 'temp-id' }));
|
||||
expect(ejs.render).toHaveBeenCalled();
|
||||
expect(result).toHaveProperty('html');
|
||||
expect(result.width).toBe(100);
|
||||
expect(result.height).toBe(100);
|
||||
});
|
||||
|
||||
it('should return error if template not found', async () => {
|
||||
getObject.mockResolvedValue(null);
|
||||
|
||||
const result = await templateManager.renderTemplate('invalid', 'content');
|
||||
|
||||
expect(result).toEqual({ error: 'Document template not found.', code: 404 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTemplate', () => {
|
||||
it('should return true for valid EJS', () => {
|
||||
expect(templateManager.validateTemplate('<div><%= name %></div>')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid EJS', () => {
|
||||
ejs.compile.mockImplementationOnce(() => {
|
||||
throw new Error('syntax error');
|
||||
});
|
||||
expect(templateManager.validateTemplate('<% invalid %>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderPDF', () => {
|
||||
it('should render a PDF successfully', async () => {
|
||||
const mockTemplate = {
|
||||
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||
global: false,
|
||||
objectType: 'printer',
|
||||
};
|
||||
getObject.mockResolvedValue(mockTemplate);
|
||||
|
||||
const result = await templateManager.renderPDF('temp-id', 'content');
|
||||
|
||||
expect(generatePDF).toHaveBeenCalled();
|
||||
expect(result.pdf).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderDownload', () => {
|
||||
it('should reject unsupported types', async () => {
|
||||
const result = await templateManager.renderDownload('temp-id', 'content', {}, 'gif');
|
||||
expect(result.error).toMatch(/Unsupported download type/);
|
||||
expect(result.code).toBe(400);
|
||||
});
|
||||
|
||||
it('should render a PDF download', async () => {
|
||||
getObject.mockResolvedValue({
|
||||
documentSize: { width: 100, height: 100, infiniteHeight: false },
|
||||
global: false,
|
||||
objectType: 'printer',
|
||||
});
|
||||
|
||||
const result = await templateManager.renderDownload('temp-id', 'content', {}, 'pdf');
|
||||
expect(result.type).toBe('pdf');
|
||||
expect(result.buffers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should format date correctly', () => {
|
||||
const date = new Date('2023-01-01T12:00:00Z');
|
||||
const formatted = templateManager.formatDate(date, 'YYYY-MM-DD');
|
||||
expect(formatted).toBe('2023-01-01');
|
||||
});
|
||||
});
|
||||
});
|
||||
49
src/templates/assets/basetemplate.ejs
Normal file
49
src/templates/assets/basetemplate.ejs
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
<%- baseCSS %>
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
}
|
||||
|
||||
.previewWrapper {
|
||||
width: <%= (scaledWidth) || '50mm' %>;
|
||||
height: <%= (scaledHeight) || '50mm' %>;
|
||||
}
|
||||
.previewDocument {
|
||||
width: <%= (width) || '50mm' %>;
|
||||
height: <%= (height) || '50mm' %>;
|
||||
transform: scale(<%= scale || '1' %>);
|
||||
transform-origin: top left;
|
||||
}
|
||||
.renderDocument {
|
||||
width: <%= (scaledWidth) || '50mm' %>;
|
||||
height: <%= (scaledHeight) || '50mm' %>;
|
||||
transform: scale(<%= scale || '1' %>);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%- content %>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/JsBarcode.all.min.js"></script>
|
||||
<script>
|
||||
JsBarcode('.documentBarcode').init();
|
||||
</script>
|
||||
<% if (typeof previewPaginationScript !== 'undefined' && previewPaginationScript) { %>
|
||||
<script>
|
||||
<%- previewPaginationScript %>
|
||||
</script>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
||||
3
src/templates/assets/contentplaceholder.ejs
Normal file
3
src/templates/assets/contentplaceholder.ejs
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="contentPlaceholder">
|
||||
<p>Content</p>
|
||||
</div>
|
||||
0
src/templates/assets/previewpagination.js
Normal file
0
src/templates/assets/previewpagination.js
Normal file
5
src/templates/assets/previewtemplate.ejs
Normal file
5
src/templates/assets/previewtemplate.ejs
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="previewContainer">
|
||||
<div class="previewWrapper">
|
||||
<div class="previewDocument" id="content"><%- content %></div>
|
||||
</div>
|
||||
</div>
|
||||
1
src/templates/assets/rendertemplate.ejs
Normal file
1
src/templates/assets/rendertemplate.ejs
Normal file
@ -0,0 +1 @@
|
||||
<div class="renderDocument" id="content"><%- content %></div>
|
||||
131
src/templates/assets/styles.css
Normal file
131
src/templates/assets/styles.css
Normal file
@ -0,0 +1,131 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Figtree', sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.previewContainer {
|
||||
display: flex;
|
||||
justify-content: center; /* Horizontal center */
|
||||
align-items: center; /* Vertical center */
|
||||
padding: 60px;
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
}
|
||||
.previewWrapper {
|
||||
position: relative;
|
||||
}
|
||||
.previewDocument {
|
||||
background: #ffffff;
|
||||
border: 1px solid #000;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
transform-origin: top left;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.documentText {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.documentTitle {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1.documentTitle {
|
||||
font-weight: 800;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
h2.documentTitle {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h3.documentTitle {
|
||||
font-weight: 700;
|
||||
}
|
||||
h4.documentTitle {
|
||||
font-weight: 700;
|
||||
}
|
||||
.documentFlex {
|
||||
display: flex;
|
||||
}
|
||||
.documentDivider {
|
||||
background: black;
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.contentPlaceholder {
|
||||
border: 1px solid black;
|
||||
max-height: 250px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
/* Angle of the stripes */ #ccc,
|
||||
/* Light grey */ #ccc 10px,
|
||||
/* End of first stripe */ #eee 10px,
|
||||
/* Start of next stripe (slightly lighter grey) */ #eee 20px
|
||||
/* End of second stripe */
|
||||
);
|
||||
}
|
||||
|
||||
.contentPlaceholder > p {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.documentBarcode {
|
||||
width: auto !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.documentProgressBar {
|
||||
height: 8px;
|
||||
width: 260px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #000000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.documentProgressBarInner {
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.documentTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRow {
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRow td,
|
||||
.documentTableRowHeader th,
|
||||
.documentTableRowFooter td {
|
||||
padding: 4px;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRowHeader {
|
||||
background: #0000002e;
|
||||
text-align: left;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.documentTableRowFooter {
|
||||
background: #0000002e;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
78
src/templates/pdfUtils.js
Normal file
78
src/templates/pdfUtils.js
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* PDF conversion utilities loaded on demand so startup
|
||||
* does not require ESM-only pdf-to-img at module load time.
|
||||
*/
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
async function importPackage(name) {
|
||||
const packagePath = require.resolve(name);
|
||||
return import(pathToFileURL(packagePath).href);
|
||||
}
|
||||
|
||||
function resolveDefault(module) {
|
||||
return module?.default ?? module;
|
||||
}
|
||||
|
||||
export async function convertPDFToImage(pdfInput, options = {}) {
|
||||
const [pdfToImgModule, sharpModule] = await Promise.all([
|
||||
importPackage('pdf-to-img'),
|
||||
importPackage('sharp')
|
||||
]);
|
||||
const { pdf } = pdfToImgModule;
|
||||
const sharp = resolveDefault(sharpModule);
|
||||
|
||||
try {
|
||||
const { width, height, page_numbers, scale = 2, ...pdfOnlyOptions } =
|
||||
options;
|
||||
|
||||
const pdfOptions = {
|
||||
scale,
|
||||
...pdfOnlyOptions
|
||||
};
|
||||
|
||||
const document = await pdf(pdfInput, pdfOptions);
|
||||
const outputImages = [];
|
||||
|
||||
if (
|
||||
page_numbers &&
|
||||
Array.isArray(page_numbers) &&
|
||||
page_numbers.length > 0
|
||||
) {
|
||||
for (const pageNum of page_numbers) {
|
||||
let image = await document.getPage(pageNum);
|
||||
|
||||
if (width || height) {
|
||||
const resizeOptions = {};
|
||||
if (width) resizeOptions.width = width;
|
||||
if (height) resizeOptions.height = height;
|
||||
image = await sharp(image).resize(resizeOptions).toBuffer();
|
||||
}
|
||||
|
||||
outputImages.push(image);
|
||||
}
|
||||
} else {
|
||||
for await (const image of document) {
|
||||
let processedImage = image;
|
||||
|
||||
if (width || height) {
|
||||
const resizeOptions = {};
|
||||
if (width) resizeOptions.width = width;
|
||||
if (height) resizeOptions.height = height;
|
||||
processedImage = await sharp(image)
|
||||
.resize(resizeOptions)
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
outputImages.push(processedImage);
|
||||
}
|
||||
}
|
||||
|
||||
return outputImages;
|
||||
} catch (error) {
|
||||
console.error('Error converting PDF to image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
149
src/templates/pdffactory.js
Normal file
149
src/templates/pdffactory.js
Normal file
@ -0,0 +1,149 @@
|
||||
import log4js from 'log4js';
|
||||
import puppeteer from 'puppeteer';
|
||||
import config from '../config.js';
|
||||
|
||||
const logger = log4js.getLogger('PDF Factory');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
const LAUNCH_OPTIONS = {
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
};
|
||||
|
||||
let browser = null;
|
||||
let launchPromise = null;
|
||||
let shutdownRegistered = false;
|
||||
|
||||
function isBrowserConnected() {
|
||||
return browser != null && browser.connected;
|
||||
}
|
||||
|
||||
async function launchBrowser() {
|
||||
logger.info('Launching persistent Puppeteer browser...');
|
||||
const instance = await puppeteer.launch(LAUNCH_OPTIONS);
|
||||
instance.on('disconnected', () => {
|
||||
if (browser === instance) {
|
||||
logger.warn('Puppeteer browser disconnected');
|
||||
browser = null;
|
||||
launchPromise = null;
|
||||
}
|
||||
});
|
||||
logger.info('Persistent Puppeteer browser ready');
|
||||
return instance;
|
||||
}
|
||||
|
||||
function registerShutdownHandlers() {
|
||||
if (shutdownRegistered) {
|
||||
return;
|
||||
}
|
||||
shutdownRegistered = true;
|
||||
|
||||
const shutdown = async () => {
|
||||
await closePDFBrowser();
|
||||
};
|
||||
|
||||
process.once('SIGINT', shutdown);
|
||||
process.once('SIGTERM', shutdown);
|
||||
process.once('beforeExit', shutdown);
|
||||
}
|
||||
|
||||
export async function startPDFBrowser() {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
registerShutdownHandlers();
|
||||
}
|
||||
|
||||
if (isBrowserConnected()) {
|
||||
return browser;
|
||||
}
|
||||
|
||||
if (launchPromise == null) {
|
||||
launchPromise = launchBrowser()
|
||||
.then((instance) => {
|
||||
browser = instance;
|
||||
return instance;
|
||||
})
|
||||
.catch((error) => {
|
||||
launchPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return launchPromise;
|
||||
}
|
||||
|
||||
export async function closePDFBrowser() {
|
||||
const instance = browser;
|
||||
browser = null;
|
||||
launchPromise = null;
|
||||
if (instance != null) {
|
||||
try {
|
||||
await instance.close();
|
||||
} catch (error) {
|
||||
logger.warn('Error closing Puppeteer browser:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function withPage(run) {
|
||||
let page;
|
||||
try {
|
||||
const instance = await startPDFBrowser();
|
||||
page = await instance.newPage();
|
||||
return await run(page);
|
||||
} catch (error) {
|
||||
if (page == null && isBrowserConnected() === false) {
|
||||
const instance = await startPDFBrowser();
|
||||
page = await instance.newPage();
|
||||
return await run(page);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (page != null) {
|
||||
await page.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PDF from HTML content using a persistent Puppeteer browser
|
||||
* @param {string} html - The HTML content to convert to PDF
|
||||
* @param {Object} options - PDF generation options
|
||||
* @param {number} options.width - Document width in mm
|
||||
* @param {number|string} options.height - Document height in mm, or 'auto'
|
||||
* @returns {Promise<Buffer>} The PDF buffer
|
||||
*/
|
||||
export async function generatePDF(html, options = {}) {
|
||||
try {
|
||||
return await withPage(async (page) => {
|
||||
await page.setContent(html, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
var height = options?.height || '50';
|
||||
|
||||
if (options.height == 'auto') {
|
||||
const calculatedHeight = await page.evaluate(() => {
|
||||
return document.getElementById('content').scrollHeight;
|
||||
});
|
||||
|
||||
height = calculatedHeight;
|
||||
}
|
||||
|
||||
return page.pdf({
|
||||
printBackground: true,
|
||||
preferCSSPageSize: true,
|
||||
width: options.width ? `${options.width}mm` : undefined,
|
||||
height: height ? (options.height == 'auto' ? `${height + 1}px` : `${height}mm`) : undefined,
|
||||
margin: {
|
||||
top: '0mm',
|
||||
right: '0mm',
|
||||
bottom: '0mm',
|
||||
left: '0mm',
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error generating PDF:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
683
src/templates/templatemanager.js
Normal file
683
src/templates/templatemanager.js
Normal file
@ -0,0 +1,683 @@
|
||||
import ejs from 'ejs';
|
||||
import log4js from 'log4js';
|
||||
import posthtml from 'posthtml';
|
||||
import fs from 'fs';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import timezone from 'dayjs/plugin/timezone.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { documentTemplateModel } from '../database/schemas/management/documenttemplate.schema.js';
|
||||
import '../database/schemas/management/documentsize.schema.js';
|
||||
import config from '../config.js';
|
||||
import { getObject, listObjects } from '../database/database.js';
|
||||
import { getModelByName } from '../services/misc/model.js';
|
||||
import { generatePDF } from './pdffactory.js';
|
||||
import { convertPDFToImage } from './pdfUtils.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
const logger = log4js.getLogger('Template Manager');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
const DOWNLOAD_TYPES = ['pdf', 'png', 'svg', 'jpeg'];
|
||||
|
||||
let baseTemplate;
|
||||
let baseCSS;
|
||||
let previewTemplate;
|
||||
let renderTemplateEjs;
|
||||
let contentPlaceholder;
|
||||
let previewPaginationScript;
|
||||
|
||||
async function loadTemplates() {
|
||||
baseTemplate = fs.readFileSync(join(__dirname, '/assets/basetemplate.ejs'), 'utf8');
|
||||
baseCSS = fs.readFileSync(join(__dirname, '/assets/styles.css'), 'utf8');
|
||||
previewTemplate = fs.readFileSync(join(__dirname, '/assets/previewtemplate.ejs'), 'utf8');
|
||||
renderTemplateEjs = fs.readFileSync(join(__dirname, '/assets/rendertemplate.ejs'), 'utf8');
|
||||
contentPlaceholder = fs.readFileSync(
|
||||
join(__dirname, '/assets/contentplaceholder.ejs'),
|
||||
'utf8'
|
||||
);
|
||||
previewPaginationScript = fs.readFileSync(
|
||||
join(__dirname, '/assets/previewpagination.js'),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
loadTemplates();
|
||||
|
||||
function getNodeStyles(attributes) {
|
||||
var styles = '';
|
||||
if (attributes?.padding) {
|
||||
styles += `padding: ${attributes.padding};`;
|
||||
}
|
||||
if (attributes?.width) {
|
||||
styles += `width: ${attributes.width};`;
|
||||
}
|
||||
if (attributes?.height) {
|
||||
styles += `height: ${attributes.height};`;
|
||||
}
|
||||
if (attributes?.maxWidth) {
|
||||
styles += `max-width: ${attributes.maxWidth};`;
|
||||
}
|
||||
if (attributes?.maxHeight) {
|
||||
styles += `max-height: ${attributes.maxHeight};`;
|
||||
}
|
||||
if (attributes?.gap && attributes?.vertical != 'true') {
|
||||
styles += `column-gap: ${attributes.gap};`;
|
||||
}
|
||||
if (attributes?.gap && attributes?.vertical == 'true') {
|
||||
styles += `row-gap: ${attributes.gap};`;
|
||||
}
|
||||
if (attributes?.justify) {
|
||||
styles += `justify-content: ${attributes.justify};`;
|
||||
}
|
||||
if (attributes?.align) {
|
||||
styles += `align-items: ${attributes.align};`;
|
||||
}
|
||||
if (attributes?.border) {
|
||||
styles += `border: ${attributes.border};`;
|
||||
}
|
||||
if (attributes?.borderRadius) {
|
||||
styles += `border-radius: ${attributes.borderRadius};`;
|
||||
}
|
||||
if (attributes?.vertical == 'true') {
|
||||
styles += `flex-direction: column;`;
|
||||
}
|
||||
if (attributes?.grow) {
|
||||
styles += `flex-grow: ${attributes.grow};`;
|
||||
}
|
||||
if (attributes?.shrink) {
|
||||
styles += `flex-shrink: ${attributes.shrink};`;
|
||||
}
|
||||
if (attributes?.color) {
|
||||
styles += `color: ${attributes.color};`;
|
||||
}
|
||||
if (attributes?.background) {
|
||||
styles += `background: ${attributes.background};`;
|
||||
}
|
||||
if (attributes?.scale) {
|
||||
styles += `transform: scale(${attributes.scale});`;
|
||||
}
|
||||
if (attributes?.textAlign) {
|
||||
styles += `text-align: ${attributes.textAlign};`;
|
||||
}
|
||||
if (attributes?.textSize) {
|
||||
styles += `font-size: ${attributes.textSize};`;
|
||||
}
|
||||
if (attributes?.wordWrap) {
|
||||
styles += `word-wrap: ${attributes.wordWrap};`;
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
async function transformCustomElements(content) {
|
||||
const result = await posthtml([
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title1' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h1',
|
||||
attrs: { class: 'documentTitle' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title2' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h2',
|
||||
attrs: { class: 'documentTitle' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title3' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h3',
|
||||
attrs: { class: 'documentText' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Title4' }, (node) => ({
|
||||
...node,
|
||||
tag: 'h4',
|
||||
attrs: { class: 'documentText' },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Text' }, (node) => ({
|
||||
...node,
|
||||
tag: 'p',
|
||||
attrs: { class: 'documentText', style: getNodeStyles(node.attrs) },
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Bold' }, (node) => ({
|
||||
...node,
|
||||
tag: 'strong',
|
||||
attrs: {
|
||||
class: 'documentBoldText',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Barcode' }, (node) => {
|
||||
return {
|
||||
tag: 'div',
|
||||
content: [
|
||||
{
|
||||
tag: 'svg',
|
||||
attrs: {
|
||||
class: 'documentBarcode',
|
||||
'jsbarcode-displayValue': 'false',
|
||||
'jsbarcode-value': node.content.at(0) || '',
|
||||
'jsbarcode-format': node.attrs.format || 'code128',
|
||||
'jsbarcode-width': node.attrs.barcodeWidth || 2,
|
||||
'jsbarcode-margin': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
class: 'documentBarcodeContainer',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Container' }, (node) => ({
|
||||
...node,
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentContainer',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
})),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Flex' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentFlex',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Divider' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'hr',
|
||||
attrs: {
|
||||
class: 'documentDivider',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'ProgressBar' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentProgressBar',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'documentProgressBarInner',
|
||||
style: `width: ${Math.round((node.content[0] || 0) * 100)}%`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'DateTime' }, (node) => {
|
||||
const dateTime = dayjs.utc(node.content[0]);
|
||||
return {
|
||||
content: [dateTime.format('YYYY-MM-DD hh:mm:ss')],
|
||||
tag: 'span',
|
||||
attrs: {
|
||||
class: 'documentDateTime',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Table' }, (node) => {
|
||||
return {
|
||||
...node,
|
||||
tag: 'table',
|
||||
attrs: {
|
||||
class: 'documentTable',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
(tree) =>
|
||||
tree.match({ tag: 'Row' }, (node) => {
|
||||
const rowType = node.attrs?.type?.toLowerCase() || '';
|
||||
|
||||
const transformCols = (content) => {
|
||||
if (!Array.isArray(content)) return content;
|
||||
return content.map((child) => {
|
||||
if (typeof child === 'string' || child == null) {
|
||||
return child;
|
||||
}
|
||||
if (child.tag !== 'Col') {
|
||||
return child;
|
||||
}
|
||||
|
||||
const baseAttrs = {
|
||||
...child.attrs,
|
||||
style: getNodeStyles(child.attrs),
|
||||
};
|
||||
|
||||
if (rowType === 'header') {
|
||||
return {
|
||||
...child,
|
||||
tag: 'th',
|
||||
attrs: baseAttrs,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...child,
|
||||
tag: 'td',
|
||||
attrs: baseAttrs,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const content = transformCols(node.content);
|
||||
|
||||
if (rowType === 'header') {
|
||||
return {
|
||||
...node,
|
||||
tag: 'tr',
|
||||
content,
|
||||
attrs: {
|
||||
class: 'documentTableRowHeader',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (rowType === 'footer') {
|
||||
return {
|
||||
...node,
|
||||
tag: 'tr',
|
||||
content,
|
||||
attrs: {
|
||||
class: 'documentTableRowFooter',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
tag: 'tr',
|
||||
content,
|
||||
attrs: {
|
||||
class: 'documentTableRow',
|
||||
style: getNodeStyles(node.attrs),
|
||||
},
|
||||
};
|
||||
}),
|
||||
]).process(content);
|
||||
|
||||
return result.html;
|
||||
}
|
||||
|
||||
function normalizeDownloadType(type) {
|
||||
const normalized = String(type || 'pdf').toLowerCase();
|
||||
if (normalized === 'jpg') {
|
||||
return 'jpeg';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function imageToSvg(pngBuffer, widthPx, heightPx, documentWidthMm, documentHeightMm) {
|
||||
const b64 = pngBuffer.toString('base64');
|
||||
const heightAttr =
|
||||
documentHeightMm == 'auto' || documentHeightMm == null
|
||||
? ''
|
||||
: ` height="${documentHeightMm}mm"`;
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${documentWidthMm}mm"${heightAttr} viewBox="0 0 ${widthPx} ${heightPx}">
|
||||
<image href="data:image/png;base64,${b64}" width="${widthPx}" height="${heightPx}"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
export class TemplateManager {
|
||||
constructor() {
|
||||
this.fc = {
|
||||
listObjects: this.listObjects.bind(this),
|
||||
getObject: this.getObject.bind(this),
|
||||
formatDate: this.formatDate.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
async renderTemplate(id, content, data = {}, scale = 1, options = {}, preview = true) {
|
||||
try {
|
||||
const defaultOptions = {
|
||||
async: true,
|
||||
...options,
|
||||
};
|
||||
logger.debug('Rendering template:', id);
|
||||
|
||||
const documentTemplate = await getObject({
|
||||
model: documentTemplateModel,
|
||||
id,
|
||||
populate: [
|
||||
{ path: 'documentSize' },
|
||||
{ path: 'parent', strictPopulate: false },
|
||||
],
|
||||
});
|
||||
|
||||
if (documentTemplate == null || documentTemplate.error) {
|
||||
return { error: documentTemplate?.error || 'Document template not found.', code: 404 };
|
||||
}
|
||||
|
||||
const documentSize = documentTemplate.documentSize;
|
||||
if (documentSize == null) {
|
||||
return { error: 'Document template size not found.', code: 400 };
|
||||
}
|
||||
|
||||
const templateContentSource =
|
||||
content != null && typeof content === 'string' ? content : documentTemplate.content;
|
||||
|
||||
if (templateContentSource == null || typeof templateContentSource !== 'string') {
|
||||
return { error: 'Template content is required and must be a string.', code: 400 };
|
||||
}
|
||||
|
||||
var templateData = {};
|
||||
if (documentTemplate.global == true) {
|
||||
templateData = { content: contentPlaceholder, fc: this.fc };
|
||||
} else {
|
||||
const objectType = documentTemplate?.objectType;
|
||||
const modelEntry = getModelByName(objectType);
|
||||
const model = modelEntry?.model || modelEntry;
|
||||
if (model == null || model.schema == null) {
|
||||
return { error: `Unknown object type: ${objectType}`, code: 400 };
|
||||
}
|
||||
const defaultKeys = Object.keys(model.schema.obj);
|
||||
const defaultValues = {};
|
||||
for (const key of defaultKeys) {
|
||||
defaultValues[key] = null;
|
||||
}
|
||||
templateData = { ...defaultValues, ...data, fc: this.fc };
|
||||
}
|
||||
|
||||
const templateContent = await ejs.render(templateContentSource, templateData, defaultOptions);
|
||||
|
||||
var templateWithParentContent;
|
||||
var parentTemplate = documentTemplate.parent;
|
||||
|
||||
if (documentTemplate.parent != undefined) {
|
||||
if (typeof parentTemplate === 'string') {
|
||||
parentTemplate = await getObject({
|
||||
model: documentTemplateModel,
|
||||
id: parentTemplate,
|
||||
populate: [
|
||||
{ path: 'documentSize' },
|
||||
{ path: 'parent', strictPopulate: false },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (parentTemplate?.error || parentTemplate.content == null || typeof parentTemplate.content !== 'string') {
|
||||
logger.error(
|
||||
'Parent template content is required and must be a string.',
|
||||
parentTemplate?.content
|
||||
);
|
||||
return {
|
||||
error:
|
||||
'Parent template content is required and must be a string.' +
|
||||
(parentTemplate?.content || ''),
|
||||
code: 400,
|
||||
};
|
||||
}
|
||||
templateWithParentContent = await ejs.render(
|
||||
parentTemplate.content,
|
||||
{ content: templateContent, fc: this.fc },
|
||||
defaultOptions
|
||||
);
|
||||
} else {
|
||||
templateWithParentContent = templateContent;
|
||||
}
|
||||
|
||||
if (templateWithParentContent == null || typeof templateWithParentContent !== 'string') {
|
||||
return { error: 'Failed to render template content.', code: 500 };
|
||||
}
|
||||
|
||||
const templateHtml = await transformCustomElements(templateWithParentContent);
|
||||
|
||||
if (templateHtml == null || typeof templateHtml !== 'string') {
|
||||
return { error: 'Failed to transform template content.', code: 500 };
|
||||
}
|
||||
|
||||
var innerHtml = null;
|
||||
|
||||
if (preview == true) {
|
||||
innerHtml = await ejs.render(previewTemplate, { content: templateHtml }, defaultOptions);
|
||||
} else {
|
||||
innerHtml = await ejs.render(renderTemplateEjs, { content: templateHtml }, defaultOptions);
|
||||
}
|
||||
|
||||
if (innerHtml == null || typeof innerHtml !== 'string') {
|
||||
return { error: 'Failed to render inner template content.', code: 500 };
|
||||
}
|
||||
|
||||
const infiniteHeight = documentSize.infiniteHeight == true;
|
||||
|
||||
const baseHtml = await ejs.render(
|
||||
baseTemplate,
|
||||
{
|
||||
content: innerHtml,
|
||||
width: `${documentSize.width}mm`,
|
||||
height: infiniteHeight ? 'fit-content' : `${documentSize.height}mm`,
|
||||
scaledWidth: `${documentSize.width * scale}mm`,
|
||||
scaledHeight: infiniteHeight ? 'auto' : `${documentSize.height * scale}mm`,
|
||||
scale: `${scale}`,
|
||||
baseCSS: baseCSS,
|
||||
previewPaginationScript: preview ? previewPaginationScript : '',
|
||||
},
|
||||
defaultOptions
|
||||
);
|
||||
|
||||
return {
|
||||
html: baseHtml,
|
||||
width: documentSize.width,
|
||||
height: infiniteHeight ? 'auto' : documentSize.height,
|
||||
infiniteHeight: infiniteHeight,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.warn('Error whilst previewing template:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
validateTemplate(templateString) {
|
||||
try {
|
||||
ejs.compile(templateString);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async renderPDF(id, content, data = {}, options = {}) {
|
||||
try {
|
||||
logger.debug('Rendering PDF for template:', id);
|
||||
|
||||
const renderedTemplate = await this.renderTemplate(id, content, data, 1, options, false);
|
||||
|
||||
if (renderedTemplate.error != undefined) {
|
||||
return { error: renderedTemplate.error, code: renderedTemplate.code };
|
||||
}
|
||||
const baseHtml = renderedTemplate.html;
|
||||
|
||||
const pdfBuffer = await generatePDF(baseHtml, {
|
||||
width: renderedTemplate.width,
|
||||
height: renderedTemplate.height,
|
||||
infiniteHeight: renderedTemplate.infiniteHeight,
|
||||
});
|
||||
|
||||
return {
|
||||
pdf: pdfBuffer,
|
||||
width: renderedTemplate.width,
|
||||
height: renderedTemplate.height,
|
||||
infiniteHeight: renderedTemplate.infiniteHeight,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn('Error whilst rendering PDF:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
async renderImages(id, content, data = {}, options = {}, format = 'jpeg') {
|
||||
try {
|
||||
logger.debug(`Rendering ${format} for template:`, id);
|
||||
|
||||
const pdfResult = await this.renderPDF(id, content, data, options);
|
||||
if (pdfResult.error != undefined) {
|
||||
return { error: pdfResult.error, code: pdfResult.code };
|
||||
}
|
||||
|
||||
const images = await convertPDFToImage(pdfResult.pdf, options);
|
||||
const converted = [];
|
||||
for (const image of images) {
|
||||
if (format === 'png') {
|
||||
converted.push(await sharp(image).png().toBuffer());
|
||||
} else {
|
||||
converted.push(await sharp(image).jpeg().toBuffer());
|
||||
}
|
||||
}
|
||||
return { images: converted };
|
||||
} catch (error) {
|
||||
logger.warn('Error whilst rendering image:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
async renderSVG(id, content, data = {}, options = {}) {
|
||||
try {
|
||||
logger.debug('Rendering SVG for template:', id);
|
||||
|
||||
const pdfResult = await this.renderPDF(id, content, data, options);
|
||||
if (pdfResult.error != undefined) {
|
||||
return { error: pdfResult.error, code: pdfResult.code };
|
||||
}
|
||||
|
||||
const images = await convertPDFToImage(pdfResult.pdf, options);
|
||||
const svgs = [];
|
||||
for (const image of images) {
|
||||
const png = await sharp(image).png().toBuffer();
|
||||
const meta = await sharp(png).metadata();
|
||||
svgs.push(
|
||||
imageToSvg(
|
||||
png,
|
||||
meta.width,
|
||||
meta.height,
|
||||
pdfResult.width,
|
||||
pdfResult.height
|
||||
)
|
||||
);
|
||||
}
|
||||
return { svgs };
|
||||
} catch (error) {
|
||||
logger.warn('Error whilst rendering SVG:', error.message);
|
||||
return { error: error.message, code: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
async renderDownload(id, content, data = {}, type = 'pdf', options = {}) {
|
||||
const normalizedType = normalizeDownloadType(type);
|
||||
if (!DOWNLOAD_TYPES.includes(normalizedType)) {
|
||||
return {
|
||||
error: `Unsupported download type: ${type}. Use pdf, png, svg or jpeg.`,
|
||||
code: 400,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedType === 'pdf') {
|
||||
const result = await this.renderPDF(id, content, data, options);
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
type: 'pdf',
|
||||
mime: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
buffers: [result.pdf],
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedType === 'svg') {
|
||||
const result = await this.renderSVG(id, content, data, options);
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
type: 'svg',
|
||||
mime: 'image/svg+xml',
|
||||
extension: 'svg',
|
||||
buffers: result.svgs.map((svg) => Buffer.from(svg, 'utf8')),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.renderImages(id, content, data, options, normalizedType);
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
type: normalizedType,
|
||||
mime: normalizedType === 'png' ? 'image/png' : 'image/jpeg',
|
||||
extension: normalizedType === 'png' ? 'png' : 'jpeg',
|
||||
buffers: result.images,
|
||||
};
|
||||
}
|
||||
|
||||
async listObjects(objectType, filter = {}, populate = []) {
|
||||
const modelEntry = getModelByName(objectType);
|
||||
const model = modelEntry?.model || modelEntry;
|
||||
if (model == undefined) {
|
||||
throw new Error('Farm Control: Object type not found.');
|
||||
}
|
||||
const objects = await listObjects({
|
||||
model,
|
||||
filter,
|
||||
populate,
|
||||
pagination: false,
|
||||
});
|
||||
return objects;
|
||||
}
|
||||
|
||||
formatDate(date, format) {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
async getObject(objectType = undefined, id = undefined, populate = []) {
|
||||
if (objectType == undefined || objectType == '') {
|
||||
logger.warn('Object type is required');
|
||||
return { error: 'Object type is required' };
|
||||
}
|
||||
if (id == undefined || id == '') {
|
||||
logger.warn('Object ID is required');
|
||||
return { error: 'Object ID is required' };
|
||||
}
|
||||
const modelEntry = getModelByName(objectType);
|
||||
const model = modelEntry?.model || modelEntry;
|
||||
if (model == undefined) {
|
||||
logger.warn('Object type not found:', objectType);
|
||||
}
|
||||
const object = await getObject({ model, id, populate });
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
export const templateManager = new TemplateManager();
|
||||
export { DOWNLOAD_TYPES, normalizeDownloadType };
|
||||
Loading…
x
Reference in New Issue
Block a user