retold-remote 0.0.22 → 0.0.25

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.
Files changed (47) hide show
  1. package/css/retold-remote.css +87 -20
  2. package/docs/README.md +59 -11
  3. package/docs/_sidebar.md +1 -0
  4. package/docs/collections.md +30 -0
  5. package/docs/ebook-reader.md +75 -1
  6. package/docs/image-explorer.md +27 -1
  7. package/docs/server-setup.md +28 -18
  8. package/docs/stack-launcher.md +218 -0
  9. package/docs/ultravisor-integration.md +2 -0
  10. package/package.json +10 -7
  11. package/source/Pict-Application-RetoldRemote.js +2 -0
  12. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  13. package/source/cli/RetoldRemote-Server-Setup.js +240 -2
  14. package/source/cli/RetoldRemote-Stack-Launcher.js +387 -0
  15. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  16. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  17. package/source/providers/CollectionManager-AddItems.js +166 -0
  18. package/source/providers/Pict-Provider-GalleryNavigation.js +46 -0
  19. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +5 -0
  20. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  21. package/source/server/RetoldRemote-CollectionExportService.js +696 -0
  22. package/source/server/RetoldRemote-CollectionService.js +5 -0
  23. package/source/server/RetoldRemote-EbookService.js +194 -3
  24. package/source/server/RetoldRemote-SubimageService.js +530 -0
  25. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  26. package/source/server/RetoldRemote-UltravisorOperations.js +6 -6
  27. package/source/views/MediaViewer-EbookViewer.js +419 -1
  28. package/source/views/MediaViewer-PdfViewer.js +963 -0
  29. package/source/views/PictView-Remote-CollectionsPanel.js +166 -0
  30. package/source/views/PictView-Remote-ImageExplorer.js +606 -1
  31. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  32. package/source/views/PictView-Remote-Layout.js +12 -0
  33. package/source/views/PictView-Remote-MediaViewer.js +83 -25
  34. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  35. package/web-application/css/retold-remote.css +87 -20
  36. package/web-application/docs/README.md +59 -11
  37. package/web-application/docs/_sidebar.md +1 -0
  38. package/web-application/docs/collections.md +30 -0
  39. package/web-application/docs/ebook-reader.md +75 -1
  40. package/web-application/docs/image-explorer.md +27 -1
  41. package/web-application/docs/server-setup.md +28 -18
  42. package/web-application/docs/stack-launcher.md +218 -0
  43. package/web-application/docs/ultravisor-integration.md +2 -0
  44. package/web-application/retold-remote.js +399 -45
  45. package/web-application/retold-remote.js.map +1 -1
  46. package/web-application/retold-remote.min.js +13 -12
  47. package/web-application/retold-remote.min.js.map +1 -1
@@ -0,0 +1,963 @@
1
+ /**
2
+ * MediaViewer — PDF Viewer Mixin
3
+ *
4
+ * Full pdf.js canvas renderer with text layer, page navigation,
5
+ * zoom controls, text selection saving, and visual region selection.
6
+ *
7
+ * Mixed into RetoldRemoteMediaViewerView.prototype via Object.assign().
8
+ * All methods access state through `this` (the view instance).
9
+ *
10
+ * @license MIT
11
+ */
12
+
13
+ const _PDF_JS_CDN_URL = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.min.mjs';
14
+ const _PDF_JS_WORKER_CDN_URL = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.worker.min.mjs';
15
+
16
+ module.exports =
17
+ {
18
+ /**
19
+ * Build the HTML shell for the PDF viewer.
20
+ *
21
+ * @param {string} pURL - Content URL for the file
22
+ * @param {string} pFileName - Display file name
23
+ * @param {string} pFilePath - Relative file path
24
+ * @returns {string} HTML string
25
+ */
26
+ _buildPdfHTML: function _buildPdfHTML(pURL, pFileName, pFilePath)
27
+ {
28
+ let tmpViewRef = "pict.views['RetoldRemote-MediaViewer']";
29
+
30
+ let tmpHTML = '<div class="retold-remote-pdf-viewer">';
31
+
32
+ // Controls bar
33
+ tmpHTML += '<div class="retold-remote-pdf-controls" id="RetoldRemote-PdfControls">';
34
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfPrevPage()" title="Previous page">&larr; Prev</button>';
35
+ tmpHTML += '<span class="retold-remote-pdf-page-info">';
36
+ tmpHTML += 'Page <input type="number" id="RetoldRemote-PdfPageInput" class="retold-remote-pdf-page-input" value="1" min="1" '
37
+ + 'onchange="' + tmpViewRef + '.pdfGoToPage(parseInt(this.value,10))" '
38
+ + 'onkeydown="if(event.key===\'Enter\'){' + tmpViewRef + '.pdfGoToPage(parseInt(this.value,10));event.preventDefault();}">';
39
+ tmpHTML += ' of <span id="RetoldRemote-PdfPageCount">0</span>';
40
+ tmpHTML += '</span>';
41
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfNextPage()" title="Next page">Next &rarr;</button>';
42
+ tmpHTML += '<span class="retold-remote-pdf-separator"></span>';
43
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfSaveSelection()" title="Save selected text">&#128190; Save Selection</button>';
44
+ tmpHTML += '<button class="retold-remote-pdf-btn" id="RetoldRemote-PdfRegionBtn" onclick="' + tmpViewRef + '.pdfToggleRegionSelect()" title="Select a visual region">&#9986; Select Region</button>';
45
+ tmpHTML += '<span class="retold-remote-pdf-separator"></span>';
46
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfZoomIn()" title="Zoom in (+)">+ Zoom In</button>';
47
+ tmpHTML += '<span class="retold-remote-pdf-zoom-label" id="RetoldRemote-PdfZoomLabel">150%</span>';
48
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfZoomOut()" title="Zoom out (-)">- Zoom Out</button>';
49
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfZoomFit()" title="Fit to width">Fit</button>';
50
+ tmpHTML += '</div>';
51
+
52
+ // Content area
53
+ tmpHTML += '<div class="retold-remote-pdf-content" id="RetoldRemote-PdfContent">';
54
+ tmpHTML += '<div class="retold-remote-pdf-wrap" id="RetoldRemote-PdfWrap">';
55
+ tmpHTML += '<canvas id="RetoldRemote-PdfCanvas"></canvas>';
56
+ tmpHTML += '<div id="RetoldRemote-PdfTextLayer" class="retold-remote-pdf-text-layer"></div>';
57
+ tmpHTML += '<div id="RetoldRemote-PdfSelectionOverlay" class="retold-remote-pdf-selection-overlay" style="display:none;"></div>';
58
+ tmpHTML += '<div id="RetoldRemote-PdfRegionOverlays" class="retold-remote-pdf-region-overlays"></div>';
59
+ tmpHTML += '</div>';
60
+ tmpHTML += '<div class="retold-remote-pdf-loading" id="RetoldRemote-PdfLoading">Loading PDF...</div>';
61
+ tmpHTML += '</div>';
62
+
63
+ // Inline label input (hidden until needed)
64
+ tmpHTML += '<div class="retold-remote-pdf-label-bar" id="RetoldRemote-PdfLabelInput" style="display:none;">';
65
+ tmpHTML += '<input type="text" id="RetoldRemote-PdfLabelField" class="retold-remote-pdf-label-field" placeholder="Label this selection\u2026" '
66
+ + 'onkeydown="if(event.key===\'Enter\'){' + tmpViewRef + '.pdfSaveLabel();event.preventDefault();event.stopPropagation();}'
67
+ + 'if(event.key===\'Escape\'){' + tmpViewRef + '.pdfCancelSelection();event.preventDefault();event.stopPropagation();}">';
68
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfSaveLabel()">Save</button>';
69
+ tmpHTML += '<button class="retold-remote-pdf-btn" onclick="' + tmpViewRef + '.pdfCancelSelection()">Cancel</button>';
70
+ tmpHTML += '</div>';
71
+
72
+ tmpHTML += '</div>';
73
+
74
+ // Inline styles for the PDF viewer
75
+ tmpHTML += '<style>';
76
+ tmpHTML += '.retold-remote-pdf-viewer { display: flex; flex-direction: column; height: 100%; overflow: hidden; }';
77
+ tmpHTML += '.retold-remote-pdf-controls { display: flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--retold-bg-secondary, #252526); border-bottom: 1px solid var(--retold-border, #3e4451); flex-shrink: 0; flex-wrap: wrap; }';
78
+ tmpHTML += '.retold-remote-pdf-btn { background: var(--retold-bg-tertiary, #2d2d2d); color: var(--retold-text-primary, #abb2bf); border: 1px solid var(--retold-border, #3e4451); border-radius: 4px; padding: 4px 10px; font-size: 0.78rem; cursor: pointer; white-space: nowrap; }';
79
+ tmpHTML += '.retold-remote-pdf-btn:hover { background: var(--retold-bg-hover, #3e4451); }';
80
+ tmpHTML += '.retold-remote-pdf-btn.active { background: var(--retold-accent, #569cd6); color: #fff; }';
81
+ tmpHTML += '.retold-remote-pdf-page-info { font-size: 0.78rem; color: var(--retold-text-secondary, #8b949e); display: flex; align-items: center; gap: 4px; }';
82
+ tmpHTML += '.retold-remote-pdf-page-input { width: 48px; background: var(--retold-bg-input, #1e1e1e); color: var(--retold-text-primary, #abb2bf); border: 1px solid var(--retold-border, #3e4451); border-radius: 4px; padding: 2px 6px; font-size: 0.78rem; text-align: center; }';
83
+ tmpHTML += '.retold-remote-pdf-separator { width: 1px; height: 20px; background: var(--retold-border, #3e4451); margin: 0 4px; }';
84
+ tmpHTML += '.retold-remote-pdf-zoom-label { font-size: 0.78rem; color: var(--retold-text-secondary, #8b949e); min-width: 40px; text-align: center; }';
85
+ tmpHTML += '.retold-remote-pdf-content { flex: 1; overflow: auto; position: relative; background: var(--retold-bg-primary, #1e1e1e); }';
86
+ tmpHTML += '.retold-remote-pdf-wrap { position: relative; display: inline-block; margin: 16px auto; }';
87
+ tmpHTML += '.retold-remote-pdf-content { text-align: center; }';
88
+ tmpHTML += '.retold-remote-pdf-text-layer { position: absolute; top: 0; left: 0; overflow: hidden; opacity: 0.25; line-height: 1.0; }';
89
+ tmpHTML += '.retold-remote-pdf-text-layer > span { position: absolute; white-space: pre; color: transparent; }';
90
+ tmpHTML += '.retold-remote-pdf-text-layer ::selection { background: rgba(86, 156, 214, 0.4); }';
91
+ tmpHTML += '.retold-remote-pdf-selection-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: crosshair; z-index: 5; }';
92
+ tmpHTML += '.retold-remote-pdf-region-overlays { position: absolute; top: 0; left: 0; pointer-events: none; }';
93
+ tmpHTML += '.retold-remote-pdf-region-rect { position: absolute; border: 2px solid rgba(86, 156, 214, 0.8); background: rgba(86, 156, 214, 0.12); pointer-events: none; }';
94
+ tmpHTML += '.retold-remote-pdf-region-label { position: absolute; bottom: -18px; left: 0; font-size: 0.65rem; color: var(--retold-accent, #569cd6); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px; pointer-events: none; }';
95
+ tmpHTML += '.retold-remote-pdf-active-rect { position: absolute; border: 2px dashed rgba(214, 156, 86, 0.9); background: rgba(214, 156, 86, 0.15); pointer-events: none; }';
96
+ tmpHTML += '.retold-remote-pdf-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 0.9rem; color: var(--retold-text-secondary, #8b949e); }';
97
+ tmpHTML += '.retold-remote-pdf-label-bar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--retold-bg-secondary, #252526); border-top: 1px solid var(--retold-border, #3e4451); flex-shrink: 0; }';
98
+ tmpHTML += '.retold-remote-pdf-label-field { flex: 1; background: var(--retold-bg-input, #1e1e1e); color: var(--retold-text-primary, #abb2bf); border: 1px solid var(--retold-border, #3e4451); border-radius: 4px; padding: 4px 10px; font-size: 0.78rem; }';
99
+ tmpHTML += '</style>';
100
+
101
+ return tmpHTML;
102
+ },
103
+
104
+ /**
105
+ * Load the pdf.js library from CDN (if needed) and open a PDF document.
106
+ *
107
+ * @param {string} pContentURL - URL to fetch the PDF from
108
+ * @param {string} pFilePath - Relative file path
109
+ */
110
+ _loadPdfViewer: function _loadPdfViewer(pContentURL, pFilePath)
111
+ {
112
+ let tmpSelf = this;
113
+
114
+ // Initialize instance state
115
+ this._pdfDocument = null;
116
+ this._pdfCurrentPage = 1;
117
+ this._pdfPageCount = 0;
118
+ this._pdfScale = 1.5;
119
+ this._pdfSelectionMode = false;
120
+ this._pdfSavedRegions = [];
121
+ this._pdfPendingRegion = null;
122
+ this._pdfPendingText = null;
123
+ this._pdfLibLoaded = false;
124
+ this._pdfFilePath = pFilePath;
125
+
126
+ this._ensurePdfJsLoaded(function ()
127
+ {
128
+ tmpSelf._pdfLibLoaded = true;
129
+ tmpSelf._openPdfDocument(pContentURL);
130
+ });
131
+ },
132
+
133
+ /**
134
+ * Ensure pdf.js is loaded from CDN via dynamic import.
135
+ *
136
+ * @param {Function} fCallback - Called when pdf.js is ready
137
+ */
138
+ _ensurePdfJsLoaded: function _ensurePdfJsLoaded(fCallback)
139
+ {
140
+ if (typeof window !== 'undefined' && window.pdfjsLib)
141
+ {
142
+ return fCallback();
143
+ }
144
+
145
+ let tmpSelf = this;
146
+ let tmpLoadingEl = document.getElementById('RetoldRemote-PdfLoading');
147
+ if (tmpLoadingEl)
148
+ {
149
+ tmpLoadingEl.textContent = 'Loading PDF renderer...';
150
+ }
151
+
152
+ import(_PDF_JS_CDN_URL)
153
+ .then(function (pModule)
154
+ {
155
+ window.pdfjsLib = pModule;
156
+ window.pdfjsLib.GlobalWorkerOptions.workerSrc = _PDF_JS_WORKER_CDN_URL;
157
+ fCallback();
158
+ })
159
+ .catch(function (pError)
160
+ {
161
+ let tmpEl = document.getElementById('RetoldRemote-PdfLoading');
162
+ if (tmpEl)
163
+ {
164
+ tmpEl.textContent = 'Failed to load PDF renderer: ' + pError.message;
165
+ }
166
+ });
167
+ },
168
+
169
+ /**
170
+ * Open a PDF document from a URL using pdf.js.
171
+ *
172
+ * @param {string} pContentURL - URL to fetch the PDF from
173
+ */
174
+ _openPdfDocument: function _openPdfDocument(pContentURL)
175
+ {
176
+ let tmpSelf = this;
177
+ let tmpLoadingEl = document.getElementById('RetoldRemote-PdfLoading');
178
+ if (tmpLoadingEl)
179
+ {
180
+ tmpLoadingEl.textContent = 'Opening PDF...';
181
+ }
182
+
183
+ let tmpLoadingTask = window.pdfjsLib.getDocument(pContentURL);
184
+ tmpLoadingTask.promise
185
+ .then(function (pDocument)
186
+ {
187
+ tmpSelf._pdfDocument = pDocument;
188
+ tmpSelf._pdfPageCount = pDocument.numPages;
189
+ tmpSelf._pdfCurrentPage = 1;
190
+
191
+ // Update the page count display
192
+ let tmpCountEl = document.getElementById('RetoldRemote-PdfPageCount');
193
+ if (tmpCountEl)
194
+ {
195
+ tmpCountEl.textContent = pDocument.numPages;
196
+ }
197
+
198
+ let tmpPageInput = document.getElementById('RetoldRemote-PdfPageInput');
199
+ if (tmpPageInput)
200
+ {
201
+ tmpPageInput.max = pDocument.numPages;
202
+ }
203
+
204
+ // Hide loading indicator
205
+ if (tmpLoadingEl)
206
+ {
207
+ tmpLoadingEl.style.display = 'none';
208
+ }
209
+
210
+ // Render the first page
211
+ tmpSelf._renderPdfPage(1);
212
+
213
+ // Load saved regions
214
+ tmpSelf._pdfLoadSavedRegions(tmpSelf._pdfFilePath);
215
+ })
216
+ .catch(function (pError)
217
+ {
218
+ if (tmpLoadingEl)
219
+ {
220
+ tmpLoadingEl.textContent = 'Failed to open PDF: ' + pError.message;
221
+ }
222
+ });
223
+ },
224
+
225
+ /**
226
+ * Render a specific page of the PDF onto the canvas.
227
+ *
228
+ * @param {number} pPageNumber - 1-based page number
229
+ */
230
+ _renderPdfPage: function _renderPdfPage(pPageNumber)
231
+ {
232
+ if (!this._pdfDocument)
233
+ {
234
+ return;
235
+ }
236
+
237
+ if (pPageNumber < 1 || pPageNumber > this._pdfPageCount)
238
+ {
239
+ return;
240
+ }
241
+
242
+ let tmpSelf = this;
243
+ this._pdfCurrentPage = pPageNumber;
244
+
245
+ this._pdfDocument.getPage(pPageNumber)
246
+ .then(function (pPage)
247
+ {
248
+ let tmpViewport = pPage.getViewport({ scale: tmpSelf._pdfScale });
249
+
250
+ let tmpCanvas = document.getElementById('RetoldRemote-PdfCanvas');
251
+ if (!tmpCanvas)
252
+ {
253
+ return;
254
+ }
255
+ let tmpContext = tmpCanvas.getContext('2d');
256
+
257
+ // Set canvas dimensions to match the viewport
258
+ tmpCanvas.width = tmpViewport.width;
259
+ tmpCanvas.height = tmpViewport.height;
260
+
261
+ // Set the wrap container dimensions
262
+ let tmpWrap = document.getElementById('RetoldRemote-PdfWrap');
263
+ if (tmpWrap)
264
+ {
265
+ tmpWrap.style.width = tmpViewport.width + 'px';
266
+ tmpWrap.style.height = tmpViewport.height + 'px';
267
+ }
268
+
269
+ // Render the page onto the canvas
270
+ let tmpRenderContext =
271
+ {
272
+ canvasContext: tmpContext,
273
+ viewport: tmpViewport
274
+ };
275
+
276
+ pPage.render(tmpRenderContext).promise
277
+ .then(function ()
278
+ {
279
+ // After canvas renders, build the text layer
280
+ tmpSelf._renderPdfTextLayer(pPage, tmpViewport);
281
+
282
+ // Render saved region overlays for this page
283
+ tmpSelf._pdfRenderRegionOverlays();
284
+ });
285
+
286
+ // Update the page number input
287
+ let tmpPageInput = document.getElementById('RetoldRemote-PdfPageInput');
288
+ if (tmpPageInput)
289
+ {
290
+ tmpPageInput.value = pPageNumber;
291
+ }
292
+
293
+ // Update the zoom label
294
+ tmpSelf._pdfUpdateZoomLabel();
295
+ })
296
+ .catch(function (pError)
297
+ {
298
+ let tmpLoadingEl = document.getElementById('RetoldRemote-PdfLoading');
299
+ if (tmpLoadingEl)
300
+ {
301
+ tmpLoadingEl.style.display = '';
302
+ tmpLoadingEl.textContent = 'Failed to render page: ' + pError.message;
303
+ }
304
+ });
305
+ },
306
+
307
+ /**
308
+ * Render the text layer on top of the canvas so text can be selected.
309
+ *
310
+ * @param {Object} pPage - pdf.js page object
311
+ * @param {Object} pViewport - pdf.js viewport for the current scale
312
+ */
313
+ _renderPdfTextLayer: function _renderPdfTextLayer(pPage, pViewport)
314
+ {
315
+ let tmpTextLayerEl = document.getElementById('RetoldRemote-PdfTextLayer');
316
+ if (!tmpTextLayerEl)
317
+ {
318
+ return;
319
+ }
320
+
321
+ // Clear previous text layer content
322
+ tmpTextLayerEl.innerHTML = '';
323
+
324
+ // Match text layer dimensions to the canvas
325
+ tmpTextLayerEl.style.width = pViewport.width + 'px';
326
+ tmpTextLayerEl.style.height = pViewport.height + 'px';
327
+
328
+ pPage.getTextContent()
329
+ .then(function (pTextContent)
330
+ {
331
+ // Use pdf.js built-in renderTextLayer if available
332
+ if (window.pdfjsLib && window.pdfjsLib.renderTextLayer)
333
+ {
334
+ let tmpTask = window.pdfjsLib.renderTextLayer(
335
+ {
336
+ textContentSource: pTextContent,
337
+ container: tmpTextLayerEl,
338
+ viewport: pViewport
339
+ });
340
+ tmpTask.promise.catch(function ()
341
+ {
342
+ // Silently ignore text layer errors
343
+ });
344
+ }
345
+ else
346
+ {
347
+ // Manual fallback: position spans from the text content items
348
+ let tmpItems = pTextContent.items;
349
+ for (let i = 0; i < tmpItems.length; i++)
350
+ {
351
+ let tmpItem = tmpItems[i];
352
+ if (!tmpItem.str)
353
+ {
354
+ continue;
355
+ }
356
+
357
+ let tmpSpan = document.createElement('span');
358
+ tmpSpan.textContent = tmpItem.str;
359
+
360
+ // Position from the transform matrix [scaleX, skewX, skewY, scaleY, translateX, translateY]
361
+ let tmpTx = tmpItem.transform;
362
+ let tmpFontSize = Math.sqrt(tmpTx[2] * tmpTx[2] + tmpTx[3] * tmpTx[3]);
363
+ let tmpLeft = tmpTx[4] * (pViewport.width / (pViewport.viewBox[2] - pViewport.viewBox[0]));
364
+ // PDF coordinates have origin at bottom-left; canvas at top-left
365
+ let tmpTop = pViewport.height - (tmpTx[5] * (pViewport.height / (pViewport.viewBox[3] - pViewport.viewBox[1])));
366
+
367
+ tmpSpan.style.left = tmpLeft + 'px';
368
+ tmpSpan.style.top = (tmpTop - tmpFontSize) + 'px';
369
+ tmpSpan.style.fontSize = tmpFontSize + 'px';
370
+ tmpSpan.style.fontFamily = tmpItem.fontName || 'sans-serif';
371
+
372
+ tmpTextLayerEl.appendChild(tmpSpan);
373
+ }
374
+ }
375
+ });
376
+ },
377
+
378
+ /**
379
+ * Update the zoom percentage label.
380
+ */
381
+ _pdfUpdateZoomLabel: function _pdfUpdateZoomLabel()
382
+ {
383
+ let tmpLabel = document.getElementById('RetoldRemote-PdfZoomLabel');
384
+ if (tmpLabel)
385
+ {
386
+ tmpLabel.textContent = Math.round(this._pdfScale * 100) + '%';
387
+ }
388
+ },
389
+
390
+ // -----------------------------------------------------------------
391
+ // Page navigation
392
+ // -----------------------------------------------------------------
393
+
394
+ /**
395
+ * Go to the next page.
396
+ */
397
+ pdfNextPage: function pdfNextPage()
398
+ {
399
+ if (this._pdfCurrentPage < this._pdfPageCount)
400
+ {
401
+ this._renderPdfPage(this._pdfCurrentPage + 1);
402
+ }
403
+ },
404
+
405
+ /**
406
+ * Go to the previous page.
407
+ */
408
+ pdfPrevPage: function pdfPrevPage()
409
+ {
410
+ if (this._pdfCurrentPage > 1)
411
+ {
412
+ this._renderPdfPage(this._pdfCurrentPage - 1);
413
+ }
414
+ },
415
+
416
+ /**
417
+ * Jump to a specific page number.
418
+ *
419
+ * @param {number} pPageNumber - 1-based page number
420
+ */
421
+ pdfGoToPage: function pdfGoToPage(pPageNumber)
422
+ {
423
+ if (isNaN(pPageNumber) || pPageNumber < 1)
424
+ {
425
+ pPageNumber = 1;
426
+ }
427
+ if (pPageNumber > this._pdfPageCount)
428
+ {
429
+ pPageNumber = this._pdfPageCount;
430
+ }
431
+ this._renderPdfPage(pPageNumber);
432
+ },
433
+
434
+ // -----------------------------------------------------------------
435
+ // Zoom
436
+ // -----------------------------------------------------------------
437
+
438
+ /**
439
+ * Zoom in by 25%.
440
+ */
441
+ pdfZoomIn: function pdfZoomIn()
442
+ {
443
+ this._pdfScale = Math.min(this._pdfScale + 0.25, 5.0);
444
+ this._renderPdfPage(this._pdfCurrentPage);
445
+ },
446
+
447
+ /**
448
+ * Zoom out by 25%.
449
+ */
450
+ pdfZoomOut: function pdfZoomOut()
451
+ {
452
+ this._pdfScale = Math.max(this._pdfScale - 0.25, 0.25);
453
+ this._renderPdfPage(this._pdfCurrentPage);
454
+ },
455
+
456
+ /**
457
+ * Fit the PDF page width to the content area.
458
+ */
459
+ pdfZoomFit: function pdfZoomFit()
460
+ {
461
+ if (!this._pdfDocument)
462
+ {
463
+ return;
464
+ }
465
+
466
+ let tmpSelf = this;
467
+ this._pdfDocument.getPage(this._pdfCurrentPage)
468
+ .then(function (pPage)
469
+ {
470
+ let tmpContentEl = document.getElementById('RetoldRemote-PdfContent');
471
+ if (!tmpContentEl)
472
+ {
473
+ return;
474
+ }
475
+
476
+ // Get the page dimensions at scale 1.0
477
+ let tmpBaseViewport = pPage.getViewport({ scale: 1.0 });
478
+ let tmpAvailableWidth = tmpContentEl.clientWidth - 32; // Subtract padding
479
+
480
+ let tmpFitScale = tmpAvailableWidth / tmpBaseViewport.width;
481
+ tmpSelf._pdfScale = Math.max(0.25, Math.min(tmpFitScale, 5.0));
482
+ tmpSelf._renderPdfPage(tmpSelf._pdfCurrentPage);
483
+ });
484
+ },
485
+
486
+ // -----------------------------------------------------------------
487
+ // Text selection saving
488
+ // -----------------------------------------------------------------
489
+
490
+ /**
491
+ * Save the currently selected text from the text layer.
492
+ */
493
+ pdfSaveSelection: function pdfSaveSelection()
494
+ {
495
+ let tmpSelectedText = '';
496
+ if (typeof window !== 'undefined' && window.getSelection)
497
+ {
498
+ tmpSelectedText = window.getSelection().toString().trim();
499
+ }
500
+
501
+ if (!tmpSelectedText)
502
+ {
503
+ let tmpToast = this.pict.providers['RetoldRemote-ToastNotification'];
504
+ if (tmpToast)
505
+ {
506
+ tmpToast.showToast('Select some text in the PDF first.');
507
+ }
508
+ return;
509
+ }
510
+
511
+ this._pdfPendingText = tmpSelectedText;
512
+ this._pdfPendingRegion = null;
513
+
514
+ // Show the label input bar
515
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
516
+ if (tmpLabelBar)
517
+ {
518
+ tmpLabelBar.style.display = '';
519
+ }
520
+
521
+ let tmpField = document.getElementById('RetoldRemote-PdfLabelField');
522
+ if (tmpField)
523
+ {
524
+ tmpField.value = '';
525
+ tmpField.placeholder = 'Label this text selection\u2026';
526
+ tmpField.focus();
527
+ }
528
+ },
529
+
530
+ // -----------------------------------------------------------------
531
+ // Visual region selection
532
+ // -----------------------------------------------------------------
533
+
534
+ /**
535
+ * Toggle the visual region selection mode.
536
+ */
537
+ pdfToggleRegionSelect: function pdfToggleRegionSelect()
538
+ {
539
+ this._pdfSelectionMode = !this._pdfSelectionMode;
540
+
541
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
542
+ let tmpBtn = document.getElementById('RetoldRemote-PdfRegionBtn');
543
+
544
+ if (this._pdfSelectionMode)
545
+ {
546
+ if (tmpOverlay)
547
+ {
548
+ tmpOverlay.style.display = '';
549
+ }
550
+ if (tmpBtn)
551
+ {
552
+ tmpBtn.classList.add('active');
553
+ }
554
+
555
+ this._pdfSetupRegionDrag();
556
+ }
557
+ else
558
+ {
559
+ if (tmpOverlay)
560
+ {
561
+ tmpOverlay.style.display = 'none';
562
+ tmpOverlay.innerHTML = '';
563
+ }
564
+ if (tmpBtn)
565
+ {
566
+ tmpBtn.classList.remove('active');
567
+ }
568
+
569
+ this._pdfCleanupRegionDrag();
570
+ }
571
+ },
572
+
573
+ /**
574
+ * Set up mouse event handlers for dragging a selection rectangle.
575
+ */
576
+ _pdfSetupRegionDrag: function _pdfSetupRegionDrag()
577
+ {
578
+ let tmpSelf = this;
579
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
580
+ if (!tmpOverlay)
581
+ {
582
+ return;
583
+ }
584
+
585
+ let tmpDragging = false;
586
+ let tmpStartX = 0;
587
+ let tmpStartY = 0;
588
+ let tmpRectEl = null;
589
+
590
+ this._pdfRegionMouseDown = function (pEvent)
591
+ {
592
+ pEvent.preventDefault();
593
+ tmpDragging = true;
594
+
595
+ let tmpRect = tmpOverlay.getBoundingClientRect();
596
+ tmpStartX = pEvent.clientX - tmpRect.left;
597
+ tmpStartY = pEvent.clientY - tmpRect.top;
598
+
599
+ // Create the selection rectangle element
600
+ tmpRectEl = document.createElement('div');
601
+ tmpRectEl.className = 'retold-remote-pdf-active-rect';
602
+ tmpRectEl.style.left = tmpStartX + 'px';
603
+ tmpRectEl.style.top = tmpStartY + 'px';
604
+ tmpRectEl.style.width = '0px';
605
+ tmpRectEl.style.height = '0px';
606
+ tmpOverlay.appendChild(tmpRectEl);
607
+ };
608
+
609
+ this._pdfRegionMouseMove = function (pEvent)
610
+ {
611
+ if (!tmpDragging || !tmpRectEl)
612
+ {
613
+ return;
614
+ }
615
+
616
+ let tmpRect = tmpOverlay.getBoundingClientRect();
617
+ let tmpCurrentX = pEvent.clientX - tmpRect.left;
618
+ let tmpCurrentY = pEvent.clientY - tmpRect.top;
619
+
620
+ let tmpLeft = Math.min(tmpStartX, tmpCurrentX);
621
+ let tmpTop = Math.min(tmpStartY, tmpCurrentY);
622
+ let tmpWidth = Math.abs(tmpCurrentX - tmpStartX);
623
+ let tmpHeight = Math.abs(tmpCurrentY - tmpStartY);
624
+
625
+ tmpRectEl.style.left = tmpLeft + 'px';
626
+ tmpRectEl.style.top = tmpTop + 'px';
627
+ tmpRectEl.style.width = tmpWidth + 'px';
628
+ tmpRectEl.style.height = tmpHeight + 'px';
629
+ };
630
+
631
+ this._pdfRegionMouseUp = function (pEvent)
632
+ {
633
+ if (!tmpDragging)
634
+ {
635
+ return;
636
+ }
637
+ tmpDragging = false;
638
+
639
+ let tmpRect = tmpOverlay.getBoundingClientRect();
640
+ let tmpEndX = pEvent.clientX - tmpRect.left;
641
+ let tmpEndY = pEvent.clientY - tmpRect.top;
642
+
643
+ let tmpLeft = Math.min(tmpStartX, tmpEndX);
644
+ let tmpTop = Math.min(tmpStartY, tmpEndY);
645
+ let tmpWidth = Math.abs(tmpEndX - tmpStartX);
646
+ let tmpHeight = Math.abs(tmpEndY - tmpStartY);
647
+
648
+ // Ignore tiny drags (likely accidental clicks)
649
+ if (tmpWidth < 5 || tmpHeight < 5)
650
+ {
651
+ if (tmpRectEl && tmpRectEl.parentElement)
652
+ {
653
+ tmpRectEl.parentElement.removeChild(tmpRectEl);
654
+ }
655
+ tmpRectEl = null;
656
+ return;
657
+ }
658
+
659
+ // Convert screen pixels to PDF coordinate units
660
+ let tmpCanvas = document.getElementById('RetoldRemote-PdfCanvas');
661
+ if (!tmpCanvas)
662
+ {
663
+ return;
664
+ }
665
+
666
+ let tmpCanvasWidth = tmpCanvas.width;
667
+ let tmpCanvasHeight = tmpCanvas.height;
668
+ let tmpDisplayWidth = tmpCanvas.offsetWidth;
669
+ let tmpDisplayHeight = tmpCanvas.offsetHeight;
670
+
671
+ // Scale from display pixels to canvas pixels, then to PDF units via scale
672
+ let tmpScaleX = tmpCanvasWidth / tmpDisplayWidth;
673
+ let tmpScaleY = tmpCanvasHeight / tmpDisplayHeight;
674
+
675
+ let tmpPdfX = (tmpLeft * tmpScaleX) / tmpSelf._pdfScale;
676
+ let tmpPdfY = (tmpTop * tmpScaleY) / tmpSelf._pdfScale;
677
+ let tmpPdfWidth = (tmpWidth * tmpScaleX) / tmpSelf._pdfScale;
678
+ let tmpPdfHeight = (tmpHeight * tmpScaleY) / tmpSelf._pdfScale;
679
+
680
+ tmpSelf._pdfPendingRegion =
681
+ {
682
+ X: Math.round(tmpPdfX * 100) / 100,
683
+ Y: Math.round(tmpPdfY * 100) / 100,
684
+ Width: Math.round(tmpPdfWidth * 100) / 100,
685
+ Height: Math.round(tmpPdfHeight * 100) / 100
686
+ };
687
+ tmpSelf._pdfPendingText = null;
688
+
689
+ // Show label input
690
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
691
+ if (tmpLabelBar)
692
+ {
693
+ tmpLabelBar.style.display = '';
694
+ }
695
+ let tmpField = document.getElementById('RetoldRemote-PdfLabelField');
696
+ if (tmpField)
697
+ {
698
+ tmpField.value = '';
699
+ tmpField.placeholder = 'Label this region\u2026';
700
+ tmpField.focus();
701
+ }
702
+ };
703
+
704
+ tmpOverlay.addEventListener('mousedown', this._pdfRegionMouseDown);
705
+ tmpOverlay.addEventListener('mousemove', this._pdfRegionMouseMove);
706
+ tmpOverlay.addEventListener('mouseup', this._pdfRegionMouseUp);
707
+ },
708
+
709
+ /**
710
+ * Remove mouse event handlers for region dragging.
711
+ */
712
+ _pdfCleanupRegionDrag: function _pdfCleanupRegionDrag()
713
+ {
714
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
715
+ if (tmpOverlay)
716
+ {
717
+ if (this._pdfRegionMouseDown)
718
+ {
719
+ tmpOverlay.removeEventListener('mousedown', this._pdfRegionMouseDown);
720
+ }
721
+ if (this._pdfRegionMouseMove)
722
+ {
723
+ tmpOverlay.removeEventListener('mousemove', this._pdfRegionMouseMove);
724
+ }
725
+ if (this._pdfRegionMouseUp)
726
+ {
727
+ tmpOverlay.removeEventListener('mouseup', this._pdfRegionMouseUp);
728
+ }
729
+ }
730
+
731
+ this._pdfRegionMouseDown = null;
732
+ this._pdfRegionMouseMove = null;
733
+ this._pdfRegionMouseUp = null;
734
+ },
735
+
736
+ // -----------------------------------------------------------------
737
+ // Label save / cancel
738
+ // -----------------------------------------------------------------
739
+
740
+ /**
741
+ * Save the labeled selection (text or visual region) to the server.
742
+ */
743
+ pdfSaveLabel: function pdfSaveLabel()
744
+ {
745
+ let tmpField = document.getElementById('RetoldRemote-PdfLabelField');
746
+ let tmpLabel = tmpField ? tmpField.value.trim() : '';
747
+
748
+ let tmpSelf = this;
749
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
750
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._pdfFilePath) : encodeURIComponent(this._pdfFilePath);
751
+
752
+ let tmpBody =
753
+ {
754
+ Path: this._pdfFilePath,
755
+ Region:
756
+ {
757
+ Label: tmpLabel,
758
+ PageNumber: this._pdfCurrentPage
759
+ }
760
+ };
761
+
762
+ if (this._pdfPendingText)
763
+ {
764
+ tmpBody.Region.Type = 'text-selection';
765
+ tmpBody.Region.SelectedText = this._pdfPendingText;
766
+ }
767
+ else if (this._pdfPendingRegion)
768
+ {
769
+ tmpBody.Region.Type = 'visual-region';
770
+ tmpBody.Region.X = this._pdfPendingRegion.X;
771
+ tmpBody.Region.Y = this._pdfPendingRegion.Y;
772
+ tmpBody.Region.Width = this._pdfPendingRegion.Width;
773
+ tmpBody.Region.Height = this._pdfPendingRegion.Height;
774
+ }
775
+ else
776
+ {
777
+ // Nothing pending
778
+ this.pdfCancelSelection();
779
+ return;
780
+ }
781
+
782
+ fetch('/api/media/subimage-regions',
783
+ {
784
+ method: 'POST',
785
+ headers: { 'Content-Type': 'application/json' },
786
+ body: JSON.stringify(tmpBody)
787
+ })
788
+ .then(function (pResponse) { return pResponse.json(); })
789
+ .then(function (pResult)
790
+ {
791
+ if (pResult && pResult.Success)
792
+ {
793
+ tmpSelf._pdfSavedRegions = pResult.Regions || [];
794
+ tmpSelf._pdfRenderRegionOverlays();
795
+
796
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
797
+ if (tmpToast)
798
+ {
799
+ tmpToast.showToast('Selection saved' + (tmpLabel ? ': ' + tmpLabel : ''));
800
+ }
801
+
802
+ // Update the subimages panel if visible
803
+ let tmpSubPanel = tmpSelf.pict.views['RetoldRemote-SubimagesPanel'];
804
+ if (tmpSubPanel)
805
+ {
806
+ tmpSubPanel.render();
807
+ }
808
+ }
809
+ })
810
+ .catch(function (pErr)
811
+ {
812
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
813
+ if (tmpToast)
814
+ {
815
+ tmpToast.showToast('Failed to save selection: ' + pErr.message);
816
+ }
817
+ });
818
+
819
+ // Clean up
820
+ this._pdfPendingRegion = null;
821
+ this._pdfPendingText = null;
822
+
823
+ // Hide the label bar
824
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
825
+ if (tmpLabelBar)
826
+ {
827
+ tmpLabelBar.style.display = 'none';
828
+ }
829
+
830
+ // Remove the active drag rectangle if any
831
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
832
+ if (tmpOverlay)
833
+ {
834
+ tmpOverlay.innerHTML = '';
835
+ }
836
+ },
837
+
838
+ /**
839
+ * Cancel the current selection without saving.
840
+ */
841
+ pdfCancelSelection: function pdfCancelSelection()
842
+ {
843
+ this._pdfPendingRegion = null;
844
+ this._pdfPendingText = null;
845
+
846
+ // Hide the label bar
847
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
848
+ if (tmpLabelBar)
849
+ {
850
+ tmpLabelBar.style.display = 'none';
851
+ }
852
+
853
+ // Remove any active drag rectangle
854
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
855
+ if (tmpOverlay)
856
+ {
857
+ tmpOverlay.innerHTML = '';
858
+ }
859
+ },
860
+
861
+ // -----------------------------------------------------------------
862
+ // Saved region management
863
+ // -----------------------------------------------------------------
864
+
865
+ /**
866
+ * Load saved subimage regions from the server for this PDF.
867
+ *
868
+ * @param {string} pFilePath - Relative file path
869
+ */
870
+ _pdfLoadSavedRegions: function _pdfLoadSavedRegions(pFilePath)
871
+ {
872
+ let tmpSelf = this;
873
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
874
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
875
+
876
+ fetch('/api/media/subimage-regions?path=' + tmpPathParam)
877
+ .then(function (pResponse) { return pResponse.json(); })
878
+ .then(function (pResult)
879
+ {
880
+ if (pResult && pResult.Success && Array.isArray(pResult.Regions))
881
+ {
882
+ tmpSelf._pdfSavedRegions = pResult.Regions;
883
+ tmpSelf._pdfRenderRegionOverlays();
884
+ }
885
+ })
886
+ .catch(function ()
887
+ {
888
+ // Silently ignore — regions are optional
889
+ });
890
+ },
891
+
892
+ /**
893
+ * Render saved region overlays for the current page as colored rectangles
894
+ * positioned over the canvas.
895
+ */
896
+ _pdfRenderRegionOverlays: function _pdfRenderRegionOverlays()
897
+ {
898
+ let tmpOverlaysContainer = document.getElementById('RetoldRemote-PdfRegionOverlays');
899
+ if (!tmpOverlaysContainer)
900
+ {
901
+ return;
902
+ }
903
+
904
+ let tmpCanvas = document.getElementById('RetoldRemote-PdfCanvas');
905
+ if (!tmpCanvas)
906
+ {
907
+ return;
908
+ }
909
+
910
+ // Match container dimensions to the canvas
911
+ tmpOverlaysContainer.style.width = tmpCanvas.offsetWidth + 'px';
912
+ tmpOverlaysContainer.style.height = tmpCanvas.offsetHeight + 'px';
913
+
914
+ // Clear existing overlays
915
+ tmpOverlaysContainer.innerHTML = '';
916
+
917
+ let tmpCurrentPage = this._pdfCurrentPage;
918
+ let tmpScale = this._pdfScale;
919
+ let tmpDisplayWidth = tmpCanvas.offsetWidth;
920
+ let tmpCanvasWidth = tmpCanvas.width;
921
+ let tmpDisplayScale = tmpDisplayWidth / tmpCanvasWidth;
922
+
923
+ for (let i = 0; i < this._pdfSavedRegions.length; i++)
924
+ {
925
+ let tmpRegion = this._pdfSavedRegions[i];
926
+
927
+ // Only show regions for the current page (or regions without a page number)
928
+ if (tmpRegion.PageNumber && tmpRegion.PageNumber !== tmpCurrentPage)
929
+ {
930
+ continue;
931
+ }
932
+
933
+ // Only render visual-region types as overlays
934
+ if (tmpRegion.Type !== 'visual-region')
935
+ {
936
+ continue;
937
+ }
938
+
939
+ // Convert PDF coordinates to display pixels
940
+ let tmpLeft = tmpRegion.X * tmpScale * tmpDisplayScale;
941
+ let tmpTop = tmpRegion.Y * tmpScale * tmpDisplayScale;
942
+ let tmpWidth = tmpRegion.Width * tmpScale * tmpDisplayScale;
943
+ let tmpHeight = tmpRegion.Height * tmpScale * tmpDisplayScale;
944
+
945
+ let tmpRectEl = document.createElement('div');
946
+ tmpRectEl.className = 'retold-remote-pdf-region-rect';
947
+ tmpRectEl.style.left = tmpLeft + 'px';
948
+ tmpRectEl.style.top = tmpTop + 'px';
949
+ tmpRectEl.style.width = tmpWidth + 'px';
950
+ tmpRectEl.style.height = tmpHeight + 'px';
951
+
952
+ if (tmpRegion.Label)
953
+ {
954
+ let tmpLabelEl = document.createElement('div');
955
+ tmpLabelEl.className = 'retold-remote-pdf-region-label';
956
+ tmpLabelEl.textContent = tmpRegion.Label;
957
+ tmpRectEl.appendChild(tmpLabelEl);
958
+ }
959
+
960
+ tmpOverlaysContainer.appendChild(tmpRectEl);
961
+ }
962
+ }
963
+ };