/** * Writing the drawings back into the PDF. * * pdf.js can only read; producing a file needs a second library, loaded the same * way — behind a dynamic import, so the bundle everyone downloads does not carry * a PDF writer for the sake of the people who never export one. * * This is the moment the coordinate decision in `pageStrokes` pays off: strokes * were stored in page points all along, which is the only space a PDF * understands. Nothing has to be guessed back from screen pixels. */ import type { Stroke } from "../../types"; import { HIGHLIGHTER_ALPHA } from "../drawing/strokes.js"; import type { PdfStrokeDoc } from "./pageStrokes"; import { drawnPages } from "./pageStrokes"; /** `#rrggbb` → the 0–1 components pdf-lib wants. */ function toRgb(hex: string): { r: number; g: number; b: number } { const m = /^#?([0-9a-f]{6})$/i.exec(String(hex).trim()); if (!m) return { r: 0, g: 0, b: 0 }; const n = parseInt(m[1]!, 16); return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 }; } /** Highlighter strokes are drawn translucent on screen; the export matches. */ const opacityFor = (tool: string) => (tool === "highlighter" ? parseInt(HIGHLIGHTER_ALPHA, 16) / 255 : 1); /** * Stamps a note's strokes onto a copy of its PDF. * * The original bytes are never modified — pdf-lib builds a new document — so the * stored file stays exactly as it was imported and can be exported clean again * afterwards. * * Erased areas cannot come along. On screen an eraser stroke removes ink by * drawing in `destination-out`, which has no equivalent in a PDF content stream; * such strokes are dropped rather than being stamped as visible marks, which * would put grey scribbles where the user had rubbed something out. */ export async function stampStrokesIntoPdf(originalBytes: Uint8Array, ink: PdfStrokeDoc): Promise { const { PDFDocument, rgb } = await import("pdf-lib"); const doc = await PDFDocument.load(originalBytes); const pages = doc.getPages(); for (const pageNo of drawnPages(ink)) { const page = pages[pageNo - 1]; const stored = ink.pages[String(pageNo)]; if (!page || !stored) continue; // The page may have been stored at a different size than the PDF reports — // a rotated page, or a file edited elsewhere since. Scale rather than trust. const { width, height } = page.getSize(); const sx = width / stored.w; const sy = height / stored.h; for (const stroke of stored.strokes as Stroke[]) { if (stroke.t === "eraser") continue; const pts = stroke.p; if (!Array.isArray(pts) || pts.length < 4) continue; const { r, g, b } = toRgb(stroke.c); const opacity = opacityFor(stroke.t); const thickness = stroke.s * ((sx + sy) / 2); for (let i = 0; i + 3 < pts.length; i += 2) { page.drawLine({ // PDF's origin is the bottom-left corner; the drawing's is the top-left. start: { x: pts[i]! * sx, y: height - pts[i + 1]! * sy }, end: { x: pts[i + 2]! * sx, y: height - pts[i + 3]! * sy }, thickness, color: rgb(r, g, b), opacity, lineCap: 1, // round, so segments of one stroke join without notches }); } } } return doc.save(); }