|
|
@ -1,12 +1,11 @@ |
|
|
|
// you may not need all of these but they come up a lot
|
|
|
|
// you may not need all of these but they come up a lot
|
|
|
|
import fs from "fs"; |
|
|
|
import fs from "fs"; |
|
|
|
import assert from "assert"; |
|
|
|
|
|
|
|
import logging from '../lib/logging.js'; |
|
|
|
import logging from '../lib/logging.js'; |
|
|
|
import glob from "fast-glob"; |
|
|
|
|
|
|
|
import path from "path"; |
|
|
|
import path from "path"; |
|
|
|
import template from "lodash/template.js"; |
|
|
|
|
|
|
|
import { defer } from "../lib/api.js"; |
|
|
|
import { defer } from "../lib/api.js"; |
|
|
|
import PDFDocument from "pdfkit"; |
|
|
|
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 log = logging.create(import.meta.url); |
|
|
|
|
|
|
|
|
|
|
@ -14,7 +13,7 @@ const title_box = { x: 17, y: 415, width: 405, align: "center" }; |
|
|
|
const summary_box = { width: 405, height: 540 }; |
|
|
|
const summary_box = { width: 405, height: 540 }; |
|
|
|
const main_title = { align: "center", width: 1196, height: 883, x: 586, y: 99}; |
|
|
|
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 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." |
|
|
|
export const description = "Generates a PDF presentation from an input .md file and a background template." |
|
|
|
|
|
|
|
|
|
|
@ -22,23 +21,16 @@ export const description = "Generates a PDF presentation from an input .md file |
|
|
|
export const options = [ |
|
|
|
export const options = [ |
|
|
|
["--heading-font <path>", "Font file to use for headings.", "static/fonts/VictorMono-Bold.ttf"], |
|
|
|
["--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"], |
|
|
|
["--body-font <path>", "Font file to use for body text.", "static/fonts/VictorMono-Medium.ttf"], |
|
|
|
["--output <path>", "The output .pdf file to write. Defaults to <input>.pdf"], |
|
|
|
["--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
|
|
|
|
// put required options in the required variable
|
|
|
|
export const required = [ |
|
|
|
export const required = [ |
|
|
|
["--input <path>", "The input .md file for the generated presentation."], |
|
|
|
["--input <path>", "The input .md file _GLOB_ for the source files."], |
|
|
|
["--template <path>", "The background template image to use."], |
|
|
|
["--template <path>", "The background template image to use."], |
|
|
|
] |
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
// handy function for checking things are good and aborting
|
|
|
|
|
|
|
|
const check = (test, fail_message) => { |
|
|
|
|
|
|
|
if(!test) { |
|
|
|
|
|
|
|
log.error(fail_message); |
|
|
|
|
|
|
|
process.exit(1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const start_pdf = (opts) => { |
|
|
|
export const start_pdf = (opts) => { |
|
|
|
const doc = new PDFDocument({ |
|
|
|
const doc = new PDFDocument({ |
|
|
|
font: "Courier", |
|
|
|
font: "Courier", |
|
|
@ -60,6 +52,16 @@ export const start_pdf = (opts) => { |
|
|
|
return [doc, out_stream]; |
|
|
|
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) => { |
|
|
|
const write_page = (doc, template, content) => { |
|
|
|
// place the background image
|
|
|
|
// place the background image
|
|
|
|
doc.image(template, 0, 0, { |
|
|
|
doc.image(template, 0, 0, { |
|
|
@ -100,19 +102,43 @@ const write_page = (doc, template, content) => { |
|
|
|
} else if(content.type == "title-only") { |
|
|
|
} else if(content.type == "title-only") { |
|
|
|
// it's a title only slide
|
|
|
|
// it's a title only slide
|
|
|
|
doc.fontSize(160); |
|
|
|
doc.fontSize(160); |
|
|
|
doc.font("heading").text(content.slide_title, main_title.x, main_title.y + (main_title.height / 3.5), main_title); |
|
|
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
} else { |
|
|
|
// this handles title-text slides, and anything else
|
|
|
|
// this handles title-text slides, and anything else
|
|
|
|
|
|
|
|
// if I put a # I want this to be a subtitle
|
|
|
|
doc.fontSize(120); |
|
|
|
doc.fontSize(120); |
|
|
|
// add the title/body text
|
|
|
|
doc.font("heading"); |
|
|
|
doc.font("heading").text(content.slide_title, main_title.x, main_title.y, main_title); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// if I put a # I want this to be a subtitle
|
|
|
|
|
|
|
|
if(content.slide_body.startsWith("#")) { |
|
|
|
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); |
|
|
|
doc.fontSize(100); |
|
|
|
const trim_body = content.slide_body.slice(1).trim(); |
|
|
|
const trim_body = content.slide_body.slice(1).trim(); |
|
|
|
doc.font("body").text(trim_body, main_box.x, main_box.y, {align: "center", ...main_box}); |
|
|
|
doc.moveDown(1); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc.font("body").text(trim_body, { |
|
|
|
|
|
|
|
align: "center", |
|
|
|
|
|
|
|
}); |
|
|
|
} else { |
|
|
|
} 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.fontSize(60); |
|
|
|
doc.font("body").text(content.slide_body, main_box.x, main_box.y, main_box); |
|
|
|
doc.font("body").text(content.slide_body, main_box.x, main_box.y, main_box); |
|
|
|
} |
|
|
|
} |
|
|
@ -168,16 +194,13 @@ const parse_input = (input) => { |
|
|
|
|
|
|
|
|
|
|
|
const make_pdf_path = (input) => { |
|
|
|
const make_pdf_path = (input) => { |
|
|
|
const result = path.parse(input); |
|
|
|
const result = path.parse(input); |
|
|
|
|
|
|
|
const target = path.join(result.dir, `${result.name}.pdf`); |
|
|
|
return path.join(result.dir, `${result.name}.pdf`); |
|
|
|
mkdir_to(target); |
|
|
|
|
|
|
|
return target; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const generate_presentation = (opts, waiting) => { |
|
|
|
const generate_presentation = (opts, waiting) => { |
|
|
|
try { |
|
|
|
try { |
|
|
|
if(!opts.output) { |
|
|
|
|
|
|
|
opts.output = make_pdf_path(opts.input); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [doc, out_stream] = start_pdf(opts); |
|
|
|
const [doc, out_stream] = start_pdf(opts); |
|
|
|
|
|
|
|
|
|
|
|
out_stream.on("finish", () => waiting.resolve()); |
|
|
|
out_stream.on("finish", () => waiting.resolve()); |
|
|
@ -203,16 +226,21 @@ const generate_presentation = (opts, waiting) => { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export const main = async (opts) => { |
|
|
|
export const main = async (opts) => { |
|
|
|
const in_files = glob.sync(opts.input); |
|
|
|
const in_files = glob(opts.input); |
|
|
|
|
|
|
|
|
|
|
|
for(let file of in_files) { |
|
|
|
for(let file of in_files) { |
|
|
|
console.log(file); |
|
|
|
|
|
|
|
const waiting = defer(file); |
|
|
|
|
|
|
|
const settings = {...opts}; |
|
|
|
const settings = {...opts}; |
|
|
|
settings.input = file; |
|
|
|
settings.input = file; |
|
|
|
generate_presentation(settings, waiting); |
|
|
|
settings.output = make_pdf_path(settings.input); |
|
|
|
await waiting; |
|
|
|
|
|
|
|
|
|
|
|
if(opts.force || changed(settings.input, settings.output)) { |
|
|
|
|
|
|
|
const waiting = defer(file); |
|
|
|
|
|
|
|
generate_presentation(settings, waiting); |
|
|
|
|
|
|
|
await waiting; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
console.log("SKIP", settings.input); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
process.exit(0); |
|
|
|
if(!opts.noExit) process.exit(0); |
|
|
|
} |
|
|
|
} |
|
|
|