Skip to content

Commit

Permalink
Add outline to pdf documents
Browse files Browse the repository at this point in the history
Ideally this would be handled by Chrome during printing. However,
https://bugs.chromium.org/p/chromium/issues/detail?id=840455 is not
implemented yet (and cannot rely on metadata extracted from the asciidoc
format directly).

Therefore this implements it by introducing some kind of post-processing
using `pdf-lib`. If the `:toc:` property is set, the `.adoc` file is
scanned for sections (respecting the `:toclevels:` attribute) and an
outline is generated.

This only works if a ToC is also generated within the document (or
better: links exist to each section), because otherwise Chrome would not
generate the necessary `Dests` fields within the PDF.

Unfortunately, Chrome also has some bugs regarding Umlaute in anchors,
leading to omission of the relevant `Dests` fields. Therefore a warning
is printed if a anchor cannot be located in the `Dests` field of the PDF
catalog.

Based upon
https://gitlab.pagedmedia.org/tools/pagedjs-cli/commit/df0d10bd1bb12d1e2077323e1fb5eec75da35a1e
which itself is based on @Dopding's comment at:
Hopding/pdf-lib#127 (comment)
  • Loading branch information
Erik Schilling committed Oct 25, 2019
1 parent 1e0de98 commit 05414d6
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 40 deletions.
114 changes: 108 additions & 6 deletions lib/converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fsExtra = require('fs-extra')
const path = require('path')
const mkdirs = util.promisify(fsExtra.mkdirs)
const puppeteer = require('puppeteer')
const { PDFDict, PDFDocument, PDFName, PDFNumber, PDFString } = require('pdf-lib')

function registerTemplateConverter (processor, templates) {
class TemplateConverter {
Expand All @@ -24,6 +25,103 @@ function registerTemplateConverter (processor, templates) {
processor.ConverterFactory.register(new TemplateConverter(), ['html5'])
}

function getOutline (node, depth) {
if (depth == 0) {
return []
}

return node.getSections().map(section => ({
title: section.getTitle(),
destination: section.getId(),
children: getOutline(section, depth - 1)
}))
}

function setRefsForOutlineItems (layer, context, parentRef) {
for (const item of layer) {
item.ref = context.nextRef()
item.parentRef = parentRef
setRefsForOutlineItems(item.children, context, item.ref)
}
}

function countChildrenOfOutline (layer) {
let count = 0
for (const item of layer) {
++count
count += countChildrenOfOutline(item.children)
}
return count
}

function buildPdfObjectsForOutline (layer, context) {
for (const [i, item] of layer.entries()) {
const prev = layer[i - 1]
const next = layer[i + 1]

const pdfObject = new Map([
[PDFName.of('Title'), PDFString.of(item.title)],
[PDFName.of('Dest'), PDFName.of(item.destination)],
[PDFName.of('Parent'), item.parentRef]
])
if (prev) {
pdfObject.set(PDFName.of('Prev'), prev.ref)
}
if (next) {
pdfObject.set(PDFName.of('Next'), next.ref)
}
if (item.children.length > 0) {
pdfObject.set(PDFName.of('First'), item.children[0].ref)
pdfObject.set(PDFName.of('Last'), item.children[item.children.length - 1].ref)
pdfObject.set(PDFName.of('Count'), PDFNumber.of(countChildrenOfOutline(item.children)))
}

context.assign(item.ref, PDFDict.fromMapWithContext(pdfObject, context))

buildPdfObjectsForOutline(item.children, context)
}
}

function generateWarningsAboutMissingDestinations (layer, pdfDoc) {
const dests = pdfDoc.context.lookup(pdfDoc.catalog.get(PDFName.of('Dests')))
const validDestinationTargets = dests.entries().map(([key, _]) => key.value())

for (const item of layer) {
if (!validDestinationTargets.includes('/' + item.destination)) {
console.warn(`Unable to find destination ${item.destination} `+
'while generating PDF outline! This likely happened because ' +
'an anchor link contained an umlaut ' +
'(https://bugs.chromium.org/p/chromium/issues/detail?id=985254).')
}
generateWarningsAboutMissingDestinations(item.children, pdfDoc)
}
}

async function addOutline (pdf, doc) {
const depth = doc.getAttribute('toclevels') || 2;
const pdfDoc = await PDFDocument.load(pdf)
const context = pdfDoc.context
const outlineRef = context.nextRef()

const outline = getOutline(doc, depth)
if (outline.length == 0) {
return pdf;
}
generateWarningsAboutMissingDestinations(outline, pdfDoc)
setRefsForOutlineItems(outline, context, outlineRef)
buildPdfObjectsForOutline(outline, context)

const outlineObject = PDFDict.fromMapWithContext(new Map([
[PDFName.of('First'), outline[0].ref],
[PDFName.of('Last'), outline[outline.length - 1].ref],
[PDFName.of('Count'), PDFNumber.of(countChildrenOfOutline(outline))]
]), context)
context.assign(outlineRef, outlineObject)

pdfDoc.catalog.set(PDFName.of('Outlines'), outlineRef)
return pdfDoc.save()
}

async function convert (processor, inputFile, options, timings, debug) {
const tempFile = getTemporaryHtmlFile(inputFile, options)
let workingDir
Expand Down Expand Up @@ -73,9 +171,6 @@ async function convert (processor, inputFile, options, timings, debug) {
printBackground: true,
preferCSSPageSize: true
}
if (!outputToStdout) {
pdfOptions.path = outputFile
}
const pdfWidth = doc.getAttributes()['pdf-width']
if (pdfWidth) {
pdfOptions.width = pdfWidth
Expand All @@ -88,9 +183,16 @@ async function convert (processor, inputFile, options, timings, debug) {
if (format) {
pdfOptions.format = format // Paper format. If set, takes priority over width or height options. Defaults to 'Letter'.
}
const pdf = debug || await page.pdf(pdfOptions)
if (!debug && outputToStdout) {
console.log(pdf.toString())
let pdf = debug || await page.pdf(pdfOptions)
if (!debug) {
if (doc.getAttribute('toc') !== undefined) {
pdf = await addOutline(pdf, doc)
}
fsExtra.writeFileSync(outputFile, pdf)

if (outputToStdout) {
console.log(pdf.toString())
}
}
} finally {
try {
Expand Down
82 changes: 50 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@
"@fortawesome/free-regular-svg-icons": "5.11.2",
"@fortawesome/free-solid-svg-icons": "5.11.2",
"chokidar": "2.0.4",
"file-url": "3.0.0",
"fs-extra": "7.0.1",
"mathjax": "^2.7.6",
"pagedjs": "0.1.34",
"pdf-lib": "^1.2.1",
"puppeteer": "1.15.0",
"mathjax": "^2.7.6",
"file-url": "3.0.0",
"yargs": "13.2.2"
},
"devDependencies": {
Expand Down

0 comments on commit 05414d6

Please sign in to comment.