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.
- package/css/retold-remote.css +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- 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">← 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 →</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">💾 Save Selection</button>';
|
|
44
|
+
tmpHTML += '<button class="retold-remote-pdf-btn" id="RetoldRemote-PdfRegionBtn" onclick="' + tmpViewRef + '.pdfToggleRegionSelect()" title="Select a visual region">✂ 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
|
+
};
|