retold-remote 0.0.22 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/css/retold-remote.css +87 -20
  2. package/docs/README.md +59 -11
  3. package/docs/_sidebar.md +1 -0
  4. package/docs/collections.md +30 -0
  5. package/docs/ebook-reader.md +75 -1
  6. package/docs/image-explorer.md +27 -1
  7. package/docs/server-setup.md +28 -18
  8. package/docs/stack-launcher.md +218 -0
  9. package/docs/ultravisor-integration.md +2 -0
  10. package/package.json +10 -7
  11. package/source/Pict-Application-RetoldRemote.js +2 -0
  12. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  13. package/source/cli/RetoldRemote-Server-Setup.js +240 -2
  14. package/source/cli/RetoldRemote-Stack-Launcher.js +387 -0
  15. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  16. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  17. package/source/providers/CollectionManager-AddItems.js +166 -0
  18. package/source/providers/Pict-Provider-GalleryNavigation.js +46 -0
  19. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +5 -0
  20. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  21. package/source/server/RetoldRemote-CollectionExportService.js +696 -0
  22. package/source/server/RetoldRemote-CollectionService.js +5 -0
  23. package/source/server/RetoldRemote-EbookService.js +194 -3
  24. package/source/server/RetoldRemote-SubimageService.js +530 -0
  25. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  26. package/source/server/RetoldRemote-UltravisorOperations.js +6 -6
  27. package/source/views/MediaViewer-EbookViewer.js +419 -1
  28. package/source/views/MediaViewer-PdfViewer.js +963 -0
  29. package/source/views/PictView-Remote-CollectionsPanel.js +166 -0
  30. package/source/views/PictView-Remote-ImageExplorer.js +606 -1
  31. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  32. package/source/views/PictView-Remote-Layout.js +12 -0
  33. package/source/views/PictView-Remote-MediaViewer.js +83 -25
  34. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  35. package/web-application/css/retold-remote.css +87 -20
  36. package/web-application/docs/README.md +59 -11
  37. package/web-application/docs/_sidebar.md +1 -0
  38. package/web-application/docs/collections.md +30 -0
  39. package/web-application/docs/ebook-reader.md +75 -1
  40. package/web-application/docs/image-explorer.md +27 -1
  41. package/web-application/docs/server-setup.md +28 -18
  42. package/web-application/docs/stack-launcher.md +218 -0
  43. package/web-application/docs/ultravisor-integration.md +2 -0
  44. package/web-application/retold-remote.js +399 -45
  45. package/web-application/retold-remote.js.map +1 -1
  46. package/web-application/retold-remote.min.js +13 -12
  47. package/web-application/retold-remote.min.js.map +1 -1
@@ -1,12 +1,19 @@
1
1
  /**
2
- * Retold Remote -- Ebook Conversion Service
2
+ * Retold Remote -- Document Conversion Service
3
3
  *
4
- * Converts MOBI/AZW/KF8 ebooks to EPUB using Calibre's ebook-convert tool.
5
- * Conversions are cached so repeated requests are instant.
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
  *