diff --git a/plugins/pdf-office-workbench/CHANGELOG.md b/plugins/pdf-office-workbench/CHANGELOG.md
new file mode 100644
index 00000000..d92c66b9
--- /dev/null
+++ b/plugins/pdf-office-workbench/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+## v0.2.0
+
+- 接入真实 PDF 文件读写:选择、合并、按页拆分和输出结果。
+- 新增批量 PDF 重命名,支持 `{name}` 与 `{index}` 模板。
+- 支持从 ZTools 超级面板接收 PDF 文件,并可打开结果所在文件夹。
+- 接入 `pdf-lib` 作为本地处理依赖,不上传用户文件。
diff --git a/plugins/pdf-office-workbench/README.md b/plugins/pdf-office-workbench/README.md
new file mode 100644
index 00000000..a95ffcb3
--- /dev/null
+++ b/plugins/pdf-office-workbench/README.md
@@ -0,0 +1,22 @@
+# PDF 办公工坊
+
+把 PDF 合并拆分、票据字段提取、文档批量重命名和批次汇总组合成一个办公插件。当前版本提供真实的本地 PDF 文件处理,不上传文件。
+
+当前范围聚焦 PDF 与票据流程;Word、Excel、PPT 的原生编辑/转换仍属于后续能力,不在本版本中宣称支持。
+
+## 首批能力
+
+- 选择多个 PDF 并真实合并为一个 PDF。
+- 按页码表达式(如 `1-3,5`)真实拆分 PDF。
+- 使用 `{name}` / `{index}` 模板批量重命名 PDF 文件。
+- 从 ZTools 超级面板接收 PDF 文件。
+- 从 OCR 文本中提取发票字段。
+- 展示批次文档数、页数和体积。
+
+## 验证
+
+```bash
+npm test
+```
+
+插件运行时需要 ZTools 注入的文件对话框和 preload 能力;PDF 内容在本机通过 `pdf-lib` 处理。
diff --git a/plugins/pdf-office-workbench/index.html b/plugins/pdf-office-workbench/index.html
new file mode 100644
index 00000000..b6b26cb7
--- /dev/null
+++ b/plugins/pdf-office-workbench/index.html
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+ PDF 办公工坊
+
+
+
+
+
+
+
PDF WORKBENCH
+
PDF 办公工坊
+
在本机完成合并、拆分、批量重命名和票据字段整理。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/pdf-office-workbench/logo.svg b/plugins/pdf-office-workbench/logo.svg
new file mode 100644
index 00000000..e6484388
--- /dev/null
+++ b/plugins/pdf-office-workbench/logo.svg
@@ -0,0 +1,6 @@
+
diff --git a/plugins/pdf-office-workbench/package-lock.json b/plugins/pdf-office-workbench/package-lock.json
new file mode 100644
index 00000000..837f3b7d
--- /dev/null
+++ b/plugins/pdf-office-workbench/package-lock.json
@@ -0,0 +1,15 @@
+{
+ "name": "ztools-pdf-office-workbench",
+ "version": "0.2.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ztools-pdf-office-workbench",
+ "version": "0.2.0",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ }
+ }
+}
diff --git a/plugins/pdf-office-workbench/package.json b/plugins/pdf-office-workbench/package.json
new file mode 100644
index 00000000..f279ea7c
--- /dev/null
+++ b/plugins/pdf-office-workbench/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "ztools-pdf-office-workbench",
+ "version": "0.2.0",
+ "private": true,
+ "type": "module",
+ "description": "ZTools PDF and office document workflow plugin.",
+ "scripts": {
+ "test": "node --test tests/*.test.mjs"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/plugins/pdf-office-workbench/plugin.json b/plugins/pdf-office-workbench/plugin.json
new file mode 100644
index 00000000..795ddccf
--- /dev/null
+++ b/plugins/pdf-office-workbench/plugin.json
@@ -0,0 +1,36 @@
+{
+ "name": "pdf-office-workbench",
+ "title": "PDF 办公工坊",
+ "description": "本地 PDF 和票据处理工作台:真实合并拆分、批量重命名、票据字段提取和批次汇总。",
+ "author": "harris",
+ "version": "0.2.0",
+ "main": "index.html",
+ "preload": "preload/services.js",
+ "logo": "logo.svg",
+ "categories": ["productivity", "text"],
+ "pluginSetting": {
+ "single": true,
+ "height": 720
+ },
+ "features": [
+ {
+ "code": "pdf-office-workbench",
+ "explain": "PDF 办公工坊",
+ "icon": "logo.svg",
+ "cmds": [
+ "PDF 办公工坊",
+ "PDF 合并",
+ "PDF 拆分",
+ "发票提取",
+ "文档重命名",
+ {
+ "type": "files",
+ "fileType": "file",
+ "label": "处理 PDF",
+ "extensions": ["pdf"],
+ "maxLength": 300
+ }
+ ]
+ }
+ ]
+}
diff --git a/plugins/pdf-office-workbench/preload/package-lock.json b/plugins/pdf-office-workbench/preload/package-lock.json
new file mode 100644
index 00000000..a8ecf243
--- /dev/null
+++ b/plugins/pdf-office-workbench/preload/package-lock.json
@@ -0,0 +1,54 @@
+{
+ "name": "preload",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "pdf-lib": "1.17.1"
+ }
+ },
+ "node_modules/@pdf-lib/standard-fonts": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
+ "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^1.0.6"
+ }
+ },
+ "node_modules/@pdf-lib/upng": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/@pdf-lib/upng/-/upng-1.0.1.tgz",
+ "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^1.0.10"
+ }
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/pdf-lib": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmmirror.com/pdf-lib/-/pdf-lib-1.17.1.tgz",
+ "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@pdf-lib/standard-fonts": "^1.0.0",
+ "@pdf-lib/upng": "^1.0.1",
+ "pako": "^1.0.11",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ }
+ }
+}
diff --git a/plugins/pdf-office-workbench/preload/package.json b/plugins/pdf-office-workbench/preload/package.json
new file mode 100644
index 00000000..1f3abd15
--- /dev/null
+++ b/plugins/pdf-office-workbench/preload/package.json
@@ -0,0 +1,7 @@
+{
+ "type": "commonjs",
+ "private": true,
+ "dependencies": {
+ "pdf-lib": "1.17.1"
+ }
+}
diff --git a/plugins/pdf-office-workbench/preload/services.js b/plugins/pdf-office-workbench/preload/services.js
new file mode 100644
index 00000000..cc9d0502
--- /dev/null
+++ b/plugins/pdf-office-workbench/preload/services.js
@@ -0,0 +1,219 @@
+const fs = require('node:fs')
+const os = require('node:os')
+const path = require('node:path')
+const { PDFDocument } = require('pdf-lib')
+
+let shell
+try {
+ shell = require('electron').shell
+} catch (_) {
+ shell = null
+}
+
+function hostPath(name, fallback) {
+ try {
+ const value = window.ztools?.getPath?.(name)
+ if (value) return value
+ } catch (_) {
+ // Standalone previews do not inject window.ztools.
+ }
+ return fallback
+}
+
+function downloadsDirectory() {
+ return hostPath('downloads', path.join(os.homedir(), 'Downloads'))
+}
+
+function normalizePaths(value) {
+ if (typeof value === 'string') return value ? [value] : []
+ if (!Array.isArray(value)) return []
+ return value
+ .flatMap(item => {
+ if (typeof item === 'string') return item
+ if (!item || typeof item !== 'object') return []
+ return item.path || item.filePath || item.url || []
+ })
+ .filter(item => typeof item === 'string' && item.length > 0)
+}
+
+function basenameWithoutExtension(filePath) {
+ const name = path.basename(String(filePath))
+ const extension = path.extname(name)
+ return extension ? name.slice(0, -extension.length) : name
+}
+
+function sanitizeFileName(value) {
+ return String(value || '未命名')
+ .trim()
+ .replace(/[\\/:*?"<>|]/g, '-')
+ .replace(/[. ]+$/g, '') || '未命名'
+}
+
+function parsePageRanges(expression) {
+ const ranges = String(expression ?? '')
+ .split(',')
+ .map(part => part.trim())
+ .filter(Boolean)
+ .map(part => {
+ const match = /^(\d+)(?:\s*-\s*(\d+))?$/.exec(part)
+ if (!match) throw new Error(`无效页码范围:${part}`)
+ const start = Number(match[1])
+ const end = Number(match[2] || match[1])
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) {
+ throw new Error(`无效页码范围:${part}`)
+ }
+ return [start, end]
+ })
+ if (ranges.length === 0) throw new Error('请输入至少一个页码范围')
+ return ranges
+}
+
+function ensurePdfPath(filePath) {
+ const resolved = path.resolve(String(filePath || ''))
+ if (!resolved.toLowerCase().endsWith('.pdf')) throw new Error(`仅支持 PDF 文件:${resolved}`)
+ if (!fs.existsSync(resolved)) throw new Error(`文件不存在:${resolved}`)
+ return resolved
+}
+
+async function readPdfInfo(filePath) {
+ const resolved = ensurePdfPath(filePath)
+ const bytes = fs.readFileSync(resolved)
+ const document = await PDFDocument.load(bytes, { ignoreEncryption: true })
+ return {
+ path: resolved,
+ name: path.basename(resolved),
+ pages: document.getPageCount(),
+ size: bytes.length
+ }
+}
+
+async function chooseFiles() {
+ const selected = await Promise.resolve(window.ztools?.showOpenDialog?.({
+ properties: ['openFile', 'multiSelections'],
+ filters: [{ name: 'PDF 文件', extensions: ['pdf'] }]
+ }))
+ const paths = normalizePaths(selected)
+ const results = []
+ for (const filePath of paths) {
+ try {
+ results.push(await readPdfInfo(filePath))
+ } catch (error) {
+ window.ztools?.showNotification?.(error instanceof Error ? error.message : String(error))
+ }
+ }
+ return results
+}
+
+async function chooseSavePath(defaultPath) {
+ return await Promise.resolve(window.ztools?.showSaveDialog?.({
+ defaultPath,
+ filters: [{ name: 'PDF 文件', extensions: ['pdf'] }]
+ }))
+}
+
+async function mergePdfs(paths, outputPath) {
+ const sources = normalizePaths(paths).map(ensurePdfPath)
+ if (sources.length < 2) throw new Error('至少选择两个 PDF 文件才能合并')
+ const output = path.resolve(String(outputPath || ''))
+ if (!output.toLowerCase().endsWith('.pdf')) throw new Error('合并输出文件必须是 .pdf')
+ if (sources.includes(output)) throw new Error('输出文件不能覆盖输入文件')
+
+ const merged = await PDFDocument.create()
+ let totalPages = 0
+ for (const source of sources) {
+ const document = await PDFDocument.load(fs.readFileSync(source), { ignoreEncryption: true })
+ const pages = await merged.copyPages(document, document.getPageIndices())
+ pages.forEach(page => merged.addPage(page))
+ totalPages += pages.length
+ }
+ fs.mkdirSync(path.dirname(output), { recursive: true })
+ fs.writeFileSync(output, await merged.save())
+ return { path: output, pages: totalPages, size: fs.statSync(output).size }
+}
+
+async function splitPdf(sourcePath, expression, outputDirectory) {
+ const source = ensurePdfPath(sourcePath)
+ const ranges = parsePageRanges(expression)
+ const sourceDocument = await PDFDocument.load(fs.readFileSync(source), { ignoreEncryption: true })
+ const pageCount = sourceDocument.getPageCount()
+ const outputDir = path.resolve(String(outputDirectory || path.join(downloadsDirectory(), 'PDF 办公工坊')))
+ fs.mkdirSync(outputDir, { recursive: true })
+ const base = basenameWithoutExtension(source)
+ const outputs = []
+
+ for (const [start, end] of ranges) {
+ if (end > pageCount) throw new Error(`页码 ${start}-${end} 超出文档总页数 ${pageCount}`)
+ const document = await PDFDocument.create()
+ const pages = await document.copyPages(
+ sourceDocument,
+ Array.from({ length: end - start + 1 }, (_, offset) => start - 1 + offset)
+ )
+ pages.forEach(page => document.addPage(page))
+ const suffix = start === end ? `p${start}` : `p${start}-${end}`
+ const output = path.join(outputDir, `${sanitizeFileName(base)}-${suffix}.pdf`)
+ fs.writeFileSync(output, await document.save())
+ outputs.push({ path: output, pages: pages.length, size: fs.statSync(output).size })
+ }
+ return outputs
+}
+
+async function renameFiles(paths, template) {
+ const sources = normalizePaths(paths).map(item => path.resolve(item))
+ if (!sources.length) throw new Error('请先选择需要重命名的 PDF 文件')
+ const pattern = String(template || '{name}-{index}')
+ const operations = sources.map((source, index) => {
+ const name = basenameWithoutExtension(source)
+ const targetName = sanitizeFileName(pattern
+ .replace(/\{name\}/g, name)
+ .replace(/\{index\}/g, String(index + 1)))
+ return { source, target: path.join(path.dirname(source), `${targetName}.pdf`) }
+ })
+ const targets = new Set()
+ for (const operation of operations) {
+ if (targets.has(operation.target)) throw new Error(`重命名目标重复:${operation.target}`)
+ targets.add(operation.target)
+ if (operation.target !== operation.source && fs.existsSync(operation.target) && !sources.includes(operation.target)) {
+ throw new Error(`目标文件已存在:${operation.target}`)
+ }
+ }
+ for (const operation of operations) {
+ if (operation.source !== operation.target) fs.renameSync(operation.source, operation.target)
+ }
+ return operations.map(operation => ({ path: operation.target, name: path.basename(operation.target) }))
+}
+
+const services = {
+ chooseFiles,
+ chooseSavePath,
+ readPdfInfo,
+ mergePdfs,
+ splitPdf,
+ renameFiles,
+ getDefaultOutputDirectory() {
+ return path.join(downloadsDirectory(), 'PDF 办公工坊')
+ },
+ reveal(filePath) {
+ if (shell?.showItemInFolder) shell.showItemInFolder(filePath)
+ },
+ async handlePluginEnter(action) {
+ const paths = normalizePaths(action?.payload)
+ if (!paths.length) return
+ const files = []
+ for (const filePath of paths) {
+ try {
+ files.push(await readPdfInfo(filePath))
+ } catch (_) {
+ // Ignore non-PDF launch payloads and let the user choose files manually.
+ }
+ }
+ if (files.length) window.dispatchEvent(new CustomEvent('pdf-office-enter', { detail: { files, action } }))
+ }
+}
+
+window.services = services
+
+window.ztools?.onPluginEnter?.(action => {
+ services.handlePluginEnter(action).catch(error => {
+ window.ztools?.showNotification?.(error instanceof Error ? error.message : String(error))
+ })
+})
diff --git a/plugins/pdf-office-workbench/src/core/index.mjs b/plugins/pdf-office-workbench/src/core/index.mjs
new file mode 100644
index 00000000..df60d7eb
--- /dev/null
+++ b/plugins/pdf-office-workbench/src/core/index.mjs
@@ -0,0 +1,87 @@
+export function buildPdfMergePlan(files, options = {}) {
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
+ return {
+ outputName: options.outputName || 'merged.pdf',
+ totalPages: sorted.reduce((sum, file) => sum + (file.pages || 0), 0),
+ files: sorted.map(file => file.path)
+ };
+}
+
+export function buildPdfSplitPlan(source, expression) {
+ const base = basename(source);
+ return parsePageRanges(expression).map(([start, end]) => {
+ return {
+ source,
+ range: [start, end],
+ targetName: `${base}-p${start}${end === start ? '' : `-${end}`}.pdf`
+ };
+ });
+}
+
+/** Parse a user-facing page expression such as "1-3,5,8-9". */
+export function parsePageRanges(expression) {
+ const ranges = String(expression ?? '')
+ .split(',')
+ .map(part => part.trim())
+ .filter(Boolean)
+ .map(part => {
+ const match = /^(\d+)(?:\s*-\s*(\d+))?$/.exec(part);
+ if (!match) throw new Error(`无效页码范围:${part}`);
+ const start = Number(match[1]);
+ const end = Number(match[2] || match[1]);
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) {
+ throw new Error(`无效页码范围:${part}`);
+ }
+ return [start, end];
+ });
+
+ if (ranges.length === 0) throw new Error('请输入至少一个页码范围');
+ return ranges;
+}
+
+export function extractInvoiceFields(text) {
+ const source = String(text);
+ return {
+ invoiceNumber: pick(source, /发票号码[::\s]*([A-Za-z0-9-]+)/),
+ date: pick(source, /开票日期[::\s]*(\d{4}-\d{2}-\d{2})/),
+ amount: pick(source, /(?:金额|价税合计)[^\d]*(\d+(?:\.\d{1,2})?)/),
+ buyer: pick(source, /购买方[::\s]*([^\n\r]+)/)
+ };
+}
+
+export function buildDocumentRenamePlan(documents, template) {
+ return documents.map((document, index) => {
+ const fields = document.fields || {};
+ const hasFields = Object.keys(fields).length > 0;
+ const targetName = hasFields
+ ? template.replace(/\{(\w+)\}/g, (_, key) => sanitize(fields[key] || '未命名'))
+ : `未命名-${index + 1}`;
+ return {
+ source: document.path,
+ targetName: `${targetName}.pdf`
+ };
+ });
+}
+
+export function summarizePdfBatch(files) {
+ return {
+ documents: files.length,
+ pages: files.reduce((sum, file) => sum + (file.pages || 0), 0),
+ size: files.reduce((sum, file) => sum + (file.size || 0), 0)
+ };
+}
+
+function pick(text, pattern) {
+ const match = text.match(pattern);
+ return match ? match[1].trim() : '';
+}
+
+function sanitize(value) {
+ return String(value).trim().replace(/[\\/:*?"<>|]/g, '-');
+}
+
+function basename(filePath) {
+ const name = String(filePath).split(/[\\/]/).pop() || '';
+ const dotIndex = name.lastIndexOf('.');
+ return dotIndex > 0 ? name.slice(0, dotIndex) : name;
+}
diff --git a/plugins/pdf-office-workbench/tests/core.test.mjs b/plugins/pdf-office-workbench/tests/core.test.mjs
new file mode 100644
index 00000000..909d367c
--- /dev/null
+++ b/plugins/pdf-office-workbench/tests/core.test.mjs
@@ -0,0 +1,63 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+
+import {
+ buildDocumentRenamePlan,
+ buildPdfMergePlan,
+ buildPdfSplitPlan,
+ extractInvoiceFields,
+ parsePageRanges,
+ summarizePdfBatch
+} from '../src/core/index.mjs';
+
+test('buildPdfMergePlan orders files and names output', () => {
+ const plan = buildPdfMergePlan([
+ { path: 'b.pdf', pages: 2 },
+ { path: 'a.pdf', pages: 3 }
+ ], { outputName: 'merged.pdf' });
+
+ assert.deepEqual(plan, { outputName: 'merged.pdf', totalPages: 5, files: ['a.pdf', 'b.pdf'] });
+});
+
+test('buildPdfSplitPlan creates page range tasks', () => {
+ assert.deepEqual(buildPdfSplitPlan('report.pdf', '1-2,4,6-7'), [
+ { source: 'report.pdf', range: [1, 2], targetName: 'report-p1-2.pdf' },
+ { source: 'report.pdf', range: [4, 4], targetName: 'report-p4.pdf' },
+ { source: 'report.pdf', range: [6, 7], targetName: 'report-p6-7.pdf' }
+ ]);
+});
+
+test('parsePageRanges rejects malformed and reversed ranges', () => {
+ assert.deepEqual(parsePageRanges('1-3, 5'), [[1, 3], [5, 5]]);
+ assert.throws(() => parsePageRanges('0-2'), /无效页码范围/);
+ assert.throws(() => parsePageRanges('4-2'), /无效页码范围/);
+ assert.throws(() => parsePageRanges(''), /至少一个页码范围/);
+});
+
+test('extractInvoiceFields reads common invoice text fields', () => {
+ const fields = extractInvoiceFields('发票号码: 12345678\n开票日期: 2026-06-01\n金额 ¥88.50\n购买方: ZTools');
+
+ assert.deepEqual(fields, {
+ invoiceNumber: '12345678',
+ date: '2026-06-01',
+ amount: '88.50',
+ buyer: 'ZTools'
+ });
+});
+
+test('buildDocumentRenamePlan uses extracted fields with fallback index', () => {
+ const plan = buildDocumentRenamePlan([
+ { path: '/tmp/a.pdf', fields: { date: '2026-06-01', buyer: 'ZTools', amount: '88.50' } },
+ { path: '/tmp/b.pdf', fields: {} }
+ ], '{date}-{buyer}-{amount}');
+
+ assert.deepEqual(plan.map(item => item.targetName), ['2026-06-01-ZTools-88.50.pdf', '未命名-2.pdf']);
+});
+
+test('summarizePdfBatch totals documents and pages', () => {
+ assert.deepEqual(summarizePdfBatch([{ pages: 2, size: 1024 }, { pages: 3, size: 2048 }]), {
+ documents: 2,
+ pages: 5,
+ size: 3072
+ });
+});
diff --git a/plugins/pdf-office-workbench/tests/file-operations.test.mjs b/plugins/pdf-office-workbench/tests/file-operations.test.mjs
new file mode 100644
index 00000000..16f85103
--- /dev/null
+++ b/plugins/pdf-office-workbench/tests/file-operations.test.mjs
@@ -0,0 +1,49 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs/promises';
+import os from 'node:os';
+import path from 'node:path';
+import { createRequire } from 'node:module';
+const require = createRequire(import.meta.url);
+const { PDFDocument } = require('../preload/node_modules/pdf-lib');
+
+async function makePdf(filePath, pageCount) {
+ const document = await PDFDocument.create();
+ for (let index = 0; index < pageCount; index += 1) {
+ const page = document.addPage([240, 160]);
+ page.drawText(`page ${index + 1}`);
+ }
+ await fs.writeFile(filePath, await document.save());
+}
+
+test('preload services merge, split and rename real PDF files', async () => {
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pdf-office-workbench-'));
+ const previousWindow = globalThis.window;
+ globalThis.window = { ztools: {} };
+ try {
+ require('../preload/services.js');
+ const serviceModule = globalThis.window.services;
+ const first = path.join(root, 'first.pdf');
+ const second = path.join(root, 'second.pdf');
+ await makePdf(first, 2);
+ await makePdf(second, 1);
+
+ const firstInfo = await serviceModule.readPdfInfo(first);
+ assert.equal(firstInfo.pages, 2);
+
+ const merged = await serviceModule.mergePdfs([first, second], path.join(root, 'merged.pdf'));
+ assert.equal(merged.pages, 3);
+ assert.equal((await serviceModule.readPdfInfo(merged.path)).pages, 3);
+
+ const split = await serviceModule.splitPdf(merged.path, '1-2,3', root);
+ assert.deepEqual(split.map(item => item.pages), [2, 1]);
+ assert.equal((await serviceModule.readPdfInfo(split[0].path)).pages, 2);
+
+ const renamed = await serviceModule.renameFiles([split[0].path, split[1].path], 'invoice-{index}');
+ assert.deepEqual(renamed.map(item => item.name), ['invoice-1.pdf', 'invoice-2.pdf']);
+ assert.equal((await serviceModule.readPdfInfo(renamed[1].path)).pages, 1);
+ } finally {
+ globalThis.window = previousWindow;
+ await fs.rm(root, { recursive: true, force: true });
+ }
+});