hazo_pdf 1.0.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/dist/index.js ADDED
@@ -0,0 +1,2815 @@
1
+ "use client";
2
+ import {
3
+ __require,
4
+ default_config,
5
+ download_pdf,
6
+ save_and_download_pdf,
7
+ save_annotations_to_pdf
8
+ } from "./chunk-2POHPGR3.js";
9
+
10
+ // src/components/pdf_viewer/pdf_viewer.tsx
11
+ import { useState as useState5, useEffect as useEffect5, useRef as useRef6, useCallback as useCallback2 } from "react";
12
+ import { Save, Undo2 as Undo22, Redo2 } from "lucide-react";
13
+
14
+ // src/components/pdf_viewer/pdf_worker_setup.ts
15
+ var worker_configured = false;
16
+ async function configure_worker() {
17
+ if (typeof window === "undefined" || typeof document === "undefined") {
18
+ return;
19
+ }
20
+ if (worker_configured) {
21
+ return;
22
+ }
23
+ try {
24
+ const pdfjs_module = await (async () => {
25
+ if (typeof window === "undefined") {
26
+ throw new Error("Cannot configure PDF.js worker in non-browser environment");
27
+ }
28
+ return await import("pdfjs-dist");
29
+ })();
30
+ const { GlobalWorkerOptions, version } = pdfjs_module;
31
+ const pdfjsVersion = version;
32
+ GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjsVersion}/build/pdf.worker.min.mjs`;
33
+ console.log(`[PDF Worker] Worker configured: ${GlobalWorkerOptions.workerSrc}`);
34
+ worker_configured = true;
35
+ } catch (error) {
36
+ if (typeof window !== "undefined") {
37
+ console.error("[PDF Worker] Error configuring worker:", error);
38
+ throw error;
39
+ }
40
+ }
41
+ }
42
+ async function load_pdf_document(source) {
43
+ if (typeof window === "undefined" || typeof document === "undefined") {
44
+ throw new Error("load_pdf_document can only be called in the browser");
45
+ }
46
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
47
+ throw new Error("load_pdf_document cannot be called in Node.js environment");
48
+ }
49
+ await configure_worker();
50
+ const pdfjs_module = await import("pdfjs-dist");
51
+ const { getDocument } = pdfjs_module;
52
+ console.log("[PDF Loader] Loading PDF from:", typeof source === "string" ? source : "ArrayBuffer/Uint8Array");
53
+ try {
54
+ let pdfData = source;
55
+ if (typeof source === "string") {
56
+ if (source.startsWith("/") || source.startsWith("./") || !source.includes("://")) {
57
+ console.log("[PDF Loader] Fetching local PDF file as ArrayBuffer:", source);
58
+ try {
59
+ const response = await fetch(source);
60
+ if (!response.ok) {
61
+ throw new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`);
62
+ }
63
+ pdfData = await response.arrayBuffer();
64
+ console.log("[PDF Loader] PDF fetched successfully, size:", pdfData.byteLength, "bytes");
65
+ } catch (fetchError) {
66
+ console.error("[PDF Loader] Error fetching PDF as ArrayBuffer, trying URL directly:", fetchError);
67
+ pdfData = source;
68
+ }
69
+ }
70
+ }
71
+ const loading_task = getDocument({
72
+ url: typeof pdfData === "string" ? pdfData : void 0,
73
+ data: typeof pdfData !== "string" ? pdfData : void 0,
74
+ verbosity: 0,
75
+ // Suppress console warnings
76
+ // Add CORS support
77
+ httpHeaders: {},
78
+ withCredentials: false,
79
+ // Enable range requests for better performance
80
+ rangeChunkSize: 65536
81
+ });
82
+ const document2 = await loading_task.promise;
83
+ console.log("[PDF Loader] PDF loaded successfully:", document2.numPages, "pages");
84
+ return document2;
85
+ } catch (error) {
86
+ console.error("[PDF Loader] Error loading PDF:", error);
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ // src/components/pdf_viewer/pdf_viewer_layout.tsx
92
+ import { useState as useState2, useRef as useRef3, useEffect as useEffect2, useCallback } from "react";
93
+
94
+ // src/components/pdf_viewer/pdf_page_renderer.tsx
95
+ import { useRef, useEffect, useMemo } from "react";
96
+
97
+ // src/utils/coordinate_mapper.ts
98
+ function create_coordinate_mapper(page, scale) {
99
+ const viewport = page.getViewport({ scale });
100
+ return {
101
+ /**
102
+ * Convert screen (CSS pixel) coordinates to PDF (abstract) coordinates
103
+ * @param x_screen - X coordinate in screen space
104
+ * @param y_screen - Y coordinate in screen space
105
+ * @returns [x_pdf, y_pdf] coordinates in PDF space
106
+ */
107
+ to_pdf: (x_screen, y_screen) => {
108
+ const result = viewport.convertToPdfPoint(x_screen, y_screen);
109
+ return [result[0], result[1]];
110
+ },
111
+ /**
112
+ * Convert PDF (abstract) coordinates to Screen (CSS pixel) coordinates
113
+ * @param x_pdf - X coordinate in PDF space
114
+ * @param y_pdf - Y coordinate in PDF space
115
+ * @returns [x_screen, y_screen] coordinates in screen space
116
+ */
117
+ to_screen: (x_pdf, y_pdf) => {
118
+ const result = viewport.convertToViewportPoint(x_pdf, y_pdf);
119
+ return [result[0], result[1]];
120
+ }
121
+ };
122
+ }
123
+ function get_viewport_dimensions(page, scale) {
124
+ const viewport = page.getViewport({ scale });
125
+ return {
126
+ width: viewport.width,
127
+ height: viewport.height
128
+ };
129
+ }
130
+
131
+ // src/utils/cn.ts
132
+ import { clsx } from "clsx";
133
+ import { twMerge } from "tailwind-merge";
134
+ function cn(...inputs) {
135
+ return twMerge(clsx(inputs));
136
+ }
137
+
138
+ // src/components/pdf_viewer/pdf_page_renderer.tsx
139
+ import { jsx } from "react/jsx-runtime";
140
+ var PdfPageRenderer = ({
141
+ page,
142
+ page_index,
143
+ scale,
144
+ className = "",
145
+ on_coordinate_mapper_ready,
146
+ config = null
147
+ }) => {
148
+ const canvas_ref = useRef(null);
149
+ const render_task_ref = useRef(null);
150
+ const notified_dimensions_ref = useRef(null);
151
+ const callback_ref = useRef(on_coordinate_mapper_ready);
152
+ useEffect(() => {
153
+ callback_ref.current = on_coordinate_mapper_ready;
154
+ }, [on_coordinate_mapper_ready]);
155
+ const viewport_dimensions = useMemo(() => {
156
+ if (!page) return { width: 0, height: 0 };
157
+ const viewport = page.getViewport({ scale });
158
+ return {
159
+ width: viewport.width,
160
+ height: viewport.height
161
+ };
162
+ }, [page, scale]);
163
+ const coordinate_mapper = useMemo(() => {
164
+ if (!page) return null;
165
+ return create_coordinate_mapper(page, scale);
166
+ }, [page, scale]);
167
+ useEffect(() => {
168
+ if (!page || !coordinate_mapper) return;
169
+ const current_dimensions = viewport_dimensions;
170
+ const last_notified = notified_dimensions_ref.current;
171
+ if (!last_notified || last_notified.width !== current_dimensions.width || last_notified.height !== current_dimensions.height || last_notified.scale !== scale) {
172
+ if (callback_ref.current) {
173
+ callback_ref.current(coordinate_mapper, current_dimensions);
174
+ notified_dimensions_ref.current = { ...current_dimensions, scale };
175
+ }
176
+ }
177
+ }, [page, coordinate_mapper, viewport_dimensions.width, viewport_dimensions.height, scale]);
178
+ useEffect(() => {
179
+ if (typeof window === "undefined") {
180
+ return;
181
+ }
182
+ if (!page) {
183
+ return;
184
+ }
185
+ if (!canvas_ref.current) {
186
+ return;
187
+ }
188
+ let cancelled = false;
189
+ const cancel_previous = async () => {
190
+ if (render_task_ref.current) {
191
+ try {
192
+ render_task_ref.current.cancel();
193
+ await render_task_ref.current.promise.catch(() => {
194
+ });
195
+ } catch (error) {
196
+ }
197
+ render_task_ref.current = null;
198
+ }
199
+ };
200
+ cancel_previous().then(() => {
201
+ if (cancelled || !canvas_ref.current) {
202
+ return;
203
+ }
204
+ const viewport = page.getViewport({ scale });
205
+ const canvas = canvas_ref.current;
206
+ const context = canvas.getContext("2d", { alpha: false });
207
+ if (!context) {
208
+ console.error(`[PdfPageRenderer] Page ${page_index}: Cannot get 2D context`);
209
+ return;
210
+ }
211
+ const output_scale = window.devicePixelRatio || 1;
212
+ canvas.width = viewport.width * output_scale;
213
+ canvas.height = viewport.height * output_scale;
214
+ canvas.style.width = `${viewport.width}px`;
215
+ canvas.style.height = `${viewport.height}px`;
216
+ context.scale(output_scale, output_scale);
217
+ const render_context = {
218
+ canvasContext: context,
219
+ viewport
220
+ };
221
+ const render_task = page.render(render_context);
222
+ render_task_ref.current = render_task;
223
+ render_task.promise.then(() => {
224
+ if (render_task_ref.current === render_task) {
225
+ render_task_ref.current = null;
226
+ }
227
+ }).catch((error) => {
228
+ if (error.name !== "RenderingCancelledException") {
229
+ console.error(`[PdfPageRenderer] Page ${page_index}: Error rendering:`, error);
230
+ }
231
+ if (render_task_ref.current === render_task) {
232
+ render_task_ref.current = null;
233
+ }
234
+ });
235
+ });
236
+ return () => {
237
+ cancelled = true;
238
+ if (render_task_ref.current) {
239
+ try {
240
+ render_task_ref.current.cancel();
241
+ } catch (error) {
242
+ }
243
+ render_task_ref.current = null;
244
+ }
245
+ };
246
+ }, [page, scale, page_index]);
247
+ if (!page) {
248
+ return /* @__PURE__ */ jsx("div", { className: cn("cls_pdf_page_loading", className), children: /* @__PURE__ */ jsx("div", { className: "cls_pdf_page_spinner", children: "Loading page..." }) });
249
+ }
250
+ const page_config = config?.page_styling || default_config.page_styling;
251
+ return /* @__PURE__ */ jsx(
252
+ "div",
253
+ {
254
+ className: cn("cls_pdf_page_container", className),
255
+ style: {
256
+ position: "relative",
257
+ width: viewport_dimensions.width,
258
+ height: viewport_dimensions.height,
259
+ margin: "0 auto",
260
+ border: `1px solid ${page_config.page_border_color}`,
261
+ boxShadow: page_config.page_box_shadow,
262
+ backgroundColor: page_config.page_background_color,
263
+ // Inherit cursor from parent (grab/grabbing in pan mode)
264
+ cursor: "inherit"
265
+ },
266
+ children: /* @__PURE__ */ jsx(
267
+ "canvas",
268
+ {
269
+ ref: canvas_ref,
270
+ className: "cls_pdf_page_canvas",
271
+ style: {
272
+ display: "block",
273
+ width: "100%",
274
+ height: "100%",
275
+ pointerEvents: "none"
276
+ // Allow events to pass through to SVG overlay
277
+ },
278
+ "aria-label": `PDF page ${page_index + 1}`
279
+ }
280
+ )
281
+ }
282
+ );
283
+ };
284
+
285
+ // src/components/pdf_viewer/annotation_overlay.tsx
286
+ import { useState, useRef as useRef2 } from "react";
287
+
288
+ // src/utils/annotation_utils.ts
289
+ function calculate_rectangle_coords(p1, p2) {
290
+ const x = Math.min(p1.x, p2.x);
291
+ const y = Math.min(p1.y, p2.y);
292
+ const width = Math.abs(p1.x - p2.x);
293
+ const height = Math.abs(p1.y - p2.y);
294
+ return { x, y, width, height };
295
+ }
296
+ function rectangle_to_pdf_rect(rect) {
297
+ return [
298
+ rect.x,
299
+ rect.y,
300
+ rect.x + rect.width,
301
+ rect.y + rect.height
302
+ ];
303
+ }
304
+ function pdf_rect_to_rectangle(pdf_rect) {
305
+ const [x1, y1, x2, y2] = pdf_rect;
306
+ return {
307
+ x: Math.min(x1, x2),
308
+ y: Math.min(y1, y2),
309
+ width: Math.abs(x2 - x1),
310
+ height: Math.abs(y2 - y1)
311
+ };
312
+ }
313
+ function is_rectangle_too_small(rect, min_size = 5) {
314
+ return rect.width < min_size || rect.height < min_size;
315
+ }
316
+
317
+ // src/components/pdf_viewer/annotation_overlay.tsx
318
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
319
+ var TempDrawBox = ({ start, current, tool_type = "Square", config = null }) => {
320
+ if (!start || !current) return null;
321
+ const { x, y, width, height } = calculate_rectangle_coords(start, current);
322
+ const highlight_config = config?.highlight_annotation || default_config.highlight_annotation;
323
+ const square_config = config?.square_annotation || default_config.square_annotation;
324
+ const hex_to_rgba = (hex, opacity) => {
325
+ const r = parseInt(hex.slice(1, 3), 16);
326
+ const g = parseInt(hex.slice(3, 5), 16);
327
+ const b = parseInt(hex.slice(5, 7), 16);
328
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
329
+ };
330
+ const get_fill_props = () => {
331
+ switch (tool_type) {
332
+ case "Highlight":
333
+ return {
334
+ fill: hex_to_rgba(highlight_config.highlight_fill_color, highlight_config.highlight_fill_opacity),
335
+ stroke: highlight_config.highlight_border_color
336
+ };
337
+ case "Square":
338
+ return {
339
+ fill: hex_to_rgba(square_config.square_fill_color, square_config.square_fill_opacity),
340
+ stroke: square_config.square_border_color
341
+ };
342
+ default:
343
+ return {
344
+ fill: "rgba(0, 0, 255, 0.2)",
345
+ stroke: "#0000FF"
346
+ };
347
+ }
348
+ };
349
+ const { fill, stroke } = get_fill_props();
350
+ return /* @__PURE__ */ jsx2(
351
+ "rect",
352
+ {
353
+ x,
354
+ y,
355
+ width,
356
+ height,
357
+ stroke,
358
+ strokeWidth: "2",
359
+ fill,
360
+ pointerEvents: "none"
361
+ }
362
+ );
363
+ };
364
+ var AnnotationOverlay = ({
365
+ width,
366
+ height,
367
+ page_index,
368
+ map_coords,
369
+ annotations = [],
370
+ current_tool = "Square",
371
+ on_annotation_create,
372
+ on_context_menu,
373
+ on_annotation_click,
374
+ config = null,
375
+ className = ""
376
+ }) => {
377
+ const [is_drawing, setIsDrawing] = useState(false);
378
+ const [start_point, setStartPoint] = useState(null);
379
+ const [current_point, setCurrentPoint] = useState(null);
380
+ const [hovered_annotation_id, setHoveredAnnotationId] = useState(null);
381
+ const svg_ref = useRef2(null);
382
+ const log_annotation_click = (annotation, origin, point) => {
383
+ console.log(
384
+ `\u{1F7E2} [AnnotationClick] origin=${origin}, id=${annotation.id}, page=${annotation.page_index}, screen=(${point.x.toFixed(
385
+ 1
386
+ )}, ${point.y.toFixed(1)})`
387
+ );
388
+ };
389
+ const page_annotations = annotations.filter(
390
+ (ann) => ann.page_index === page_index
391
+ );
392
+ const is_point_in_annotation = (point, annotation) => {
393
+ const [screen_x1, screen_y1] = map_coords.to_screen(
394
+ annotation.rect[0],
395
+ annotation.rect[1]
396
+ );
397
+ const [screen_x2, screen_y2] = map_coords.to_screen(
398
+ annotation.rect[2],
399
+ annotation.rect[3]
400
+ );
401
+ if (annotation.type === "FreeText") {
402
+ const fonts_config = config?.fonts || default_config.fonts;
403
+ const freetext_config = config?.freetext_annotation || default_config.freetext_annotation;
404
+ const text = annotation.contents || "";
405
+ if (!text) return false;
406
+ const font_size = fonts_config.freetext_font_size_default;
407
+ const padding_h = freetext_config.freetext_padding_horizontal;
408
+ const padding_v = freetext_config.freetext_padding_vertical;
409
+ const text_width_estimate = font_size * 0.6 * text.length;
410
+ const box_width = text_width_estimate + padding_h * 2;
411
+ const box_height = font_size + padding_v * 2;
412
+ const box_x = screen_x1;
413
+ const box_y = screen_y1;
414
+ return point.x >= box_x && point.x <= box_x + box_width && point.y >= box_y && point.y <= box_y + box_height;
415
+ }
416
+ const screen_x = Math.min(screen_x1, screen_x2);
417
+ const screen_y = Math.min(screen_y1, screen_y2);
418
+ const screen_width = Math.abs(screen_x2 - screen_x1);
419
+ const screen_height = Math.abs(screen_y2 - screen_y1);
420
+ return point.x >= screen_x && point.x <= screen_x + screen_width && point.y >= screen_y && point.y <= screen_y + screen_height;
421
+ };
422
+ const handle_mouse_down = (e) => {
423
+ if (e.button !== 0) return;
424
+ const rect = svg_ref.current?.getBoundingClientRect();
425
+ if (!rect) return;
426
+ const point = {
427
+ x: e.clientX - rect.left,
428
+ y: e.clientY - rect.top
429
+ };
430
+ if (e.button === 0 && on_annotation_click) {
431
+ for (const annotation of page_annotations) {
432
+ if (is_point_in_annotation(point, annotation)) {
433
+ e.nativeEvent.__annotation_clicked = annotation.id;
434
+ e.nativeEvent.__annotation_click_source = "svg_hit_test";
435
+ e.preventDefault();
436
+ e.stopPropagation();
437
+ e.nativeEvent.stopImmediatePropagation();
438
+ log_annotation_click(annotation, "svg_hit_test", point);
439
+ on_annotation_click(annotation, point.x, point.y);
440
+ return;
441
+ }
442
+ }
443
+ } else if (e.button !== 0) {
444
+ return;
445
+ } else if (!on_annotation_click) {
446
+ console.warn(`\u26A0\uFE0F [AnnotationOverlay] on_annotation_click not provided`);
447
+ }
448
+ if (!current_tool) {
449
+ return;
450
+ }
451
+ setIsDrawing(true);
452
+ setStartPoint(point);
453
+ setCurrentPoint(point);
454
+ };
455
+ const handle_mouse_move = (e) => {
456
+ if (!is_drawing || !start_point) return;
457
+ const rect = svg_ref.current?.getBoundingClientRect();
458
+ if (!rect) return;
459
+ const point = {
460
+ x: e.clientX - rect.left,
461
+ y: e.clientY - rect.top
462
+ };
463
+ setCurrentPoint(point);
464
+ };
465
+ const handle_mouse_up = (_e) => {
466
+ if (!is_drawing || !start_point || !current_point) return;
467
+ setIsDrawing(false);
468
+ const screen_rect = calculate_rectangle_coords(start_point, current_point);
469
+ if (is_rectangle_too_small(screen_rect, 5)) {
470
+ setStartPoint(null);
471
+ setCurrentPoint(null);
472
+ return;
473
+ }
474
+ const [pdf_x1, pdf_y1] = map_coords.to_pdf(screen_rect.x, screen_rect.y);
475
+ const [pdf_x2, pdf_y2] = map_coords.to_pdf(
476
+ screen_rect.x + screen_rect.width,
477
+ screen_rect.y + screen_rect.height
478
+ );
479
+ const pdf_rect = [
480
+ Math.min(pdf_x1, pdf_x2),
481
+ Math.min(pdf_y1, pdf_y2),
482
+ Math.max(pdf_x1, pdf_x2),
483
+ Math.max(pdf_y1, pdf_y2)
484
+ ];
485
+ const highlight_config = config?.highlight_annotation || default_config.highlight_annotation;
486
+ const square_config = config?.square_annotation || default_config.square_annotation;
487
+ const new_annotation = {
488
+ id: crypto.randomUUID(),
489
+ type: current_tool || "Square",
490
+ page_index,
491
+ rect: pdf_rect,
492
+ author: "User",
493
+ date: (/* @__PURE__ */ new Date()).toISOString(),
494
+ contents: "",
495
+ color: current_tool === "Highlight" ? highlight_config.highlight_fill_color : square_config.square_fill_color
496
+ };
497
+ if (on_annotation_create) {
498
+ on_annotation_create(new_annotation);
499
+ }
500
+ setStartPoint(null);
501
+ setCurrentPoint(null);
502
+ };
503
+ const handle_mouse_leave = () => {
504
+ if (is_drawing) {
505
+ setIsDrawing(false);
506
+ setStartPoint(null);
507
+ setCurrentPoint(null);
508
+ }
509
+ };
510
+ const handle_context_menu = (e) => {
511
+ e.preventDefault();
512
+ e.stopPropagation();
513
+ const rect = svg_ref.current?.getBoundingClientRect();
514
+ if (!rect) {
515
+ console.warn("[AnnotationOverlay] Could not get SVG bounding rect");
516
+ return;
517
+ }
518
+ if (!on_context_menu) {
519
+ console.warn("[AnnotationOverlay] on_context_menu callback not provided");
520
+ return;
521
+ }
522
+ const screen_x = e.clientX - rect.left;
523
+ const screen_y = e.clientY - rect.top;
524
+ on_context_menu(e, screen_x, screen_y);
525
+ };
526
+ const render_annotation = (annotation) => {
527
+ const [screen_x1, screen_y1] = map_coords.to_screen(
528
+ annotation.rect[0],
529
+ // Top-left X in PDF space
530
+ annotation.rect[1]
531
+ // Top-left Y in PDF space
532
+ );
533
+ const [screen_x2, screen_y2] = map_coords.to_screen(
534
+ annotation.rect[2],
535
+ // Bottom-right X in PDF space
536
+ annotation.rect[3]
537
+ // Bottom-right Y in PDF space
538
+ );
539
+ const screen_x = Math.min(screen_x1, screen_x2);
540
+ const screen_y = Math.min(screen_y1, screen_y2);
541
+ const screen_width = Math.abs(screen_x2 - screen_x1);
542
+ const screen_height = Math.abs(screen_y2 - screen_y1);
543
+ const fonts_config = config?.fonts || default_config.fonts;
544
+ const highlight_config = config?.highlight_annotation || default_config.highlight_annotation;
545
+ const square_config = config?.square_annotation || default_config.square_annotation;
546
+ const freetext_config = config?.freetext_annotation || default_config.freetext_annotation;
547
+ const hex_to_rgba = (hex, opacity) => {
548
+ const r = parseInt(hex.slice(1, 3), 16);
549
+ const g = parseInt(hex.slice(3, 5), 16);
550
+ const b = parseInt(hex.slice(5, 7), 16);
551
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
552
+ };
553
+ if (annotation.type === "FreeText") {
554
+ const text = annotation.contents || "";
555
+ if (!text) return null;
556
+ let stamp_styling = null;
557
+ if (annotation.subject) {
558
+ try {
559
+ const parsed = JSON.parse(annotation.subject);
560
+ if (parsed && parsed.stamp_name) {
561
+ stamp_styling = parsed;
562
+ }
563
+ } catch (e) {
564
+ }
565
+ }
566
+ const font_size = stamp_styling?.font_size !== void 0 ? stamp_styling.font_size : fonts_config.freetext_font_size_default;
567
+ const text_color = stamp_styling?.font_color || annotation.color || (freetext_config.freetext_text_color && freetext_config.freetext_text_color !== "#000000" ? freetext_config.freetext_text_color : fonts_config.font_foreground_color) || "#000000";
568
+ const font_family = stamp_styling?.font_name || fonts_config.freetext_font_family;
569
+ const font_weight = stamp_styling?.font_weight !== void 0 ? stamp_styling.font_weight : freetext_config.freetext_font_weight;
570
+ const font_style = stamp_styling?.font_style !== void 0 ? stamp_styling.font_style : freetext_config.freetext_font_style;
571
+ const padding_h = freetext_config.freetext_padding_horizontal;
572
+ const padding_v = freetext_config.freetext_padding_vertical;
573
+ const text_lines = text.split("\n");
574
+ const max_line_length = Math.max(...text_lines.map((line) => line.length), 1);
575
+ const text_width_estimate = font_size * 0.6 * max_line_length;
576
+ const box_width = text_width_estimate + padding_h * 2;
577
+ const box_height = font_size * text_lines.length + padding_v * 2;
578
+ const box_x = screen_x1;
579
+ const box_y = screen_y1;
580
+ const text_x = box_x + padding_h;
581
+ const text_y = box_y + padding_v + font_size;
582
+ const border_size = stamp_styling?.border_size !== void 0 ? stamp_styling.border_size : freetext_config.freetext_border_width;
583
+ const border_color_trimmed = freetext_config.freetext_border_color?.trim() || "";
584
+ const has_border = border_size > 0 && border_color_trimmed !== "";
585
+ const background_color_trimmed = (stamp_styling?.background_color !== void 0 ? String(stamp_styling.background_color) : freetext_config.freetext_background_color)?.trim() || "";
586
+ const has_background = background_color_trimmed !== "";
587
+ const hex_to_rgba_bg = (color_str, opacity) => {
588
+ if (!color_str || color_str === "") return "transparent";
589
+ const trimmed = color_str.trim();
590
+ const rgb_pattern = /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i;
591
+ const rgb_match = trimmed.match(rgb_pattern);
592
+ if (rgb_match) {
593
+ const r = parseInt(rgb_match[1], 10);
594
+ const g = parseInt(rgb_match[2], 10);
595
+ const b = parseInt(rgb_match[3], 10);
596
+ if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
597
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
598
+ }
599
+ console.warn(`[FreeText] Invalid RGB values: r=${r}, g=${g}, b=${b} (must be 0-255)`);
600
+ return "transparent";
601
+ }
602
+ if (trimmed.startsWith("#")) {
603
+ const hex = trimmed.slice(1).trim();
604
+ if (hex.length === 6 && /^[0-9A-Fa-f]{6}$/.test(hex)) {
605
+ const r = parseInt(hex.slice(0, 2), 16);
606
+ const g = parseInt(hex.slice(2, 4), 16);
607
+ const b = parseInt(hex.slice(4, 6), 16);
608
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
609
+ }
610
+ console.warn(`[FreeText] Invalid hex color format: ${trimmed}`);
611
+ return "transparent";
612
+ }
613
+ console.warn(`[FreeText] Unrecognized color format: ${trimmed}`);
614
+ return "transparent";
615
+ };
616
+ return /* @__PURE__ */ jsxs("g", { children: [
617
+ has_background && /* @__PURE__ */ jsx2(
618
+ "rect",
619
+ {
620
+ x: box_x,
621
+ y: box_y,
622
+ width: box_width,
623
+ height: box_height,
624
+ fill: hex_to_rgba_bg(background_color_trimmed, freetext_config.freetext_background_opacity),
625
+ pointerEvents: "none"
626
+ }
627
+ ),
628
+ has_border && /* @__PURE__ */ jsx2(
629
+ "rect",
630
+ {
631
+ x: box_x,
632
+ y: box_y,
633
+ width: box_width,
634
+ height: box_height,
635
+ fill: "none",
636
+ stroke: border_color_trimmed,
637
+ strokeWidth: border_size,
638
+ pointerEvents: "none"
639
+ }
640
+ ),
641
+ /* @__PURE__ */ jsx2(
642
+ "rect",
643
+ {
644
+ x: box_x,
645
+ y: box_y,
646
+ width: box_width,
647
+ height: box_height,
648
+ fill: "transparent",
649
+ stroke: "none",
650
+ pointerEvents: "auto",
651
+ style: { cursor: "pointer" },
652
+ onMouseEnter: () => {
653
+ setHoveredAnnotationId(annotation.id);
654
+ if (svg_ref.current) {
655
+ svg_ref.current.style.cursor = "pointer";
656
+ }
657
+ },
658
+ onMouseLeave: () => {
659
+ setHoveredAnnotationId(null);
660
+ if (svg_ref.current) {
661
+ if (current_tool === null) {
662
+ svg_ref.current.style.cursor = "inherit";
663
+ } else {
664
+ svg_ref.current.style.cursor = current_tool ? "crosshair" : "default";
665
+ }
666
+ }
667
+ },
668
+ onMouseDown: (e) => {
669
+ if (e.button !== 0) return;
670
+ e.nativeEvent.__annotation_clicked = annotation.id;
671
+ e.nativeEvent.__annotation_click_source = "freetext_rect";
672
+ e.stopPropagation();
673
+ e.nativeEvent.stopImmediatePropagation();
674
+ if (on_annotation_click) {
675
+ const rect = svg_ref.current?.getBoundingClientRect();
676
+ if (!rect) return;
677
+ const click_x = e.clientX - rect.left;
678
+ const click_y = e.clientY - rect.top;
679
+ log_annotation_click(annotation, "freetext_rect", { x: click_x, y: click_y });
680
+ on_annotation_click(annotation, click_x, click_y);
681
+ }
682
+ },
683
+ onClick: (e) => {
684
+ e.preventDefault();
685
+ e.stopPropagation();
686
+ }
687
+ }
688
+ ),
689
+ /* @__PURE__ */ jsx2(
690
+ "text",
691
+ {
692
+ x: text_x,
693
+ y: text_y,
694
+ fontSize: font_size,
695
+ fill: text_color,
696
+ pointerEvents: "none",
697
+ style: {
698
+ fontFamily: font_family,
699
+ fontWeight: font_weight,
700
+ fontStyle: font_style,
701
+ textDecoration: freetext_config.freetext_text_decoration,
702
+ userSelect: "none"
703
+ },
704
+ children: text.split("\n").map((line, line_index) => /* @__PURE__ */ jsx2(
705
+ "tspan",
706
+ {
707
+ x: text_x,
708
+ dy: line_index === 0 ? 0 : font_size,
709
+ children: line
710
+ },
711
+ line_index
712
+ ))
713
+ }
714
+ )
715
+ ] }, annotation.id);
716
+ }
717
+ const get_annotation_props = () => {
718
+ switch (annotation.type) {
719
+ case "Highlight":
720
+ const highlight_color = annotation.color || highlight_config.highlight_fill_color;
721
+ return {
722
+ fill: hex_to_rgba(highlight_color, highlight_config.highlight_fill_opacity),
723
+ stroke: highlight_config.highlight_border_color
724
+ };
725
+ case "Square":
726
+ const square_color = annotation.color || square_config.square_fill_color;
727
+ return {
728
+ fill: hex_to_rgba(square_color, square_config.square_fill_opacity),
729
+ stroke: square_config.square_border_color
730
+ };
731
+ default:
732
+ return {
733
+ fill: "rgba(0, 0, 255, 0.2)",
734
+ stroke: "#0000FF"
735
+ };
736
+ }
737
+ };
738
+ const { fill, stroke } = get_annotation_props();
739
+ return /* @__PURE__ */ jsxs("g", { children: [
740
+ /* @__PURE__ */ jsx2(
741
+ "rect",
742
+ {
743
+ x: screen_x,
744
+ y: screen_y,
745
+ width: screen_width,
746
+ height: screen_height,
747
+ fill: "transparent",
748
+ stroke: "none",
749
+ pointerEvents: "auto",
750
+ style: { cursor: "pointer" },
751
+ onMouseEnter: () => {
752
+ setHoveredAnnotationId(annotation.id);
753
+ if (svg_ref.current) {
754
+ svg_ref.current.style.cursor = "pointer";
755
+ }
756
+ },
757
+ onMouseLeave: () => {
758
+ setHoveredAnnotationId(null);
759
+ if (svg_ref.current) {
760
+ if (current_tool === null) {
761
+ svg_ref.current.style.cursor = "inherit";
762
+ } else {
763
+ svg_ref.current.style.cursor = current_tool ? "crosshair" : "default";
764
+ }
765
+ }
766
+ },
767
+ onMouseDown: (e) => {
768
+ if (e.button !== 0) return;
769
+ e.nativeEvent.__annotation_clicked = annotation.id;
770
+ e.nativeEvent.__annotation_click_source = "rect_overlay";
771
+ e.stopPropagation();
772
+ e.nativeEvent.stopImmediatePropagation();
773
+ if (on_annotation_click) {
774
+ const rect = svg_ref.current?.getBoundingClientRect();
775
+ if (!rect) return;
776
+ const click_x = e.clientX - rect.left;
777
+ const click_y = e.clientY - rect.top;
778
+ log_annotation_click(annotation, "rect_overlay", { x: click_x, y: click_y });
779
+ on_annotation_click(annotation, click_x, click_y);
780
+ }
781
+ },
782
+ onClick: (e) => {
783
+ e.preventDefault();
784
+ e.stopPropagation();
785
+ }
786
+ }
787
+ ),
788
+ /* @__PURE__ */ jsx2(
789
+ "rect",
790
+ {
791
+ x: screen_x,
792
+ y: screen_y,
793
+ width: screen_width,
794
+ height: screen_height,
795
+ stroke,
796
+ strokeWidth: "2",
797
+ fill,
798
+ pointerEvents: "none"
799
+ }
800
+ )
801
+ ] }, annotation.id);
802
+ };
803
+ return /* @__PURE__ */ jsxs(
804
+ "svg",
805
+ {
806
+ ref: svg_ref,
807
+ className: cn("cls_annotation_overlay", className),
808
+ style: {
809
+ position: "absolute",
810
+ top: 0,
811
+ left: 0,
812
+ width,
813
+ height,
814
+ // Cursor will be dynamically updated by onMouseEnter/Leave on annotations
815
+ // Default: In pan mode, inherit cursor from parent (grab/grabbing)
816
+ // In annotation mode, show crosshair
817
+ cursor: hovered_annotation_id ? "pointer" : current_tool === null ? "inherit" : current_tool ? "crosshair" : "default",
818
+ // Always allow pointer events so context menu (right-click) and annotation clicks work
819
+ // Left-click panning is handled by returning early in handle_mouse_down
820
+ pointerEvents: "auto",
821
+ zIndex: 10
822
+ // Ensure annotations are above the canvas but below dialogs
823
+ },
824
+ width,
825
+ height,
826
+ onMouseDown: handle_mouse_down,
827
+ onMouseMove: handle_mouse_move,
828
+ onMouseUp: handle_mouse_up,
829
+ onMouseLeave: () => {
830
+ setHoveredAnnotationId(null);
831
+ handle_mouse_leave();
832
+ },
833
+ onContextMenu: handle_context_menu,
834
+ children: [
835
+ page_annotations.map(render_annotation),
836
+ is_drawing && /* @__PURE__ */ jsx2(
837
+ TempDrawBox,
838
+ {
839
+ start: start_point,
840
+ current: current_point,
841
+ tool_type: current_tool || "Square",
842
+ config
843
+ }
844
+ )
845
+ ]
846
+ }
847
+ );
848
+ };
849
+
850
+ // src/components/pdf_viewer/pdf_viewer_layout.tsx
851
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
852
+ var PdfViewerLayout = ({
853
+ pdf_document,
854
+ scale,
855
+ annotations = [],
856
+ current_tool = "Square",
857
+ on_annotation_create,
858
+ on_context_menu,
859
+ on_annotation_click,
860
+ background_color = "#2d2d2d",
861
+ config = null,
862
+ className = ""
863
+ }) => {
864
+ const [pages, setPages] = useState2([]);
865
+ const [loading, setLoading] = useState2(true);
866
+ const [coordinate_mappers, setCoordinateMappers] = useState2(/* @__PURE__ */ new Map());
867
+ const container_ref = useRef3(null);
868
+ const has_centered_ref = useRef3(false);
869
+ const [is_panning, setIsPanning] = useState2(false);
870
+ const pan_start_ref = useRef3(null);
871
+ useEffect2(() => {
872
+ if (!pdf_document) {
873
+ return;
874
+ }
875
+ setLoading(true);
876
+ const num_pages = pdf_document.numPages;
877
+ const page_promises = [];
878
+ for (let i = 1; i <= num_pages; i++) {
879
+ page_promises.push(pdf_document.getPage(i));
880
+ }
881
+ Promise.all(page_promises).then((loaded_pages) => {
882
+ setPages(loaded_pages);
883
+ setLoading(false);
884
+ has_centered_ref.current = false;
885
+ }).catch((error) => {
886
+ console.error("[PdfViewerLayout] Error loading PDF pages:", error);
887
+ setLoading(false);
888
+ });
889
+ }, [pdf_document]);
890
+ const handle_coordinate_mapper_ready = useCallback((page_index, mapper, dimensions) => {
891
+ setCoordinateMappers((prev) => {
892
+ const existing = prev.get(page_index);
893
+ if (existing && existing.dimensions.width === dimensions.width && existing.dimensions.height === dimensions.height) {
894
+ return prev;
895
+ }
896
+ const new_map = new Map(prev);
897
+ new_map.set(page_index, { mapper, dimensions });
898
+ return new_map;
899
+ });
900
+ }, []);
901
+ useEffect2(() => {
902
+ if (loading || pages.length === 0 || coordinate_mappers.size === 0 || has_centered_ref.current) {
903
+ return;
904
+ }
905
+ const timeout_id = setTimeout(() => {
906
+ if (container_ref.current) {
907
+ const container = container_ref.current;
908
+ const scroll_width = container.scrollWidth;
909
+ const client_width = container.clientWidth;
910
+ if (scroll_width > client_width) {
911
+ const center_scroll = (scroll_width - client_width) / 2;
912
+ container.scrollLeft = center_scroll;
913
+ has_centered_ref.current = true;
914
+ } else {
915
+ has_centered_ref.current = true;
916
+ }
917
+ }
918
+ }, 100);
919
+ return () => {
920
+ clearTimeout(timeout_id);
921
+ };
922
+ }, [loading, pages.length, coordinate_mappers.size]);
923
+ const handle_mouse_down = useCallback((e) => {
924
+ if (e.button !== 0) return;
925
+ const native_event = e.nativeEvent;
926
+ if (native_event?.__annotation_clicked) {
927
+ console.log(
928
+ `\u{1F7E2} [AnnotationClick] layout received marker id=${native_event.__annotation_clicked}, source=${native_event.__annotation_click_source || "unknown"}`
929
+ );
930
+ return;
931
+ }
932
+ if (current_tool !== null) return;
933
+ if (container_ref.current) {
934
+ setIsPanning(true);
935
+ pan_start_ref.current = {
936
+ x: e.clientX,
937
+ y: e.clientY,
938
+ scrollLeft: container_ref.current.scrollLeft,
939
+ scrollTop: container_ref.current.scrollTop
940
+ };
941
+ e.preventDefault();
942
+ e.stopPropagation();
943
+ container_ref.current.style.cursor = "grabbing";
944
+ }
945
+ }, [current_tool]);
946
+ const handle_mouse_move = useCallback((e) => {
947
+ if (!is_panning || !pan_start_ref.current || !container_ref.current) return;
948
+ e.preventDefault();
949
+ const delta_x = pan_start_ref.current.x - e.clientX;
950
+ const delta_y = pan_start_ref.current.y - e.clientY;
951
+ const max_scroll_left = Math.max(0, container_ref.current.scrollWidth - container_ref.current.clientWidth);
952
+ const max_scroll_top = Math.max(0, container_ref.current.scrollHeight - container_ref.current.clientHeight);
953
+ const new_scroll_left = Math.max(0, Math.min(max_scroll_left, pan_start_ref.current.scrollLeft + delta_x));
954
+ const new_scroll_top = Math.max(0, Math.min(max_scroll_top, pan_start_ref.current.scrollTop + delta_y));
955
+ container_ref.current.scrollLeft = new_scroll_left;
956
+ container_ref.current.scrollTop = new_scroll_top;
957
+ }, [is_panning]);
958
+ const handle_mouse_up = useCallback(() => {
959
+ if (container_ref.current) {
960
+ container_ref.current.style.cursor = current_tool === null ? "grab" : "default";
961
+ }
962
+ setIsPanning(false);
963
+ pan_start_ref.current = null;
964
+ }, [current_tool]);
965
+ useEffect2(() => {
966
+ if (container_ref.current) {
967
+ if (current_tool === null) {
968
+ container_ref.current.style.cursor = is_panning ? "grabbing" : "grab";
969
+ } else {
970
+ container_ref.current.style.cursor = "default";
971
+ }
972
+ }
973
+ }, [current_tool, is_panning]);
974
+ useEffect2(() => {
975
+ if (is_panning) {
976
+ window.addEventListener("mousemove", handle_mouse_move);
977
+ window.addEventListener("mouseup", handle_mouse_up);
978
+ return () => {
979
+ window.removeEventListener("mousemove", handle_mouse_move);
980
+ window.removeEventListener("mouseup", handle_mouse_up);
981
+ };
982
+ }
983
+ return void 0;
984
+ }, [is_panning, handle_mouse_move, handle_mouse_up]);
985
+ if (loading) {
986
+ return /* @__PURE__ */ jsx3("div", { className: cn("cls_pdf_viewer_loading", className), children: /* @__PURE__ */ jsx3("div", { className: "cls_pdf_viewer_spinner", children: "Loading PDF..." }) });
987
+ }
988
+ return /* @__PURE__ */ jsx3(
989
+ "div",
990
+ {
991
+ ref: container_ref,
992
+ className: cn("cls_pdf_viewer_layout", className),
993
+ style: {
994
+ // Don't constrain width/height - let content determine size
995
+ // This allows container to expand beyond viewport when zoomed
996
+ position: "relative",
997
+ backgroundColor: background_color,
998
+ // Cursor is managed dynamically - default to grab in pan mode, but annotations will override
999
+ cursor: current_tool === null ? is_panning ? "grabbing" : "grab" : "default",
1000
+ userSelect: is_panning ? "none" : "auto",
1001
+ // Allow both horizontal and vertical scrolling
1002
+ overflow: "auto",
1003
+ overflowX: "auto",
1004
+ overflowY: "auto",
1005
+ // Container must be viewport size (100%) to create scrollable area
1006
+ // Content inside (pages_container) expands beyond this to enable scrolling
1007
+ width: "100%",
1008
+ height: "100%",
1009
+ // Don't constrain content expansion
1010
+ minWidth: 0,
1011
+ minHeight: 0
1012
+ },
1013
+ onMouseDown: handle_mouse_down,
1014
+ onMouseMove: (e) => {
1015
+ const target = e.target;
1016
+ if (target.closest("svg.cls_annotation_overlay") || target.closest('rect[style*="cursor: pointer"]')) {
1017
+ return;
1018
+ }
1019
+ if (container_ref.current && current_tool === null && !is_panning) {
1020
+ container_ref.current.style.cursor = "grab";
1021
+ }
1022
+ },
1023
+ children: /* @__PURE__ */ jsx3("div", { className: "cls_pdf_viewer_pages_container", children: pages.map((page, index) => {
1024
+ const mapper_data = coordinate_mappers.get(index);
1025
+ const page_annotations = annotations?.filter(
1026
+ (ann) => ann.page_index === index
1027
+ ) || [];
1028
+ return /* @__PURE__ */ jsxs2(
1029
+ "div",
1030
+ {
1031
+ className: "cls_pdf_viewer_page_wrapper",
1032
+ style: {
1033
+ position: "relative",
1034
+ marginBottom: "20px",
1035
+ display: "flex",
1036
+ justifyContent: "center",
1037
+ // Inherit cursor from parent (grab/grabbing in pan mode)
1038
+ cursor: "inherit",
1039
+ // Ensure page wrapper doesn't constrain width
1040
+ width: "auto",
1041
+ minWidth: "fit-content"
1042
+ },
1043
+ children: [
1044
+ /* @__PURE__ */ jsx3(
1045
+ PdfPageRenderer,
1046
+ {
1047
+ page,
1048
+ page_index: index,
1049
+ scale,
1050
+ config,
1051
+ on_coordinate_mapper_ready: (mapper, dimensions) => handle_coordinate_mapper_ready(index, mapper, dimensions)
1052
+ }
1053
+ ),
1054
+ mapper_data && /* @__PURE__ */ jsx3(
1055
+ AnnotationOverlay,
1056
+ {
1057
+ width: mapper_data.dimensions.width,
1058
+ height: mapper_data.dimensions.height,
1059
+ page_index: index,
1060
+ map_coords: mapper_data.mapper,
1061
+ annotations: page_annotations,
1062
+ current_tool,
1063
+ config,
1064
+ on_annotation_create,
1065
+ on_context_menu: (e, screen_x, screen_y) => {
1066
+ if (on_context_menu && mapper_data.mapper) {
1067
+ on_context_menu(e, index, screen_x, screen_y, mapper_data.mapper);
1068
+ }
1069
+ },
1070
+ on_annotation_click: (annotation, screen_x, screen_y) => {
1071
+ if (on_annotation_click && mapper_data.mapper) {
1072
+ on_annotation_click(annotation, screen_x, screen_y, mapper_data.mapper);
1073
+ }
1074
+ }
1075
+ }
1076
+ )
1077
+ ]
1078
+ },
1079
+ index
1080
+ );
1081
+ }) })
1082
+ }
1083
+ );
1084
+ };
1085
+
1086
+ // src/components/pdf_viewer/context_menu.tsx
1087
+ import { useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
1088
+ import { createPortal } from "react-dom";
1089
+ import { Undo2, FileText } from "lucide-react";
1090
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1091
+ var ContextMenu = ({
1092
+ x,
1093
+ y,
1094
+ can_undo = false,
1095
+ on_undo,
1096
+ on_annotate,
1097
+ on_close,
1098
+ config = null,
1099
+ custom_stamps = [],
1100
+ on_stamp_click
1101
+ }) => {
1102
+ const menu_ref = useRef4(null);
1103
+ const render_count_ref = useRef4(0);
1104
+ const [adjusted_position, setAdjustedPosition] = useState3({ x, y });
1105
+ const props_received_time_ref = useRef4(performance.now());
1106
+ const [mounted, setMounted] = useState3(false);
1107
+ useEffect3(() => {
1108
+ setMounted(true);
1109
+ return () => setMounted(false);
1110
+ }, []);
1111
+ useEffect3(() => {
1112
+ props_received_time_ref.current = performance.now();
1113
+ }, [x, y]);
1114
+ useEffect3(() => {
1115
+ render_count_ref.current += 1;
1116
+ if (menu_ref.current && mounted) {
1117
+ const rect = menu_ref.current.getBoundingClientRect();
1118
+ const actual_x = x;
1119
+ const actual_y = y;
1120
+ const menu_element = menu_ref.current;
1121
+ const positioned_ancestors = [];
1122
+ let current = menu_element.parentElement;
1123
+ while (current && current !== document.body && current !== document.documentElement) {
1124
+ const style = window.getComputedStyle(current);
1125
+ const position = style.position;
1126
+ if (position === "fixed" || position === "absolute" || position === "relative" || position === "sticky") {
1127
+ positioned_ancestors.push({
1128
+ element: current,
1129
+ boundingRect: current.getBoundingClientRect(),
1130
+ computedStyle: style
1131
+ });
1132
+ }
1133
+ current = current.parentElement;
1134
+ }
1135
+ const parent_transforms = [];
1136
+ let check = menu_element.parentElement;
1137
+ while (check && check !== document.body) {
1138
+ const style = window.getComputedStyle(check);
1139
+ const transform = style.transform;
1140
+ if (transform && transform !== "none") {
1141
+ parent_transforms.push({
1142
+ element: check,
1143
+ transform
1144
+ });
1145
+ }
1146
+ check = check.parentElement;
1147
+ }
1148
+ const x_offset = rect.left - x;
1149
+ const y_offset = rect.top - y;
1150
+ const position_correct = Math.abs(x_offset) < 1 && Math.abs(y_offset) < 1;
1151
+ const parent_transform_count = parent_transforms.length;
1152
+ const positioned_ancestor_count = positioned_ancestors.length;
1153
+ console.log(`\u{1F7E1} [ContextMenu] Position debug: render=${render_count_ref.current}, expected=(${x}, ${y}), actual=(${rect.left.toFixed(1)}, ${rect.top.toFixed(1)}), offset=(${x_offset.toFixed(1)}, ${y_offset.toFixed(1)}), correct=${position_correct}, ancestors=${positioned_ancestor_count}, transforms=${parent_transform_count}`);
1154
+ const bottom_left_x = rect.left;
1155
+ const bottom_left_y = rect.bottom;
1156
+ const tolerance = 5;
1157
+ const offset_x = 0;
1158
+ const offset_y = 0;
1159
+ if (Math.abs(bottom_left_x - x) < tolerance && Math.abs(bottom_left_y - y) < tolerance) {
1160
+ console.log("[ContextMenu] Detected bottom-left positioning, adjusting to top-left");
1161
+ setAdjustedPosition({
1162
+ x: actual_x + offset_x,
1163
+ y: actual_y + offset_y - rect.height
1164
+ });
1165
+ } else {
1166
+ setAdjustedPosition({
1167
+ x: actual_x + offset_x,
1168
+ y: actual_y + offset_y
1169
+ });
1170
+ }
1171
+ }
1172
+ }, [x, y, mounted]);
1173
+ const handle_item_click = (callback) => {
1174
+ if (callback) {
1175
+ callback();
1176
+ }
1177
+ if (on_close) {
1178
+ on_close();
1179
+ }
1180
+ };
1181
+ console.log(`\u26AA [ContextMenu] Render call: render=${render_count_ref.current}, props=(${x}, ${y}), adjusted=(${adjusted_position.x}, ${adjusted_position.y}), mounted=${mounted}`);
1182
+ if (!mounted) {
1183
+ return null;
1184
+ }
1185
+ const handle_mouse_leave = () => {
1186
+ if (on_close) {
1187
+ on_close();
1188
+ }
1189
+ };
1190
+ const menu_config = config?.context_menu || default_config.context_menu;
1191
+ const menu_content = /* @__PURE__ */ jsx4(
1192
+ "div",
1193
+ {
1194
+ ref: menu_ref,
1195
+ className: "cls_pdf_viewer_context_menu",
1196
+ style: {
1197
+ position: "fixed",
1198
+ left: `${adjusted_position.x}px`,
1199
+ top: `${adjusted_position.y}px`,
1200
+ zIndex: 1e4,
1201
+ backgroundColor: menu_config.context_menu_background_color,
1202
+ borderColor: menu_config.context_menu_border_color
1203
+ },
1204
+ onClick: (e) => e.stopPropagation(),
1205
+ onContextMenu: (e) => {
1206
+ e.preventDefault();
1207
+ e.stopPropagation();
1208
+ },
1209
+ onMouseLeave: handle_mouse_leave,
1210
+ children: /* @__PURE__ */ jsxs3("div", { className: "cls_pdf_viewer_context_menu_items", children: [
1211
+ /* @__PURE__ */ jsxs3(
1212
+ "button",
1213
+ {
1214
+ type: "button",
1215
+ onClick: () => handle_item_click(on_undo),
1216
+ disabled: !can_undo,
1217
+ className: cn(
1218
+ "cls_pdf_viewer_context_menu_item",
1219
+ !can_undo && "cls_pdf_viewer_context_menu_item_disabled"
1220
+ ),
1221
+ style: {
1222
+ opacity: !can_undo ? menu_config.context_menu_item_disabled_opacity : 1
1223
+ },
1224
+ onMouseEnter: (e) => {
1225
+ if (can_undo) {
1226
+ e.currentTarget.style.backgroundColor = menu_config.context_menu_item_hover_background;
1227
+ }
1228
+ },
1229
+ onMouseLeave: (e) => {
1230
+ e.currentTarget.style.backgroundColor = "transparent";
1231
+ },
1232
+ "aria-label": "Undo",
1233
+ children: [
1234
+ /* @__PURE__ */ jsx4(Undo2, { className: "cls_pdf_viewer_context_menu_icon", size: 16 }),
1235
+ /* @__PURE__ */ jsx4("span", { className: "cls_pdf_viewer_context_menu_text", children: "Undo" })
1236
+ ]
1237
+ }
1238
+ ),
1239
+ /* @__PURE__ */ jsxs3(
1240
+ "button",
1241
+ {
1242
+ type: "button",
1243
+ onClick: () => handle_item_click(on_annotate),
1244
+ className: "cls_pdf_viewer_context_menu_item",
1245
+ onMouseEnter: (e) => {
1246
+ e.currentTarget.style.backgroundColor = menu_config.context_menu_item_hover_background;
1247
+ },
1248
+ onMouseLeave: (e) => {
1249
+ e.currentTarget.style.backgroundColor = "transparent";
1250
+ },
1251
+ "aria-label": "Annotate",
1252
+ children: [
1253
+ /* @__PURE__ */ jsx4(FileText, { className: "cls_pdf_viewer_context_menu_icon", size: 16 }),
1254
+ /* @__PURE__ */ jsx4("span", { className: "cls_pdf_viewer_context_menu_text", children: "Annotate" })
1255
+ ]
1256
+ }
1257
+ ),
1258
+ custom_stamps.length > 0 && /* @__PURE__ */ jsxs3(Fragment, { children: [
1259
+ /* @__PURE__ */ jsx4(
1260
+ "div",
1261
+ {
1262
+ style: {
1263
+ height: "1px",
1264
+ backgroundColor: menu_config.context_menu_border_color,
1265
+ margin: "4px 0"
1266
+ }
1267
+ }
1268
+ ),
1269
+ [...custom_stamps].sort((a, b) => a.order - b.order).map((stamp, index) => /* @__PURE__ */ jsxs3(
1270
+ "button",
1271
+ {
1272
+ type: "button",
1273
+ onClick: () => {
1274
+ if (on_stamp_click) {
1275
+ on_stamp_click(stamp);
1276
+ }
1277
+ if (on_close) {
1278
+ on_close();
1279
+ }
1280
+ },
1281
+ className: "cls_pdf_viewer_context_menu_item",
1282
+ onMouseEnter: (e) => {
1283
+ e.currentTarget.style.backgroundColor = menu_config.context_menu_item_hover_background;
1284
+ },
1285
+ onMouseLeave: (e) => {
1286
+ e.currentTarget.style.backgroundColor = "transparent";
1287
+ },
1288
+ "aria-label": stamp.name,
1289
+ children: [
1290
+ /* @__PURE__ */ jsx4(FileText, { className: "cls_pdf_viewer_context_menu_icon", size: 16 }),
1291
+ /* @__PURE__ */ jsx4("span", { className: "cls_pdf_viewer_context_menu_text", children: stamp.name })
1292
+ ]
1293
+ },
1294
+ `${stamp.name}-${index}`
1295
+ ))
1296
+ ] })
1297
+ ] })
1298
+ }
1299
+ );
1300
+ return createPortal(menu_content, document.body);
1301
+ };
1302
+
1303
+ // src/components/pdf_viewer/text_annotation_dialog.tsx
1304
+ import { useState as useState4, useEffect as useEffect4, useRef as useRef5 } from "react";
1305
+ import { createPortal as createPortal2 } from "react-dom";
1306
+ import { Check, X, Trash2 } from "lucide-react";
1307
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1308
+ var TextAnnotationDialog = ({
1309
+ open,
1310
+ x,
1311
+ y,
1312
+ on_close,
1313
+ on_submit,
1314
+ on_delete,
1315
+ initial_text = "",
1316
+ is_editing = false,
1317
+ config = null
1318
+ }) => {
1319
+ const [text, setText] = useState4(initial_text);
1320
+ const input_ref = useRef5(null);
1321
+ const [mounted, setMounted] = useState4(false);
1322
+ useEffect4(() => {
1323
+ setMounted(true);
1324
+ return () => setMounted(false);
1325
+ }, []);
1326
+ useEffect4(() => {
1327
+ if (open) {
1328
+ setText(initial_text);
1329
+ setTimeout(() => {
1330
+ input_ref.current?.focus();
1331
+ input_ref.current?.select();
1332
+ }, 0);
1333
+ }
1334
+ }, [open, initial_text]);
1335
+ const handle_submit = (e) => {
1336
+ if (e) {
1337
+ e.preventDefault();
1338
+ }
1339
+ if (text.trim()) {
1340
+ on_submit(text.trim());
1341
+ setText("");
1342
+ on_close();
1343
+ }
1344
+ };
1345
+ const handle_cancel = () => {
1346
+ setText("");
1347
+ on_close();
1348
+ };
1349
+ const handle_delete = () => {
1350
+ if (on_delete) {
1351
+ on_delete();
1352
+ setText("");
1353
+ on_close();
1354
+ }
1355
+ };
1356
+ useEffect4(() => {
1357
+ const handle_keydown = (e) => {
1358
+ if (!open) return;
1359
+ if (e.key === "Escape") {
1360
+ e.preventDefault();
1361
+ handle_cancel();
1362
+ } else if (e.key === "Enter" && !e.shiftKey) {
1363
+ e.preventDefault();
1364
+ handle_submit();
1365
+ }
1366
+ };
1367
+ if (open) {
1368
+ window.addEventListener("keydown", handle_keydown);
1369
+ return () => {
1370
+ window.removeEventListener("keydown", handle_keydown);
1371
+ };
1372
+ }
1373
+ return void 0;
1374
+ }, [open, text]);
1375
+ if (!open || !mounted) {
1376
+ return null;
1377
+ }
1378
+ const dialog_config = config?.dialog || default_config.dialog;
1379
+ const dialog_content = /* @__PURE__ */ jsxs4(Fragment2, { children: [
1380
+ /* @__PURE__ */ jsx5(
1381
+ "div",
1382
+ {
1383
+ className: "cls_pdf_viewer_dialog_backdrop",
1384
+ onClick: handle_cancel,
1385
+ "aria-hidden": "true",
1386
+ style: {
1387
+ backgroundColor: `rgba(0, 0, 0, ${dialog_config.dialog_backdrop_opacity})`
1388
+ }
1389
+ }
1390
+ ),
1391
+ /* @__PURE__ */ jsx5(
1392
+ "div",
1393
+ {
1394
+ className: "cls_pdf_viewer_text_dialog",
1395
+ role: "dialog",
1396
+ "aria-modal": "true",
1397
+ style: {
1398
+ position: "fixed",
1399
+ left: `${x}px`,
1400
+ top: `${y}px`,
1401
+ zIndex: 10001,
1402
+ // Higher than context menu
1403
+ backgroundColor: dialog_config.dialog_background_color,
1404
+ borderColor: dialog_config.dialog_border_color
1405
+ },
1406
+ onClick: (e) => e.stopPropagation(),
1407
+ children: /* @__PURE__ */ jsxs4("form", { onSubmit: handle_submit, className: "cls_pdf_viewer_dialog_form", children: [
1408
+ /* @__PURE__ */ jsx5(
1409
+ "input",
1410
+ {
1411
+ ref: input_ref,
1412
+ type: "text",
1413
+ value: text,
1414
+ onChange: (e) => setText(e.target.value),
1415
+ className: "cls_pdf_viewer_dialog_input",
1416
+ placeholder: "Enter annotation text...",
1417
+ autoFocus: true
1418
+ }
1419
+ ),
1420
+ /* @__PURE__ */ jsxs4("div", { className: "cls_pdf_viewer_dialog_buttons", children: [
1421
+ is_editing && on_delete && /* @__PURE__ */ jsx5(
1422
+ "button",
1423
+ {
1424
+ type: "button",
1425
+ onClick: handle_delete,
1426
+ className: cn(
1427
+ "cls_pdf_viewer_dialog_button",
1428
+ "cls_pdf_viewer_dialog_button_delete"
1429
+ ),
1430
+ style: {
1431
+ color: dialog_config.dialog_button_cancel_color
1432
+ },
1433
+ onMouseEnter: (e) => {
1434
+ e.currentTarget.style.color = dialog_config.dialog_button_cancel_color_hover;
1435
+ },
1436
+ onMouseLeave: (e) => {
1437
+ e.currentTarget.style.color = dialog_config.dialog_button_cancel_color;
1438
+ },
1439
+ "aria-label": "Delete annotation",
1440
+ title: "Delete annotation",
1441
+ children: /* @__PURE__ */ jsx5(Trash2, { size: 14 })
1442
+ }
1443
+ ),
1444
+ /* @__PURE__ */ jsx5(
1445
+ "button",
1446
+ {
1447
+ type: "button",
1448
+ onClick: handle_cancel,
1449
+ className: cn(
1450
+ "cls_pdf_viewer_dialog_button",
1451
+ "cls_pdf_viewer_dialog_button_cancel"
1452
+ ),
1453
+ style: {
1454
+ color: dialog_config.dialog_button_cancel_color
1455
+ },
1456
+ onMouseEnter: (e) => {
1457
+ e.currentTarget.style.color = dialog_config.dialog_button_cancel_color_hover;
1458
+ },
1459
+ onMouseLeave: (e) => {
1460
+ e.currentTarget.style.color = dialog_config.dialog_button_cancel_color;
1461
+ },
1462
+ "aria-label": "Cancel",
1463
+ title: "Cancel (Esc)",
1464
+ children: /* @__PURE__ */ jsx5(X, { size: 14 })
1465
+ }
1466
+ ),
1467
+ /* @__PURE__ */ jsx5(
1468
+ "button",
1469
+ {
1470
+ type: "submit",
1471
+ disabled: !text.trim(),
1472
+ className: cn(
1473
+ "cls_pdf_viewer_dialog_button",
1474
+ "cls_pdf_viewer_dialog_button_submit",
1475
+ !text.trim() && "cls_pdf_viewer_dialog_button_disabled"
1476
+ ),
1477
+ style: {
1478
+ color: !text.trim() ? void 0 : dialog_config.dialog_button_submit_color,
1479
+ opacity: !text.trim() ? dialog_config.dialog_button_disabled_opacity : 1
1480
+ },
1481
+ onMouseEnter: (e) => {
1482
+ if (text.trim()) {
1483
+ e.currentTarget.style.color = dialog_config.dialog_button_submit_color_hover;
1484
+ }
1485
+ },
1486
+ onMouseLeave: (e) => {
1487
+ if (text.trim()) {
1488
+ e.currentTarget.style.color = dialog_config.dialog_button_submit_color;
1489
+ }
1490
+ },
1491
+ "aria-label": "Submit",
1492
+ title: "Submit (Enter)",
1493
+ children: /* @__PURE__ */ jsx5(Check, { size: 14 })
1494
+ }
1495
+ )
1496
+ ] })
1497
+ ] })
1498
+ }
1499
+ )
1500
+ ] });
1501
+ return createPortal2(dialog_content, document.body);
1502
+ };
1503
+
1504
+ // src/utils/config_loader.ts
1505
+ function parse_ini_browser(ini_text) {
1506
+ const result = {};
1507
+ let current_section = "";
1508
+ const lines = ini_text.split("\n");
1509
+ for (const line of lines) {
1510
+ const trimmed = line.trim();
1511
+ if (!trimmed || trimmed.startsWith("#")) {
1512
+ continue;
1513
+ }
1514
+ const section_match = trimmed.match(/^\[([^\]]+)\]$/);
1515
+ if (section_match) {
1516
+ current_section = section_match[1].trim();
1517
+ if (!result[current_section]) {
1518
+ result[current_section] = {};
1519
+ }
1520
+ continue;
1521
+ }
1522
+ const key_value_match = trimmed.match(/^([^=]+)=(.*)$/);
1523
+ if (key_value_match && current_section) {
1524
+ const key = key_value_match[1].trim();
1525
+ const value = key_value_match[2].trim();
1526
+ result[current_section][key] = value;
1527
+ }
1528
+ }
1529
+ return result;
1530
+ }
1531
+ async function load_config_browser(config_file) {
1532
+ try {
1533
+ const config_url = config_file === "hazo_pdf_config.ini" || config_file.includes("hazo_pdf_config.ini") ? "/api/config" : config_file;
1534
+ const response = await fetch(config_url);
1535
+ if (!response.ok) {
1536
+ throw new Error(`Failed to fetch config file: ${response.status} ${response.statusText}`);
1537
+ }
1538
+ const ini_text = await response.text();
1539
+ const parsed = parse_ini_browser(ini_text);
1540
+ console.log("[ConfigLoader] Parsed config sections:", Object.keys(parsed));
1541
+ if (parsed["fonts"]) {
1542
+ console.log("[ConfigLoader] fonts section:", parsed["fonts"]);
1543
+ }
1544
+ if (parsed["freetext_annotation"]) {
1545
+ console.log("[ConfigLoader] freetext_annotation section:", parsed["freetext_annotation"]);
1546
+ }
1547
+ if (parsed["viewer"]) {
1548
+ console.log("[ConfigLoader] viewer section:", parsed["viewer"]);
1549
+ } else {
1550
+ console.warn("[ConfigLoader] viewer section NOT found in parsed config!");
1551
+ }
1552
+ const get_value = (section, key) => {
1553
+ return parsed[section]?.[key];
1554
+ };
1555
+ const config = build_config_from_ini(get_value);
1556
+ console.log("[ConfigLoader] Final config values:");
1557
+ console.log(" font_foreground_color:", config.fonts.font_foreground_color);
1558
+ console.log(" freetext_text_color:", config.freetext_annotation.freetext_text_color);
1559
+ console.log(" freetext_background_color:", config.freetext_annotation.freetext_background_color);
1560
+ console.log(" freetext_background_opacity:", config.freetext_annotation.freetext_background_opacity);
1561
+ console.log(" append_timestamp_to_text_edits:", config.viewer.append_timestamp_to_text_edits);
1562
+ console.log(" annotation_text_suffix_fixed_text:", config.viewer.annotation_text_suffix_fixed_text);
1563
+ const raw_value = get_value("viewer", "append_timestamp_to_text_edits");
1564
+ console.log("[ConfigLoader] append_timestamp_to_text_edits:");
1565
+ console.log(" raw_value from INI:", raw_value, "(type:", typeof raw_value, ")");
1566
+ console.log(" parsed boolean:", config.viewer.append_timestamp_to_text_edits);
1567
+ console.log(" parse_boolean test:", parse_boolean(raw_value, false));
1568
+ if (parsed["viewer"]) {
1569
+ console.log("[ConfigLoader] All viewer section keys:", Object.keys(parsed["viewer"]));
1570
+ console.log("[ConfigLoader] All viewer section values:", parsed["viewer"]);
1571
+ }
1572
+ return config;
1573
+ } catch (error) {
1574
+ console.warn(`[ConfigLoader] Could not load config file "${config_file}" in browser, using defaults:`, error);
1575
+ return default_config;
1576
+ }
1577
+ }
1578
+ async function load_pdf_config_async(config_file) {
1579
+ const is_browser = typeof window !== "undefined" && typeof fetch !== "undefined";
1580
+ if (is_browser) {
1581
+ return load_config_browser(config_file);
1582
+ }
1583
+ try {
1584
+ const { HazoConfig } = __require("hazo_config");
1585
+ const hazo_config = new HazoConfig({ filePath: config_file });
1586
+ console.log(`[ConfigLoader] Using hazo_config to load: ${config_file}`);
1587
+ const get_value = (section, key) => {
1588
+ return hazo_config.get(section, key);
1589
+ };
1590
+ const config = build_config_from_ini(get_value);
1591
+ console.log(`[ConfigLoader] Successfully loaded config using hazo_config from: ${config_file}`);
1592
+ return config;
1593
+ } catch (error) {
1594
+ console.warn(
1595
+ `[ConfigLoader] Could not load config file "${config_file}" using hazo_config, using defaults:`,
1596
+ error instanceof Error ? error.message : String(error)
1597
+ );
1598
+ return default_config;
1599
+ }
1600
+ }
1601
+ function build_config_from_ini(get_value) {
1602
+ return {
1603
+ fonts: {
1604
+ freetext_font_family: parse_string(
1605
+ get_value("fonts", "freetext_font_family"),
1606
+ default_config.fonts.freetext_font_family
1607
+ ),
1608
+ freetext_font_size_min: parse_number(
1609
+ get_value("fonts", "freetext_font_size_min"),
1610
+ default_config.fonts.freetext_font_size_min
1611
+ ),
1612
+ freetext_font_size_max: parse_number(
1613
+ get_value("fonts", "freetext_font_size_max"),
1614
+ default_config.fonts.freetext_font_size_max
1615
+ ),
1616
+ freetext_font_size_default: parse_number(
1617
+ get_value("fonts", "freetext_font_size_default"),
1618
+ default_config.fonts.freetext_font_size_default
1619
+ ),
1620
+ font_foreground_color: parse_color(
1621
+ get_value("fonts", "font_foreground_color"),
1622
+ default_config.fonts.font_foreground_color
1623
+ )
1624
+ },
1625
+ highlight_annotation: {
1626
+ highlight_fill_color: parse_color(
1627
+ get_value("highlight_annotation", "highlight_fill_color"),
1628
+ default_config.highlight_annotation.highlight_fill_color
1629
+ ),
1630
+ highlight_fill_opacity: parse_opacity(
1631
+ get_value("highlight_annotation", "highlight_fill_opacity"),
1632
+ default_config.highlight_annotation.highlight_fill_opacity
1633
+ ),
1634
+ highlight_border_color: parse_color(
1635
+ get_value("highlight_annotation", "highlight_border_color"),
1636
+ default_config.highlight_annotation.highlight_border_color
1637
+ ),
1638
+ highlight_border_color_hover: parse_color(
1639
+ get_value("highlight_annotation", "highlight_border_color_hover"),
1640
+ default_config.highlight_annotation.highlight_border_color_hover
1641
+ ),
1642
+ highlight_fill_opacity_hover: parse_opacity(
1643
+ get_value("highlight_annotation", "highlight_fill_opacity_hover"),
1644
+ default_config.highlight_annotation.highlight_fill_opacity_hover
1645
+ )
1646
+ },
1647
+ square_annotation: {
1648
+ square_fill_color: parse_color(
1649
+ get_value("square_annotation", "square_fill_color"),
1650
+ default_config.square_annotation.square_fill_color
1651
+ ),
1652
+ square_fill_opacity: parse_opacity(
1653
+ get_value("square_annotation", "square_fill_opacity"),
1654
+ default_config.square_annotation.square_fill_opacity
1655
+ ),
1656
+ square_border_color: parse_color(
1657
+ get_value("square_annotation", "square_border_color"),
1658
+ default_config.square_annotation.square_border_color
1659
+ ),
1660
+ square_border_color_hover: parse_color(
1661
+ get_value("square_annotation", "square_border_color_hover"),
1662
+ default_config.square_annotation.square_border_color_hover
1663
+ ),
1664
+ square_fill_opacity_hover: parse_opacity(
1665
+ get_value("square_annotation", "square_fill_opacity_hover"),
1666
+ default_config.square_annotation.square_fill_opacity_hover
1667
+ )
1668
+ },
1669
+ freetext_annotation: {
1670
+ freetext_text_color: parse_color(
1671
+ get_value("freetext_annotation", "freetext_text_color"),
1672
+ default_config.freetext_annotation.freetext_text_color
1673
+ ),
1674
+ freetext_text_color_hover: parse_color(
1675
+ get_value("freetext_annotation", "freetext_text_color_hover"),
1676
+ default_config.freetext_annotation.freetext_text_color_hover
1677
+ ),
1678
+ freetext_border_color: parse_string(
1679
+ get_value("freetext_annotation", "freetext_border_color"),
1680
+ default_config.freetext_annotation.freetext_border_color
1681
+ ),
1682
+ freetext_border_width: parse_number(
1683
+ get_value("freetext_annotation", "freetext_border_width"),
1684
+ default_config.freetext_annotation.freetext_border_width
1685
+ ),
1686
+ freetext_background_color: (() => {
1687
+ const raw_value = get_value("freetext_annotation", "freetext_background_color");
1688
+ const parsed = parse_string(raw_value, default_config.freetext_annotation.freetext_background_color);
1689
+ console.log(`[ConfigLoader] freetext_background_color: raw="${raw_value}", parsed="${parsed}"`);
1690
+ return parsed;
1691
+ })(),
1692
+ freetext_background_opacity: parse_opacity(
1693
+ get_value("freetext_annotation", "freetext_background_opacity"),
1694
+ default_config.freetext_annotation.freetext_background_opacity
1695
+ ),
1696
+ freetext_font_weight: parse_string(
1697
+ get_value("freetext_annotation", "freetext_font_weight"),
1698
+ default_config.freetext_annotation.freetext_font_weight
1699
+ ),
1700
+ freetext_font_style: parse_string(
1701
+ get_value("freetext_annotation", "freetext_font_style"),
1702
+ default_config.freetext_annotation.freetext_font_style
1703
+ ),
1704
+ freetext_text_decoration: parse_string(
1705
+ get_value("freetext_annotation", "freetext_text_decoration"),
1706
+ default_config.freetext_annotation.freetext_text_decoration
1707
+ ),
1708
+ freetext_padding_horizontal: parse_number(
1709
+ get_value("freetext_annotation", "freetext_padding_horizontal"),
1710
+ default_config.freetext_annotation.freetext_padding_horizontal
1711
+ ),
1712
+ freetext_padding_vertical: parse_number(
1713
+ get_value("freetext_annotation", "freetext_padding_vertical"),
1714
+ default_config.freetext_annotation.freetext_padding_vertical
1715
+ )
1716
+ },
1717
+ page_styling: {
1718
+ page_border_color: parse_color(
1719
+ get_value("page_styling", "page_border_color"),
1720
+ default_config.page_styling.page_border_color
1721
+ ),
1722
+ page_box_shadow: parse_string(
1723
+ get_value("page_styling", "page_box_shadow"),
1724
+ default_config.page_styling.page_box_shadow
1725
+ ),
1726
+ page_background_color: parse_color(
1727
+ get_value("page_styling", "page_background_color"),
1728
+ default_config.page_styling.page_background_color
1729
+ )
1730
+ },
1731
+ viewer: {
1732
+ viewer_background_color: parse_color(
1733
+ get_value("viewer", "viewer_background_color"),
1734
+ default_config.viewer.viewer_background_color
1735
+ ),
1736
+ append_timestamp_to_text_edits: parse_boolean(
1737
+ get_value("viewer", "append_timestamp_to_text_edits"),
1738
+ default_config.viewer.append_timestamp_to_text_edits
1739
+ ),
1740
+ annotation_text_suffix_fixed_text: parse_string(
1741
+ get_value("viewer", "annotation_text_suffix_fixed_text"),
1742
+ default_config.viewer.annotation_text_suffix_fixed_text
1743
+ ),
1744
+ add_enclosing_brackets_to_suffixes: parse_boolean(
1745
+ get_value("viewer", "add_enclosing_brackets_to_suffixes"),
1746
+ default_config.viewer.add_enclosing_brackets_to_suffixes
1747
+ ),
1748
+ suffix_enclosing_brackets: (() => {
1749
+ const raw_value = parse_string(
1750
+ get_value("viewer", "suffix_enclosing_brackets"),
1751
+ default_config.viewer.suffix_enclosing_brackets
1752
+ );
1753
+ if (raw_value.length === 2) {
1754
+ return raw_value;
1755
+ }
1756
+ console.warn(
1757
+ `[ConfigLoader] suffix_enclosing_brackets must be 2 characters, received "${raw_value}". Using default.`
1758
+ );
1759
+ return default_config.viewer.suffix_enclosing_brackets;
1760
+ })(),
1761
+ suffix_text_position: (() => {
1762
+ const raw_value = parse_string(
1763
+ get_value("viewer", "suffix_text_position"),
1764
+ default_config.viewer.suffix_text_position
1765
+ );
1766
+ const valid_values = [
1767
+ "adjacent",
1768
+ "below_single_line",
1769
+ "below_multi_line"
1770
+ ];
1771
+ if (valid_values.includes(raw_value)) {
1772
+ return raw_value;
1773
+ }
1774
+ console.warn(
1775
+ `[ConfigLoader] Invalid suffix_text_position "${raw_value}". Using default "${default_config.viewer.suffix_text_position}".`
1776
+ );
1777
+ return default_config.viewer.suffix_text_position;
1778
+ })()
1779
+ },
1780
+ context_menu: {
1781
+ context_menu_background_color: parse_color(
1782
+ get_value("context_menu", "context_menu_background_color"),
1783
+ default_config.context_menu.context_menu_background_color
1784
+ ),
1785
+ context_menu_border_color: parse_color(
1786
+ get_value("context_menu", "context_menu_border_color"),
1787
+ default_config.context_menu.context_menu_border_color
1788
+ ),
1789
+ context_menu_item_hover_background: parse_color(
1790
+ get_value("context_menu", "context_menu_item_hover_background"),
1791
+ default_config.context_menu.context_menu_item_hover_background
1792
+ ),
1793
+ context_menu_item_disabled_opacity: parse_opacity(
1794
+ get_value("context_menu", "context_menu_item_disabled_opacity"),
1795
+ default_config.context_menu.context_menu_item_disabled_opacity
1796
+ ),
1797
+ right_click_custom_stamps: parse_string(
1798
+ get_value("context_menu", "right_click_custom_stamps"),
1799
+ default_config.context_menu.right_click_custom_stamps
1800
+ )
1801
+ },
1802
+ dialog: {
1803
+ dialog_backdrop_opacity: parse_opacity(
1804
+ get_value("dialog", "dialog_backdrop_opacity"),
1805
+ default_config.dialog.dialog_backdrop_opacity
1806
+ ),
1807
+ dialog_background_color: parse_color(
1808
+ get_value("dialog", "dialog_background_color"),
1809
+ default_config.dialog.dialog_background_color
1810
+ ),
1811
+ dialog_border_color: parse_color(
1812
+ get_value("dialog", "dialog_border_color"),
1813
+ default_config.dialog.dialog_border_color
1814
+ ),
1815
+ dialog_button_submit_color: parse_color(
1816
+ get_value("dialog", "dialog_button_submit_color"),
1817
+ default_config.dialog.dialog_button_submit_color
1818
+ ),
1819
+ dialog_button_submit_color_hover: parse_color(
1820
+ get_value("dialog", "dialog_button_submit_color_hover"),
1821
+ default_config.dialog.dialog_button_submit_color_hover
1822
+ ),
1823
+ dialog_button_cancel_color: parse_color(
1824
+ get_value("dialog", "dialog_button_cancel_color"),
1825
+ default_config.dialog.dialog_button_cancel_color
1826
+ ),
1827
+ dialog_button_cancel_color_hover: parse_color(
1828
+ get_value("dialog", "dialog_button_cancel_color_hover"),
1829
+ default_config.dialog.dialog_button_cancel_color_hover
1830
+ ),
1831
+ dialog_button_disabled_opacity: parse_opacity(
1832
+ get_value("dialog", "dialog_button_disabled_opacity"),
1833
+ default_config.dialog.dialog_button_disabled_opacity
1834
+ )
1835
+ }
1836
+ };
1837
+ }
1838
+ function parse_color(value, default_value) {
1839
+ if (!value) return default_value;
1840
+ if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
1841
+ return value;
1842
+ }
1843
+ if (value.startsWith("rgba") || value.startsWith("rgb")) {
1844
+ return value;
1845
+ }
1846
+ console.warn(`[ConfigLoader] Invalid color format: ${value}, using default: ${default_value}`);
1847
+ return default_value;
1848
+ }
1849
+ function parse_opacity(value, default_value) {
1850
+ if (!value) return default_value;
1851
+ const parsed = parseFloat(value);
1852
+ if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) {
1853
+ return parsed;
1854
+ }
1855
+ console.warn(`[ConfigLoader] Invalid opacity value: ${value}, using default: ${default_value}`);
1856
+ return default_value;
1857
+ }
1858
+ function parse_number(value, default_value) {
1859
+ if (!value) return default_value;
1860
+ const parsed = parseFloat(value);
1861
+ if (!isNaN(parsed)) {
1862
+ return parsed;
1863
+ }
1864
+ console.warn(`[ConfigLoader] Invalid number value: ${value}, using default: ${default_value}`);
1865
+ return default_value;
1866
+ }
1867
+ function parse_string(value, default_value) {
1868
+ return value || default_value;
1869
+ }
1870
+ function parse_boolean(value, default_value) {
1871
+ if (!value) return default_value;
1872
+ const lower = value.toLowerCase().trim();
1873
+ if (lower === "true" || lower === "yes" || lower === "1") {
1874
+ return true;
1875
+ }
1876
+ if (lower === "false" || lower === "no" || lower === "0") {
1877
+ return false;
1878
+ }
1879
+ console.warn(`[ConfigLoader] Invalid boolean value: ${value}, using default: ${default_value}`);
1880
+ return default_value;
1881
+ }
1882
+ function load_pdf_config(config_file) {
1883
+ if (!config_file) {
1884
+ console.log("[ConfigLoader] No config file specified, using defaults");
1885
+ return default_config;
1886
+ }
1887
+ const is_browser = typeof window !== "undefined" && typeof fetch !== "undefined";
1888
+ if (is_browser) {
1889
+ console.warn("[ConfigLoader] Browser environment detected. Config loading should be async, but load_pdf_config is sync. Using defaults. Consider making this async or using a different approach.");
1890
+ return default_config;
1891
+ }
1892
+ try {
1893
+ const { HazoConfig } = __require("hazo_config");
1894
+ const hazo_config = new HazoConfig({ filePath: config_file });
1895
+ const get_value = (section, key) => {
1896
+ return hazo_config.get(section, key);
1897
+ };
1898
+ const config = build_config_from_ini(get_value);
1899
+ console.log(`[ConfigLoader] Successfully loaded config from: ${config_file}`);
1900
+ return config;
1901
+ } catch (error) {
1902
+ console.warn(
1903
+ `[ConfigLoader] Could not load config file "${config_file}", using defaults:`,
1904
+ error instanceof Error ? error.message : String(error)
1905
+ );
1906
+ return default_config;
1907
+ }
1908
+ }
1909
+
1910
+ // src/components/pdf_viewer/pdf_viewer.tsx
1911
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1912
+ var PdfViewer = ({
1913
+ url,
1914
+ className = "",
1915
+ scale: initial_scale = 1,
1916
+ on_load,
1917
+ on_error,
1918
+ annotations: initial_annotations = [],
1919
+ on_annotation_create,
1920
+ on_annotation_update,
1921
+ on_annotation_delete,
1922
+ on_save,
1923
+ background_color,
1924
+ config_file,
1925
+ append_timestamp_to_text_edits,
1926
+ annotation_text_suffix_fixed_text,
1927
+ right_click_custom_stamps
1928
+ }) => {
1929
+ const [pdf_document, setPdfDocument] = useState5(null);
1930
+ const [loading, setLoading] = useState5(true);
1931
+ const [error, setError] = useState5(null);
1932
+ const [scale, setScale] = useState5(initial_scale);
1933
+ const [annotations, setAnnotations] = useState5(initial_annotations);
1934
+ const [current_tool, setCurrentTool] = useState5(null);
1935
+ const [saving, setSaving] = useState5(false);
1936
+ const config_ref = useRef6(null);
1937
+ useEffect5(() => {
1938
+ if (!config_file) {
1939
+ config_ref.current = load_pdf_config();
1940
+ return;
1941
+ }
1942
+ const is_browser = typeof window !== "undefined" && typeof fetch !== "undefined";
1943
+ if (is_browser) {
1944
+ load_pdf_config_async(config_file).then((config) => {
1945
+ config_ref.current = config;
1946
+ console.log("[PdfViewer] Config loaded:", {
1947
+ append_timestamp_to_text_edits: config.viewer.append_timestamp_to_text_edits,
1948
+ config_object: config
1949
+ });
1950
+ }).catch((error2) => {
1951
+ console.warn(`[PdfViewer] Could not load config file "${config_file}", using defaults:`, error2);
1952
+ config_ref.current = load_pdf_config();
1953
+ });
1954
+ } else {
1955
+ config_ref.current = load_pdf_config(config_file);
1956
+ console.log("[PdfViewer] Config loaded (Node.js):", {
1957
+ append_timestamp_to_text_edits: config_ref.current?.viewer.append_timestamp_to_text_edits
1958
+ });
1959
+ }
1960
+ }, [config_file]);
1961
+ const effective_background_color = background_color || config_ref.current?.viewer.viewer_background_color || "#2d2d2d";
1962
+ const format_annotation_timestamp = () => {
1963
+ const now = /* @__PURE__ */ new Date();
1964
+ const year = now.getFullYear();
1965
+ const month = String(now.getMonth() + 1).padStart(2, "0");
1966
+ const day = String(now.getDate()).padStart(2, "0");
1967
+ let hours = now.getHours();
1968
+ const minutes = String(now.getMinutes()).padStart(2, "0");
1969
+ const am_pm = hours >= 12 ? "pm" : "am";
1970
+ hours = hours % 12;
1971
+ if (hours === 0) hours = 12;
1972
+ return `${year}-${month}-${day} ${hours}:${minutes}${am_pm}`;
1973
+ };
1974
+ const parse_custom_stamps = (stamps_json) => {
1975
+ if (!stamps_json || stamps_json.trim() === "") {
1976
+ return [];
1977
+ }
1978
+ try {
1979
+ const parsed = JSON.parse(stamps_json);
1980
+ if (!Array.isArray(parsed)) {
1981
+ console.warn("[PdfViewer] Custom stamps must be a JSON array");
1982
+ return [];
1983
+ }
1984
+ return parsed.filter((stamp) => stamp && typeof stamp === "object").map((stamp) => ({
1985
+ name: String(stamp.name || ""),
1986
+ text: String(stamp.text || ""),
1987
+ order: typeof stamp.order === "number" ? stamp.order : 999,
1988
+ time_stamp_suffix_enabled: Boolean(stamp.time_stamp_suffix_enabled),
1989
+ fixed_text_suffix_enabled: Boolean(stamp.fixed_text_suffix_enabled),
1990
+ // Optional styling fields
1991
+ background_color: stamp.background_color !== void 0 ? String(stamp.background_color) : void 0,
1992
+ border_size: stamp.border_size !== void 0 ? typeof stamp.border_size === "number" ? stamp.border_size : void 0 : void 0,
1993
+ font_color: stamp.font_color !== void 0 ? String(stamp.font_color) : void 0,
1994
+ font_weight: stamp.font_weight !== void 0 ? String(stamp.font_weight) : void 0,
1995
+ font_style: stamp.font_style !== void 0 ? String(stamp.font_style) : void 0,
1996
+ font_size: stamp.font_size !== void 0 ? typeof stamp.font_size === "number" ? stamp.font_size : void 0 : void 0,
1997
+ font_name: stamp.font_name !== void 0 ? String(stamp.font_name) : void 0
1998
+ })).filter((stamp) => stamp.name && stamp.text);
1999
+ } catch (error2) {
2000
+ console.warn("[PdfViewer] Failed to parse custom stamps JSON:", error2);
2001
+ return [];
2002
+ }
2003
+ };
2004
+ const add_suffix_text = (text, fixed_text_suffix_enabled, time_stamp_suffix_enabled, fixed_text, add_enclosing_brackets_override) => {
2005
+ const viewer_config = config_ref.current?.viewer;
2006
+ const add_enclosing_brackets = add_enclosing_brackets_override ?? viewer_config?.add_enclosing_brackets_to_suffixes ?? true;
2007
+ const suffix_enclosing_brackets = viewer_config?.suffix_enclosing_brackets || "[]";
2008
+ const suffix_text_position = viewer_config?.suffix_text_position || "below_multi_line";
2009
+ const opening_bracket = suffix_enclosing_brackets[0] || "[";
2010
+ const closing_bracket = suffix_enclosing_brackets[1] || "]";
2011
+ const suffix_parts = [];
2012
+ if (fixed_text_suffix_enabled) {
2013
+ const trimmed_fixed_text = fixed_text?.trim();
2014
+ if (trimmed_fixed_text && trimmed_fixed_text.length > 0) {
2015
+ suffix_parts.push(
2016
+ add_enclosing_brackets ? `${opening_bracket}${trimmed_fixed_text}${closing_bracket}` : trimmed_fixed_text
2017
+ );
2018
+ }
2019
+ }
2020
+ if (time_stamp_suffix_enabled) {
2021
+ const timestamp = format_annotation_timestamp();
2022
+ suffix_parts.push(
2023
+ add_enclosing_brackets ? `${opening_bracket}${timestamp}${closing_bracket}` : timestamp
2024
+ );
2025
+ }
2026
+ if (suffix_parts.length === 0) {
2027
+ return text;
2028
+ }
2029
+ switch (suffix_text_position) {
2030
+ case "adjacent": {
2031
+ if (!text) {
2032
+ return suffix_parts.join(" ");
2033
+ }
2034
+ const separator = text.endsWith(" ") ? "" : " ";
2035
+ return `${text}${separator}${suffix_parts.join(" ")}`;
2036
+ }
2037
+ case "below_single_line":
2038
+ return text ? `${text}
2039
+ ${suffix_parts.join(" ")}` : suffix_parts.join(" ");
2040
+ case "below_multi_line":
2041
+ default:
2042
+ return text ? `${text}
2043
+ ${suffix_parts.join("\n")}` : suffix_parts.join("\n");
2044
+ }
2045
+ };
2046
+ const format_stamp_text = (stamp, base_text) => {
2047
+ const fixed_text_prop = annotation_text_suffix_fixed_text;
2048
+ const fixed_text_config = config_ref.current?.viewer.annotation_text_suffix_fixed_text || "";
2049
+ const fixed_text = fixed_text_prop !== void 0 ? fixed_text_prop : fixed_text_config;
2050
+ return add_suffix_text(
2051
+ base_text,
2052
+ stamp.fixed_text_suffix_enabled ?? false,
2053
+ stamp.time_stamp_suffix_enabled ?? false,
2054
+ fixed_text
2055
+ );
2056
+ };
2057
+ const strip_auto_inserted_suffix = (text) => {
2058
+ const viewer_config = config_ref.current?.viewer;
2059
+ const bracket_pair = viewer_config?.suffix_enclosing_brackets || "[]";
2060
+ const add_enclosing_brackets = viewer_config?.add_enclosing_brackets_to_suffixes ?? true;
2061
+ const suffix_text_position = viewer_config?.suffix_text_position || "below_multi_line";
2062
+ const opening_bracket = bracket_pair[0] || "[";
2063
+ const closing_bracket = bracket_pair[1] || "]";
2064
+ const escape_regexp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2065
+ const timestamp_core_pattern = "\\d{4}-\\d{2}-\\d{2} \\d{1,2}:\\d{2}(?:am|pm)";
2066
+ const timestamp_pattern = add_enclosing_brackets ? `${escape_regexp(opening_bracket)}${timestamp_core_pattern}${escape_regexp(
2067
+ closing_bracket
2068
+ )}` : timestamp_core_pattern;
2069
+ const timestamp_line_regex = new RegExp(`^${timestamp_pattern}$`);
2070
+ const timestamp_trailing_regex = new RegExp(
2071
+ `(?:[ \\t]+)?${timestamp_pattern}$`
2072
+ );
2073
+ const fixed_text_prop = annotation_text_suffix_fixed_text;
2074
+ const fixed_text_config = viewer_config?.annotation_text_suffix_fixed_text || "";
2075
+ const fixed_text = fixed_text_prop !== void 0 ? fixed_text_prop : fixed_text_config;
2076
+ const trimmed_fixed_text = fixed_text?.trim() || "";
2077
+ const fixed_segment = trimmed_fixed_text ? add_enclosing_brackets ? `${opening_bracket}${trimmed_fixed_text}${closing_bracket}` : trimmed_fixed_text : null;
2078
+ const fixed_line_regex = fixed_segment ? new RegExp(`^${escape_regexp(fixed_segment)}$`) : null;
2079
+ const fixed_trailing_regex = fixed_segment ? new RegExp(`(?:[ \\t]+)?${escape_regexp(fixed_segment)}$`) : null;
2080
+ const remove_trailing_pattern = (value, pattern) => {
2081
+ if (!pattern) {
2082
+ return { updated: value, removed: false };
2083
+ }
2084
+ const new_value = value.replace(pattern, "");
2085
+ return { updated: new_value, removed: new_value !== value };
2086
+ };
2087
+ const strip_adjacent_suffix = (value) => {
2088
+ let updated = value;
2089
+ let removed_any = false;
2090
+ const timestamp_removal = remove_trailing_pattern(
2091
+ updated,
2092
+ timestamp_trailing_regex
2093
+ );
2094
+ updated = timestamp_removal.updated;
2095
+ removed_any || (removed_any = timestamp_removal.removed);
2096
+ const fixed_removal = remove_trailing_pattern(
2097
+ updated,
2098
+ fixed_trailing_regex
2099
+ );
2100
+ updated = fixed_removal.updated;
2101
+ removed_any || (removed_any = fixed_removal.removed);
2102
+ return removed_any ? updated.replace(/[ \\t]+$/, "") : value;
2103
+ };
2104
+ const strip_below_single_line_suffix = (value) => {
2105
+ const last_newline_index = value.lastIndexOf("\n");
2106
+ if (last_newline_index === -1) {
2107
+ return strip_adjacent_suffix(value);
2108
+ }
2109
+ const prefix = value.slice(0, last_newline_index);
2110
+ let suffix_line = value.slice(last_newline_index + 1);
2111
+ let suffix_changed = false;
2112
+ const timestamp_removal = remove_trailing_pattern(
2113
+ suffix_line,
2114
+ timestamp_trailing_regex
2115
+ );
2116
+ suffix_line = timestamp_removal.updated;
2117
+ suffix_changed || (suffix_changed = timestamp_removal.removed);
2118
+ const fixed_removal = remove_trailing_pattern(
2119
+ suffix_line,
2120
+ fixed_trailing_regex
2121
+ );
2122
+ suffix_line = fixed_removal.updated;
2123
+ suffix_changed || (suffix_changed = fixed_removal.removed);
2124
+ if (!suffix_changed) {
2125
+ return value;
2126
+ }
2127
+ if (suffix_line.trim().length === 0) {
2128
+ return prefix;
2129
+ }
2130
+ return `${prefix}
2131
+ ${suffix_line}`;
2132
+ };
2133
+ const strip_below_multi_line_suffix = (value) => {
2134
+ const lines = value.split("\n");
2135
+ if (lines.length === 0) {
2136
+ return value;
2137
+ }
2138
+ let changed = false;
2139
+ const remove_last_line_if_matches = (pattern) => {
2140
+ if (!pattern || lines.length === 0) {
2141
+ return;
2142
+ }
2143
+ const last_line = lines[lines.length - 1].trim();
2144
+ if (pattern.test(last_line)) {
2145
+ lines.pop();
2146
+ changed = true;
2147
+ }
2148
+ };
2149
+ remove_last_line_if_matches(timestamp_line_regex);
2150
+ remove_last_line_if_matches(fixed_line_regex);
2151
+ return changed ? lines.join("\n") : value;
2152
+ };
2153
+ switch (suffix_text_position) {
2154
+ case "adjacent":
2155
+ return strip_adjacent_suffix(text);
2156
+ case "below_single_line":
2157
+ return strip_below_single_line_suffix(text);
2158
+ case "below_multi_line":
2159
+ default:
2160
+ return strip_below_multi_line_suffix(text);
2161
+ }
2162
+ };
2163
+ const append_timestamp_if_enabled = (text) => {
2164
+ const prop_value = append_timestamp_to_text_edits;
2165
+ const config_value = config_ref.current?.viewer.append_timestamp_to_text_edits;
2166
+ const should_append = prop_value !== void 0 ? prop_value : config_value ?? false;
2167
+ const fixed_text_prop = annotation_text_suffix_fixed_text;
2168
+ const fixed_text_config = config_ref.current?.viewer.annotation_text_suffix_fixed_text || "";
2169
+ const fixed_text = fixed_text_prop !== void 0 ? fixed_text_prop : fixed_text_config;
2170
+ console.log("[PdfViewer] append_timestamp_if_enabled:", {
2171
+ prop_value,
2172
+ config_value,
2173
+ should_append,
2174
+ fixed_text_prop,
2175
+ fixed_text_config,
2176
+ fixed_text,
2177
+ config_loaded: !!config_ref.current,
2178
+ original_text: text
2179
+ });
2180
+ if (!should_append) {
2181
+ console.log("[PdfViewer] Timestamp NOT appended (disabled)");
2182
+ return text;
2183
+ }
2184
+ const include_fixed_text_suffix = Boolean(fixed_text && fixed_text.trim().length > 0);
2185
+ const result = add_suffix_text(
2186
+ text,
2187
+ include_fixed_text_suffix,
2188
+ true,
2189
+ fixed_text
2190
+ );
2191
+ console.log("[PdfViewer] Timestamp appended via add_suffix_text:", {
2192
+ original: text,
2193
+ fixed_text,
2194
+ include_fixed_text_suffix,
2195
+ result
2196
+ });
2197
+ return result;
2198
+ };
2199
+ const [history, setHistory] = useState5([initial_annotations]);
2200
+ const [history_index, setHistoryIndex] = useState5(0);
2201
+ const history_ref = useRef6({ saving: false });
2202
+ const [context_menu, setContextMenu] = useState5(null);
2203
+ const [text_dialog, setTextDialog] = useState5(null);
2204
+ useEffect5(() => {
2205
+ if (history.length === 1 && history[0].length === 0 && initial_annotations.length === 0) {
2206
+ return;
2207
+ }
2208
+ setHistory([initial_annotations]);
2209
+ setHistoryIndex(0);
2210
+ history_ref.current.saving = false;
2211
+ }, [url]);
2212
+ useEffect5(() => {
2213
+ if (typeof window === "undefined") {
2214
+ return;
2215
+ }
2216
+ if (!url) {
2217
+ console.warn("PdfViewer: No URL provided");
2218
+ return;
2219
+ }
2220
+ setLoading(true);
2221
+ setError(null);
2222
+ const load_timeout = setTimeout(() => {
2223
+ load_pdf_document(url).then((document2) => {
2224
+ setPdfDocument(document2);
2225
+ setLoading(false);
2226
+ if (on_load) {
2227
+ on_load(document2);
2228
+ }
2229
+ }).catch((err) => {
2230
+ console.error("PdfViewer: Error loading PDF:", err);
2231
+ const error_obj = err instanceof Error ? err : new Error(String(err));
2232
+ setError(error_obj);
2233
+ setLoading(false);
2234
+ if (on_error) {
2235
+ on_error(error_obj);
2236
+ }
2237
+ });
2238
+ }, 0);
2239
+ return () => {
2240
+ clearTimeout(load_timeout);
2241
+ };
2242
+ }, [url, on_load, on_error]);
2243
+ const save_to_history = (new_annotations) => {
2244
+ if (history_ref.current.saving) {
2245
+ return;
2246
+ }
2247
+ const new_history = history.slice(0, history_index + 1);
2248
+ new_history.push(new_annotations);
2249
+ if (new_history.length > 50) {
2250
+ new_history.shift();
2251
+ setHistory(new_history);
2252
+ setHistoryIndex(new_history.length - 1);
2253
+ } else {
2254
+ setHistory(new_history);
2255
+ setHistoryIndex(new_history.length - 1);
2256
+ }
2257
+ };
2258
+ const handle_annotation_create = (annotation) => {
2259
+ const new_annotations = [...annotations, annotation];
2260
+ setAnnotations(new_annotations);
2261
+ save_to_history(new_annotations);
2262
+ if (on_annotation_create) {
2263
+ on_annotation_create(annotation);
2264
+ }
2265
+ };
2266
+ const handle_annotation_update = (annotation) => {
2267
+ const updated_annotations = annotations.map(
2268
+ (ann) => ann.id === annotation.id ? annotation : ann
2269
+ );
2270
+ setAnnotations(updated_annotations);
2271
+ save_to_history(updated_annotations);
2272
+ if (on_annotation_update) {
2273
+ on_annotation_update(annotation);
2274
+ }
2275
+ };
2276
+ const handle_annotation_delete = (annotation_id) => {
2277
+ const filtered_annotations = annotations.filter(
2278
+ (ann) => ann.id !== annotation_id
2279
+ );
2280
+ setAnnotations(filtered_annotations);
2281
+ save_to_history(filtered_annotations);
2282
+ if (on_annotation_delete) {
2283
+ on_annotation_delete(annotation_id);
2284
+ }
2285
+ };
2286
+ const handle_undo = useCallback2(() => {
2287
+ if (history_index > 0) {
2288
+ history_ref.current.saving = true;
2289
+ const previous_index = history_index - 1;
2290
+ const previous_annotations = history[previous_index];
2291
+ setAnnotations([...previous_annotations]);
2292
+ setHistoryIndex(previous_index);
2293
+ setTimeout(() => {
2294
+ history_ref.current.saving = false;
2295
+ }, 0);
2296
+ }
2297
+ }, [history_index, history]);
2298
+ const handle_redo = useCallback2(() => {
2299
+ if (history_index < history.length - 1) {
2300
+ history_ref.current.saving = true;
2301
+ const next_index = history_index + 1;
2302
+ const next_annotations = history[next_index];
2303
+ setAnnotations([...next_annotations]);
2304
+ setHistoryIndex(next_index);
2305
+ setTimeout(() => {
2306
+ history_ref.current.saving = false;
2307
+ }, 0);
2308
+ }
2309
+ }, [history_index, history]);
2310
+ useEffect5(() => {
2311
+ const handle_keydown = (e) => {
2312
+ const target = e.target;
2313
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
2314
+ return;
2315
+ }
2316
+ if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
2317
+ e.preventDefault();
2318
+ handle_undo();
2319
+ }
2320
+ if ((e.ctrlKey || e.metaKey) && (e.key === "y" || e.key === "z" && e.shiftKey)) {
2321
+ e.preventDefault();
2322
+ handle_redo();
2323
+ }
2324
+ };
2325
+ window.addEventListener("keydown", handle_keydown);
2326
+ return () => {
2327
+ window.removeEventListener("keydown", handle_keydown);
2328
+ };
2329
+ }, [handle_undo, handle_redo]);
2330
+ const handle_zoom_in = () => {
2331
+ setScale((prev) => Math.min(prev + 0.25, 3));
2332
+ };
2333
+ const handle_zoom_out = () => {
2334
+ setScale((prev) => Math.max(prev - 0.25, 0.5));
2335
+ };
2336
+ const handle_zoom_reset = () => {
2337
+ setScale(1);
2338
+ };
2339
+ const handle_save = async () => {
2340
+ if (annotations.length === 0) {
2341
+ console.warn("PdfViewer: No annotations to save");
2342
+ return;
2343
+ }
2344
+ if (!url) {
2345
+ console.error("PdfViewer: No PDF URL available for saving");
2346
+ return;
2347
+ }
2348
+ setSaving(true);
2349
+ try {
2350
+ const original_filename = url.split("/").pop() || "document.pdf";
2351
+ const filename_without_ext = original_filename.replace(/\.pdf$/i, "");
2352
+ const output_filename = `${filename_without_ext}_annotated.pdf`;
2353
+ const { save_annotations_to_pdf: save_annotations_to_pdf2, download_pdf: download_pdf2 } = await import("./pdf_saver-D2H5SLPN.js");
2354
+ const pdf_bytes = await save_annotations_to_pdf2(url, annotations, output_filename, config_ref.current);
2355
+ download_pdf2(pdf_bytes, output_filename);
2356
+ if (on_save) {
2357
+ on_save(pdf_bytes, output_filename);
2358
+ }
2359
+ } catch (error2) {
2360
+ console.error("PdfViewer: Error saving PDF:", error2);
2361
+ const error_obj = error2 instanceof Error ? error2 : new Error(String(error2));
2362
+ if (on_error) {
2363
+ on_error(error_obj);
2364
+ }
2365
+ } finally {
2366
+ setSaving(false);
2367
+ }
2368
+ };
2369
+ if (loading) {
2370
+ return /* @__PURE__ */ jsx6("div", { className: cn("cls_pdf_viewer", "cls_pdf_viewer_loading", className), children: /* @__PURE__ */ jsx6("div", { className: "cls_pdf_viewer_spinner", children: "Loading PDF document..." }) });
2371
+ }
2372
+ if (error) {
2373
+ return /* @__PURE__ */ jsx6("div", { className: cn("cls_pdf_viewer", "cls_pdf_viewer_error", className), children: /* @__PURE__ */ jsxs5("div", { className: "cls_pdf_viewer_error_message", children: [
2374
+ "Error loading PDF: ",
2375
+ error.message
2376
+ ] }) });
2377
+ }
2378
+ if (!pdf_document) {
2379
+ return /* @__PURE__ */ jsx6("div", { className: cn("cls_pdf_viewer", className), children: /* @__PURE__ */ jsx6("div", { className: "cls_pdf_viewer_no_document", children: "No PDF document loaded" }) });
2380
+ }
2381
+ return /* @__PURE__ */ jsxs5("div", { className: cn("cls_pdf_viewer", className), children: [
2382
+ /* @__PURE__ */ jsxs5("div", { className: "cls_pdf_viewer_toolbar", children: [
2383
+ /* @__PURE__ */ jsxs5("div", { className: "cls_pdf_viewer_toolbar_group", children: [
2384
+ /* @__PURE__ */ jsx6(
2385
+ "button",
2386
+ {
2387
+ type: "button",
2388
+ onClick: handle_zoom_out,
2389
+ className: "cls_pdf_viewer_toolbar_button",
2390
+ "aria-label": "Zoom out",
2391
+ children: "\u2212"
2392
+ }
2393
+ ),
2394
+ /* @__PURE__ */ jsxs5("span", { className: "cls_pdf_viewer_zoom_level", children: [
2395
+ Math.round(scale * 100),
2396
+ "%"
2397
+ ] }),
2398
+ /* @__PURE__ */ jsx6(
2399
+ "button",
2400
+ {
2401
+ type: "button",
2402
+ onClick: handle_zoom_in,
2403
+ className: "cls_pdf_viewer_toolbar_button",
2404
+ "aria-label": "Zoom in",
2405
+ children: "+"
2406
+ }
2407
+ ),
2408
+ /* @__PURE__ */ jsx6(
2409
+ "button",
2410
+ {
2411
+ type: "button",
2412
+ onClick: handle_zoom_reset,
2413
+ className: "cls_pdf_viewer_toolbar_button",
2414
+ "aria-label": "Reset zoom",
2415
+ children: "Reset"
2416
+ }
2417
+ )
2418
+ ] }),
2419
+ /* @__PURE__ */ jsx6("div", { className: "cls_pdf_viewer_toolbar_group", children: /* @__PURE__ */ jsx6(
2420
+ "button",
2421
+ {
2422
+ type: "button",
2423
+ onClick: () => setCurrentTool("Square"),
2424
+ className: cn(
2425
+ "cls_pdf_viewer_toolbar_button",
2426
+ current_tool === "Square" && "cls_pdf_viewer_toolbar_button_active"
2427
+ ),
2428
+ "aria-label": "Square annotation tool",
2429
+ children: "Square"
2430
+ }
2431
+ ) }),
2432
+ /* @__PURE__ */ jsxs5("div", { className: "cls_pdf_viewer_toolbar_group", children: [
2433
+ /* @__PURE__ */ jsxs5(
2434
+ "button",
2435
+ {
2436
+ type: "button",
2437
+ onClick: handle_undo,
2438
+ disabled: history_index === 0,
2439
+ className: cn(
2440
+ "cls_pdf_viewer_toolbar_button",
2441
+ history_index === 0 && "cls_pdf_viewer_toolbar_button_disabled"
2442
+ ),
2443
+ "aria-label": "Undo last annotation",
2444
+ title: "Undo (Ctrl+Z)",
2445
+ children: [
2446
+ /* @__PURE__ */ jsx6(Undo22, { className: "cls_pdf_viewer_toolbar_icon", size: 16 }),
2447
+ /* @__PURE__ */ jsx6("span", { className: "cls_pdf_viewer_toolbar_button_text", children: "Undo" })
2448
+ ]
2449
+ }
2450
+ ),
2451
+ /* @__PURE__ */ jsxs5(
2452
+ "button",
2453
+ {
2454
+ type: "button",
2455
+ onClick: handle_redo,
2456
+ disabled: history_index >= history.length - 1,
2457
+ className: cn(
2458
+ "cls_pdf_viewer_toolbar_button",
2459
+ history_index >= history.length - 1 && "cls_pdf_viewer_toolbar_button_disabled"
2460
+ ),
2461
+ "aria-label": "Redo last undone annotation",
2462
+ title: "Redo (Ctrl+Y)",
2463
+ children: [
2464
+ /* @__PURE__ */ jsx6(Redo2, { className: "cls_pdf_viewer_toolbar_icon", size: 16 }),
2465
+ /* @__PURE__ */ jsx6("span", { className: "cls_pdf_viewer_toolbar_button_text", children: "Redo" })
2466
+ ]
2467
+ }
2468
+ )
2469
+ ] }),
2470
+ /* @__PURE__ */ jsx6("div", { className: "cls_pdf_viewer_toolbar_group", children: /* @__PURE__ */ jsxs5(
2471
+ "button",
2472
+ {
2473
+ type: "button",
2474
+ onClick: handle_save,
2475
+ disabled: saving || annotations.length === 0,
2476
+ className: cn(
2477
+ "cls_pdf_viewer_toolbar_button",
2478
+ "cls_pdf_viewer_toolbar_button_save",
2479
+ (saving || annotations.length === 0) && "cls_pdf_viewer_toolbar_button_disabled"
2480
+ ),
2481
+ "aria-label": "Save annotations to PDF",
2482
+ title: annotations.length === 0 ? "No annotations to save" : "Save annotations to PDF",
2483
+ children: [
2484
+ /* @__PURE__ */ jsx6(Save, { className: "cls_pdf_viewer_toolbar_icon", size: 16 }),
2485
+ /* @__PURE__ */ jsx6("span", { className: "cls_pdf_viewer_toolbar_button_text", children: saving ? "Saving..." : "Save" })
2486
+ ]
2487
+ }
2488
+ ) })
2489
+ ] }),
2490
+ /* @__PURE__ */ jsx6("div", { className: "cls_pdf_viewer_content", children: /* @__PURE__ */ jsx6(
2491
+ PdfViewerLayout,
2492
+ {
2493
+ pdf_document,
2494
+ scale,
2495
+ annotations,
2496
+ current_tool,
2497
+ background_color: effective_background_color,
2498
+ config: config_ref.current,
2499
+ on_annotation_create: handle_annotation_create,
2500
+ on_annotation_click: (annotation, screen_x, screen_y, mapper) => {
2501
+ console.log(
2502
+ `\u{1F7E0} [AnnotationClick] opening editor id=${annotation.id}, page=${annotation.page_index}, screen=(${screen_x.toFixed(
2503
+ 1
2504
+ )}, ${screen_y.toFixed(1)})`
2505
+ );
2506
+ const dialog_x = window.innerWidth / 2;
2507
+ const dialog_y = Math.max(50, screen_y);
2508
+ setTextDialog({
2509
+ open: true,
2510
+ page_index: annotation.page_index,
2511
+ x: dialog_x,
2512
+ y: dialog_y,
2513
+ screen_x,
2514
+ screen_y,
2515
+ mapper,
2516
+ editing_annotation: annotation
2517
+ });
2518
+ },
2519
+ on_context_menu: (e, page_index, screen_x, screen_y, mapper) => {
2520
+ const menu_x = e.clientX;
2521
+ const menu_y = e.clientY;
2522
+ setContextMenu({
2523
+ visible: true,
2524
+ x: menu_x,
2525
+ y: menu_y,
2526
+ page_index,
2527
+ screen_x,
2528
+ screen_y,
2529
+ mapper
2530
+ });
2531
+ }
2532
+ }
2533
+ ) }),
2534
+ context_menu?.visible && /* @__PURE__ */ jsxs5(Fragment3, { children: [
2535
+ /* @__PURE__ */ jsx6(
2536
+ "div",
2537
+ {
2538
+ className: "cls_pdf_viewer_context_menu_backdrop",
2539
+ onClick: () => setContextMenu(null),
2540
+ onContextMenu: (e) => {
2541
+ e.preventDefault();
2542
+ setContextMenu(null);
2543
+ }
2544
+ }
2545
+ ),
2546
+ /* @__PURE__ */ jsx6(
2547
+ ContextMenu,
2548
+ {
2549
+ x: context_menu.x,
2550
+ y: context_menu.y,
2551
+ can_undo: history_index > 0,
2552
+ config: config_ref.current,
2553
+ custom_stamps: (() => {
2554
+ const stamps_json = right_click_custom_stamps || config_ref.current?.context_menu.right_click_custom_stamps || "";
2555
+ return parse_custom_stamps(stamps_json);
2556
+ })(),
2557
+ on_undo: () => {
2558
+ handle_undo();
2559
+ setContextMenu(null);
2560
+ },
2561
+ on_annotate: () => {
2562
+ setTextDialog({
2563
+ open: true,
2564
+ page_index: context_menu.page_index,
2565
+ x: context_menu.x,
2566
+ // Use same viewport coordinates as context menu
2567
+ y: context_menu.y,
2568
+ screen_x: context_menu.screen_x,
2569
+ screen_y: context_menu.screen_y,
2570
+ mapper: context_menu.mapper
2571
+ });
2572
+ setContextMenu(null);
2573
+ },
2574
+ on_stamp_click: (stamp) => {
2575
+ if (!context_menu || !context_menu.mapper) return;
2576
+ const formatted_text = format_stamp_text(stamp, stamp.text);
2577
+ const [pdf_x, pdf_y] = context_menu.mapper.to_pdf(context_menu.screen_x, context_menu.screen_y);
2578
+ const fonts_config = config_ref.current?.fonts || default_config.fonts;
2579
+ const freetext_config = config_ref.current?.freetext_annotation || default_config.freetext_annotation;
2580
+ const text_color = stamp.font_color || (freetext_config.freetext_text_color && freetext_config.freetext_text_color !== "#000000" ? freetext_config.freetext_text_color : fonts_config.font_foreground_color);
2581
+ const stamp_styling = {
2582
+ stamp_name: stamp.name,
2583
+ background_color: stamp.background_color,
2584
+ border_size: stamp.border_size,
2585
+ font_color: stamp.font_color,
2586
+ font_weight: stamp.font_weight,
2587
+ font_style: stamp.font_style,
2588
+ font_size: stamp.font_size,
2589
+ font_name: stamp.font_name
2590
+ };
2591
+ const annotation = {
2592
+ id: crypto.randomUUID(),
2593
+ type: "FreeText",
2594
+ page_index: context_menu.page_index,
2595
+ rect: [pdf_x, pdf_y, pdf_x + 10, pdf_y + 10],
2596
+ // Placeholder rect
2597
+ author: "User",
2598
+ date: (/* @__PURE__ */ new Date()).toISOString(),
2599
+ contents: formatted_text,
2600
+ color: text_color,
2601
+ subject: JSON.stringify(stamp_styling)
2602
+ // Store stamp styling metadata
2603
+ };
2604
+ handle_annotation_create(annotation);
2605
+ setContextMenu(null);
2606
+ },
2607
+ on_close: () => setContextMenu(null)
2608
+ }
2609
+ )
2610
+ ] }),
2611
+ text_dialog && /* @__PURE__ */ jsx6(
2612
+ TextAnnotationDialog,
2613
+ {
2614
+ open: text_dialog.open,
2615
+ x: text_dialog.x,
2616
+ y: text_dialog.y,
2617
+ config: config_ref.current,
2618
+ initial_text: text_dialog.editing_annotation ? strip_auto_inserted_suffix(text_dialog.editing_annotation.contents) : "",
2619
+ is_editing: !!text_dialog.editing_annotation,
2620
+ on_close: () => setTextDialog(null),
2621
+ on_delete: () => {
2622
+ if (text_dialog.editing_annotation) {
2623
+ handle_annotation_delete(text_dialog.editing_annotation.id);
2624
+ }
2625
+ setTextDialog(null);
2626
+ },
2627
+ on_submit: (text) => {
2628
+ if (!text_dialog || !text_dialog.mapper) return;
2629
+ if (text_dialog.editing_annotation) {
2630
+ const stripped_text = strip_auto_inserted_suffix(text);
2631
+ const final_text2 = append_timestamp_if_enabled(stripped_text);
2632
+ const updated_annotation = {
2633
+ ...text_dialog.editing_annotation,
2634
+ contents: final_text2,
2635
+ date: (/* @__PURE__ */ new Date()).toISOString()
2636
+ // Update modification date
2637
+ };
2638
+ handle_annotation_update(updated_annotation);
2639
+ setTextDialog(null);
2640
+ return;
2641
+ }
2642
+ const final_text = append_timestamp_if_enabled(text);
2643
+ const [pdf_x, pdf_y] = text_dialog.mapper.to_pdf(text_dialog.screen_x, text_dialog.screen_y);
2644
+ const placeholder_width = 100;
2645
+ const placeholder_height = 30;
2646
+ const freetext_text_color = config_ref.current?.freetext_annotation.freetext_text_color;
2647
+ const font_foreground_color = config_ref.current?.fonts.font_foreground_color;
2648
+ const text_color = freetext_text_color && freetext_text_color !== "#000000" ? freetext_text_color : font_foreground_color || "#000000";
2649
+ const annotation = {
2650
+ id: crypto.randomUUID(),
2651
+ type: "FreeText",
2652
+ page_index: text_dialog.page_index,
2653
+ rect: [
2654
+ pdf_x,
2655
+ // Top-left X (click position)
2656
+ pdf_y,
2657
+ // Top-left Y (click position)
2658
+ pdf_x + placeholder_width,
2659
+ // Bottom-right X (placeholder)
2660
+ pdf_y + placeholder_height
2661
+ // Bottom-right Y (placeholder)
2662
+ ],
2663
+ author: "User",
2664
+ date: (/* @__PURE__ */ new Date()).toISOString(),
2665
+ contents: final_text,
2666
+ color: text_color
2667
+ };
2668
+ handle_annotation_create(annotation);
2669
+ setTextDialog(null);
2670
+ }
2671
+ }
2672
+ )
2673
+ ] });
2674
+ };
2675
+
2676
+ // src/utils/xfdf_generator.ts
2677
+ function format_pdf_date(date) {
2678
+ let date_obj;
2679
+ if (typeof date === "string") {
2680
+ date_obj = new Date(date);
2681
+ if (isNaN(date_obj.getTime())) {
2682
+ console.warn(`Invalid date string: ${date}. Using current date.`);
2683
+ date_obj = /* @__PURE__ */ new Date();
2684
+ }
2685
+ } else {
2686
+ date_obj = date;
2687
+ if (isNaN(date_obj.getTime())) {
2688
+ console.warn(`Invalid date object. Using current date.`);
2689
+ date_obj = /* @__PURE__ */ new Date();
2690
+ }
2691
+ }
2692
+ const year = date_obj.getFullYear();
2693
+ const month = String(date_obj.getMonth() + 1).padStart(2, "0");
2694
+ const day = String(date_obj.getDate()).padStart(2, "0");
2695
+ const hours = String(date_obj.getHours()).padStart(2, "0");
2696
+ const minutes = String(date_obj.getMinutes()).padStart(2, "0");
2697
+ const seconds = String(date_obj.getSeconds()).padStart(2, "0");
2698
+ const offset_minutes = date_obj.getTimezoneOffset();
2699
+ const offset_hours = Math.abs(Math.floor(offset_minutes / 60));
2700
+ const offset_mins = Math.abs(offset_minutes % 60);
2701
+ const offset_sign = offset_minutes <= 0 ? "+" : "-";
2702
+ return `D:${year}${month}${day}${hours}${minutes}${seconds}${offset_sign}${String(offset_hours).padStart(2, "0")}'${String(offset_mins).padStart(2, "0")}'`;
2703
+ }
2704
+ function escape_xml(text) {
2705
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2706
+ }
2707
+ function annotation_type_to_tag(type) {
2708
+ return type.toLowerCase();
2709
+ }
2710
+ function generate_xfdf(annotations, bookmarks = [], pdf_file_name = "document.pdf") {
2711
+ let xfdf = `<?xml version="1.0" encoding="UTF-8"?>
2712
+ `;
2713
+ xfdf += `<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
2714
+ `;
2715
+ xfdf += ` <f href="${escape_xml(pdf_file_name)}"/>
2716
+ `;
2717
+ if (annotations.length > 0) {
2718
+ xfdf += ` <annots>
2719
+ `;
2720
+ annotations.forEach((ann) => {
2721
+ const rect_string = ann.rect.map((c) => c.toFixed(2)).join(", ");
2722
+ let date_string;
2723
+ try {
2724
+ date_string = format_pdf_date(ann.date);
2725
+ } catch (error) {
2726
+ console.warn(`Error formatting date for annotation ${ann.id}:`, error);
2727
+ date_string = format_pdf_date(/* @__PURE__ */ new Date());
2728
+ }
2729
+ const tag_name = annotation_type_to_tag(ann.type);
2730
+ const attributes = [];
2731
+ attributes.push(`subject="${escape_xml(ann.subject || ann.type)}"`);
2732
+ attributes.push(`page="${ann.page_index}"`);
2733
+ attributes.push(`rect="${rect_string}"`);
2734
+ attributes.push(`flags="print"`);
2735
+ attributes.push(`name="${escape_xml(ann.id)}"`);
2736
+ attributes.push(`title="${escape_xml(ann.author)}"`);
2737
+ attributes.push(`date="${date_string}"`);
2738
+ if (ann.color) {
2739
+ attributes.push(`color="${escape_xml(ann.color)}"`);
2740
+ }
2741
+ xfdf += ` <${tag_name} ${attributes.join(" ")}>
2742
+ `;
2743
+ if (ann.contents) {
2744
+ xfdf += ` <contents><![CDATA[${ann.contents}]]></contents>
2745
+ `;
2746
+ }
2747
+ xfdf += ` </${tag_name}>
2748
+ `;
2749
+ });
2750
+ xfdf += ` </annots>
2751
+ `;
2752
+ }
2753
+ if (bookmarks.length > 0) {
2754
+ xfdf += ` <bookmarks>
2755
+ `;
2756
+ bookmarks.forEach((bookmark) => {
2757
+ const attributes = [];
2758
+ attributes.push(`title="${escape_xml(bookmark.title)}"`);
2759
+ attributes.push(`action="${bookmark.action || "GoTo"}"`);
2760
+ attributes.push(`page="${bookmark.page_index}"`);
2761
+ if (bookmark.y !== void 0) {
2762
+ attributes.push(`y="${bookmark.y}"`);
2763
+ }
2764
+ xfdf += ` <bookmark ${attributes.join(" ")} />
2765
+ `;
2766
+ });
2767
+ xfdf += ` </bookmarks>
2768
+ `;
2769
+ }
2770
+ xfdf += `</xfdf>`;
2771
+ return xfdf;
2772
+ }
2773
+ function download_xfdf(xfdf_content, file_name = "annotations.xfdf") {
2774
+ const blob = new Blob([xfdf_content], { type: "application/vnd.adobe.xfdf" });
2775
+ const url = URL.createObjectURL(blob);
2776
+ const link = document.createElement("a");
2777
+ link.href = url;
2778
+ link.download = file_name;
2779
+ document.body.appendChild(link);
2780
+ link.click();
2781
+ document.body.removeChild(link);
2782
+ URL.revokeObjectURL(url);
2783
+ }
2784
+ function export_annotations_to_xfdf(annotations, bookmarks = [], pdf_file_name = "document.pdf", file_name = "annotations.xfdf") {
2785
+ const xfdf_content = generate_xfdf(annotations, bookmarks, pdf_file_name);
2786
+ download_xfdf(xfdf_content, file_name);
2787
+ }
2788
+ export {
2789
+ AnnotationOverlay,
2790
+ PdfPageRenderer,
2791
+ PdfViewer,
2792
+ PdfViewerLayout,
2793
+ build_config_from_ini,
2794
+ calculate_rectangle_coords,
2795
+ create_coordinate_mapper,
2796
+ default_config,
2797
+ download_pdf,
2798
+ download_xfdf,
2799
+ export_annotations_to_xfdf,
2800
+ generate_xfdf,
2801
+ get_viewport_dimensions,
2802
+ is_rectangle_too_small,
2803
+ load_pdf_config,
2804
+ load_pdf_config_async,
2805
+ load_pdf_document,
2806
+ parse_color,
2807
+ parse_number,
2808
+ parse_opacity,
2809
+ parse_string,
2810
+ pdf_rect_to_rectangle,
2811
+ rectangle_to_pdf_rect,
2812
+ save_and_download_pdf,
2813
+ save_annotations_to_pdf
2814
+ };
2815
+ //# sourceMappingURL=index.js.map