Add PDF first-page thumbnails via pdftoppm

Generated at upload time, stored alongside the PDF in data/pdfs/.
Shown in the file picker (48px) and component detail view (80px).
Gracefully skipped if pdftoppm is unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 00:40:43 +00:00
parent 61d52d8076
commit 451b04ad03
3 changed files with 54 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ import express from 'express';
import multer from 'multer';
import { unlinkSync, mkdirSync } from 'node:fs';
import { extname, join } from 'node:path';
import { execFileSync } from 'node:child_process';
import sharp from 'sharp';
import { generate_id } from './lib/ids.mjs';
import { compute_cell_size, process_grid_image } from './lib/grid-image.mjs';
@@ -62,6 +63,20 @@ function remove_image_file(img_id) {
try { unlinkSync(join('./data/images', img_id)); } catch {}
}
function generate_pdf_thumb(pdf_path, thumb_prefix) {
// Returns thumb filename (id-thumb.png) on success, null if pdftoppm unavailable.
try {
execFileSync('pdftoppm', [
'-png', '-singlefile', '-scale-to', '512',
'-f', '1', '-l', '1',
pdf_path, thumb_prefix,
]);
return thumb_prefix.split('/').pop() + '.png';
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// Field definitions
// ---------------------------------------------------------------------------
@@ -491,12 +506,16 @@ app.post('/api/pdfs', pdf_upload.single('file'), (req, res) => {
try { unlinkSync(join('./data/pdfs', req.file.filename)); } catch {}
return fail(res, 'a file with that name already exists');
}
const id = generate_id();
const thumb_prefix = join('./data/pdfs', id + '-thumb');
const thumb_file = generate_pdf_thumb(join('./data/pdfs', req.file.filename), thumb_prefix);
const pdf = {
id: generate_id(),
id,
filename: req.file.filename,
display_name,
original_name: req.file.originalname,
size: req.file.size,
thumb_filename: thumb_file,
uploaded_at: Date.now(),
};
set_pdf(pdf);
@@ -520,6 +539,7 @@ app.delete('/api/pdfs/:id', (req, res) => {
const pdf = get_pdf(req.params.id);
if (!pdf) return fail(res, 'not found', 404);
try { unlinkSync(join('./data/pdfs', pdf.filename)); } catch {}
if (pdf.thumb_filename) { try { unlinkSync(join('./data/pdfs', pdf.thumb_filename)); } catch {} }
delete_pdf(req.params.id);
ok(res);
});