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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/chunk-2POHPGR3.js +283 -0
- package/dist/chunk-2POHPGR3.js.map +1 -0
- package/dist/index.d.ts +593 -0
- package/dist/index.js +2815 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf_saver-D2H5SLPN.js +12 -0
- package/dist/pdf_saver-D2H5SLPN.js.map +1 -0
- package/dist/styles/index.css +1789 -0
- package/dist/styles/index.css.map +1 -0
- package/package.json +94 -0
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|