UNPKG

@vivliostyle/cli

Version:

Save the pdf file via headless browser and Vivliostyle.

731 lines (724 loc) 23.1 kB
import { resolveViteConfig } from "./chunk-4V7POPZ3.js"; import { buildWebPublication, cleanupWorkspace, compile, copyAssets, createViteServer, getFullBrowserName, getSourceUrl, getViewerFullUrl, isWebPubConfig, launchPreview, loadVivliostyleConfig, mergeInlineConfig, prepareThemeDirectory, resolveTaskConfig, warnDeprecatedConfig } from "./chunk-5YYKJN5S.js"; import { importNodeModule } from "./chunk-FXUEYQRY.js"; import { Logger, cwd, isInContainer, isUnicodeSupported, isValidUri, pathEquals, randomBookSymbol, runExitHandlers, setupConfigFromFlags } from "./chunk-HCZKJQUX.js"; import { CONTAINER_LOCAL_HOSTNAME, CONTAINER_ROOT_DIR, coreVersion } from "./chunk-4IIM6RSG.js"; import { __callDispose, __using } from "./chunk-I7BWSAN6.js"; // src/core/build.ts import { pathToFileURL as pathToFileURL2 } from "node:url"; import terminalLink2 from "terminal-link"; import upath4 from "upath"; import { build as viteBuild } from "vite"; import { cyan as cyan2, gray as gray2 } from "yoctocolors"; // src/container.ts import { execFile } from "node:child_process"; import process from "node:process"; import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import upath from "upath"; var execFileAsync = promisify(execFile); function toContainerPath(urlOrAbsPath) { if (isValidUri(urlOrAbsPath)) { if (urlOrAbsPath.toLowerCase().startsWith("file")) { return pathToFileURL( upath.posix.join( CONTAINER_ROOT_DIR, upath.toUnix(fileURLToPath(urlOrAbsPath)).replace(/^\w:/, "") ) ).href; } else { return urlOrAbsPath; } } return upath.posix.join( CONTAINER_ROOT_DIR, upath.toUnix(urlOrAbsPath).replace(/^\w:/, "") ); } function collectVolumeArgs(mountPoints) { return mountPoints.filter((p, i, array) => { if (i !== array.indexOf(p)) { return false; } let parent = p; while (!pathEquals(parent, upath.dirname(parent))) { parent = upath.dirname(parent); if (array.includes(parent)) { return false; } } return true; }).map((p) => `${p}:${toContainerPath(p)}`); } async function runContainer({ image, userVolumeArgs, commandArgs, entrypoint, env, workdir }) { const { default: commandExists } = await importNodeModule("command-exists"); if (!await commandExists("docker")) { throw new Error( `Docker isn't be installed. To use this feature, you'll need to install Docker.` ); } const versionCmd = await execFileAsync("docker", [ "version", "--format", "{{.Server.Version}}" ]); const version = versionCmd.stdout.trim(); const [major, minor] = version.split(".").map(Number); if (major < 20 || major === 20 && minor < 10) { throw new Error( `Docker version ${version} is not supported. Please upgrade to Docker 20.10.0 or later.` ); } try { var _stack = []; try { const _2 = __using(_stack, Logger.suspendLogging("Launching docker container")); const args = [ "run", ...Logger.isInteractive ? ["-it"] : [], "--rm", ...entrypoint ? ["--entrypoint", entrypoint] : [], ...env ? env.flatMap(([k, v]) => ["-e", `${k}=${v}`]) : [], ...process.env.DEBUG ? ["-e", `DEBUG=${process.env.DEBUG}`] : [], ...userVolumeArgs.flatMap((arg) => ["-v", arg]), ...workdir ? ["-w", workdir] : [], image, ...commandArgs ]; Logger.debug(`docker ${args.join(" ")}`); const { execa } = await importNodeModule("execa"); const proc = execa("docker", args, { stdio: "inherit" }); await proc; } catch (_) { var _error = _, _hasError = true; } finally { __callDispose(_stack, _error, _hasError); } } catch (error) { throw new Error( "An error occurred on the running container. Please see logs above." ); } } async function buildPDFWithContainer({ target, config, inlineConfig }) { const sourceUrl = new URL(await getSourceUrl(config)); if (sourceUrl.origin === config.rootUrl) { sourceUrl.hostname = CONTAINER_LOCAL_HOSTNAME; } const bypassedOption = { ...inlineConfig, input: { format: "webbook", entry: sourceUrl.href }, output: [ { ...target, path: toContainerPath(target.path) } ], host: CONTAINER_LOCAL_HOSTNAME }; await runContainer({ image: config.image, userVolumeArgs: collectVolumeArgs([ ...typeof config.serverRootDir === "string" ? [config.serverRootDir] : [], upath.dirname(target.path) ]), env: [["VS_CLI_BUILD_PDF_OPTIONS", JSON.stringify(bypassedOption)]], commandArgs: ["build"], workdir: typeof config.serverRootDir === "string" ? toContainerPath(config.serverRootDir) : void 0 }); return target.path; } // src/output/pdf.ts import fs2 from "node:fs"; import { URL as URL2 } from "node:url"; import terminalLink from "terminal-link"; import upath3 from "upath"; import { cyan, gray, green, red } from "yoctocolors"; // src/output/pdf-postprocess.ts import decamelize from "decamelize"; import fs from "node:fs"; import os from "node:os"; import upath2 from "upath"; import { v1 as uuid } from "uuid"; var prefixes = { dcterms: "http://purl.org/dc/terms/", meta: "http://idpf.org/epub/vocab/package/meta/#" }; var metaTerms = { title: `${prefixes.dcterms}title`, creator: `${prefixes.dcterms}creator`, description: `${prefixes.dcterms}description`, subject: `${prefixes.dcterms}subject`, contributor: `${prefixes.dcterms}contributor`, language: `${prefixes.dcterms}language`, role: `${prefixes.meta}role`, created: `${prefixes.meta}created`, date: `${prefixes.meta}date` }; async function pressReadyWithContainer({ input, output, preflightOption, image }) { await runContainer({ image, entrypoint: "press-ready", userVolumeArgs: collectVolumeArgs([ upath2.dirname(input), upath2.dirname(output) ]), commandArgs: [ "build", "-i", toContainerPath(input), "-o", toContainerPath(output), ...preflightOption.map((opt) => `--${decamelize(opt, { separator: "-" })}`).filter((str) => /^[\w-]+/.test(str)) ] }); } var PostProcess = class _PostProcess { constructor(document2) { this.document = document2; } static async load(pdf) { const { PDFDocument } = await importNodeModule("pdf-lib"); const document2 = await PDFDocument.load(pdf, { updateMetadata: false }); return new _PostProcess(document2); } async save(output, { preflight, preflightOption, image }) { const input = preflight ? upath2.join(os.tmpdir(), `vivliostyle-cli-${uuid()}.pdf`) : output; const pdf = await this.document.save(); await fs.promises.writeFile(input, pdf); if (preflight === "press-ready-local" || preflight === "press-ready" && isInContainer()) { var _stack = []; try { const _3 = __using(_stack, Logger.suspendLogging("Running press-ready")); const { build: build2 } = await importNodeModule("press-ready"); await build2({ ...preflightOption.reduce((acc, opt) => { const optName = decamelize(opt, { separator: "-" }); return optName.startsWith("no-") ? { ...acc, [optName.slice(3)]: false } : { ...acc, [optName]: true }; }, {}), input, output }); } catch (_) { var _error = _, _hasError = true; } finally { __callDispose(_stack, _error, _hasError); } } else if (preflight === "press-ready") { var _stack2 = []; try { const _3 = __using(_stack2, Logger.suspendLogging("Running press-ready")); await pressReadyWithContainer({ input, output, preflightOption, image }); } catch (_2) { var _error2 = _2, _hasError2 = true; } finally { __callDispose(_stack2, _error2, _hasError2); } } } async metadata(tree, { pageProgression, browserVersion, viewerCoreVersion, disableCreatorOption } = {}) { const { ReadingDirection } = await importNodeModule("pdf-lib"); const title = tree[metaTerms.title]?.[0].v; if (title) { this.document.setTitle(title); } const author = tree[metaTerms.creator]?.map((item) => item.v)?.join("; "); if (author) { this.document.setAuthor(author); } const subject = tree[metaTerms.description]?.[0].v; if (subject) { this.document.setSubject(subject); } const keywords = tree[metaTerms.subject]?.map((item) => item.v); if (keywords) { this.document.setKeywords(keywords); } let creatorOpt = `Vivliostyle.js ${viewerCoreVersion ?? coreVersion}`; if (browserVersion) { creatorOpt += `; ${browserVersion}`; } this.document.setCreator( disableCreatorOption ? "Vivliostyle" : `Vivliostyle (${creatorOpt})` ); const language = tree[metaTerms.language]?.[0].v; if (language) { this.document.setLanguage(language); } const creation = (tree[metaTerms.created] || tree[metaTerms.date])?.[0].v; const creationDate = creation && new Date(creation); if (creationDate) { this.document.setCreationDate(creationDate); } if (pageProgression === "rtl") { const viewerPrefs = this.document.catalog.getOrCreateViewerPreferences(); viewerPrefs.setReadingDirection(ReadingDirection.R2L); } } async toc(items) { const { PDFDict, PDFHexString, PDFName, PDFNumber } = await importNodeModule("pdf-lib"); if (!items || !items.length) { return; } const addRefs = (items2, parentRef) => items2.map((item) => { const ref = this.document.context.nextRef(); return { ...item, parentRef, ref, children: addRefs(item.children, ref) }; }); const countAll = (items2) => items2.reduce((sum, item) => sum + countAll(item.children), items2.length); const addObjectsToPDF = (items2) => { for (const [i, item] of items2.entries()) { const child = PDFDict.withContext(this.document.context); child.set(PDFName.of("Title"), PDFHexString.fromText(item.title)); child.set(PDFName.of("Dest"), PDFName.of(item.id)); child.set(PDFName.of("Parent"), item.parentRef); const prev = items2[i - 1]; if (prev) { child.set(PDFName.of("Prev"), prev.ref); } const next = items2[i + 1]; if (next) { child.set(PDFName.of("Next"), next.ref); } if (item.children.length) { child.set(PDFName.of("First"), item.children[0].ref); child.set( PDFName.of("Last"), item.children[item.children.length - 1].ref ); child.set(PDFName.of("Count"), PDFNumber.of(countAll(item.children))); } this.document.context.assign(item.ref, child); addObjectsToPDF(item.children); } }; const outlineRef = this.document.context.nextRef(); const itemsWithRefs = addRefs(items, outlineRef); addObjectsToPDF(itemsWithRefs); const outline = PDFDict.withContext(this.document.context); outline.set(PDFName.of("First"), itemsWithRefs[0].ref); outline.set( PDFName.of("Last"), itemsWithRefs[itemsWithRefs.length - 1].ref ); outline.set(PDFName.of("Count"), PDFNumber.of(countAll(itemsWithRefs))); this.document.context.assign(outlineRef, outline); this.document.catalog.set(PDFName.of("Outlines"), outlineRef); } async setPageBoxes(pageSizeData) { if (pageSizeData.length + 1 === this.document.getPageCount()) { this.document.removePage(pageSizeData.length); } if (pageSizeData.length !== this.document.getPageCount()) { return; } for (let i = 0; i < pageSizeData.length; i++) { const page = this.document.getPage(i); const sizeData = pageSizeData[i]; if (!sizeData.mediaWidth || !sizeData.mediaHeight || isNaN(sizeData.bleedOffset) || isNaN(sizeData.bleedSize)) { continue; } const yOffset = page.getHeight() - sizeData.mediaHeight; page.setMediaBox(0, yOffset, sizeData.mediaWidth, sizeData.mediaHeight); if (!sizeData.bleedOffset && !sizeData.bleedSize) { continue; } page.setBleedBox( sizeData.bleedOffset, yOffset + sizeData.bleedOffset, sizeData.mediaWidth - sizeData.bleedOffset * 2, sizeData.mediaHeight - sizeData.bleedOffset * 2 ); const trimOffset = sizeData.bleedOffset + sizeData.bleedSize; page.setTrimBox( trimOffset, yOffset + trimOffset, sizeData.mediaWidth - trimOffset * 2, sizeData.mediaHeight - trimOffset * 2 ); } } }; // src/output/pdf.ts async function buildPDF({ target, config }) { Logger.logUpdate(`Launching PDF build environment`); const viewerFullUrl = await getViewerFullUrl(config); Logger.debug("viewerFullUrl", viewerFullUrl); let lastEntry; function stringifyEntry(entry) { const formattedSourcePath = cyan( entry.source.type === "file" ? upath3.relative(config.entryContextDir, entry.source.pathname) : entry.source.href ); return `${terminalLink( formattedSourcePath, entry.source.type === "file" ? `file://${entry.source.pathname}` : entry.source.href, { fallback: () => formattedSourcePath } )} ${entry.title ? gray(entry.title) : ""}`; } function handleEntry(response) { const entry = config.entries.find((entry2) => { if (!("source" in entry2)) { return false; } const url = new URL2(response.url()); return url.protocol === "file:" ? pathEquals(entry2.target, url.pathname) : pathEquals( upath3.relative(config.workspaceDir, entry2.target), url.pathname.substring(1) ); }); if (entry) { if (!lastEntry) { lastEntry = entry; Logger.logUpdate(stringifyEntry(entry)); return; } Logger.logSuccess(stringifyEntry(lastEntry)); Logger.startLogging(stringifyEntry(entry)); lastEntry = entry; } } const { browser, page } = await launchPreview({ mode: "build", url: viewerFullUrl, config, onBrowserOpen: () => { Logger.logUpdate("Building pages"); }, onPageOpen: async (page2) => { page2.on("pageerror", (error) => { Logger.logError(red(error.message)); }); page2.on("console", (msg) => { switch (msg.type()) { case "error": if (/\/vivliostyle-viewer\.js$/.test(msg.location().url ?? "")) { Logger.logError(msg.text()); throw msg.text(); } return; case "debug": if (/time slice/.test(msg.text())) { return; } break; } if (msg.type() === "error") { Logger.logVerbose(red("console.error()"), msg.text()); } else { Logger.logVerbose(gray(`console.${msg.type()}()`), msg.text()); } }); page2.on("response", (response) => { Logger.debug( gray("viewer:response"), green(response.status().toString()), response.url() ); handleEntry(response); if (300 > response.status() && 200 <= response.status()) return; if (response.url().startsWith("file://") && response.ok()) return; Logger.logError(red(`${response.status()}`), response.url()); }); await page2.setDefaultTimeout(config.timeout); } }); const browserName = getFullBrowserName(config.browser.type); const browserVersion = `${browserName}/${await browser.version()}`; Logger.debug(green("success"), `browserVersion=${browserVersion}`); let remainTime = config.timeout; const startTime = Date.now(); await page.waitForLoadState("networkidle"); await page.waitForFunction(() => !!window.coreViewer); await page.emulateMedia({ media: "print" }); await page.waitForFunction( /* v8 ignore next */ () => window.coreViewer.readyState === "complete", void 0, { polling: 1e3 } ); if (lastEntry) { Logger.logSuccess(stringifyEntry(lastEntry)); } const pageProgression = await page.evaluate( () => ( /* v8 ignore next 5 */ document.querySelector("#vivliostyle-viewer-viewport")?.getAttribute("data-vivliostyle-page-progression") === "rtl" ? "rtl" : "ltr" ) ); const viewerCoreVersion = await page.evaluate( () => ( /* v8 ignore next 3 */ document.querySelector("#vivliostyle-menu_settings .version")?.textContent?.replace(/^.*?: (\d[-+.\w]+).*$/, "$1") ) ); const metadata = await loadMetadata(page); const toc = await loadTOC(page); const pageSizeData = await loadPageSizeData(page); remainTime -= Date.now() - startTime; if (remainTime <= 0) { throw new Error("Typesetting process timed out"); } Logger.debug("Remaining timeout:", remainTime); Logger.logUpdate("Building PDF"); const pdf = await page.pdf({ margin: { top: 0, bottom: 0, right: 0, left: 0 }, printBackground: true, preferCSSPageSize: true, tagged: true // timeout: remainTime, }); await browser.close(); Logger.logUpdate("Processing PDF"); fs2.mkdirSync(upath3.dirname(target.path), { recursive: true }); const post = await PostProcess.load(pdf); await post.metadata(metadata, { pageProgression, browserVersion, viewerCoreVersion, // If custom viewer is set and its version info is not available, // there is no guarantee that the default creator option is correct. disableCreatorOption: !!config.viewer && !viewerCoreVersion }); await post.toc(toc); await post.setPageBoxes(pageSizeData); await post.save(target.path, { preflight: target.preflight, preflightOption: target.preflightOption, image: config.image }); return target.path; } async function loadMetadata(page) { return page.evaluate(() => window.coreViewer.getMetadata()); } async function loadTOC(page) { return page.evaluate( () => new Promise((resolve) => { function listener(payload) { if (payload.a !== "toc") { return; } window.coreViewer.removeListener("done", listener); window.coreViewer.showTOC(false); resolve(window.coreViewer.getTOC()); } window.coreViewer.addListener("done", listener); window.coreViewer.showTOC(true); }) ); } async function loadPageSizeData(page) { return page.evaluate(() => { const sizeData = []; const pageContainers = document.querySelectorAll( "#vivliostyle-viewer-viewport > div > div > div[data-vivliostyle-page-container]" ); for (const pageContainer of pageContainers) { const bleedBox = pageContainer.querySelector( "div[data-vivliostyle-bleed-box]" ); sizeData.push({ mediaWidth: parseFloat(pageContainer.style.width) * 0.75, mediaHeight: parseFloat(pageContainer.style.height) * 0.75, bleedOffset: parseFloat(bleedBox?.style.left) * 0.75, bleedSize: parseFloat(bleedBox?.style.paddingLeft) * 0.75 }); } return sizeData; }); } // src/core/build.ts async function build(inlineConfig, { containerForkMode = false } = {}) { Logger.setLogLevel(inlineConfig.logLevel); Logger.setCustomLogger(inlineConfig.logger); if (containerForkMode) { Logger.setLogPrefix(gray2("[Docker]")); } Logger.debug("build > inlineConfig %O", inlineConfig); let vivliostyleConfig = await loadVivliostyleConfig(inlineConfig) ?? setupConfigFromFlags(inlineConfig); warnDeprecatedConfig(vivliostyleConfig); vivliostyleConfig = mergeInlineConfig(vivliostyleConfig, { ...inlineConfig, quick: false }); const { inlineOptions } = vivliostyleConfig; Logger.debug("build > vivliostyleConfig %O", vivliostyleConfig); for (let [i, task] of vivliostyleConfig.tasks.entries()) { var _stack2 = []; try { const _3 = __using(_stack2, Logger.startLogging("Start building")); const config = resolveTaskConfig(task, inlineOptions); Logger.debug("build > config %O", config); const viteConfig = await resolveViteConfig({ ...config, mode: "build" }); let server; if (!containerForkMode) { Logger.debug("build > viteConfig.configFile %s", viteConfig.configFile); if (viteConfig.configFile && typeof config.serverRootDir === "string") { var _stack = []; try { const _4 = __using(_stack, Logger.suspendLogging("Building Vite project")); await viteBuild({ configFile: viteConfig.configFile, root: config.serverRootDir }); } catch (_) { var _error = _, _hasError = true; } finally { __callDispose(_stack, _error, _hasError); } } if (!inlineConfig.disableServerStartup) { server = await createViteServer({ config, viteConfig, inlineConfig, mode: "build" }); } if (isWebPubConfig(config)) { await cleanupWorkspace(config); await prepareThemeDirectory(config); await compile(config); await copyAssets(config); } } for (const target of config.outputs) { let output = null; const { format } = target; if (format === "pdf") { if (!containerForkMode && target.renderMode === "docker") { output = await buildPDFWithContainer({ target, config, inlineConfig }); } else { output = await buildPDF({ target, config }); } } else if (format === "webpub" || format === "epub") { output = await buildWebPublication({ target, config }); } if (output && !containerForkMode) { const formattedOutput = cyan2( upath4.relative(inlineConfig.cwd ?? cwd, output) ); Logger.logSuccess( `Finished building ${terminalLink2( formattedOutput, pathToFileURL2(output).href, { fallback: () => formattedOutput } )}` ); } } await server?.close(); } catch (_2) { var _error2 = _2, _hasError2 = true; } finally { __callDispose(_stack2, _error2, _hasError2); } } runExitHandlers(); if (!containerForkMode) { const num = vivliostyleConfig.tasks.flatMap((t) => t.output ?? []).length; const symbol = isUnicodeSupported ? `${num > 1 ? "\u{1F4DA}" : randomBookSymbol} ` : ""; Logger.log(`${symbol}Built successfully!`); } } export { build }; //# sourceMappingURL=chunk-XDT2MS4O.js.map