Files
mpcfill-pdf/index.js
orion kindel ae5193700a init
2025-11-06 15:16:08 -06:00

164 lines
4.1 KiB
JavaScript

import Progress from 'cli-progress'
import chalk from 'chalk'
import * as Log from './log.js'
import * as Card from './card'
import * as Mpc from './mpcfill.js'
import * as Drive from './drive'
import * as Pdf from 'pdf-lib'
/** @type {(from: number) => (to: number) => Array<number>} */
const range =
from =>
to =>
new Array(to - from)
.fill(undefined)
.map((_, ix) => ix + from)
const dumpPath = process.argv[2]
if (!dumpPath) {
Log.error('missing DUMP_PATH:\n bun run index.js DUMP_PATH.xml\n')
process.exit(1)
}
const dump = Mpc.readDump(await Bun.file(dumpPath).text())
Log.info(`successfully read XML dump with ${dump.order.details.quantity} cards`)
const driveAuth = await Drive.init()
Log.debug('authenticated to google drive')
Log.info(`Downloading card fronts...`)
let progress = new Progress.SingleBar({}, Progress.Presets.shades_classic)
progress.start(dump.order.fronts.card.length, 0)
/** @type {Array<Card.Card>} */
const fronts = []
/** @type {Array<Card.Card>} */
const backs = []
/** @type {Array<Promise<void>>} */
let parChunk = []
for (const card of dump.order.fronts.card) {
parChunk.push(
Card
.get({ driveAuth, id: card.id, dump: card })
.then(a => {
fronts.push(a)
progress.increment()
})
)
if (parChunk.length >= 5) {
await Promise.all(parChunk)
parChunk = []
}
}
await Promise.all(parChunk)
parChunk = []
for (const card of dump.order.backs.card) {
parChunk.push(
Card
.get({ driveAuth, id: card.id, dump: card })
.then(a => {
backs.push(a)
progress.increment()
})
)
if (parChunk.length >= 5) {
await Promise.all(parChunk)
parChunk = []
}
}
await Promise.all(parChunk)
parChunk = []
const defaultCardback = await Card.get({ driveAuth, id: dump.order.cardback, dump: undefined })
Log.info(`Done!`)
Log.info(`Building pdf...`)
const doc = await Pdf.PDFDocument.create()
/**
* @param {Card.Card} card
*/
const embedCardImage = async card => {
try {
return card.type === 'jpg'
? await doc.embedJpg(await Card.image({ id: card.id, driveAuth }))
: await doc.embedPng(await Card.image({ id: card.id, driveAuth }))
} catch (e) {
Log.error(`card id ${card.id}: ${e.toString()}`)
throw e
}
}
const firstPage = doc.addPage(Pdf.PageSizes.Letter)
const firstBackPage = doc.addPage(Pdf.PageSizes.Letter)
const defaultCardbackPdfImage = await embedCardImage(defaultCardback)
const cardCenters = (() => {
const trim = firstPage.getTrimBox()
const xDelta = trim.width / 3
const yDelta = trim.height / 3
/** @type {(n: number) => number} */
const xOff = n => trim.x + (xDelta / 2) + (xDelta * n);
/** @type {(n: number) => number} */
const yOff = n => trim.y + (yDelta / 2) + (yDelta * n);
const xs = range(0)(3).map(n => xOff(n))
const ys = range(0)(3).map(n => yOff(n))
return xs.flatMap(x =>
ys.map(/** @returns {[number, number]} */ y => [x, y])
)
})()
/**
* @param {Pdf.PDFPage} page
* @param {Card.Card} card
* @param {Pdf.PDFImage} img
* @param {number} position
*/
const renderAt = (page, card, img, position) => {
const [width, height] = [card.width.pt, card.height.pt]
const [centerX, centerY] = cardCenters[position]
const [x, y] = [centerX - (width / 2), centerY - (height / 2)]
page.drawImage(img, {width, height, x, y })
}
let frontPage = firstPage
let backPage = firstBackPage
const numPages = Math.ceil(fronts.length / 9)
for (let pageIx = 0; pageIx < numPages; pageIx++) {
const cardIxOffset = pageIx * 9;
for (let cardPos = 0; cardPos < 9; cardPos++) {
const cardIx = cardPos + cardIxOffset
const front = fronts[cardIx]
if (!front) break;
const frontImg = await embedCardImage(front)
const back = backs.find(back => back.dump.slots === front.dump.slots)
const backImg = back ? await embedCardImage(back) : defaultCardbackPdfImage;
renderAt(frontPage, front, frontImg, cardPos)
renderAt(backPage, back || defaultCardback, backImg, cardPos)
}
frontPage = doc.addPage(Pdf.PageSizes.Letter)
backPage = doc.addPage(Pdf.PageSizes.Letter)
}
await Bun.write('out.pdf', await doc.save())
Log.info('Wrote to out.pdf')
process.exit(0)