prince
Version:
Node API for executing XML/HTML to PDF renderer PrinceXML via prince(1) CLI
342 lines (330 loc) • 17.9 kB
JavaScript
/*
** node-prince -- Node API for executing PrinceXML via prince(1) CLI
** Copyright (c) 2014-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
**
** Permission is hereby granted, free of charge, to any person obtaining
** a copy of this software and associated documentation files (the
** "Software"), to deal in the Software without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Software, and to
** permit persons to whom the Software is furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be included
** in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* eslint no-console: 0 */
/*
* prince-npm.js: NPM install-time integration
*/
/* core requirements */
const child_process = require("child_process")
const fs = require("fs")
const path = require("path")
const zlib = require("zlib")
/* extra requirements */
const progress = require("progress")
const promise = require("promise")
const axios = require("axios")
const which = require("which")
const chalk = require("chalk")
const tar = require("tar")
const streamzip = require("node-stream-zip")
const rimraf = require("rimraf")
const mkdirp = require("mkdirp")
/* determine path and version of prince(1) */
const princeInfo = function () {
return new promise(function (resolve, reject) {
which("prince").then(function (filename) {
child_process.execFile(filename, [ "--version" ], function (error, stdout, stderr) {
if (error !== null) {
reject(`prince(1) failed on "--version": ${error}`)
return
}
const m = stdout.match(/^Prince\s+(\d+(?:\.\d+)?)/)
if (m === null) {
reject(`prince(1) returned unexpected output on "--version":\n${stdout}${stderr}`)
return
}
resolve({ command: filename, version: m[1] })
})
}).catch(function (error) {
reject(`prince(1) not found in PATH: ${error}`)
})
})
}
/* return download URL for latest PrinceXML distribution */
const princeDownloadURL = function () {
return new promise(function (resolve /*, reject */) {
const id = `${process.arch}-${process.platform}`
if (id.match(/^ia32-win32$/))
resolve("https://www.princexml.com/download/prince-16.2-win32.zip")
else if (id.match(/^x64-win32$/))
resolve("https://www.princexml.com/download/prince-16.2-win64.zip")
else if (id.match(/^arm64-win32$/))
resolve("https://www.princexml.com/download/prince-16.2-win-arm64.zip")
else if (id.match(/^(?:x64|arm64)-darwin/))
resolve("https://www.princexml.com/download/prince-16.2-macos.zip")
else {
child_process.execFile("sh", [ path.join(__dirname, "shtool"), "platform", "-t", "binary" ], function (error, stdout /*, stderr */) {
if (error) {
console.log(chalk.red(`ERROR: failed to determine platform details on platform "${id}": ${error}`))
process.exit(1)
}
const platform = stdout.toString().replace(/^(\S+).*\n?$/, "$1")
if (id.match(/^x64-linux/)) {
if (platform.match(/^amd64-ubuntu1[89](?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-ubuntu20.04-amd64.tar.gz")
else if (platform.match(/^amd64-ubuntu2[23](?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-ubuntu22.04-amd64.tar.gz")
else if (platform.match(/^amd64-ubuntu24(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-ubuntu24.04-amd64.tar.gz")
else if (platform.match(/^amd64-debian13(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-debian13-amd64.tar.gz")
else if (platform.match(/^amd64-debian12(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-debian12-amd64.tar.gz")
else if (platform.match(/^amd64-debian11(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-debian11-amd64.tar.gz")
else if (platform.match(/^amd64-almalinux10(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-almalinux10-x86_64.tar.gz")
else if (platform.match(/^amd64-almalinux9(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-almalinux9-x86_64.tar.gz")
else if (platform.match(/^amd64-almalinux8(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-almalinux8-x86_64.tar.gz")
else if (platform.match(/^amd64-alpine3\.23(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.23-x86_64.tar.gz")
else if (platform.match(/^amd64-alpine3\.22(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.22-x86_64.tar.gz")
else if (platform.match(/^amd64-alpine3\.21(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.21-x86_64.tar.gz")
else if (platform.match(/^amd64-alpine3\.20(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.20-x86_64.tar.gz")
else if (platform.match(/^amd64-alpine3\.19(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.19-x86_64.tar.gz")
else if (platform.match(/^amd64-opensuse15\.6(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-opensuse15.6-x86_64.tar.gz")
else if (id.match(/^x64-/))
resolve("https://www.princexml.com/download/prince-16.2-linux-generic-x86_64.tar.gz")
}
else if (id.match(/^arm64-linux/)) {
if (platform.match(/^arm64-ubuntu24(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-ubuntu24.04-arm64.tar.gz")
else if (platform.match(/^arm64-ubuntu2[23](?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-ubuntu22.04-arm64.tar.gz")
else if (platform.match(/^arm64-debian13(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-debian13-arm64.tar.gz")
else if (platform.match(/^arm64-debian12(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-debian12-arm64.tar.gz")
else if (platform.match(/^arm64-debian11(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-debian11-arm64.tar.gz")
else if (platform.match(/^arm64-alpine3\.23(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.23-aarch64.tar.gz")
else if (platform.match(/^arm64-alpine3\.22(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.22-aarch64.tar.gz")
else if (platform.match(/^arm64-alpine3\.21(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.21-aarch64.tar.gz")
else if (platform.match(/^arm64-alpine3\.20(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.20-aarch64.tar.gz")
else if (platform.match(/^arm64-alpine3\.19(?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-alpine3.19-aarch64.tar.gz")
else if (platform.match(/^aarch64-alpine[23](?:\.\d+)*$/))
resolve("https://www.princexml.com/download/prince-16.2-linux-generic-aarch64-musl.tar.gz")
else
resolve("https://www.princexml.com/download/prince-16.2-linux-generic-aarch64.tar.gz")
}
else if (platform.match(/^amd64-freebsd15(?:\.\d+)*/))
resolve("https://www.princexml.com/download/prince-16.2-freebsd15.0-amd64.tar.gz")
else if (platform.match(/^amd64-freebsd14(?:\.\d+)*/))
resolve("https://www.princexml.com/download/prince-16.2-freebsd14.0-amd64.tar.gz")
else if (platform.match(/^amd64-freebsd13(?:\.\d+)*/))
resolve("https://www.princexml.com/download/prince-16.2-freebsd13.0-amd64.tar.gz")
else {
console.log(chalk.red(`ERROR: PrinceXML not available for platform "${platform}" ("${id}")`))
process.exit(1)
}
})
}
})
}
/* download data from URL */
const downloadData = function (url) {
return new promise(function (resolve, reject) {
let progress_bar = null
const options = {
method: "GET",
url: url,
encoding: null,
headers: {
"User-Agent": "node-prince (prince-npm.js:install)"
},
responseType: "arraybuffer",
onDownloadProgress: function(progressEvent) {
if (!progress_bar) {
progress_bar = new progress(
"-- download: [:bar] :percent (ETA: :etas)", {
complete: "#",
incomplete: "=",
width: 40,
total: progressEvent.total
})
}
progress_bar.tick(progressEvent.loaded)
}
}
;(new promise(function (resolve /*, reject */) {
if (typeof process.env.http_proxy === "string" && process.env.http_proxy !== "") {
options.proxy = process.env.http_proxy
console.log(`-- using proxy ($http_proxy): ${options.proxy}`)
resolve()
}
else {
child_process.exec("npm config get proxy", function (error, stdout /*, stderr */) {
if (error === null) {
stdout = stdout.toString().replace(/\r?\n$/, "")
if (stdout.match(/^https?:\/\/.+/)) {
options.proxy = stdout
console.log(`-- using proxy (npm config get proxy): ${options.proxy}`)
}
}
resolve()
})
}
})).then(function () {
console.log(`-- download: ${url}`)
axios(options).then(function (response) {
if (response.status === 200) {
console.log(`-- download: ${response.data.length} bytes received.`)
resolve(response.data)
}
}).catch(function (error) {
reject(`download failed: ${error}`)
})
})
})
}
/* extract a zipfile (*.zip) */
const extractZipfile = function (zipfile, stripdir, destdir) {
return new promise(function (resolve, reject) {
const zip = new streamzip({ file: zipfile })
zip.on("ready", function () {
zip.extract(stripdir, destdir, function (error) {
zip.close()
if (error) {
reject(error)
} else {
setTimeout(function () { resolve() }, 500)
}
})
})
})
}
/* extract a tarball (*.tar.gz) */
const extractTarball = function (tarball, destdir, stripdirs) {
return new promise(function (resolve, reject) {
fs.createReadStream(tarball)
.pipe(zlib.createGunzip())
.pipe(tar.extract({ cwd: destdir, strip: stripdirs }))
.on("error", function (error) { reject(error) })
.on("close", function () { setTimeout(function () { resolve() }, 500) })
})
}
/* main procedure */
if (process.argv.length !== 3) {
console.log(chalk.red("ERROR: invalid number of arguments"))
process.exit(1)
}
let destdir
if (process.argv[2] === "install") {
/* installation procedure */
console.log("++ checking for globally installed PrinceXML")
princeInfo().then(function (prince) {
console.log(`-- found prince(1) command: ${chalk.blue(prince.command)}`)
console.log(`-- found prince(1) version: ${chalk.blue(prince.version)}`)
}, function (/* error */) {
console.log("++ downloading PrinceXML distribution")
princeDownloadURL().then(function (url) {
downloadData(url).then(function (data) {
console.log("++ locally unpacking PrinceXML distribution")
destdir = path.join(__dirname, "prince")
let destfile
const id = `${process.arch}-${process.platform}`
if (id.match(/^ia32-win32$/)) {
destfile = path.join(__dirname, "prince.zip")
fs.writeFileSync(destfile, data, { encoding: null })
mkdirp.sync(destdir)
extractZipfile(destfile, "prince-16.2-win32", destdir).then(function () {
fs.chmodSync(path.join(destdir, "bin", "prince.exe"), fs.constants.S_IRWXU
| fs.constants.S_IRGRP | fs.constants.S_IXGRP | fs.constants.S_IROTH | fs.constants.S_IXOTH)
fs.unlinkSync(destfile)
console.log("-- OK: local PrinceXML installation now available")
}, function (error) {
console.log(chalk.red(`** ERROR: failed to extract: ${error}`))
})
}
else if (id.match(/^(?:x64|arm64)-win32$/)) {
destfile = path.join(__dirname, "prince.zip")
fs.writeFileSync(destfile, data, { encoding: null })
mkdirp.sync(destdir)
extractZipfile(destfile, "prince-16.2-win64", destdir).then(function () {
fs.chmodSync(path.join(destdir, "bin", "prince.exe"), fs.constants.S_IRWXU
| fs.constants.S_IRGRP | fs.constants.S_IXGRP | fs.constants.S_IROTH | fs.constants.S_IXOTH)
fs.unlinkSync(destfile)
console.log("-- OK: local PrinceXML installation now available")
}, function (error) {
console.log(chalk.red(`** ERROR: failed to extract: ${error}`))
})
}
else if (process.platform === "darwin") {
destfile = path.join(__dirname, "prince.zip")
fs.writeFileSync(destfile, data, { encoding: null })
mkdirp.sync(destdir)
extractZipfile(destfile, "prince-16.2-macos", destdir).then(function () {
fs.chmodSync(path.join(destdir, "lib/prince/bin/prince"), fs.constants.S_IRWXU
| fs.constants.S_IRGRP | fs.constants.S_IXGRP | fs.constants.S_IROTH | fs.constants.S_IXOTH)
fs.unlinkSync(destfile)
console.log("-- OK: local PrinceXML installation now available")
}, function (error) {
console.log(chalk.red(`** ERROR: failed to extract: ${error}`))
})
}
else {
destfile = path.join(__dirname, "prince.tgz")
fs.writeFileSync(destfile, data, { encoding: null })
mkdirp.sync(destdir)
extractTarball(destfile, destdir, 1).then(function () {
fs.unlinkSync(destfile)
console.log("-- OK: local PrinceXML installation now available")
}, function (error) {
console.log(chalk.red(`** ERROR: failed to extract: ${error}`))
})
}
}, function (error) {
console.log(chalk.red(`** ERROR: failed to download: ${error}`))
})
})
})
}
else if (process.argv[2] === "uninstall") {
/* uninstallation procedure */
destdir = path.join(__dirname, "prince")
if (fs.existsSync(destdir)) {
console.log("++ deleting locally unpacked PrinceXML distribution")
rimraf(destdir).then(function () {
console.log("-- OK: done")
}).catch(function (error) {
console.log(chalk.red(`** ERROR: ${error}`))
})
}
}
else {
console.log(chalk.red("ERROR: invalid argument"))
process.exit(1)
}