You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
105 lines
3.2 KiB
105 lines
3.2 KiB
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execFile } = require('child_process');
|
|
const { promisify } = require('util');
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
// ========== 用 pdfinfo 获取页数(保持不变) ==========
|
|
async function getPDFPageCount(pdfPath) {
|
|
try {
|
|
const { stdout } = await execFileAsync('pdfinfo', [pdfPath], {
|
|
timeout: 10000,
|
|
maxBuffer: 1024 * 1024
|
|
});
|
|
const match = stdout.match(/Pages:\s*(\d+)/i);
|
|
return match ? parseInt(match[1], 10) : 1;
|
|
} catch (error) {
|
|
console.error(`[PDF2IMG] pdfinfo failed:`, error.message);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// ========== 直接调用 pdftoppm 转换单页 ==========
|
|
async function convertPdfPageToPng(pdfPath, outputDir, pageNum, dpi = 150) {
|
|
// 通过 -singlefile + 带页码的基础名,确保输出文件名稳定为 page-<pageNum>.png
|
|
const expectedOutputPath = path.join(outputDir, `page-${pageNum}.png`);
|
|
const outputBaseName = path.join(outputDir, `page-${pageNum}`); // 不包含扩展名
|
|
|
|
try {
|
|
await execFileAsync('pdftoppm', [
|
|
'-png',
|
|
'-singlefile',
|
|
'-r', dpi.toString(),
|
|
'-f', pageNum.toString(),
|
|
'-l', pageNum.toString(),
|
|
pdfPath,
|
|
outputBaseName // 只提供基础名称
|
|
], {
|
|
timeout: 30000, // 30秒超时
|
|
windowsHide: true
|
|
});
|
|
|
|
if (fs.existsSync(expectedOutputPath)) {
|
|
const size = fs.statSync(expectedOutputPath).size;
|
|
console.log(`[PDF2IMG] Page ${pageNum} saved (${size} bytes): ${expectedOutputPath}`);
|
|
return expectedOutputPath;
|
|
} else {
|
|
throw new Error(`Expected output file not found: ${expectedOutputPath}`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`[PDF2IMG] pdftoppm failed for page ${pageNum}:`, error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ========== 主转换函数 ==========
|
|
async function convertPdfToImages(
|
|
pdfPath,
|
|
outputDir,
|
|
dpi = 150,
|
|
startPage = 1,
|
|
endPage = null
|
|
) {
|
|
const startTime = Date.now();
|
|
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
const stats = fs.statSync(pdfPath);
|
|
console.log(`[PDF2IMG] Input PDF: ${pdfPath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
|
|
const totalPages = await getPDFPageCount(pdfPath);
|
|
console.log(`[PDF2IMG] Detected ${totalPages} pages`);
|
|
|
|
const normalizedStartPage = Math.max(
|
|
1,
|
|
Math.min(totalPages, Number(startPage) || 1)
|
|
);
|
|
const normalizedEndPage = Math.max(
|
|
normalizedStartPage,
|
|
Math.min(totalPages, Number(endPage) || totalPages)
|
|
);
|
|
|
|
const imagePaths = [];
|
|
console.log(
|
|
`[PDF2IMG] Converting pages ${normalizedStartPage}-${normalizedEndPage} with DPI=${dpi}...`
|
|
);
|
|
|
|
for (let pageNum = normalizedStartPage; pageNum <= normalizedEndPage; pageNum++) {
|
|
try {
|
|
const imagePath = await convertPdfPageToPng(pdfPath, outputDir, pageNum, dpi);
|
|
imagePaths.push(imagePath);
|
|
} catch (err) {
|
|
console.warn(`[PDF2IMG] Skipping page ${pageNum} due to error:`, err.message);
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
const plannedPages = normalizedEndPage - normalizedStartPage + 1;
|
|
console.log(`[PDF2IMG] ✅ Converted ${imagePaths.length}/${plannedPages} pages in ${duration}ms`);
|
|
return imagePaths;
|
|
}
|
|
|
|
module.exports = { convertPdfToImages };
|
|
|