diff --git a/CV.pdf b/CV.pdf index c3e5d55..2b7a238 100644 Binary files a/CV.pdf and b/CV.pdf differ diff --git a/package-lock.json b/package-lock.json index da70d08..085eb6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@axe-core/playwright": "^4.13.0", "@eslint/js": "^9.39.5", "@lhci/cli": "^0.15.1", - "@playwright/test": "^1.62.1", + "@playwright/test": "1.62.1", "@types/express": "^5.0.1", "@types/node": "^20.17.19", "angular-eslint": "^21.4.0", @@ -35,6 +35,7 @@ "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", "jsdom": "^27.1.0", + "pdf-lib": "^1.17.1", "prettier": "^3.9.6", "typescript": "~5.9.2", "typescript-eslint": "^8.68.0", @@ -4285,6 +4286,26 @@ "third-party-web": "latest" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@playwright/test": { "version": "1.62.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", @@ -10888,6 +10909,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11064,6 +11092,26 @@ "dev": true, "license": "MIT" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", diff --git a/package.json b/package.json index 196cc5b..982f73e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "ci": "npm run lint && npm run format:check && npm run test:ci && npm run build", "e2e": "playwright test", "e2e:install": "playwright install chromium", - "lighthouse": "npm run build && lhci autorun" + "lighthouse": "npm run build && lhci autorun", + "cv:link": "node scripts/add-cv-pitch-link.mjs" }, "prettier": { "printWidth": 100, @@ -61,6 +62,7 @@ "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", "jsdom": "^27.1.0", + "pdf-lib": "^1.17.1", "prettier": "^3.9.6", "typescript": "~5.9.2", "typescript-eslint": "^8.68.0", diff --git a/scripts/add-cv-pitch-link.mjs b/scripts/add-cv-pitch-link.mjs new file mode 100644 index 0000000..aee8866 --- /dev/null +++ b/scripts/add-cv-pitch-link.mjs @@ -0,0 +1,107 @@ +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; + +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; +} + +const bytes = readFileSync(CV_PATH); +const pdfDoc = await PDFDocument.load(bytes); +const title = pdfDoc.getTitle(); +const author = pdfDoc.getAuthor(); +const creator = pdfDoc.getCreator(); +const page = pdfDoc.getPage(0); + +if (pageHasPitchLink(page)) { + console.log('Pitch link annotation already present; leaving CV.pdf unchanged.'); + process.exit(0); +} + +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 (title) { + pdfDoc.setTitle(title); +} +if (author) { + pdfDoc.setAuthor(author); +} +if (creator) { + pdfDoc.setCreator(creator); +} + +const saved = await pdfDoc.save({ useObjectStreams: false }); +writeFileSync(CV_PATH, saved); +console.log('Added visible pitch URL and link annotation to page 1 of CV.pdf.');