diff --git a/public/pdf.html b/public/pdf.html index 03a0bbb..6e97ef3 100644 --- a/public/pdf.html +++ b/public/pdf.html @@ -147,6 +147,19 @@ .file-card-icon { flex-shrink: 0; 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 { flex: 1; @@ -1071,7 +1084,7 @@ card.dataset.id = f.id; card.innerHTML = `
${idx + 1}
-
${pdfIcon(20)}
+
${escHtml(f.name)}
${formatSize(f.size)} · ${f.pages} стр.
diff --git a/routes/pdf.js b/routes/pdf.js index da3f9a4..4c82ab9 100644 --- a/routes/pdf.js +++ b/routes/pdf.js @@ -101,6 +101,55 @@ router.post('/upload', limiter, upload.array('files', 10), async (req, res) => { 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 router.get('/info/:fileId', async (req, res) => { const f = getFile(req.params.fileId);