pdf-to-image.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /**
  2. * PDF转图片模块
  3. * 步骤:
  4. * 1. 创建start()函数
  5. * 2. 创建变量pdfPath用来储存外部传入的pdf路径
  6. * 3. 创建outPut 变量用来储存输出png的目录
  7. * 4. 调用pdf2image.js将pdf转换为png图片
  8. * 5. 保存结果
  9. */
  10. import fs from 'fs';
  11. import path from 'path';
  12. import { execSync } from 'child_process';
  13. import { fileURLToPath } from 'url';
  14. const __filename = fileURLToPath(import.meta.url);
  15. const __dirname = path.dirname(__filename);
  16. /**
  17. * 获取项目根目录
  18. */
  19. function getProjectRoot() {
  20. return path.join(__dirname, '..');
  21. }
  22. /**
  23. * PDF转图片主函数
  24. * @param {string} pdfPath - PDF文件路径
  25. * @param {string} outputDir - 输出PNG图片的目录
  26. * @param {Object} options - 可选参数
  27. * @param {number} options.dpi - 图片分辨率(默认200)
  28. * @param {string} options.format - 输出格式(默认'png')
  29. * @param {number} options.firstPage - 起始页码(可选)
  30. * @param {number} options.lastPage - 结束页码(可选)
  31. * @param {string} projectRoot - 项目根目录(可选)
  32. * @returns {Promise<Object>} 转换结果
  33. */
  34. async function start(pdfPath, outputDir, options = {}, projectRoot = null) {
  35. try {
  36. // 步骤1: 创建start()函数
  37. if (!projectRoot) {
  38. projectRoot = getProjectRoot();
  39. }
  40. // 步骤2: 创建变量pdfPath用来储存外部传入的pdf路径
  41. if (!pdfPath || !fs.existsSync(pdfPath)) {
  42. throw new Error(`PDF文件不存在: ${pdfPath}`);
  43. }
  44. // 步骤3: 创建outPut 变量用来储存输出png的目录
  45. if (!outputDir) {
  46. const pdfDir = path.dirname(pdfPath);
  47. const pdfName = path.basename(pdfPath, path.extname(pdfPath));
  48. outputDir = path.join(pdfDir, `${pdfName}_images`);
  49. }
  50. // 确保输出目录存在
  51. if (!fs.existsSync(outputDir)) {
  52. fs.mkdirSync(outputDir, { recursive: true });
  53. }
  54. console.log('='.repeat(60));
  55. console.log('📄 PDF转图片');
  56. console.log('='.repeat(60));
  57. console.log(`📁 PDF文件: ${pdfPath}`);
  58. console.log(`📂 输出目录: ${outputDir}`);
  59. // 步骤4: 调用Python脚本将pdf转换为png图片
  60. const pythonEnv = path.join(projectRoot, 'python', 'venv', 'Scripts', 'python.exe');
  61. const pythonScript = path.join(projectRoot, 'python', 'generate-anim', 'pdf_to_images.py');
  62. // 检查Python脚本是否存在,如果不存在则创建
  63. if (!fs.existsSync(pythonScript)) {
  64. console.log('⚠️ Python脚本不存在,正在创建...');
  65. createPdfToImageScript(pythonScript, projectRoot);
  66. }
  67. const {
  68. dpi = 200,
  69. format = 'png',
  70. firstPage = null,
  71. lastPage = null
  72. } = options;
  73. // 构建命令参数
  74. let command = `"${pythonEnv}" "${pythonScript}" "${pdfPath}" "${outputDir}" --dpi ${dpi} --format ${format}`;
  75. if (firstPage !== null) {
  76. command += ` --first-page ${firstPage}`;
  77. }
  78. if (lastPage !== null) {
  79. command += ` --last-page ${lastPage}`;
  80. }
  81. console.log(`\n🔄 开始转换PDF...`);
  82. console.log(` 分辨率: ${dpi} DPI`);
  83. console.log(` 格式: ${format.toUpperCase()}`);
  84. // 执行Python脚本
  85. execSync(command, {
  86. encoding: 'utf-8',
  87. stdio: 'inherit',
  88. cwd: projectRoot,
  89. env: { ...process.env, PYTHONIOENCODING: 'utf-8' }
  90. });
  91. // 步骤5: 保存结果 - 读取生成的图片文件
  92. const imageFiles = fs.readdirSync(outputDir)
  93. .filter(file => {
  94. const ext = path.extname(file).toLowerCase();
  95. return ['.png', '.jpg', '.jpeg'].includes(ext);
  96. })
  97. .map(file => path.join(outputDir, file))
  98. .sort((a, b) => {
  99. // 按文件名排序(页码顺序)
  100. const nameA = path.basename(a);
  101. const nameB = path.basename(b);
  102. return nameA.localeCompare(nameB, undefined, { numeric: true, sensitivity: 'base' });
  103. });
  104. if (imageFiles.length === 0) {
  105. throw new Error('未生成任何图片文件');
  106. }
  107. console.log(`\n✅ 转换完成!`);
  108. console.log(` 生成图片数: ${imageFiles.length}`);
  109. console.log(` 输出目录: ${outputDir}`);
  110. return {
  111. success: true,
  112. pdfPath: pdfPath,
  113. outputDir: outputDir,
  114. imageFiles: imageFiles,
  115. count: imageFiles.length
  116. };
  117. } catch (error) {
  118. console.error(`\n❌ PDF转图片失败: ${error.message}`);
  119. if (error.stack) {
  120. console.error(error.stack);
  121. }
  122. throw error;
  123. }
  124. }
  125. /**
  126. * 创建PDF转图片的Python脚本
  127. * @param {string} scriptPath - Python脚本路径
  128. * @param {string} projectRoot - 项目根目录
  129. */
  130. function createPdfToImageScript(scriptPath, projectRoot) {
  131. const scriptDir = path.dirname(scriptPath);
  132. if (!fs.existsSync(scriptDir)) {
  133. fs.mkdirSync(scriptDir, { recursive: true });
  134. }
  135. const scriptContent = `# -*- coding: utf-8 -*-
  136. """
  137. PDF转图片脚本
  138. 使用pdf2image库将PDF转换为PNG图片
  139. """
  140. import sys
  141. import os
  142. import argparse
  143. from pathlib import Path
  144. try:
  145. from pdf2image import convert_from_path
  146. from pdf2image.exceptions import (
  147. PDFInfoNotInstalledError,
  148. PDFPageCountError,
  149. PDFSyntaxError
  150. )
  151. except ImportError:
  152. print("错误: 未安装pdf2image库,请运行: pip install pdf2image")
  153. print("注意: Windows系统还需要安装poppler,请参考: https://github.com/Belval/pdf2image")
  154. sys.exit(1)
  155. def pdf_to_images(pdf_path, output_dir, dpi=200, fmt='png', first_page=None, last_page=None):
  156. """
  157. 将PDF转换为图片
  158. Args:
  159. pdf_path: PDF文件路径
  160. output_dir: 输出目录
  161. dpi: 图片分辨率(默认200)
  162. fmt: 输出格式(默认png)
  163. first_page: 起始页码(从1开始,可选)
  164. last_page: 结束页码(可选)
  165. """
  166. pdf_path = Path(pdf_path)
  167. output_dir = Path(output_dir)
  168. if not pdf_path.exists():
  169. raise FileNotFoundError(f"PDF文件不存在: {pdf_path}")
  170. # 确保输出目录存在
  171. output_dir.mkdir(parents=True, exist_ok=True)
  172. print(f"正在转换PDF: {pdf_path.name}")
  173. print(f"输出目录: {output_dir}")
  174. print(f"分辨率: {dpi} DPI")
  175. print(f"格式: {fmt.upper()}")
  176. try:
  177. # 转换PDF为图片
  178. images = convert_from_path(
  179. str(pdf_path),
  180. dpi=dpi,
  181. fmt=fmt,
  182. first_page=first_page,
  183. last_page=last_page,
  184. output_folder=str(output_dir),
  185. output_file='page'
  186. )
  187. # 如果使用output_folder,文件已经保存,images是文件路径列表
  188. if isinstance(images[0], str):
  189. image_files = images
  190. else:
  191. # 否则需要手动保存
  192. image_files = []
  193. pdf_name = pdf_path.stem
  194. for i, image in enumerate(images, start=1):
  195. page_num = first_page + i - 1 if first_page else i
  196. output_file = output_dir / f"{pdf_name}_page{page_num:04d}.{fmt}"
  197. image.save(output_file, fmt.upper())
  198. image_files.append(str(output_file))
  199. print(f" 已保存: {output_file.name}")
  200. print(f"\\n转换完成!共生成 {len(image_files)} 张图片")
  201. return image_files
  202. except PDFInfoNotInstalledError:
  203. print("错误: 未安装poppler工具")
  204. print("Windows: 下载并安装poppler,或使用conda: conda install -c conda-forge poppler")
  205. sys.exit(1)
  206. except PDFPageCountError:
  207. print("错误: 无法获取PDF页数")
  208. sys.exit(1)
  209. except PDFSyntaxError:
  210. print("错误: PDF文件格式错误")
  211. sys.exit(1)
  212. except Exception as e:
  213. print(f"错误: {str(e)}")
  214. sys.exit(1)
  215. if __name__ == '__main__':
  216. parser = argparse.ArgumentParser(description='将PDF转换为图片')
  217. parser.add_argument('pdf_path', help='PDF文件路径')
  218. parser.add_argument('output_dir', help='输出目录')
  219. parser.add_argument('--dpi', type=int, default=200, help='图片分辨率(默认200)')
  220. parser.add_argument('--format', default='png', choices=['png', 'jpg', 'jpeg'], help='输出格式(默认png)')
  221. parser.add_argument('--first-page', type=int, help='起始页码(从1开始)')
  222. parser.add_argument('--last-page', type=int, help='结束页码')
  223. args = parser.parse_args()
  224. pdf_to_images(
  225. args.pdf_path,
  226. args.output_dir,
  227. dpi=args.dpi,
  228. fmt=args.format,
  229. first_page=args.first_page,
  230. last_page=args.last_page
  231. )
  232. `;
  233. fs.writeFileSync(scriptPath, scriptContent, 'utf-8');
  234. console.log(`✅ 已创建Python脚本: ${scriptPath}`);
  235. }
  236. /**
  237. * 导出函数
  238. */
  239. export { start };
  240. // 如果直接运行此文件,执行测试
  241. if (import.meta.url === `file://${path.resolve(process.argv[1])}` ||
  242. process.argv[1]?.endsWith('pdf-to-image.js')) {
  243. // 测试代码
  244. const testPdfPath = path.join(getProjectRoot(), 'static', '漫画', 'pdf', 'test.pdf');
  245. if (fs.existsSync(testPdfPath)) {
  246. start(testPdfPath, null, { dpi: 200, format: 'png' })
  247. .then(result => {
  248. console.log('\n测试完成!');
  249. console.log(`生成图片: ${result.count} 张`);
  250. })
  251. .catch(error => {
  252. console.error('测试失败:', error.message);
  253. process.exit(1);
  254. });
  255. } else {
  256. console.log('测试PDF文件不存在,跳过测试');
  257. console.log(`请提供PDF文件路径: ${testPdfPath}`);
  258. }
  259. }