retold-remote 0.0.22 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/css/retold-remote.css +87 -20
- package/docs/README.md +59 -11
- package/docs/_sidebar.md +1 -0
- package/docs/collections.md +30 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +27 -1
- package/docs/server-setup.md +28 -18
- package/docs/stack-launcher.md +218 -0
- package/docs/ultravisor-integration.md +2 -0
- package/package.json +10 -7
- package/source/Pict-Application-RetoldRemote.js +2 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +240 -2
- package/source/cli/RetoldRemote-Stack-Launcher.js +387 -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 +46 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +5 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-CollectionExportService.js +696 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +194 -3
- package/source/server/RetoldRemote-SubimageService.js +530 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorOperations.js +6 -6
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +963 -0
- package/source/views/PictView-Remote-CollectionsPanel.js +166 -0
- package/source/views/PictView-Remote-ImageExplorer.js +606 -1
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +12 -0
- package/source/views/PictView-Remote-MediaViewer.js +83 -25
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/web-application/css/retold-remote.css +87 -20
- package/web-application/docs/README.md +59 -11
- package/web-application/docs/_sidebar.md +1 -0
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +27 -1
- package/web-application/docs/server-setup.md +28 -18
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/ultravisor-integration.md +2 -0
- package/web-application/retold-remote.js +399 -45
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +13 -12
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -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,23 @@ 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
|
+
|
|
65
95
|
this.fable.log.info('Ebook Service: using ParimeBinaryStorage (category: ebook-cache)');
|
|
66
96
|
}
|
|
67
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Set the orator-conversion service reference for document conversion.
|
|
100
|
+
* Called from Server-Setup after the conversion service is instantiated.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} pService - OratorFileTranslation instance
|
|
103
|
+
*/
|
|
104
|
+
setConversionService(pService)
|
|
105
|
+
{
|
|
106
|
+
this._conversionService = pService;
|
|
107
|
+
}
|
|
108
|
+
|
|
68
109
|
/**
|
|
69
110
|
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
70
111
|
*
|
|
@@ -86,6 +127,17 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
86
127
|
return !!_ConvertibleExtensions[pExtension];
|
|
87
128
|
}
|
|
88
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Check if a file extension can be converted to PDF for viewing.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} pExtension - Lowercase file extension (no dot)
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
isPdfConvertible(pExtension)
|
|
137
|
+
{
|
|
138
|
+
return !!_PdfConvertibleExtensions[pExtension];
|
|
139
|
+
}
|
|
140
|
+
|
|
89
141
|
/**
|
|
90
142
|
* Get the cache directory for a specific ebook file.
|
|
91
143
|
* The key is based on the absolute path and modification time,
|
|
@@ -295,6 +347,145 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
295
347
|
}
|
|
296
348
|
}
|
|
297
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Convert a document to PDF via the orator-conversion doc-to-pdf converter.
|
|
352
|
+
* Falls back to ebook-convert if the conversion service is not available.
|
|
353
|
+
* Results are cached for fast repeated access.
|
|
354
|
+
*
|
|
355
|
+
* @param {string} pAbsPath - Absolute path to the source document
|
|
356
|
+
* @param {string} pRelPath - Relative path (for the response)
|
|
357
|
+
* @param {Function} fCallback - Callback(pError, pResult)
|
|
358
|
+
*/
|
|
359
|
+
convertToPdf(pAbsPath, pRelPath, fCallback)
|
|
360
|
+
{
|
|
361
|
+
let tmpSelf = this;
|
|
362
|
+
|
|
363
|
+
// Get file stats for cache key
|
|
364
|
+
let tmpStat;
|
|
365
|
+
try
|
|
366
|
+
{
|
|
367
|
+
tmpStat = libFs.statSync(pAbsPath);
|
|
368
|
+
}
|
|
369
|
+
catch (pError)
|
|
370
|
+
{
|
|
371
|
+
return fCallback(new Error('File not found.'));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let tmpCacheDir = this._getCacheDir(pAbsPath, tmpStat.mtimeMs);
|
|
375
|
+
|
|
376
|
+
// Check for cached manifest
|
|
377
|
+
let tmpManifestPath = libPath.join(tmpCacheDir, 'manifest-pdf.json');
|
|
378
|
+
if (libFs.existsSync(tmpManifestPath))
|
|
379
|
+
{
|
|
380
|
+
try
|
|
381
|
+
{
|
|
382
|
+
let tmpManifest = JSON.parse(libFs.readFileSync(tmpManifestPath, 'utf8'));
|
|
383
|
+
let tmpOutputPath = libPath.join(tmpCacheDir, tmpManifest.OutputFilename);
|
|
384
|
+
if (libFs.existsSync(tmpOutputPath))
|
|
385
|
+
{
|
|
386
|
+
this.fable.log.info(`PDF conversion cache hit for ${pRelPath}`);
|
|
387
|
+
return fCallback(null, tmpManifest);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (pError)
|
|
391
|
+
{
|
|
392
|
+
// Corrupted manifest, regenerate
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Ensure cache directory exists
|
|
397
|
+
if (!libFs.existsSync(tmpCacheDir))
|
|
398
|
+
{
|
|
399
|
+
libFs.mkdirSync(tmpCacheDir, { recursive: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let tmpOutputFilename = 'converted.pdf';
|
|
403
|
+
let tmpOutputPath = libPath.join(tmpCacheDir, tmpOutputFilename);
|
|
404
|
+
let tmpExt = libPath.extname(pAbsPath).replace(/^\./, '').toLowerCase();
|
|
405
|
+
|
|
406
|
+
this.fable.log.info(`Converting document to PDF: ${pRelPath}`);
|
|
407
|
+
|
|
408
|
+
// Try orator-conversion doc-to-pdf converter first
|
|
409
|
+
if (this._conversionService && this._conversionService.converters['doc-to-pdf'])
|
|
410
|
+
{
|
|
411
|
+
let tmpInputBuffer = libFs.readFileSync(pAbsPath);
|
|
412
|
+
let tmpMockRequest = { query: { ext: tmpExt }, params: {} };
|
|
413
|
+
|
|
414
|
+
this._conversionService.converters['doc-to-pdf'](tmpInputBuffer, tmpMockRequest,
|
|
415
|
+
(pConvertError, pPdfBuffer) =>
|
|
416
|
+
{
|
|
417
|
+
if (!pConvertError && pPdfBuffer && pPdfBuffer.length > 0)
|
|
418
|
+
{
|
|
419
|
+
libFs.writeFileSync(tmpOutputPath, pPdfBuffer);
|
|
420
|
+
return tmpSelf._finishPdfConversion(tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, 'orator-conversion', fCallback);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Fall back to ebook-convert
|
|
424
|
+
tmpSelf.fable.log.info(`Orator-conversion failed, trying ebook-convert: ${pConvertError ? pConvertError.message : 'empty output'}`);
|
|
425
|
+
tmpSelf._convertToPdfLocal(pAbsPath, tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, fCallback);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
else
|
|
429
|
+
{
|
|
430
|
+
// No orator-conversion doc-to-pdf — fall back to ebook-convert
|
|
431
|
+
this._convertToPdfLocal(pAbsPath, tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, fCallback);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Convert to PDF locally using Calibre's ebook-convert.
|
|
437
|
+
*/
|
|
438
|
+
_convertToPdfLocal(pAbsPath, pOutputPath, pOutputFilename, pCacheDir, pManifestPath, pRelPath, fCallback)
|
|
439
|
+
{
|
|
440
|
+
try
|
|
441
|
+
{
|
|
442
|
+
let tmpCmd = `ebook-convert "${pAbsPath}" "${pOutputPath}"`;
|
|
443
|
+
libChildProcess.execSync(tmpCmd, { stdio: 'ignore', timeout: 120000 });
|
|
444
|
+
|
|
445
|
+
if (!libFs.existsSync(pOutputPath))
|
|
446
|
+
{
|
|
447
|
+
return fCallback(new Error('ebook-convert produced no output file.'));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return this._finishPdfConversion(pOutputPath, pOutputFilename, pCacheDir, pManifestPath, pRelPath, 'ebook-convert', fCallback);
|
|
451
|
+
}
|
|
452
|
+
catch (pError)
|
|
453
|
+
{
|
|
454
|
+
return fCallback(new Error('Document conversion failed: ' + pError.message));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Finalize a PDF conversion: write manifest and return result.
|
|
460
|
+
*/
|
|
461
|
+
_finishPdfConversion(pOutputPath, pOutputFilename, pCacheDir, pManifestPath, pRelPath, pTool, fCallback)
|
|
462
|
+
{
|
|
463
|
+
let tmpOutputStat = libFs.statSync(pOutputPath);
|
|
464
|
+
|
|
465
|
+
let tmpResult =
|
|
466
|
+
{
|
|
467
|
+
Success: true,
|
|
468
|
+
SourcePath: pRelPath,
|
|
469
|
+
CacheKey: libPath.basename(pCacheDir),
|
|
470
|
+
OutputFilename: pOutputFilename,
|
|
471
|
+
FileSize: tmpOutputStat.size,
|
|
472
|
+
ConvertedAt: new Date().toISOString(),
|
|
473
|
+
ConvertedWith: pTool
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
try
|
|
477
|
+
{
|
|
478
|
+
libFs.writeFileSync(pManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
479
|
+
}
|
|
480
|
+
catch (pWriteError)
|
|
481
|
+
{
|
|
482
|
+
this.fable.log.warn(`Could not write PDF manifest: ${pWriteError.message}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.fable.log.info(`Converted to PDF: ${pRelPath} (${tmpOutputStat.size} bytes, via ${pTool})`);
|
|
486
|
+
return fCallback(null, tmpResult);
|
|
487
|
+
}
|
|
488
|
+
|
|
298
489
|
/**
|
|
299
490
|
* Get the absolute path to a cached converted ebook file.
|
|
300
491
|
*
|