The script now repairs metadata independently of the annotation, and a repeat run leaves the bytes untouched. Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
3.3 KiB
JavaScript
124 lines
3.3 KiB
JavaScript
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.');
|
|
}
|