PDF preview: thumbnails via Ghostscript, preview in file cards
- GET /pdf/preview/:fileId — renders first page as PNG (72 DPI) - GET /pdf/preview/:fileId/:page — specific page preview - GET /pdf/thumbnails/:fileId — list of all page preview URLs - File cards show actual PDF thumbnail instead of icon - Cached previews (30 min cleanup) - Fallback to PDF icon if preview fails
This commit is contained in:
parent
ee49fed415
commit
4bd90b26bc
@ -147,6 +147,19 @@
|
|||||||
.file-card-icon {
|
.file-card-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--surface-600);
|
||||||
|
}
|
||||||
|
.file-card-icon img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.file-card-info {
|
.file-card-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -1071,7 +1084,7 @@
|
|||||||
card.dataset.id = f.id;
|
card.dataset.id = f.id;
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="file-card-order">${idx + 1}</div>
|
<div class="file-card-order">${idx + 1}</div>
|
||||||
<div class="file-card-icon">${pdfIcon(20)}</div>
|
<div class="file-card-icon"><img src="/pdf/preview/${f.id}" alt="" onerror="this.parentNode.innerHTML=pdfIcon(20)"></div>
|
||||||
<div class="file-card-info">
|
<div class="file-card-info">
|
||||||
<div class="file-card-name">${escHtml(f.name)}</div>
|
<div class="file-card-name">${escHtml(f.name)}</div>
|
||||||
<div class="file-card-meta">${formatSize(f.size)} · ${f.pages} стр.</div>
|
<div class="file-card-meta">${formatSize(f.size)} · ${f.pages} стр.</div>
|
||||||
|
|||||||
@ -101,6 +101,55 @@ router.post('/upload', limiter, upload.array('files', 10), async (req, res) => {
|
|||||||
res.json({ files: results });
|
res.json({ files: results });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Preview — render PDF page as PNG thumbnail via Ghostscript
|
||||||
|
router.get('/preview/:fileId', (req, res) => renderPreview(req, res, 1));
|
||||||
|
router.get('/preview/:fileId/:page', (req, res) => renderPreview(req, res, parseInt(req.params.page) || 1));
|
||||||
|
|
||||||
|
async function renderPreview(req, res, pageNum) {
|
||||||
|
const f = getFile(req.params.fileId);
|
||||||
|
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||||||
|
|
||||||
|
const cacheKey = `${f.id}_p${pageNum}`;
|
||||||
|
const cachePath = path.join(DOWNLOADS_DIR, `preview_${cacheKey}.png`);
|
||||||
|
|
||||||
|
// Return cached if exists
|
||||||
|
if (fs.existsSync(cachePath)) {
|
||||||
|
return res.type('png').sendFile(cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const proc = spawn('gs', [
|
||||||
|
'-sDEVICE=png16m', '-r72', '-dNOPAUSE', '-dBATCH', '-dQUIET',
|
||||||
|
`-dFirstPage=${pageNum}`, `-dLastPage=${pageNum}`,
|
||||||
|
'-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||||
|
`-sOutputFile=${cachePath}`, f.path,
|
||||||
|
], { timeout: 15000 });
|
||||||
|
proc.on('close', code => code === 0 ? resolve() : reject(new Error('Preview failed')));
|
||||||
|
proc.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup after 30 min
|
||||||
|
setTimeout(() => { try { fs.unlinkSync(cachePath); } catch {} }, 30 * 60 * 1000);
|
||||||
|
|
||||||
|
res.type('png').sendFile(cachePath);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Не удалось создать превью' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnails — return preview URLs for all pages
|
||||||
|
router.get('/thumbnails/:fileId', async (req, res) => {
|
||||||
|
const f = getFile(req.params.fileId);
|
||||||
|
if (!f) return res.status(404).json({ error: 'Файл не найден' });
|
||||||
|
|
||||||
|
const urls = [];
|
||||||
|
for (let i = 1; i <= Math.min(f.pages, 50); i++) {
|
||||||
|
urls.push(`/pdf/preview/${f.id}/${i}`);
|
||||||
|
}
|
||||||
|
res.json({ pages: f.pages, thumbnails: urls });
|
||||||
|
});
|
||||||
|
|
||||||
// Info
|
// Info
|
||||||
router.get('/info/:fileId', async (req, res) => {
|
router.get('/info/:fileId', async (req, res) => {
|
||||||
const f = getFile(req.params.fileId);
|
const f = getFile(req.params.fileId);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user