Skip to content

Commit

Permalink
support merging/appending pdf with outlines
Browse files Browse the repository at this point in the history
  • Loading branch information
pofider committed Jan 24, 2024
1 parent f1e65cc commit f2b3e0e
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 8 deletions.
3 changes: 3 additions & 0 deletions packages/jsreport-pdf-utils/lib/worker.js
Expand Up @@ -96,6 +96,9 @@ module.exports = (reporter, definition) => {
})

reporter.beforeRenderListeners.add('pdf-utils', (req, res) => {
// we need to avoid that nested calls are adding the same root outlines
req.context.pdfUtilsOutlines = null

// we use just root template setting for pdfAccessibility
// this is because otherwise you need to set the accessibility on every merged/template
// the use case when you would want root template to have for example pdf/ua enabled but not some child renders doesn't exist I tihnk
Expand Down
92 changes: 91 additions & 1 deletion packages/jsreport-pdf-utils/test/test.js
Expand Up @@ -1096,6 +1096,96 @@ describe('pdf utils', () => {
doc.catalog.properties.get('Outlines').object.properties.get('First').object.properties.get('First').object.properties.get('Count').should.be.eql(-2)
})

it('should be able to add outlines and append another template with outlines', async () => {
const result = await jsreport.render({
template: {
content: `
<a href='#outline1' data-pdf-outline data-pdf-outline-text='outline1'>
outline1
</a>
<br>
<h1 id='outline1'>outline1</h1>
`,
name: 'content',
engine: 'none',
recipe: 'chrome-pdf',
pdfOperations: [{
type: 'append',
template: {
content: `
<a href='#outline2' data-pdf-outline data-pdf-outline-text='outline2'>
outline2
</a>
<br>
<h1 id='outline2'>outline2</h1>
`,
engine: 'none',
recipe: 'chrome-pdf'
}
}]
}
})

const doc = new External(result.content)
doc.catalog.properties.get('Outlines').object.properties.get('First').should.not.be.eql(doc.catalog.properties.get('Outlines').object.properties.get('Last'))
})

it('should be able add outlines and merge in another document', async () => {
const res = await jsreport.render({
template: {
content: `
<a href='#outline1' data-pdf-outline data-pdf-outline-text='outline1'>
outline1
</a>
<br>
<a href='#outline2' data-pdf-outline data-pdf-outline-text='outline2'>
outline2
</a>
<br>
<a href='#outline3' data-pdf-outline data-pdf-outline-text='outline3'>
outline3
</a>
<br>
<div style='page-break-before: always;'></div>
<h1 id='outline1'>outline1</h1>
<div style='page-break-before: always;'></div>
<h1 id='outline2'>outline2</h1>
<div style='page-break-before: always;'></div>
<h1 id='outline3'>outline3</h1>
`,
name: 'content',
engine: 'none',
recipe: 'chrome-pdf',
pdfOperations: [{
type: 'merge',
mergeWholeDocument: true,
template: {
content: `
<a href='#outline1' data-pdf-outline data-pdf-outline-text='outline1'>
outline1
</a>
<br>
<a href='#outline2' data-pdf-outline data-pdf-outline-text='outline2'>
outline2
</a>
<br>
<a href='#outline3' data-pdf-outline data-pdf-outline-text='outline3'>
outline3
</a>
`,
engine: 'none',
recipe: 'chrome-pdf'
}
}]
}
})

require('fs').writeFileSync('out.pdf', res.content)

const doc = new External(res.content)
should(doc.catalog.properties.get('Outlines').object.properties.get('Last').object.properties.get('Last')).be.undefined()
})

it('should be able to add outlines through child template', async () => {
await jsreport.documentStore.collection('templates').insert({
content: '<a href="#child" id="child" data-pdf-outline data-pdf-outline-parent="root">Child</a>',
Expand Down Expand Up @@ -2464,7 +2554,7 @@ describe('pdf utils', () => {

describe('processText with pdf from alpine', () => {
it('should deal with double f ligature and remove hidden mark', async () => {
const manipulator = PdfManipulator(fs.readFileSync(path.join(__dirname, 'alpine.pdf')), { removeHiddenMarks: true })
const manipulator = await PdfManipulator(fs.readFileSync(path.join(__dirname, 'alpine.pdf')), { removeHiddenMarks: true })
await manipulator.postprocess({
hiddenPageFields: {
ff2181tsdwkqil98bfi73sks: Buffer.from(JSON.stringify({
Expand Down
20 changes: 19 additions & 1 deletion packages/pdfjs/lib/mixins/outlines.js
Expand Up @@ -6,7 +6,6 @@ module.exports = (doc) => {
function outlines (aoutlines, doc) {
const rootOutline = new PDF.Object('Outlines')
rootOutline.data = {}
doc.catalog.prop('Outlines', rootOutline.toReference())

const outlinesObjects = [rootOutline]
for (const { title, parent, id } of aoutlines) {
Expand All @@ -24,6 +23,10 @@ function outlines (aoutlines, doc) {
outlinesObjects.push(outline)
}

if (outlinesObjects.length === 1) {
return
}

for (let i = 0; i < outlinesObjects.length; i++) {
const outline = outlinesObjects[i]
const parentIndex = outline.data.parent == null || outline.data.parent === '' ? 0 : outlinesObjects.findIndex(o => o.data.id === outline.data.parent)
Expand Down Expand Up @@ -68,4 +71,19 @@ function outlines (aoutlines, doc) {

// no open outlines at all level by default
rootOutline.properties.del('Count')

const docOutline = doc.catalog.properties.get('Outlines')?.object
if (docOutline == null) {
doc.catalog.prop('Outlines', rootOutline.toReference())
} else {
// append to existing outlines
let currentNext = docOutline.properties.get('First').object
while (currentNext?.properties.get('Next')?.object) {
currentNext = currentNext.properties.get('Next').object
}

rootOutline.properties.get('First').object.properties.set('Prev', currentNext.toReference())
currentNext.properties.set('Next', rootOutline.properties.get('First'))
docOutline.properties.set('Last', rootOutline.properties.get('Last'))
}
}
43 changes: 40 additions & 3 deletions packages/pdfjs/lib/mixins/utils/unionGlobalObjects.js
Expand Up @@ -20,9 +20,7 @@ module.exports = (doc, ext, options) => {
embeddedFilesDictionary.set('Names', new PDF.Array([...embeddedFilesDictionary.get('Names'), ...ext.catalog.properties.get('Names').object.properties.get('EmbeddedFiles').get('Names')]))
}

if (ext.catalog.properties.get('Outlines')?.object && doc.catalog.properties.get('Outlines') == null) {
doc.catalog.properties.set('Outlines', ext.catalog.properties.get('Outlines').object.toReference())
}
unionOutlines(ext, doc)

if (options.copyAccessibilityTags) {
mergeStructTree(ext, doc)
Expand Down Expand Up @@ -61,6 +59,45 @@ module.exports = (doc, ext, options) => {
}
}

function unionOutlines (ext, doc) {
const docOutline = doc.catalog.properties.get('Outlines')?.object
const extOutline = ext.catalog.properties.get('Outlines')?.object

if (extOutline == null) {
return
}

if (docOutline == null) {
doc.catalog.properties.set('Outlines', extOutline.toReference())
return
}

if (docOutline.properties.get('First').object && extOutline.properties.get('Last').object) {
const docDests = doc.catalog.properties.get('Dests')?.object
const extDests = ext.catalog.properties.get('Dests')?.object

if (!docDests || !extDests) {
// something wrong, there needs to be dests when we want to union outlines
return
}

for (const key in extDests.dictionary) {
if (extDests.has(key)) {
return
}
}

let currentNext = docOutline.properties.get('First').object
while (currentNext?.properties.get('Next')?.object) {
currentNext = currentNext.properties.get('Next').object
}

extOutline.properties.get('First').object.properties.set('Prev', currentNext.toReference())
currentNext.properties.set('Next', extOutline.properties.get('First'))
docOutline.properties.set('Last', extOutline.properties.get('Last'))
}
}

function mergeStructTree (ext, doc) {
const docStructTreeRoot = doc.catalog.properties.get('StructTreeRoot')?.object
const extStructTreeRoot = ext.catalog.properties.get('StructTreeRoot')?.object
Expand Down
Binary file added packages/pdfjs/test/links2.pdf
Binary file not shown.
25 changes: 22 additions & 3 deletions packages/pdfjs/test/test.js
Expand Up @@ -421,18 +421,37 @@ describe('pdfjs', () => {
}])

const pdfWithOutlines = await document.asBuffer()

document = new Document()
external = new External(pdfWithOutlines)
external = new External(fs.readFileSync(path.join(__dirname, 'links2.pdf')))
document.append(external)
document.outlines([{
title: '21 title',
id: '21'
}, {
title: '22 title',
id: '22',
parent: '21'
}])

const pdfWithOutlines2 = await document.asBuffer()

document = new Document()
document.append(new External(pdfWithOutlines))
document.append(new External(pdfWithOutlines2))

const pdfBuffer = await document.asBuffer()

require('fs').writeFileSync('out.pdf', pdfBuffer)

const { catalog } = await validate(pdfBuffer)
const outlines = catalog.properties.get('Outlines').object
const outline1 = outlines.properties.get('First').object
outlines.properties.get('Last').object.should.be.eql(outline1)
const first = outlines.properties.get('First').object
const last = outlines.properties.get('Last').object

first.should.not.be.eql(last)
first.properties.get('Next').object.should.be.eql(last)
last.properties.get('Prev').object.should.be.eql(first)
})

it('merge should union Dests', async () => {
Expand Down

0 comments on commit f2b3e0e

Please sign in to comment.