feat: add pdf-utils.js — pdfInfo, pdfRenderPage, bufferToTempFile
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* pdf-utils.js — Thin wrappers around poppler-utils CLI tools.
|
||||
*
|
||||
* Uses pdfinfo + pdftoppm (from poppler-utils) for PDF metadata extraction
|
||||
* and page rendering. No external Node dependencies required.
|
||||
*/
|
||||
|
||||
const { execFile } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const TIMEOUT_MS = 60_000;
|
||||
|
||||
/**
|
||||
* Write a buffer to a temporary file. Returns { tmpPath, cleanup }.
|
||||
* Caller MUST call cleanup() when done.
|
||||
*/
|
||||
function bufferToTempFile(buffer, ext = '.bin') {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'refboard-pdf-'));
|
||||
const tmpPath = path.join(tmpDir, `file${ext}`);
|
||||
fs.writeFileSync(tmpPath, buffer);
|
||||
return {
|
||||
tmpPath,
|
||||
cleanup() {
|
||||
try { fs.unlinkSync(tmpPath); } catch {}
|
||||
try { fs.rmdirSync(tmpDir); } catch {}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract PDF metadata via pdfinfo.
|
||||
* Returns { pageCount, dimensions: [{ w, h }, ...] }
|
||||
*/
|
||||
async function pdfInfo(filePath) {
|
||||
// Get basic info (page count)
|
||||
const { stdout: basicOut } = await execFileAsync('pdfinfo', [filePath], { timeout: TIMEOUT_MS });
|
||||
|
||||
let pageCount = 0;
|
||||
for (const line of basicOut.split('\n')) {
|
||||
const match = line.match(/^Pages:\s+(\d+)/);
|
||||
if (match) {
|
||||
pageCount = parseInt(match[1], 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pageCount === 0) {
|
||||
throw new Error('Could not determine PDF page count');
|
||||
}
|
||||
|
||||
// Get per-page dimensions
|
||||
const dimensions = [];
|
||||
const { stdout: dimOut } = await execFileAsync(
|
||||
'pdfinfo',
|
||||
['-f', '1', '-l', String(pageCount), filePath],
|
||||
{ timeout: TIMEOUT_MS }
|
||||
);
|
||||
|
||||
// Parse "Page N size: W x H pts" lines
|
||||
for (const line of dimOut.split('\n')) {
|
||||
const match = line.match(/^Page\s+\d+\s+size:\s+([\d.]+)\s+x\s+([\d.]+)/);
|
||||
if (match) {
|
||||
dimensions.push({
|
||||
w: Math.round(parseFloat(match[1])),
|
||||
h: Math.round(parseFloat(match[2])),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no per-page sizes found, try global "Page size:"
|
||||
if (dimensions.length === 0) {
|
||||
for (const line of basicOut.split('\n')) {
|
||||
const match = line.match(/^Page size:\s+([\d.]+)\s+x\s+([\d.]+)/);
|
||||
if (match) {
|
||||
const dim = {
|
||||
w: Math.round(parseFloat(match[1])),
|
||||
h: Math.round(parseFloat(match[2])),
|
||||
};
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
dimensions.push(dim);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { pageCount, dimensions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single PDF page to PNG at the given DPI.
|
||||
* Returns a PNG Buffer.
|
||||
*/
|
||||
async function pdfRenderPage(filePath, pageNum, dpi = 150) {
|
||||
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'refboard-render-'));
|
||||
|
||||
try {
|
||||
const outPrefix = path.join(outDir, 'page');
|
||||
|
||||
await execFileAsync('pdftoppm', [
|
||||
'-png',
|
||||
'-r', String(dpi),
|
||||
'-f', String(pageNum),
|
||||
'-l', String(pageNum),
|
||||
filePath,
|
||||
outPrefix,
|
||||
], { timeout: TIMEOUT_MS });
|
||||
|
||||
// pdftoppm names output like page-01.png, page-1.png, etc. — discover it
|
||||
const files = fs.readdirSync(outDir).filter(f => f.endsWith('.png'));
|
||||
if (files.length === 0) {
|
||||
throw new Error(`pdftoppm produced no output for page ${pageNum}`);
|
||||
}
|
||||
|
||||
const pngPath = path.join(outDir, files[0]);
|
||||
const pngBuffer = fs.readFileSync(pngPath);
|
||||
return pngBuffer;
|
||||
} finally {
|
||||
// Clean up temp dir
|
||||
try {
|
||||
for (const f of fs.readdirSync(outDir)) {
|
||||
fs.unlinkSync(path.join(outDir, f));
|
||||
}
|
||||
fs.rmdirSync(outDir);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { pdfInfo, pdfRenderPage, bufferToTempFile };
|
||||
Reference in New Issue
Block a user