2017-08-18 13:51:31 +03:00
#!/usr/bin/env node
2017-08-22 11:48:55 +03:00
'use strict' ;
2022-12-06 01:06:22 +03:00
import chalk from 'chalk' ;
import crypto from 'crypto' ;
2023-06-23 09:44:26 +03:00
import { Font } from 'fonteditor-core' ;
2022-12-06 01:06:22 +03:00
import fs from 'fs' ;
import os from 'os' ;
import parser from './libs/nomnom.js' ;
import path from 'path' ;
import puppeteer from 'puppeteer' ;
import URI from 'urijs' ;
import util from 'util' ;
import { fileURLToPath } from 'url' ;
import { PDFDocument , PDFName , ParseSpeeds , decodePDFRawStream } from 'pdf-lib' ;
import { delay , pause } from './libs/util.js' ;
2017-08-18 13:51:31 +03:00
2017-08-22 11:48:55 +03:00
parser . script ( 'decktape' ) . options ( {
url : {
2017-11-10 19:49:52 +03:00
position : 1 ,
required : true ,
transform : parseUrl ,
help : 'URL of the slides deck' ,
2017-08-22 11:48:55 +03:00
} ,
filename : {
position : 2 ,
required : true ,
help : 'Filename of the output PDF file' ,
} ,
size : {
abbr : 's' ,
metavar : '<size>' ,
type : 'string' ,
callback : parseSize ,
transform : parseSize ,
2018-01-03 15:13:31 +03:00
help : 'Size of the slides deck viewport: <width>x<height> (e.g. \'1280x720\')' ,
2017-08-22 11:48:55 +03:00
} ,
pause : {
abbr : 'p' ,
metavar : '<ms>' ,
default : 1000 ,
help : 'Duration in milliseconds before each slide is exported' ,
} ,
loadPause : {
full : 'load-pause' ,
metavar : '<ms>' ,
default : 0 ,
help : 'Duration in milliseconds between the page has loaded and starting to export slides' ,
} ,
2022-12-09 20:26:37 +03:00
urlLoadTimeout : {
full : 'url-load-timeout' ,
metavar : '<ms>' ,
default : 60000 ,
help : 'Timeout in milliseconds to use when waiting for the initial URL to load' ,
2022-08-18 23:11:54 +03:00
} ,
2022-12-09 20:26:37 +03:00
pageLoadTimeout : {
full : 'page-load-timeout' ,
metavar : '<ms>' ,
default : 20000 ,
help : 'Timeout in milliseconds to use when waiting for the slide deck page to load' ,
2022-08-18 23:11:54 +03:00
} ,
2022-12-09 20:26:37 +03:00
bufferTimeout : {
full : 'buffer-timeout' ,
metavar : '<ms>' ,
default : 30000 ,
help : 'Timeout in milliseconds to use when waiting for a slide to finish buffering (set to 0 to disable)' ,
2022-11-28 23:10:32 +03:00
} ,
2017-08-22 11:48:55 +03:00
screenshots : {
default : false ,
flag : true ,
help : 'Capture each slide as an image' ,
} ,
screenshotDirectory : {
full : 'screenshots-directory' ,
metavar : '<dir>' ,
default : 'screenshots' ,
help : 'Screenshots output directory' ,
} ,
2018-10-19 13:08:30 +03:00
screenshotSizes : {
2017-08-22 11:48:55 +03:00
full : 'screenshots-size' ,
metavar : '<size>' ,
type : 'string' ,
list : true ,
callback : parseSize ,
transform : parseSize ,
help : 'Screenshots resolution, can be repeated' ,
} ,
screenshotFormat : {
full : 'screenshots-format' ,
metavar : '<format>' ,
default : 'png' ,
choices : [ 'jpg' , 'png' ] ,
help : 'Screenshots image format, one of [jpg, png]' ,
} ,
slides : {
metavar : '<range>' ,
type : 'string' ,
callback : parseRange ,
transform : parseRange ,
help : 'Range of slides to be exported, a combination of slide indexes and ranges (e.g. \'1-3,5,8\')' ,
2017-08-25 13:41:05 +03:00
} ,
2023-05-22 19:40:03 +03:00
headless : {
default : 'new' , // false to enable headed mode and true to enable old puppeteer headless. See: https://developer.chrome.com/articles/new-headless/#new-headless-in-puppeteer
2023-05-23 12:26:47 +03:00
help : 'Puppeteer headless mode, one if [new, true, false]' ,
2023-05-22 19:40:03 +03:00
} ,
headers : {
type : 'string' ,
callback : parseHeaders ,
transform : parseHeaders ,
2023-05-23 12:26:47 +03:00
help : 'HTTP headers, comma-separated list of <header>,<value> pairs (e.g. "Authorization,\'Bearer ASDJASLKJALKSJDL\'")' ,
2023-05-22 19:40:03 +03:00
} ,
2017-08-25 14:33:56 +03:00
// Chrome options
2018-10-19 13:08:30 +03:00
chromePath : {
full : 'chrome-path' ,
2017-08-25 14:33:56 +03:00
metavar : '<path>' ,
type : 'string' ,
2018-10-19 13:08:30 +03:00
help : 'Path to the Chromium or Chrome executable to run instead of the bundled Chromium' ,
2017-08-25 14:33:56 +03:00
} ,
2018-10-19 13:08:30 +03:00
chromeArgs : {
full : 'chrome-arg' ,
metavar : '<arg>' ,
type : 'string' ,
list : true ,
help : 'Additional argument to pass to the Chrome instance, can be repeated' ,
2017-08-25 13:41:05 +03:00
} ,
2021-09-29 21:37:55 +03:00
// PDF meta data
metaAuthor : {
full : 'pdf-author' ,
metavar : '<arg>' ,
type : 'string' ,
2023-05-23 12:26:47 +03:00
help : 'String to set as the author of the resulting PDF document' ,
2021-09-29 21:37:55 +03:00
} ,
metaTitle : {
full : 'pdf-title' ,
metavar : '<arg>' ,
type : 'string' ,
2023-05-23 12:26:47 +03:00
help : 'String to set as the title of the resulting PDF document' ,
2021-09-29 21:37:55 +03:00
} ,
metaSubject : {
full : 'pdf-subject' ,
metavar : '<arg>' ,
type : 'string' ,
2023-05-23 12:26:47 +03:00
help : 'String to set as the subject of the resulting PDF document' ,
2021-09-29 21:37:55 +03:00
} ,
2017-08-22 11:48:55 +03:00
} ) ;
2017-08-18 13:51:31 +03:00
2023-05-22 19:40:03 +03:00
function parseHeaders ( headerString ) {
2023-05-23 12:26:47 +03:00
const h = headerString . split ( "," ) ;
2023-05-22 19:40:03 +03:00
if ( ( h . length % 2 ) != 0 ) {
2023-05-23 12:26:47 +03:00
return 'header flag must be a comma delimited key value pairing and should always have an even number of kv pairs' ;
2023-05-22 19:40:03 +03:00
}
2023-05-23 12:26:47 +03:00
let headers = { } ;
2023-05-22 19:40:03 +03:00
for ( let i = 0 ; i < h . length ; i += 2 ) {
2023-05-23 12:26:47 +03:00
headers [ h [ i ] ] = h [ i + 1 ] ;
2023-05-22 19:40:03 +03:00
}
2023-05-23 12:26:47 +03:00
return headers ;
2023-05-22 19:40:03 +03:00
}
2017-08-18 13:51:31 +03:00
function parseSize ( size ) {
2017-12-20 13:04:44 +03:00
// we may want to support height and width labeled with units
// /^(\d+(?:px)?|\d+(?:\.\d+)?(?:in|cm|mm)?)\s?x\s?(\d+(?:px)?|\d+(?:\.\d+)?(?:in|cm|mm)?)$/
const match = size . match ( /^(\d+)x(\d+)$/ ) ;
if ( match ) {
const [ , width , height ] = match ;
2018-01-03 13:20:59 +03:00
return { width : parseInt ( width , 10 ) , height : parseInt ( height , 10 ) } ;
2017-12-20 13:04:44 +03:00
} else {
return '<size> must follow the <width>x<height> notation, e.g., \'1280x720\'' ;
}
2017-08-18 13:51:31 +03:00
}
function parseRange ( range ) {
2017-08-22 11:48:55 +03:00
const regex = /(\d+)(?:-(\d+))?/g ;
if ( ! range . match ( regex ) )
return '<range> must be a combination of slide indexes and ranges, e.g., \'1-3,5,8\'' ;
let slide , slides = { } ;
while ( ( slide = regex . exec ( range ) ) !== null ) {
2017-08-31 19:36:58 +03:00
const [ , m , n ] = slide . map ( i => parseInt ( i ) ) ;
if ( isNaN ( n ) ) {
slides [ m ] = true ;
} else {
for ( let i = m ; i <= n ; i ++ ) {
2017-08-22 11:48:55 +03:00
slides [ i ] = true ;
}
}
}
return slides ;
2017-08-18 13:51:31 +03:00
}
2017-11-10 19:49:52 +03:00
function parseUrl ( url ) {
const uri = URI ( url ) ;
if ( ! uri . protocol ( ) ) {
2017-11-18 17:25:31 +03:00
if ( path . isAbsolute ( url ) ) {
return 'file://' + path . normalize ( url ) ;
} else {
return 'file://' + path . normalize ( path . join ( process . cwd ( ) , url ) ) ;
}
2017-11-10 19:49:52 +03:00
}
return url ;
}
2017-08-22 18:53:50 +03:00
parser . command ( 'version' )
. root ( true )
. help ( 'Display decktape package version' )
. callback ( _ => {
2022-12-06 01:06:22 +03:00
const pkg = JSON . parse ( fs . readFileSync ( new URL ( './package.json' , import . meta . url ) ) ) ;
console . log ( pkg . version ) ;
2017-08-22 18:53:50 +03:00
process . exit ( ) ;
} ) ;
parser . nocommand ( )
. help (
2017-08-22 11:48:55 +03:00
` Defaults to the automatic command.
Iterates over the available plugins , picks the compatible one for presentation at the
specified < url > and uses it to export and write the PDF into the specified < filename > . `
2017-08-22 18:53:50 +03:00
) ;
parser . command ( 'automatic' )
. help (
2017-08-22 11:48:55 +03:00
` Iterates over the available plugins, picks the compatible one for presentation at the
specified < url > and uses it to export and write the PDF into the specified < filename > . `
2017-08-22 18:53:50 +03:00
) ;
2022-12-06 01:06:22 +03:00
2017-08-18 13:51:31 +03:00
// TODO: should be deactivated as well when it does not execute in a TTY context
2017-08-22 11:48:55 +03:00
if ( os . name === 'windows' ) parser . nocolors ( ) ;
2018-08-23 13:36:58 +03:00
const color = type => {
switch ( type ) {
case 'error' : return chalk . red ;
2024-03-02 17:33:33 +03:00
case 'warn' : return chalk . yellow ;
2018-08-23 13:36:58 +03:00
default : return chalk . gray ;
}
} ;
2017-09-12 17:28:14 +03:00
process . on ( 'unhandledRejection' , error => {
console . log ( error . stack ) ;
process . exit ( 1 ) ;
} ) ;
2017-08-22 11:48:55 +03:00
( async ( ) => {
2022-12-06 01:06:22 +03:00
const plugins = await loadAvailablePlugins ( path . join ( path . dirname ( fileURLToPath ( import . meta . url ) ) , 'plugins' ) ) ;
Object . entries ( plugins ) . forEach ( ( [ id , plugin ] ) => {
const command = parser . command ( id ) ;
if ( typeof plugin . options === 'object' ) {
command . options ( plugin . options ) ;
}
if ( typeof plugin . help === 'string' ) {
command . help ( plugin . help ) ;
}
} ) ;
const options = parser . parse ( process . argv . slice ( 2 ) ) ;
2017-08-22 11:48:55 +03:00
2017-08-25 13:41:05 +03:00
const browser = await puppeteer . launch ( {
2023-05-22 19:40:03 +03:00
headless : options . headless ,
2018-07-11 20:52:06 +03:00
// TODO: add a verbose option
2018-10-19 13:08:30 +03:00
// dumpio : true,
executablePath : options . chromePath ,
args : options . chromeArgs ,
2017-08-25 13:41:05 +03:00
} ) ;
2017-08-25 18:49:39 +03:00
const page = await browser . newPage ( ) ;
2023-05-22 19:40:03 +03:00
if ( options . headers )
page . setExtraHTTPHeaders ( options . headers )
2020-01-02 22:01:47 +03:00
await page . emulateMediaType ( 'screen' ) ;
2020-05-15 21:16:56 +03:00
const pdf = await PDFDocument . create ( ) ;
pdf . setCreator ( 'Decktape' ) ;
2021-09-29 21:37:55 +03:00
if ( options . metaAuthor )
pdf . setAuthor ( options . metaAuthor ) ;
if ( options . metaSubject )
pdf . setSubject ( options . metaSubject ) ;
2022-12-09 20:26:37 +03:00
if ( options . metaTitle )
2021-09-29 21:37:55 +03:00
pdf . setTitle ( options . metaTitle ) ;
2017-08-22 11:48:55 +03:00
2017-08-23 11:43:03 +03:00
page
2018-08-23 13:36:58 +03:00
. on ( 'console' , async msg => {
2024-03-02 17:33:33 +03:00
if ( msg . type ( ) === 'log' ) {
const args = await Promise . all ( msg . args ( ) . map ( arg => arg . evaluate ( obj => obj , arg ) ) ) ;
2023-12-23 17:50:54 +03:00
console . log ( ... args . map ( arg => color ( msg . type ( ) ) ( util . format ( arg ) ) ) ) ;
2024-03-02 17:33:33 +03:00
} else {
console . log ( color ( msg . type ( ) ) ( util . format ( msg . text ( ) ) ) ) ;
}
2018-07-19 10:38:20 +03:00
} )
2018-07-19 11:38:56 +03:00
. on ( 'requestfailed' , request => {
// do not output warning for cancelled requests
if ( request . failure ( ) && request . failure ( ) . errorText === 'net::ERR_ABORTED' ) return ;
2022-12-09 20:03:21 +03:00
console . log ( chalk . yellow ( '\nUnable to load resource from URL: %s' ) , request . url ( ) ) ;
2018-07-19 11:38:56 +03:00
} )
2022-12-09 20:03:21 +03:00
. on ( 'pageerror' , error => console . log ( chalk . red ( '\nPage error: %s' ) , error . message ) ) ;
2017-08-22 11:48:55 +03:00
2017-08-23 11:43:03 +03:00
console . log ( 'Loading page' , options . url , '...' ) ;
2022-08-18 23:11:54 +03:00
const load = page . waitForNavigation ( { waitUntil : 'load' , timeout : options . urlLoadTimeout } ) ;
page . goto ( options . url , { waitUntil : 'networkidle0' , timeout : options . pageLoadTimeout } )
2017-09-29 16:47:41 +03:00
// wait until the load event is dispatched
2018-05-09 19:11:19 +03:00
. then ( response => load
. catch ( error => response . status ( ) !== 200 ? Promise . reject ( error ) : response )
. then ( _ => response ) )
2018-09-14 09:42:40 +03:00
// TODO: improve message when reading file locally
2022-12-09 20:03:21 +03:00
. then ( response => console . log ( 'Loading page finished with status: %s' , response . status ( ) ) )
2017-08-22 11:48:55 +03:00
. then ( delay ( options . loadPause ) )
2022-12-09 20:26:37 +03:00
. then ( _ => createPlugin ( page , plugins , options ) )
2017-08-23 18:04:37 +03:00
. then ( plugin => configurePlugin ( plugin )
2022-12-09 20:26:37 +03:00
. then ( _ => configurePage ( page , plugin , options ) )
. then ( _ => exportSlides ( page , plugin , pdf , options ) )
2020-05-15 21:16:56 +03:00
. then ( async context => {
2020-08-11 18:18:56 +03:00
await writePdf ( options . filename , pdf ) ;
2022-12-09 20:03:21 +03:00
console . log ( chalk . green ( ` \n Printed ${ chalk . bold ( '%s' ) } slides ` ) , context . exportedSlides ) ;
2024-02-29 16:09:19 +03:00
// Wait for the browser to close before exiting the process
await browser . close ( ) ;
2017-09-11 15:50:59 +03:00
process . exit ( ) ;
2017-08-23 18:04:37 +03:00
} ) )
2024-02-29 16:09:19 +03:00
. catch ( async error => {
2022-12-09 20:03:21 +03:00
console . log ( chalk . red ( '\n%s' ) , error ) ;
2024-02-29 16:09:19 +03:00
// Wait for the browser to close before exiting the process
await browser . close ( ) ;
2017-08-22 11:48:55 +03:00
process . exit ( 1 ) ;
2017-08-18 13:51:31 +03:00
} ) ;
2022-12-09 20:26:37 +03:00
} ) ( ) ;
2017-08-22 11:48:55 +03:00
2022-12-09 20:26:37 +03:00
async function loadAvailablePlugins ( pluginsPath ) {
const plugins = await fs . promises . readdir ( pluginsPath ) ;
const entries = await Promise . all ( plugins . map ( async pluginPath => {
const [ , plugin ] = pluginPath . match ( /^(.*)\.js$/ ) ;
if ( plugin && ( await fs . promises . stat ( path . join ( pluginsPath , pluginPath ) ) ) . isFile ( ) ) {
return [ plugin , await import ( ` ./plugins/ ${ pluginPath } ` ) ] ;
2022-12-06 01:06:22 +03:00
}
2022-12-09 20:26:37 +03:00
} ) ) ;
return Object . fromEntries ( entries . filter ( Boolean ) ) ;
}
2017-08-18 13:51:31 +03:00
2022-12-09 20:26:37 +03:00
async function createPlugin ( page , plugins , options ) {
let plugin ;
if ( ! options . command || options . command === 'automatic' ) {
plugin = await createActivePlugin ( page , plugins , options ) ;
if ( ! plugin ) {
console . log ( 'No supported DeckTape plugin detected, falling back to generic plugin' ) ;
plugin = plugins [ 'generic' ] . create ( page , options ) ;
}
} else {
plugin = plugins [ options . command ] . create ( page , options ) ;
if ( ! await plugin . isActive ( ) ) {
throw Error ( ` Unable to activate the ${ plugin . getName ( ) } DeckTape plugin for the address: ${ options . url } ` ) ;
2017-08-23 18:04:37 +03:00
}
}
2022-12-09 20:26:37 +03:00
console . log ( chalk . cyan ( ` ${ chalk . bold ( '%s' ) } plugin activated ` ) , plugin . getName ( ) ) ;
return plugin ;
}
2017-08-23 18:04:37 +03:00
2022-12-09 20:26:37 +03:00
async function createActivePlugin ( page , plugins , options ) {
for ( let id in plugins ) {
if ( id === 'generic' ) continue ;
const plugin = plugins [ id ] . create ( page , options ) ;
if ( await plugin . isActive ( ) ) return plugin ;
2017-08-23 18:04:37 +03:00
}
2022-12-09 20:26:37 +03:00
}
2017-08-23 18:04:37 +03:00
2022-12-09 20:26:37 +03:00
async function configurePage ( page , plugin , options ) {
if ( ! options . size ) {
options . size = typeof plugin . size === 'function' ? await plugin . size ( ) : { width : 1280 , height : 720 } ;
2017-08-23 18:04:37 +03:00
}
2022-12-09 20:26:37 +03:00
await page . setViewport ( options . size ) ;
}
2017-08-23 18:04:37 +03:00
2022-12-09 20:26:37 +03:00
async function configurePlugin ( plugin ) {
if ( typeof plugin . configure === 'function' ) {
await plugin . configure ( ) ;
2020-05-15 21:16:56 +03:00
}
2022-12-09 20:26:37 +03:00
}
2017-08-18 13:51:31 +03:00
2022-12-09 20:26:37 +03:00
async function exportSlides ( page , plugin , pdf , options ) {
const context = {
progressBarOverflow : 0 ,
currentSlide : 1 ,
exportedSlides : 0 ,
pdfFonts : { } ,
pdfXObjects : { } ,
totalSlides : await plugin . slideCount ( ) ,
} ;
// TODO: support a more advanced "fragment to pause" mapping
// for special use cases like GIF animations
// TODO: support plugin optional promise to wait until a particular mutation
// instead of a pause
if ( options . slides && ! options . slides [ context . currentSlide ] ) {
process . stdout . write ( '\r' + await progressBar ( plugin , context , { skip : true } ) ) ;
} else {
await pause ( options . pause ) ;
await exportSlide ( page , plugin , pdf , context , options ) ;
}
const maxSlide = options . slides ? Math . max ( ... Object . keys ( options . slides ) ) : Infinity ;
let hasNext = await hasNextSlide ( plugin , context ) ;
while ( hasNext && context . currentSlide < maxSlide ) {
await nextSlide ( plugin , context ) ;
await pause ( options . pause ) ;
2017-09-14 19:54:48 +03:00
if ( options . slides && ! options . slides [ context . currentSlide ] ) {
process . stdout . write ( '\r' + await progressBar ( plugin , context , { skip : true } ) ) ;
2017-08-23 20:37:14 +03:00
} else {
2022-12-09 20:26:37 +03:00
await exportSlide ( page , plugin , pdf , context , options ) ;
2017-08-23 20:37:14 +03:00
}
2022-12-09 20:26:37 +03:00
hasNext = await hasNextSlide ( plugin , context ) ;
2017-08-23 18:04:37 +03:00
}
2022-12-09 20:26:37 +03:00
// Flush consolidated fonts
Object . values ( context . pdfFonts ) . forEach ( ( { ref , font } ) => {
2023-06-23 09:44:26 +03:00
pdf . context . assign ( ref , pdf . context . flateStream ( font . write ( { type : 'ttf' , hinting : true } ) ) ) ;
2022-12-09 20:26:37 +03:00
} ) ;
return context ;
}
2017-08-23 18:04:37 +03:00
2022-12-09 20:26:37 +03:00
async function exportSlide ( page , plugin , pdf , context , options ) {
process . stdout . write ( '\r' + await progressBar ( plugin , context ) ) ;
2017-09-14 19:54:48 +03:00
2022-12-09 20:26:37 +03:00
const buffer = await page . pdf ( {
width : options . size . width ,
height : options . size . height ,
printBackground : true ,
pageRanges : '1' ,
displayHeaderFooter : false ,
timeout : options . bufferTimeout ,
} ) ;
await printSlide ( pdf , await PDFDocument . load ( buffer , { parseSpeed : ParseSpeeds . Fastest } ) , context ) ;
context . exportedSlides ++ ;
if ( options . screenshots ) {
for ( let resolution of options . screenshotSizes || [ options . size ] ) {
await page . setViewport ( resolution ) ;
// Delay page rendering to wait for the resize event to complete,
// e.g. for impress.js (may be needed to be configurable)
await pause ( 1000 ) ;
await page . screenshot ( {
path : path . join ( options . screenshotDirectory , options . filename
. replace ( '.pdf' , ` _ ${ context . currentSlide } _ ${ resolution . width } x ${ resolution . height } . ${ options . screenshotFormat } ` ) ) ,
fullPage : false ,
omitBackground : true ,
} ) ;
await page . setViewport ( options . size ) ;
await pause ( 1000 ) ;
2017-08-24 18:04:28 +03:00
}
}
2022-12-09 20:26:37 +03:00
}
2017-08-23 18:04:37 +03:00
2022-12-09 20:26:37 +03:00
async function printSlide ( pdf , slide , context ) {
const duplicatedEntries = [ ] ;
const [ page ] = await pdf . copyPages ( slide , [ 0 ] ) ;
2023-06-23 13:41:17 +03:00
2022-12-09 20:26:37 +03:00
pdf . addPage ( page ) ;
// Traverse the page to consolidate duplicates
parseResources ( page . node ) ;
// And delete all the collected duplicates
duplicatedEntries . forEach ( ref => pdf . context . delete ( ref ) ) ;
function parseResources ( dictionary ) {
const resources = dictionary . get ( PDFName . Resources ) ;
if ( resources . has ( PDFName . XObject ) ) {
const xObject = resources . get ( PDFName . XObject ) ;
xObject . entries ( ) . forEach ( entry => parseXObject ( entry , xObject ) ) ;
2020-05-15 21:16:56 +03:00
}
2022-12-09 20:26:37 +03:00
if ( resources . has ( PDFName . Font ) ) {
resources . get ( PDFName . Font ) . entries ( ) . forEach ( parseFont ) ;
}
}
2020-05-15 21:16:56 +03:00
2022-12-09 20:26:37 +03:00
function parseXObject ( [ name , entry ] , xObject ) {
const object = page . node . context . lookup ( entry ) ;
const subtype = object . dict . get ( PDFName . of ( 'Subtype' ) ) ;
if ( subtype === PDFName . of ( 'Image' ) ) {
const digest = crypto . createHash ( 'SHA1' ) . update ( object . contents ) . digest ( 'hex' ) ;
2023-11-04 18:52:43 +03:00
const existing = context . pdfXObjects [ digest ] ;
if ( ! existing ) {
// Store the entry that'll replace references with the same content
2022-12-09 20:26:37 +03:00
context . pdfXObjects [ digest ] = entry ;
2023-11-04 18:52:43 +03:00
} else if ( entry !== existing ) {
// Only remove references from different pages
2022-12-09 20:26:37 +03:00
xObject . set ( name , context . pdfXObjects [ digest ] ) ;
duplicatedEntries . push ( entry ) ;
2017-09-15 11:56:35 +03:00
}
2022-12-09 20:26:37 +03:00
} else {
parseResources ( object . dict ) ;
}
} ;
function parseFont ( [ _ , entry ] ) {
const object = page . node . context . lookup ( entry ) ;
const subtype = object . get ( PDFName . of ( 'Subtype' ) ) ;
// See "Introduction to Font Data Structures" from PDF specification
if ( subtype === PDFName . of ( 'Type0' ) ) {
// TODO: properly support composite fonts with multiple descendants
const descendant = page . node . context . lookup ( object . get ( PDFName . of ( 'DescendantFonts' ) ) . get ( 0 ) ) ;
if ( descendant . get ( PDFName . of ( 'Subtype' ) ) === PDFName . of ( 'CIDFontType2' ) ) {
const descriptor = page . node . context . lookup ( descendant . get ( PDFName . of ( 'FontDescriptor' ) ) ) ;
const ref = descriptor . get ( PDFName . of ( 'FontFile2' ) ) ;
const file = page . node . context . lookup ( ref ) ;
if ( ! file ) {
// The font has already been processed and removed
return ;
2017-09-24 17:00:41 +03:00
}
2022-12-09 20:26:37 +03:00
const bytes = decodePDFRawStream ( file ) . decode ( ) ;
2023-05-05 12:49:11 +03:00
let font ;
try {
// Some fonts written in the PDF may be ill-formed. Let's skip font compression in that case,
// until it's fixed in Puppeteer > Chromium > Skia.
// This happens for system fonts like Helvetica Neue for which cmap table is missing.
2023-06-23 09:44:26 +03:00
font = Font . create ( Buffer . from ( bytes ) , { type : 'ttf' , hinting : true } ) ;
2023-05-05 12:49:11 +03:00
} catch ( e ) {
console . log ( chalk . yellow ( '\nSkipping font compression: %s' ) , e . message ) ;
return ;
}
2023-06-23 13:41:17 +03:00
// Some fonts happen to miss some metadata and tables required by fonteditor
2023-06-23 09:44:26 +03:00
if ( ! font . data . name ) {
font . data . name = { } ;
2022-12-09 20:26:37 +03:00
}
2023-06-23 13:41:17 +03:00
if ( ! font . data [ 'OS/2' ] ) {
font . data [ 'OS/2' ] = { } ;
}
2022-12-09 20:26:37 +03:00
// PDF font name does not contain sub family on Windows 10,
// so a more robust key is computed from the font metadata
2023-06-23 09:44:26 +03:00
const id = descriptor . get ( PDFName . of ( 'FontName' ) ) . value ( ) + ' - ' + fontMetadataKey ( font . data . name ) ;
2022-12-09 20:26:37 +03:00
if ( context . pdfFonts [ id ] ) {
const f = context . pdfFonts [ id ] . font ;
2023-06-23 09:44:26 +03:00
font . data . glyf . forEach ( ( g , i ) => {
if ( g . contours && g . contours . length > 0 ) {
if ( ! f . data . glyf [ i ] || ! f . data . glyf [ i ] . contours || f . data . glyf [ i ] . contours . length === 0 ) {
mergeGlyph ( f , i , g ) ;
}
} else if ( g . compound ) {
if ( ! f . data . glyf [ i ] || typeof f . data . glyf [ i ] . compound === 'undefined' ) {
mergeGlyph ( f , i , g ) ;
2022-12-09 20:26:37 +03:00
}
}
2023-06-23 09:44:26 +03:00
} ) ;
2022-12-09 20:26:37 +03:00
descriptor . set ( PDFName . of ( 'FontFile2' ) , context . pdfFonts [ id ] . ref ) ;
duplicatedEntries . push ( ref ) ;
} else {
context . pdfFonts [ id ] = { ref : ref , font : font } ;
2022-12-06 01:06:22 +03:00
}
2020-01-02 22:01:47 +03:00
}
}
2022-12-09 20:26:37 +03:00
} ;
2020-01-02 22:01:47 +03:00
2023-06-23 09:44:26 +03:00
function mergeGlyph ( font , index , glyf ) {
if ( font . data . glyf . length <= index ) {
for ( let i = font . data . glyf . length ; i < index ; i ++ ) {
font . data . glyf . push ( { contours : Array ( 0 ) , advanceWidth : 0 , leftSideBearing : 0 } ) ;
}
font . data . glyf . push ( glyf ) ;
} else {
font . data . glyf [ index ] = glyf ;
}
}
2022-12-09 20:26:37 +03:00
function fontMetadataKey ( font ) {
2023-06-23 09:44:26 +03:00
const keys = [ 'fontFamily' , 'fontSubFamily' , 'fullName' , 'preferredFamily' , 'preferredSubFamily' , 'uniqueSubFamily' ] ;
2022-12-09 20:26:37 +03:00
return Object . entries ( font )
. filter ( ( [ key , _ ] ) => keys . includes ( key ) )
2023-06-23 09:44:26 +03:00
. reduce ( ( r , [ k , v ] , i ) => r + ( i > 0 ? ',' : '' ) + k + '=' + v , '' ) ;
2020-05-15 21:16:56 +03:00
}
2022-12-09 20:26:37 +03:00
}
2017-08-18 13:51:31 +03:00
2022-12-09 20:26:37 +03:00
async function hasNextSlide ( plugin , context ) {
if ( typeof plugin . hasNextSlide === 'function' ) {
return await plugin . hasNextSlide ( ) ;
} else {
return context . currentSlide < context . totalSlides ;
2022-12-06 01:06:22 +03:00
}
2022-12-09 20:26:37 +03:00
}
async function nextSlide ( plugin , context ) {
context . currentSlide ++ ;
return plugin . nextSlide ( ) ;
}
2017-08-18 13:51:31 +03:00
2022-12-09 20:26:37 +03:00
async function writePdf ( filename , pdf ) {
const pdfDir = path . dirname ( filename ) ;
try {
fs . accessSync ( pdfDir , fs . constants . F _OK ) ;
} catch {
fs . mkdirSync ( pdfDir , { recursive : true } ) ;
2022-12-06 01:06:22 +03:00
}
2022-12-09 20:26:37 +03:00
fs . writeFileSync ( filename , await pdf . save ( { addDefaultPage : false } ) ) ;
}
// TODO: add progress bar, duration, ETA and file size
async function progressBar ( plugin , context , { skip } = { skip : false } ) {
const cols = [ ] ;
const index = await plugin . currentSlideIndex ( ) ;
cols . push ( ` ${ skip ? 'Skipping' : 'Printing' } slide ` ) ;
cols . push ( ` # ${ index } ` . padEnd ( 8 ) ) ;
cols . push ( ' (' ) ;
cols . push ( ` ${ context . currentSlide } ` . padStart ( context . totalSlides ? context . totalSlides . toString ( ) . length : 3 ) ) ;
cols . push ( '/' ) ;
cols . push ( context . totalSlides || ' ?' ) ;
cols . push ( ') ...' ) ;
// erase overflowing slide fragments
cols . push ( ' ' . repeat ( Math . max ( context . progressBarOverflow - Math . max ( index . length + 1 - 8 , 0 ) , 0 ) ) ) ;
context . progressBarOverflow = Math . max ( index . length + 1 - 8 , 0 ) ;
return cols . join ( '' ) ;
}