import chalk from 'chalk' ; import shelljs from 'shelljs' ; import terminalLink from 'terminal-link' ; import path from 'upath' ; import { URL } from 'url' ; import { checkBrowserAvailability , downloadBrowser , launchBrowser , } from './browser' ; import { ManuscriptEntry , MergedConfig } from './config' ; import { checkContainerEnvironment , collectVolumeArgs , runContainer , toContainerPath , } from './container' ; import { Meta , Payload , TOCItem } from './global-viewer' ; import { PdfOutput } from './output' ; import { PostProcess } from './postprocess' ; import { prepareServer } from './server' ; import { debug , logError , logInfo , logSuccess , logUpdate , startLogging , } from './util' ; type PuppeteerPage = Resolved < ReturnType < Resolved < ReturnType < typeof launchBrowser > > [ 'newPage' ] > > ; export type BuildPdfOptions = Omit < MergedConfig , 'outputs' | 'input' > & { input : string ; target : PdfOutput ; } ; export async function buildPDFWithContainer ( option ) { const bypassedOption = { ... option , input : toContainerPath ( option . input ) , target : { ... option . target , path : toContainerPath ( option . target . path ) , } , entryContextDir : toContainerPath ( option . entryContextDir ) , workspaceDir : toContainerPath ( option . workspaceDir ) , customStyle : option . customStyle && toContainerPath ( option . customStyle ) , customUserStyle : option . customUserStyle && toContainerPath ( option . customUserStyle ) , sandbox : false , } ; await runContainer ( { image : option . image , userVolumeArgs : collectVolumeArgs ( [ option . workspaceDir , path . dirname ( option . target . path ) , ] ) , commandArgs : [ 'build' , '--bypassed-pdf-builder-option' , JSON . stringify ( bypassedOption ) , ] , } ) ; return option . target . path ; } export async function buildPDF ( { input , target , workspaceDir , size , customStyle , customUserStyle , singleDoc , executableChromium , image , sandbox , verbose , timeout , entryContextDir , entries , httpServer , viewer , } ) { const isInContainer = checkContainerEnvironment ( ) ; logUpdate ( `Launching build environment` ) ; const { viewerFullUrl } = await prepareServer ( { input , workspaceDir , httpServer , viewer , size , style : customStyle , userStyle : customUserStyle , singleDoc , quick : false , } ) ; debug ( 'viewerFullUrl' , viewerFullUrl ) ; debug ( `Executing Chromium path: ${ executableChromium }` ) ; if ( ! checkBrowserAvailability ( executableChromium ) ) { const puppeteerDir = path . dirname ( require . resolve ( 'puppeteer-core/package.json' ) ) ; if ( ! path . relative ( puppeteerDir , executableChromium ) . startsWith ( '..' ) ) { await downloadBrowser ( ) ; } else { throw new Error ( `Cannot find the browser. Please check the executable chromium path: ${ executableChromium }` ) ; } } const browser = await launchBrowser ( { headless : true , executablePath : executableChromium , args : [ '--allow-file-access-from-files' , sandbox ? '' : '--no-sandbox' , '--disable-web-security' , isInContainer ? '--disable-dev-shm-usage' : '' , ] , } ) ; const version = await browser . version ( ) ; debug ( chalk . green ( 'success' ) , `version=${ version }` ) ; logUpdate ( 'Building pages' ) ; await new Promise ( ( res ) => setTimeout ( res , 1000 ) ) ; const page = await browser . newPage ( ) ; page . on ( 'pageerror' , ( error ) => { logError ( chalk . red ( error . message ) ) ; } ) ; page . on ( 'console' , ( msg ) => { switch ( msg . type ( ) ) { case 'error' : if ( /\/vivliostyle-viewer\.js$/ . test ( msg . location ( ) . url ?? '' ) ) { logError ( msg . text ( ) ) ; throw msg . text ( ) ; } return ; case 'debug' : if ( /time slice/ . test ( msg . text ( ) ) ) { return ; } break ; } if ( ! verbose ) { return ; } if ( msg . type ( ) === 'error' ) { logError ( msg . text ( ) ) ; } else { logInfo ( msg . text ( ) ) ; } } ) ; let lastEntry ; function stringifyEntry ( entry ) { const formattedSourcePath = chalk . bold . cyan ( path . relative ( entryContextDir , entry . source ) ) ; return `${ terminalLink ( formattedSourcePath , 'file://' + entry . source , { fallback : ( ) => formattedSourcePath , } ) } ${ entry . title ? chalk . gray ( entry . title ) : '' }` ; } function handleEntry ( response ) { const entry = entries . find ( ( entry ) : entry is ManuscriptEntry => { if ( ! ( 'source' in entry ) ) { return false ; } const url = new URL ( response . url ( ) ) ; return url . protocol === 'file:' ? entry . target === url . pathname : path . relative ( workspaceDir , entry . target ) === url . pathname . substring ( 1 ) ; } ) ; if ( entry ) { if ( ! lastEntry ) { lastEntry = entry ; return logUpdate ( stringifyEntry ( entry ) ) ; } logSuccess ( stringifyEntry ( lastEntry ) ) ; startLogging ( stringifyEntry ( entry ) ) ; lastEntry = entry ; } } page . on ( 'response' , ( response ) => { debug ( chalk . gray ( 'viewer:response' ) , chalk . green ( response . status ( ) . toString ( ) ) , response . url ( ) ) ; handleEntry ( response ) ; if ( 300 > response . status ( ) && 200 <= response . status ( ) ) return ; if ( response . url ( ) . startsWith ( 'file://' ) && response . ok ( ) ) return ; logError ( chalk . red ( `${ response . status ( ) }` , response . url ( ) ) ) ; startLogging ( ) ; } ) ; await page . setDefaultNavigationTimeout ( timeout ) ; await page . goto ( viewerFullUrl , { waitUntil : 'networkidle0' } ) ; await page . waitForFunction ( ( ) => ! ! window . coreViewer ) ; await page . emulateMediaType ( 'print' ) ; await page . waitForFunction ( ( ) => window . coreViewer . readyState === 'complete' , { polling : 1000 , timeout , } ) ; if ( lastEntry ) { logSuccess ( stringifyEntry ( lastEntry ) ) ; } const metadata = await loadMetadata ( page ) ; const toc = await loadTOC ( page ) ; startLogging ( 'Building PDF' ) ; const pdf = await page . pdf ( { margin : { top : 0 , bottom : 0 , right : 0 , left : 0 , } , printBackground : true , preferCSSPageSize : true , } ) ; await browser . close ( ) ; logUpdate ( 'Processing PDF' ) ; shelljs . mkdir ( '-p' , path . dirname ( target . path ) ) ; const post = await PostProcess . load ( pdf ) ; await post . metadata ( metadata , { disableCreatorOption : ! ! viewer , } ) ; await post . toc ( toc ) ; await post . save ( target . path , { preflight : target . preflight , preflightOption : target . preflightOption , image , } ) ; return target . path ; } async function loadMetadata ( page ) { return page . evaluate ( ( ) => window . coreViewer . getMetadata ( ) ) ; } async function loadTOC ( page ) { return page . evaluate ( ( ) => new Promise < TOCItem [ ] > ( ( 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 ) ; } ) ) ; }