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 = `
${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);