import { readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { PDFDocument, PDFName, PDFString, rgb, StandardFonts } from 'pdf-lib'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const CV_PATH = join(ROOT, 'CV.pdf'); const PITCH_URI = 'https://antoniolede.de/pitch'; const VISIBLE_TEXT = 'antoniolede.de/pitch'; const TEXT_X = 59.6; const TEXT_Y = 215.4; const FONT_SIZE = 10.5; const PADDING = 2; const ORIGINAL_TITLE = 'CV'; const ORIGINAL_CREATOR = 'Pages'; // Producer from the unmodified Pages/Quartz export of CV.pdf. const ORIGINAL_PRODUCER = 'macOS Version 26.5.2 (Build 25F84) Quartz PDFContext'; function asString(value) { if (!value) { return ''; } if (typeof value.decodeText === 'function') { return value.decodeText(); } return String(value); } function annotationUri(annotation) { const action = annotation.lookup(PDFName.of('A')); if (!action || typeof action.lookup !== 'function') { return ''; } const uri = action.lookup(PDFName.of('URI')); return asString(uri); } function pageHasPitchLink(page) { const annots = page.node.lookup(PDFName.of('Annots')); if (!annots || typeof annots.size !== 'function') { return false; } for (let index = 0; index < annots.size(); index += 1) { const annotation = annots.lookup(index); if (annotationUri(annotation) === PITCH_URI) { return true; } } return false; } function hasOriginalMetadata(pdfDoc) { return ( pdfDoc.getTitle() === ORIGINAL_TITLE && pdfDoc.getCreator() === ORIGINAL_CREATOR && pdfDoc.getProducer() === ORIGINAL_PRODUCER ); } const bytes = readFileSync(CV_PATH); const pdfDoc = await PDFDocument.load(bytes, { updateMetadata: false }); const page = pdfDoc.getPage(0); const needsLink = !pageHasPitchLink(page); const needsMetadata = !hasOriginalMetadata(pdfDoc); if (!needsLink && !needsMetadata) { console.log('Pitch link and original metadata already present; leaving CV.pdf unchanged.'); process.exit(0); } if (needsLink) { const font = await pdfDoc.embedFont(StandardFonts.Helvetica); const textWidth = font.widthOfTextAtSize(VISIBLE_TEXT, FONT_SIZE); page.drawText(VISIBLE_TEXT, { x: TEXT_X, y: TEXT_Y, size: FONT_SIZE, font, color: rgb(54 / 255, 125 / 255, 162 / 255), }); const link = pdfDoc.context.register( pdfDoc.context.obj({ Type: 'Annot', Subtype: 'Link', Rect: [ TEXT_X - PADDING, TEXT_Y - PADDING, TEXT_X + textWidth + PADDING, TEXT_Y + FONT_SIZE + PADDING, ], Border: [0, 0, 0], A: { S: 'URI', URI: PDFString.of(PITCH_URI), }, }), ); page.node.addAnnot(link); } if (needsMetadata) { pdfDoc.setTitle(ORIGINAL_TITLE); pdfDoc.setCreator(ORIGINAL_CREATOR); pdfDoc.setProducer(ORIGINAL_PRODUCER); } const saved = await pdfDoc.save({ useObjectStreams: false }); writeFileSync(CV_PATH, saved); if (needsLink && needsMetadata) { console.log('Added the pitch link and restored the original CV.pdf metadata.'); } else if (needsLink) { console.log('Added visible pitch URL and link annotation to page 1 of CV.pdf.'); } else { console.log('Restored the original Title, Creator and Producer on CV.pdf.'); }