jsreport-chrome-pdf
Version:
jsreport recipe which is rendering pdf from html using headless chrome
328 lines (256 loc) • 8.91 kB
JavaScript
module.exports = async ({ reporter, getBrowser, htmlUrl, strategy, timeout, req, imageExecution, allowLocalFilesAccess, options }) => {
const optionsToUse = Object.assign({}, options)
const browser = await getBrowser()
function pageLog (level, message) {
const maxLogEntrySize = 1000
let newMsg = message
if (newMsg.length > maxLogEntrySize) {
newMsg = `${newMsg.substring(0, maxLogEntrySize)}...`
}
reporter.logger[level](newMsg, { timestamp: new Date().getTime(), ...req })
}
function trimUrl (url) {
// this is special case, because phantom logs base64 images content completely into the output
if (url.startsWith('data:image') && url.length > 100) {
return `${url.substring(0, 100)}...`
}
return url
}
const conversionResult = await runWithTimeout(async (executionInfo, reject) => {
const chromeVersion = await browser.version()
if (executionInfo.error) {
return
}
reporter.logger.debug(`Converting with chrome ${chromeVersion} using ${strategy} strategy`, req)
const page = await browser.newPage()
if (executionInfo.error) {
return
}
await page.setRequestInterception(true)
if (executionInfo.error) {
return
}
page.on('pageerror', (err) => {
pageLog('warn', `Page error: ${err.message}${err.stack ? ` , stack: ${err.stack}` : ''}`)
})
page.on('error', (err) => {
err.workerCrashed = true
if (!page.isClosed()) {
page.close().catch(() => {})
}
reject(err)
})
page.on('console', (m) => {
pageLog('debug', m.text())
})
page.on('request', (r) => {
let detail = ''
if (r.redirectChain().length > 0) {
detail = ` (redirect from: ${trimUrl(r.redirectChain().slice(-1)[0].url())})`
}
pageLog('debug', `Page request: ${r.method()} (${r.resourceType()}) ${trimUrl(r.url())}${detail}`)
const isRelativeToHtmlUrl = r.url().lastIndexOf(htmlUrl, 0) === 0
if (
!allowLocalFilesAccess &&
!isRelativeToHtmlUrl &&
// potentially dangerous request to local file
r.url().lastIndexOf('file://', 0) === 0
) {
r.abort('accessdenied')
return
}
r.continue()
})
page.on('requestfinished', (r) => {
let requestDesc = 'finished'
let status = ''
let detail = ''
if (r.response() != null) {
if (r.response().status() !== 0) {
status = ` ${r.response().status()}`
}
if (!r.response().ok()) {
requestDesc = 'failed'
}
}
if (
r.redirectChain().length > 0 &&
r.redirectChain().slice(-1)[0].url() === r.url() &&
r.response() &&
r.response().headers() &&
r.response().headers().location != null
) {
detail = ` (redirect to: ${r.response().headers().location})`
}
const log = `Page request ${requestDesc}: ${r.method()} (${r.resourceType()})${status} ${trimUrl(r.url())}${detail}`
if (requestDesc === 'failed') {
pageLog('warn', log)
} else {
pageLog('debug', log)
}
})
page.on('requestfailed', (r) => {
pageLog('warn', `Page request failed: ${r.method()} (${r.resourceType()}) ${trimUrl(r.url())}, failure: ${r.failure().errorText}`)
})
if (optionsToUse.waitForNetworkIddle === true) {
reporter.logger.debug('Chrome will wait for network iddle before printing', req)
}
// this is the same as sending timeout options to the page.goto
// but additionally setting it more generally in the page
page.setDefaultNavigationTimeout(timeout)
await page.goto(
htmlUrl,
optionsToUse.waitForNetworkIddle === true
? { waitUntil: 'networkidle0' }
: { }
)
if (executionInfo.error) {
return
}
if (optionsToUse.waitForJS === true) {
reporter.logger.debug('Chrome will wait for printing trigger', req)
await page.waitForFunction('window.JSREPORT_READY_TO_START === true', { timeout })
}
if (executionInfo.error) {
return
}
let newChromeSettings
if (imageExecution) {
newChromeSettings = await page.evaluate(() => window.JSREPORT_CHROME_IMAGE_OPTIONS)
} else {
newChromeSettings = await page.evaluate(() => window.JSREPORT_CHROME_PDF_OPTIONS)
}
if (executionInfo.error) {
return
}
if (newChromeSettings != null) {
delete newChromeSettings.path
}
Object.assign(optionsToUse, newChromeSettings)
if (optionsToUse.mediaType) {
if (optionsToUse.mediaType !== 'screen' && optionsToUse.mediaType !== 'print') {
throw reporter.createError(`chrome.mediaType must be equal to 'screen' or 'print'`, { weak: true })
}
await page.emulateMedia(optionsToUse.mediaType)
}
if (executionInfo.error) {
return
}
if (imageExecution) {
if (optionsToUse.type == null) {
optionsToUse.type = 'png'
}
if (optionsToUse.type !== 'png' && optionsToUse.type !== 'jpeg') {
throw reporter.createError(`chromeImage.type must be equal to 'jpeg' or 'png'`, { weak: true })
}
if (optionsToUse.type === 'png') {
delete optionsToUse.quality
}
if (optionsToUse.quality == null) {
delete optionsToUse.quality
}
optionsToUse.clip = {}
if (optionsToUse.clipX != null) {
optionsToUse.clip.x = optionsToUse.clipX
}
if (optionsToUse.clipY != null) {
optionsToUse.clip.y = optionsToUse.clipY
}
if (optionsToUse.clipWidth != null) {
optionsToUse.clip.width = optionsToUse.clipWidth
}
if (optionsToUse.clipHeight != null) {
optionsToUse.clip.height = optionsToUse.clipHeight
}
if (Object.keys(optionsToUse.clip).length === 0) {
delete optionsToUse.clip
} else if (
optionsToUse.clip.x == null ||
optionsToUse.clip.y == null ||
optionsToUse.clip.width == null ||
optionsToUse.clip.height == null
) {
throw reporter.createError(`All chromeImage clip properties needs to be specified when at least one of them is passed. Make sure to specify values for "chromeImage.clipX", "chromeImage.clipY", "chromeImage.clipWidth", "chromeImage.clipHeight"`, { weak: true })
}
optionsToUse.encoding = 'binary'
} else {
optionsToUse.margin = {
top: optionsToUse.marginTop,
right: optionsToUse.marginRight,
bottom: optionsToUse.marginBottom,
left: optionsToUse.marginLeft
}
// if no specified then default to print the background
if (optionsToUse.printBackground == null) {
optionsToUse.printBackground = true
}
}
// don't log header/footer template content
reporter.logger.debug(`Running chrome with params ${
JSON.stringify(Object.assign({}, optionsToUse, {
headerTemplate: optionsToUse.headerTemplate ? '...' : undefined,
footerTemplate: optionsToUse.footerTemplate ? '...' : undefined
}))
}`, req)
if (optionsToUse.scale == null) {
delete optionsToUse.scale
}
let result
let resultType
if (imageExecution) {
resultType = optionsToUse.type
result = await page.screenshot(optionsToUse)
} else {
resultType = 'pdf'
result = await page.pdf(optionsToUse)
}
if (executionInfo.error) {
return
}
return {
page,
type: resultType,
content: result
}
}, timeout, `${imageExecution ? 'image' : 'pdf'} generation not completed after ${timeout}ms`)
return conversionResult
}
function runWithTimeout (fn, ms, msg) {
return new Promise(async (resolve, reject) => {
let resolved = false
const info = {
// information to pass to fn to ensure it can cancel
// things if it needs to
error: null
}
const timer = setTimeout(() => {
const err = new Error(`Timeout Error: ${msg}`)
err.workerTimeout = true
info.error = err
resolved = true
reject(err)
}, ms)
try {
const result = await fn(info, (err) => {
if (resolved) {
return
}
resolved = true
clearTimeout(timer)
info.error = err
reject(err)
})
if (resolved) {
return
}
resolve(result)
} catch (e) {
if (resolved) {
return
}
reject(e)
} finally {
clearTimeout(timer)
}
})
}