react-pdf-highlighter-plus 1.1.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,8 +46,13 @@
46
46
  | **PDF Search** | Search through all PDF text with next/previous navigation |
47
47
  | **PDF Export** | Export annotated PDF with all highlights embedded |
48
48
  | **Local PDF Worker** | Uses the packaged PDF.js worker by default |
49
- | **Light/Dark Theme** | Eye-friendly dark mode with customizable intensity |
50
- | **Zoom Support** | Full zoom functionality with position-independent data |
49
+ | **Light/Dark Theme** | Hue-preserving dark mode (OKLab recolor) photos & colors stay readable |
50
+ | **Zoom Support** | Buttons, **pinch / ctrl+wheel zoom**, position-independent data |
51
+ | **Smooth Scroll** | Animated scroll-to-highlight (respects reduced-motion) |
52
+ | **Deep Linking** | `initialPage` + `onPageChange` for `?page=N` style navigation |
53
+ | **Locate Text** | `getTextPosition` — turn any quote into a precise highlight (citations) |
54
+ | **Read Aloud** | Text-to-speech that highlights & follows each sentence (recipe) |
55
+ | **Fast Loading** | Progressive range loading, auth headers, document caching |
51
56
  | **Fully Customizable** | Exposed styling on all components |
52
57
 
53
58
  ## Quick Links
@@ -299,10 +304,13 @@ highlighterUtilsRef.current?.clearSearch();
299
304
 
300
305
  ## Light/Dark Theme
301
306
 
302
- Toggle between light and dark modes with customizable styling for comfortable reading.
307
+ Dark mode recolors each page **at render time** using a hue-preserving OKLab map —
308
+ white paper maps to a dark background and black text to a light foreground, while
309
+ **colors keep their hue and embedded photos keep their pixels** (unlike a CSS
310
+ `invert()` filter). Highlights, the text selection, and the left panel all adapt.
303
311
 
304
312
  ```tsx
305
- // Enable dark mode
313
+ // Enable dark mode (warm-gray default palette)
306
314
  <PdfHighlighter
307
315
  pdfDocument={pdfDocument}
308
316
  theme={{ mode: "dark" }}
@@ -311,12 +319,15 @@ Toggle between light and dark modes with customizable styling for comfortable re
311
319
  <HighlightContainer />
312
320
  </PdfHighlighter>
313
321
 
314
- // Customize dark mode intensity and colors
322
+ // Customize the dark palette
315
323
  <PdfHighlighter
316
324
  pdfDocument={pdfDocument}
317
325
  theme={{
318
326
  mode: "dark",
319
- darkModeInvertIntensity: 0.85, // Softer (0.8-1.0)
327
+ darkModeColors: {
328
+ background: "#141210", // replaces white paper
329
+ foreground: "#eae6e0", // replaces black text / line-art
330
+ },
320
331
  containerBackgroundColor: "#3a3a3a",
321
332
  scrollbarThumbColor: "#6b6b6b",
322
333
  scrollbarTrackColor: "#2c2c2c",
@@ -327,25 +338,146 @@ Toggle between light and dark modes with customizable styling for comfortable re
327
338
  </PdfHighlighter>
328
339
  ```
329
340
 
330
- **Features:**
331
- - Eye-friendly dark mode using CSS filter inversion
332
- - Customizable inversion intensity (0.8-1.0)
333
- - Preserve original highlight colors in dark mode
334
- - Custom scrollbar styling
335
- - Full theming control for container background
336
-
337
- **Inversion Intensity Guide:**
338
- | Value | Result | Use Case |
339
- |-------|--------|----------|
340
- | `1.0` | Pure black | High contrast |
341
- | `0.9` | Dark gray (~#1a1a1a) | **Recommended** |
342
- | `0.85` | Softer gray (~#262626) | Long reading sessions |
343
- | `0.8` | Medium gray (~#333333) | Maximum comfort |
341
+ **Highlights:**
342
+ - Hue-preserving recolor (red stays red, blue links stay blue); photos untouched.
343
+ - Highlights stay readable: translucent fill + a border, with no `mix-blend` wash-out.
344
+ - Scroll **and** zoom are preserved when toggling the theme.
345
+ - `LeftPanel` accepts `mode="dark"` so the outline/thumbnails panel matches.
346
+ - Drawing/shape default ink becomes white in dark mode.
347
+
348
+ > **Deprecated:** `theme.darkModeInvertIntensity` is ignored — dark mode no longer
349
+ > uses a CSS `invert()` filter. Use `theme.darkModeColors` instead.
344
350
 
345
351
  [Full Documentation →](docs/theming.md)
346
352
 
347
353
  ---
348
354
 
355
+ ## Loading & Performance
356
+
357
+ `PdfLoader` loads documents progressively and caches them.
358
+
359
+ ```tsx
360
+ <PdfLoader
361
+ document="https://api.example.com/files/report.pdf"
362
+ // Fetch only the pages needed to render first (needs server HTTP range support)
363
+ disableAutoFetch={true}
364
+ // Auth / cross-origin
365
+ httpHeaders={{ Authorization: `Bearer ${token}` }}
366
+ withCredentials={false}
367
+ // Reuse the same URL instantly on remount / re-open (default true)
368
+ enableCache={true}
369
+ >
370
+ {(pdfDocument) => <PdfHighlighter pdfDocument={pdfDocument} /* … */ />}
371
+ </PdfLoader>
372
+ ```
373
+
374
+ | Prop | Default | Description |
375
+ |------|---------|-------------|
376
+ | `disableAutoFetch` | `true` | Fetch pages on demand instead of the whole file (first page shows fast on range-capable servers) |
377
+ | `disableStream` | `false` | Disable progressive streaming |
378
+ | `rangeChunkSize` | pdf.js | Size of each range request in bytes |
379
+ | `httpHeaders` | – | Extra request headers (e.g. an auth token) |
380
+ | `withCredentials` | `false` | Send cookies with the request |
381
+ | `enableCache` | `true` | Cache the loaded document by URL (also dedupes StrictMode double-mount) |
382
+ | `beforeLoad` | spinner | Render while loading; receives progress or `null` |
383
+
384
+ > Progressive loading requires the server to support HTTP range requests
385
+ > (`Accept-Ranges: bytes`) **and** expose `Content-Range` via CORS.
386
+
387
+ ---
388
+
389
+ ## Navigation, Zoom & Smooth Scroll
390
+
391
+ ```tsx
392
+ <PdfHighlighter
393
+ pdfDocument={pdfDocument}
394
+ initialPage={12} // jump here on first load (deep-link)
395
+ onPageChange={(page) => syncUrl(page)} // current page changed
396
+ onZoomChange={(scale) => setZoom(scale)} // pinch / ctrl+wheel zoom changed
397
+ highlights={highlights}
398
+ >
399
+ <HighlightContainer />
400
+ </PdfHighlighter>
401
+ ```
402
+
403
+ - **Pinch / ctrl(⌘)+wheel zoom** is built in — smooth (GPU transform during the
404
+ gesture, one crisp re-render on settle), anchored to the cursor.
405
+ - **`scrollToHighlight(highlight)`** (from `usePdfHighlighterContext`) now scrolls
406
+ **smoothly** and respects `prefers-reduced-motion`.
407
+ - `initialPage` is applied once on load; `onPageChange` fires as the visible page
408
+ changes — wire them to a `?page=N` URL for deep links.
409
+
410
+ ---
411
+
412
+ ## Locating Text (Citations)
413
+
414
+ `getTextPosition` finds a piece of text in the PDF and returns a precise
415
+ `ScaledPosition` — turn an external quote (e.g. an AI citation) into a highlight
416
+ you can render or scroll to. Matching ignores whitespace/line-wraps and falls
417
+ back to fuzzy matching.
418
+
419
+ ```tsx
420
+ import { getTextPosition } from "react-pdf-highlighter-plus";
421
+
422
+ const match = await getTextPosition(pdfDocument, "the exact or near-exact quote");
423
+ if (match) {
424
+ const citation = {
425
+ id: "cite-1",
426
+ type: "text",
427
+ content: { text: match.matchedText },
428
+ position: match.position, // precise rects, page-independent
429
+ };
430
+ setHighlights((prev) => [citation, ...prev]);
431
+ utils.scrollToHighlight(citation); // smooth scroll + flash
432
+ }
433
+ // match: { position, pageNumber, matchedText, confidence: "exact" | "fuzzy" }
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Read Aloud (Text-to-Speech)
439
+
440
+ Build a read-aloud / "PDF to audio" experience: speak the document and
441
+ highlight + auto-scroll to each sentence as it's read. The pieces are already
442
+ here — `extractSentences` gives the ordered script (text + position) and
443
+ `scrollToHighlight` follows along. The TTS engine is yours to choose (the
444
+ browser `speechSynthesis`, or a cloud voice).
445
+
446
+ ```tsx
447
+ import { extractSentences } from "react-pdf-highlighter-plus";
448
+
449
+ // 1. Build the ordered script once.
450
+ const sentences = (
451
+ await extractSentences(pdfDocument, { includePositions: true })
452
+ ).filter((s) => s.position);
453
+
454
+ // 2. Speak each sentence; highlight + scroll as it starts.
455
+ function readFrom(index: number) {
456
+ const s = sentences[index];
457
+ if (!s) return;
458
+
459
+ const reading = {
460
+ id: "reading",
461
+ type: "text",
462
+ content: { text: s.text },
463
+ position: s.position!,
464
+ };
465
+ setHighlights((prev) => [reading, ...prev.filter((h) => h.id !== "reading")]);
466
+ utils.scrollToHighlight(reading); // smooth follow
467
+
468
+ const utter = new SpeechSynthesisUtterance(s.text);
469
+ utter.onend = () => readFrom(index + 1); // advance
470
+ speechSynthesis.speak(utter);
471
+ }
472
+
473
+ readFrom(0);
474
+ ```
475
+
476
+ Swap `speechSynthesis` for any engine — sentence-level sync needs no word
477
+ timing. See the example app for a full transport (play/pause/seek/speed/voice).
478
+
479
+ ---
480
+
349
481
  ## PDF Export
350
482
 
351
483
  Export your annotated PDF with all highlights embedded.
@@ -1,5 +1,6 @@
1
+ import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
2
+
1
3
  // src/lib/export-pdf.ts
2
- import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
3
4
  function parseColor(color) {
4
5
  const rgbaMatch = color.match(
5
6
  /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
@@ -52,6 +53,82 @@ function dataUrlToBytes(dataUrl) {
52
53
  const type = dataUrl.includes("image/png") ? "png" : "jpg";
53
54
  return { bytes, type };
54
55
  }
56
+ function dataUrlFormat(dataUrl) {
57
+ const mime = dataUrl.slice(5, dataUrl.indexOf(";")).toLowerCase();
58
+ if (mime === "image/png") return "png";
59
+ if (mime === "image/jpeg" || mime === "image/jpg") return "jpg";
60
+ return "other";
61
+ }
62
+ var CONTENT_RADIUS_PT = 9;
63
+ function roundedRectPath(ctx, w, h, r) {
64
+ const radius = Math.max(0, Math.min(r, w / 2, h / 2));
65
+ ctx.beginPath();
66
+ ctx.moveTo(radius, 0);
67
+ ctx.arcTo(w, 0, w, h, radius);
68
+ ctx.arcTo(w, h, 0, h, radius);
69
+ ctx.arcTo(0, h, 0, 0, radius);
70
+ ctx.arcTo(0, 0, w, 0, radius);
71
+ ctx.closePath();
72
+ }
73
+ async function compositeHighlightImageToPng(dataUrl, boxWidthPts, boxHeightPts, opts) {
74
+ if (typeof document === "undefined" || typeof Image === "undefined") {
75
+ return null;
76
+ }
77
+ const img = await new Promise((resolve) => {
78
+ const image = new Image();
79
+ image.onload = () => resolve(image);
80
+ image.onerror = () => resolve(null);
81
+ image.src = dataUrl;
82
+ });
83
+ if (!img) return null;
84
+ const scale = 2;
85
+ const cw = Math.max(1, Math.round(boxWidthPts * scale));
86
+ const ch = Math.max(1, Math.round(boxHeightPts * scale));
87
+ const canvas = document.createElement("canvas");
88
+ canvas.width = cw;
89
+ canvas.height = ch;
90
+ const ctx = canvas.getContext("2d");
91
+ if (!ctx) return null;
92
+ roundedRectPath(ctx, cw, ch, CONTENT_RADIUS_PT * scale);
93
+ ctx.clip();
94
+ if (opts.background) {
95
+ ctx.fillStyle = opts.background;
96
+ ctx.fillRect(0, 0, cw, ch);
97
+ }
98
+ const iw = img.naturalWidth || cw;
99
+ const ih = img.naturalHeight || ch;
100
+ if (opts.fit === "contain") {
101
+ const s = Math.min(cw / iw, ch / ih);
102
+ const dw = iw * s;
103
+ const dh = ih * s;
104
+ ctx.drawImage(img, (cw - dw) / 2, (ch - dh) / 2, dw, dh);
105
+ } else {
106
+ ctx.drawImage(img, 0, 0, cw, ch);
107
+ }
108
+ return dataUrlToBytes(canvas.toDataURL("image/png")).bytes;
109
+ }
110
+ function drawRoundedRectangle(page, o) {
111
+ const r = Math.max(0, Math.min(o.radius, o.width / 2, o.height / 2));
112
+ if (r <= 0) {
113
+ page.drawRectangle({
114
+ x: o.x,
115
+ y: o.y,
116
+ width: o.width,
117
+ height: o.height,
118
+ color: o.color,
119
+ opacity: o.opacity
120
+ });
121
+ return;
122
+ }
123
+ const { width: w, height: h } = o;
124
+ const path = `M ${r} 0 H ${w - r} A ${r} ${r} 0 0 1 ${w} ${r} V ${h - r} A ${r} ${r} 0 0 1 ${w - r} ${h} H ${r} A ${r} ${r} 0 0 1 0 ${h - r} V ${r} A ${r} ${r} 0 0 1 ${r} 0 Z`;
125
+ page.drawSvgPath(path, {
126
+ x: o.x,
127
+ y: o.y + h,
128
+ color: o.color,
129
+ opacity: o.opacity
130
+ });
131
+ }
55
132
  function wrapText(text, font, fontSize, maxWidth) {
56
133
  if (!text || maxWidth <= 0) return [];
57
134
  const lines = [];
@@ -114,13 +191,14 @@ async function renderTextHighlight(page, highlight, options) {
114
191
  for (const rect of rects) {
115
192
  const { x, y, width, height } = scaledToPdfPoints(rect, page);
116
193
  if (highlightStyle === "highlight") {
117
- page.drawRectangle({
194
+ drawRoundedRectangle(page, {
118
195
  x,
119
196
  y,
120
197
  width,
121
198
  height,
122
199
  color: rgb(color.r, color.g, color.b),
123
- opacity: color.a
200
+ opacity: color.a,
201
+ radius: CONTENT_RADIUS_PT
124
202
  });
125
203
  } else if (highlightStyle === "underline") {
126
204
  const lineThickness = Math.max(1, height * 0.1);
@@ -153,13 +231,14 @@ async function renderAreaHighlight(page, highlight, options) {
153
231
  highlight.position.boundingRect,
154
232
  page
155
233
  );
156
- page.drawRectangle({
234
+ drawRoundedRectangle(page, {
157
235
  x,
158
236
  y,
159
237
  width,
160
238
  height,
161
239
  color: rgb(color.r, color.g, color.b),
162
- opacity: color.a
240
+ opacity: color.a,
241
+ radius: CONTENT_RADIUS_PT
163
242
  });
164
243
  }
165
244
  async function renderFreetextHighlight(page, highlight, options, font) {
@@ -185,13 +264,14 @@ async function renderFreetextHighlight(page, highlight, options, font) {
185
264
  const bgColorValue = highlight.backgroundColor || options.defaultFreetextBgColor || "#ffffc8";
186
265
  if (bgColorValue !== "transparent") {
187
266
  const bgColor = parseColor(bgColorValue);
188
- page.drawRectangle({
267
+ drawRoundedRectangle(page, {
189
268
  x,
190
269
  y,
191
270
  width,
192
271
  height,
193
272
  color: rgb(bgColor.r, bgColor.g, bgColor.b),
194
- opacity: bgColor.a
273
+ opacity: bgColor.a,
274
+ radius: CONTENT_RADIUS_PT
195
275
  });
196
276
  }
197
277
  const padding = 4 * yRatio;
@@ -243,16 +323,34 @@ function transformToRawCoordinates(page, x, y, width, height) {
243
323
  }
244
324
  return { x, y, width, height };
245
325
  }
246
- async function renderImageHighlight(pdfDoc, page, highlight) {
326
+ async function renderImageHighlight(pdfDoc, page, highlight, kind = "image") {
247
327
  const imageDataUrl = highlight.content?.image;
248
328
  if (!imageDataUrl) return;
249
329
  try {
250
- const { bytes, type } = dataUrlToBytes(imageDataUrl);
251
- const image = type === "png" ? await pdfDoc.embedPng(bytes) : await pdfDoc.embedJpg(bytes);
252
330
  const visualCoords = scaledToPdfPoints(
253
331
  highlight.position.boundingRect,
254
332
  page
255
333
  );
334
+ let image;
335
+ const pngBytes = await compositeHighlightImageToPng(
336
+ imageDataUrl,
337
+ visualCoords.width,
338
+ visualCoords.height,
339
+ { fit: kind === "drawing" ? "contain" : "fill" }
340
+ );
341
+ if (pngBytes) {
342
+ image = await pdfDoc.embedPng(pngBytes);
343
+ } else {
344
+ const format = dataUrlFormat(imageDataUrl);
345
+ if (format === "png") {
346
+ image = await pdfDoc.embedPng(dataUrlToBytes(imageDataUrl).bytes);
347
+ } else if (format === "jpg") {
348
+ image = await pdfDoc.embedJpg(dataUrlToBytes(imageDataUrl).bytes);
349
+ } else {
350
+ console.error("Cannot embed image: unsupported format and no canvas to rasterize");
351
+ return;
352
+ }
353
+ }
256
354
  const rawCoords = transformToRawCoordinates(
257
355
  page,
258
356
  visualCoords.x,
@@ -383,7 +481,7 @@ async function exportPdf(pdfSource, highlights, options = {}) {
383
481
  await renderImageHighlight(pdfDoc, page, highlight);
384
482
  break;
385
483
  case "drawing":
386
- await renderImageHighlight(pdfDoc, page, highlight);
484
+ await renderImageHighlight(pdfDoc, page, highlight, "drawing");
387
485
  break;
388
486
  case "shape":
389
487
  await renderShapeHighlight(page, highlight);
@@ -397,7 +495,7 @@ async function exportPdf(pdfSource, highlights, options = {}) {
397
495
  }
398
496
  return pdfDoc.save();
399
497
  }
400
- export {
401
- exportPdf
402
- };
403
- //# sourceMappingURL=export-pdf-W2QGWADM.js.map
498
+
499
+ export { exportPdf };
500
+ //# sourceMappingURL=export-pdf-DD7J5UHW.js.map
501
+ //# sourceMappingURL=export-pdf-DD7J5UHW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/export-pdf.ts"],"names":[],"mappings":"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","file":"export-pdf-DD7J5UHW.js","sourcesContent":["import { PDFDocument, rgb, StandardFonts, PDFPage, PDFFont } from \"pdf-lib\";\nimport type { Scaled, ScaledPosition, ShapeData } from \"../types\";\n\n/**\n * Options for the PDF export function.\n *\n * @category Type\n */\nexport interface ExportPdfOptions {\n /** Default color for text highlights. Default: \"rgba(255, 226, 143, 0.5)\" */\n textHighlightColor?: string;\n /** Default color for area highlights. Default: \"rgba(255, 226, 143, 0.5)\" */\n areaHighlightColor?: string;\n /** Default text color for freetext. Default: \"#333333\" */\n defaultFreetextColor?: string;\n /** Default background for freetext. Default: \"#ffffc8\" */\n defaultFreetextBgColor?: string;\n /** Default font size for freetext. Default: 14 */\n defaultFreetextFontSize?: number;\n /** Progress callback for large PDFs */\n onProgress?: (current: number, total: number) => void;\n}\n\n/**\n * A highlight that can be exported to PDF.\n *\n * @category Type\n */\nexport interface ExportableHighlight {\n id: string;\n type?: \"text\" | \"area\" | \"freetext\" | \"image\" | \"drawing\" | \"shape\";\n content?: {\n text?: string;\n image?: string; // Base64 data URL\n shape?: ShapeData; // Shape data for shape highlights\n };\n position: ScaledPosition;\n /** Per-highlight color override (for text/area highlights) */\n highlightColor?: string;\n /** Style mode for text highlights: \"highlight\" (default), \"underline\", or \"strikethrough\" */\n highlightStyle?: \"highlight\" | \"underline\" | \"strikethrough\";\n /** Text color for freetext highlights */\n color?: string;\n /** Background color for freetext highlights */\n backgroundColor?: string;\n /** Font size for freetext highlights */\n fontSize?: string;\n /** Font family for freetext highlights (not used in export, Helvetica is always used) */\n fontFamily?: string;\n /** Shape type for shape highlights */\n shapeType?: \"rectangle\" | \"circle\" | \"arrow\";\n /** Stroke color for shape highlights */\n strokeColor?: string;\n /** Stroke width for shape highlights */\n strokeWidth?: number;\n}\n\n/**\n * Parse a color string to RGB values (0-1 range).\n */\nfunction parseColor(color: string): {\n r: number;\n g: number;\n b: number;\n a: number;\n} {\n // Handle rgba(r, g, b, a) and rgb(r, g, b)\n const rgbaMatch = color.match(\n /rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)/\n );\n if (rgbaMatch) {\n return {\n r: parseInt(rgbaMatch[1]) / 255,\n g: parseInt(rgbaMatch[2]) / 255,\n b: parseInt(rgbaMatch[3]) / 255,\n a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,\n };\n }\n\n // Handle hex (#RRGGBB or #RGB)\n const hex = color.replace(\"#\", \"\");\n if (hex.length === 3) {\n return {\n r: parseInt(hex[0] + hex[0], 16) / 255,\n g: parseInt(hex[1] + hex[1], 16) / 255,\n b: parseInt(hex[2] + hex[2], 16) / 255,\n a: 1,\n };\n }\n if (hex.length === 6) {\n return {\n r: parseInt(hex.slice(0, 2), 16) / 255,\n g: parseInt(hex.slice(2, 4), 16) / 255,\n b: parseInt(hex.slice(4, 6), 16) / 255,\n a: 1,\n };\n }\n\n // Default yellow\n return { r: 1, g: 0.89, b: 0.56, a: 0.5 };\n}\n\n/**\n * Convert ScaledPosition coordinates to PDF points.\n * PDF coordinate system has origin at bottom-left.\n */\nfunction scaledToPdfPoints(\n scaled: Scaled,\n page: PDFPage\n): { x: number; y: number; width: number; height: number } {\n const pdfWidth = page.getWidth();\n const pdfHeight = page.getHeight();\n\n // Calculate position ratios\n const xRatio = pdfWidth / scaled.width;\n const yRatio = pdfHeight / scaled.height;\n\n const x = scaled.x1 * xRatio;\n const width = (scaled.x2 - scaled.x1) * xRatio;\n const height = (scaled.y2 - scaled.y1) * yRatio;\n\n // Flip Y (PDF origin is bottom-left, screen origin is top-left)\n const y = pdfHeight - scaled.y1 * yRatio - height;\n\n return { x, y, width, height };\n}\n\n/**\n * Convert base64 data URL to bytes.\n */\nfunction dataUrlToBytes(dataUrl: string): {\n bytes: Uint8Array;\n type: \"png\" | \"jpg\";\n} {\n const base64 = dataUrl.split(\",\")[1];\n const byteString = atob(base64);\n const bytes = new Uint8Array(byteString.length);\n for (let i = 0; i < byteString.length; i++) {\n bytes[i] = byteString.charCodeAt(i);\n }\n const type = dataUrl.includes(\"image/png\") ? \"png\" : \"jpg\";\n return { bytes, type };\n}\n\n/**\n * Detect the encoded image format from a data URL's MIME type.\n * pdf-lib can embed PNG and JPEG directly; anything else (notably SVG) must be\n * rasterized to PNG first.\n */\nfunction dataUrlFormat(dataUrl: string): \"png\" | \"jpg\" | \"other\" {\n const mime = dataUrl.slice(5, dataUrl.indexOf(\";\")).toLowerCase();\n if (mime === \"image/png\") return \"png\";\n if (mime === \"image/jpeg\" || mime === \"image/jpg\") return \"jpg\";\n return \"other\";\n}\n\n/**\n * Corner radius (in PDF points) baked into exported image/drawing highlights so\n * they match the rounded `--rphp-radius: 9px` box used in the on-screen preview.\n */\nconst CONTENT_RADIUS_PT = 9;\n\n/** Trace a rounded-rectangle subpath (fallback for browsers without ctx.roundRect). */\nfunction roundedRectPath(\n ctx: CanvasRenderingContext2D,\n w: number,\n h: number,\n r: number,\n): void {\n const radius = Math.max(0, Math.min(r, w / 2, h / 2));\n ctx.beginPath();\n ctx.moveTo(radius, 0);\n ctx.arcTo(w, 0, w, h, radius);\n ctx.arcTo(w, h, 0, h, radius);\n ctx.arcTo(0, h, 0, 0, radius);\n ctx.arcTo(0, 0, w, 0, radius);\n ctx.closePath();\n}\n\n/**\n * Composite an image/drawing highlight onto an offscreen canvas exactly as the\n * preview renders it, then return PNG bytes for pdf-lib to embed. This does\n * three things the old raw-embed path couldn't:\n *\n * 1. Handles SVG/WebP/etc. — the previous code blindly fed non-PNG to embedJpg,\n * failing with \"SOI not found in JPEG\" and dropping the image.\n * 2. Bakes in the rounded corners (`--rphp-radius`) so export matches preview.\n * 3. Applies the correct object-fit — `fill` for images (stretch to box),\n * `contain` for drawings (keep aspect, centered on a white backing) — so a\n * drawing whose aspect differs from its box is no longer distorted.\n *\n * The canvas is sized to the box's aspect ratio, so the caller draws it filling\n * the box (rotation handling unchanged). Requires a DOM; returns null\n * server-side so the caller can fall back to a direct embed.\n *\n * @param boxWidthPts / boxHeightPts - Box size in PDF points, used for aspect\n * ratio, raster resolution, and radius scaling.\n */\nasync function compositeHighlightImageToPng(\n dataUrl: string,\n boxWidthPts: number,\n boxHeightPts: number,\n opts: { fit: \"fill\" | \"contain\"; background?: string },\n): Promise<Uint8Array | null> {\n if (typeof document === \"undefined\" || typeof Image === \"undefined\") {\n return null;\n }\n\n const img = await new Promise<HTMLImageElement | null>((resolve) => {\n const image = new Image();\n image.onload = () => resolve(image);\n image.onerror = () => resolve(null);\n image.src = dataUrl;\n });\n if (!img) return null;\n\n // Render at 2× the box size (in points) so the baked-in result stays crisp\n // when embedded and scaled back to the box.\n const scale = 2;\n const cw = Math.max(1, Math.round(boxWidthPts * scale));\n const ch = Math.max(1, Math.round(boxHeightPts * scale));\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = cw;\n canvas.height = ch;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n // Clip everything to the rounded box so corners are transparent (page shows\n // through), matching the preview's `overflow: hidden` rounded content box.\n roundedRectPath(ctx, cw, ch, CONTENT_RADIUS_PT * scale);\n ctx.clip();\n\n if (opts.background) {\n ctx.fillStyle = opts.background;\n ctx.fillRect(0, 0, cw, ch);\n }\n\n const iw = img.naturalWidth || cw;\n const ih = img.naturalHeight || ch;\n\n if (opts.fit === \"contain\") {\n const s = Math.min(cw / iw, ch / ih);\n const dw = iw * s;\n const dh = ih * s;\n ctx.drawImage(img, (cw - dw) / 2, (ch - dh) / 2, dw, dh);\n } else {\n ctx.drawImage(img, 0, 0, cw, ch);\n }\n\n return dataUrlToBytes(canvas.toDataURL(\"image/png\")).bytes;\n}\n\n/**\n * Draw a filled rounded rectangle. pdf-lib has no rounded-rect primitive, so\n * this builds an SVG path and draws it via drawSvgPath. Matches the preview's\n * `--rphp-radius: 9px` rounded highlight/area/freetext boxes, which the old\n * drawRectangle calls rendered with sharp corners.\n *\n * @param x, y - Bottom-left of the box in PDF points (Y up, as elsewhere here).\n */\nfunction drawRoundedRectangle(\n page: PDFPage,\n o: {\n x: number;\n y: number;\n width: number;\n height: number;\n color: ReturnType<typeof rgb>;\n opacity: number;\n radius: number;\n }\n): void {\n const r = Math.max(0, Math.min(o.radius, o.width / 2, o.height / 2));\n if (r <= 0) {\n page.drawRectangle({\n x: o.x,\n y: o.y,\n width: o.width,\n height: o.height,\n color: o.color,\n opacity: o.opacity,\n });\n return;\n }\n const { width: w, height: h } = o;\n // Path is in SVG space (origin top-left, Y down). drawSvgPath anchors SVG\n // (0,0) at the given (x, y), so pass the box's TOP edge (o.y + h) as y.\n const path =\n `M ${r} 0 H ${w - r} A ${r} ${r} 0 0 1 ${w} ${r} ` +\n `V ${h - r} A ${r} ${r} 0 0 1 ${w - r} ${h} ` +\n `H ${r} A ${r} ${r} 0 0 1 0 ${h - r} ` +\n `V ${r} A ${r} ${r} 0 0 1 ${r} 0 Z`;\n page.drawSvgPath(path, {\n x: o.x,\n y: o.y + h,\n color: o.color,\n opacity: o.opacity,\n });\n}\n\n/**\n * Wrap text into multiple lines that fit within maxWidth.\n * Long words are broken character by character (like CSS word-wrap: break-word).\n */\nfunction wrapText(\n text: string,\n font: PDFFont,\n fontSize: number,\n maxWidth: number\n): string[] {\n if (!text || maxWidth <= 0) return [];\n\n const lines: string[] = [];\n\n // Split by newlines first to preserve intentional line breaks\n const paragraphs = text.split(/\\n/);\n\n for (const paragraph of paragraphs) {\n if (!paragraph.trim()) {\n lines.push(\"\");\n continue;\n }\n\n const words = paragraph.split(/\\s+/);\n let currentLine = \"\";\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word;\n const testWidth = font.widthOfTextAtSize(testLine, fontSize);\n\n if (testWidth <= maxWidth) {\n currentLine = testLine;\n } else {\n // Push current line if exists\n if (currentLine) {\n lines.push(currentLine);\n currentLine = \"\";\n }\n\n // Check if word itself is too wide - break it character by character\n if (font.widthOfTextAtSize(word, fontSize) > maxWidth) {\n let remaining = word;\n while (remaining.length > 0) {\n let charCount = 1;\n // Find how many characters fit in maxWidth\n while (\n charCount < remaining.length &&\n font.widthOfTextAtSize(remaining.substring(0, charCount + 1), fontSize) <= maxWidth\n ) {\n charCount++;\n }\n const chunk = remaining.substring(0, charCount);\n remaining = remaining.substring(charCount);\n\n if (remaining.length > 0) {\n // More characters remaining, push this chunk as a complete line\n lines.push(chunk);\n } else {\n // Last chunk, keep it as current line (may combine with next word)\n currentLine = chunk;\n }\n }\n } else {\n currentLine = word;\n }\n }\n }\n if (currentLine) lines.push(currentLine);\n }\n\n return lines;\n}\n\n/**\n * Group highlights by page number.\n */\nfunction groupByPage(\n highlights: ExportableHighlight[]\n): Map<number, ExportableHighlight[]> {\n const map = new Map<number, ExportableHighlight[]>();\n for (const h of highlights) {\n const pageNum = h.position.boundingRect.pageNumber;\n if (!map.has(pageNum)) map.set(pageNum, []);\n map.get(pageNum)!.push(h);\n }\n return map;\n}\n\n/**\n * Render a text highlight (multiple rectangles for multi-line selections).\n * Supports highlight (background), underline, and strikethrough styles.\n */\nasync function renderTextHighlight(\n page: PDFPage,\n highlight: ExportableHighlight,\n options: ExportPdfOptions\n): Promise<void> {\n // Per-highlight color override or fallback to default\n const colorStr =\n highlight.highlightColor ||\n options.textHighlightColor ||\n \"rgba(255, 226, 143, 0.5)\";\n const color = parseColor(colorStr);\n const highlightStyle = highlight.highlightStyle || \"highlight\";\n\n // Text highlights use rects array for multi-line selections\n const rects =\n highlight.position.rects.length > 0\n ? highlight.position.rects\n : [highlight.position.boundingRect];\n\n for (const rect of rects) {\n const { x, y, width, height } = scaledToPdfPoints(rect, page);\n\n if (highlightStyle === \"highlight\") {\n // Draw filled rounded rectangle for background highlight (rounded to\n // match the preview's `--rphp-radius` highlight parts; clamps to a pill\n // on short rects).\n drawRoundedRectangle(page, {\n x,\n y,\n width,\n height,\n color: rgb(color.r, color.g, color.b),\n opacity: color.a,\n radius: CONTENT_RADIUS_PT,\n });\n } else if (highlightStyle === \"underline\") {\n // Draw line at bottom of rectangle\n const lineThickness = Math.max(1, height * 0.1);\n page.drawRectangle({\n x,\n y,\n width,\n height: lineThickness,\n color: rgb(color.r, color.g, color.b),\n opacity: color.a,\n });\n } else if (highlightStyle === \"strikethrough\") {\n // Draw line through middle of rectangle\n const lineThickness = Math.max(1, height * 0.1);\n const lineY = y + height / 2 - lineThickness / 2;\n page.drawRectangle({\n x,\n y: lineY,\n width,\n height: lineThickness,\n color: rgb(color.r, color.g, color.b),\n opacity: color.a,\n });\n }\n }\n}\n\n/**\n * Render an area highlight (single rectangle).\n */\nasync function renderAreaHighlight(\n page: PDFPage,\n highlight: ExportableHighlight,\n options: ExportPdfOptions\n): Promise<void> {\n // Per-highlight color override or fallback to default\n const colorStr =\n highlight.highlightColor ||\n options.areaHighlightColor ||\n \"rgba(255, 226, 143, 0.5)\";\n const color = parseColor(colorStr);\n const { x, y, width, height } = scaledToPdfPoints(\n highlight.position.boundingRect,\n page\n );\n\n drawRoundedRectangle(page, {\n x,\n y,\n width,\n height,\n color: rgb(color.r, color.g, color.b),\n opacity: color.a,\n radius: CONTENT_RADIUS_PT,\n });\n}\n\n/**\n * Render a freetext highlight (background rectangle + text).\n * Text is wrapped to fit within the box.\n */\nasync function renderFreetextHighlight(\n page: PDFPage,\n highlight: ExportableHighlight,\n options: ExportPdfOptions,\n font: PDFFont\n): Promise<void> {\n const text = highlight.content?.text || \"\";\n const textColor = parseColor(\n highlight.color || options.defaultFreetextColor || \"#333333\"\n );\n\n // Get box dimensions in PDF points\n const { x, y, width, height } = scaledToPdfPoints(\n highlight.position.boundingRect,\n page\n );\n\n // Scale font size by the same ratio used for the box coordinates\n // This ensures the font scales proportionally with the box\n const pdfHeight = page.getHeight();\n const yRatio = pdfHeight / highlight.position.boundingRect.height;\n const storedFontSize =\n parseInt(highlight.fontSize || \"\") || options.defaultFreetextFontSize || 14;\n const fontSize = storedFontSize * yRatio;\n\n console.log(\"Freetext export:\", {\n storedFontSize,\n yRatio,\n fontSize,\n boxDimensions: { x, y, width, height },\n text: text.substring(0, 50),\n });\n\n // Draw background (skip if transparent)\n const bgColorValue = highlight.backgroundColor || options.defaultFreetextBgColor || \"#ffffc8\";\n if (bgColorValue !== \"transparent\") {\n const bgColor = parseColor(bgColorValue);\n drawRoundedRectangle(page, {\n x,\n y,\n width,\n height,\n color: rgb(bgColor.r, bgColor.g, bgColor.b),\n opacity: bgColor.a,\n radius: CONTENT_RADIUS_PT,\n });\n }\n\n // Draw wrapped text with scaled padding\n const padding = 4 * yRatio;\n const maxWidth = width - padding * 2;\n const lineHeight = fontSize * 1.3;\n\n if (maxWidth > 0 && text) {\n const lines = wrapText(text, font, fontSize, maxWidth);\n let currentY = y + height - fontSize - padding;\n\n for (const line of lines) {\n // Stop if we've run out of vertical space\n if (currentY < y + padding) break;\n\n // Skip empty lines but still move down\n if (line.trim()) {\n page.drawText(line, {\n x: x + padding,\n y: currentY,\n size: fontSize,\n font,\n color: rgb(textColor.r, textColor.g, textColor.b),\n });\n }\n\n currentY -= lineHeight;\n }\n }\n}\n\n/**\n * Transform visual coordinates to raw MediaBox coordinates.\n * pdf-lib's drawImage uses raw MediaBox space, but our coordinates are in visual space.\n */\nfunction transformToRawCoordinates(\n page: PDFPage,\n x: number,\n y: number,\n width: number,\n height: number\n): { x: number; y: number; width: number; height: number } {\n const rotation = page.getRotation().angle;\n const pageWidth = page.getWidth(); // Visual width\n const pageHeight = page.getHeight(); // Visual height\n\n if (rotation === 90) {\n // Visual (x, y) → Raw MediaBox coordinates\n // When rotated 90° CCW, visual top-left maps to raw bottom-left\n return {\n x: y,\n y: pageWidth - x - width,\n width: height,\n height: width,\n };\n } else if (rotation === 180) {\n // Rotated 180°, origin flips to opposite corner\n return {\n x: pageWidth - x - width,\n y: pageHeight - y - height,\n width,\n height,\n };\n } else if (rotation === 270) {\n // When rotated 90° CW (270° CCW)\n return {\n x: pageHeight - y - height,\n y: x,\n width: height,\n height: width,\n };\n }\n\n // No rotation - coordinates are already correct\n return { x, y, width, height };\n}\n\n/**\n * Render an image or drawing highlight (embedded image).\n * Handles page rotation by transforming visual coordinates to raw MediaBox space.\n * The image is composited to match the preview's rounded, white-backed content\n * box before embedding, so corners, backing, and aspect-fit all line up.\n *\n * @param kind - \"image\" fills the box (object-fit: fill, like a photo tile);\n * \"drawing\" fits inside keeping aspect (object-fit: contain) on a white\n * backing, matching each type's CSS in the preview.\n */\nasync function renderImageHighlight(\n pdfDoc: PDFDocument,\n page: PDFPage,\n highlight: ExportableHighlight,\n kind: \"image\" | \"drawing\" = \"image\"\n): Promise<void> {\n const imageDataUrl = highlight.content?.image;\n if (!imageDataUrl) return;\n\n try {\n // Calculate coordinates in visual space - use full bounding box dimensions\n const visualCoords = scaledToPdfPoints(\n highlight.position.boundingRect,\n page\n );\n\n // Composite to a PNG that bakes in the preview's rounded corners and\n // per-type object-fit. No background fill: signatures and drawings are\n // transparent-PNG ink, so they read as ink-only over the page — matching\n // shape highlights (rectangle/arrow), which are stroke-only on a\n // transparent background. Opaque photos still cover their box on their own.\n // Falls back to a direct embed when no canvas is available (e.g.\n // server-side) — sharp corners, but never drops the image (and still\n // rasterizes SVG, which pdf-lib can't embed itself).\n let image;\n const pngBytes = await compositeHighlightImageToPng(\n imageDataUrl,\n visualCoords.width,\n visualCoords.height,\n { fit: kind === \"drawing\" ? \"contain\" : \"fill\" }\n );\n if (pngBytes) {\n image = await pdfDoc.embedPng(pngBytes);\n } else {\n const format = dataUrlFormat(imageDataUrl);\n if (format === \"png\") {\n image = await pdfDoc.embedPng(dataUrlToBytes(imageDataUrl).bytes);\n } else if (format === \"jpg\") {\n image = await pdfDoc.embedJpg(dataUrlToBytes(imageDataUrl).bytes);\n } else {\n console.error(\"Cannot embed image: unsupported format and no canvas to rasterize\");\n return;\n }\n }\n\n // Transform to raw MediaBox coordinates based on page rotation\n const rawCoords = transformToRawCoordinates(\n page,\n visualCoords.x,\n visualCoords.y,\n visualCoords.width,\n visualCoords.height\n );\n\n console.log(\"Image export:\", {\n rotation: page.getRotation().angle,\n visualCoords,\n rawCoords,\n });\n\n // Draw image filling the entire bounding box\n page.drawImage(image, {\n x: rawCoords.x,\n y: rawCoords.y,\n width: rawCoords.width,\n height: rawCoords.height,\n });\n } catch (error) {\n console.error(\"Failed to embed image:\", error);\n }\n}\n\n/**\n * Render a shape highlight (rectangle, circle, or arrow).\n */\nasync function renderShapeHighlight(\n page: PDFPage,\n highlight: ExportableHighlight\n): Promise<void> {\n // Get shape data from content or top-level properties\n const shapeType = highlight.content?.shape?.shapeType || highlight.shapeType || \"rectangle\";\n const strokeColorStr = highlight.content?.shape?.strokeColor || highlight.strokeColor || \"#000000\";\n const strokeWidth = highlight.content?.shape?.strokeWidth || highlight.strokeWidth || 2;\n\n const color = parseColor(strokeColorStr);\n const { x, y, width, height } = scaledToPdfPoints(\n highlight.position.boundingRect,\n page\n );\n\n switch (shapeType) {\n case \"rectangle\":\n page.drawRectangle({\n x,\n y,\n width,\n height,\n borderColor: rgb(color.r, color.g, color.b),\n borderWidth: strokeWidth,\n opacity: color.a,\n });\n break;\n\n case \"circle\":\n page.drawEllipse({\n x: x + width / 2,\n y: y + height / 2,\n xScale: width / 2,\n yScale: height / 2,\n borderColor: rgb(color.r, color.g, color.b),\n borderWidth: strokeWidth,\n opacity: color.a,\n });\n break;\n\n case \"arrow\": {\n // Use stored start/end points if available, otherwise default to left-to-right\n const startPt = highlight.content?.shape?.startPoint;\n const endPt = highlight.content?.shape?.endPoint;\n\n // Calculate actual coordinates\n // Note: PDF coordinates have Y going up, so we need to flip the Y\n const startX = startPt ? x + startPt.x * width : x;\n const startY = startPt ? y + (1 - startPt.y) * height : y + height / 2;\n const endX = endPt ? x + endPt.x * width : x + width;\n const endY = endPt ? y + (1 - endPt.y) * height : y + height / 2;\n\n // Draw the main line\n page.drawLine({\n start: { x: startX, y: startY },\n end: { x: endX, y: endY },\n color: rgb(color.r, color.g, color.b),\n thickness: strokeWidth,\n opacity: color.a,\n });\n\n // Calculate arrowhead direction\n const angle = Math.atan2(endY - startY, endX - startX);\n const arrowSize = Math.min(15, width * 0.2, height * 0.4);\n const arrowAngle = Math.PI / 6; // 30 degrees\n\n // Draw arrowhead (two lines forming a V at the end)\n page.drawLine({\n start: {\n x: endX - arrowSize * Math.cos(angle - arrowAngle),\n y: endY - arrowSize * Math.sin(angle - arrowAngle),\n },\n end: { x: endX, y: endY },\n color: rgb(color.r, color.g, color.b),\n thickness: strokeWidth,\n opacity: color.a,\n });\n page.drawLine({\n start: {\n x: endX - arrowSize * Math.cos(angle + arrowAngle),\n y: endY - arrowSize * Math.sin(angle + arrowAngle),\n },\n end: { x: endX, y: endY },\n color: rgb(color.r, color.g, color.b),\n thickness: strokeWidth,\n opacity: color.a,\n });\n break;\n }\n }\n}\n\n/**\n * Export a PDF with annotations embedded.\n *\n * @param pdfSource - The source PDF as a URL string, Uint8Array, or ArrayBuffer\n * @param highlights - Array of highlights to embed in the PDF\n * @param options - Export options for customizing colors and behavior\n * @returns Promise<Uint8Array> - The modified PDF as bytes\n *\n * @example\n * ```typescript\n * const pdfBytes = await exportPdf(pdfUrl, highlights, {\n * textHighlightColor: \"rgba(255, 255, 0, 0.4)\",\n * onProgress: (current, total) => console.log(`${current}/${total} pages`)\n * });\n *\n * // Download the file\n * const blob = new Blob([pdfBytes], { type: \"application/pdf\" });\n * const url = URL.createObjectURL(blob);\n * const a = document.createElement(\"a\");\n * a.href = url;\n * a.download = \"annotated.pdf\";\n * a.click();\n * URL.revokeObjectURL(url);\n * ```\n *\n * @category Function\n */\nexport async function exportPdf(\n pdfSource: string | Uint8Array | ArrayBuffer,\n highlights: ExportableHighlight[],\n options: ExportPdfOptions = {}\n): Promise<Uint8Array> {\n // Load PDF\n let pdfBytes: ArrayBuffer;\n if (typeof pdfSource === \"string\") {\n const response = await fetch(pdfSource);\n pdfBytes = await response.arrayBuffer();\n } else {\n pdfBytes =\n pdfSource instanceof Uint8Array\n ? pdfSource.buffer.slice(\n pdfSource.byteOffset,\n pdfSource.byteOffset + pdfSource.byteLength\n )\n : pdfSource;\n }\n\n const pdfDoc = await PDFDocument.load(pdfBytes);\n const pages = pdfDoc.getPages();\n const font = await pdfDoc.embedFont(StandardFonts.Helvetica);\n\n // Group by page and render\n const byPage = groupByPage(highlights);\n const totalPages = byPage.size;\n let currentPage = 0;\n\n for (const [pageNum, pageHighlights] of byPage) {\n const page = pages[pageNum - 1]; // 1-indexed to 0-indexed\n if (!page) continue;\n\n for (const highlight of pageHighlights) {\n switch (highlight.type) {\n case \"text\":\n await renderTextHighlight(page, highlight, options);\n break;\n case \"area\":\n await renderAreaHighlight(page, highlight, options);\n break;\n case \"freetext\":\n await renderFreetextHighlight(page, highlight, options, font);\n break;\n case \"image\":\n await renderImageHighlight(pdfDoc, page, highlight);\n break;\n case \"drawing\":\n // Drawings are stored as PNG images, reuse image highlight rendering\n // but with contain-fit so an ink drawing keeps its aspect ratio.\n await renderImageHighlight(pdfDoc, page, highlight, \"drawing\");\n break;\n case \"shape\":\n await renderShapeHighlight(page, highlight);\n break;\n default:\n // Default to area highlight for backwards compatibility\n await renderAreaHighlight(page, highlight, options);\n }\n }\n\n currentPage++;\n options.onProgress?.(currentPage, totalPages);\n }\n\n return pdfDoc.save();\n}\n"]}