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} */ 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} */ const fronts = [] /** @type {Array} */ const backs = [] /** @type {Array>} */ 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)