node-pdf2img 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * pdf2img CLI - 命令行 PDF 转图片工具
5
+ *
6
+ * 用法:
7
+ * pdf2img <input> [options]
8
+ *
9
+ * 示例:
10
+ * pdf2img document.pdf -o ./output
11
+ * pdf2img https://example.com/doc.pdf -o ./output
12
+ * pdf2img document.pdf -p 1,2,3 -o ./output
13
+ * pdf2img document.pdf --quality 90 --width 1920 -o ./output
14
+ * pdf2img document.pdf --format png -o ./output # 输出 PNG 格式
15
+ * pdf2img document.pdf --cos --cos-prefix images/doc # 上传到 COS
16
+ *
17
+ * COS 环境变量:
18
+ * COS_SECRET_ID - 腾讯云 SecretId
19
+ * COS_SECRET_KEY - 腾讯云 SecretKey
20
+ * COS_BUCKET - COS 存储桶名称
21
+ * COS_REGION - COS 地域(如 ap-guangzhou)
22
+ */
23
+
24
+ import { program } from 'commander';
25
+ import path from 'path';
26
+ import fs from 'fs';
27
+ import { fileURLToPath } from 'url';
28
+
29
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
31
+
32
+ // 动态导入 ora(ESM 模块)
33
+ let ora;
34
+ try {
35
+ ora = (await import('ora')).default;
36
+ } catch {
37
+ // 如果 ora 不可用,提供一个简单的替代
38
+ ora = (text) => ({
39
+ start: () => ({ text, succeed: () => {}, fail: () => {}, stop: () => {} }),
40
+ });
41
+ }
42
+
43
+ // 处理 --version-info 选项(在解析前检查)
44
+ if (process.argv.includes('--version-info')) {
45
+ const { isAvailable, getVersion } = await import('../src/index.js');
46
+ console.log(`pdf2img v${pkg.version}`);
47
+ console.log(`原生渲染器: ${getVersion()}`);
48
+ console.log(`可用: ${isAvailable() ? '是' : '否'}`);
49
+ process.exit(0);
50
+ }
51
+
52
+ program
53
+ .name('pdf2img')
54
+ .description('高性能 PDF 转图片工具,基于 PDFium')
55
+ .version(pkg.version)
56
+ .argument('<input>', 'PDF 文件路径或 URL')
57
+ .option('-o, --output <dir>', '输出目录(本地模式)', './output')
58
+ .option('-p, --pages <pages>', '要转换的页码(逗号分隔,如 1,2,3)')
59
+ .option('-w, --width <width>', '目标渲染宽度(像素)', '1920')
60
+ .option('-q, --quality <quality>', '图片质量(0-100,用于 webp/jpg)', '100')
61
+ .option('-f, --format <format>', '输出格式:webp, png, jpg', 'webp')
62
+ .option('--prefix <prefix>', '输出文件名前缀', 'page')
63
+ .option('--info', '仅显示 PDF 信息(页数)')
64
+ .option('--version-info', '显示原生渲染器版本')
65
+ .option('-v, --verbose', '详细输出')
66
+ // COS 相关选项
67
+ .option('--cos', '上传到腾讯云 COS(需配置环境变量)')
68
+ .option('--cos-prefix <prefix>', 'COS key 前缀(如 images/doc-123)')
69
+ .option('--cos-secret-id <id>', 'COS SecretId(优先使用环境变量 COS_SECRET_ID)')
70
+ .option('--cos-secret-key <key>', 'COS SecretKey(优先使用环境变量 COS_SECRET_KEY)')
71
+ .option('--cos-bucket <bucket>', 'COS 存储桶(优先使用环境变量 COS_BUCKET)')
72
+ .option('--cos-region <region>', 'COS 地域(优先使用环境变量 COS_REGION)')
73
+ .action(async (input, options) => {
74
+ // 设置调试模式
75
+ if (options.verbose) {
76
+ process.env.PDF2IMG_DEBUG = 'true';
77
+ }
78
+
79
+ // 动态导入主模块
80
+ const { convert, getPageCount, isAvailable, getVersion } = await import('../src/index.js');
81
+
82
+ // 显示版本信息
83
+ if (options.versionInfo) {
84
+ console.log(`pdf2img v${pkg.version}`);
85
+ console.log(`原生渲染器: ${getVersion()}`);
86
+ console.log(`可用: ${isAvailable() ? '是' : '否'}`);
87
+ return;
88
+ }
89
+
90
+ // 检查 native renderer
91
+ if (!isAvailable()) {
92
+ console.error('错误:原生渲染器不可用。');
93
+ console.error('请确保 PDFium 库已正确安装。');
94
+ process.exit(1);
95
+ }
96
+
97
+ // 验证格式
98
+ const supportedFormats = ['webp', 'png', 'jpg', 'jpeg'];
99
+ const format = options.format.toLowerCase();
100
+ if (!supportedFormats.includes(format)) {
101
+ console.error(`错误:不支持的格式 "${options.format}"。支持的格式:webp, png, jpg`);
102
+ process.exit(1);
103
+ }
104
+
105
+ // 检查输入
106
+ const isUrl = input.startsWith('http://') || input.startsWith('https://');
107
+ if (!isUrl && !fs.existsSync(input)) {
108
+ console.error(`错误:文件不存在: ${input}`);
109
+ process.exit(1);
110
+ }
111
+
112
+ // 仅显示 PDF 信息
113
+ if (options.info) {
114
+ if (isUrl) {
115
+ console.error('错误:--info 选项仅支持本地文件');
116
+ process.exit(1);
117
+ }
118
+
119
+ try {
120
+ const pageCount = getPageCount(input);
121
+ const stat = fs.statSync(input);
122
+ console.log(`文件: ${path.basename(input)}`);
123
+ console.log(`大小: ${(stat.size / 1024 / 1024).toFixed(2)} MB`);
124
+ console.log(`页数: ${pageCount}`);
125
+ } catch (err) {
126
+ console.error(`错误: ${err.message}`);
127
+ process.exit(1);
128
+ }
129
+ return;
130
+ }
131
+
132
+ // 解析页码
133
+ let pages = [];
134
+ if (options.pages) {
135
+ pages = options.pages.split(',').map(p => parseInt(p.trim(), 10)).filter(p => !isNaN(p) && p > 0);
136
+ }
137
+
138
+ // 确定输出类型和配置
139
+ let outputType = 'file';
140
+ let convertOptions = {
141
+ pages,
142
+ prefix: options.prefix,
143
+ targetWidth: parseInt(options.width, 10),
144
+ quality: parseInt(options.quality, 10),
145
+ format: format,
146
+ };
147
+
148
+ // COS 模式
149
+ if (options.cos) {
150
+ outputType = 'cos';
151
+
152
+ // 从环境变量或命令行参数获取 COS 配置
153
+ const cosConfig = {
154
+ secretId: options.cosSecretId || process.env.COS_SECRET_ID,
155
+ secretKey: options.cosSecretKey || process.env.COS_SECRET_KEY,
156
+ bucket: options.cosBucket || process.env.COS_BUCKET,
157
+ region: options.cosRegion || process.env.COS_REGION,
158
+ };
159
+
160
+ // 验证 COS 配置
161
+ const missingFields = [];
162
+ if (!cosConfig.secretId) missingFields.push('COS_SECRET_ID');
163
+ if (!cosConfig.secretKey) missingFields.push('COS_SECRET_KEY');
164
+ if (!cosConfig.bucket) missingFields.push('COS_BUCKET');
165
+ if (!cosConfig.region) missingFields.push('COS_REGION');
166
+
167
+ if (missingFields.length > 0) {
168
+ console.error('错误:COS 配置不完整,缺少以下环境变量或参数:');
169
+ console.error(` ${missingFields.join(', ')}`);
170
+ console.error('\n请设置环境变量或使用命令行参数:');
171
+ console.error(' export COS_SECRET_ID=xxx');
172
+ console.error(' export COS_SECRET_KEY=xxx');
173
+ console.error(' export COS_BUCKET=xxx');
174
+ console.error(' export COS_REGION=ap-guangzhou');
175
+ process.exit(1);
176
+ }
177
+
178
+ convertOptions.cos = cosConfig;
179
+ convertOptions.cosKeyPrefix = options.cosPrefix || '';
180
+ } else {
181
+ // 本地文件模式
182
+ convertOptions.outputDir = options.output;
183
+ }
184
+
185
+ convertOptions.outputType = outputType;
186
+
187
+ // 开始转换
188
+ const modeText = options.cos ? '转换并上传' : '转换';
189
+ const spinner = ora(`正在${modeText} ${path.basename(input)}...`).start();
190
+
191
+ try {
192
+ const startTime = Date.now();
193
+
194
+ const result = await convert(input, convertOptions);
195
+
196
+ const duration = Date.now() - startTime;
197
+
198
+ spinner.succeed(`${modeText}完成 ${result.renderedPages}/${result.numPages} 页,格式: ${result.format.toUpperCase()},耗时 ${duration}ms`);
199
+
200
+ // 显示结果
201
+ if (options.cos) {
202
+ console.log('\n已上传到 COS:');
203
+ for (const page of result.pages) {
204
+ if (page.success) {
205
+ console.log(` 第 ${page.pageNum} 页: ${page.width}x${page.height} -> ${page.cosKey} (${formatBytes(page.size)})`);
206
+ } else {
207
+ console.log(` 第 ${page.pageNum} 页: 失败 - ${page.error}`);
208
+ }
209
+ }
210
+ } else {
211
+ console.log(`\n输出目录: ${path.resolve(options.output)}`);
212
+ console.log('\n页面详情:');
213
+ for (const page of result.pages) {
214
+ if (page.success) {
215
+ console.log(` 第 ${page.pageNum} 页: ${page.width}x${page.height} -> ${path.basename(page.outputPath)} (${formatBytes(page.size)})`);
216
+ } else {
217
+ console.log(` 第 ${page.pageNum} 页: 失败 - ${page.error}`);
218
+ }
219
+ }
220
+ }
221
+
222
+ // 统计
223
+ const totalSize = result.pages.reduce((sum, p) => sum + (p.size || 0), 0);
224
+ console.log(`\n总输出大小: ${formatBytes(totalSize)}`);
225
+ console.log(`平均每页耗时: ${Math.round(duration / result.renderedPages)}ms`);
226
+
227
+ } catch (err) {
228
+ spinner.fail(`${modeText}失败: ${err.message}`);
229
+ if (options.verbose) {
230
+ console.error(err.stack);
231
+ }
232
+ process.exit(1);
233
+ }
234
+ });
235
+
236
+ /**
237
+ * 格式化字节数
238
+ */
239
+ function formatBytes(bytes) {
240
+ if (bytes === 0) return '0 B';
241
+ const k = 1024;
242
+ const sizes = ['B', 'KB', 'MB', 'GB'];
243
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
244
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
245
+ }
246
+
247
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "node-pdf2img",
3
+ "version": "0.1.1",
4
+ "description": "High-performance PDF to image converter using PDFium native renderer",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "bin": {
9
+ "pdf2img": "./bin/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./src/index.js",
14
+ "types": "./src/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "src",
19
+ "bin"
20
+ ],
21
+ "scripts": {
22
+ "test": "node --test test/*.test.js",
23
+ "test:run": "node test/api.test.js",
24
+ "build:native": "cd ../native-renderer && npm run build",
25
+ "prepublishOnly": "npm run test"
26
+ },
27
+ "keywords": [
28
+ "pdf",
29
+ "image",
30
+ "converter",
31
+ "pdfium",
32
+ "webp",
33
+ "native",
34
+ "cli"
35
+ ],
36
+ "author": "",
37
+ "license": "MIT",
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "os": [
42
+ "linux",
43
+ "darwin",
44
+ "win32"
45
+ ],
46
+ "cpu": [
47
+ "x64",
48
+ "arm64"
49
+ ],
50
+ "dependencies": {
51
+ "commander": "^12.0.0",
52
+ "cos-nodejs-sdk-v5": "^2.14.6",
53
+ "dotenv": "^16.0.3",
54
+ "ora": "^8.0.0",
55
+ "p-limit": "^7.2.0",
56
+ "piscina": "^5.1.4",
57
+ "sharp": "^0.33.0",
58
+ "node-pdf2img-native": "file:../native-renderer"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^20.0.0"
62
+ },
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/sigma-2026/node-pdf2img.git"
66
+ },
67
+ "bugs": {
68
+ "url": "https://github.com/sigma-2026/node-pdf2img/issues"
69
+ },
70
+ "homepage": "https://github.com/sigma-2026/node-pdf2img#readme"
71
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * PDF2IMG 配置
3
+ */
4
+
5
+ // ==================== 渲染配置 ====================
6
+ export const RENDER_CONFIG = {
7
+ // 目标渲染宽度(像素)
8
+ TARGET_RENDER_WIDTH: parseInt(process.env.TARGET_RENDER_WIDTH) || 1280,
9
+
10
+ // 图片密集型页面的目标宽度(像素)
11
+ IMAGE_HEAVY_TARGET_WIDTH: parseInt(process.env.IMAGE_HEAVY_TARGET_WIDTH) || 1024,
12
+
13
+ // 最大渲染缩放比例
14
+ MAX_RENDER_SCALE: parseFloat(process.env.MAX_RENDER_SCALE) || 4.0,
15
+
16
+ // 默认输出格式:webp, png, jpg
17
+ OUTPUT_FORMAT: process.env.OUTPUT_FORMAT || 'webp',
18
+
19
+ // Native Stream 阈值(字节)- 大于此值使用流式加载
20
+ NATIVE_STREAM_THRESHOLD: parseInt(process.env.NATIVE_STREAM_THRESHOLD) || 5 * 1024 * 1024, // 5MB
21
+ };
22
+
23
+ // ==================== 编码器配置 ====================
24
+ export const ENCODER_CONFIG = {
25
+ // WebP 编码质量(0-100)
26
+ WEBP_QUALITY: parseInt(process.env.WEBP_QUALITY) || 80,
27
+
28
+ // WebP 编码方法/速度(0-6,0最快,6最慢但压缩最好)
29
+ // 默认值 4 是速度和压缩率的最佳平衡点
30
+ WEBP_METHOD: parseInt(process.env.WEBP_METHOD) || 4,
31
+
32
+ // JPEG 编码质量(0-100)
33
+ JPEG_QUALITY: parseInt(process.env.JPEG_QUALITY) || 85,
34
+
35
+ // PNG 压缩级别(0-9,0不压缩,9最大压缩)
36
+ PNG_COMPRESSION: parseInt(process.env.PNG_COMPRESSION) || 6,
37
+ };
38
+
39
+ // ==================== 超时配置 ====================
40
+ export const TIMEOUT_CONFIG = {
41
+ // 分片请求超时
42
+ RANGE_REQUEST_TIMEOUT: parseInt(process.env.RANGE_REQUEST_TIMEOUT) || 25000, // 25s
43
+
44
+ // 下载超时
45
+ DOWNLOAD_TIMEOUT: parseInt(process.env.DOWNLOAD_TIMEOUT) || 60000, // 60s
46
+ };
47
+
48
+ // ==================== 支持的输出格式 ====================
49
+ export const SUPPORTED_FORMATS = ['webp', 'png', 'jpg', 'jpeg'];
50
+
51
+ /**
52
+ * 合并用户配置与默认配置
53
+ * @param {Object} userConfig - 用户配置
54
+ * @returns {Object} 合并后的配置(用于原生渲染器)
55
+ */
56
+ export function mergeConfig(userConfig = {}) {
57
+ const format = userConfig.format ?? RENDER_CONFIG.OUTPUT_FORMAT;
58
+
59
+ return {
60
+ targetWidth: userConfig.targetWidth ?? RENDER_CONFIG.TARGET_RENDER_WIDTH,
61
+ imageHeavyWidth: userConfig.imageHeavyWidth ?? RENDER_CONFIG.IMAGE_HEAVY_TARGET_WIDTH,
62
+ maxScale: userConfig.maxScale ?? RENDER_CONFIG.MAX_RENDER_SCALE,
63
+ detectScan: userConfig.detectScan ?? true,
64
+ format,
65
+
66
+ // WebP 编码配置
67
+ webpQuality: userConfig.webp?.quality ?? userConfig.quality ?? ENCODER_CONFIG.WEBP_QUALITY,
68
+ webpMethod: userConfig.webp?.method ?? ENCODER_CONFIG.WEBP_METHOD,
69
+
70
+ // JPEG 编码配置
71
+ jpegQuality: userConfig.jpeg?.quality ?? userConfig.quality ?? ENCODER_CONFIG.JPEG_QUALITY,
72
+
73
+ // PNG 编码配置
74
+ pngCompression: userConfig.png?.compressionLevel ?? ENCODER_CONFIG.PNG_COMPRESSION,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * 获取文件扩展名
80
+ * @param {string} format - 格式名称
81
+ * @returns {string} 文件扩展名
82
+ */
83
+ export function getExtension(format) {
84
+ if (format === 'jpeg') return 'jpg';
85
+ return format;
86
+ }
87
+
88
+ /**
89
+ * 获取 MIME 类型
90
+ * @param {string} format - 格式名称
91
+ * @returns {string} MIME 类型
92
+ */
93
+ export function getMimeType(format) {
94
+ const mimeTypes = {
95
+ webp: 'image/webp',
96
+ png: 'image/png',
97
+ jpg: 'image/jpeg',
98
+ jpeg: 'image/jpeg',
99
+ };
100
+ return mimeTypes[format] || 'image/webp';
101
+ }