retold-remote 0.0.23 → 0.0.26

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 (79) hide show
  1. package/css/retold-remote.css +343 -20
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +64 -12
  4. package/docs/_cover.md +6 -6
  5. package/docs/_sidebar.md +2 -0
  6. package/docs/_topbar.md +1 -1
  7. package/docs/_version.json +7 -0
  8. package/docs/collections.md +30 -0
  9. package/docs/css/docuserve.css +327 -0
  10. package/docs/ebook-reader.md +75 -1
  11. package/docs/image-explorer.md +62 -2
  12. package/docs/index.html +39 -0
  13. package/docs/retold-catalog.json +254 -0
  14. package/docs/retold-keyword-index.json +31216 -0
  15. package/docs/server-setup.md +122 -91
  16. package/docs/stack-launcher.md +218 -0
  17. package/docs/synology.md +585 -0
  18. package/docs/ultravisor-configuration.md +5 -5
  19. package/docs/ultravisor-integration.md +4 -2
  20. package/package.json +20 -14
  21. package/source/Pict-Application-RetoldRemote.js +22 -0
  22. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  23. package/source/cli/RetoldRemote-Server-Setup.js +460 -7
  24. package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
  25. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  26. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  27. package/source/providers/CollectionManager-AddItems.js +166 -0
  28. package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
  29. package/source/providers/Pict-Provider-OperationStatus.js +597 -0
  30. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
  31. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  32. package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
  33. package/source/server/RetoldRemote-CollectionExportService.js +763 -0
  34. package/source/server/RetoldRemote-CollectionService.js +5 -0
  35. package/source/server/RetoldRemote-EbookService.js +218 -3
  36. package/source/server/RetoldRemote-ImageService.js +221 -46
  37. package/source/server/RetoldRemote-MediaService.js +63 -4
  38. package/source/server/RetoldRemote-MetadataCache.js +25 -5
  39. package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
  40. package/source/server/RetoldRemote-SubimageService.js +680 -0
  41. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  42. package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
  43. package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
  44. package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
  45. package/source/server/RetoldRemote-VideoFrameService.js +302 -9
  46. package/source/views/MediaViewer-EbookViewer.js +419 -1
  47. package/source/views/MediaViewer-PdfViewer.js +1050 -0
  48. package/source/views/PictView-Remote-AudioExplorer.js +77 -1
  49. package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
  50. package/source/views/PictView-Remote-Gallery.js +365 -64
  51. package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
  52. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  53. package/source/views/PictView-Remote-Layout.js +58 -0
  54. package/source/views/PictView-Remote-MediaViewer.js +100 -25
  55. package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
  56. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  57. package/source/views/PictView-Remote-TopBar.js +1 -0
  58. package/source/views/PictView-Remote-VideoExplorer.js +77 -1
  59. package/web-application/css/docuserve.css +277 -23
  60. package/web-application/css/retold-remote.css +343 -20
  61. package/web-application/docs/README.md +64 -12
  62. package/web-application/docs/_cover.md +6 -6
  63. package/web-application/docs/_sidebar.md +2 -0
  64. package/web-application/docs/_topbar.md +1 -1
  65. package/web-application/docs/collections.md +30 -0
  66. package/web-application/docs/ebook-reader.md +75 -1
  67. package/web-application/docs/image-explorer.md +62 -2
  68. package/web-application/docs/server-setup.md +122 -91
  69. package/web-application/docs/stack-launcher.md +218 -0
  70. package/web-application/docs/synology.md +585 -0
  71. package/web-application/docs/ultravisor-configuration.md +5 -5
  72. package/web-application/docs/ultravisor-integration.md +4 -2
  73. package/web-application/js/pict-docuserve.min.js +12 -12
  74. package/web-application/js/pict.min.js +2 -2
  75. package/web-application/js/pict.min.js.map +1 -1
  76. package/web-application/retold-remote.js +6596 -1784
  77. package/web-application/retold-remote.js.map +1 -1
  78. package/web-application/retold-remote.min.js +75 -23
  79. package/web-application/retold-remote.min.js.map +1 -1
@@ -0,0 +1,1050 @@
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
+ * Note: we intentionally wrap the dynamic import() call in a Function
137
+ * constructor so that Browserify/Babel cannot see it at build time
138
+ * and rewrite it into a require() call. pdf.js v4 is ESM-only, so we
139
+ * have no choice but to use a true native dynamic import at runtime.
140
+ *
141
+ * @param {Function} fCallback - Called when pdf.js is ready
142
+ */
143
+ _ensurePdfJsLoaded: function _ensurePdfJsLoaded(fCallback)
144
+ {
145
+ if (typeof window !== 'undefined' && window.pdfjsLib)
146
+ {
147
+ return fCallback();
148
+ }
149
+
150
+ let tmpSelf = this;
151
+ let tmpLoadingEl = document.getElementById('RetoldRemote-PdfLoading');
152
+ if (tmpLoadingEl)
153
+ {
154
+ tmpLoadingEl.textContent = 'Loading PDF renderer...';
155
+ }
156
+
157
+ // Hide the dynamic import from the bundler by constructing it at runtime.
158
+ // Without this, Browserify rewrites `import(x)` into
159
+ // `new Promise(r => r(x)).then(s => _interopRequireWildcard(require(s)))`
160
+ // which fails in the browser because there's no `require` on the window.
161
+ let tmpDynamicImport;
162
+ try
163
+ {
164
+ tmpDynamicImport = new Function('url', 'return import(url);');
165
+ }
166
+ catch (pFuncError)
167
+ {
168
+ // Some strict CSP environments forbid new Function — fall back to a
169
+ // <script type="module"> tag injection (see below).
170
+ tmpDynamicImport = null;
171
+ }
172
+
173
+ let _handleLoaded = function (pModule)
174
+ {
175
+ window.pdfjsLib = pModule;
176
+ if (window.pdfjsLib.GlobalWorkerOptions)
177
+ {
178
+ window.pdfjsLib.GlobalWorkerOptions.workerSrc = _PDF_JS_WORKER_CDN_URL;
179
+ }
180
+ fCallback();
181
+ };
182
+
183
+ let _handleError = function (pError)
184
+ {
185
+ let tmpEl = document.getElementById('RetoldRemote-PdfLoading');
186
+ if (tmpEl)
187
+ {
188
+ tmpEl.textContent = 'Failed to load PDF renderer: ' + (pError && pError.message ? pError.message : 'unknown error');
189
+ }
190
+ if (tmpSelf.pict && tmpSelf.pict.log)
191
+ {
192
+ tmpSelf.pict.log.error('PDF renderer load failed: ' + (pError && pError.stack ? pError.stack : pError));
193
+ }
194
+ };
195
+
196
+ if (tmpDynamicImport)
197
+ {
198
+ tmpDynamicImport(_PDF_JS_CDN_URL).then(_handleLoaded).catch(function (pImportError)
199
+ {
200
+ // If the Function-wrapped import fails for any reason, fall back
201
+ // to the script-tag loader below.
202
+ tmpSelf._loadPdfJsViaScriptTag(_handleLoaded, _handleError);
203
+ });
204
+ }
205
+ else
206
+ {
207
+ tmpSelf._loadPdfJsViaScriptTag(_handleLoaded, _handleError);
208
+ }
209
+ },
210
+
211
+ /**
212
+ * Fallback loader that injects a <script type="module"> tag to import pdf.js.
213
+ * Used when `new Function('return import(url)')` is blocked by CSP.
214
+ *
215
+ * @param {Function} fOnLoad - Called with the loaded module namespace
216
+ * @param {Function} fOnError - Called with any error
217
+ */
218
+ _loadPdfJsViaScriptTag: function _loadPdfJsViaScriptTag(fOnLoad, fOnError)
219
+ {
220
+ try
221
+ {
222
+ // Generate a unique global name so we don't collide with concurrent loads.
223
+ let tmpResolverName = '__retoldPdfJsResolver_' + Date.now();
224
+ let tmpRejecterName = '__retoldPdfJsRejecter_' + Date.now();
225
+
226
+ let tmpScript = document.createElement('script');
227
+ tmpScript.type = 'module';
228
+ tmpScript.textContent =
229
+ 'import("' + _PDF_JS_CDN_URL + '")' +
230
+ ' .then(function (m) { window["' + tmpResolverName + '"](m); })' +
231
+ ' .catch(function (e) { window["' + tmpRejecterName + '"](e); });';
232
+
233
+ window[tmpResolverName] = function (pModule)
234
+ {
235
+ delete window[tmpResolverName];
236
+ delete window[tmpRejecterName];
237
+ tmpScript.remove();
238
+ fOnLoad(pModule);
239
+ };
240
+ window[tmpRejecterName] = function (pError)
241
+ {
242
+ delete window[tmpResolverName];
243
+ delete window[tmpRejecterName];
244
+ tmpScript.remove();
245
+ fOnError(pError);
246
+ };
247
+
248
+ document.head.appendChild(tmpScript);
249
+ }
250
+ catch (pError)
251
+ {
252
+ fOnError(pError);
253
+ }
254
+ },
255
+
256
+ /**
257
+ * Open a PDF document from a URL using pdf.js.
258
+ *
259
+ * @param {string} pContentURL - URL to fetch the PDF from
260
+ */
261
+ _openPdfDocument: function _openPdfDocument(pContentURL)
262
+ {
263
+ let tmpSelf = this;
264
+ let tmpLoadingEl = document.getElementById('RetoldRemote-PdfLoading');
265
+ if (tmpLoadingEl)
266
+ {
267
+ tmpLoadingEl.textContent = 'Opening PDF...';
268
+ }
269
+
270
+ let tmpLoadingTask = window.pdfjsLib.getDocument(pContentURL);
271
+ tmpLoadingTask.promise
272
+ .then(function (pDocument)
273
+ {
274
+ tmpSelf._pdfDocument = pDocument;
275
+ tmpSelf._pdfPageCount = pDocument.numPages;
276
+ tmpSelf._pdfCurrentPage = 1;
277
+
278
+ // Update the page count display
279
+ let tmpCountEl = document.getElementById('RetoldRemote-PdfPageCount');
280
+ if (tmpCountEl)
281
+ {
282
+ tmpCountEl.textContent = pDocument.numPages;
283
+ }
284
+
285
+ let tmpPageInput = document.getElementById('RetoldRemote-PdfPageInput');
286
+ if (tmpPageInput)
287
+ {
288
+ tmpPageInput.max = pDocument.numPages;
289
+ }
290
+
291
+ // Hide loading indicator
292
+ if (tmpLoadingEl)
293
+ {
294
+ tmpLoadingEl.style.display = 'none';
295
+ }
296
+
297
+ // Render the first page
298
+ tmpSelf._renderPdfPage(1);
299
+
300
+ // Load saved regions
301
+ tmpSelf._pdfLoadSavedRegions(tmpSelf._pdfFilePath);
302
+ })
303
+ .catch(function (pError)
304
+ {
305
+ if (tmpLoadingEl)
306
+ {
307
+ tmpLoadingEl.textContent = 'Failed to open PDF: ' + pError.message;
308
+ }
309
+ });
310
+ },
311
+
312
+ /**
313
+ * Render a specific page of the PDF onto the canvas.
314
+ *
315
+ * @param {number} pPageNumber - 1-based page number
316
+ */
317
+ _renderPdfPage: function _renderPdfPage(pPageNumber)
318
+ {
319
+ if (!this._pdfDocument)
320
+ {
321
+ return;
322
+ }
323
+
324
+ if (pPageNumber < 1 || pPageNumber > this._pdfPageCount)
325
+ {
326
+ return;
327
+ }
328
+
329
+ let tmpSelf = this;
330
+ this._pdfCurrentPage = pPageNumber;
331
+
332
+ this._pdfDocument.getPage(pPageNumber)
333
+ .then(function (pPage)
334
+ {
335
+ let tmpViewport = pPage.getViewport({ scale: tmpSelf._pdfScale });
336
+
337
+ let tmpCanvas = document.getElementById('RetoldRemote-PdfCanvas');
338
+ if (!tmpCanvas)
339
+ {
340
+ return;
341
+ }
342
+ let tmpContext = tmpCanvas.getContext('2d');
343
+
344
+ // Set canvas dimensions to match the viewport
345
+ tmpCanvas.width = tmpViewport.width;
346
+ tmpCanvas.height = tmpViewport.height;
347
+
348
+ // Set the wrap container dimensions
349
+ let tmpWrap = document.getElementById('RetoldRemote-PdfWrap');
350
+ if (tmpWrap)
351
+ {
352
+ tmpWrap.style.width = tmpViewport.width + 'px';
353
+ tmpWrap.style.height = tmpViewport.height + 'px';
354
+ }
355
+
356
+ // Render the page onto the canvas
357
+ let tmpRenderContext =
358
+ {
359
+ canvasContext: tmpContext,
360
+ viewport: tmpViewport
361
+ };
362
+
363
+ pPage.render(tmpRenderContext).promise
364
+ .then(function ()
365
+ {
366
+ // After canvas renders, build the text layer
367
+ tmpSelf._renderPdfTextLayer(pPage, tmpViewport);
368
+
369
+ // Render saved region overlays for this page
370
+ tmpSelf._pdfRenderRegionOverlays();
371
+ });
372
+
373
+ // Update the page number input
374
+ let tmpPageInput = document.getElementById('RetoldRemote-PdfPageInput');
375
+ if (tmpPageInput)
376
+ {
377
+ tmpPageInput.value = pPageNumber;
378
+ }
379
+
380
+ // Update the zoom label
381
+ tmpSelf._pdfUpdateZoomLabel();
382
+ })
383
+ .catch(function (pError)
384
+ {
385
+ let tmpLoadingEl = document.getElementById('RetoldRemote-PdfLoading');
386
+ if (tmpLoadingEl)
387
+ {
388
+ tmpLoadingEl.style.display = '';
389
+ tmpLoadingEl.textContent = 'Failed to render page: ' + pError.message;
390
+ }
391
+ });
392
+ },
393
+
394
+ /**
395
+ * Render the text layer on top of the canvas so text can be selected.
396
+ *
397
+ * @param {Object} pPage - pdf.js page object
398
+ * @param {Object} pViewport - pdf.js viewport for the current scale
399
+ */
400
+ _renderPdfTextLayer: function _renderPdfTextLayer(pPage, pViewport)
401
+ {
402
+ let tmpTextLayerEl = document.getElementById('RetoldRemote-PdfTextLayer');
403
+ if (!tmpTextLayerEl)
404
+ {
405
+ return;
406
+ }
407
+
408
+ // Clear previous text layer content
409
+ tmpTextLayerEl.innerHTML = '';
410
+
411
+ // Match text layer dimensions to the canvas
412
+ tmpTextLayerEl.style.width = pViewport.width + 'px';
413
+ tmpTextLayerEl.style.height = pViewport.height + 'px';
414
+
415
+ pPage.getTextContent()
416
+ .then(function (pTextContent)
417
+ {
418
+ // Use pdf.js built-in renderTextLayer if available
419
+ if (window.pdfjsLib && window.pdfjsLib.renderTextLayer)
420
+ {
421
+ let tmpTask = window.pdfjsLib.renderTextLayer(
422
+ {
423
+ textContentSource: pTextContent,
424
+ container: tmpTextLayerEl,
425
+ viewport: pViewport
426
+ });
427
+ tmpTask.promise.catch(function ()
428
+ {
429
+ // Silently ignore text layer errors
430
+ });
431
+ }
432
+ else
433
+ {
434
+ // Manual fallback: position spans from the text content items
435
+ let tmpItems = pTextContent.items;
436
+ for (let i = 0; i < tmpItems.length; i++)
437
+ {
438
+ let tmpItem = tmpItems[i];
439
+ if (!tmpItem.str)
440
+ {
441
+ continue;
442
+ }
443
+
444
+ let tmpSpan = document.createElement('span');
445
+ tmpSpan.textContent = tmpItem.str;
446
+
447
+ // Position from the transform matrix [scaleX, skewX, skewY, scaleY, translateX, translateY]
448
+ let tmpTx = tmpItem.transform;
449
+ let tmpFontSize = Math.sqrt(tmpTx[2] * tmpTx[2] + tmpTx[3] * tmpTx[3]);
450
+ let tmpLeft = tmpTx[4] * (pViewport.width / (pViewport.viewBox[2] - pViewport.viewBox[0]));
451
+ // PDF coordinates have origin at bottom-left; canvas at top-left
452
+ let tmpTop = pViewport.height - (tmpTx[5] * (pViewport.height / (pViewport.viewBox[3] - pViewport.viewBox[1])));
453
+
454
+ tmpSpan.style.left = tmpLeft + 'px';
455
+ tmpSpan.style.top = (tmpTop - tmpFontSize) + 'px';
456
+ tmpSpan.style.fontSize = tmpFontSize + 'px';
457
+ tmpSpan.style.fontFamily = tmpItem.fontName || 'sans-serif';
458
+
459
+ tmpTextLayerEl.appendChild(tmpSpan);
460
+ }
461
+ }
462
+ });
463
+ },
464
+
465
+ /**
466
+ * Update the zoom percentage label.
467
+ */
468
+ _pdfUpdateZoomLabel: function _pdfUpdateZoomLabel()
469
+ {
470
+ let tmpLabel = document.getElementById('RetoldRemote-PdfZoomLabel');
471
+ if (tmpLabel)
472
+ {
473
+ tmpLabel.textContent = Math.round(this._pdfScale * 100) + '%';
474
+ }
475
+ },
476
+
477
+ // -----------------------------------------------------------------
478
+ // Page navigation
479
+ // -----------------------------------------------------------------
480
+
481
+ /**
482
+ * Go to the next page.
483
+ */
484
+ pdfNextPage: function pdfNextPage()
485
+ {
486
+ if (this._pdfCurrentPage < this._pdfPageCount)
487
+ {
488
+ this._renderPdfPage(this._pdfCurrentPage + 1);
489
+ }
490
+ },
491
+
492
+ /**
493
+ * Go to the previous page.
494
+ */
495
+ pdfPrevPage: function pdfPrevPage()
496
+ {
497
+ if (this._pdfCurrentPage > 1)
498
+ {
499
+ this._renderPdfPage(this._pdfCurrentPage - 1);
500
+ }
501
+ },
502
+
503
+ /**
504
+ * Jump to a specific page number.
505
+ *
506
+ * @param {number} pPageNumber - 1-based page number
507
+ */
508
+ pdfGoToPage: function pdfGoToPage(pPageNumber)
509
+ {
510
+ if (isNaN(pPageNumber) || pPageNumber < 1)
511
+ {
512
+ pPageNumber = 1;
513
+ }
514
+ if (pPageNumber > this._pdfPageCount)
515
+ {
516
+ pPageNumber = this._pdfPageCount;
517
+ }
518
+ this._renderPdfPage(pPageNumber);
519
+ },
520
+
521
+ // -----------------------------------------------------------------
522
+ // Zoom
523
+ // -----------------------------------------------------------------
524
+
525
+ /**
526
+ * Zoom in by 25%.
527
+ */
528
+ pdfZoomIn: function pdfZoomIn()
529
+ {
530
+ this._pdfScale = Math.min(this._pdfScale + 0.25, 5.0);
531
+ this._renderPdfPage(this._pdfCurrentPage);
532
+ },
533
+
534
+ /**
535
+ * Zoom out by 25%.
536
+ */
537
+ pdfZoomOut: function pdfZoomOut()
538
+ {
539
+ this._pdfScale = Math.max(this._pdfScale - 0.25, 0.25);
540
+ this._renderPdfPage(this._pdfCurrentPage);
541
+ },
542
+
543
+ /**
544
+ * Fit the PDF page width to the content area.
545
+ */
546
+ pdfZoomFit: function pdfZoomFit()
547
+ {
548
+ if (!this._pdfDocument)
549
+ {
550
+ return;
551
+ }
552
+
553
+ let tmpSelf = this;
554
+ this._pdfDocument.getPage(this._pdfCurrentPage)
555
+ .then(function (pPage)
556
+ {
557
+ let tmpContentEl = document.getElementById('RetoldRemote-PdfContent');
558
+ if (!tmpContentEl)
559
+ {
560
+ return;
561
+ }
562
+
563
+ // Get the page dimensions at scale 1.0
564
+ let tmpBaseViewport = pPage.getViewport({ scale: 1.0 });
565
+ let tmpAvailableWidth = tmpContentEl.clientWidth - 32; // Subtract padding
566
+
567
+ let tmpFitScale = tmpAvailableWidth / tmpBaseViewport.width;
568
+ tmpSelf._pdfScale = Math.max(0.25, Math.min(tmpFitScale, 5.0));
569
+ tmpSelf._renderPdfPage(tmpSelf._pdfCurrentPage);
570
+ });
571
+ },
572
+
573
+ // -----------------------------------------------------------------
574
+ // Text selection saving
575
+ // -----------------------------------------------------------------
576
+
577
+ /**
578
+ * Save the currently selected text from the text layer.
579
+ */
580
+ pdfSaveSelection: function pdfSaveSelection()
581
+ {
582
+ let tmpSelectedText = '';
583
+ if (typeof window !== 'undefined' && window.getSelection)
584
+ {
585
+ tmpSelectedText = window.getSelection().toString().trim();
586
+ }
587
+
588
+ if (!tmpSelectedText)
589
+ {
590
+ let tmpToast = this.pict.providers['RetoldRemote-ToastNotification'];
591
+ if (tmpToast)
592
+ {
593
+ tmpToast.showToast('Select some text in the PDF first.');
594
+ }
595
+ return;
596
+ }
597
+
598
+ this._pdfPendingText = tmpSelectedText;
599
+ this._pdfPendingRegion = null;
600
+
601
+ // Show the label input bar
602
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
603
+ if (tmpLabelBar)
604
+ {
605
+ tmpLabelBar.style.display = '';
606
+ }
607
+
608
+ let tmpField = document.getElementById('RetoldRemote-PdfLabelField');
609
+ if (tmpField)
610
+ {
611
+ tmpField.value = '';
612
+ tmpField.placeholder = 'Label this text selection\u2026';
613
+ tmpField.focus();
614
+ }
615
+ },
616
+
617
+ // -----------------------------------------------------------------
618
+ // Visual region selection
619
+ // -----------------------------------------------------------------
620
+
621
+ /**
622
+ * Toggle the visual region selection mode.
623
+ */
624
+ pdfToggleRegionSelect: function pdfToggleRegionSelect()
625
+ {
626
+ this._pdfSelectionMode = !this._pdfSelectionMode;
627
+
628
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
629
+ let tmpBtn = document.getElementById('RetoldRemote-PdfRegionBtn');
630
+
631
+ if (this._pdfSelectionMode)
632
+ {
633
+ if (tmpOverlay)
634
+ {
635
+ tmpOverlay.style.display = '';
636
+ }
637
+ if (tmpBtn)
638
+ {
639
+ tmpBtn.classList.add('active');
640
+ }
641
+
642
+ this._pdfSetupRegionDrag();
643
+ }
644
+ else
645
+ {
646
+ if (tmpOverlay)
647
+ {
648
+ tmpOverlay.style.display = 'none';
649
+ tmpOverlay.innerHTML = '';
650
+ }
651
+ if (tmpBtn)
652
+ {
653
+ tmpBtn.classList.remove('active');
654
+ }
655
+
656
+ this._pdfCleanupRegionDrag();
657
+ }
658
+ },
659
+
660
+ /**
661
+ * Set up mouse event handlers for dragging a selection rectangle.
662
+ */
663
+ _pdfSetupRegionDrag: function _pdfSetupRegionDrag()
664
+ {
665
+ let tmpSelf = this;
666
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
667
+ if (!tmpOverlay)
668
+ {
669
+ return;
670
+ }
671
+
672
+ let tmpDragging = false;
673
+ let tmpStartX = 0;
674
+ let tmpStartY = 0;
675
+ let tmpRectEl = null;
676
+
677
+ this._pdfRegionMouseDown = function (pEvent)
678
+ {
679
+ pEvent.preventDefault();
680
+ tmpDragging = true;
681
+
682
+ let tmpRect = tmpOverlay.getBoundingClientRect();
683
+ tmpStartX = pEvent.clientX - tmpRect.left;
684
+ tmpStartY = pEvent.clientY - tmpRect.top;
685
+
686
+ // Create the selection rectangle element
687
+ tmpRectEl = document.createElement('div');
688
+ tmpRectEl.className = 'retold-remote-pdf-active-rect';
689
+ tmpRectEl.style.left = tmpStartX + 'px';
690
+ tmpRectEl.style.top = tmpStartY + 'px';
691
+ tmpRectEl.style.width = '0px';
692
+ tmpRectEl.style.height = '0px';
693
+ tmpOverlay.appendChild(tmpRectEl);
694
+ };
695
+
696
+ this._pdfRegionMouseMove = function (pEvent)
697
+ {
698
+ if (!tmpDragging || !tmpRectEl)
699
+ {
700
+ return;
701
+ }
702
+
703
+ let tmpRect = tmpOverlay.getBoundingClientRect();
704
+ let tmpCurrentX = pEvent.clientX - tmpRect.left;
705
+ let tmpCurrentY = pEvent.clientY - tmpRect.top;
706
+
707
+ let tmpLeft = Math.min(tmpStartX, tmpCurrentX);
708
+ let tmpTop = Math.min(tmpStartY, tmpCurrentY);
709
+ let tmpWidth = Math.abs(tmpCurrentX - tmpStartX);
710
+ let tmpHeight = Math.abs(tmpCurrentY - tmpStartY);
711
+
712
+ tmpRectEl.style.left = tmpLeft + 'px';
713
+ tmpRectEl.style.top = tmpTop + 'px';
714
+ tmpRectEl.style.width = tmpWidth + 'px';
715
+ tmpRectEl.style.height = tmpHeight + 'px';
716
+ };
717
+
718
+ this._pdfRegionMouseUp = function (pEvent)
719
+ {
720
+ if (!tmpDragging)
721
+ {
722
+ return;
723
+ }
724
+ tmpDragging = false;
725
+
726
+ let tmpRect = tmpOverlay.getBoundingClientRect();
727
+ let tmpEndX = pEvent.clientX - tmpRect.left;
728
+ let tmpEndY = pEvent.clientY - tmpRect.top;
729
+
730
+ let tmpLeft = Math.min(tmpStartX, tmpEndX);
731
+ let tmpTop = Math.min(tmpStartY, tmpEndY);
732
+ let tmpWidth = Math.abs(tmpEndX - tmpStartX);
733
+ let tmpHeight = Math.abs(tmpEndY - tmpStartY);
734
+
735
+ // Ignore tiny drags (likely accidental clicks)
736
+ if (tmpWidth < 5 || tmpHeight < 5)
737
+ {
738
+ if (tmpRectEl && tmpRectEl.parentElement)
739
+ {
740
+ tmpRectEl.parentElement.removeChild(tmpRectEl);
741
+ }
742
+ tmpRectEl = null;
743
+ return;
744
+ }
745
+
746
+ // Convert screen pixels to PDF coordinate units
747
+ let tmpCanvas = document.getElementById('RetoldRemote-PdfCanvas');
748
+ if (!tmpCanvas)
749
+ {
750
+ return;
751
+ }
752
+
753
+ let tmpCanvasWidth = tmpCanvas.width;
754
+ let tmpCanvasHeight = tmpCanvas.height;
755
+ let tmpDisplayWidth = tmpCanvas.offsetWidth;
756
+ let tmpDisplayHeight = tmpCanvas.offsetHeight;
757
+
758
+ // Scale from display pixels to canvas pixels, then to PDF units via scale
759
+ let tmpScaleX = tmpCanvasWidth / tmpDisplayWidth;
760
+ let tmpScaleY = tmpCanvasHeight / tmpDisplayHeight;
761
+
762
+ let tmpPdfX = (tmpLeft * tmpScaleX) / tmpSelf._pdfScale;
763
+ let tmpPdfY = (tmpTop * tmpScaleY) / tmpSelf._pdfScale;
764
+ let tmpPdfWidth = (tmpWidth * tmpScaleX) / tmpSelf._pdfScale;
765
+ let tmpPdfHeight = (tmpHeight * tmpScaleY) / tmpSelf._pdfScale;
766
+
767
+ tmpSelf._pdfPendingRegion =
768
+ {
769
+ X: Math.round(tmpPdfX * 100) / 100,
770
+ Y: Math.round(tmpPdfY * 100) / 100,
771
+ Width: Math.round(tmpPdfWidth * 100) / 100,
772
+ Height: Math.round(tmpPdfHeight * 100) / 100
773
+ };
774
+ tmpSelf._pdfPendingText = null;
775
+
776
+ // Show label input
777
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
778
+ if (tmpLabelBar)
779
+ {
780
+ tmpLabelBar.style.display = '';
781
+ }
782
+ let tmpField = document.getElementById('RetoldRemote-PdfLabelField');
783
+ if (tmpField)
784
+ {
785
+ tmpField.value = '';
786
+ tmpField.placeholder = 'Label this region\u2026';
787
+ tmpField.focus();
788
+ }
789
+ };
790
+
791
+ tmpOverlay.addEventListener('mousedown', this._pdfRegionMouseDown);
792
+ tmpOverlay.addEventListener('mousemove', this._pdfRegionMouseMove);
793
+ tmpOverlay.addEventListener('mouseup', this._pdfRegionMouseUp);
794
+ },
795
+
796
+ /**
797
+ * Remove mouse event handlers for region dragging.
798
+ */
799
+ _pdfCleanupRegionDrag: function _pdfCleanupRegionDrag()
800
+ {
801
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
802
+ if (tmpOverlay)
803
+ {
804
+ if (this._pdfRegionMouseDown)
805
+ {
806
+ tmpOverlay.removeEventListener('mousedown', this._pdfRegionMouseDown);
807
+ }
808
+ if (this._pdfRegionMouseMove)
809
+ {
810
+ tmpOverlay.removeEventListener('mousemove', this._pdfRegionMouseMove);
811
+ }
812
+ if (this._pdfRegionMouseUp)
813
+ {
814
+ tmpOverlay.removeEventListener('mouseup', this._pdfRegionMouseUp);
815
+ }
816
+ }
817
+
818
+ this._pdfRegionMouseDown = null;
819
+ this._pdfRegionMouseMove = null;
820
+ this._pdfRegionMouseUp = null;
821
+ },
822
+
823
+ // -----------------------------------------------------------------
824
+ // Label save / cancel
825
+ // -----------------------------------------------------------------
826
+
827
+ /**
828
+ * Save the labeled selection (text or visual region) to the server.
829
+ */
830
+ pdfSaveLabel: function pdfSaveLabel()
831
+ {
832
+ let tmpField = document.getElementById('RetoldRemote-PdfLabelField');
833
+ let tmpLabel = tmpField ? tmpField.value.trim() : '';
834
+
835
+ let tmpSelf = this;
836
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
837
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._pdfFilePath) : encodeURIComponent(this._pdfFilePath);
838
+
839
+ let tmpBody =
840
+ {
841
+ Path: this._pdfFilePath,
842
+ Region:
843
+ {
844
+ Label: tmpLabel,
845
+ PageNumber: this._pdfCurrentPage
846
+ }
847
+ };
848
+
849
+ if (this._pdfPendingText)
850
+ {
851
+ tmpBody.Region.Type = 'text-selection';
852
+ tmpBody.Region.SelectedText = this._pdfPendingText;
853
+ }
854
+ else if (this._pdfPendingRegion)
855
+ {
856
+ tmpBody.Region.Type = 'visual-region';
857
+ tmpBody.Region.X = this._pdfPendingRegion.X;
858
+ tmpBody.Region.Y = this._pdfPendingRegion.Y;
859
+ tmpBody.Region.Width = this._pdfPendingRegion.Width;
860
+ tmpBody.Region.Height = this._pdfPendingRegion.Height;
861
+ }
862
+ else
863
+ {
864
+ // Nothing pending
865
+ this.pdfCancelSelection();
866
+ return;
867
+ }
868
+
869
+ fetch('/api/media/subimage-regions',
870
+ {
871
+ method: 'POST',
872
+ headers: { 'Content-Type': 'application/json' },
873
+ body: JSON.stringify(tmpBody)
874
+ })
875
+ .then(function (pResponse) { return pResponse.json(); })
876
+ .then(function (pResult)
877
+ {
878
+ if (pResult && pResult.Success)
879
+ {
880
+ tmpSelf._pdfSavedRegions = pResult.Regions || [];
881
+ tmpSelf._pdfRenderRegionOverlays();
882
+
883
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
884
+ if (tmpToast)
885
+ {
886
+ tmpToast.showToast('Selection saved' + (tmpLabel ? ': ' + tmpLabel : ''));
887
+ }
888
+
889
+ // Update the subimages panel if visible
890
+ let tmpSubPanel = tmpSelf.pict.views['RetoldRemote-SubimagesPanel'];
891
+ if (tmpSubPanel)
892
+ {
893
+ tmpSubPanel.render();
894
+ }
895
+ }
896
+ })
897
+ .catch(function (pErr)
898
+ {
899
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
900
+ if (tmpToast)
901
+ {
902
+ tmpToast.showToast('Failed to save selection: ' + pErr.message);
903
+ }
904
+ });
905
+
906
+ // Clean up
907
+ this._pdfPendingRegion = null;
908
+ this._pdfPendingText = null;
909
+
910
+ // Hide the label bar
911
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
912
+ if (tmpLabelBar)
913
+ {
914
+ tmpLabelBar.style.display = 'none';
915
+ }
916
+
917
+ // Remove the active drag rectangle if any
918
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
919
+ if (tmpOverlay)
920
+ {
921
+ tmpOverlay.innerHTML = '';
922
+ }
923
+ },
924
+
925
+ /**
926
+ * Cancel the current selection without saving.
927
+ */
928
+ pdfCancelSelection: function pdfCancelSelection()
929
+ {
930
+ this._pdfPendingRegion = null;
931
+ this._pdfPendingText = null;
932
+
933
+ // Hide the label bar
934
+ let tmpLabelBar = document.getElementById('RetoldRemote-PdfLabelInput');
935
+ if (tmpLabelBar)
936
+ {
937
+ tmpLabelBar.style.display = 'none';
938
+ }
939
+
940
+ // Remove any active drag rectangle
941
+ let tmpOverlay = document.getElementById('RetoldRemote-PdfSelectionOverlay');
942
+ if (tmpOverlay)
943
+ {
944
+ tmpOverlay.innerHTML = '';
945
+ }
946
+ },
947
+
948
+ // -----------------------------------------------------------------
949
+ // Saved region management
950
+ // -----------------------------------------------------------------
951
+
952
+ /**
953
+ * Load saved subimage regions from the server for this PDF.
954
+ *
955
+ * @param {string} pFilePath - Relative file path
956
+ */
957
+ _pdfLoadSavedRegions: function _pdfLoadSavedRegions(pFilePath)
958
+ {
959
+ let tmpSelf = this;
960
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
961
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
962
+
963
+ fetch('/api/media/subimage-regions?path=' + tmpPathParam)
964
+ .then(function (pResponse) { return pResponse.json(); })
965
+ .then(function (pResult)
966
+ {
967
+ if (pResult && pResult.Success && Array.isArray(pResult.Regions))
968
+ {
969
+ tmpSelf._pdfSavedRegions = pResult.Regions;
970
+ tmpSelf._pdfRenderRegionOverlays();
971
+ }
972
+ })
973
+ .catch(function ()
974
+ {
975
+ // Silently ignore — regions are optional
976
+ });
977
+ },
978
+
979
+ /**
980
+ * Render saved region overlays for the current page as colored rectangles
981
+ * positioned over the canvas.
982
+ */
983
+ _pdfRenderRegionOverlays: function _pdfRenderRegionOverlays()
984
+ {
985
+ let tmpOverlaysContainer = document.getElementById('RetoldRemote-PdfRegionOverlays');
986
+ if (!tmpOverlaysContainer)
987
+ {
988
+ return;
989
+ }
990
+
991
+ let tmpCanvas = document.getElementById('RetoldRemote-PdfCanvas');
992
+ if (!tmpCanvas)
993
+ {
994
+ return;
995
+ }
996
+
997
+ // Match container dimensions to the canvas
998
+ tmpOverlaysContainer.style.width = tmpCanvas.offsetWidth + 'px';
999
+ tmpOverlaysContainer.style.height = tmpCanvas.offsetHeight + 'px';
1000
+
1001
+ // Clear existing overlays
1002
+ tmpOverlaysContainer.innerHTML = '';
1003
+
1004
+ let tmpCurrentPage = this._pdfCurrentPage;
1005
+ let tmpScale = this._pdfScale;
1006
+ let tmpDisplayWidth = tmpCanvas.offsetWidth;
1007
+ let tmpCanvasWidth = tmpCanvas.width;
1008
+ let tmpDisplayScale = tmpDisplayWidth / tmpCanvasWidth;
1009
+
1010
+ for (let i = 0; i < this._pdfSavedRegions.length; i++)
1011
+ {
1012
+ let tmpRegion = this._pdfSavedRegions[i];
1013
+
1014
+ // Only show regions for the current page (or regions without a page number)
1015
+ if (tmpRegion.PageNumber && tmpRegion.PageNumber !== tmpCurrentPage)
1016
+ {
1017
+ continue;
1018
+ }
1019
+
1020
+ // Only render visual-region types as overlays
1021
+ if (tmpRegion.Type !== 'visual-region')
1022
+ {
1023
+ continue;
1024
+ }
1025
+
1026
+ // Convert PDF coordinates to display pixels
1027
+ let tmpLeft = tmpRegion.X * tmpScale * tmpDisplayScale;
1028
+ let tmpTop = tmpRegion.Y * tmpScale * tmpDisplayScale;
1029
+ let tmpWidth = tmpRegion.Width * tmpScale * tmpDisplayScale;
1030
+ let tmpHeight = tmpRegion.Height * tmpScale * tmpDisplayScale;
1031
+
1032
+ let tmpRectEl = document.createElement('div');
1033
+ tmpRectEl.className = 'retold-remote-pdf-region-rect';
1034
+ tmpRectEl.style.left = tmpLeft + 'px';
1035
+ tmpRectEl.style.top = tmpTop + 'px';
1036
+ tmpRectEl.style.width = tmpWidth + 'px';
1037
+ tmpRectEl.style.height = tmpHeight + 'px';
1038
+
1039
+ if (tmpRegion.Label)
1040
+ {
1041
+ let tmpLabelEl = document.createElement('div');
1042
+ tmpLabelEl.className = 'retold-remote-pdf-region-label';
1043
+ tmpLabelEl.textContent = tmpRegion.Label;
1044
+ tmpRectEl.appendChild(tmpLabelEl);
1045
+ }
1046
+
1047
+ tmpOverlaysContainer.appendChild(tmpRectEl);
1048
+ }
1049
+ }
1050
+ };