page2pdf_server 1.0.0
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/.babelrc +3 -0
- package/.eslintrc +33 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/ISSUE_TEMPLATE/refactoring.md +15 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
- package/.github/stale.yml +17 -0
- package/.github/workflows/cd.yml +75 -0
- package/.github/workflows/ci.yml +36 -0
- package/.husky/pre-commit +6 -0
- package/.husky/pre-push +4 -0
- package/.idea/codeStyles/Project.xml +58 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/encodings.xml +7 -0
- package/.idea/inspectionProfiles/Project_Default.xml +7 -0
- package/.idea/modules.xml +8 -0
- package/.idea/page2pdf-server.iml +12 -0
- package/.idea/tenstack-starter-main.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierrc +8 -0
- package/.vscode/settings.json +3 -0
- package/LICENSE +18 -0
- package/README.md +238 -0
- package/__tests__/UrltoPdf/generatePdf.test.ts +207 -0
- package/__tests__/UrltoPdf/pdfSplit.test.ts +69 -0
- package/__tests__/helpers/index.ts +21 -0
- package/__tests__/home.test.ts +77 -0
- package/config/default.json +10 -0
- package/config/development.json +3 -0
- package/config/production.json +3 -0
- package/config/test.json +3 -0
- package/ecosystem.config.js +41 -0
- package/eslintrc.json +14 -0
- package/jest.config.js +35 -0
- package/nodemon.json +6 -0
- package/package.json +105 -0
- package/src/CSS/345/205/274/345/256/271/346/200/247.txt +56 -0
- package/src/app.ts +41 -0
- package/src/components/home/controller.ts +27 -0
- package/src/components/home/index.ts +4 -0
- package/src/components/home/pdfController.ts +112 -0
- package/src/components/home/services.ts +31 -0
- package/src/components/home/splitController.ts +124 -0
- package/src/components/home/validators.ts +12 -0
- package/src/configEnv/index.ts +62 -0
- package/src/db/home.ts +14 -0
- package/src/helpers/apiResponse.ts +10 -0
- package/src/helpers/dataSanitizers.ts +31 -0
- package/src/helpers/error/ApiError.ts +25 -0
- package/src/helpers/error/ForbiddenError.ts +15 -0
- package/src/helpers/error/NotFoundException.ts +15 -0
- package/src/helpers/error/TimeOutError.ts +20 -0
- package/src/helpers/error/UnauthorizedError.ts +15 -0
- package/src/helpers/error/ValidationError.ts +20 -0
- package/src/helpers/error/index.ts +15 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/loggers.ts +73 -0
- package/src/index.ts +13 -0
- package/src/middlewares/errorHandler.ts +52 -0
- package/src/new_tab1.mhtml +722 -0
- package/src/routes/index.ts +22 -0
- package/src/server.ts +30 -0
- package/src/testCSS.html +241 -0
- package/src/types/global.d.ts +13 -0
- package/src/types/request/home.ts +166 -0
- package/src/types/request/split.ts +18 -0
- package/src/types/response/AppInformation.ts +9 -0
- package/src/types/response/index.ts +5 -0
- package/src/utils/array.ts +19 -0
- package/src/utils/auth.ts +12 -0
- package/src/utils/crypt.ts +26 -0
- package/src/utils/filter.ts +59 -0
- package/src/utils/object.ts +58 -0
- package/src/utils/pdfgen.ts +998 -0
- package/src/utils/url.ts +54 -0
- package/src//346/265/213/350/257/225.txt +241 -0
- package/tsconfig.json +41 -0
- package/tslint.json +22 -0
- package//346/226/207/344/271/246/346/211/223/345/215/260/350/275/254/346/215/242/345/231/250.bat +2 -0
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import CDP from "chrome-remote-interface";
|
|
5
|
+
import _ from "lodash";
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
import config from "config";
|
|
8
|
+
import CONFIG from "@/configEnv";
|
|
9
|
+
import { ConfigRoot, FileTransform, MyPaperSize } from "@/types/request/home";
|
|
10
|
+
import { filePathToUrl } from "@/utils/url";
|
|
11
|
+
|
|
12
|
+
//自定义纸张系列的:
|
|
13
|
+
const paperSZs = config.get("size") as MyPaperSize[];
|
|
14
|
+
const chrome_port = config.get("chrome_port");
|
|
15
|
+
const jsTimeBudget = 5000;
|
|
16
|
+
const animationTimeBudget = 5000;
|
|
17
|
+
//Pdf-lib库获取的尺寸单位是px, 需要除以28.34627471532708才是 纸张的*cm单位。
|
|
18
|
+
const LIB_FACTOR = 1 / 28.34627471532708 / 2.54;
|
|
19
|
+
//页码是罗马数字; 表盘符号
|
|
20
|
+
const ROMA_NUMS = [
|
|
21
|
+
"",
|
|
22
|
+
"I",
|
|
23
|
+
"II",
|
|
24
|
+
"III",
|
|
25
|
+
"IV",
|
|
26
|
+
"V",
|
|
27
|
+
"VI",
|
|
28
|
+
"VII",
|
|
29
|
+
"VIII",
|
|
30
|
+
"IX",
|
|
31
|
+
"X",
|
|
32
|
+
"XI",
|
|
33
|
+
"XII",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const yemeiyejiaoHtml = `<!DOCTYPE html>
|
|
37
|
+
<html>
|
|
38
|
+
<head>
|
|
39
|
+
<title>footer header</title>
|
|
40
|
+
<meta charset="utf-8">
|
|
41
|
+
<style>
|
|
42
|
+
body {
|
|
43
|
+
padding: 0;
|
|
44
|
+
margin: 0;
|
|
45
|
+
}
|
|
46
|
+
@media print {
|
|
47
|
+
#justforpage {
|
|
48
|
+
visibility: hidden;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<h4 id="justforpage">这个仅仅用于打印页眉页脚</h4>
|
|
55
|
+
<script>
|
|
56
|
+
window.onbeforeprint = (event) => {
|
|
57
|
+
var adom=document.getElementById("justforpage");
|
|
58
|
+
adom.innerHTML="在打印...";
|
|
59
|
+
document.title="在打印";
|
|
60
|
+
};
|
|
61
|
+
window.onafterprint = (event) => {
|
|
62
|
+
var adom=document.getElementById("justforpage");
|
|
63
|
+
adom.innerHTML=""打印完";
|
|
64
|
+
document.title="打印好";
|
|
65
|
+
};
|
|
66
|
+
</script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>`;
|
|
69
|
+
|
|
70
|
+
interface IRenderPdfOptions {
|
|
71
|
+
printLogs?: boolean;
|
|
72
|
+
printErrors?: boolean;
|
|
73
|
+
chromeBinary?: string;
|
|
74
|
+
chromeOptions?: string[];
|
|
75
|
+
remoteHost?: string;
|
|
76
|
+
remotePort?: string;
|
|
77
|
+
noMargins?: boolean;
|
|
78
|
+
landscape?: boolean;
|
|
79
|
+
includeBackground?: boolean;
|
|
80
|
+
windowSize?: boolean;
|
|
81
|
+
paperWidth?: number;
|
|
82
|
+
paperHeight?: number;
|
|
83
|
+
pageRanges?: string;
|
|
84
|
+
scale?: number;
|
|
85
|
+
displayHeaderFooter?: boolean;
|
|
86
|
+
headerTemplate?: string;
|
|
87
|
+
footerTemplate?: string;
|
|
88
|
+
jsTimeBudget?: number;
|
|
89
|
+
animationTimeBudget?: number;
|
|
90
|
+
//新增加的
|
|
91
|
+
marginTop?: number;
|
|
92
|
+
marginBottom?: number;
|
|
93
|
+
marginLeft?: number;
|
|
94
|
+
marginRight?: number;
|
|
95
|
+
printBackground?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface CDPbeginParam {
|
|
99
|
+
host: string;
|
|
100
|
+
port: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**主要依照 "chrome-headless-render-pdf" 来修改的! 原来有命令行CML启动 "babel-cli": "^6.24.1"参数处置的。
|
|
104
|
+
* 原来参数--remote-host set chrome host (for remote process)都需要填的,都改成本机运行的。
|
|
105
|
+
* */
|
|
106
|
+
export class RenderPDF {
|
|
107
|
+
private options = {} as IRenderPdfOptions;
|
|
108
|
+
private host = "localhost";
|
|
109
|
+
private port = chrome_port as number;
|
|
110
|
+
private sumNoPages = 0 as number; //合并输出pdf的页码汇总数合计
|
|
111
|
+
constructor(options: IRenderPdfOptions) {
|
|
112
|
+
options;
|
|
113
|
+
this.sumNoPages;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**用CDP的版本; 另外: 那个pupepeter版本,cookie不能自动取得,若是browserWSEndpoint--ws://后面链接又是经常变动的,打印的边距有跟默认不精确一致,
|
|
117
|
+
* -- 还有字体间距什么的都感觉不完全一样的,总有点差异存在的。
|
|
118
|
+
* 页眉页脚和正文重叠了,需把上下边距默认10mm都拉大点!
|
|
119
|
+
* 如果整一份文书只有单一次url生成的可以,或者虽然两个url生成的但是没必要页码接续数字的话,这个场合可以使用这个插入点加页眉页脚的配置。否则这里留白,后面合并后再加。
|
|
120
|
+
* 缺点:必须前台有浏览器的,纸张尺寸A4还需要自己输入英寸数字;margin也是英寸单位数字的。
|
|
121
|
+
* 前提客户机浏览器的启动参数必须加上 --remote-debugging-port=9222(?被别人访问)端口的;长期暴露风险!,对比pupepeter是短期的端口。
|
|
122
|
+
* 【安全点】还是短期链接端口更稳妥吧。? CDP版本也不能使用。
|
|
123
|
+
* 用户可查看正在生成访问页面的情况。 直接覆盖当前操作活动网页毛病。
|
|
124
|
+
* 奇数页还是偶数页的,双面打印对页码位置可能会调整位置的,而单独一个URL生成pdf时刻有可能没法就一定能够正确打印页码文字的是定位位置啊。抛弃支持:纯静态html横竖方向混合的场景,独立转换抽取部分纸张到过渡的pdf或拆解组合来做。
|
|
125
|
+
* 还是考虑:保留第一阶段页眉页脚的打印能力,但是仅仅针对:特殊url:file[url:{ 只能是独立的页码编码和页码合计数独自计数,不涉及到其它的URL的 }],双面打印:页码只能放在中间位置为好,页码必须从第一页开始编码的,而且这种情况目标是不采用叠加页眉页脚两次pdf方案(一次性,不提取出CSS纸张方向)。
|
|
126
|
+
* 最终采用引入reverseRanges参数设置部分纸张反转纸张方向,需要配置某某页并和CSS打印第一阶段生成的pdf能够吻合纸张方向的话,就能确保页眉页脚叠加正常位置。这里步骤就免于处理页眉页脚的生成了。
|
|
127
|
+
* */
|
|
128
|
+
async renderPdf(task: ConfigRoot<FileTransform>, tsobj: FileTransform) {
|
|
129
|
+
let options = {} as IRenderPdfOptions;
|
|
130
|
+
let url: string;
|
|
131
|
+
if (tsobj.url.startsWith("http://") || tsobj.url.startsWith("https://"))
|
|
132
|
+
url = tsobj.url;
|
|
133
|
+
else {
|
|
134
|
+
const filenamepath = `${CONFIG.APP.PATH}/${tsobj.url}`;
|
|
135
|
+
//本地文件,拖进浏览器窗口后,实际转义显示的URL
|
|
136
|
+
url = filePathToUrl(filenamepath);
|
|
137
|
+
}
|
|
138
|
+
//独立的NewTab(new Page)能力:避免干预其它的用户正在浏览窗口。
|
|
139
|
+
const { CDPclient, targetId } = await this.newTabOrPage(
|
|
140
|
+
this.getCDPhostPort(),
|
|
141
|
+
url,
|
|
142
|
+
);
|
|
143
|
+
// await RenderPDF.newTabOrPage(this.getCDPhostPort(), url);
|
|
144
|
+
// const client = await CDP({ host: this.host, port: this.port });
|
|
145
|
+
// await CDPclient.close();
|
|
146
|
+
// const client = await CDP(this.getCDPhostPort());
|
|
147
|
+
const { Page, Emulation, LayerTree, Inspector } = CDPclient;
|
|
148
|
+
await Inspector.disable();
|
|
149
|
+
await Inspector.enable();
|
|
150
|
+
await Page.enable();
|
|
151
|
+
await LayerTree.enable();
|
|
152
|
+
const loaded = this.cbToPromise(Page.loadEventFired);
|
|
153
|
+
const jsDone = this.cbToPromise(Emulation.virtualTimeBudgetExpired);
|
|
154
|
+
//这里必须navigate{url再做一次,否则获取的东西不对:缺少await loaded可能不是最终目标。
|
|
155
|
+
const { frameId, errorText } = await Page.navigate({
|
|
156
|
+
url: url,
|
|
157
|
+
//frameId: targetId, 不能加上: 本地pdf情形实际会有webview/iframe,实际会看到pdf之外的浏览控件框框的。
|
|
158
|
+
});
|
|
159
|
+
if (errorText) throw new Error(errorText); //若是访问失败应该抛异常的!
|
|
160
|
+
frameId;
|
|
161
|
+
await Emulation.setVirtualTimePolicy({
|
|
162
|
+
policy: "pauseIfNetworkFetchesPending",
|
|
163
|
+
budget: jsTimeBudget,
|
|
164
|
+
});
|
|
165
|
+
//等待loaded事件也是必须的, ?可能死等,没法触发事件。
|
|
166
|
+
await this.profileScope("Wait for load", async () => {
|
|
167
|
+
console.log("等加载好:", url);
|
|
168
|
+
await loaded;
|
|
169
|
+
});
|
|
170
|
+
await this.profileScope("Wait for js execution", async () => {
|
|
171
|
+
await jsDone;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await this.profileScope("Wait for animations", async () => {
|
|
175
|
+
await new Promise((resolve) => {
|
|
176
|
+
setTimeout(resolve, animationTimeBudget); // max waiting time
|
|
177
|
+
let timeout = setTimeout(resolve, 100);
|
|
178
|
+
LayerTree.layerPainted(() => {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
timeout = setTimeout(resolve, 100);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
//接着可用Page.navigate({ url 可惜直接覆盖掉,打印后面的纯粹的快照样子的动画界面的。Page.printToPDF实际只管当前的Tab界面页数;
|
|
185
|
+
//针对PartHeadFooter其余参数的注入!
|
|
186
|
+
options = this.applyForOptions(options, tsobj);
|
|
187
|
+
//动态生成的网页SPA不能支持纸张横屏排版和竖屏排版混合一个打印命令一次输出; 静态的html横屏竖屏才会同时出现。
|
|
188
|
+
//为何preferCSSPageSize设置=false打印出来的纸张大小保证只听这里配置,但是横屏竖屏都有的,没能影响横竖方向的!
|
|
189
|
+
//实际上这里假如设置landscape: false/true的还是不会改变静态的html内部以及生命好的CSS横屏竖屏说明的,应用层优先,对于自定义的纸张尺寸大小css也没法定义横竖方向的{这个就没影响}。
|
|
190
|
+
//【特别注意】若该url是本地的pdf文件,无法打印正常,打开本地pdf文件后实际Target.getTargets():tab.type:会有webview,iframe,page三种,不是正常page,当前窗口捕捉不匹配。
|
|
191
|
+
const pdf = await Page.printToPDF({
|
|
192
|
+
...options,
|
|
193
|
+
displayHeaderFooter: false,
|
|
194
|
+
// preferCSSPageSize: false, //和浏览器差异; 默认修改成:允许上层控制尺寸:可惜页眉页脚独立打印叠加的它打印里面是拼凑html没有同步放入CSS来控制大小啊,造成不一致性可能!!#不好提取动态网页生成的每一张页面纸张的实际大小啊。
|
|
195
|
+
});
|
|
196
|
+
const buff = Buffer.from(pdf.data, "base64");
|
|
197
|
+
// client.close();
|
|
198
|
+
|
|
199
|
+
// //共用一个浏览器窗口 默认=true
|
|
200
|
+
// singleTab?: boolean;
|
|
201
|
+
// //自动关闭浏览器窗口 默认=false
|
|
202
|
+
// closeTab?: boolean;
|
|
203
|
+
if (task.closeTab !== true && task.singleTab === false)
|
|
204
|
+
tsobj.CDPtab = targetId; //不能此刻就关闭。
|
|
205
|
+
else {
|
|
206
|
+
if (task.singleTab === true || task.singleTab === undefined) {
|
|
207
|
+
await this.closeTabOrPage(CDPclient, targetId);
|
|
208
|
+
task.CDPtab = targetId;
|
|
209
|
+
} else tsobj.CDPtab = targetId;
|
|
210
|
+
}
|
|
211
|
+
await CDPclient.close();
|
|
212
|
+
return buff;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// generatePdfOptions() {
|
|
216
|
+
// options.displayHeaderFooter = !!this.options.displayHeaderFooter;
|
|
217
|
+
// }
|
|
218
|
+
|
|
219
|
+
getCDPhostPort() {
|
|
220
|
+
return { host: this.host, port: this.port } as CDPbeginParam;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
error(...msg: any[]) {
|
|
224
|
+
if (this.options.printErrors) {
|
|
225
|
+
console.error(...msg);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
log(...msg: any[]) {
|
|
230
|
+
if (this.options.printLogs) {
|
|
231
|
+
console.log(...msg);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async cbToPromise(cb: any) {
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
cb((resp: any) => {
|
|
238
|
+
resolve(resp);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getPerfTime(prev: [number, number]) {
|
|
244
|
+
const time = process.hrtime(prev);
|
|
245
|
+
return time[0] * 1e3 + time[1] / 1e6;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async profileScope(msg: string, cb: () => {}) {
|
|
249
|
+
const start = process.hrtime();
|
|
250
|
+
await cb();
|
|
251
|
+
this.log(msg, `took ${Math.round(this.getPerfTime(start))}ms`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//屏幕输出多行的
|
|
255
|
+
browserLog(type: string, msg: string) {
|
|
256
|
+
const lines = msg.split("\n");
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
this.log(`(chrome) (${type}) ${line}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async wait(ms: number) {
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
setTimeout(resolve, ms);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**简单的合并 多个PDF 文件的内容
|
|
269
|
+
* */
|
|
270
|
+
static async mergePdfs(files: [string], outfile: string) {
|
|
271
|
+
// const renderer = new RenderPDF({});
|
|
272
|
+
//旧的采用 await renderer.connectToChrome();
|
|
273
|
+
try {
|
|
274
|
+
// Create a new document
|
|
275
|
+
const doc = await PDFDocument.create();
|
|
276
|
+
for (const fileone of files) {
|
|
277
|
+
const onepdf = await PDFDocument.load(
|
|
278
|
+
fs.readFileSync(CONFIG.APP.PATH + "/" + fileone),
|
|
279
|
+
);
|
|
280
|
+
// Add individual content pages
|
|
281
|
+
const contentPages = await doc.copyPages(
|
|
282
|
+
onepdf,
|
|
283
|
+
onepdf.getPageIndices(),
|
|
284
|
+
);
|
|
285
|
+
for (const page of contentPages) {
|
|
286
|
+
doc.addPage(page);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const name = CONFIG.APP.PATH + "/" + outfile + ".pdf";
|
|
290
|
+
fs.writeFileSync(name, await doc.save());
|
|
291
|
+
console.log(`合并结果:${name}`);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
console.error("error:", e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**页脚页码生成:第一步生成的PDF应该预留页眉页脚的空白区域(通常还有+边距),汇总大文件之后重新写总页码。
|
|
297
|
+
* 纸张的默认值改成是 a4 portrait
|
|
298
|
+
* */
|
|
299
|
+
static async pageNoForPdf(
|
|
300
|
+
url: string,
|
|
301
|
+
filename: string,
|
|
302
|
+
options: IRenderPdfOptions,
|
|
303
|
+
) {
|
|
304
|
+
const renderer = new RenderPDF(options);
|
|
305
|
+
url;
|
|
306
|
+
filename;
|
|
307
|
+
//旧的采用 await renderer.connectToChrome();
|
|
308
|
+
try {
|
|
309
|
+
// const buff = await renderer.renderPdf(url, renderer.generatePdfOptions());
|
|
310
|
+
// buff;
|
|
311
|
+
const content = await PDFDocument.load(
|
|
312
|
+
fs.readFileSync(CONFIG.APP.PATH + "/" + "test--001.pdf"),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Add a font to the doc
|
|
316
|
+
const helveticaFont = await content.embedFont(StandardFonts.Helvetica);
|
|
317
|
+
|
|
318
|
+
// Draw a number at the bottom of each page.
|
|
319
|
+
// Note that the bottom of the page is `y = 0`, not the top
|
|
320
|
+
const pages = await content.getPages();
|
|
321
|
+
const noStart = 2 as number; //从第2页开始的编码的 默认=1
|
|
322
|
+
for (const [i, page] of Object.entries(pages)) {
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
if (i >= noStart - 1) {
|
|
325
|
+
// @ts-ignore
|
|
326
|
+
page.drawText(`${+i + 2 - noStart}`, {
|
|
327
|
+
// @ts-ignore
|
|
328
|
+
x: page.getWidth() / 2,
|
|
329
|
+
y: 10,
|
|
330
|
+
size: 11,
|
|
331
|
+
font: helveticaFont,
|
|
332
|
+
color: rgb(0, 0, 0),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const name = CONFIG.APP.PATH + "/" + "test--022.pdf";
|
|
338
|
+
fs.writeFileSync(name, await content.save());
|
|
339
|
+
renderer.log(`Saved ${name}`);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
console.error("error:", e);
|
|
342
|
+
}
|
|
343
|
+
// renderer.killChrome();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**关键的: 某一个url的转换步骤 */
|
|
347
|
+
async generateSinglePdf(
|
|
348
|
+
tsobj: FileTransform,
|
|
349
|
+
task: ConfigRoot<FileTransform>,
|
|
350
|
+
options: IRenderPdfOptions,
|
|
351
|
+
) {
|
|
352
|
+
const renderer = new RenderPDF(options);
|
|
353
|
+
task;
|
|
354
|
+
try {
|
|
355
|
+
const buff = await renderer.renderPdf(task, tsobj);
|
|
356
|
+
const name = CONFIG.APP.PATH + "/" + tsobj.out + ".pdf";
|
|
357
|
+
fs.writeFileSync(name, buff);
|
|
358
|
+
renderer.log(`Saved ${name}`);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
console.error("error:", e);
|
|
361
|
+
return "失败," + e;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**最简易 默认操作: 简单合并pdf; # pdf文件太大的靠控制吗?
|
|
367
|
+
* */
|
|
368
|
+
static async mergeAllPdfto() {
|
|
369
|
+
// const renderer = new RenderPDF({});
|
|
370
|
+
const root = CONFIG.APP.PATH; //path.join(__dirname);
|
|
371
|
+
//要扣除掉 合并输出的pdf
|
|
372
|
+
function notMergeFile(name: string): boolean {
|
|
373
|
+
const start = name.indexOf(CONFIG.APP.MERGE);
|
|
374
|
+
if (start === 0) {
|
|
375
|
+
const tparts = name.substring(CONFIG.APP.MERGE.length);
|
|
376
|
+
if (tparts === ".pdf") return false;
|
|
377
|
+
if (tparts.at(0) === "-") {
|
|
378
|
+
const arrs = tparts.split(".");
|
|
379
|
+
if (arrs.length === 2) {
|
|
380
|
+
const plain = Number(arrs[0]).toString();
|
|
381
|
+
if (arrs[0] === plain) return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
/* 内部使用的函数啊? 加载指定文件夹下指定后缀的文件路径列表 (不给exts参数时则获取所有类型文件)
|
|
388
|
+
* loadFileNameByPath4Ext(‘C:\xxx\xxx’,['mp4','gcc'])
|
|
389
|
+
* 要求排序:默认按照修改时间顺序? 最老的最先打印的顺序, 通常最先生成的在前面的,
|
|
390
|
+
* */
|
|
391
|
+
function loadFileNameByPath4Ext(val: string, exts?: [string]): any {
|
|
392
|
+
const arrFiles = []; //{name:string,time:number}
|
|
393
|
+
const files = fs.readdirSync(val);
|
|
394
|
+
for (let i = 0; i < files.length; i++) {
|
|
395
|
+
const item = files[i];
|
|
396
|
+
const stat = fs.lstatSync(val + "\\" + item);
|
|
397
|
+
if (stat.isDirectory()) {
|
|
398
|
+
// arrFiles.push(loadFileNameByPath4Ext(val + '\\' + item,exts))
|
|
399
|
+
} else {
|
|
400
|
+
if (exts != undefined && exts.length > 0) {
|
|
401
|
+
for (let j = 0; j < exts.length; j++) {
|
|
402
|
+
const ext = exts[j];
|
|
403
|
+
if (
|
|
404
|
+
item?.split(".")?.pop()?.toLowerCase() ==
|
|
405
|
+
ext.trim().toLowerCase()
|
|
406
|
+
) {
|
|
407
|
+
notMergeFile(item) &&
|
|
408
|
+
arrFiles.push({ name: item, time: stat.mtimeMs }); //val + '\\' + item全路径的
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
notMergeFile(item) &&
|
|
414
|
+
arrFiles.push({ name: item, time: stat.mtimeMs });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const sortFiles = _.sortBy(
|
|
420
|
+
arrFiles,
|
|
421
|
+
function (it: { name: string; time: number }) {
|
|
422
|
+
return it.time;
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
return sortFiles.map((fileob: { name: string }) => fileob.name);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
//旧的采用 await renderer.connectToChrome();
|
|
429
|
+
try {
|
|
430
|
+
const files = loadFileNameByPath4Ext(root, ["pdf"]);
|
|
431
|
+
RenderPDF.mergePdfs(files, CONFIG.APP.MERGE);
|
|
432
|
+
console.log(
|
|
433
|
+
"简单合并工作目录的所有pdf: " + `输出是: ${CONFIG.APP.MERGE}`,
|
|
434
|
+
);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
console.error("error:", e);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**转换配置参数适配给CDP; 纸张默认是 "a4 portrait" 浏览器默认是letter大小而非a4大小的。
|
|
441
|
+
* 这里页面参数转换和页眉页脚那一块处理html内置CSS表示的size:页面方式不一样。
|
|
442
|
+
* */
|
|
443
|
+
applyForOptions(
|
|
444
|
+
old: IRenderPdfOptions,
|
|
445
|
+
ts: FileTransform,
|
|
446
|
+
): IRenderPdfOptions {
|
|
447
|
+
//纸张大小 方向;
|
|
448
|
+
const INUT = 25.4;
|
|
449
|
+
const ret = old;
|
|
450
|
+
//浏览器默认的是letter 8.5in x 11in. paperWidth X paperHeight, 1英寸=25.4毫米(mm)
|
|
451
|
+
switch (ts.lay?.size) {
|
|
452
|
+
case "a5":
|
|
453
|
+
ret.paperWidth = 148 / INUT;
|
|
454
|
+
ret.paperHeight = 210 / INUT;
|
|
455
|
+
break;
|
|
456
|
+
case "a4":
|
|
457
|
+
case undefined:
|
|
458
|
+
case "":
|
|
459
|
+
ret.paperWidth = 210 / INUT;
|
|
460
|
+
ret.paperHeight = 297 / INUT;
|
|
461
|
+
break;
|
|
462
|
+
case "a3":
|
|
463
|
+
ret.paperWidth = 297 / INUT;
|
|
464
|
+
ret.paperHeight = 420 / INUT;
|
|
465
|
+
break;
|
|
466
|
+
case "b5":
|
|
467
|
+
ret.paperWidth = 176 / INUT;
|
|
468
|
+
ret.paperHeight = 250 / INUT;
|
|
469
|
+
break;
|
|
470
|
+
case "b4":
|
|
471
|
+
ret.paperWidth = 250 / INUT;
|
|
472
|
+
ret.paperHeight = 353 / INUT;
|
|
473
|
+
break;
|
|
474
|
+
case "jis-b5":
|
|
475
|
+
ret.paperWidth = 182 / INUT;
|
|
476
|
+
ret.paperHeight = 257 / INUT;
|
|
477
|
+
break;
|
|
478
|
+
case "jis-b4":
|
|
479
|
+
ret.paperWidth = 257 / INUT;
|
|
480
|
+
ret.paperHeight = 364 / INUT;
|
|
481
|
+
break;
|
|
482
|
+
case "letter":
|
|
483
|
+
case "auto":
|
|
484
|
+
ret.paperWidth = 8.5;
|
|
485
|
+
ret.paperHeight = 11;
|
|
486
|
+
break;
|
|
487
|
+
case "legal":
|
|
488
|
+
ret.paperWidth = 8.5;
|
|
489
|
+
ret.paperHeight = 14;
|
|
490
|
+
break;
|
|
491
|
+
case "ledger":
|
|
492
|
+
ret.paperWidth = 11;
|
|
493
|
+
ret.paperHeight = 17;
|
|
494
|
+
break;
|
|
495
|
+
default:
|
|
496
|
+
const custom = paperSZs.find(function (value) {
|
|
497
|
+
return value.name === ts.lay.size;
|
|
498
|
+
});
|
|
499
|
+
//配置文件预定义的纸张类型,
|
|
500
|
+
if (custom) {
|
|
501
|
+
ret.paperWidth = custom.w;
|
|
502
|
+
ret.paperHeight = custom.h;
|
|
503
|
+
} else throw new Error("不可识别纸张尺寸:" + ts.lay.size);
|
|
504
|
+
}
|
|
505
|
+
ret.landscape = ts.lay?.landscape;
|
|
506
|
+
ret.printBackground = ts.lay?.printBackground;
|
|
507
|
+
ret.pageRanges = ts.lay?.pageRanges;
|
|
508
|
+
ret.marginTop = ts.lay?.marginTop;
|
|
509
|
+
ret.marginBottom = ts.lay?.marginBottom;
|
|
510
|
+
ret.marginLeft = ts.lay?.marginLeft;
|
|
511
|
+
ret.marginRight = ts.lay?.marginRight;
|
|
512
|
+
return ret;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**汇总步骤入口: 前提 :前面已经生成独立的多个pdf */
|
|
516
|
+
static async MergeAllPdfs(
|
|
517
|
+
task: ConfigRoot<FileTransform>,
|
|
518
|
+
options: IRenderPdfOptions,
|
|
519
|
+
) {
|
|
520
|
+
const renderer = new RenderPDF(options);
|
|
521
|
+
try {
|
|
522
|
+
//唯一 汇总的PDF;
|
|
523
|
+
const docAll = await PDFDocument.create();
|
|
524
|
+
//【预处理】页眉页脚的处理: 有些不计算进入页码统计基数,有些不需要打印页眉页脚的(不叠加{单独编号,非单独确实不打印}),有些独立拆开pdf文件的。 ?合计多少页数(总页数的)
|
|
525
|
+
for (const meta of task.files) {
|
|
526
|
+
if (!meta.out) {
|
|
527
|
+
if (!meta.url.endsWith(".pdf") || meta.url.startsWith("http"))
|
|
528
|
+
throw new Error("要配置out参数,url:" + meta.url);
|
|
529
|
+
}
|
|
530
|
+
if (meta.displayHeaderFooter === undefined)
|
|
531
|
+
meta.displayHeaderFooter = true;
|
|
532
|
+
//转换页眉页脚的html存储 :直接地附加到FileTransform模型对象;
|
|
533
|
+
if (meta.lay) {
|
|
534
|
+
const newfoot = "";
|
|
535
|
+
const formfoot =
|
|
536
|
+
meta.lay.foot &&
|
|
537
|
+
newfoot.concat(...meta.lay.foot).replaceAll(`\\"`, '"');
|
|
538
|
+
meta.footerTemplate = formfoot ?? undefined;
|
|
539
|
+
const formhead =
|
|
540
|
+
meta.lay.head &&
|
|
541
|
+
newfoot.concat(...meta.lay.head).replaceAll(`\\"`, '"');
|
|
542
|
+
meta.headerTemplate = formhead ?? undefined;
|
|
543
|
+
const formfootL =
|
|
544
|
+
meta.lay.footL &&
|
|
545
|
+
newfoot.concat(...meta.lay.footL).replaceAll(`\\"`, '"');
|
|
546
|
+
meta.footerTemplateL = formfootL ?? undefined;
|
|
547
|
+
const formheadL =
|
|
548
|
+
meta.lay.headL &&
|
|
549
|
+
newfoot.concat(...meta.lay.headL).replaceAll(`\\"`, '"');
|
|
550
|
+
meta.headerTemplateL = formheadL ?? undefined;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
//计算汇总pdf的页码总数,不打印页码的扣除, 独立pdf页数扣除,独立打印页码合计数的pdf扣除, renderer.sumNoPages @这步骤有可能添加空白页的!
|
|
554
|
+
await renderer.searchForNoSumm(task);
|
|
555
|
+
// //目前是第几页(要打印页眉页脚的 并且 要求输出页码的) : ?最后一个:noSumPages的数字不能正好吻合??
|
|
556
|
+
let currentNo = 1;
|
|
557
|
+
let mergedPrintSeq = 1; //合并汇总pdf文件的:目前打印第几页
|
|
558
|
+
//独立的NewTab(new Page)能力:避免干预其它的用户正在浏览窗口。 只开一次会影响到用户切换激活的窗口! await RenderPDF.newTabOrPage2(
|
|
559
|
+
const { CDPclient, targetId } = await renderer.newTabOrPage(
|
|
560
|
+
renderer.getCDPhostPort(),
|
|
561
|
+
"chrome://newtab/",
|
|
562
|
+
);
|
|
563
|
+
for (const meta of task.files) {
|
|
564
|
+
//当前独立打印页码的特别pdf部分的部分合计页码合计数; 应该是独立的url才能设置吧 独立打印页码的也不一定单独输出pdf但是一定是单一个URL
|
|
565
|
+
let urlCurrentNo = 1;
|
|
566
|
+
// content 轮到了第几个URL ,读取前面步骤的pdf; 非独立的都应该复制,并且加上页眉页脚》》给docAll;
|
|
567
|
+
const ufname = meta.out ? meta.out + ".pdf" : meta.url; //都是pdf了
|
|
568
|
+
const content = await PDFDocument.load(
|
|
569
|
+
fs.readFileSync(CONFIG.APP.PATH + "/" + ufname),
|
|
570
|
+
);
|
|
571
|
+
const totalNOs = renderer.sumNoPages; //非局部化页码自己计数的;
|
|
572
|
+
let docCPdf = docAll; //当前操作那个pdf :可能独立输出pdf
|
|
573
|
+
if (meta.merge === false) {
|
|
574
|
+
docCPdf = await PDFDocument.create();
|
|
575
|
+
//独立的输出,还是抛出异常!
|
|
576
|
+
if (!meta.out)
|
|
577
|
+
throw new Error("独立输出pdf必须配置out参数,url:" + meta.url);
|
|
578
|
+
}
|
|
579
|
+
const curUrlpages = content.getPages().length;
|
|
580
|
+
meta.frNo = meta.frNo === undefined ? 1 : meta.frNo;
|
|
581
|
+
if (meta.localSumNo && meta.frNo === 0)
|
|
582
|
+
throw new Error("URL:" + meta?.url + " localSumNo, frNo冲突");
|
|
583
|
+
const urlTotalNOs = meta.localSumNo
|
|
584
|
+
? curUrlpages - meta.frNo + 1
|
|
585
|
+
: undefined; //局部化页码自己计数的;
|
|
586
|
+
let urlPrintSeq = 1; //独立pdf文件的:目前打印第几页
|
|
587
|
+
for (let i = 0; i < curUrlpages; i++) {
|
|
588
|
+
if (
|
|
589
|
+
!meta.displayHeaderFooter ||
|
|
590
|
+
(meta.headFrom && i < meta.headFrom - 1)
|
|
591
|
+
) {
|
|
592
|
+
//不输出页眉页脚的 只能复制第一阶段的pdf! 【注意】headFrom 和 frNo 设置相互影响, 没有页眉页脚就 失去页码意义!
|
|
593
|
+
const [secondDonorPage] = await docCPdf.copyPages(content, [i]);
|
|
594
|
+
docCPdf.addPage(secondDonorPage);
|
|
595
|
+
} else {
|
|
596
|
+
//页眉页脚只好拆解成一页一页的,没法一次性输出pdf。 meta task 当前是那一页的配置?生成 类似于generatePdfOptions?
|
|
597
|
+
const cotPage = content.getPages()[i];
|
|
598
|
+
//这里单位是px, 需要除以28.34627471532708才是 纸张的*cm单位。
|
|
599
|
+
const { width, height } = cotPage.getSize();
|
|
600
|
+
//需要传递:当前内容pdf页面实际size: 页眉页脚占用的空白;
|
|
601
|
+
const pageNumber =
|
|
602
|
+
meta.frNo === 0
|
|
603
|
+
? undefined
|
|
604
|
+
: i < meta.frNo - 1
|
|
605
|
+
? undefined
|
|
606
|
+
: meta.localSumNo
|
|
607
|
+
? urlCurrentNo++
|
|
608
|
+
: currentNo++;
|
|
609
|
+
|
|
610
|
+
const buff = await renderer.yemeiyejiaoPageGen(
|
|
611
|
+
CDPclient,
|
|
612
|
+
targetId,
|
|
613
|
+
meta,
|
|
614
|
+
task,
|
|
615
|
+
{
|
|
616
|
+
marginTop: meta.lay?.marginTop,
|
|
617
|
+
marginBottom: meta.lay?.marginBottom,
|
|
618
|
+
//纸张尺寸和pdf的像素数如何映射的?
|
|
619
|
+
paperWidth: width * LIB_FACTOR,
|
|
620
|
+
paperHeight: height * LIB_FACTOR,
|
|
621
|
+
},
|
|
622
|
+
meta.merge === false ? urlPrintSeq : mergedPrintSeq,
|
|
623
|
+
meta.localSumNo ? urlTotalNOs! : totalNOs,
|
|
624
|
+
pageNumber,
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const cover = await PDFDocument.load(buff!);
|
|
628
|
+
const yejiaoPage = cover.getPages()[0];
|
|
629
|
+
const yeMeiyeJiao = await docCPdf.embedPage(yejiaoPage, {
|
|
630
|
+
left: 0,
|
|
631
|
+
bottom: 0,
|
|
632
|
+
right: yejiaoPage.getWidth(),
|
|
633
|
+
top: yejiaoPage.getHeight(),
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const preamble = await docCPdf.embedPage(cotPage, {
|
|
637
|
+
left: 0,
|
|
638
|
+
bottom: 0,
|
|
639
|
+
right: cotPage.getWidth(),
|
|
640
|
+
top: cotPage.getHeight(),
|
|
641
|
+
});
|
|
642
|
+
const yeMeiyeJiaoDims = yeMeiyeJiao.scale(1);
|
|
643
|
+
const preambleDims = preamble.scale(1);
|
|
644
|
+
//这里必须约束尺寸, 否则 横着只打印部分的。
|
|
645
|
+
const page = docCPdf.addPage([width, height]);
|
|
646
|
+
await page.drawPage(yeMeiyeJiao, {
|
|
647
|
+
...yeMeiyeJiaoDims,
|
|
648
|
+
x: 0,
|
|
649
|
+
y: 0,
|
|
650
|
+
});
|
|
651
|
+
await page.drawPage(preamble, {
|
|
652
|
+
...preambleDims,
|
|
653
|
+
x: 0,
|
|
654
|
+
y: 0,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (meta.merge === false) urlPrintSeq++;
|
|
658
|
+
else mergedPrintSeq++;
|
|
659
|
+
}
|
|
660
|
+
//这里如果是独立的拆开输出pdf的提前就保存了。 docCPdf
|
|
661
|
+
if (docCPdf !== docAll) {
|
|
662
|
+
//独立的输出, 注意会覆盖原先的文件的
|
|
663
|
+
const name = CONFIG.APP.PATH + "/" + ufname;
|
|
664
|
+
fs.writeFileSync(name, await docCPdf.save());
|
|
665
|
+
renderer.log(`Saved ${name}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (task.closeTab) await renderer.closeTabOrPage(CDPclient, targetId);
|
|
669
|
+
CDPclient.close();
|
|
670
|
+
const name =
|
|
671
|
+
CONFIG.APP.PATH + "/" + (task.name ?? CONFIG.APP.MERGE) + ".pdf";
|
|
672
|
+
fs.writeFileSync(name, await docAll.save());
|
|
673
|
+
renderer.log(`Saved ${name}`);
|
|
674
|
+
} catch (e) {
|
|
675
|
+
console.error("error:", e);
|
|
676
|
+
return "失败," + e;
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**必须为每一页的页眉页脚,单独立输出固定的pdf文件名。把第一步骤的页眉页脚生成挪过来。
|
|
682
|
+
* 注意这个html注入<span class={${pageNumber+2}}>${pageNumber+2}</span>/<span class={{totalPages+2}}>${totalPages+2}</span></div>可以打印变量取值+数的。可惜不能配合pageNumber浏览器的内置计数器啊,
|
|
683
|
+
* 但是加了class=pageNumber></span> / <span class=totalPages之后,文字就不被外部控制了,只能CDP自己输出页码,外部无法参与修改页码啊。一次性打印全部页码,pk:?自己外部控制只能一页一页打印。
|
|
684
|
+
* 不依靠Chrome的页码,自己维护页码。只能舍弃chrome自带的部分功能,没法满足复杂需求啊。第几个 共几页,页眉页脚位置详情 双面打印的左边还是右边的预期版面?。
|
|
685
|
+
* @return 页眉页脚空文件的pdf或可识别pdf-lib 字节码;
|
|
686
|
+
* 参数pageNumber :为空的表示:不显示页码的!
|
|
687
|
+
* @param CDPclient
|
|
688
|
+
* @param targetId
|
|
689
|
+
* @param tsobj 当前URL处理
|
|
690
|
+
* @param task 整体作业
|
|
691
|
+
* @param options 传递给CDP:Page.printToPDF的参数
|
|
692
|
+
* @param printSeq: 正在打印的第几个面的,独立pdf分开计算。双面打印场景用的,第一页面:1,2,3.。。。
|
|
693
|
+
* @param totalPages 页码总页数
|
|
694
|
+
* @param pageNumber 页码
|
|
695
|
+
* */
|
|
696
|
+
async yemeiyejiaoPageGen(
|
|
697
|
+
CDPclient: any,
|
|
698
|
+
targetId: string,
|
|
699
|
+
tsobj: FileTransform,
|
|
700
|
+
task: ConfigRoot<FileTransform>,
|
|
701
|
+
options: IRenderPdfOptions,
|
|
702
|
+
printSeq: number,
|
|
703
|
+
totalPages: number,
|
|
704
|
+
pageNumber?: number,
|
|
705
|
+
) {
|
|
706
|
+
const renderer = new RenderPDF(options);
|
|
707
|
+
const needDoublePrnt = !(tsobj.merge === false
|
|
708
|
+
? !tsobj.doubleSide
|
|
709
|
+
: task.doubleSide === false);
|
|
710
|
+
const leftHandPanel = printSeq % 2 == 0;
|
|
711
|
+
const onLeft = needDoublePrnt && leftHandPanel; //左手边打印页面:其它情形都是采用headerTemplate footerTemplate不带xxL尾巴的页眉页脚模板。
|
|
712
|
+
let headerTemplate0;
|
|
713
|
+
if (onLeft)
|
|
714
|
+
headerTemplate0 =
|
|
715
|
+
tsobj.headerTemplateL !== undefined
|
|
716
|
+
? tsobj.headerTemplateL
|
|
717
|
+
: task.headerTemplateL !== undefined
|
|
718
|
+
? task.headerTemplateL
|
|
719
|
+
: tsobj.headerTemplate !== undefined
|
|
720
|
+
? tsobj.headerTemplate
|
|
721
|
+
: task.headerTemplate;
|
|
722
|
+
else
|
|
723
|
+
headerTemplate0 =
|
|
724
|
+
tsobj.headerTemplate !== undefined
|
|
725
|
+
? tsobj.headerTemplate
|
|
726
|
+
: task.headerTemplate;
|
|
727
|
+
//改成A4尺寸的; 默认产生是Letter尺寸; {"name":"paperWidth","description":"Paper width in inches. Defaults to 8.5 inches.","optional":true,"type":"number"},{"name":"paperHeight","description":"Paper height in inches. Defaults to 11 inches.","optional":true,"type":"number"} 25.4
|
|
728
|
+
//保留 页码位置? 占位替换特殊标记的?--#pageNumber#-- --#totalPages#-- 直接替换掉html; 双面打印的页码位置飘移处理:?多加一个footerTemplate2代表偶数页;
|
|
729
|
+
// options.paperHeight = 11.69291338582677; //'29.701cm'; //8.267716535433071 11.69291338582677
|
|
730
|
+
options.displayHeaderFooter = true;
|
|
731
|
+
if (!pageNumber)
|
|
732
|
+
headerTemplate0 = headerTemplate0?.replace(
|
|
733
|
+
"-NOT_DISPLAY-",
|
|
734
|
+
"display:none;",
|
|
735
|
+
);
|
|
736
|
+
else {
|
|
737
|
+
headerTemplate0 = headerTemplate0?.replace("-NOT_DISPLAY-", "");
|
|
738
|
+
headerTemplate0 = headerTemplate0
|
|
739
|
+
?.replace(
|
|
740
|
+
"${pageNumber}",
|
|
741
|
+
tsobj.roman && pageNumber > 0 && pageNumber < 13
|
|
742
|
+
? ROMA_NUMS[pageNumber]
|
|
743
|
+
: "" + pageNumber,
|
|
744
|
+
)
|
|
745
|
+
.replace("${totalPages}", "" + totalPages);
|
|
746
|
+
}
|
|
747
|
+
options.headerTemplate = headerTemplate0;
|
|
748
|
+
let footerTemplate0;
|
|
749
|
+
if (onLeft)
|
|
750
|
+
footerTemplate0 =
|
|
751
|
+
tsobj.footerTemplateL !== undefined
|
|
752
|
+
? tsobj.footerTemplateL
|
|
753
|
+
: task.footerTemplateL !== undefined
|
|
754
|
+
? task.footerTemplateL
|
|
755
|
+
: tsobj.footerTemplate !== undefined
|
|
756
|
+
? tsobj.footerTemplate
|
|
757
|
+
: task.footerTemplate;
|
|
758
|
+
else
|
|
759
|
+
footerTemplate0 =
|
|
760
|
+
tsobj.footerTemplate !== undefined
|
|
761
|
+
? tsobj.footerTemplate
|
|
762
|
+
: task.footerTemplate;
|
|
763
|
+
if (!pageNumber)
|
|
764
|
+
footerTemplate0 = footerTemplate0?.replace(
|
|
765
|
+
"-NOT_DISPLAY-",
|
|
766
|
+
"display:none;",
|
|
767
|
+
);
|
|
768
|
+
else {
|
|
769
|
+
footerTemplate0 = footerTemplate0?.replace("-NOT_DISPLAY-", "");
|
|
770
|
+
footerTemplate0 = footerTemplate0
|
|
771
|
+
?.replace(
|
|
772
|
+
"${pageNumber}",
|
|
773
|
+
tsobj.roman && pageNumber > 0 && pageNumber < 13
|
|
774
|
+
? ROMA_NUMS[pageNumber]
|
|
775
|
+
: "" + pageNumber,
|
|
776
|
+
)
|
|
777
|
+
.replace("${totalPages}", "" + totalPages);
|
|
778
|
+
}
|
|
779
|
+
options.footerTemplate = footerTemplate0;
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
//页眉页脚的处理: 有些不计算进入页码统计基数,有些不需要打印页眉页脚的(不叠加{单独编号,非单独确实不打印}),有些独立拆开pdf文件的。 ?合计多少页数(总页数的)
|
|
783
|
+
//正常第一阶段一个URL一种纸张尺寸:(纯粹静态html可能会横向竖向的混合),其它工具生成的pdf可能尺寸混合大小的。
|
|
784
|
+
const buff = await renderer.onePageGenernate(CDPclient, targetId, {
|
|
785
|
+
...options,
|
|
786
|
+
});
|
|
787
|
+
return buff;
|
|
788
|
+
} catch (e) {
|
|
789
|
+
console.error("error:", e);
|
|
790
|
+
return "失败," + e;
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**直接嵌入用户打开的浏览器的模式下,独立的开启新的浏览器窗口需要:
|
|
796
|
+
* 问题:刚用createTarget立刻获取targetId(frameId)没法正常工作,
|
|
797
|
+
* 这里await CDP(options)必须做两次!!:没法避免,否则干扰用户窗口,要么新Tab没有实际准备好的。
|
|
798
|
+
* */
|
|
799
|
+
async newTabOrPage(options: CDPbeginParam, url: string) {
|
|
800
|
+
const client1 = await CDP(options); // await CDP({ host: options.host, port: options.port });
|
|
801
|
+
const { Target } = client1;
|
|
802
|
+
await Target.createTarget({
|
|
803
|
+
url: url,
|
|
804
|
+
background: true,
|
|
805
|
+
});
|
|
806
|
+
//前端编码之escape、encodeURI对的, 和 encodeURIComponent
|
|
807
|
+
const { targetInfos } = await Target.getTargets();
|
|
808
|
+
const target = targetInfos.find(function (tab: any) {
|
|
809
|
+
//"type": "webview", iframe, background_page, ...
|
|
810
|
+
return tab.type === "page" && tab.url === url;
|
|
811
|
+
});
|
|
812
|
+
client1.close(); //原先每次关闭的:
|
|
813
|
+
if (!target.targetId) throw new Error("没开启窗口:" + url);
|
|
814
|
+
const client = await CDP(options);
|
|
815
|
+
//这些用例简易网页:frameId实际等同targetId;因没有嵌套<iframe />标签的。
|
|
816
|
+
return { CDPclient: client, targetId: target.targetId };
|
|
817
|
+
}
|
|
818
|
+
/**假如不是当前刚刚操作的Tab的话?:
|
|
819
|
+
* */
|
|
820
|
+
async closeTabOrPage(client: any, target: string) {
|
|
821
|
+
const { Page } = client;
|
|
822
|
+
target;
|
|
823
|
+
await Page.close(); //当前浏览器的正在查看Tab也就是网站SPA页面,Page实际就是独立浏览器窗口:关闭掉;
|
|
824
|
+
}
|
|
825
|
+
async closeAllTab(options: CDPbeginParam, task: ConfigRoot<FileTransform>) {
|
|
826
|
+
const client1 = await CDP(options); // await CDP({ host: options.host, port: options.port });
|
|
827
|
+
const { Target } = client1;
|
|
828
|
+
const { targetInfos } = await Target.getTargets();
|
|
829
|
+
for (const meta of task.files) {
|
|
830
|
+
if (meta.CDPtab) {
|
|
831
|
+
try {
|
|
832
|
+
const target = targetInfos.find(function (tab: any) {
|
|
833
|
+
return tab.type === "page" && tab.targetId === meta.CDPtab;
|
|
834
|
+
});
|
|
835
|
+
await Target.closeTarget({ targetId: target.targetId });
|
|
836
|
+
} catch (e) {
|
|
837
|
+
// result = "算url失败:" + meta.url;
|
|
838
|
+
console.error(e);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
client1.close(); //原先每次关闭的:
|
|
843
|
+
// if (!target.targetId) throw new Error("没开启窗口:" + url);
|
|
844
|
+
// const client = await CDP(options);
|
|
845
|
+
//这些用例简易网页:frameId实际等同targetId;因没有嵌套<iframe />标签的。
|
|
846
|
+
}
|
|
847
|
+
/**页眉页脚打印pdf:
|
|
848
|
+
* 为何这里CDP开动要2次:@是为了不要把浏览器当前用户正在交互操作的网页给它冲没掉,为了确保能够另外开启一个浏览窗口进程的。
|
|
849
|
+
* 只含有页眉页脚的空白页
|
|
850
|
+
* @page :left 双面打印的 装订线位置留白的,还是依赖物理打印机处置为好,这里生成可适应非面打印也能适配的pdf。
|
|
851
|
+
* */
|
|
852
|
+
async onePageGenernate(
|
|
853
|
+
CDPclient: any,
|
|
854
|
+
targetId: string,
|
|
855
|
+
options: IRenderPdfOptions,
|
|
856
|
+
) {
|
|
857
|
+
//独立的NewTab(new Page)能力:避免干预其它的用户正在浏览窗口。假如所有页数共用一个窗口的也会出现问题。用户切换激活窗口导致当前窗口自动切换到用户浏览窗口!都是错误!
|
|
858
|
+
// await RenderPDF.newTabOrPage(this.getCDPhostPort(), "about:blank");
|
|
859
|
+
// const client = await CDP({ host: this.host, port: this.port });
|
|
860
|
+
const client = CDPclient;
|
|
861
|
+
const { Page, Emulation, LayerTree, Target } = client;
|
|
862
|
+
Target;
|
|
863
|
+
// await Target.attachToTarget({ targetId });
|
|
864
|
+
// await Target.activateTarget({ targetId });
|
|
865
|
+
|
|
866
|
+
await Page.enable();
|
|
867
|
+
await LayerTree.enable();
|
|
868
|
+
const loaded = this.cbToPromise(Page.loadEventFired);
|
|
869
|
+
const jsDone = this.cbToPromise(Emulation.virtualTimeBudgetExpired);
|
|
870
|
+
// const { frameId, errorText } = await Page.navigate({ url: "about:blank" });
|
|
871
|
+
// if (errorText) throw new Error(errorText); //若是访问失败应该抛异常的!
|
|
872
|
+
//如果是setDocumentContent({html:html1+html2})内容的可能混乱:脚本css以及DOM的id都可能交叉影响了,最终强制合并输出效果可能不是预期的网页内容。
|
|
873
|
+
|
|
874
|
+
// await Page.setDocumentContent({ html: yemeiyejiaoHtml });
|
|
875
|
+
await Page.setDocumentContent({ frameId: targetId, html: yemeiyejiaoHtml });
|
|
876
|
+
|
|
877
|
+
await Emulation.setVirtualTimePolicy({
|
|
878
|
+
policy: "pauseIfNetworkFetchesPending",
|
|
879
|
+
budget: jsTimeBudget,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
await this.profileScope("Wait for load", async () => {
|
|
883
|
+
await loaded;
|
|
884
|
+
});
|
|
885
|
+
await this.profileScope("Wait for js execution", async () => {
|
|
886
|
+
await jsDone;
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
await this.profileScope("Wait for animations", async () => {
|
|
890
|
+
await new Promise((resolve) => {
|
|
891
|
+
setTimeout(resolve, animationTimeBudget); // max waiting time
|
|
892
|
+
let timeout = setTimeout(resolve, 100);
|
|
893
|
+
LayerTree.layerPainted(() => {
|
|
894
|
+
clearTimeout(timeout);
|
|
895
|
+
timeout = setTimeout(resolve, 100);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
//动态生成的网页SPA不能支持纸张横屏排版和竖屏排版混合一个打印命令一次输出;
|
|
900
|
+
//若是标准纸张大小的横屏竖屏都有的,动态APP生成的页面要么横的要么竖方向的能够真保证同一个方向!可是纯粹静态的html的打印有可能一个pdf既有横方向的掺杂这竖方向的网页输出;影响到页眉页脚啊独立的pdf生成,叠加PDF位置没能按预期摆放啊!
|
|
901
|
+
//纯粹静态的html的打印?标准纸张大小的横竖方向掺杂这种情况:一次提取一部分pageRanges[]吗,分解多个的files[url,out]来独立生成之后汇总合并的吗。
|
|
902
|
+
const pdf = await Page.printToPDF({
|
|
903
|
+
...options,
|
|
904
|
+
preferCSSPageSize: false, //允许应用上层控制尺寸:可惜页眉页脚独立打印叠加的它打印里面是拼凑html没有同步放入CSS来控制大小啊,造成不一致性可能!!
|
|
905
|
+
});
|
|
906
|
+
const buff = Buffer.from(pdf.data, "base64");
|
|
907
|
+
//let buff = await Buffer.concat([buff1,buff2]); 不能使用buff追加,生成PDF实际无法显示后面追加的页面内容的!
|
|
908
|
+
// await Page.close(); //当前浏览器的正在查看Tab也就是网站SPA页面,Page实际就是独立浏览器窗口:关闭掉;
|
|
909
|
+
// client.close(); //原先每次关闭的:
|
|
910
|
+
return buff;
|
|
911
|
+
}
|
|
912
|
+
/**双面打印的:留白页, 有页眉页脚页码的可打印,若页眉页脚都不输出的话,这个这一页就是完全白纸了?加html内容输出?
|
|
913
|
+
* 纸张尺寸是整个pdf范围之类一致的大小。 是在meta对应的pdf末尾添加的空白页!
|
|
914
|
+
* */
|
|
915
|
+
async createBlankFirst(meta: FileTransform) {
|
|
916
|
+
const ufname = meta.out ? meta.out + ".pdf" : meta.url; //都是pdf了
|
|
917
|
+
const content = await PDFDocument.load(
|
|
918
|
+
fs.readFileSync(CONFIG.APP.PATH + "/" + ufname),
|
|
919
|
+
);
|
|
920
|
+
const beforeCnt = content.getPages().length;
|
|
921
|
+
//const [firstPage, fourthPage, ninetiethPage] = await pdfDoc.copyPages(srcDoc, [0, 3, 89])
|
|
922
|
+
//纸张尺寸: 需要抄袭该pdf的最后一个page==右边位置哪一个页;
|
|
923
|
+
const rightPage = content.getPages()[beforeCnt - 1];
|
|
924
|
+
const { width, height } = rightPage.getSize(); //const { width, height } = page.getSize(); setSize(page.getWidth() - 50, page.getHeight() - 100)
|
|
925
|
+
const newPage = content.insertPage(beforeCnt);
|
|
926
|
+
newPage.setSize(width, height);
|
|
927
|
+
//const helveticaFont = await content.embedFont(StandardFonts.Helvetica);
|
|
928
|
+
// x: page.getWidth() / 2 - jpgDims.width / 2,
|
|
929
|
+
// y: page.getHeight() / 2 - jpgDims.height / 2,
|
|
930
|
+
//这里若没有drawText会导致无法嵌入到汇总页面中去:embed无效。 x,y默认=0 左下角的位置;drawText("",不可以省略的!!
|
|
931
|
+
newPage.drawText("", {
|
|
932
|
+
// x: newPage.getWidth() / 2,
|
|
933
|
+
// y: 10,
|
|
934
|
+
size: 11,
|
|
935
|
+
//font: helveticaFont,
|
|
936
|
+
color: rgb(0, 0, 0),
|
|
937
|
+
});
|
|
938
|
+
const name = CONFIG.APP.PATH + "/" + ufname;
|
|
939
|
+
fs.writeFileSync(name, await content.save());
|
|
940
|
+
}
|
|
941
|
+
/**汇总阶段:首先要敲定合并pdf的页码统计总数。 但单独给页码合计数的url要除外。
|
|
942
|
+
* 特例:若ConfigRoot<T>.merge=false那么必然导致FileTransform.merge强制=false,每个独立pdf页码独立的独立都从1开始编号吗还需要打印吗,页眉页脚需要打印吗?页眉页脚打印的默认值改成false;
|
|
943
|
+
* 小心了 createBlankFirst()修改上一步骤pdf的。
|
|
944
|
+
* */
|
|
945
|
+
async searchForNoSumm(task: ConfigRoot<FileTransform>) {
|
|
946
|
+
let sumpages = 0; //没有页码的也算,才能决定刷面打印的左边还是右边位置啊。影响双面打印的特殊需求(某些url硬要位置是左手边开始的)
|
|
947
|
+
//未考虑 headFrom 和 frNo 配置冲突情形的带来计数器损失。
|
|
948
|
+
let counter = 0; //页码合计数, 汇总合并pdf的统计数。并非独立的局部页码统计数。
|
|
949
|
+
let nearDoubleMeta = null; //往前面数最靠近的 merge !=false的哪一个URL
|
|
950
|
+
let nearDoubleMetaYema; //跟随着nearDoubleMeta的变动!表示nearDoubleMeta对应url:pdf有没有影响汇总合并pdf的页码合计数打印;
|
|
951
|
+
for (let k = 0; k < task.files.length; k++) {
|
|
952
|
+
const meta = task.files[k];
|
|
953
|
+
//没经过CDP:Chrome转换的,直接合并的 pdf也能再次打印页眉和页码啊。
|
|
954
|
+
const ufname = meta.out ? meta.out + ".pdf" : meta.url; //都是pdf了
|
|
955
|
+
const content = await PDFDocument.load(
|
|
956
|
+
fs.readFileSync(CONFIG.APP.PATH + "/" + ufname),
|
|
957
|
+
);
|
|
958
|
+
const curUrlpages = content.getPages().length;
|
|
959
|
+
if (meta?.count && curUrlpages !== meta?.count)
|
|
960
|
+
throw new Error(
|
|
961
|
+
"URL:" + meta?.url + "页数不符合预期,实际=" + curUrlpages + "页的",
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
//第一个url肯定没有的! 往前面数最靠近的merge!=false的哪一个URL:pdf;最尾巴那个URL就不必关心双面打印的最后一页左边留白。
|
|
965
|
+
if (task.doubleSide === undefined || task.doubleSide) {
|
|
966
|
+
if (meta.rightHand) {
|
|
967
|
+
if ((sumpages + 1) % 2 == 0) {
|
|
968
|
+
if ((task.merge === undefined || task.merge) && nearDoubleMeta) {
|
|
969
|
+
//为URL的pdf,合并位置处于左手边位置的情况,需要添加一个空白页; 注意原来的pdf文件很可能被直接添加一个空白首页?请备份好pdf源文件!
|
|
970
|
+
await this.createBlankFirst(nearDoubleMeta); //跟随后面的预算,倒推:为前面哪一个pdf末尾添加的空白页de!
|
|
971
|
+
if (nearDoubleMetaYema) counter++; //如果空白页也需要页码顺序配个页号的。 nearDoubleMeta它的页码编注情况:是否有连续总体编码 ?局部url内部编码的?不显示页脚的隐含编码吗
|
|
972
|
+
sumpages++; //为前面一个URL pdf增加空白页;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
sumpages += curUrlpages; //影响左右双面场景的位置
|
|
978
|
+
|
|
979
|
+
const previousCounter = counter;
|
|
980
|
+
if (meta?.frNo !== 0) {
|
|
981
|
+
if (meta.displayHeaderFooter !== undefined && !meta.displayHeaderFooter)
|
|
982
|
+
counter; //页眉页脚都没了,就不能计算进入页码合计数了!
|
|
983
|
+
else if (meta.localSumNo) counter; //整个URL有效的单独页码计数器的。
|
|
984
|
+
else {
|
|
985
|
+
if (meta.frNo === undefined) counter += curUrlpages;
|
|
986
|
+
else if (curUrlpages - meta?.frNo >= 0)
|
|
987
|
+
counter += curUrlpages - meta?.frNo + 1;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (meta.merge === undefined || meta.merge) {
|
|
991
|
+
nearDoubleMeta = meta; //往前面数最靠近的 merge !=false的哪一个URL
|
|
992
|
+
nearDoubleMetaYema = counter > previousCounter; //有真的编页码(影响汇总合并pdf的)
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
//合计数存储实例上;
|
|
996
|
+
this.sumNoPages = counter;
|
|
997
|
+
}
|
|
998
|
+
}
|