react-pdf-highlighter-plus 1.2.0 → 1.3.1
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/dist/esm/{export-pdf-W2QGWADM.js → export-pdf-DD7J5UHW.js} +113 -15
- package/dist/esm/export-pdf-DD7J5UHW.js.map +1 -0
- package/dist/esm/index.d.ts +45 -8
- package/dist/esm/index.js +1553 -988
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/pdf.worker.min.mjs +2 -2
- package/dist/esm/style/AreaHighlight.css +178 -73
- package/dist/esm/style/DrawingCanvas.css +24 -16
- package/dist/esm/style/DrawingHighlight.css +193 -61
- package/dist/esm/style/FreetextHighlight.css +217 -76
- package/dist/esm/style/ImageHighlight.css +98 -26
- package/dist/esm/style/MouseSelection.css +2 -2
- package/dist/esm/style/PdfHighlighter.css +29 -15
- package/dist/esm/style/ShapeCanvas.css +18 -12
- package/dist/esm/style/ShapeHighlight.css +142 -72
- package/dist/esm/style/SignaturePad.css +154 -38
- package/dist/esm/style/TextHighlight.css +205 -75
- package/dist/esm/style/style.css +1 -0
- package/dist/esm/style/tokens.css +88 -0
- package/package.json +32 -16
- package/dist/esm/export-pdf-W2QGWADM.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
.TextHighlight {
|
|
2
2
|
position: absolute;
|
|
3
|
+
font-family: var(--rphp-font);
|
|
3
4
|
}
|
|
4
5
|
|
|
5
6
|
.TextHighlight__parts {
|
|
@@ -9,14 +10,21 @@
|
|
|
9
10
|
.TextHighlight__part {
|
|
10
11
|
cursor: pointer;
|
|
11
12
|
position: absolute;
|
|
12
|
-
background: rgba(255,
|
|
13
|
+
background: rgba(255, 214, 102, 0.95); /* --rphp-hl-yellow */
|
|
13
14
|
transition: background 0.3s, box-shadow 0.2s ease;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/* Selected: accent ring (same shape as scrolledTo) */
|
|
18
|
+
.TextHighlight--selected .TextHighlight__part {
|
|
19
|
+
box-shadow:
|
|
20
|
+
0 0 0 2px var(--rphp-accent, #5b50e6),
|
|
21
|
+
0 0 0 5px rgba(91, 80, 230, 0.25);
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
.TextHighlight--scrolledTo .TextHighlight__part {
|
|
17
25
|
box-shadow:
|
|
18
|
-
0 0 0 2px #
|
|
19
|
-
0 0 0
|
|
26
|
+
0 0 0 2px var(--rphp-accent, #5b50e6),
|
|
27
|
+
0 0 0 5px rgba(91, 80, 230, 0.25);
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/* Toolbar wrapper - creates hover bridge between toolbar and highlight */
|
|
@@ -24,68 +32,186 @@
|
|
|
24
32
|
z-index: 10;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
/* Toolbar -
|
|
35
|
+
/* Toolbar - dark ink pill floating ABOVE the highlight (never covers it).
|
|
36
|
+
Shown on hover or while the highlight is selected. */
|
|
28
37
|
.TextHighlight__toolbar {
|
|
29
38
|
display: flex;
|
|
30
39
|
align-items: center;
|
|
31
|
-
gap:
|
|
32
|
-
padding:
|
|
33
|
-
background:
|
|
34
|
-
border-radius:
|
|
40
|
+
gap: 2px;
|
|
41
|
+
padding: 4px 6px;
|
|
42
|
+
background: var(--rphp-toolbar-bg, #1c1b18);
|
|
43
|
+
border-radius: var(--rphp-radius, 9px);
|
|
44
|
+
box-shadow: var(--rphp-shadow-lg, 0 8px 24px -6px rgba(28, 27, 24, 0.22));
|
|
45
|
+
position: absolute;
|
|
46
|
+
bottom: calc(100% + 6px);
|
|
47
|
+
left: 0;
|
|
35
48
|
opacity: 0;
|
|
36
49
|
pointer-events: none;
|
|
37
|
-
transition: opacity 0.
|
|
50
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
51
|
+
transform: translateY(2px);
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Hover bridge across the 6px gap between toolbar and highlight */
|
|
56
|
+
.TextHighlight__toolbar::after {
|
|
57
|
+
content: "";
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: 100%;
|
|
60
|
+
left: 0;
|
|
61
|
+
right: 0;
|
|
62
|
+
height: 10px;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
.TextHighlight__toolbar--visible {
|
|
41
66
|
opacity: 1;
|
|
42
67
|
pointer-events: auto;
|
|
68
|
+
transform: translateY(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Flipped below the highlight — used when there's no room above (e.g. the
|
|
72
|
+
highlight sits at the very top of the page) so the toolbar doesn't float
|
|
73
|
+
up over whatever page content is above it. */
|
|
74
|
+
.TextHighlight__toolbar--below {
|
|
75
|
+
bottom: auto;
|
|
76
|
+
top: calc(100% + 6px);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.TextHighlight__toolbar--below::after {
|
|
80
|
+
top: auto;
|
|
81
|
+
bottom: 100%;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Color dropdown trigger: current-color swatch + chevron */
|
|
85
|
+
.TextHighlight__color-trigger {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 4px;
|
|
89
|
+
height: 26px;
|
|
90
|
+
padding: 0 6px 0 4px;
|
|
91
|
+
border: none;
|
|
92
|
+
background: transparent;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
color: #efece5;
|
|
95
|
+
border-radius: var(--rphp-radius-sm, 6px);
|
|
96
|
+
transition: background 0.15s, color 0.15s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.TextHighlight__color-trigger:hover,
|
|
100
|
+
.TextHighlight__color-trigger[aria-expanded="true"] {
|
|
101
|
+
background: rgba(255, 255, 255, 0.14);
|
|
102
|
+
color: #ffffff;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.TextHighlight__color-trigger-dot {
|
|
106
|
+
width: 14px;
|
|
107
|
+
height: 14px;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
border: 1.5px solid rgba(255, 255, 255, 0.5);
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.TextHighlight__toolbar-divider {
|
|
114
|
+
width: 1px;
|
|
115
|
+
height: 16px;
|
|
116
|
+
margin: 0 3px;
|
|
117
|
+
background: rgba(255, 255, 255, 0.2);
|
|
43
118
|
}
|
|
44
119
|
|
|
45
120
|
.TextHighlight__style-button,
|
|
46
121
|
.TextHighlight__copy-button,
|
|
122
|
+
.TextHighlight__comment-button,
|
|
47
123
|
.TextHighlight__delete-button {
|
|
48
124
|
display: flex;
|
|
49
125
|
align-items: center;
|
|
50
126
|
justify-content: center;
|
|
51
|
-
width:
|
|
52
|
-
height:
|
|
127
|
+
width: 26px;
|
|
128
|
+
height: 26px;
|
|
53
129
|
border: none;
|
|
54
130
|
background: transparent;
|
|
55
131
|
cursor: pointer;
|
|
56
|
-
color:
|
|
57
|
-
border-radius:
|
|
132
|
+
color: #efece5;
|
|
133
|
+
border-radius: var(--rphp-radius-sm, 6px);
|
|
58
134
|
padding: 0;
|
|
59
|
-
|
|
135
|
+
position: relative;
|
|
136
|
+
transition: background 0.15s, color 0.15s;
|
|
60
137
|
}
|
|
61
138
|
|
|
62
|
-
.TextHighlight__style-button:hover
|
|
63
|
-
|
|
139
|
+
.TextHighlight__style-button:hover,
|
|
140
|
+
.TextHighlight__copy-button:hover,
|
|
141
|
+
.TextHighlight__comment-button:hover,
|
|
142
|
+
.TextHighlight__comment-button.active {
|
|
143
|
+
background: rgba(255, 255, 255, 0.14);
|
|
144
|
+
color: #ffffff;
|
|
64
145
|
}
|
|
65
146
|
|
|
66
|
-
.
|
|
67
|
-
background: rgba(
|
|
147
|
+
.TextHighlight__delete-button:hover {
|
|
148
|
+
background: rgba(239, 68, 68, 0.24);
|
|
149
|
+
color: #ff8a80;
|
|
68
150
|
}
|
|
69
151
|
|
|
70
|
-
|
|
71
|
-
|
|
152
|
+
/* Dot badge: this highlight has a note, shown while the panel is closed */
|
|
153
|
+
.TextHighlight__comment-dot {
|
|
154
|
+
position: absolute;
|
|
155
|
+
top: 3px;
|
|
156
|
+
right: 3px;
|
|
157
|
+
width: 6px;
|
|
158
|
+
height: 6px;
|
|
159
|
+
border-radius: 50%;
|
|
160
|
+
background: var(--rphp-accent-tint, #d8d4fa);
|
|
161
|
+
pointer-events: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Comment panel - light surface card (Verso popover), same slot as the style panel */
|
|
165
|
+
.TextHighlight__comment-panel {
|
|
166
|
+
margin-top: 6px;
|
|
167
|
+
width: 260px;
|
|
168
|
+
background: var(--rphp-surface, #ffffff);
|
|
169
|
+
border: 1px solid var(--rphp-border, #e6e2da);
|
|
170
|
+
border-radius: var(--rphp-radius-lg, 12px);
|
|
171
|
+
box-shadow: var(--rphp-shadow-lg, 0 8px 24px -6px rgba(28, 27, 24, 0.22));
|
|
172
|
+
overflow: hidden;
|
|
72
173
|
}
|
|
73
174
|
|
|
74
|
-
|
|
175
|
+
.TextHighlight__comment-panel textarea {
|
|
176
|
+
display: block;
|
|
177
|
+
width: 100%;
|
|
178
|
+
box-sizing: border-box;
|
|
179
|
+
max-height: 160px;
|
|
180
|
+
padding: 10px 12px;
|
|
181
|
+
border: none;
|
|
182
|
+
outline: none;
|
|
183
|
+
resize: none;
|
|
184
|
+
background: transparent;
|
|
185
|
+
color: var(--rphp-text, #1c1b18);
|
|
186
|
+
font: inherit;
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
line-height: 1.5;
|
|
189
|
+
white-space: pre-wrap;
|
|
190
|
+
word-break: break-word;
|
|
191
|
+
overflow-y: auto;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.TextHighlight__comment-panel textarea::placeholder {
|
|
195
|
+
color: var(--rphp-text-muted, #8b8880);
|
|
196
|
+
font-style: italic;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Style Panel - light surface card (Verso popover) */
|
|
75
200
|
.TextHighlight__style-panel {
|
|
76
|
-
margin-top:
|
|
77
|
-
background:
|
|
78
|
-
border
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
201
|
+
margin-top: 6px;
|
|
202
|
+
background: var(--rphp-surface, #ffffff);
|
|
203
|
+
border: 1px solid var(--rphp-border, #e6e2da);
|
|
204
|
+
border-radius: var(--rphp-radius-lg, 12px);
|
|
205
|
+
padding: 12px;
|
|
206
|
+
min-width: 200px;
|
|
207
|
+
box-shadow: var(--rphp-shadow-lg, 0 8px 24px -6px rgba(28, 27, 24, 0.22));
|
|
82
208
|
}
|
|
83
209
|
|
|
84
210
|
.TextHighlight__style-row {
|
|
85
211
|
display: flex;
|
|
86
212
|
align-items: center;
|
|
87
213
|
justify-content: space-between;
|
|
88
|
-
margin-bottom:
|
|
214
|
+
margin-bottom: 10px;
|
|
89
215
|
}
|
|
90
216
|
|
|
91
217
|
.TextHighlight__style-row:last-child {
|
|
@@ -93,11 +219,12 @@
|
|
|
93
219
|
}
|
|
94
220
|
|
|
95
221
|
.TextHighlight__style-row label {
|
|
96
|
-
color: #
|
|
97
|
-
font-size:
|
|
222
|
+
color: var(--rphp-text-muted, #8b8880);
|
|
223
|
+
font-size: 10px;
|
|
224
|
+
font-weight: 600;
|
|
98
225
|
text-transform: uppercase;
|
|
99
|
-
letter-spacing: 0.
|
|
100
|
-
margin-right:
|
|
226
|
+
letter-spacing: 0.8px;
|
|
227
|
+
margin-right: 10px;
|
|
101
228
|
}
|
|
102
229
|
|
|
103
230
|
/* Style type buttons (highlight, underline, strikethrough) */
|
|
@@ -113,71 +240,74 @@
|
|
|
113
240
|
width: 28px;
|
|
114
241
|
height: 28px;
|
|
115
242
|
padding: 0;
|
|
116
|
-
background:
|
|
117
|
-
border: 1px solid #
|
|
118
|
-
border-radius:
|
|
243
|
+
background: var(--rphp-surface-alt, #f4f2ee);
|
|
244
|
+
border: 1px solid var(--rphp-border, #e6e2da);
|
|
245
|
+
border-radius: var(--rphp-radius-sm, 6px);
|
|
119
246
|
cursor: pointer;
|
|
120
|
-
color: #
|
|
121
|
-
transition: all 0.
|
|
247
|
+
color: var(--rphp-text-secondary, #57544d);
|
|
248
|
+
transition: all 0.15s;
|
|
122
249
|
}
|
|
123
250
|
|
|
124
251
|
.TextHighlight__style-type-button:hover {
|
|
125
|
-
border-color: #
|
|
252
|
+
border-color: var(--rphp-accent, #5b50e6);
|
|
253
|
+
color: var(--rphp-accent, #5b50e6);
|
|
126
254
|
}
|
|
127
255
|
|
|
128
256
|
.TextHighlight__style-type-button.active {
|
|
129
|
-
color: #
|
|
130
|
-
border-color: #
|
|
131
|
-
background:
|
|
257
|
+
color: var(--rphp-accent, #5b50e6);
|
|
258
|
+
border-color: var(--rphp-accent, #5b50e6);
|
|
259
|
+
background: var(--rphp-accent-soft, #eceaf9);
|
|
132
260
|
}
|
|
133
261
|
|
|
134
|
-
/* Color
|
|
135
|
-
.TextHighlight__color-
|
|
262
|
+
/* Color dropdown menu - same slot/positioning as the style panel */
|
|
263
|
+
.TextHighlight__color-menu {
|
|
264
|
+
margin-top: 6px;
|
|
265
|
+
width: 180px;
|
|
266
|
+
background: var(--rphp-surface, #ffffff);
|
|
267
|
+
border: 1px solid var(--rphp-border, #e6e2da);
|
|
268
|
+
border-radius: var(--rphp-radius-lg, 12px);
|
|
269
|
+
box-shadow: var(--rphp-shadow-lg, 0 8px 24px -6px rgba(28, 27, 24, 0.22));
|
|
270
|
+
padding: 6px;
|
|
136
271
|
display: flex;
|
|
137
|
-
|
|
138
|
-
gap:
|
|
272
|
+
flex-direction: column;
|
|
273
|
+
gap: 1px;
|
|
139
274
|
}
|
|
140
275
|
|
|
141
|
-
.TextHighlight__color-
|
|
276
|
+
.TextHighlight__color-menu-item {
|
|
142
277
|
display: flex;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
border:
|
|
150
|
-
border-radius: 50%;
|
|
278
|
+
align-items: center;
|
|
279
|
+
gap: 10px;
|
|
280
|
+
width: 100%;
|
|
281
|
+
padding: 7px 8px;
|
|
282
|
+
border: none;
|
|
283
|
+
background: transparent;
|
|
284
|
+
border-radius: var(--rphp-radius-sm, 6px);
|
|
151
285
|
cursor: pointer;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.TextHighlight__color-preset:hover {
|
|
157
|
-
transform: scale(1.15);
|
|
286
|
+
color: var(--rphp-text, #1c1b18);
|
|
287
|
+
font-size: 13px;
|
|
288
|
+
text-align: left;
|
|
158
289
|
}
|
|
159
290
|
|
|
160
|
-
.TextHighlight__color-
|
|
161
|
-
|
|
291
|
+
.TextHighlight__color-menu-item:hover {
|
|
292
|
+
background: var(--rphp-surface-alt, #f4f2ee);
|
|
162
293
|
}
|
|
163
294
|
|
|
164
|
-
.TextHighlight__color-
|
|
165
|
-
width:
|
|
166
|
-
height:
|
|
167
|
-
|
|
168
|
-
border:
|
|
169
|
-
|
|
170
|
-
cursor: pointer;
|
|
171
|
-
background: transparent;
|
|
295
|
+
.TextHighlight__color-menu-dot {
|
|
296
|
+
width: 16px;
|
|
297
|
+
height: 16px;
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
border: 1px solid var(--rphp-border, #e6e2da);
|
|
300
|
+
flex-shrink: 0;
|
|
172
301
|
}
|
|
173
302
|
|
|
174
|
-
.TextHighlight__color-
|
|
175
|
-
|
|
303
|
+
.TextHighlight__color-menu-label {
|
|
304
|
+
flex: 1;
|
|
176
305
|
}
|
|
177
306
|
|
|
178
|
-
.TextHighlight__color-
|
|
179
|
-
|
|
180
|
-
|
|
307
|
+
.TextHighlight__color-menu-check {
|
|
308
|
+
display: flex;
|
|
309
|
+
color: var(--rphp-accent, #5b50e6);
|
|
310
|
+
flex-shrink: 0;
|
|
181
311
|
}
|
|
182
312
|
|
|
183
313
|
/* Underline style */
|
package/dist/esm/style/style.css
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* Design tokens — Verso design system.
|
|
2
|
+
Warm paper neutrals, indigo accent, pastel highlight palette, soft shadows.
|
|
3
|
+
Every default-UI component reads these vars; consumers can retheme the whole
|
|
4
|
+
library by overriding them on .PdfHighlighter (or :root). Dark values are
|
|
5
|
+
provided under .PdfHighlighter--dark. */
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
/* Accent */
|
|
9
|
+
--rphp-accent: #5b50e6;
|
|
10
|
+
--rphp-accent-strong: #4a40d4;
|
|
11
|
+
--rphp-accent-soft: #eceaf9;
|
|
12
|
+
--rphp-accent-tint: #d8d4fa;
|
|
13
|
+
|
|
14
|
+
/* Warm neutral surfaces */
|
|
15
|
+
--rphp-bg: #faf9f6;
|
|
16
|
+
--rphp-surface: #ffffff;
|
|
17
|
+
--rphp-surface-alt: #f4f2ee;
|
|
18
|
+
--rphp-surface-sunken: #efece5;
|
|
19
|
+
--rphp-border: #e6e2da;
|
|
20
|
+
--rphp-border-strong: #b7b3ab;
|
|
21
|
+
|
|
22
|
+
/* Ink */
|
|
23
|
+
--rphp-text: #1c1b18;
|
|
24
|
+
--rphp-text-secondary: #57544d;
|
|
25
|
+
--rphp-text-muted: #8b8880;
|
|
26
|
+
--rphp-text-on-accent: #ffffff;
|
|
27
|
+
|
|
28
|
+
/* Floating toolbar chrome (Text/Area/Note/Image/Drawing/Shape toolbars) —
|
|
29
|
+
always a dark pill with light icons, regardless of page theme. Kept
|
|
30
|
+
separate from --rphp-text (which flips light/dark with the theme) so
|
|
31
|
+
the toolbar doesn't lose contrast against a dark page. */
|
|
32
|
+
--rphp-toolbar-bg: #1c1b18;
|
|
33
|
+
--rphp-toolbar-icon: #efece5;
|
|
34
|
+
|
|
35
|
+
/* Highlight palette (pastels) */
|
|
36
|
+
--rphp-hl-yellow: #ffd666;
|
|
37
|
+
--rphp-hl-blue: #93c5fd;
|
|
38
|
+
--rphp-hl-green: #86efac;
|
|
39
|
+
--rphp-hl-pink: #f9a8d4;
|
|
40
|
+
--rphp-hl-purple: #c4b5fd;
|
|
41
|
+
--rphp-danger: #ef4444;
|
|
42
|
+
|
|
43
|
+
/* Shape */
|
|
44
|
+
--rphp-radius-sm: 6px;
|
|
45
|
+
--rphp-radius: 9px;
|
|
46
|
+
--rphp-radius-lg: 12px;
|
|
47
|
+
--rphp-radius-pill: 999px;
|
|
48
|
+
|
|
49
|
+
/* Elevation */
|
|
50
|
+
--rphp-shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
51
|
+
--rphp-shadow: 0 2px 8px rgba(28, 27, 24, 0.12);
|
|
52
|
+
--rphp-shadow-lg: 0 8px 24px -6px rgba(28, 27, 24, 0.22);
|
|
53
|
+
--rphp-shadow-accent: 0 4px 12px -3px var(--rphp-accent);
|
|
54
|
+
|
|
55
|
+
/* Type */
|
|
56
|
+
--rphp-font:
|
|
57
|
+
"Hanken Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
58
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Dark chrome: same hue relationships on a warm near-black surface. */
|
|
62
|
+
.PdfHighlighter--dark,
|
|
63
|
+
.rphp-dark {
|
|
64
|
+
--rphp-accent: #7b71f0;
|
|
65
|
+
--rphp-accent-strong: #8f86f4;
|
|
66
|
+
--rphp-accent-soft: #2c2a3f;
|
|
67
|
+
--rphp-accent-tint: #3b3760;
|
|
68
|
+
|
|
69
|
+
--rphp-bg: #141210;
|
|
70
|
+
--rphp-surface: #211f1c;
|
|
71
|
+
--rphp-surface-alt: #2a2724;
|
|
72
|
+
--rphp-surface-sunken: #191714;
|
|
73
|
+
--rphp-border: #3a3733;
|
|
74
|
+
--rphp-border-strong: #57544d;
|
|
75
|
+
|
|
76
|
+
--rphp-text: #eae6e0;
|
|
77
|
+
--rphp-text-secondary: #b7b3ab;
|
|
78
|
+
--rphp-text-muted: #8b8880;
|
|
79
|
+
|
|
80
|
+
/* A touch lighter than the near-black dark-mode page so the toolbar pill
|
|
81
|
+
still reads as an elevated surface instead of vanishing into the page. */
|
|
82
|
+
--rphp-toolbar-bg: #38352f;
|
|
83
|
+
--rphp-toolbar-icon: #efece5;
|
|
84
|
+
|
|
85
|
+
--rphp-shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.4);
|
|
86
|
+
--rphp-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
|
87
|
+
--rphp-shadow-lg: 0 8px 24px -6px rgba(0, 0, 0, 0.65);
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-pdf-highlighter-plus",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.1",
|
|
5
|
+
"sideEffects": [
|
|
6
|
+
"*.css"
|
|
7
|
+
],
|
|
5
8
|
"description": "Set of modern React components for PDF highlighting",
|
|
6
9
|
"author": "Edward Ha <quocvietha08@gmail.com>",
|
|
7
10
|
"license": "MIT",
|
|
@@ -55,14 +58,37 @@
|
|
|
55
58
|
"build:copy-styles": "cp -r ./src/style ./dist/esm",
|
|
56
59
|
"build:example": "(cd ./example && npm install && tsc && vite build && mkdir -p \"../public/\" && cp -r example-app ../public/)",
|
|
57
60
|
"build:docs": "npx typedoc",
|
|
61
|
+
"test:size": "size-limit",
|
|
58
62
|
"clean": "rm -rf dist public node_modules package-lock.json"
|
|
59
63
|
},
|
|
64
|
+
"size-limit": [
|
|
65
|
+
{
|
|
66
|
+
"name": "core (import * — no exportPdf chunk)",
|
|
67
|
+
"path": "dist/esm/index.js",
|
|
68
|
+
"limit": "220 kB",
|
|
69
|
+
"ignore": [
|
|
70
|
+
"react",
|
|
71
|
+
"react-dom",
|
|
72
|
+
"pdfjs-dist"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
],
|
|
60
76
|
"peerDependencies": {
|
|
61
77
|
"pdfjs-dist": "^4.4.168",
|
|
62
78
|
"react": "^18.3.1",
|
|
63
79
|
"react-dom": "^18.3.1"
|
|
64
80
|
},
|
|
65
81
|
"dependencies": {
|
|
82
|
+
"@tanstack/react-virtual": "^3.13.14",
|
|
83
|
+
"lucide-react": "^0.559.0",
|
|
84
|
+
"pdf-lib": "^1.17.1",
|
|
85
|
+
"react-rnd": "10.5.2"
|
|
86
|
+
},
|
|
87
|
+
"repository": {
|
|
88
|
+
"type": "git",
|
|
89
|
+
"url": "git+https://github.com/QuocVietHa08/react-pdf-highlighter-plus"
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
66
92
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
67
93
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
68
94
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
@@ -74,27 +100,17 @@
|
|
|
74
100
|
"@radix-ui/react-slot": "^1.2.4",
|
|
75
101
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
76
102
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
77
|
-
"@
|
|
78
|
-
"class-variance-authority": "^0.7.1",
|
|
79
|
-
"clsx": "^2.1.1",
|
|
80
|
-
"lodash.debounce": "^4.0.8",
|
|
81
|
-
"lucide-react": "^0.559.0",
|
|
82
|
-
"pdf-lib": "^1.17.1",
|
|
83
|
-
"react-rnd": "^10.4.11",
|
|
84
|
-
"tailwind-merge": "^3.4.0"
|
|
85
|
-
},
|
|
86
|
-
"repository": {
|
|
87
|
-
"type": "git",
|
|
88
|
-
"url": "git+https://github.com/QuocVietHa08/react-pdf-highlighter-plus"
|
|
89
|
-
},
|
|
90
|
-
"devDependencies": {
|
|
91
|
-
"@types/lodash.debounce": "^4.0.9",
|
|
103
|
+
"@size-limit/preset-small-lib": "^11.1.6",
|
|
92
104
|
"@types/node": "^20.14.9",
|
|
93
105
|
"@types/react": "^18.3.3",
|
|
94
106
|
"@types/react-dom": "^18.3.0",
|
|
95
107
|
"@vitejs/plugin-react": "^4.3.1",
|
|
96
108
|
"autoprefixer": "^10.4.22",
|
|
109
|
+
"class-variance-authority": "^0.7.1",
|
|
110
|
+
"clsx": "^2.1.1",
|
|
97
111
|
"postcss": "^8.5.6",
|
|
112
|
+
"size-limit": "^11.1.6",
|
|
113
|
+
"tailwind-merge": "^3.4.0",
|
|
98
114
|
"tailwindcss": "^3.4.0",
|
|
99
115
|
"tsup": "^8.5.1",
|
|
100
116
|
"typedoc": "latest",
|
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|