This commit is contained in:
seb
2026-09-18 07:13:56 +02:00
parent 9e9d3154ca
commit bac6f2a801
3 changed files with 107 additions and 18 deletions

View File

@@ -0,0 +1,85 @@
import sharp from 'sharp';
import { spawn } from 'node:child_process';
const THUMB_MAX = 200;
/**
* Renders page 1 of a PDF into a PNG buffer using pdftocairo.
* @param {Buffer} pdfBuffer
* @returns {Promise<Buffer>}
*/
function renderPdfPageToPng(pdfBuffer) {
return new Promise((resolve, reject) => {
const proc = spawn('pdftocairo', ['-png', '-singlefile', '-', '-'], {
stdio: ['pipe', 'pipe', 'pipe'],
});
const chunks = [];
const errChunks = [];
proc.stdout.on('data', (chunk) => chunks.push(chunk));
proc.stderr.on('data', (chunk) => errChunks.push(chunk));
proc.on('error', (err) => {
reject(new Error(`Failed to start pdftocairo: ${err.message}`));
});
proc.on('close', (code) => {
if (code !== 0) {
const stderr = Buffer.concat(errChunks).toString('utf8').trim();
reject(new Error(`pdftocairo exited with code ${code}${stderr ? `: ${stderr}` : ''}`));
} else {
resolve(Buffer.concat(chunks));
}
});
proc.stdin.end(pdfBuffer);
});
}
/**
* Generates an AVIF thumbnail for a document (PDF or image).
* Returns { thumbavif, thumbx, thumby } or nulls if unsupported or rendering fails.
*
* @param {Buffer} content
* @param {string} mimetype
* @returns {Promise<{thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>}
*/
export default async function generateDocumentThumb(content, mimetype) {
if (!Buffer.isBuffer(content) || content.length === 0) {
return { thumbavif: null, thumbx: null, thumby: null };
}
const type = typeof mimetype === 'string' ? mimetype.trim().toLowerCase() : '';
let imageBuffer = null;
if (type === 'application/pdf') {
try {
imageBuffer = await renderPdfPageToPng(content);
} catch (err) {
console.warn('generateDocumentThumb: PDF render failed:', err.message);
return { thumbavif: null, thumbx: null, thumby: null };
}
} else if (type.startsWith('image/')) {
imageBuffer = content;
} else {
// Other formats (e.g. text/plain) do not have an image thumbnail
return { thumbavif: null, thumbx: null, thumby: null };
}
try {
const thumbResult = await sharp(imageBuffer)
.resize(THUMB_MAX, THUMB_MAX, { fit: 'inside', withoutEnlargement: true })
.avif({ effort: 4 })
.toBuffer({ resolveWithObject: true });
return {
thumbavif: thumbResult.data,
thumbx: thumbResult.info.width,
thumby: thumbResult.info.height,
};
} catch (err) {
console.warn('generateDocumentThumb: sharp thumbnail generation failed:', err.message);
return { thumbavif: null, thumbx: null, thumby: null };
}
}