/** * ───────────────────────────────────────────── * SPB Drive — Personal Cloud Drive * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/preview/office-thumb.mjs * Purpose : Thumbnail page 1 of a LibreOffice-converted PDF from cache * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { execFile } from 'node:child_process'; import { existsSync } from 'node:fs'; import { rename, rm } from 'node:fs/promises'; import path from 'node:path'; import { promisify } from 'node:util'; import sharp from 'sharp'; import { config } from '../config.mjs'; import { enqueue } from './queue.mjs'; import { hasBin } from './thumbs.mjs'; const execFileP = promisify(execFile); /** Thumbnail an office doc via its cached converted PDF (webp, cached). */ export async function getThumbFromFile(sha, size = 256) { const sz = Number(size) === 512 ? 512 : 256; const out = path.join(config.cacheDir, 'thumbs', `${sha}.${sz}.webp`); if (existsSync(out)) return out; const pdf = path.join(config.cacheDir, 'office', `${sha}.pdf`); if (!existsSync(pdf) || !(await hasBin('pdftoppm'))) return null; return enqueue(`thumb:${sha}:${sz}`, async () => { if (existsSync(out)) return out; const tmpBase = `${out}.tmp-${process.pid}`; try { await execFileP('pdftoppm', ['-png', '-f', '1', '-l', '1', '-scale-to', String(sz), pdf, tmpBase], { timeout: 60_000, }); const png = `${tmpBase}-1.png`; if (!existsSync(png)) return null; await sharp(png).webp({ quality: 80 }).toFile(`${tmpBase}.webp`); await rm(png, { force: true }); await rename(`${tmpBase}.webp`, out); return out; } catch { await rm(`${tmpBase}.webp`, { force: true }); return null; } }); }