@jsreport/jsreport-pdf-utils
Version:
jsreport extension providing pdf operations like merge or concatenation
222 lines (189 loc) • 7.41 kB
JavaScript
const parsePdf = require('./utils/parsePdf')
const { Document, External } = require('@jsreport/pdfjs')
const PDF = require('@jsreport/pdfjs/lib/object')
const HIDDEN_TEXT_SIZE = 1.1
module.exports = (contentBuffer, { pdfMeta, pdfPassword, pdfSign, pdfA, outlines, removeHiddenMarks, pdfAccessibility, pdfCompression } = {}) => {
let currentBuffer = contentBuffer
let currentlyParsedPdf
return {
async parse ({
includeText = false,
hiddenPageFields = {}
} = {}) {
currentlyParsedPdf = await parsePdf(currentBuffer, {
hiddenPageFields,
includeText
})
return currentlyParsedPdf
},
get parsedPdf () {
return currentlyParsedPdf
},
async append (appendBuffer, options = {}) {
const document = new Document()
document.append(new External(currentBuffer), { copyAccessibilityTags: pdfAccessibility?.enabled })
document.append(new External(appendBuffer), {
copyAccessibilityTags: pdfAccessibility?.enabled,
appendAfterIndex: options.appendAfterPageNumber != null ? options.appendAfterPageNumber - 1 : null
})
currentBuffer = await document.asBuffer()
},
async prepend (prependBuffer) {
const document = new Document()
document.append(new External(prependBuffer), { copyAccessibilityTags: pdfAccessibility?.enabled })
document.append(new External(currentBuffer), { copyAccessibilityTags: pdfAccessibility?.enabled })
currentBuffer = await document.asBuffer()
},
async merge (pageBuffersOrDocBuffer, mergeToFront) {
const document = new Document()
document.append(new External(currentBuffer), { copyAccessibilityTags: pdfAccessibility?.enabled })
if (Buffer.isBuffer(pageBuffersOrDocBuffer)) {
document.merge(new External(pageBuffersOrDocBuffer), { mergeToFront, copyAccessibilityTags: pdfAccessibility?.enabled })
} else {
for (const i in pageBuffersOrDocBuffer) {
document.merge(new External(pageBuffersOrDocBuffer[i]), { mergeToFront, pageNum: i, copyAccessibilityTags: pdfAccessibility?.enabled })
}
}
currentBuffer = await document.asBuffer()
},
async removePages (pageNumbersToRemove) {
const document = new Document()
if (!Array.isArray(pageNumbersToRemove)) {
pageNumbersToRemove = [pageNumbersToRemove]
}
for (const n of pageNumbersToRemove) {
if (!Number.isInteger(n)) {
throw new Error('Page number for remove operation needs to be an integer, got ' + pageNumbersToRemove)
}
if (n < 1) {
throw new Error('Page number for remove operation needs to be bigger than 0')
}
}
const ext = new External(currentBuffer)
function collectPages (pagesObj, pages) {
for (const page of pagesObj.properties.get('Kids').map(k => k.object)) {
if (page.properties.get('Type').name === 'Pages') {
collectPages(page, pages)
} else {
pages.push(page)
}
}
}
const pages = []
collectPages(ext.catalog.properties.get('Pages').object, pages)
const pageIndexesToAppend = []
for (let i = 0; i < pages.length; i++) {
if (!pageNumbersToRemove.includes(i + 1)) {
pageIndexesToAppend.push(i)
}
}
document.append(ext, { pageIndexes: pageIndexesToAppend, copyAccessibilityTags: pdfAccessibility?.enabled })
currentBuffer = await document.asBuffer()
},
async addAttachment (buf, options) {
const doc = new Document()
doc.append(new External(currentBuffer), { copyAccessibilityTags: pdfAccessibility?.enabled })
doc.attachment(buf, options)
currentBuffer = await doc.asBuffer()
},
toBuffer () {
return Promise.resolve(currentBuffer)
},
async postprocess ({
hiddenPageFields
}) {
const doc = new Document()
const ext = new External(currentBuffer)
doc.append(ext, { copyAccessibilityTags: pdfAccessibility?.enabled })
if (pdfSign) {
doc.sign({
certificateBuffer: Buffer.from(pdfSign.certificateContent, 'base64'),
password: pdfSign.password,
reason: pdfSign.reason,
maxSignaturePlaceholderLength: pdfSign.maxSignaturePlaceholderLength
})
}
doc.processText({
resolver: async (text, { remove, getPosition }) => {
for (const mark of ['group', 'item', 'form', 'dest']) {
let i = -1
while ((i = text.indexOf(`${mark}@@@`, i + 1)) !== -1) {
if (removeHiddenMarks || mark === 'form' || mark === 'dest') {
remove(i, text.indexOf('@@@', i + `${mark}@@@`.length) + '@@@'.length)
}
if (mark !== 'form' && mark !== 'dest') {
continue
}
const trimmedText = text.substring(i + `${mark}@@@`.length, text.indexOf('@@@', i + `${mark}@@@`.length))
const valueOfText = hiddenPageFields[trimmedText]
const { pageIndex, position, matrix } = getPosition(i)
position[5] -= HIDDEN_TEXT_SIZE * matrix[3]
if (mark === 'dest') {
const dest = new PDF.Array([
doc.pages[pageIndex].toReference(),
new PDF.Name('XYZ'),
position[4],
position[5],
0
])
doc.catalog.properties.get('Dests').object.properties.set(`/${valueOfText}`, dest)
}
if (mark === 'form') {
const formSpec = JSON.parse(Buffer.from(valueOfText, 'base64').toString())
await doc.acroForm({
...formSpec,
position,
pageIndex
})
}
}
}
}
})
if (outlines) {
// needs to be after the processText because it may need pdfDest processed
doc.outlines(outlines)
}
if (pdfPassword) {
doc.encrypt({
password: pdfPassword.password,
ownerPassword: pdfPassword.ownerPassword,
permissions: {
printing: pdfPassword.printing,
modifying: pdfPassword.modifying,
copying: pdfPassword.copying,
annotating: pdfPassword.annotating,
fillingForms: pdfPassword.fillingForms,
contentAccessibility: pdfPassword.contentAccessibility,
documentAssembly: pdfPassword.documentAssembly
}
})
}
if (pdfMeta) {
const meta = { ...pdfMeta }
if (meta.custom && typeof meta.custom === 'string' && meta.custom !== '') {
meta.custom = JSON.parse(meta.custom)
}
doc.info(meta)
}
if (pdfA?.enabled === true) {
doc.pdfA()
}
if (pdfAccessibility?.pdfUA === true) {
doc.pdfUA()
}
if (pdfCompression?.enabled === true) {
doc.compress(pdfCompression)
}
try {
currentBuffer = await doc.asBuffer()
} catch (e) {
if (e.message.includes('Signature exceeds placeholder')) {
e.message += '. Increase placeholder length using config extensions.pdfUtils.maxSignaturePlaceholderLength'
throw e
}
throw e
}
}
}
}