Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 70bace54357d4b57b674108f
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/pdf.ts
// The PDF, made in the browser with pdf-lib: it draws the same layout as the on-screen paper, with the
// resume fonts embedded (so accents print) and real, selectable text. No watermark. Links are clickable.
// The cover letter is drawn the same way from its own layout.
import { PDFDocument, PDFString, rgb, setCharacterSpacing, type PDFFont, type PDFPage } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";
import type { Resume } from "../functions/lib/resume";
import { layout, layoutLetter, type FontKey, type Layout } from "./layout";
import type { Letter } from "./store";
import { saveFile } from "./ui";
import { fileBase } from "./files";
const fontFiles = new Map<FontKey, Promise<ArrayBuffer>>();
function fontBytes(key: FontKey): Promise<ArrayBuffer> {
let p = fontFiles.get(key);
if (!p) {
p = fetch(`/fonts/resume-${key}.ttf`).then((r) => {
if (!r.ok) throw new Error("The fonts could not be loaded. Check your connection and try again.");
return r.arrayBuffer();
});
p.catch(() => fontFiles.delete(key));
fontFiles.set(key, p);
}
return p;
}
/** Fetches the fonts ahead of time, so the first download is quick (and works if the connection drops later). */
export function warm() {
for (const key of ["sans", "sansBold", "serif"] as FontKey[]) fontBytes(key).catch(() => {});
}
const color = (hex: string) => rgb(parseInt(hex.slice(1, 3), 16) / 255, parseInt(hex.slice(3, 5), 16) / 255, parseInt(hex.slice(5, 7), 16) / 255);
function addLink(doc: PDFDocument, page: PDFPage, rect: [number, number, number, number], url: string) {
const annot = doc.context.obj({
Type: "Annot", Subtype: "Link", Rect: rect, Border: [0, 0, 0],
A: { Type: "Action", S: "URI", URI: PDFString.of(url) },
});
page.node.addAnnot(doc.context.register(annot));
}
async function makePdf(lay: Layout, resume: Resume, what: string): Promise<Uint8Array> {
const doc = await PDFDocument.create();
doc.registerFontkit(fontkit);
const name = resume.basics.name.trim();
doc.setTitle(name ? `${name}, ${what.toLowerCase()}` : what);
if (name) doc.setAuthor(name);
if (resume.basics.label.trim()) doc.setSubject(resume.basics.label.trim());
doc.setCreator("Resume by OpenApps");
doc.setProducer("Resume by OpenApps");
const used = [...new Set(lay.pages.flat().flatMap((op) => (op.kind === "text" ? [op.font] : [])))];
const fonts = {} as Record<FontKey, PDFFont>;
for (const key of used) fonts[key] = await doc.embedFont(await fontBytes(key), { subset: true });
for (const ops of lay.pages) {
const page = doc.addPage([lay.width, lay.height]);
const H = lay.height;
for (const op of ops) {
if (op.kind === "rule") {
page.drawLine({ start: { x: op.x1, y: H - op.y }, end: { x: op.x2, y: H - op.y }, thickness: op.width, color: color(op.color) });
continue;
}
if (op.tracking) page.pushOperators(setCharacterSpacing(op.tracking));
page.drawText(op.text, { x: op.x, y: H - op.y, size: op.size, font: fonts[op.font], color: color(op.color) });
if (op.tracking) page.pushOperators(setCharacterSpacing(0));
if (op.link) addLink(doc, page, [op.x, H - op.y - op.size * 0.25, op.x + op.width, H - op.y + op.size * 0.85], op.link);
}
}
return doc.save();
}
export async function downloadPdf(resume: Resume) {
const bytes = await makePdf(layout(resume, { placeholders: false }), resume, "Resume");
saveFile(`${fileBase(resume)}.pdf`, new Blob([bytes], { type: "application/pdf" }));
}
export async function downloadLetterPdf(resume: Resume, letter: Letter) {
const bytes = await makePdf(layoutLetter(resume, letter, { placeholders: false }), resume, "Cover letter");
saveFile(`${fileBase(resume, "Cover Letter")}.pdf`, new Blob([bytes], { type: "application/pdf" }));
}
Change history
Quality pass: several resumes, cover letters, phone-readable shared pages
Bounded source sync
Bounded source sync
Bounded source sync