This is the template project that's checked out and configured when you run the bando-up command from ljsthw-bandolier. This is where the code really lives.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bandolier-template/commands/pdfpresgen.js

247 lines
6.7 KiB

// 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);
}