deepfish-ai 1.0.29 → 2.0.3
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/README.md +195 -344
- package/dist/generate-skill.md +104 -0
- package/dist/generate-tool.md +88 -0
- package/dist/index.js +26831 -0
- package/dist/serve/client/assets/index-DUHYC91l.js +1 -0
- package/dist/serve/client/assets/index-zcrdNLZs.css +1 -0
- package/dist/serve/client/index.html +14 -0
- package/dist/serve/pm2-server.js +24405 -0
- package/dist/serve/server/entry-server.mjs +224 -0
- package/dist/view-learn-cache.md +65 -0
- package/package.json +77 -83
- package/LICENSE +0 -21
- package/README_CN.md +0 -460
- package/src/AgentRobot/AgentRobotFactory/MainAgentRobot.js +0 -88
- package/src/AgentRobot/AgentRobotFactory/SubAgentRobot.js +0 -45
- package/src/AgentRobot/AgentRobotFactory/SubSkillAgentRobot.js +0 -45
- package/src/AgentRobot/AgentRobotFactory/index.js +0 -48
- package/src/AgentRobot/BaseAgentRobot/Brain.js +0 -210
- package/src/AgentRobot/BaseAgentRobot/BrainEvent.js +0 -17
- package/src/AgentRobot/BaseAgentRobot/Hand.js +0 -119
- package/src/AgentRobot/BaseAgentRobot/Heart.js +0 -0
- package/src/AgentRobot/BaseAgentRobot/Logger.js +0 -114
- package/src/AgentRobot/BaseAgentRobot/ScreenPrinter.js +0 -49
- package/src/AgentRobot/BaseAgentRobot/index.js +0 -266
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/AliBailian.js +0 -997
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/doc-transform.js +0 -204
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/docx.js +0 -1286
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/img.js +0 -765
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/pdf.js +0 -736
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/pptx.js +0 -567
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/video.js +0 -36
- package/src/AgentRobot/BaseAgentRobot/lazy-tools/xlsx.js +0 -611
- package/src/AgentRobot/BaseAgentRobot/tools/BaseTools.js +0 -13
- package/src/AgentRobot/BaseAgentRobot/tools/CreateAgentTools.js +0 -158
- package/src/AgentRobot/BaseAgentRobot/tools/FileTools.js +0 -768
- package/src/AgentRobot/BaseAgentRobot/tools/GenerateTools.js +0 -441
- package/src/AgentRobot/BaseAgentRobot/tools/InquirerTools.js +0 -244
- package/src/AgentRobot/BaseAgentRobot/tools/SystemTools.js +0 -424
- package/src/AgentRobot/BaseAgentRobot/tools/TaskTools.js +0 -211
- package/src/AgentRobot/BaseAgentRobot/tools/TestTools.js +0 -100
- package/src/AgentRobot/BaseAgentRobot/tools/UserTool.js +0 -87
- package/src/AgentRobot/BaseAgentRobot/tools/WebTools.js +0 -257
- package/src/AgentRobot/BaseAgentRobot/utils/AIRequest.js +0 -297
- package/src/AgentRobot/BaseAgentRobot/utils/AIToolManager.js +0 -131
- package/src/AgentRobot/BaseAgentRobot/utils/AgentTree.js +0 -66
- package/src/AgentRobot/BaseAgentRobot/utils/AttachmentToolScanner.js +0 -235
- package/src/AgentRobot/BaseAgentRobot/utils/MessageCompresser.js +0 -236
- package/src/AgentRobot/BaseAgentRobot/utils/aiConsole.js +0 -74
- package/src/AgentRobot/BaseAgentRobot/utils/aiInquirer.js +0 -78
- package/src/AgentRobot/BaseAgentRobot/utils/analyzeReturn.test.js +0 -104
- package/src/AgentRobot/BaseAgentRobot/utils/copilot.js +0 -117
- package/src/AgentRobot/BaseAgentRobot/utils/node-root.js +0 -138
- package/src/AgentRobot/BaseAgentRobot/utils/normal.js +0 -399
- package/src/cli/ConfigManager.js +0 -283
- package/src/cli/DefaultConfig.js +0 -98
- package/src/cli/GlobalVariable.js +0 -8
- package/src/cli/MemoryManager.js +0 -53
- package/src/cli/SkillConfigManager.js +0 -354
- package/src/cli/SkillParser.js +0 -58
- package/src/cli/ai-auth.js +0 -231
- package/src/cli/ai-config.js +0 -225
- package/src/cli/ai-memory.js +0 -22
- package/src/cli/ai-skill.js +0 -65
- package/src/cli/index.js +0 -77
- package/src/index.js +0 -54
|
@@ -1,736 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @Author: Roman 306863030@qq.com
|
|
3
|
-
* @Description: PDF 文档处理工具集(pdf-parse / pdf-lib / pdfkit)
|
|
4
|
-
*/
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const fs = require('fs-extra')
|
|
7
|
-
const pdfParse = require('pdf-parse')
|
|
8
|
-
const { PDFDocument, degrees, rgb, StandardFonts } = require('pdf-lib')
|
|
9
|
-
const PDFKit = require('pdfkit')
|
|
10
|
-
const { pdf } = require('pdf-to-img')
|
|
11
|
-
const sharp = require('sharp')
|
|
12
|
-
|
|
13
|
-
// ─── 统一返回结构 ─────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
function ok(data = null) {
|
|
16
|
-
return { success: true, data }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function fail(error, data = null) {
|
|
20
|
-
return { success: false, error: error?.message || String(error), data }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function resolvePath(filePath) {
|
|
24
|
-
return path.resolve(process.cwd(), filePath)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 读取 PDF 中的全部文本内容
|
|
31
|
-
*/
|
|
32
|
-
async function readPdfText(filePath) {
|
|
33
|
-
try {
|
|
34
|
-
const fullPath = resolvePath(filePath)
|
|
35
|
-
if (!fs.existsSync(fullPath)) return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
36
|
-
const dataBuffer = fs.readFileSync(fullPath)
|
|
37
|
-
const data = await pdfParse(dataBuffer)
|
|
38
|
-
return ok({
|
|
39
|
-
filePath: fullPath,
|
|
40
|
-
text: data.text,
|
|
41
|
-
pageCount: data.numpages,
|
|
42
|
-
wordCount: data.text.trim().split(/\s+/).filter(Boolean).length,
|
|
43
|
-
charCount: data.text.length,
|
|
44
|
-
})
|
|
45
|
-
} catch (error) {
|
|
46
|
-
return fail(error, { filePath })
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 获取 PDF 文档元信息(页数、作者、标题、创建时间等)
|
|
52
|
-
*/
|
|
53
|
-
async function getPdfInfo(filePath) {
|
|
54
|
-
try {
|
|
55
|
-
const fullPath = resolvePath(filePath)
|
|
56
|
-
if (!fs.existsSync(fullPath)) return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
57
|
-
const dataBuffer = fs.readFileSync(fullPath)
|
|
58
|
-
const parseData = await pdfParse(dataBuffer)
|
|
59
|
-
const pdfDoc = await PDFDocument.load(dataBuffer)
|
|
60
|
-
const stat = fs.statSync(fullPath)
|
|
61
|
-
return ok({
|
|
62
|
-
filePath: fullPath,
|
|
63
|
-
pageCount: parseData.numpages,
|
|
64
|
-
size: stat.size,
|
|
65
|
-
mtime: stat.mtime,
|
|
66
|
-
title: pdfDoc.getTitle() || null,
|
|
67
|
-
author: pdfDoc.getAuthor() || null,
|
|
68
|
-
subject: pdfDoc.getSubject() || null,
|
|
69
|
-
keywords: pdfDoc.getKeywords() || null,
|
|
70
|
-
creator: pdfDoc.getCreator() || null,
|
|
71
|
-
producer: pdfDoc.getProducer() || null,
|
|
72
|
-
creationDate: pdfDoc.getCreationDate() || null,
|
|
73
|
-
modificationDate: pdfDoc.getModificationDate() || null,
|
|
74
|
-
pdfVersion: parseData.info?.PDFFormatVersion || null,
|
|
75
|
-
})
|
|
76
|
-
} catch (error) {
|
|
77
|
-
return fail(error, { filePath })
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 仅获取 PDF 总页数(轻量调用)
|
|
83
|
-
*/
|
|
84
|
-
async function getPdfPageCount(filePath) {
|
|
85
|
-
try {
|
|
86
|
-
const fullPath = resolvePath(filePath)
|
|
87
|
-
if (!fs.existsSync(fullPath)) return fail(`File does not exist: ${fullPath}`, { filePath: fullPath })
|
|
88
|
-
const pdfDoc = await PDFDocument.load(fs.readFileSync(fullPath))
|
|
89
|
-
return ok({ filePath: fullPath, pageCount: pdfDoc.getPageCount() })
|
|
90
|
-
} catch (error) {
|
|
91
|
-
return fail(error, { filePath })
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 在 PDF 中搜索关键词,返回包含该词的行
|
|
97
|
-
*/
|
|
98
|
-
async function searchPdfText(filePath, keyword) {
|
|
99
|
-
try {
|
|
100
|
-
const readResult = await readPdfText(filePath)
|
|
101
|
-
if (!readResult.success) return readResult
|
|
102
|
-
const lines = readResult.data.text.split('\n')
|
|
103
|
-
const matches = lines
|
|
104
|
-
.map((line, idx) => ({ line: idx + 1, text: line }))
|
|
105
|
-
.filter(({ text }) => text.includes(keyword))
|
|
106
|
-
return ok({
|
|
107
|
-
filePath: resolvePath(filePath),
|
|
108
|
-
keyword,
|
|
109
|
-
matchCount: matches.length,
|
|
110
|
-
matches,
|
|
111
|
-
})
|
|
112
|
-
} catch (error) {
|
|
113
|
-
return fail(error, { filePath, keyword })
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 合并多个 PDF 文件为一个
|
|
119
|
-
*/
|
|
120
|
-
async function mergePdfs(inputPaths, outputPath) {
|
|
121
|
-
try {
|
|
122
|
-
const fullOutput = resolvePath(outputPath)
|
|
123
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
124
|
-
const mergedDoc = await PDFDocument.create()
|
|
125
|
-
for (const inputPath of inputPaths) {
|
|
126
|
-
const fullInput = resolvePath(inputPath)
|
|
127
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
128
|
-
const srcDoc = await PDFDocument.load(fs.readFileSync(fullInput))
|
|
129
|
-
const pages = await mergedDoc.copyPages(srcDoc, srcDoc.getPageIndices())
|
|
130
|
-
pages.forEach((p) => mergedDoc.addPage(p))
|
|
131
|
-
}
|
|
132
|
-
fs.writeFileSync(fullOutput, await mergedDoc.save())
|
|
133
|
-
return ok({ outputPath: fullOutput, pageCount: mergedDoc.getPageCount(), inputCount: inputPaths.length })
|
|
134
|
-
} catch (error) {
|
|
135
|
-
return fail(error, { outputPath })
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* 将 PDF 按每页拆分为独立文件,保存到输出目录
|
|
141
|
-
*/
|
|
142
|
-
async function splitPdf(filePath, outputDir) {
|
|
143
|
-
try {
|
|
144
|
-
const fullInput = resolvePath(filePath)
|
|
145
|
-
const fullOutputDir = resolvePath(outputDir)
|
|
146
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
147
|
-
fs.ensureDirSync(fullOutputDir)
|
|
148
|
-
const srcDoc = await PDFDocument.load(fs.readFileSync(fullInput))
|
|
149
|
-
const pageCount = srcDoc.getPageCount()
|
|
150
|
-
const baseName = path.basename(fullInput, path.extname(fullInput))
|
|
151
|
-
const results = []
|
|
152
|
-
for (let i = 0; i < pageCount; i++) {
|
|
153
|
-
const newDoc = await PDFDocument.create()
|
|
154
|
-
const [page] = await newDoc.copyPages(srcDoc, [i])
|
|
155
|
-
newDoc.addPage(page)
|
|
156
|
-
const outFile = path.join(fullOutputDir, `${baseName}_page${i + 1}.pdf`)
|
|
157
|
-
fs.writeFileSync(outFile, await newDoc.save())
|
|
158
|
-
results.push(outFile)
|
|
159
|
-
}
|
|
160
|
-
return ok({ filePath: fullInput, outputDir: fullOutputDir, pageCount, files: results })
|
|
161
|
-
} catch (error) {
|
|
162
|
-
return fail(error, { filePath, outputDir })
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 从 PDF 中提取指定页码(1-based)生成新文件
|
|
168
|
-
*/
|
|
169
|
-
async function extractPdfPages(filePath, outputPath, pages) {
|
|
170
|
-
try {
|
|
171
|
-
const fullInput = resolvePath(filePath)
|
|
172
|
-
const fullOutput = resolvePath(outputPath)
|
|
173
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
174
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
175
|
-
const srcDoc = await PDFDocument.load(fs.readFileSync(fullInput))
|
|
176
|
-
const total = srcDoc.getPageCount()
|
|
177
|
-
const indices = (pages || []).map((p) => p - 1).filter((i) => i >= 0 && i < total)
|
|
178
|
-
if (indices.length === 0) return fail('No valid page numbers provided', { filePath: fullInput, pages })
|
|
179
|
-
const newDoc = await PDFDocument.create()
|
|
180
|
-
const copied = await newDoc.copyPages(srcDoc, indices)
|
|
181
|
-
copied.forEach((p) => newDoc.addPage(p))
|
|
182
|
-
fs.writeFileSync(fullOutput, await newDoc.save())
|
|
183
|
-
return ok({ filePath: fullInput, outputPath: fullOutput, extractedPages: indices.map((i) => i + 1) })
|
|
184
|
-
} catch (error) {
|
|
185
|
-
return fail(error, { filePath, outputPath })
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* 旋转 PDF 页面(可指定特定页或全部页)
|
|
191
|
-
*/
|
|
192
|
-
async function rotatePdfPages(filePath, outputPath, rotateDegrees, pageNumbers) {
|
|
193
|
-
try {
|
|
194
|
-
const fullInput = resolvePath(filePath)
|
|
195
|
-
const fullOutput = resolvePath(outputPath)
|
|
196
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
197
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
198
|
-
const pdfDoc = await PDFDocument.load(fs.readFileSync(fullInput))
|
|
199
|
-
const deg = rotateDegrees || 90
|
|
200
|
-
const total = pdfDoc.getPageCount()
|
|
201
|
-
const targets = Array.isArray(pageNumbers) && pageNumbers.length > 0
|
|
202
|
-
? pageNumbers.map((p) => p - 1).filter((i) => i >= 0 && i < total)
|
|
203
|
-
: Array.from({ length: total }, (_, i) => i)
|
|
204
|
-
for (const idx of targets) {
|
|
205
|
-
const page = pdfDoc.getPage(idx)
|
|
206
|
-
page.setRotation(degrees((page.getRotation().angle + deg) % 360))
|
|
207
|
-
}
|
|
208
|
-
fs.writeFileSync(fullOutput, await pdfDoc.save())
|
|
209
|
-
return ok({ filePath: fullInput, outputPath: fullOutput, degrees: deg, rotatedPages: targets.map((i) => i + 1) })
|
|
210
|
-
} catch (error) {
|
|
211
|
-
return fail(error, { filePath, outputPath })
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* 向 PDF 全部页面添加文字水印
|
|
217
|
-
*/
|
|
218
|
-
async function addWatermarkText(filePath, outputPath, text, options = {}) {
|
|
219
|
-
try {
|
|
220
|
-
const fullInput = resolvePath(filePath)
|
|
221
|
-
const fullOutput = resolvePath(outputPath)
|
|
222
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
223
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
224
|
-
const pdfDoc = await PDFDocument.load(fs.readFileSync(fullInput))
|
|
225
|
-
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
|
226
|
-
const fontSize = options.fontSize || 48
|
|
227
|
-
const opacity = options.opacity !== undefined ? options.opacity : 0.2
|
|
228
|
-
const color = options.color
|
|
229
|
-
? rgb(options.color.r / 255, options.color.g / 255, options.color.b / 255)
|
|
230
|
-
: rgb(0.5, 0.5, 0.5)
|
|
231
|
-
const rotateAngle = options.angle !== undefined ? options.angle : 45
|
|
232
|
-
const pages = pdfDoc.getPages()
|
|
233
|
-
for (const page of pages) {
|
|
234
|
-
const { width, height } = page.getSize()
|
|
235
|
-
page.drawText(text, {
|
|
236
|
-
x: width / 2 - (font.widthOfTextAtSize(text, fontSize) / 2),
|
|
237
|
-
y: height / 2,
|
|
238
|
-
size: fontSize,
|
|
239
|
-
font,
|
|
240
|
-
color,
|
|
241
|
-
opacity,
|
|
242
|
-
rotate: degrees(rotateAngle),
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
|
-
fs.writeFileSync(fullOutput, await pdfDoc.save())
|
|
246
|
-
return ok({ filePath: fullInput, outputPath: fullOutput, text, pageCount: pages.length })
|
|
247
|
-
} catch (error) {
|
|
248
|
-
return fail(error, { filePath, outputPath })
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* 创建新 PDF(支持文本段落、标题、分页)
|
|
254
|
-
* content 为数组,每项:{ type: 'text'|'heading'|'pageBreak', text, fontSize, bold, color, align }
|
|
255
|
-
*/
|
|
256
|
-
async function createPdf(outputPath, content = [], options = {}) {
|
|
257
|
-
try {
|
|
258
|
-
const fullOutput = resolvePath(outputPath)
|
|
259
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
260
|
-
return await new Promise((resolve, reject) => {
|
|
261
|
-
const doc = new PDFKit({
|
|
262
|
-
size: options.pageSize || 'A4',
|
|
263
|
-
margin: options.margin || 50,
|
|
264
|
-
info: {
|
|
265
|
-
Title: options.title || '',
|
|
266
|
-
Author: options.author || '',
|
|
267
|
-
Subject: options.subject || '',
|
|
268
|
-
},
|
|
269
|
-
})
|
|
270
|
-
const stream = fs.createWriteStream(fullOutput)
|
|
271
|
-
doc.pipe(stream)
|
|
272
|
-
|
|
273
|
-
for (const sec of content) {
|
|
274
|
-
switch (sec.type) {
|
|
275
|
-
case 'heading':
|
|
276
|
-
doc.font('Helvetica-Bold').fontSize(sec.fontSize || 18)
|
|
277
|
-
if (sec.color) doc.fillColor(sec.color)
|
|
278
|
-
doc.text(sec.text || '', { align: sec.align || 'left' })
|
|
279
|
-
doc.fillColor('#000000').font('Helvetica')
|
|
280
|
-
doc.moveDown(0.5)
|
|
281
|
-
break
|
|
282
|
-
case 'pageBreak':
|
|
283
|
-
doc.addPage()
|
|
284
|
-
break
|
|
285
|
-
case 'text':
|
|
286
|
-
default:
|
|
287
|
-
doc.font(sec.bold ? 'Helvetica-Bold' : 'Helvetica').fontSize(sec.fontSize || 12)
|
|
288
|
-
if (sec.color) doc.fillColor(sec.color)
|
|
289
|
-
doc.text(sec.text || '', { align: sec.align || 'left' })
|
|
290
|
-
doc.fillColor('#000000').font('Helvetica')
|
|
291
|
-
doc.moveDown(0.3)
|
|
292
|
-
break
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
doc.end()
|
|
297
|
-
stream.on('finish', () => resolve(ok({ outputPath: fullOutput, itemCount: content.length })))
|
|
298
|
-
stream.on('error', (err) => reject(err))
|
|
299
|
-
})
|
|
300
|
-
} catch (error) {
|
|
301
|
-
return fail(error, { outputPath })
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* 将纯文本文件转换为 PDF
|
|
307
|
-
*/
|
|
308
|
-
async function textToPdf(inputPath, outputPath, options = {}) {
|
|
309
|
-
try {
|
|
310
|
-
const fullInput = resolvePath(inputPath)
|
|
311
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
312
|
-
const text = fs.readFileSync(fullInput, 'utf8')
|
|
313
|
-
const content = [{ type: 'text', text }]
|
|
314
|
-
return await createPdf(outputPath, content, options)
|
|
315
|
-
} catch (error) {
|
|
316
|
-
return fail(error, { inputPath, outputPath })
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* 在 PDF 末尾追加空白页
|
|
322
|
-
*/
|
|
323
|
-
async function appendBlankPages(filePath, outputPath, count) {
|
|
324
|
-
try {
|
|
325
|
-
const fullInput = resolvePath(filePath)
|
|
326
|
-
const fullOutput = resolvePath(outputPath)
|
|
327
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
328
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
329
|
-
const pdfDoc = await PDFDocument.load(fs.readFileSync(fullInput))
|
|
330
|
-
const n = count || 1
|
|
331
|
-
for (let i = 0; i < n; i++) pdfDoc.addPage()
|
|
332
|
-
fs.writeFileSync(fullOutput, await pdfDoc.save())
|
|
333
|
-
return ok({ filePath: fullInput, outputPath: fullOutput, addedPages: n, totalPages: pdfDoc.getPageCount() })
|
|
334
|
-
} catch (error) {
|
|
335
|
-
return fail(error, { filePath, outputPath })
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* 设置 PDF 文档元信息(标题、作者、主题、关键词)
|
|
341
|
-
*/
|
|
342
|
-
async function setPdfMetadata(filePath, outputPath, metadata) {
|
|
343
|
-
try {
|
|
344
|
-
const fullInput = resolvePath(filePath)
|
|
345
|
-
const fullOutput = resolvePath(outputPath)
|
|
346
|
-
if (!fs.existsSync(fullInput)) return fail(`File does not exist: ${fullInput}`, { filePath: fullInput })
|
|
347
|
-
fs.ensureDirSync(path.dirname(fullOutput))
|
|
348
|
-
const pdfDoc = await PDFDocument.load(fs.readFileSync(fullInput))
|
|
349
|
-
if (metadata.title !== undefined) pdfDoc.setTitle(metadata.title)
|
|
350
|
-
if (metadata.author !== undefined) pdfDoc.setAuthor(metadata.author)
|
|
351
|
-
if (metadata.subject !== undefined) pdfDoc.setSubject(metadata.subject)
|
|
352
|
-
if (metadata.keywords !== undefined) pdfDoc.setKeywords(Array.isArray(metadata.keywords) ? metadata.keywords : [metadata.keywords])
|
|
353
|
-
if (metadata.creator !== undefined) pdfDoc.setCreator(metadata.creator)
|
|
354
|
-
fs.writeFileSync(fullOutput, await pdfDoc.save())
|
|
355
|
-
return ok({ filePath: fullInput, outputPath: fullOutput, metadata })
|
|
356
|
-
} catch (error) {
|
|
357
|
-
return fail(error, { filePath, outputPath })
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* 统计 PDF 各页文字字符数(基于整体文本按页估算)
|
|
363
|
-
*/
|
|
364
|
-
async function getPdfTextStats(filePath) {
|
|
365
|
-
try {
|
|
366
|
-
const readResult = await readPdfText(filePath)
|
|
367
|
-
if (!readResult.success) return readResult
|
|
368
|
-
const { text, pageCount } = readResult.data
|
|
369
|
-
const totalChars = text.length
|
|
370
|
-
const totalWords = text.trim().split(/\s+/).filter(Boolean).length
|
|
371
|
-
const lines = text.split('\n').filter(Boolean)
|
|
372
|
-
return ok({
|
|
373
|
-
filePath: resolvePath(filePath),
|
|
374
|
-
pageCount,
|
|
375
|
-
totalChars,
|
|
376
|
-
totalWords,
|
|
377
|
-
totalLines: lines.length,
|
|
378
|
-
avgCharsPerPage: pageCount > 0 ? Math.round(totalChars / pageCount) : 0,
|
|
379
|
-
avgWordsPerPage: pageCount > 0 ? Math.round(totalWords / pageCount) : 0,
|
|
380
|
-
})
|
|
381
|
-
} catch (error) {
|
|
382
|
-
return fail(error, { filePath })
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
async function pdfToLongImage(pdfPath, outputImagePath) {
|
|
388
|
-
try {
|
|
389
|
-
pdfPath = resolvePath(pdfPath)
|
|
390
|
-
outputImagePath = resolvePath(outputImagePath)
|
|
391
|
-
if (!fs.existsSync(pdfPath)) return fail(`File does not exist: ${pdfPath}`, { pdfPath })
|
|
392
|
-
// 0. 确保输出目录存在
|
|
393
|
-
fs.ensureDirSync(path.dirname(outputImagePath));
|
|
394
|
-
// 1. 读取 PDF 文档,设置 scale 参数可提高图片清晰度
|
|
395
|
-
const document = await pdf(pdfPath, { scale: 2 });
|
|
396
|
-
const pages = [];
|
|
397
|
-
|
|
398
|
-
// 2. 遍历每一页,将图片 Buffer 收集起来
|
|
399
|
-
for await (const pageBuffer of document) {
|
|
400
|
-
pages.push(pageBuffer);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (pages.length === 0) {
|
|
404
|
-
return fail(`PDF has no pages`, { pdfPath })
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// 3. 获取所有页面的尺寸,计算最大宽度和总高度
|
|
408
|
-
let maxWidth = 0;
|
|
409
|
-
let totalHeight = 0;
|
|
410
|
-
const pageHeights = [];
|
|
411
|
-
|
|
412
|
-
for (const pageBuffer of pages) {
|
|
413
|
-
const metadata = await sharp(pageBuffer).metadata();
|
|
414
|
-
maxWidth = Math.max(maxWidth, metadata.width);
|
|
415
|
-
totalHeight += metadata.height;
|
|
416
|
-
pageHeights.push(metadata.height);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// 4. 构建 sharp 的输入:将图片 Buffer 数组叠加,并设置垂直偏移
|
|
420
|
-
const sharpInput = [];
|
|
421
|
-
let currentHeight = 0;
|
|
422
|
-
|
|
423
|
-
for (let i = 0; i < pages.length; i++) {
|
|
424
|
-
const pageBuffer = pages[i];
|
|
425
|
-
const pageHeight = pageHeights[i];
|
|
426
|
-
|
|
427
|
-
// 获取当前页面的实际宽度
|
|
428
|
-
const pageMetadata = await sharp(pageBuffer).metadata();
|
|
429
|
-
const pageWidth = pageMetadata.width;
|
|
430
|
-
|
|
431
|
-
// 计算水平居中的偏移量
|
|
432
|
-
const leftOffset = Math.floor((maxWidth - pageWidth) / 2);
|
|
433
|
-
|
|
434
|
-
sharpInput.push({
|
|
435
|
-
input: pageBuffer,
|
|
436
|
-
top: currentHeight, // 每页依次向下排列
|
|
437
|
-
left: leftOffset, // 水平居中
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
currentHeight += pageHeight;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// 5. 使用 sharp 合成长图
|
|
444
|
-
await sharp({
|
|
445
|
-
create: {
|
|
446
|
-
width: maxWidth,
|
|
447
|
-
height: totalHeight, // 总高度 = 所有页面高度之和
|
|
448
|
-
channels: 4, // RGBA
|
|
449
|
-
background: { r: 255, g: 255, b: 255, alpha: 1 }, // 白色背景
|
|
450
|
-
},
|
|
451
|
-
})
|
|
452
|
-
.composite(sharpInput)
|
|
453
|
-
.png() // 输出为 PNG 格式
|
|
454
|
-
.toFile(outputImagePath)
|
|
455
|
-
|
|
456
|
-
return ok({ pdfPath, outputImagePath, pageCount: pages.length, width: maxWidth, height: totalHeight })
|
|
457
|
-
} catch (error) {
|
|
458
|
-
console.error(error)
|
|
459
|
-
return fail(error, { pdfPath, outputImagePath })
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// ─── 工具描述 ─────────────────────────────────────────────────────────────────
|
|
464
|
-
|
|
465
|
-
const descriptions = [
|
|
466
|
-
{
|
|
467
|
-
type: 'function',
|
|
468
|
-
function: {
|
|
469
|
-
name: 'readPdfText',
|
|
470
|
-
description: '读取 PDF 文件并提取全部文本内容。参数:filePath 为 PDF 路径。返回值:对象,包含 success、data(含 filePath、text、pageCount、wordCount、charCount)、error。',
|
|
471
|
-
parameters: {
|
|
472
|
-
type: 'object',
|
|
473
|
-
properties: {
|
|
474
|
-
filePath: { type: 'string', description: 'PDF 文件路径。' },
|
|
475
|
-
},
|
|
476
|
-
required: ['filePath'],
|
|
477
|
-
},
|
|
478
|
-
},
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
type: 'function',
|
|
482
|
-
function: {
|
|
483
|
-
name: 'getPdfInfo',
|
|
484
|
-
description: '获取 PDF 文档元信息(页数、标题、作者、创建时间、文件大小等)。参数:filePath 为 PDF 路径。返回值:对象,包含 success、data(含 pageCount、size、title、author、subject、keywords、creator、producer、creationDate、modificationDate)、error。',
|
|
485
|
-
parameters: {
|
|
486
|
-
type: 'object',
|
|
487
|
-
properties: {
|
|
488
|
-
filePath: { type: 'string', description: 'PDF 文件路径。' },
|
|
489
|
-
},
|
|
490
|
-
required: ['filePath'],
|
|
491
|
-
},
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
type: 'function',
|
|
496
|
-
function: {
|
|
497
|
-
name: 'getPdfPageCount',
|
|
498
|
-
description: '快速获取 PDF 总页数。参数:filePath 为 PDF 路径。返回值:对象,包含 success、data(含 filePath、pageCount)、error。',
|
|
499
|
-
parameters: {
|
|
500
|
-
type: 'object',
|
|
501
|
-
properties: {
|
|
502
|
-
filePath: { type: 'string', description: 'PDF 文件路径。' },
|
|
503
|
-
},
|
|
504
|
-
required: ['filePath'],
|
|
505
|
-
},
|
|
506
|
-
},
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
type: 'function',
|
|
510
|
-
function: {
|
|
511
|
-
name: 'searchPdfText',
|
|
512
|
-
description: '在 PDF 内容中搜索关键词,返回所有包含该词的行。参数:filePath 为 PDF 路径;keyword 为搜索关键词。返回值:对象,包含 success、data(含 keyword、matchCount、matches 数组)、error。',
|
|
513
|
-
parameters: {
|
|
514
|
-
type: 'object',
|
|
515
|
-
properties: {
|
|
516
|
-
filePath: { type: 'string', description: 'PDF 文件路径。' },
|
|
517
|
-
keyword: { type: 'string', description: '搜索关键词。' },
|
|
518
|
-
},
|
|
519
|
-
required: ['filePath', 'keyword'],
|
|
520
|
-
},
|
|
521
|
-
},
|
|
522
|
-
},
|
|
523
|
-
{
|
|
524
|
-
type: 'function',
|
|
525
|
-
function: {
|
|
526
|
-
name: 'mergePdfs',
|
|
527
|
-
description: '将多个 PDF 文件合并为一个。参数:inputPaths 为 PDF 路径数组(按顺序合并);outputPath 为输出路径。返回值:对象,包含 success、data(含 outputPath、pageCount、inputCount)、error。',
|
|
528
|
-
parameters: {
|
|
529
|
-
type: 'object',
|
|
530
|
-
properties: {
|
|
531
|
-
inputPaths: { type: 'array', items: { type: 'string' }, description: '要合并的 PDF 文件路径数组。' },
|
|
532
|
-
outputPath: { type: 'string', description: '合并后输出的 PDF 文件路径。' },
|
|
533
|
-
},
|
|
534
|
-
required: ['inputPaths', 'outputPath'],
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
type: 'function',
|
|
540
|
-
function: {
|
|
541
|
-
name: 'splitPdf',
|
|
542
|
-
description: '将 PDF 按页拆分,每页生成独立 PDF 文件。参数:filePath 为源 PDF 路径;outputDir 为输出目录。返回值:对象,包含 success、data(含 outputDir、pageCount、files 数组)、error。',
|
|
543
|
-
parameters: {
|
|
544
|
-
type: 'object',
|
|
545
|
-
properties: {
|
|
546
|
-
filePath: { type: 'string', description: '要拆分的 PDF 文件路径。' },
|
|
547
|
-
outputDir: { type: 'string', description: '拆分后各页 PDF 的输出目录。' },
|
|
548
|
-
},
|
|
549
|
-
required: ['filePath', 'outputDir'],
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
},
|
|
553
|
-
{
|
|
554
|
-
type: 'function',
|
|
555
|
-
function: {
|
|
556
|
-
name: 'extractPdfPages',
|
|
557
|
-
description: '从 PDF 中提取指定页码(1-based)生成新 PDF。参数:filePath 为源 PDF 路径;outputPath 为输出路径;pages 为页码数组(如 [1, 3, 5])。返回值:对象,包含 success、data(含 outputPath、extractedPages)、error。',
|
|
558
|
-
parameters: {
|
|
559
|
-
type: 'object',
|
|
560
|
-
properties: {
|
|
561
|
-
filePath: { type: 'string', description: '源 PDF 文件路径。' },
|
|
562
|
-
outputPath: { type: 'string', description: '输出 PDF 文件路径。' },
|
|
563
|
-
pages: { type: 'array', items: { type: 'number' }, description: '要提取的页码数组(从 1 开始),如 [1, 2, 5]。' },
|
|
564
|
-
},
|
|
565
|
-
required: ['filePath', 'outputPath', 'pages'],
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
type: 'function',
|
|
571
|
-
function: {
|
|
572
|
-
name: 'rotatePdfPages',
|
|
573
|
-
description: '旋转 PDF 页面。参数:filePath 为源 PDF 路径;outputPath 为输出路径;rotateDegrees 为旋转角度(90/180/270,默认 90);pageNumbers 为要旋转的页码数组(1-based,省略则旋转全部页)。返回值:对象,包含 success、data(含 outputPath、degrees、rotatedPages)、error。',
|
|
574
|
-
parameters: {
|
|
575
|
-
type: 'object',
|
|
576
|
-
properties: {
|
|
577
|
-
filePath: { type: 'string', description: '源 PDF 文件路径。' },
|
|
578
|
-
outputPath: { type: 'string', description: '输出 PDF 文件路径。' },
|
|
579
|
-
rotateDegrees: { type: 'number', description: '旋转角度(90/180/270),默认 90。' },
|
|
580
|
-
pageNumbers: { type: 'array', items: { type: 'number' }, description: '要旋转的页码数组(从 1 开始),省略则旋转全部页。' },
|
|
581
|
-
},
|
|
582
|
-
required: ['filePath', 'outputPath'],
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
type: 'function',
|
|
588
|
-
function: {
|
|
589
|
-
name: 'addWatermarkText',
|
|
590
|
-
description: '向 PDF 全部页面添加文字水印。参数:filePath 为源 PDF 路径;outputPath 为输出路径;text 为水印文字;options 可选(fontSize 默认 48,opacity 默认 0.2,angle 默认 45,color: {r,g,b})。返回值:对象,包含 success、data(含 outputPath、text、pageCount)、error。',
|
|
591
|
-
parameters: {
|
|
592
|
-
type: 'object',
|
|
593
|
-
properties: {
|
|
594
|
-
filePath: { type: 'string', description: '源 PDF 文件路径。' },
|
|
595
|
-
outputPath: { type: 'string', description: '输出 PDF 文件路径。' },
|
|
596
|
-
text: { type: 'string', description: '水印文字内容。' },
|
|
597
|
-
options: { type: 'object', description: '可选:{ fontSize, opacity, angle, color: {r,g,b} }。' },
|
|
598
|
-
},
|
|
599
|
-
required: ['filePath', 'outputPath', 'text'],
|
|
600
|
-
},
|
|
601
|
-
},
|
|
602
|
-
},
|
|
603
|
-
{
|
|
604
|
-
type: 'function',
|
|
605
|
-
function: {
|
|
606
|
-
name: 'createPdf',
|
|
607
|
-
description: `创建新的 PDF 文档,支持多种内容类型。
|
|
608
|
-
参数:outputPath 为输出路径;content 为内容数组,每项格式:
|
|
609
|
-
- 文本:{ type:'text', text, fontSize, bold, color, align }
|
|
610
|
-
- 标题:{ type:'heading', text, fontSize, color, align }
|
|
611
|
-
- 分页:{ type:'pageBreak' }
|
|
612
|
-
options 可选:{ pageSize: A4/A3/Letter, margin, title, author, subject }。
|
|
613
|
-
返回值:对象,包含 success、data(含 outputPath、itemCount)、error。`,
|
|
614
|
-
parameters: {
|
|
615
|
-
type: 'object',
|
|
616
|
-
properties: {
|
|
617
|
-
outputPath: { type: 'string', description: '输出 PDF 文件路径。' },
|
|
618
|
-
content: { type: 'array', description: '内容块数组,支持 text/heading/pageBreak 类型。', items: { type: 'object' } },
|
|
619
|
-
options: { type: 'object', description: '可选:{ pageSize, margin, title, author, subject }。' },
|
|
620
|
-
},
|
|
621
|
-
required: ['outputPath'],
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
},
|
|
625
|
-
{
|
|
626
|
-
type: 'function',
|
|
627
|
-
function: {
|
|
628
|
-
name: 'textToPdf',
|
|
629
|
-
description: '将纯文本文件(.txt)转换为 PDF 文档。参数:inputPath 为 .txt 文件路径;outputPath 为输出 PDF 路径;options 可选(同 createPdf options)。返回值:对象,包含 success、data(含 outputPath)、error。',
|
|
630
|
-
parameters: {
|
|
631
|
-
type: 'object',
|
|
632
|
-
properties: {
|
|
633
|
-
inputPath: { type: 'string', description: '源 .txt 文件路径。' },
|
|
634
|
-
outputPath: { type: 'string', description: '输出 PDF 文件路径。' },
|
|
635
|
-
options: { type: 'object', description: '可选:{ pageSize, margin, title, author }。' },
|
|
636
|
-
},
|
|
637
|
-
required: ['inputPath', 'outputPath'],
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
},
|
|
641
|
-
{
|
|
642
|
-
type: 'function',
|
|
643
|
-
function: {
|
|
644
|
-
name: 'appendBlankPages',
|
|
645
|
-
description: '在 PDF 末尾追加指定数量的空白页。参数:filePath 为源 PDF 路径;outputPath 为输出路径;count 为追加页数(默认 1)。返回值:对象,包含 success、data(含 outputPath、addedPages、totalPages)、error。',
|
|
646
|
-
parameters: {
|
|
647
|
-
type: 'object',
|
|
648
|
-
properties: {
|
|
649
|
-
filePath: { type: 'string', description: '源 PDF 文件路径。' },
|
|
650
|
-
outputPath: { type: 'string', description: '输出 PDF 文件路径。' },
|
|
651
|
-
count: { type: 'number', description: '要追加的空白页数量,默认 1。' },
|
|
652
|
-
},
|
|
653
|
-
required: ['filePath', 'outputPath'],
|
|
654
|
-
},
|
|
655
|
-
},
|
|
656
|
-
},
|
|
657
|
-
{
|
|
658
|
-
type: 'function',
|
|
659
|
-
function: {
|
|
660
|
-
name: 'setPdfMetadata',
|
|
661
|
-
description: '设置 PDF 文档的元信息(标题、作者、主题、关键词)。参数:filePath 为源 PDF 路径;outputPath 为输出路径;metadata 为元信息对象(含 title/author/subject/keywords/creator 字段)。返回值:对象,包含 success、data(含 outputPath、metadata)、error。',
|
|
662
|
-
parameters: {
|
|
663
|
-
type: 'object',
|
|
664
|
-
properties: {
|
|
665
|
-
filePath: { type: 'string', description: '源 PDF 文件路径。' },
|
|
666
|
-
outputPath: { type: 'string', description: '输出 PDF 文件路径。' },
|
|
667
|
-
metadata: {
|
|
668
|
-
type: 'object',
|
|
669
|
-
description: '元信息对象,如 { title, author, subject, keywords, creator }。',
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
required: ['filePath', 'outputPath', 'metadata'],
|
|
673
|
-
},
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
|
-
{
|
|
677
|
-
type: 'function',
|
|
678
|
-
function: {
|
|
679
|
-
name: 'getPdfTextStats',
|
|
680
|
-
description: '统计 PDF 文档的文字数量(总字数、总字符数、总行数及每页平均值)。参数:filePath 为 PDF 路径。返回值:对象,包含 success、data(含 pageCount、totalChars、totalWords、totalLines、avgCharsPerPage、avgWordsPerPage)、error。',
|
|
681
|
-
parameters: {
|
|
682
|
-
type: 'object',
|
|
683
|
-
properties: {
|
|
684
|
-
filePath: { type: 'string', description: 'PDF 文件路径。' },
|
|
685
|
-
},
|
|
686
|
-
required: ['filePath'],
|
|
687
|
-
},
|
|
688
|
-
},
|
|
689
|
-
},
|
|
690
|
-
{
|
|
691
|
-
type: 'function',
|
|
692
|
-
function: {
|
|
693
|
-
name: 'pdfToLongImage',
|
|
694
|
-
description: '将 PDF 所有页面依次拼接为一张竖向长图(PNG 格式)。参数:pdfPath 为 PDF 文件路径;outputImagePath 为输出 PNG 图片路径。返回值:对象,包含 success、data(含 pdfPath、outputImagePath、pageCount、width、height)、error。',
|
|
695
|
-
parameters: {
|
|
696
|
-
type: 'object',
|
|
697
|
-
properties: {
|
|
698
|
-
pdfPath: { type: 'string', description: '源 PDF 文件路径。' },
|
|
699
|
-
outputImagePath: { type: 'string', description: '输出长图的 PNG 文件路径。' },
|
|
700
|
-
},
|
|
701
|
-
required: ['pdfPath', 'outputImagePath'],
|
|
702
|
-
},
|
|
703
|
-
},
|
|
704
|
-
},
|
|
705
|
-
]
|
|
706
|
-
|
|
707
|
-
// ─── 导出 ──────────────────────────────────────────────────────────────────────
|
|
708
|
-
|
|
709
|
-
const functions = {
|
|
710
|
-
readPdfText,
|
|
711
|
-
getPdfInfo,
|
|
712
|
-
getPdfPageCount,
|
|
713
|
-
searchPdfText,
|
|
714
|
-
mergePdfs,
|
|
715
|
-
splitPdf,
|
|
716
|
-
extractPdfPages,
|
|
717
|
-
rotatePdfPages,
|
|
718
|
-
addWatermarkText,
|
|
719
|
-
createPdf,
|
|
720
|
-
textToPdf,
|
|
721
|
-
appendBlankPages,
|
|
722
|
-
setPdfMetadata,
|
|
723
|
-
getPdfTextStats,
|
|
724
|
-
pdfToLongImage,
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const PdfTool = {
|
|
728
|
-
name: 'PdfTool',
|
|
729
|
-
description: '提供 PDF 文档的文本提取、搜索、元信息读写、合并、拆分、提取页面、旋转、添加水印、创建、文本转 PDF、PDF 转长图等全面处理能力',
|
|
730
|
-
platform: 'all',
|
|
731
|
-
descriptions,
|
|
732
|
-
functions,
|
|
733
|
-
isSystem: true
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
module.exports = PdfTool
|