u
This commit is contained in:
85
src/util/generateDocumentThumb.js
Normal file
85
src/util/generateDocumentThumb.js
Normal 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user