// you may not need all of these but they come up a lot
import fs from "fs" ;
import logging from '../lib/logging.js' ;
import path from "path" ;
import { defer } from "../lib/api.js" ;
import PDFDocument from "pdfkit" ;
import assert from "assert" ;
import { mkdir _to , glob , changed } from "../lib/builderator.js" ;
const log = logging . create ( import . meta . url ) ;
const title _box = { x : 17 , y : 415 , width : 405 , align : "center" } ;
const summary _box = { width : 405 , height : 540 } ;
const main _title = { align : "center" , width : 1196 , height : 883 , x : 586 , y : 99 } ;
const main _box = { width : 928 , height : 535 , x : 720 , y : 323 } ;
const FORCE = false ;
export const description = "Generates a PDF presentation from an input .md file and a background template."
// your command uses the npm package commander's options format
export const options = [
[ "--heading-font <path>" , "Font file to use for headings." , "static/fonts/VictorMono-Bold.ttf" ] ,
[ "--body-font <path>" , "Font file to use for body text." , "static/fonts/VictorMono-Medium.ttf" ] ,
[ "--no-exit" , "Normally only used when called directly from other commands." ] ,
[ "--force" , "Force the generation no matter what." , FORCE ]
]
// put required options in the required variable
export const required = [
[ "--input <path>" , "The input .md file _GLOB_ for the source files." ] ,
[ "--template <path>" , "The background template image to use." ] ,
]
export const start _pdf = ( opts ) => {
const doc = new PDFDocument ( {
font : "Courier" ,
size : [ 1920 , 1080 ] ,
info : {
"Title" : "Test" ,
"Author" : "Zed A. Shaw" ,
"Subject" : "Test subject" ,
"Keywords" : "test test test" ,
"CreationDate" : new Date ( ) ,
"ModDate" : new Date ( ) ,
} } ) ;
const out _stream = doc . pipe ( fs . createWriteStream ( opts . output ) ) ;
doc . registerFont ( "heading" , opts . headingFont ) ;
doc . registerFont ( "body" , opts . bodyFont ) ;
return [ doc , out _stream ] ;
}
const valign = ( doc , text , options ) => {
assert ( options . y !== undefined , "Missing y in options" ) ;
assert ( options . height !== undefined , "Missing height in options" ) ;
const h = doc . heightOfString ( text , options ) ;
const start = options . y + ( 0.5 * ( options . height - h ) ) ;
return start ;
}
const write _page = ( doc , template , content ) => {
// place the background image
doc . image ( template , 0 , 0 , {
width : 1920 ,
height : 1080
} ) ;
// create the left side constant summary
doc . fontSize ( 40 ) ;
doc . font ( "heading" ) . text ( content . title , title _box . x , title _box . y , title _box ) ;
doc . fontSize ( 30 ) ;
doc . moveDown ( ) ;
// the lesson summary
doc . font ( "body" ) . text ( content . summary , summary _box ) ;
doc . fillColor ( "white" ) ;
if ( content . type == "title-bullets" ) {
doc . fontSize ( 120 ) ;
// add the title/body text
doc . font ( "heading" ) . text ( content . slide _title , main _title . x , main _title . y , main _title ) ;
doc . fontSize ( 80 ) ;
doc . list ( content . slide _body , main _box . x , main _box . y , main _box ) ;
} else if ( content . type == "title-image" ) {
// it's a title only slide
doc . fontSize ( 120 ) ;
// add the title/body text
doc . font ( "heading" ) . text ( content . slide _title , main _title . x , main _title . y , main _title ) ;
doc . image ( content . slide _body , main _box . x , main _box . y , {
fit : [ main _box . width , main _box . height ] ,
align : "center" ,
valign : "center"
} ) ;
} else if ( content . type == "title-only" ) {
// it's a title only slide
doc . fontSize ( 160 ) ;
const title _start = valign ( doc , content . slide _title , main _title ) ;
doc . font ( "heading" ) . text ( content . slide _title , main _title . x , title _start , main _title ) ;
} else {
// this handles title-text slides, and anything else
// if I put a # I want this to be a subtitle
doc . fontSize ( 120 ) ;
doc . font ( "heading" ) ;
if ( content . slide _body . startsWith ( "#" ) ) {
// calculate the title height
let title _start = valign ( doc , content . slide _title , main _title ) ;
// calculate 1 line for the move down
doc . fontSize ( 100 ) ;
doc . font ( "body" ) ;
title _start -= doc . heightOfString ( "H" , main _title ) ;
doc . fontSize ( 120 ) ;
// add the title/body text
doc . font ( "heading" ) . text ( content . slide _title , main _title . x , title _start , main _title ) ;
doc . fontSize ( 100 ) ;
const trim _body = content . slide _body . slice ( 1 ) . trim ( ) ;
doc . moveDown ( 1 ) ;
doc . font ( "body" ) . text ( trim _body , {
align : "center" ,
} ) ;
} else {
doc . fontSize ( 120 ) ;
// add the title/body text
doc . font ( "heading" ) . text ( content . slide _title , main _title . x , main _title . y , main _title ) ;
doc . moveDown ( 1 ) ;
doc . fontSize ( 60 ) ;
doc . font ( "body" ) . text ( content . slide _body , main _box . x , main _box . y , main _box ) ;
}
}
}
const next _page = ( doc ) => {
doc . addPage ( ) ;
}
const end _pdf = ( doc ) => {
doc . end ( ) ;
}
const parse _input = ( input ) => {
const data = fs . readFileSync ( input ) . toString ( ) ;
const result = [ ] ;
const [ head _data , slides _data ] = data . split ( "===" ) ;
const head = JSON . parse ( head _data ) ;
for ( let slide of slides _data . split ( "---" ) ) {
const split = slide . trim ( ) . split ( "\n" ) ;
const title = split . shift ( ) ;
const body = split . join ( "\n" ) . trim ( ) ;
if ( title || body ) {
const page = {
slide _title : title ,
slide _body : body ,
... head
}
if ( ! body ) {
page . type = "title-only" ;
} else if ( body . startsWith ( "*" ) ) {
page . type = "title-bullets" ;
page . slide _body = body . split ( "\n" ) . map ( l => l . slice ( 1 ) . trim ( ) ) ;
} else if ( body . startsWith ( "[" ) ) {
page . type = "title-image" ;
page . slide _body = body . slice ( 1 , - 1 ) ;
} else {
page . type = "title-text" ;
}
result . push ( page ) ;
}
}
return result ;
}
const make _pdf _path = ( input ) => {
const result = path . parse ( input ) ;
const target = path . join ( result . dir , ` ${ result . name } .pdf ` ) ;
mkdir _to ( target ) ;
return target ;
}
const generate _presentation = ( opts , waiting ) => {
try {
const [ doc , out _stream ] = start _pdf ( opts ) ;
out _stream . on ( "finish" , ( ) => waiting . resolve ( ) ) ;
const pages = parse _input ( opts . input ) ;
for ( let i = 0 ; i < pages . length ; i ++ ) {
const page = pages [ i ] ;
write _page ( doc , opts . template , page ) ;
// don't add a page when at the end
if ( i !== pages . length - 1 ) {
next _page ( doc ) ;
}
}
end _pdf ( doc ) ;
} catch ( error ) {
console . error ( error ) ;
waiting . resolve ( ) ;
}
}
export const main = async ( opts ) => {
const in _files = glob ( opts . input ) ;
for ( let file of in _files ) {
const settings = { ... opts } ;
settings . input = file ;
settings . output = make _pdf _path ( settings . input ) ;
if ( opts . force || changed ( settings . input , settings . output ) ) {
const waiting = defer ( file ) ;
generate _presentation ( settings , waiting ) ;
await waiting ;
} else {
console . log ( "SKIP" , settings . input ) ;
}
}
if ( ! opts . noExit ) process . exit ( 0 ) ;
}