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:
@@ -350,6 +350,14 @@ function render_detail_panel() {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'detail-file-row';
|
row.className = 'detail-file-row';
|
||||||
|
|
||||||
|
if (pdf.thumb_filename) {
|
||||||
|
const thumb = document.createElement('img');
|
||||||
|
thumb.className = 'pdf-thumb';
|
||||||
|
thumb.src = `/pdf/${pdf.thumb_filename}`;
|
||||||
|
thumb.alt = '';
|
||||||
|
row.appendChild(thumb);
|
||||||
|
}
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.className = 'detail-file-link';
|
a.className = 'detail-file-link';
|
||||||
a.href = `/pdf/${pdf.filename}`;
|
a.href = `/pdf/${pdf.filename}`;
|
||||||
@@ -1363,6 +1371,14 @@ function render_file_picker_list() {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'fp-row';
|
row.className = 'fp-row';
|
||||||
|
|
||||||
|
if (pdf.thumb_filename) {
|
||||||
|
const thumb = document.createElement('img');
|
||||||
|
thumb.className = 'fp-thumb';
|
||||||
|
thumb.src = `/pdf/${pdf.thumb_filename}`;
|
||||||
|
thumb.alt = '';
|
||||||
|
row.appendChild(thumb);
|
||||||
|
}
|
||||||
|
|
||||||
const name_el = document.createElement('span');
|
const name_el = document.createElement('span');
|
||||||
name_el.className = 'fp-name';
|
name_el.className = 'fp-name';
|
||||||
name_el.textContent = pdf.display_name;
|
name_el.textContent = pdf.display_name;
|
||||||
|
|||||||
@@ -1574,7 +1574,23 @@ nav {
|
|||||||
.detail-file-row {
|
.detail-file-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-thumb {
|
||||||
|
width: auto;
|
||||||
|
height: 80px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fp-thumb {
|
||||||
|
width: auto;
|
||||||
|
height: 48px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-file-link {
|
.detail-file-link {
|
||||||
|
|||||||
22
server.mjs
22
server.mjs
@@ -5,6 +5,7 @@ import express from 'express';
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { unlinkSync, mkdirSync } from 'node:fs';
|
import { unlinkSync, mkdirSync } from 'node:fs';
|
||||||
import { extname, join } from 'node:path';
|
import { extname, join } from 'node:path';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { generate_id } from './lib/ids.mjs';
|
import { generate_id } from './lib/ids.mjs';
|
||||||
import { compute_cell_size, process_grid_image } from './lib/grid-image.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 {}
|
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
|
// 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 {}
|
try { unlinkSync(join('./data/pdfs', req.file.filename)); } catch {}
|
||||||
return fail(res, 'a file with that name already exists');
|
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 = {
|
const pdf = {
|
||||||
id: generate_id(),
|
id,
|
||||||
filename: req.file.filename,
|
filename: req.file.filename,
|
||||||
display_name,
|
display_name,
|
||||||
original_name: req.file.originalname,
|
original_name: req.file.originalname,
|
||||||
size: req.file.size,
|
size: req.file.size,
|
||||||
|
thumb_filename: thumb_file,
|
||||||
uploaded_at: Date.now(),
|
uploaded_at: Date.now(),
|
||||||
};
|
};
|
||||||
set_pdf(pdf);
|
set_pdf(pdf);
|
||||||
@@ -520,6 +539,7 @@ app.delete('/api/pdfs/:id', (req, res) => {
|
|||||||
const pdf = get_pdf(req.params.id);
|
const pdf = get_pdf(req.params.id);
|
||||||
if (!pdf) return fail(res, 'not found', 404);
|
if (!pdf) return fail(res, 'not found', 404);
|
||||||
try { unlinkSync(join('./data/pdfs', pdf.filename)); } catch {}
|
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);
|
delete_pdf(req.params.id);
|
||||||
ok(res);
|
ok(res);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user