resume-free.openapps.xyz

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.
Owned app · rev 3f16b2375723
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.
← All files
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
3f16b23757 · 5 hours ago
Bounded source sync
b7eb5b26b5 · 1 day ago
Bounded source sync
adae97536e · 1 day ago
Bounded source sync
3b0cf36b39 · 1 day ago