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
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Retold Remote -- Region Service
3
+ *
4
+ * Stores and retrieves labeled regions for any file type: images,
5
+ * EPUB ebooks, PDF documents, CBZ/CBR comic pages. Each file can
6
+ * have multiple named regions that are persisted in Bibliograph.
7
+ *
8
+ * Region types:
9
+ * - visual-region: rectangular crop area (X, Y, Width, Height)
10
+ * - text-selection: captured text with location (CFI, PageNumber, SelectedText)
11
+ *
12
+ * Handles archive subfile paths (e.g. "comic.cbz/page001.jpg") by
13
+ * resolving to the archive file for existence checks and mtime keys.
14
+ *
15
+ * API:
16
+ * GET /api/media/subimage-regions?path= — List regions for a file
17
+ * POST /api/media/subimage-regions — Add a region
18
+ * PUT /api/media/subimage-regions/:id — Update a region
19
+ * DELETE /api/media/subimage-regions/:id — Remove a region
20
+ *
21
+ * @license MIT
22
+ */
23
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
24
+ const libFs = require('fs');
25
+ const libPath = require('path');
26
+ const libCrypto = require('crypto');
27
+ const libUrl = require('url');
28
+
29
+ const libExplorerStateMixin = require('./RetoldRemote-ExplorerStateMixin');
30
+
31
+ const SUBIMAGE_SOURCE = 'retold-remote-subimage-regions';
32
+
33
+ const _DefaultServiceConfiguration =
34
+ {
35
+ "ContentPath": "."
36
+ };
37
+
38
+ class RetoldRemoteSubimageService extends libFableServiceProviderBase
39
+ {
40
+ constructor(pFable, pOptions, pServiceHash)
41
+ {
42
+ super(pFable, pOptions, pServiceHash);
43
+
44
+ this.serviceType = 'RetoldRemoteSubimageService';
45
+
46
+ // Merge with defaults
47
+ for (let tmpKey in _DefaultServiceConfiguration)
48
+ {
49
+ if (!(tmpKey in this.options))
50
+ {
51
+ this.options[tmpKey] = _DefaultServiceConfiguration[tmpKey];
52
+ }
53
+ }
54
+
55
+ this.contentPath = libPath.resolve(this.options.ContentPath);
56
+
57
+ // Sharp module reference (set by Server-Setup via setSharpModule)
58
+ this._sharp = null;
59
+
60
+ // Apply explorer state persistence mixin for the Bibliograph source
61
+ libExplorerStateMixin.apply(this, SUBIMAGE_SOURCE, 'subimage');
62
+
63
+ this.fable.log.info('Subimage Region Service: regions stored in Bibliograph');
64
+ }
65
+
66
+ /**
67
+ * Set the sharp module reference for thumbnail generation.
68
+ *
69
+ * @param {object} pSharp - The sharp module
70
+ */
71
+ setSharpModule(pSharp)
72
+ {
73
+ this._sharp = pSharp;
74
+ }
75
+
76
+ // Regex to detect archive extensions within a path
77
+ static get ARCHIVE_PATH_PATTERN()
78
+ {
79
+ return /^(.*?\.(zip|7z|rar|tar|tgz|cbz|cbr|tar\.gz|tar\.bz2|tar\.xz))\//i;
80
+ }
81
+
82
+ /**
83
+ * Validate and sanitize a relative path.
84
+ *
85
+ * @param {string} pRelPath - Relative path
86
+ * @returns {string|null} Sanitized path or null if invalid
87
+ */
88
+ _sanitizePath(pRelPath)
89
+ {
90
+ if (!pRelPath || typeof pRelPath !== 'string')
91
+ {
92
+ return null;
93
+ }
94
+ let tmpClean = pRelPath.replace(/^\/+/, '');
95
+ if (tmpClean.includes('..') || libPath.isAbsolute(tmpClean))
96
+ {
97
+ return null;
98
+ }
99
+ return tmpClean;
100
+ }
101
+
102
+ /**
103
+ * Resolve a file path to an absolute path and stat, handling archive subfiles.
104
+ * For paths like "comics/batman.cbz/page001.jpg", the file doesn't exist on disk
105
+ * (it's extracted on the fly), so we resolve to the archive file itself.
106
+ *
107
+ * @param {string} pRelPath - Relative file path
108
+ * @returns {object|null} { absPath, stat } or null if not found
109
+ */
110
+ _resolveFileStat(pRelPath)
111
+ {
112
+ let tmpAbsPath = libPath.join(this.contentPath, pRelPath);
113
+
114
+ // Try direct file first
115
+ if (libFs.existsSync(tmpAbsPath))
116
+ {
117
+ return { absPath: tmpAbsPath, stat: libFs.statSync(tmpAbsPath) };
118
+ }
119
+
120
+ // Check if this is an archive subfile path
121
+ let tmpArchiveMatch = pRelPath.match(RetoldRemoteSubimageService.ARCHIVE_PATH_PATTERN);
122
+ if (tmpArchiveMatch)
123
+ {
124
+ let tmpArchivePath = libPath.join(this.contentPath, tmpArchiveMatch[1]);
125
+ if (libFs.existsSync(tmpArchivePath))
126
+ {
127
+ return { absPath: tmpArchivePath, stat: libFs.statSync(tmpArchivePath) };
128
+ }
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Load the regions record for a file, creating an empty one if none exists.
136
+ *
137
+ * @param {string} pRelPath - Relative file path
138
+ * @param {number} pMtimeMs - File modification time in ms
139
+ * @param {Function} fCallback - Callback(pError, pRecord)
140
+ */
141
+ _loadOrCreateRecord(pRelPath, pMtimeMs, fCallback)
142
+ {
143
+ this.loadExplorerState(pRelPath, pMtimeMs,
144
+ (pError, pRecord) =>
145
+ {
146
+ if (pError)
147
+ {
148
+ return fCallback(pError);
149
+ }
150
+
151
+ if (!pRecord)
152
+ {
153
+ pRecord =
154
+ {
155
+ Path: pRelPath,
156
+ Regions: []
157
+ };
158
+ }
159
+
160
+ // Ensure Regions array exists (guard against old records)
161
+ if (!Array.isArray(pRecord.Regions))
162
+ {
163
+ pRecord.Regions = [];
164
+ }
165
+
166
+ return fCallback(null, pRecord);
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Connect REST routes to the Orator service server.
172
+ *
173
+ * @param {object} pServiceServer - The Orator service server instance
174
+ */
175
+ connectRoutes(pServiceServer)
176
+ {
177
+ let tmpSelf = this;
178
+ let tmpContentPath = this.contentPath;
179
+
180
+ // -----------------------------------------------------------------
181
+ // GET /api/media/subimage-regions?path= — List all regions for an image
182
+ // -----------------------------------------------------------------
183
+ pServiceServer.get('/api/media/subimage-regions',
184
+ (pRequest, pResponse, fNext) =>
185
+ {
186
+ try
187
+ {
188
+ let tmpParsedUrl = libUrl.parse(pRequest.url, true);
189
+ let tmpRelPath = tmpSelf._sanitizePath(tmpParsedUrl.query.path);
190
+
191
+ if (!tmpRelPath)
192
+ {
193
+ pResponse.send(400, { Success: false, Error: 'Missing or invalid path parameter.' });
194
+ return fNext();
195
+ }
196
+
197
+ let tmpResolved = tmpSelf._resolveFileStat(tmpRelPath);
198
+ if (!tmpResolved)
199
+ {
200
+ pResponse.send(404, { Success: false, Error: 'File not found.' });
201
+ return fNext();
202
+ }
203
+
204
+ let tmpStat = tmpResolved.stat;
205
+
206
+ tmpSelf._loadOrCreateRecord(tmpRelPath, tmpStat.mtimeMs,
207
+ (pError, pRecord) =>
208
+ {
209
+ if (pError)
210
+ {
211
+ pResponse.send(500, { Success: false, Error: pError.message });
212
+ return fNext();
213
+ }
214
+
215
+ pResponse.send(
216
+ {
217
+ Success: true,
218
+ Path: tmpRelPath,
219
+ Regions: pRecord.Regions
220
+ });
221
+ return fNext();
222
+ });
223
+ }
224
+ catch (pError)
225
+ {
226
+ pResponse.send(500, { Success: false, Error: pError.message });
227
+ return fNext();
228
+ }
229
+ });
230
+
231
+ // -----------------------------------------------------------------
232
+ // POST /api/media/subimage-regions — Add a new region
233
+ // Body: { Path, Region: { Label, X, Y, Width, Height } }
234
+ // -----------------------------------------------------------------
235
+ pServiceServer.post('/api/media/subimage-regions',
236
+ (pRequest, pResponse, fNext) =>
237
+ {
238
+ try
239
+ {
240
+ let tmpBody = pRequest.body || {};
241
+ let tmpRelPath = tmpSelf._sanitizePath(tmpBody.Path);
242
+
243
+ if (!tmpRelPath)
244
+ {
245
+ pResponse.send(400, { Success: false, Error: 'Missing or invalid Path in request body.' });
246
+ return fNext();
247
+ }
248
+
249
+ let tmpRegionInput = tmpBody.Region;
250
+ if (!tmpRegionInput || typeof tmpRegionInput !== 'object')
251
+ {
252
+ pResponse.send(400, { Success: false, Error: 'Missing Region object in request body.' });
253
+ return fNext();
254
+ }
255
+
256
+ // Validate: visual regions need coordinates, text selections need SelectedText
257
+ let tmpIsTextSelection = (tmpRegionInput.Type === 'text-selection');
258
+ if (!tmpIsTextSelection)
259
+ {
260
+ if (typeof tmpRegionInput.X !== 'number' || typeof tmpRegionInput.Y !== 'number'
261
+ || typeof tmpRegionInput.Width !== 'number' || typeof tmpRegionInput.Height !== 'number'
262
+ || tmpRegionInput.Width <= 0 || tmpRegionInput.Height <= 0)
263
+ {
264
+ pResponse.send(400, { Success: false, Error: 'Visual region must have numeric X, Y, Width (>0), Height (>0).' });
265
+ return fNext();
266
+ }
267
+ }
268
+
269
+ let tmpResolved = tmpSelf._resolveFileStat(tmpRelPath);
270
+ if (!tmpResolved)
271
+ {
272
+ pResponse.send(404, { Success: false, Error: 'File not found.' });
273
+ return fNext();
274
+ }
275
+
276
+ let tmpStat = tmpResolved.stat;
277
+
278
+ tmpSelf._loadOrCreateRecord(tmpRelPath, tmpStat.mtimeMs,
279
+ (pLoadError, pRecord) =>
280
+ {
281
+ if (pLoadError)
282
+ {
283
+ pResponse.send(500, { Success: false, Error: pLoadError.message });
284
+ return fNext();
285
+ }
286
+
287
+ let tmpRegion =
288
+ {
289
+ ID: tmpSelf.fable.getUUID(),
290
+ Type: tmpRegionInput.Type || 'visual-region',
291
+ Label: tmpRegionInput.Label || '',
292
+ X: (typeof tmpRegionInput.X === 'number') ? Math.round(tmpRegionInput.X) : null,
293
+ Y: (typeof tmpRegionInput.Y === 'number') ? Math.round(tmpRegionInput.Y) : null,
294
+ Width: (typeof tmpRegionInput.Width === 'number') ? Math.round(tmpRegionInput.Width) : null,
295
+ Height: (typeof tmpRegionInput.Height === 'number') ? Math.round(tmpRegionInput.Height) : null,
296
+ CreatedAt: new Date().toISOString(),
297
+ // Document-specific fields
298
+ PageNumber: (typeof tmpRegionInput.PageNumber === 'number') ? tmpRegionInput.PageNumber : null,
299
+ CFI: tmpRegionInput.CFI || null,
300
+ SpineIndex: (typeof tmpRegionInput.SpineIndex === 'number') ? tmpRegionInput.SpineIndex : null,
301
+ ChapterTitle: tmpRegionInput.ChapterTitle || null,
302
+ SelectedText: tmpRegionInput.SelectedText || null,
303
+ ViewportWidth: (typeof tmpRegionInput.ViewportWidth === 'number') ? tmpRegionInput.ViewportWidth : null,
304
+ ViewportHeight: (typeof tmpRegionInput.ViewportHeight === 'number') ? tmpRegionInput.ViewportHeight : null
305
+ };
306
+
307
+ pRecord.Regions.push(tmpRegion);
308
+
309
+ tmpSelf.saveExplorerState(tmpRelPath, tmpStat.mtimeMs, pRecord,
310
+ (pSaveError) =>
311
+ {
312
+ if (pSaveError)
313
+ {
314
+ pResponse.send(500, { Success: false, Error: pSaveError.message });
315
+ return fNext();
316
+ }
317
+
318
+ pResponse.send(
319
+ {
320
+ Success: true,
321
+ Region: tmpRegion,
322
+ Regions: pRecord.Regions
323
+ });
324
+ return fNext();
325
+ });
326
+ });
327
+ }
328
+ catch (pError)
329
+ {
330
+ pResponse.send(500, { Success: false, Error: pError.message });
331
+ return fNext();
332
+ }
333
+ });
334
+
335
+ // -----------------------------------------------------------------
336
+ // PUT /api/media/subimage-regions/:id — Update a region's label or bounds
337
+ // Body: { Path, Label?, X?, Y?, Width?, Height? }
338
+ // -----------------------------------------------------------------
339
+ pServiceServer.put('/api/media/subimage-regions/:id',
340
+ (pRequest, pResponse, fNext) =>
341
+ {
342
+ try
343
+ {
344
+ let tmpRegionId = pRequest.params.id;
345
+ let tmpBody = pRequest.body || {};
346
+ let tmpRelPath = tmpSelf._sanitizePath(tmpBody.Path);
347
+
348
+ if (!tmpRelPath || !tmpRegionId)
349
+ {
350
+ pResponse.send(400, { Success: false, Error: 'Missing Path or region ID.' });
351
+ return fNext();
352
+ }
353
+
354
+ let tmpResolved = tmpSelf._resolveFileStat(tmpRelPath);
355
+ if (!tmpResolved)
356
+ {
357
+ pResponse.send(404, { Success: false, Error: 'File not found.' });
358
+ return fNext();
359
+ }
360
+
361
+ let tmpStat = tmpResolved.stat;
362
+
363
+ tmpSelf._loadOrCreateRecord(tmpRelPath, tmpStat.mtimeMs,
364
+ (pLoadError, pRecord) =>
365
+ {
366
+ if (pLoadError)
367
+ {
368
+ pResponse.send(500, { Success: false, Error: pLoadError.message });
369
+ return fNext();
370
+ }
371
+
372
+ let tmpRegion = null;
373
+ for (let i = 0; i < pRecord.Regions.length; i++)
374
+ {
375
+ if (pRecord.Regions[i].ID === tmpRegionId)
376
+ {
377
+ tmpRegion = pRecord.Regions[i];
378
+ break;
379
+ }
380
+ }
381
+
382
+ if (!tmpRegion)
383
+ {
384
+ pResponse.send(404, { Success: false, Error: 'Region not found.' });
385
+ return fNext();
386
+ }
387
+
388
+ // Apply updates
389
+ if (typeof tmpBody.Label === 'string')
390
+ {
391
+ tmpRegion.Label = tmpBody.Label;
392
+ }
393
+ if (typeof tmpBody.X === 'number')
394
+ {
395
+ tmpRegion.X = Math.round(tmpBody.X);
396
+ }
397
+ if (typeof tmpBody.Y === 'number')
398
+ {
399
+ tmpRegion.Y = Math.round(tmpBody.Y);
400
+ }
401
+ if (typeof tmpBody.Width === 'number' && tmpBody.Width > 0)
402
+ {
403
+ tmpRegion.Width = Math.round(tmpBody.Width);
404
+ }
405
+ if (typeof tmpBody.Height === 'number' && tmpBody.Height > 0)
406
+ {
407
+ tmpRegion.Height = Math.round(tmpBody.Height);
408
+ }
409
+ // Document-specific field updates
410
+ if (typeof tmpBody.SelectedText === 'string')
411
+ {
412
+ tmpRegion.SelectedText = tmpBody.SelectedText;
413
+ }
414
+ if (typeof tmpBody.ChapterTitle === 'string')
415
+ {
416
+ tmpRegion.ChapterTitle = tmpBody.ChapterTitle;
417
+ }
418
+ if (typeof tmpBody.PageNumber === 'number')
419
+ {
420
+ tmpRegion.PageNumber = tmpBody.PageNumber;
421
+ }
422
+
423
+ tmpSelf.saveExplorerState(tmpRelPath, tmpStat.mtimeMs, pRecord,
424
+ (pSaveError) =>
425
+ {
426
+ if (pSaveError)
427
+ {
428
+ pResponse.send(500, { Success: false, Error: pSaveError.message });
429
+ return fNext();
430
+ }
431
+
432
+ pResponse.send(
433
+ {
434
+ Success: true,
435
+ Region: tmpRegion,
436
+ Regions: pRecord.Regions
437
+ });
438
+ return fNext();
439
+ });
440
+ });
441
+ }
442
+ catch (pError)
443
+ {
444
+ pResponse.send(500, { Success: false, Error: pError.message });
445
+ return fNext();
446
+ }
447
+ });
448
+
449
+ // -----------------------------------------------------------------
450
+ // DELETE /api/media/subimage-regions/:id?path= — Remove a region
451
+ // -----------------------------------------------------------------
452
+ pServiceServer.del('/api/media/subimage-regions/:id',
453
+ (pRequest, pResponse, fNext) =>
454
+ {
455
+ try
456
+ {
457
+ let tmpRegionId = pRequest.params.id;
458
+ let tmpParsedUrl = libUrl.parse(pRequest.url, true);
459
+ let tmpRelPath = tmpSelf._sanitizePath(tmpParsedUrl.query.path);
460
+
461
+ if (!tmpRelPath || !tmpRegionId)
462
+ {
463
+ pResponse.send(400, { Success: false, Error: 'Missing path or region ID.' });
464
+ return fNext();
465
+ }
466
+
467
+ let tmpResolved = tmpSelf._resolveFileStat(tmpRelPath);
468
+ if (!tmpResolved)
469
+ {
470
+ pResponse.send(404, { Success: false, Error: 'File not found.' });
471
+ return fNext();
472
+ }
473
+
474
+ let tmpStat = tmpResolved.stat;
475
+
476
+ tmpSelf._loadOrCreateRecord(tmpRelPath, tmpStat.mtimeMs,
477
+ (pLoadError, pRecord) =>
478
+ {
479
+ if (pLoadError)
480
+ {
481
+ pResponse.send(500, { Success: false, Error: pLoadError.message });
482
+ return fNext();
483
+ }
484
+
485
+ let tmpFound = false;
486
+ pRecord.Regions = pRecord.Regions.filter(
487
+ (pRegion) =>
488
+ {
489
+ if (pRegion.ID === tmpRegionId)
490
+ {
491
+ tmpFound = true;
492
+ return false;
493
+ }
494
+ return true;
495
+ });
496
+
497
+ if (!tmpFound)
498
+ {
499
+ pResponse.send(404, { Success: false, Error: 'Region not found.' });
500
+ return fNext();
501
+ }
502
+
503
+ tmpSelf.saveExplorerState(tmpRelPath, tmpStat.mtimeMs, pRecord,
504
+ (pSaveError) =>
505
+ {
506
+ if (pSaveError)
507
+ {
508
+ pResponse.send(500, { Success: false, Error: pSaveError.message });
509
+ return fNext();
510
+ }
511
+
512
+ pResponse.send(
513
+ {
514
+ Success: true,
515
+ Regions: pRecord.Regions
516
+ });
517
+ return fNext();
518
+ });
519
+ });
520
+ }
521
+ catch (pError)
522
+ {
523
+ pResponse.send(500, { Success: false, Error: pError.message });
524
+ return fNext();
525
+ }
526
+ });
527
+ }
528
+ }
529
+
530
+ module.exports = RetoldRemoteSubimageService;
@@ -40,6 +40,7 @@ class ToolDetector
40
40
  p7zip: this._detectCommand('7z --help'),
41
41
  audiowaveform: this._detectCommand('audiowaveform --version'),
42
42
  ebook_convert: this._detectCommand('ebook-convert --version'),
43
+ libreoffice: this._detectLibreOffice(),
43
44
  exiftool: this._detectCommand('exiftool -ver'),
44
45
  dcraw: this._detectCommandExists('dcraw'),
45
46
  dcrawJs: this._detectModule('dcraw'),
@@ -151,6 +152,55 @@ class ToolDetector
151
152
  return this._detectCommand('vlc --version');
152
153
  }
153
154
 
155
+ /**
156
+ * Detect LibreOffice for headless document conversion.
157
+ * macOS: check for the .app bundle (soffice in the bundle).
158
+ * Linux: check the soffice command.
159
+ * Windows: check default install paths.
160
+ *
161
+ * @returns {boolean}
162
+ */
163
+ _detectLibreOffice()
164
+ {
165
+ const libFS = require('fs');
166
+
167
+ // macOS: check for LibreOffice.app
168
+ try
169
+ {
170
+ if (libFS.existsSync('/Applications/LibreOffice.app/Contents/MacOS/soffice'))
171
+ {
172
+ return true;
173
+ }
174
+ }
175
+ catch (pError)
176
+ {
177
+ // ignore
178
+ }
179
+
180
+ // Windows: check default install paths
181
+ if (process.platform === 'win32')
182
+ {
183
+ try
184
+ {
185
+ if (libFS.existsSync('C:\\Program Files\\LibreOffice\\program\\soffice.exe'))
186
+ {
187
+ return true;
188
+ }
189
+ if (libFS.existsSync('C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe'))
190
+ {
191
+ return true;
192
+ }
193
+ }
194
+ catch (pError)
195
+ {
196
+ // ignore
197
+ }
198
+ }
199
+
200
+ // Linux / other: check soffice on PATH
201
+ return this._detectCommandExists('soffice');
202
+ }
203
+
154
204
  /**
155
205
  * Check if a command-line tool exists on the PATH using 'which'.
156
206
  * Useful for tools that exit non-zero when invoked with no arguments (e.g. dcraw).
@@ -262,7 +262,7 @@ function getOperations()
262
262
  Height: '{~D:Record.Operation.Height~}',
263
263
  Format: '{~D:Record.Operation.Format~}',
264
264
  Quality: '{~D:Record.Operation.Quality~}',
265
- TimeoutMs: 30000
265
+ TimeoutMs: 300000
266
266
  },
267
267
  ProcessSettings: ['InputFile', 'OutputFile', 'Width', 'Height', 'Format', 'Quality'],
268
268
  ProcessOutputs: ['Result', 'StdOut'],
@@ -285,7 +285,7 @@ function getOperations()
285
285
  OutputFile: 'thumbnail.jpg',
286
286
  Timestamp: '{~D:Record.Operation.Timestamp~}',
287
287
  Width: '{~D:Record.Operation.Width~}',
288
- TimeoutMs: 60000
288
+ TimeoutMs: 600000
289
289
  },
290
290
  ProcessSettings: ['InputFile', 'OutputFile', 'Timestamp', 'Width'],
291
291
  ProcessOutputs: ['Result', 'StdOut'],
@@ -310,7 +310,7 @@ function getOperations()
310
310
  _taskNode(tmpVfe + '-probe', 'beacon-mediaconversion-mediaprobe', 'Probe Video', 660, 180,
311
311
  {
312
312
  AffinityKey: '{~D:Record.Operation.VideoAddress~}',
313
- TimeoutMs: 30000
313
+ TimeoutMs: 600000
314
314
  },
315
315
  ['InputFile'], ['Result', 'StdOut']),
316
316
  _taskNode(tmpVfe + '-extract', 'beacon-mediaconversion-videoextractframe', 'Extract Frame', 880, 180,
@@ -319,7 +319,7 @@ function getOperations()
319
319
  Timestamp: '{~D:Record.Operation.Timestamp~}',
320
320
  Width: '{~D:Record.Operation.Width~}',
321
321
  AffinityKey: '{~D:Record.Operation.VideoAddress~}',
322
- TimeoutMs: 60000
322
+ TimeoutMs: 600000
323
323
  },
324
324
  ['InputFile', 'OutputFile', 'Timestamp', 'Width'], ['Result', 'StdOut']),
325
325
  _taskNode(tmpVfe + '-result', 'send-result', 'Send Result', 1100, 180,
@@ -409,7 +409,7 @@ function getOperations()
409
409
  OutputFile: 'page.png',
410
410
  Page: '{~D:Record.Operation.Page~}',
411
411
  LongSidePixels: '{~D:Record.Operation.LongSidePixels~}',
412
- TimeoutMs: 60000
412
+ TimeoutMs: 300000
413
413
  },
414
414
  ProcessSettings: ['InputFile', 'OutputFile', 'Page', 'LongSidePixels'],
415
415
  ProcessOutputs: ['Result', 'StdOut'],
@@ -472,7 +472,7 @@ function getOperations()
472
472
  ProcessTitle: 'Probe Metadata',
473
473
  ProcessData:
474
474
  {
475
- TimeoutMs: 30000
475
+ TimeoutMs: 300000
476
476
  },
477
477
  ProcessSettings: ['InputFile'],
478
478
  ProcessOutputs: ['Result', 'StdOut'],