react-pdf-highlighter-plus 1.1.3 → 1.1.4

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
@@ -20,7 +20,7 @@
20
20
  </p>
21
21
 
22
22
  <p align="center">
23
- Text highlights • Area highlights • Freetext notes • Images & signatures • Freehand drawing • PDF export
23
+ Text highlights • Area highlights • Freetext notes • Images & signatures • Freehand drawing • Shapes • Search • PDF export
24
24
  </p>
25
25
 
26
26
  ---
@@ -29,16 +29,23 @@
29
29
 
30
30
  `react-pdf-highlighter-plus` provides a highly customizable annotation experience for PDF documents in React applications. Built on [PDF.js](https://github.com/mozilla/pdf.js), it stores highlight positions in viewport-independent coordinates, making them portable across different screen sizes.
31
31
 
32
+ <p align="center">
33
+ <img src="docs/react-pdf-highlight-plus.png" alt="react-pdf-highlighter-plus internal architecture" width="900">
34
+ </p>
35
+
32
36
  ## Features
33
37
 
34
38
  | Feature | Description |
35
39
  |---------|-------------|
36
- | **Text Highlights** | Select and highlight text passages |
37
- | **Area Highlights** | Draw rectangular regions on PDFs |
38
- | **Freetext Notes** | Draggable, editable sticky notes with custom styling |
40
+ | **Text Highlights** | Select and highlight text passages, restyle them, and copy their text |
41
+ | **Area Highlights** | Draw rectangular regions on PDFs and copy intersecting PDF text |
42
+ | **Freetext Notes** | Draggable, editable sticky notes with custom styling and compact mode |
39
43
  | **Images & Signatures** | Upload images or draw signatures directly on PDFs |
40
44
  | **Freehand Drawing** | Draw freehand annotations with customizable stroke |
45
+ | **Shapes** | Add rectangles, circles, and arrows with editable stroke style |
46
+ | **PDF Search** | Search through all PDF text with next/previous navigation |
41
47
  | **PDF Export** | Export annotated PDF with all highlights embedded |
48
+ | **Local PDF Worker** | Uses the packaged PDF.js worker by default |
42
49
  | **Light/Dark Theme** | Eye-friendly dark mode with customizable intensity |
43
50
  | **Zoom Support** | Full zoom functionality with position-independent data |
44
51
  | **Fully Customizable** | Exposed styling on all components |
@@ -65,6 +72,8 @@ npm install react-pdf-highlighter-plus
65
72
  import "react-pdf-highlighter-plus/style/style.css";
66
73
  ```
67
74
 
75
+ PDF.js worker setup is handled by the package by default. The build copies the local `pdf.worker.min.mjs` into the package output, so most apps do not need to configure `workerSrc` manually.
76
+
68
77
  ---
69
78
 
70
79
  ## Quick Start
@@ -126,6 +135,10 @@ Select text in the PDF to create highlights.
126
135
  />
127
136
  ```
128
137
 
138
+ Text and area highlight toolbars include a copy button. After copying, the icon changes to a check mark for 1.5 seconds.
139
+
140
+ [Full Documentation →](docs/text-area-highlights.md)
141
+
129
142
  ### 2. Area Highlights
130
143
 
131
144
  Hold `Alt` and drag to create rectangular highlights.
@@ -211,14 +224,12 @@ Draw freehand annotations directly on PDFs.
211
224
  import { DrawingHighlight } from "react-pdf-highlighter-plus";
212
225
 
213
226
  <PdfHighlighter
214
- enableDrawingCreation={() => drawingMode}
215
- onDrawingComplete={(position, dataUrl) => {
227
+ enableDrawingMode={drawingMode}
228
+ onDrawingComplete={(dataUrl, position, strokes) => {
216
229
  addHighlight({ type: "drawing", position, content: { image: dataUrl } });
217
230
  }}
218
- drawingConfig={{
219
- strokeColor: "#ff0000",
220
- strokeWidth: 2,
221
- }}
231
+ drawingStrokeColor="#ff0000"
232
+ drawingStrokeWidth={2}
222
233
  >
223
234
 
224
235
  // In your highlight container:
@@ -238,6 +249,54 @@ import { DrawingHighlight } from "react-pdf-highlighter-plus";
238
249
 
239
250
  ---
240
251
 
252
+ ## Shapes
253
+
254
+ Create rectangle, circle, and arrow annotations with editable stroke color and width.
255
+
256
+ ```tsx
257
+ <PdfHighlighter
258
+ enableShapeMode={shapeMode} // "rectangle" | "circle" | "arrow" | null
259
+ onShapeComplete={(position, shape) => {
260
+ addHighlight({ type: "shape", position, content: { shape } });
261
+ }}
262
+ shapeStrokeColor="#000000"
263
+ shapeStrokeWidth={2}
264
+ >
265
+ <HighlightContainer />
266
+ </PdfHighlighter>
267
+ ```
268
+
269
+ Shape geometry stays in the normal highlight layer while the style controls render in the higher config layer.
270
+
271
+ [Full Documentation →](docs/shape-highlights.md)
272
+
273
+ ---
274
+
275
+ ## PDF Search
276
+
277
+ Use `utilsRef` to access document-wide search helpers backed by PDF.js `PDFFindController`.
278
+
279
+ ```tsx
280
+ const highlighterUtilsRef = useRef<PdfHighlighterUtils>();
281
+
282
+ <PdfHighlighter
283
+ pdfDocument={pdfDocument}
284
+ highlights={highlights}
285
+ utilsRef={(utils) => (highlighterUtilsRef.current = utils)}
286
+ >
287
+ <HighlightContainer />
288
+ </PdfHighlighter>
289
+
290
+ highlighterUtilsRef.current?.search("TypeScript", {
291
+ highlightAll: true,
292
+ caseSensitive: false,
293
+ });
294
+ highlighterUtilsRef.current?.findNext();
295
+ highlighterUtilsRef.current?.clearSearch();
296
+ ```
297
+
298
+ ---
299
+
241
300
  ## Light/Dark Theme
242
301
 
243
302
  Toggle between light and dark modes with customizable styling for comfortable reading.
@@ -318,6 +377,7 @@ const handleExport = async () => {
318
377
  - Freetext notes (background + wrapped text)
319
378
  - Images & signatures (embedded PNG/JPG)
320
379
  - Freehand drawings (embedded PNG)
380
+ - Shapes (rectangle, circle, arrow)
321
381
 
322
382
  [Full Documentation →](docs/pdf-export.md)
323
383
 
@@ -325,28 +385,19 @@ const handleExport = async () => {
325
385
 
326
386
  ## Component Architecture
327
387
 
328
- ```
329
- ┌─────────────────────────────────────────────────────┐
330
- │ PdfLoader │
331
- │ Loads PDF document via PDF.js │
332
- │ │
333
- │ ┌───────────────────────────────────────────────┐ │
334
- │ │ PdfHighlighter │ │
335
- │ │ Manages viewer, events, coordinate systems │ │
336
- │ │ │ │
337
- │ │ ┌─────────────────────────────────────────┐ │ │
338
- │ │ │ User-defined HighlightContainer │ │ │
339
- │ │ │ Renders highlights using context hooks │ │ │
340
- │ │ │ │ │ │
341
- │ │ │ • TextHighlight │ │ │
342
- │ │ │ • AreaHighlight │ │ │
343
- │ │ │ • FreetextHighlight │ │ │
344
- │ │ │ • ImageHighlight │ │ │
345
- │ │ │ • DrawingHighlight │ │ │
346
- │ │ └─────────────────────────────────────────┘ │ │
347
- │ └───────────────────────────────────────────────┘ │
348
- └─────────────────────────────────────────────────────┘
349
- ```
388
+ The package renders React annotation components into PDF.js page overlay layers:
389
+
390
+ | Layer | Mount point | Purpose |
391
+ |-------|-------------|---------|
392
+ | `.PdfHighlighter__highlight-layer` | Inside PDF.js `.textLayer` | Annotation geometry for text, area, image/signature, drawing, and shape |
393
+ | `.PdfHighlighter__note-layer` | Direct child of PDF.js `.page` | Freetext notes and compact note markers above PDF content |
394
+ | `.PdfHighlighter__config-layer` | Direct child of PDF.js `.page` | Toolbars, style panels, copy buttons, and controls above PDF content |
395
+
396
+ Internal flow diagram:
397
+
398
+ - [Architecture PNG](docs/react-pdf-highlight-plus.png)
399
+ - [Draw.io source](docs/package-build-and-features.drawio)
400
+ - [Draw.io XML copy](docs/package-build-and-features.xml)
350
401
 
351
402
  ### Context Hooks
352
403
 
@@ -0,0 +1,403 @@
1
+ // src/lib/export-pdf.ts
2
+ import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
3
+ function parseColor(color) {
4
+ const rgbaMatch = color.match(
5
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
6
+ );
7
+ if (rgbaMatch) {
8
+ return {
9
+ r: parseInt(rgbaMatch[1]) / 255,
10
+ g: parseInt(rgbaMatch[2]) / 255,
11
+ b: parseInt(rgbaMatch[3]) / 255,
12
+ a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
13
+ };
14
+ }
15
+ const hex = color.replace("#", "");
16
+ if (hex.length === 3) {
17
+ return {
18
+ r: parseInt(hex[0] + hex[0], 16) / 255,
19
+ g: parseInt(hex[1] + hex[1], 16) / 255,
20
+ b: parseInt(hex[2] + hex[2], 16) / 255,
21
+ a: 1
22
+ };
23
+ }
24
+ if (hex.length === 6) {
25
+ return {
26
+ r: parseInt(hex.slice(0, 2), 16) / 255,
27
+ g: parseInt(hex.slice(2, 4), 16) / 255,
28
+ b: parseInt(hex.slice(4, 6), 16) / 255,
29
+ a: 1
30
+ };
31
+ }
32
+ return { r: 1, g: 0.89, b: 0.56, a: 0.5 };
33
+ }
34
+ function scaledToPdfPoints(scaled, page) {
35
+ const pdfWidth = page.getWidth();
36
+ const pdfHeight = page.getHeight();
37
+ const xRatio = pdfWidth / scaled.width;
38
+ const yRatio = pdfHeight / scaled.height;
39
+ const x = scaled.x1 * xRatio;
40
+ const width = (scaled.x2 - scaled.x1) * xRatio;
41
+ const height = (scaled.y2 - scaled.y1) * yRatio;
42
+ const y = pdfHeight - scaled.y1 * yRatio - height;
43
+ return { x, y, width, height };
44
+ }
45
+ function dataUrlToBytes(dataUrl) {
46
+ const base64 = dataUrl.split(",")[1];
47
+ const byteString = atob(base64);
48
+ const bytes = new Uint8Array(byteString.length);
49
+ for (let i = 0; i < byteString.length; i++) {
50
+ bytes[i] = byteString.charCodeAt(i);
51
+ }
52
+ const type = dataUrl.includes("image/png") ? "png" : "jpg";
53
+ return { bytes, type };
54
+ }
55
+ function wrapText(text, font, fontSize, maxWidth) {
56
+ if (!text || maxWidth <= 0) return [];
57
+ const lines = [];
58
+ const paragraphs = text.split(/\n/);
59
+ for (const paragraph of paragraphs) {
60
+ if (!paragraph.trim()) {
61
+ lines.push("");
62
+ continue;
63
+ }
64
+ const words = paragraph.split(/\s+/);
65
+ let currentLine = "";
66
+ for (const word of words) {
67
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
68
+ const testWidth = font.widthOfTextAtSize(testLine, fontSize);
69
+ if (testWidth <= maxWidth) {
70
+ currentLine = testLine;
71
+ } else {
72
+ if (currentLine) {
73
+ lines.push(currentLine);
74
+ currentLine = "";
75
+ }
76
+ if (font.widthOfTextAtSize(word, fontSize) > maxWidth) {
77
+ let remaining = word;
78
+ while (remaining.length > 0) {
79
+ let charCount = 1;
80
+ while (charCount < remaining.length && font.widthOfTextAtSize(remaining.substring(0, charCount + 1), fontSize) <= maxWidth) {
81
+ charCount++;
82
+ }
83
+ const chunk = remaining.substring(0, charCount);
84
+ remaining = remaining.substring(charCount);
85
+ if (remaining.length > 0) {
86
+ lines.push(chunk);
87
+ } else {
88
+ currentLine = chunk;
89
+ }
90
+ }
91
+ } else {
92
+ currentLine = word;
93
+ }
94
+ }
95
+ }
96
+ if (currentLine) lines.push(currentLine);
97
+ }
98
+ return lines;
99
+ }
100
+ function groupByPage(highlights) {
101
+ const map = /* @__PURE__ */ new Map();
102
+ for (const h of highlights) {
103
+ const pageNum = h.position.boundingRect.pageNumber;
104
+ if (!map.has(pageNum)) map.set(pageNum, []);
105
+ map.get(pageNum).push(h);
106
+ }
107
+ return map;
108
+ }
109
+ async function renderTextHighlight(page, highlight, options) {
110
+ const colorStr = highlight.highlightColor || options.textHighlightColor || "rgba(255, 226, 143, 0.5)";
111
+ const color = parseColor(colorStr);
112
+ const highlightStyle = highlight.highlightStyle || "highlight";
113
+ const rects = highlight.position.rects.length > 0 ? highlight.position.rects : [highlight.position.boundingRect];
114
+ for (const rect of rects) {
115
+ const { x, y, width, height } = scaledToPdfPoints(rect, page);
116
+ if (highlightStyle === "highlight") {
117
+ page.drawRectangle({
118
+ x,
119
+ y,
120
+ width,
121
+ height,
122
+ color: rgb(color.r, color.g, color.b),
123
+ opacity: color.a
124
+ });
125
+ } else if (highlightStyle === "underline") {
126
+ const lineThickness = Math.max(1, height * 0.1);
127
+ page.drawRectangle({
128
+ x,
129
+ y,
130
+ width,
131
+ height: lineThickness,
132
+ color: rgb(color.r, color.g, color.b),
133
+ opacity: color.a
134
+ });
135
+ } else if (highlightStyle === "strikethrough") {
136
+ const lineThickness = Math.max(1, height * 0.1);
137
+ const lineY = y + height / 2 - lineThickness / 2;
138
+ page.drawRectangle({
139
+ x,
140
+ y: lineY,
141
+ width,
142
+ height: lineThickness,
143
+ color: rgb(color.r, color.g, color.b),
144
+ opacity: color.a
145
+ });
146
+ }
147
+ }
148
+ }
149
+ async function renderAreaHighlight(page, highlight, options) {
150
+ const colorStr = highlight.highlightColor || options.areaHighlightColor || "rgba(255, 226, 143, 0.5)";
151
+ const color = parseColor(colorStr);
152
+ const { x, y, width, height } = scaledToPdfPoints(
153
+ highlight.position.boundingRect,
154
+ page
155
+ );
156
+ page.drawRectangle({
157
+ x,
158
+ y,
159
+ width,
160
+ height,
161
+ color: rgb(color.r, color.g, color.b),
162
+ opacity: color.a
163
+ });
164
+ }
165
+ async function renderFreetextHighlight(page, highlight, options, font) {
166
+ const text = highlight.content?.text || "";
167
+ const textColor = parseColor(
168
+ highlight.color || options.defaultFreetextColor || "#333333"
169
+ );
170
+ const { x, y, width, height } = scaledToPdfPoints(
171
+ highlight.position.boundingRect,
172
+ page
173
+ );
174
+ const pdfHeight = page.getHeight();
175
+ const yRatio = pdfHeight / highlight.position.boundingRect.height;
176
+ const storedFontSize = parseInt(highlight.fontSize || "") || options.defaultFreetextFontSize || 14;
177
+ const fontSize = storedFontSize * yRatio;
178
+ console.log("Freetext export:", {
179
+ storedFontSize,
180
+ yRatio,
181
+ fontSize,
182
+ boxDimensions: { x, y, width, height },
183
+ text: text.substring(0, 50)
184
+ });
185
+ const bgColorValue = highlight.backgroundColor || options.defaultFreetextBgColor || "#ffffc8";
186
+ if (bgColorValue !== "transparent") {
187
+ const bgColor = parseColor(bgColorValue);
188
+ page.drawRectangle({
189
+ x,
190
+ y,
191
+ width,
192
+ height,
193
+ color: rgb(bgColor.r, bgColor.g, bgColor.b),
194
+ opacity: bgColor.a
195
+ });
196
+ }
197
+ const padding = 4 * yRatio;
198
+ const maxWidth = width - padding * 2;
199
+ const lineHeight = fontSize * 1.3;
200
+ if (maxWidth > 0 && text) {
201
+ const lines = wrapText(text, font, fontSize, maxWidth);
202
+ let currentY = y + height - fontSize - padding;
203
+ for (const line of lines) {
204
+ if (currentY < y + padding) break;
205
+ if (line.trim()) {
206
+ page.drawText(line, {
207
+ x: x + padding,
208
+ y: currentY,
209
+ size: fontSize,
210
+ font,
211
+ color: rgb(textColor.r, textColor.g, textColor.b)
212
+ });
213
+ }
214
+ currentY -= lineHeight;
215
+ }
216
+ }
217
+ }
218
+ function transformToRawCoordinates(page, x, y, width, height) {
219
+ const rotation = page.getRotation().angle;
220
+ const pageWidth = page.getWidth();
221
+ const pageHeight = page.getHeight();
222
+ if (rotation === 90) {
223
+ return {
224
+ x: y,
225
+ y: pageWidth - x - width,
226
+ width: height,
227
+ height: width
228
+ };
229
+ } else if (rotation === 180) {
230
+ return {
231
+ x: pageWidth - x - width,
232
+ y: pageHeight - y - height,
233
+ width,
234
+ height
235
+ };
236
+ } else if (rotation === 270) {
237
+ return {
238
+ x: pageHeight - y - height,
239
+ y: x,
240
+ width: height,
241
+ height: width
242
+ };
243
+ }
244
+ return { x, y, width, height };
245
+ }
246
+ async function renderImageHighlight(pdfDoc, page, highlight) {
247
+ const imageDataUrl = highlight.content?.image;
248
+ if (!imageDataUrl) return;
249
+ try {
250
+ const { bytes, type } = dataUrlToBytes(imageDataUrl);
251
+ const image = type === "png" ? await pdfDoc.embedPng(bytes) : await pdfDoc.embedJpg(bytes);
252
+ const visualCoords = scaledToPdfPoints(
253
+ highlight.position.boundingRect,
254
+ page
255
+ );
256
+ const rawCoords = transformToRawCoordinates(
257
+ page,
258
+ visualCoords.x,
259
+ visualCoords.y,
260
+ visualCoords.width,
261
+ visualCoords.height
262
+ );
263
+ console.log("Image export:", {
264
+ rotation: page.getRotation().angle,
265
+ visualCoords,
266
+ rawCoords
267
+ });
268
+ page.drawImage(image, {
269
+ x: rawCoords.x,
270
+ y: rawCoords.y,
271
+ width: rawCoords.width,
272
+ height: rawCoords.height
273
+ });
274
+ } catch (error) {
275
+ console.error("Failed to embed image:", error);
276
+ }
277
+ }
278
+ async function renderShapeHighlight(page, highlight) {
279
+ const shapeType = highlight.content?.shape?.shapeType || highlight.shapeType || "rectangle";
280
+ const strokeColorStr = highlight.content?.shape?.strokeColor || highlight.strokeColor || "#000000";
281
+ const strokeWidth = highlight.content?.shape?.strokeWidth || highlight.strokeWidth || 2;
282
+ const color = parseColor(strokeColorStr);
283
+ const { x, y, width, height } = scaledToPdfPoints(
284
+ highlight.position.boundingRect,
285
+ page
286
+ );
287
+ switch (shapeType) {
288
+ case "rectangle":
289
+ page.drawRectangle({
290
+ x,
291
+ y,
292
+ width,
293
+ height,
294
+ borderColor: rgb(color.r, color.g, color.b),
295
+ borderWidth: strokeWidth,
296
+ opacity: color.a
297
+ });
298
+ break;
299
+ case "circle":
300
+ page.drawEllipse({
301
+ x: x + width / 2,
302
+ y: y + height / 2,
303
+ xScale: width / 2,
304
+ yScale: height / 2,
305
+ borderColor: rgb(color.r, color.g, color.b),
306
+ borderWidth: strokeWidth,
307
+ opacity: color.a
308
+ });
309
+ break;
310
+ case "arrow": {
311
+ const startPt = highlight.content?.shape?.startPoint;
312
+ const endPt = highlight.content?.shape?.endPoint;
313
+ const startX = startPt ? x + startPt.x * width : x;
314
+ const startY = startPt ? y + (1 - startPt.y) * height : y + height / 2;
315
+ const endX = endPt ? x + endPt.x * width : x + width;
316
+ const endY = endPt ? y + (1 - endPt.y) * height : y + height / 2;
317
+ page.drawLine({
318
+ start: { x: startX, y: startY },
319
+ end: { x: endX, y: endY },
320
+ color: rgb(color.r, color.g, color.b),
321
+ thickness: strokeWidth,
322
+ opacity: color.a
323
+ });
324
+ const angle = Math.atan2(endY - startY, endX - startX);
325
+ const arrowSize = Math.min(15, width * 0.2, height * 0.4);
326
+ const arrowAngle = Math.PI / 6;
327
+ page.drawLine({
328
+ start: {
329
+ x: endX - arrowSize * Math.cos(angle - arrowAngle),
330
+ y: endY - arrowSize * Math.sin(angle - arrowAngle)
331
+ },
332
+ end: { x: endX, y: endY },
333
+ color: rgb(color.r, color.g, color.b),
334
+ thickness: strokeWidth,
335
+ opacity: color.a
336
+ });
337
+ page.drawLine({
338
+ start: {
339
+ x: endX - arrowSize * Math.cos(angle + arrowAngle),
340
+ y: endY - arrowSize * Math.sin(angle + arrowAngle)
341
+ },
342
+ end: { x: endX, y: endY },
343
+ color: rgb(color.r, color.g, color.b),
344
+ thickness: strokeWidth,
345
+ opacity: color.a
346
+ });
347
+ break;
348
+ }
349
+ }
350
+ }
351
+ async function exportPdf(pdfSource, highlights, options = {}) {
352
+ let pdfBytes;
353
+ if (typeof pdfSource === "string") {
354
+ const response = await fetch(pdfSource);
355
+ pdfBytes = await response.arrayBuffer();
356
+ } else {
357
+ pdfBytes = pdfSource instanceof Uint8Array ? pdfSource.buffer.slice(
358
+ pdfSource.byteOffset,
359
+ pdfSource.byteOffset + pdfSource.byteLength
360
+ ) : pdfSource;
361
+ }
362
+ const pdfDoc = await PDFDocument.load(pdfBytes);
363
+ const pages = pdfDoc.getPages();
364
+ const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
365
+ const byPage = groupByPage(highlights);
366
+ const totalPages = byPage.size;
367
+ let currentPage = 0;
368
+ for (const [pageNum, pageHighlights] of byPage) {
369
+ const page = pages[pageNum - 1];
370
+ if (!page) continue;
371
+ for (const highlight of pageHighlights) {
372
+ switch (highlight.type) {
373
+ case "text":
374
+ await renderTextHighlight(page, highlight, options);
375
+ break;
376
+ case "area":
377
+ await renderAreaHighlight(page, highlight, options);
378
+ break;
379
+ case "freetext":
380
+ await renderFreetextHighlight(page, highlight, options, font);
381
+ break;
382
+ case "image":
383
+ await renderImageHighlight(pdfDoc, page, highlight);
384
+ break;
385
+ case "drawing":
386
+ await renderImageHighlight(pdfDoc, page, highlight);
387
+ break;
388
+ case "shape":
389
+ await renderShapeHighlight(page, highlight);
390
+ break;
391
+ default:
392
+ await renderAreaHighlight(page, highlight, options);
393
+ }
394
+ }
395
+ currentPage++;
396
+ options.onProgress?.(currentPage, totalPages);
397
+ }
398
+ return pdfDoc.save();
399
+ }
400
+ export {
401
+ exportPdf
402
+ };
403
+ //# sourceMappingURL=export-pdf-W2QGWADM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/export-pdf.ts"],"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 * 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 rectangle for background highlight\n page.drawRectangle({\n x,\n y,\n width,\n height,\n color: rgb(color.r, color.g, color.b),\n opacity: color.a,\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 page.drawRectangle({\n x,\n y,\n width,\n height,\n color: rgb(color.r, color.g, color.b),\n opacity: color.a,\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 page.drawRectangle({\n x,\n y,\n width,\n height,\n color: rgb(bgColor.r, bgColor.g, bgColor.b),\n opacity: bgColor.a,\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 highlight (embedded image).\n * Handles page rotation by transforming visual coordinates to raw MediaBox space.\n * Image fills the entire bounding box to match the visual wrapper in preview.\n */\nasync function renderImageHighlight(\n pdfDoc: PDFDocument,\n page: PDFPage,\n highlight: ExportableHighlight\n): Promise<void> {\n const imageDataUrl = highlight.content?.image;\n if (!imageDataUrl) return;\n\n try {\n const { bytes, type } = dataUrlToBytes(imageDataUrl);\n const image =\n type === \"png\"\n ? await pdfDoc.embedPng(bytes)\n : await pdfDoc.embedJpg(bytes);\n\n // Calculate coordinates in visual space - use full bounding box dimensions\n const visualCoords = scaledToPdfPoints(\n highlight.position.boundingRect,\n page\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 await renderImageHighlight(pdfDoc, page, highlight);\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"],"mappings":"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","names":[]}