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
|
@@ -462,6 +462,11 @@ class RetoldRemoteCollectionService extends libFableServiceProviderBase
|
|
|
462
462
|
FrameFilename: tmpItem.FrameFilename || null,
|
|
463
463
|
AudioStart: (typeof tmpItem.AudioStart === 'number') ? tmpItem.AudioStart : null,
|
|
464
464
|
AudioEnd: (typeof tmpItem.AudioEnd === 'number') ? tmpItem.AudioEnd : null,
|
|
465
|
+
// Document region fields
|
|
466
|
+
PageNumber: (typeof tmpItem.PageNumber === 'number') ? tmpItem.PageNumber : null,
|
|
467
|
+
CFI: tmpItem.CFI || null,
|
|
468
|
+
SelectedText: tmpItem.SelectedText || null,
|
|
469
|
+
DocumentRegionType: tmpItem.DocumentRegionType || null,
|
|
465
470
|
// Operation fields (for operation-plan collections)
|
|
466
471
|
Operation: tmpItem.Operation || null,
|
|
467
472
|
DestinationPath: tmpItem.DestinationPath || null,
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Retold Remote --
|
|
2
|
+
* Retold Remote -- Document Conversion Service
|
|
3
3
|
*
|
|
4
|
-
* Converts MOBI/AZW/KF8
|
|
5
|
-
*
|
|
4
|
+
* Converts ebooks (MOBI/AZW/KF8) to EPUB and document formats
|
|
5
|
+
* (DOC/DOCX/RTF/ODT/WPD) to PDF for in-browser viewing.
|
|
6
|
+
*
|
|
7
|
+
* Uses:
|
|
8
|
+
* - Calibre's ebook-convert for EPUB conversions
|
|
9
|
+
* - LibreOffice headless for PDF conversions (preferred for layout docs)
|
|
10
|
+
* - ebook-convert as fallback for PDF if LibreOffice unavailable
|
|
6
11
|
*
|
|
7
12
|
* API:
|
|
8
13
|
* convertToEpub(pAbsPath, pRelPath, fCallback)
|
|
9
14
|
* -> { Success, CacheKey, OutputFilename, SourcePath, FileSize }
|
|
15
|
+
* convertToPdf(pAbsPath, pRelPath, fCallback)
|
|
16
|
+
* -> { Success, CacheKey, OutputFilename, SourcePath, FileSize }
|
|
10
17
|
*
|
|
11
18
|
* @license MIT
|
|
12
19
|
*/
|
|
@@ -40,6 +47,26 @@ const _ConvertibleExtensions =
|
|
|
40
47
|
'cbr': true
|
|
41
48
|
};
|
|
42
49
|
|
|
50
|
+
// Extensions that should be converted to PDF for viewing
|
|
51
|
+
// (layout-heavy documents that benefit from fixed-page rendering)
|
|
52
|
+
const _PdfConvertibleExtensions =
|
|
53
|
+
{
|
|
54
|
+
'doc': true,
|
|
55
|
+
'docx': true,
|
|
56
|
+
'rtf': true,
|
|
57
|
+
'odt': true,
|
|
58
|
+
'wpd': true, // WordPerfect
|
|
59
|
+
'wps': true, // Microsoft Works
|
|
60
|
+
'pages': true, // Apple Pages (LibreOffice can sometimes handle)
|
|
61
|
+
'odp': true, // OpenDocument Presentation
|
|
62
|
+
'ppt': true, // PowerPoint
|
|
63
|
+
'pptx': true, // PowerPoint (XML)
|
|
64
|
+
'ods': true, // OpenDocument Spreadsheet
|
|
65
|
+
'xls': true, // Excel
|
|
66
|
+
'xlsx': true, // Excel (XML)
|
|
67
|
+
'csv': true // CSV (renders as table in PDF)
|
|
68
|
+
};
|
|
69
|
+
|
|
43
70
|
class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
44
71
|
{
|
|
45
72
|
constructor(pFable, pOptions, pServiceHash)
|
|
@@ -62,9 +89,26 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
62
89
|
// Ultravisor dispatcher — set via setDispatcher()
|
|
63
90
|
this._dispatcher = null;
|
|
64
91
|
|
|
92
|
+
// Orator-Conversion service reference — set via setConversionService()
|
|
93
|
+
this._conversionService = null;
|
|
94
|
+
|
|
95
|
+
// Operation broadcaster — set via setBroadcaster()
|
|
96
|
+
this._broadcaster = null;
|
|
97
|
+
|
|
65
98
|
this.fable.log.info('Ebook Service: using ParimeBinaryStorage (category: ebook-cache)');
|
|
66
99
|
}
|
|
67
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Set the orator-conversion service reference for document conversion.
|
|
103
|
+
* Called from Server-Setup after the conversion service is instantiated.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} pService - OratorFileTranslation instance
|
|
106
|
+
*/
|
|
107
|
+
setConversionService(pService)
|
|
108
|
+
{
|
|
109
|
+
this._conversionService = pService;
|
|
110
|
+
}
|
|
111
|
+
|
|
68
112
|
/**
|
|
69
113
|
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
70
114
|
*
|
|
@@ -75,6 +119,27 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
75
119
|
this._dispatcher = pDispatcher;
|
|
76
120
|
}
|
|
77
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Set the operation broadcaster for progress events and cancellation.
|
|
124
|
+
*/
|
|
125
|
+
setBroadcaster(pBroadcaster)
|
|
126
|
+
{
|
|
127
|
+
this._broadcaster = pBroadcaster;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_emitProgress(pOperationId, pPayload)
|
|
131
|
+
{
|
|
132
|
+
if (this._broadcaster && pOperationId)
|
|
133
|
+
{
|
|
134
|
+
this._broadcaster.broadcastProgress(pOperationId, pPayload);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_isCancelled(pOperationId)
|
|
139
|
+
{
|
|
140
|
+
return !!(this._broadcaster && pOperationId && this._broadcaster.isCancelled(pOperationId));
|
|
141
|
+
}
|
|
142
|
+
|
|
78
143
|
/**
|
|
79
144
|
* Check if a file extension is convertible to EPUB.
|
|
80
145
|
*
|
|
@@ -86,6 +151,17 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
86
151
|
return !!_ConvertibleExtensions[pExtension];
|
|
87
152
|
}
|
|
88
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Check if a file extension can be converted to PDF for viewing.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} pExtension - Lowercase file extension (no dot)
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
isPdfConvertible(pExtension)
|
|
161
|
+
{
|
|
162
|
+
return !!_PdfConvertibleExtensions[pExtension];
|
|
163
|
+
}
|
|
164
|
+
|
|
89
165
|
/**
|
|
90
166
|
* Get the cache directory for a specific ebook file.
|
|
91
167
|
* The key is based on the absolute path and modification time,
|
|
@@ -295,6 +371,145 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
295
371
|
}
|
|
296
372
|
}
|
|
297
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Convert a document to PDF via the orator-conversion doc-to-pdf converter.
|
|
376
|
+
* Falls back to ebook-convert if the conversion service is not available.
|
|
377
|
+
* Results are cached for fast repeated access.
|
|
378
|
+
*
|
|
379
|
+
* @param {string} pAbsPath - Absolute path to the source document
|
|
380
|
+
* @param {string} pRelPath - Relative path (for the response)
|
|
381
|
+
* @param {Function} fCallback - Callback(pError, pResult)
|
|
382
|
+
*/
|
|
383
|
+
convertToPdf(pAbsPath, pRelPath, fCallback)
|
|
384
|
+
{
|
|
385
|
+
let tmpSelf = this;
|
|
386
|
+
|
|
387
|
+
// Get file stats for cache key
|
|
388
|
+
let tmpStat;
|
|
389
|
+
try
|
|
390
|
+
{
|
|
391
|
+
tmpStat = libFs.statSync(pAbsPath);
|
|
392
|
+
}
|
|
393
|
+
catch (pError)
|
|
394
|
+
{
|
|
395
|
+
return fCallback(new Error('File not found.'));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let tmpCacheDir = this._getCacheDir(pAbsPath, tmpStat.mtimeMs);
|
|
399
|
+
|
|
400
|
+
// Check for cached manifest
|
|
401
|
+
let tmpManifestPath = libPath.join(tmpCacheDir, 'manifest-pdf.json');
|
|
402
|
+
if (libFs.existsSync(tmpManifestPath))
|
|
403
|
+
{
|
|
404
|
+
try
|
|
405
|
+
{
|
|
406
|
+
let tmpManifest = JSON.parse(libFs.readFileSync(tmpManifestPath, 'utf8'));
|
|
407
|
+
let tmpOutputPath = libPath.join(tmpCacheDir, tmpManifest.OutputFilename);
|
|
408
|
+
if (libFs.existsSync(tmpOutputPath))
|
|
409
|
+
{
|
|
410
|
+
this.fable.log.info(`PDF conversion cache hit for ${pRelPath}`);
|
|
411
|
+
return fCallback(null, tmpManifest);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (pError)
|
|
415
|
+
{
|
|
416
|
+
// Corrupted manifest, regenerate
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Ensure cache directory exists
|
|
421
|
+
if (!libFs.existsSync(tmpCacheDir))
|
|
422
|
+
{
|
|
423
|
+
libFs.mkdirSync(tmpCacheDir, { recursive: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let tmpOutputFilename = 'converted.pdf';
|
|
427
|
+
let tmpOutputPath = libPath.join(tmpCacheDir, tmpOutputFilename);
|
|
428
|
+
let tmpExt = libPath.extname(pAbsPath).replace(/^\./, '').toLowerCase();
|
|
429
|
+
|
|
430
|
+
this.fable.log.info(`Converting document to PDF: ${pRelPath}`);
|
|
431
|
+
|
|
432
|
+
// Try orator-conversion doc-to-pdf converter first
|
|
433
|
+
if (this._conversionService && this._conversionService.converters['doc-to-pdf'])
|
|
434
|
+
{
|
|
435
|
+
let tmpInputBuffer = libFs.readFileSync(pAbsPath);
|
|
436
|
+
let tmpMockRequest = { query: { ext: tmpExt }, params: {} };
|
|
437
|
+
|
|
438
|
+
this._conversionService.converters['doc-to-pdf'](tmpInputBuffer, tmpMockRequest,
|
|
439
|
+
(pConvertError, pPdfBuffer) =>
|
|
440
|
+
{
|
|
441
|
+
if (!pConvertError && pPdfBuffer && pPdfBuffer.length > 0)
|
|
442
|
+
{
|
|
443
|
+
libFs.writeFileSync(tmpOutputPath, pPdfBuffer);
|
|
444
|
+
return tmpSelf._finishPdfConversion(tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, 'orator-conversion', fCallback);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Fall back to ebook-convert
|
|
448
|
+
tmpSelf.fable.log.info(`Orator-conversion failed, trying ebook-convert: ${pConvertError ? pConvertError.message : 'empty output'}`);
|
|
449
|
+
tmpSelf._convertToPdfLocal(pAbsPath, tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, fCallback);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
else
|
|
453
|
+
{
|
|
454
|
+
// No orator-conversion doc-to-pdf — fall back to ebook-convert
|
|
455
|
+
this._convertToPdfLocal(pAbsPath, tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, fCallback);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Convert to PDF locally using Calibre's ebook-convert.
|
|
461
|
+
*/
|
|
462
|
+
_convertToPdfLocal(pAbsPath, pOutputPath, pOutputFilename, pCacheDir, pManifestPath, pRelPath, fCallback)
|
|
463
|
+
{
|
|
464
|
+
try
|
|
465
|
+
{
|
|
466
|
+
let tmpCmd = `ebook-convert "${pAbsPath}" "${pOutputPath}"`;
|
|
467
|
+
libChildProcess.execSync(tmpCmd, { stdio: 'ignore', timeout: 120000 });
|
|
468
|
+
|
|
469
|
+
if (!libFs.existsSync(pOutputPath))
|
|
470
|
+
{
|
|
471
|
+
return fCallback(new Error('ebook-convert produced no output file.'));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return this._finishPdfConversion(pOutputPath, pOutputFilename, pCacheDir, pManifestPath, pRelPath, 'ebook-convert', fCallback);
|
|
475
|
+
}
|
|
476
|
+
catch (pError)
|
|
477
|
+
{
|
|
478
|
+
return fCallback(new Error('Document conversion failed: ' + pError.message));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Finalize a PDF conversion: write manifest and return result.
|
|
484
|
+
*/
|
|
485
|
+
_finishPdfConversion(pOutputPath, pOutputFilename, pCacheDir, pManifestPath, pRelPath, pTool, fCallback)
|
|
486
|
+
{
|
|
487
|
+
let tmpOutputStat = libFs.statSync(pOutputPath);
|
|
488
|
+
|
|
489
|
+
let tmpResult =
|
|
490
|
+
{
|
|
491
|
+
Success: true,
|
|
492
|
+
SourcePath: pRelPath,
|
|
493
|
+
CacheKey: libPath.basename(pCacheDir),
|
|
494
|
+
OutputFilename: pOutputFilename,
|
|
495
|
+
FileSize: tmpOutputStat.size,
|
|
496
|
+
ConvertedAt: new Date().toISOString(),
|
|
497
|
+
ConvertedWith: pTool
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
try
|
|
501
|
+
{
|
|
502
|
+
libFs.writeFileSync(pManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
503
|
+
}
|
|
504
|
+
catch (pWriteError)
|
|
505
|
+
{
|
|
506
|
+
this.fable.log.warn(`Could not write PDF manifest: ${pWriteError.message}`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
this.fable.log.info(`Converted to PDF: ${pRelPath} (${tmpOutputStat.size} bytes, via ${pTool})`);
|
|
510
|
+
return fCallback(null, tmpResult);
|
|
511
|
+
}
|
|
512
|
+
|
|
298
513
|
/**
|
|
299
514
|
* Get the absolute path to a cached converted ebook file.
|
|
300
515
|
*
|
|
@@ -81,6 +81,9 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
81
81
|
|
|
82
82
|
// Ultravisor dispatcher — set via setDispatcher()
|
|
83
83
|
this._dispatcher = null;
|
|
84
|
+
|
|
85
|
+
// Operation broadcaster — set via setBroadcaster()
|
|
86
|
+
this._broadcaster = null;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
/**
|
|
@@ -93,6 +96,35 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
93
96
|
this._dispatcher = pDispatcher;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Set the operation broadcaster for progress events and cancellation.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} pBroadcaster - RetoldRemoteOperationBroadcaster instance
|
|
103
|
+
*/
|
|
104
|
+
setBroadcaster(pBroadcaster)
|
|
105
|
+
{
|
|
106
|
+
this._broadcaster = pBroadcaster;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Emit a progress event if a broadcaster is attached and an opId was supplied.
|
|
111
|
+
*/
|
|
112
|
+
_emitProgress(pOperationId, pPayload)
|
|
113
|
+
{
|
|
114
|
+
if (this._broadcaster && pOperationId)
|
|
115
|
+
{
|
|
116
|
+
this._broadcaster.broadcastProgress(pOperationId, pPayload);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check whether a given operation has been cancelled.
|
|
122
|
+
*/
|
|
123
|
+
_isCancelled(pOperationId)
|
|
124
|
+
{
|
|
125
|
+
return !!(this._broadcaster && pOperationId && this._broadcaster.isCancelled(pOperationId));
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
/**
|
|
97
129
|
* Check if sharp is available.
|
|
98
130
|
*
|
|
@@ -776,6 +808,49 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
776
808
|
let tmpOutputPath = libPath.join(tmpCacheDir, tmpManifest.OutputFilename);
|
|
777
809
|
if (libFs.existsSync(tmpOutputPath))
|
|
778
810
|
{
|
|
811
|
+
// Repair stale manifests written by an older build that
|
|
812
|
+
// hardcoded OrigWidth/OrigHeight to 0 when using the
|
|
813
|
+
// dispatcher path. The front-end relies on these values to
|
|
814
|
+
// (a) display dimensions in the info bar and (b) decide
|
|
815
|
+
// whether to auto-trigger DZI tile generation. If we read
|
|
816
|
+
// them as 0 we can patch the manifest in place using sharp
|
|
817
|
+
// metadata, no full regeneration needed.
|
|
818
|
+
if ((!tmpManifest.OrigWidth || !tmpManifest.OrigHeight) && this._sharp)
|
|
819
|
+
{
|
|
820
|
+
try
|
|
821
|
+
{
|
|
822
|
+
this._sharp(pAbsPath, { limitInputPixels: false }).metadata()
|
|
823
|
+
.then((pMeta) =>
|
|
824
|
+
{
|
|
825
|
+
if (pMeta && pMeta.width && pMeta.height)
|
|
826
|
+
{
|
|
827
|
+
tmpManifest.OrigWidth = pMeta.width;
|
|
828
|
+
tmpManifest.OrigHeight = pMeta.height;
|
|
829
|
+
try
|
|
830
|
+
{
|
|
831
|
+
libFs.writeFileSync(tmpManifestPath, JSON.stringify(tmpManifest, null, '\t'));
|
|
832
|
+
this.fable.log.info(`Image preview manifest repaired with dimensions ${pMeta.width}×${pMeta.height} for ${pRelPath}`);
|
|
833
|
+
}
|
|
834
|
+
catch (pWriteError)
|
|
835
|
+
{
|
|
836
|
+
// Non-fatal — manifest still serves the cached image
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
this.fable.log.info(`Image preview cache hit for ${pRelPath}`);
|
|
840
|
+
return fCallback(null, tmpManifest);
|
|
841
|
+
})
|
|
842
|
+
.catch(() =>
|
|
843
|
+
{
|
|
844
|
+
this.fable.log.info(`Image preview cache hit for ${pRelPath}`);
|
|
845
|
+
return fCallback(null, tmpManifest);
|
|
846
|
+
});
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
catch (pSharpError)
|
|
850
|
+
{
|
|
851
|
+
// Fall through to the original cache-hit return
|
|
852
|
+
}
|
|
853
|
+
}
|
|
779
854
|
this.fable.log.info(`Image preview cache hit for ${pRelPath}`);
|
|
780
855
|
return fCallback(null, tmpManifest);
|
|
781
856
|
}
|
|
@@ -1146,57 +1221,95 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1146
1221
|
return fCallback(new Error('File is outside content root.'));
|
|
1147
1222
|
}
|
|
1148
1223
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1224
|
+
// Read the source image's true dimensions BEFORE dispatching the resize.
|
|
1225
|
+
// We need OrigWidth/OrigHeight in the response so the front-end can:
|
|
1226
|
+
// 1. Display the correct image dimensions in the info bar
|
|
1227
|
+
// 2. Decide whether to auto-trigger DZI tile generation (>4096px on
|
|
1228
|
+
// either side)
|
|
1229
|
+
// Sharp's metadata read is just a header parse — extremely fast — and
|
|
1230
|
+
// is available locally in stack mode. If sharp is not available, we
|
|
1231
|
+
// fall back to 0,0 (which mirrors the older behavior but at least keeps
|
|
1232
|
+
// the dispatch working).
|
|
1233
|
+
let _readSourceMetadata = function (pCallback)
|
|
1158
1234
|
{
|
|
1159
|
-
if (
|
|
1235
|
+
if (!tmpSelf._sharp)
|
|
1160
1236
|
{
|
|
1161
|
-
return
|
|
1237
|
+
return pCallback(null, { width: 0, height: 0 });
|
|
1162
1238
|
}
|
|
1163
|
-
|
|
1164
1239
|
try
|
|
1165
1240
|
{
|
|
1166
|
-
|
|
1241
|
+
tmpSelf._sharp(pInputPath, { limitInputPixels: false }).metadata()
|
|
1242
|
+
.then(function (pMeta)
|
|
1243
|
+
{
|
|
1244
|
+
pCallback(null, { width: pMeta.width || 0, height: pMeta.height || 0 });
|
|
1245
|
+
})
|
|
1246
|
+
.catch(function (pError)
|
|
1247
|
+
{
|
|
1248
|
+
tmpSelf.fable.log.warn(`Could not read source metadata for ${pRelPath}: ${pError.message}`);
|
|
1249
|
+
pCallback(null, { width: 0, height: 0 });
|
|
1250
|
+
});
|
|
1167
1251
|
}
|
|
1168
|
-
catch (
|
|
1252
|
+
catch (pSyncError)
|
|
1169
1253
|
{
|
|
1170
|
-
|
|
1254
|
+
tmpSelf.fable.log.warn(`Sharp metadata threw for ${pRelPath}: ${pSyncError.message}`);
|
|
1255
|
+
return pCallback(null, { width: 0, height: 0 });
|
|
1171
1256
|
}
|
|
1257
|
+
};
|
|
1172
1258
|
|
|
1173
|
-
|
|
1259
|
+
_readSourceMetadata(function (pMetaErr, pSourceMeta)
|
|
1260
|
+
{
|
|
1261
|
+
tmpSelf._dispatcher.triggerOperation('rr-image-thumbnail',
|
|
1174
1262
|
{
|
|
1175
|
-
|
|
1176
|
-
SourcePath: pRelPath,
|
|
1177
|
-
CacheKey: pCacheKey,
|
|
1178
|
-
OutputFilename: pOutputFilename,
|
|
1263
|
+
ImageAddress: '>retold-remote/File/' + tmpRelPath,
|
|
1179
1264
|
Width: pMaxDim,
|
|
1180
1265
|
Height: pMaxDim,
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
IsRawFormat: pIsRaw,
|
|
1186
|
-
GeneratedAt: new Date().toISOString()
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
try
|
|
1266
|
+
Format: 'jpeg',
|
|
1267
|
+
Quality: tmpSelf.options.PreviewQuality || 85
|
|
1268
|
+
},
|
|
1269
|
+
(pTriggerError, pResult) =>
|
|
1190
1270
|
{
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
tmpSelf.fable.log.warn(`Could not write preview manifest: ${pWriteError.message}`);
|
|
1196
|
-
}
|
|
1271
|
+
if (pTriggerError || !pResult || !pResult.OutputBuffer)
|
|
1272
|
+
{
|
|
1273
|
+
return fCallback(new Error('Operation trigger preview generation failed: ' + (pTriggerError ? pTriggerError.message : 'no output')));
|
|
1274
|
+
}
|
|
1197
1275
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1276
|
+
try
|
|
1277
|
+
{
|
|
1278
|
+
libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
|
|
1279
|
+
}
|
|
1280
|
+
catch (pWriteError)
|
|
1281
|
+
{
|
|
1282
|
+
return fCallback(new Error('Failed to write preview output: ' + pWriteError.message));
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
let tmpResult =
|
|
1286
|
+
{
|
|
1287
|
+
Success: true,
|
|
1288
|
+
SourcePath: pRelPath,
|
|
1289
|
+
CacheKey: pCacheKey,
|
|
1290
|
+
OutputFilename: pOutputFilename,
|
|
1291
|
+
Width: pMaxDim,
|
|
1292
|
+
Height: pMaxDim,
|
|
1293
|
+
OrigWidth: pSourceMeta.width || 0,
|
|
1294
|
+
OrigHeight: pSourceMeta.height || 0,
|
|
1295
|
+
FileSize: pResult.OutputBuffer.length,
|
|
1296
|
+
NeedsPreview: true,
|
|
1297
|
+
IsRawFormat: pIsRaw,
|
|
1298
|
+
GeneratedAt: new Date().toISOString()
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
try
|
|
1302
|
+
{
|
|
1303
|
+
libFs.writeFileSync(pManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
1304
|
+
}
|
|
1305
|
+
catch (pWriteError)
|
|
1306
|
+
{
|
|
1307
|
+
tmpSelf.fable.log.warn(`Could not write preview manifest: ${pWriteError.message}`);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
tmpSelf.fable.log.info(`Generated image preview (operation trigger): ${pRelPath} (${tmpResult.OrigWidth}×${tmpResult.OrigHeight})`);
|
|
1311
|
+
return fCallback(null, tmpResult);
|
|
1312
|
+
});
|
|
1200
1313
|
});
|
|
1201
1314
|
}
|
|
1202
1315
|
|
|
@@ -1215,13 +1328,29 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1215
1328
|
* @param {string} pRelPath - Relative path (for the response)
|
|
1216
1329
|
* @param {Function} fCallback - Callback(pError, pResult)
|
|
1217
1330
|
*/
|
|
1218
|
-
generateDziTiles(pAbsPath, pRelPath, fCallback)
|
|
1331
|
+
generateDziTiles(pAbsPath, pRelPath, pOptionsOrCallback, fCallback)
|
|
1219
1332
|
{
|
|
1220
1333
|
let tmpSelf = this;
|
|
1221
1334
|
|
|
1335
|
+
// Backward-compatible: if third arg is a function, it's the callback
|
|
1336
|
+
// (no options). Otherwise it's an options bag.
|
|
1337
|
+
let tmpOptions;
|
|
1338
|
+
let tmpCallback;
|
|
1339
|
+
if (typeof pOptionsOrCallback === 'function')
|
|
1340
|
+
{
|
|
1341
|
+
tmpOptions = {};
|
|
1342
|
+
tmpCallback = pOptionsOrCallback;
|
|
1343
|
+
}
|
|
1344
|
+
else
|
|
1345
|
+
{
|
|
1346
|
+
tmpOptions = pOptionsOrCallback || {};
|
|
1347
|
+
tmpCallback = fCallback;
|
|
1348
|
+
}
|
|
1349
|
+
let tmpOpId = tmpOptions.OperationId || null;
|
|
1350
|
+
|
|
1222
1351
|
if (!this._sharp)
|
|
1223
1352
|
{
|
|
1224
|
-
return
|
|
1353
|
+
return tmpCallback(new Error('sharp is not available.'));
|
|
1225
1354
|
}
|
|
1226
1355
|
|
|
1227
1356
|
// Get file stats for cache key
|
|
@@ -1232,7 +1361,7 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1232
1361
|
}
|
|
1233
1362
|
catch (pError)
|
|
1234
1363
|
{
|
|
1235
|
-
return
|
|
1364
|
+
return tmpCallback(new Error('File not found.'));
|
|
1236
1365
|
}
|
|
1237
1366
|
|
|
1238
1367
|
let tmpCacheKey = this._buildCacheKey(pAbsPath, tmpStat.mtimeMs, 'dzi');
|
|
@@ -1250,7 +1379,8 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1250
1379
|
if (libFs.existsSync(tmpDziPath))
|
|
1251
1380
|
{
|
|
1252
1381
|
this.fable.log.info(`DZI tile cache hit for ${pRelPath}`);
|
|
1253
|
-
|
|
1382
|
+
this._emitProgress(tmpOpId, { Phase: 'cached', Message: 'Cached tiles available' });
|
|
1383
|
+
return tmpCallback(null, tmpManifest);
|
|
1254
1384
|
}
|
|
1255
1385
|
}
|
|
1256
1386
|
catch (pError)
|
|
@@ -1259,12 +1389,19 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1259
1389
|
}
|
|
1260
1390
|
}
|
|
1261
1391
|
|
|
1392
|
+
// Cancellation gate before the long work starts
|
|
1393
|
+
if (this._isCancelled(tmpOpId))
|
|
1394
|
+
{
|
|
1395
|
+
return tmpCallback(new Error('Cancelled'));
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1262
1398
|
// If another request is already generating tiles for this file,
|
|
1263
1399
|
// queue our callback to receive the same result.
|
|
1264
1400
|
if (this._dziInFlight.has(tmpCacheKey))
|
|
1265
1401
|
{
|
|
1266
1402
|
this.fable.log.info(`DZI tiles already generating, queuing: ${pRelPath}`);
|
|
1267
|
-
this.
|
|
1403
|
+
this._emitProgress(tmpOpId, { Phase: 'queued', Message: 'Tile generation already in progress, waiting' });
|
|
1404
|
+
this._dziInFlight.get(tmpCacheKey).push(tmpCallback);
|
|
1268
1405
|
return;
|
|
1269
1406
|
}
|
|
1270
1407
|
this._dziInFlight.set(tmpCacheKey, []);
|
|
@@ -1276,10 +1413,17 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1276
1413
|
}
|
|
1277
1414
|
|
|
1278
1415
|
this.fable.log.info(`Generating DZI tiles: ${pRelPath}`);
|
|
1416
|
+
this._emitProgress(tmpOpId,
|
|
1417
|
+
{
|
|
1418
|
+
Phase: 'reading',
|
|
1419
|
+
Message: 'Reading image',
|
|
1420
|
+
Cancelable: true
|
|
1421
|
+
});
|
|
1279
1422
|
|
|
1280
1423
|
// For raw formats, convert first then tile the converted file
|
|
1281
1424
|
if (this._isRawFormat(pAbsPath))
|
|
1282
1425
|
{
|
|
1426
|
+
this._emitProgress(tmpOpId, { Phase: 'raw-convert', Message: 'Converting raw camera file' });
|
|
1283
1427
|
this._ensureConvertedRaw(pAbsPath, tmpStat.mtimeMs, true, (pError, pConvertedPath) =>
|
|
1284
1428
|
{
|
|
1285
1429
|
if (pError)
|
|
@@ -1291,14 +1435,19 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1291
1435
|
{
|
|
1292
1436
|
tmpWaiters[i](tmpErr);
|
|
1293
1437
|
}
|
|
1294
|
-
return
|
|
1438
|
+
return tmpCallback(tmpErr);
|
|
1439
|
+
}
|
|
1440
|
+
if (tmpSelf._isCancelled(tmpOpId))
|
|
1441
|
+
{
|
|
1442
|
+
tmpSelf._dziInFlight.delete(tmpCacheKey);
|
|
1443
|
+
return tmpCallback(new Error('Cancelled'));
|
|
1295
1444
|
}
|
|
1296
|
-
tmpSelf._doGenerateDziTiles(pConvertedPath, pRelPath, tmpCacheKey, tmpCacheDir, tmpManifestPath,
|
|
1445
|
+
tmpSelf._doGenerateDziTiles(pConvertedPath, pRelPath, tmpCacheKey, tmpCacheDir, tmpManifestPath, tmpOpId, tmpCallback);
|
|
1297
1446
|
});
|
|
1298
1447
|
}
|
|
1299
1448
|
else
|
|
1300
1449
|
{
|
|
1301
|
-
this._doGenerateDziTiles(pAbsPath, pRelPath, tmpCacheKey, tmpCacheDir, tmpManifestPath,
|
|
1450
|
+
this._doGenerateDziTiles(pAbsPath, pRelPath, tmpCacheKey, tmpCacheDir, tmpManifestPath, tmpOpId, tmpCallback);
|
|
1302
1451
|
}
|
|
1303
1452
|
}
|
|
1304
1453
|
|
|
@@ -1310,21 +1459,46 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1310
1459
|
* @param {string} pCacheKey - Cache key
|
|
1311
1460
|
* @param {string} pCacheDir - Cache directory
|
|
1312
1461
|
* @param {string} pManifestPath - Manifest file path
|
|
1462
|
+
* @param {string} pOpId - Optional operation id for progress events
|
|
1313
1463
|
* @param {Function} fCallback - Callback(pError, pResult)
|
|
1314
1464
|
*/
|
|
1315
|
-
_doGenerateDziTiles(pInputPath, pRelPath, pCacheKey, pCacheDir, pManifestPath, fCallback)
|
|
1465
|
+
_doGenerateDziTiles(pInputPath, pRelPath, pCacheKey, pCacheDir, pManifestPath, pOpId, fCallback)
|
|
1316
1466
|
{
|
|
1317
1467
|
let tmpSelf = this;
|
|
1318
1468
|
|
|
1469
|
+
// Cancellation gate before metadata read
|
|
1470
|
+
if (this._isCancelled(pOpId))
|
|
1471
|
+
{
|
|
1472
|
+
this._dziInFlight.delete(pCacheKey);
|
|
1473
|
+
return fCallback(new Error('Cancelled'));
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
this._emitProgress(pOpId, { Phase: 'metadata', Message: 'Reading image dimensions', Cancelable: true });
|
|
1477
|
+
|
|
1319
1478
|
// Get metadata first
|
|
1320
1479
|
this._sharp(pInputPath, { limitInputPixels: false }).metadata()
|
|
1321
1480
|
.then((pMetadata) =>
|
|
1322
1481
|
{
|
|
1482
|
+
// Cancellation gate after metadata, before the expensive
|
|
1483
|
+
// tile-generation sharp call
|
|
1484
|
+
if (tmpSelf._isCancelled(pOpId))
|
|
1485
|
+
{
|
|
1486
|
+
tmpSelf._dziInFlight.delete(pCacheKey);
|
|
1487
|
+
return fCallback(new Error('Cancelled'));
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1323
1490
|
let tmpTileSize = tmpSelf.options.DziTileSize;
|
|
1324
1491
|
let tmpOverlap = tmpSelf.options.DziOverlap;
|
|
1325
1492
|
let tmpFormat = tmpSelf.options.DziFormat;
|
|
1326
1493
|
let tmpQuality = tmpSelf.options.DziQuality;
|
|
1327
1494
|
|
|
1495
|
+
tmpSelf._emitProgress(pOpId,
|
|
1496
|
+
{
|
|
1497
|
+
Phase: 'tiling',
|
|
1498
|
+
Message: 'Generating tiles for ' + pMetadata.width + '\u00d7' + pMetadata.height + ' image',
|
|
1499
|
+
Cancelable: true
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1328
1502
|
// The output filename for sharp's tile() is based on the
|
|
1329
1503
|
// input to toFile() — sharp generates:
|
|
1330
1504
|
// {basename}.dzi (the XML descriptor)
|
|
@@ -1383,6 +1557,7 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
1383
1557
|
}
|
|
1384
1558
|
|
|
1385
1559
|
tmpSelf.fable.log.info(`Generated DZI tiles: ${pRelPath} (${pMetadata.width}×${pMetadata.height})`);
|
|
1560
|
+
tmpSelf._emitProgress(pOpId, { Phase: 'done', Message: 'Tile generation complete' });
|
|
1386
1561
|
|
|
1387
1562
|
// Notify queued waiters
|
|
1388
1563
|
let tmpWaiters = tmpSelf._dziInFlight.get(pCacheKey) || [];
|