@karinjs/plugin-shotium 0.2.2 → 0.3.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/README.md +12 -10
- package/dist/{chunk-3BGX7L6U.js → chunk-YEDXVK5H.js} +1 -2
- package/dist/index.d.ts +20 -35
- package/dist/index.js +61 -211
- package/dist/web.config.js +2 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,20 +36,23 @@ karin 的渲染器就是一个函数:收下 `Options`,还回 base64。
|
|
|
36
36
|
| `pageGotoParams.waitUntil` | `pageGotoParams.waitUntil` | `networkidle0/2` 统一归到 `networkidle` |
|
|
37
37
|
| `pageGotoParams.timeout` | `pageGotoParams.timeout` | `0`(不超时) 回退到引擎默认值 |
|
|
38
38
|
| `headers` | `headers` | 只对同源子资源生效,见下 |
|
|
39
|
-
| `multiPage` |
|
|
39
|
+
| `multiPage` | `tile.height` | 走引擎的分片接口 `screenshotTiles()` |
|
|
40
40
|
| `waitForSelector` 等 | — | 引擎没有 JS 运行时,忽略并提示一次 |
|
|
41
41
|
|
|
42
42
|
### 分片渲染
|
|
43
43
|
|
|
44
|
-
`multiPage` 在 puppeteer
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
`multiPage` 在 puppeteer 那边是「改视窗高度重截几次」,这里交给引擎的
|
|
45
|
+
`screenshotTiles()`:文档只加载、布局、光栅化一次,引擎在光栅化的过程中按行切开、
|
|
46
|
+
逐片编码,同一时刻只存在一片的位图。每一片都来自同一次布局,不会出现分片之间样式对不上的情况。
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
`multiPage: true` 用配置里的 `autoMultiPageHeight`,传数字就是这个数字,
|
|
49
|
+
单位是 css 像素,上限 32000(引擎从一个滚动位置能画出的最大行数),超过会被夹到上限。
|
|
50
|
+
最后一片是余数,所以片数是 `ceil(高度 / 单片高度)`。
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
分片不再限定输出格式:png / jpeg / webp 都能分片,`quality` 照常生效。
|
|
53
|
+
|
|
54
|
+
分片模式下没有「整张图」可以往 `path` 上写,所以 `path` 按片编号存:路径里写了 `{n}`
|
|
55
|
+
就替换成 1 起的序号,没写就把序号插在扩展名前面(`a.png` → `a-1.png`、`a-2.png`)。
|
|
53
56
|
|
|
54
57
|
### 本地文件怎么交给引擎
|
|
55
58
|
|
|
@@ -79,8 +82,7 @@ karin 的渲染器就是一个函数:收下 `Options`,还回 base64。
|
|
|
79
82
|
| `viewport` | `800x600` | 默认视窗 |
|
|
80
83
|
| `scale` | `1` | 默认设备像素比 |
|
|
81
84
|
| `timeout` | `30000` | 默认导航超时 |
|
|
82
|
-
| `autoMultiPageHeight` | `4000` | `multiPage: true`
|
|
83
|
-
| `sliceCompression` | `3` | 分片重新编码的 deflate 级别 0-9 |
|
|
85
|
+
| `autoMultiPageHeight` | `4000` | `multiPage: true` 时每片的高度,css 像素,上限 32000 |
|
|
84
86
|
| `cacheDir` | `''` | HTTP 磁盘缓存目录,留空用引擎默认,填 `off` 关闭 |
|
|
85
87
|
| `cacheMaxBytes` | `256MB` | 缓存上限 |
|
|
86
88
|
| `userAgent` | `''` | 留空使用引擎内置 |
|
|
@@ -6,7 +6,7 @@ import { basePath } from 'node-karin/root';
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@karinjs/plugin-shotium",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.3.0",
|
|
10
10
|
description: "karin \u7684 shotium \u622A\u56FE\u3001\u6E32\u67D3\u63D2\u4EF6",
|
|
11
11
|
keywords: [
|
|
12
12
|
"karin",
|
|
@@ -79,7 +79,6 @@ var defaultConfig = {
|
|
|
79
79
|
scale: 1,
|
|
80
80
|
timeout: 3e4,
|
|
81
81
|
autoMultiPageHeight: 4e3,
|
|
82
|
-
sliceCompression: 3,
|
|
83
82
|
cacheDir: "",
|
|
84
83
|
cacheMaxBytes: 256 * 1024 * 1024,
|
|
85
84
|
userAgent: "",
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ScreenshotOptions } from '@shotkit/shotium';
|
|
|
2
2
|
import { Snapka } from 'node-karin';
|
|
3
3
|
|
|
4
4
|
var name = "@karinjs/plugin-shotium";
|
|
5
|
-
var version = "0.
|
|
5
|
+
var version = "0.3.0";
|
|
6
6
|
var description = "karin 的 shotium 截图、渲染插件";
|
|
7
7
|
var keywords = [
|
|
8
8
|
"karin",
|
|
@@ -114,16 +114,8 @@ interface ShotiumConfig {
|
|
|
114
114
|
scale: number;
|
|
115
115
|
/** 默认导航超时(ms) */
|
|
116
116
|
timeout: number;
|
|
117
|
-
/** `multiPage: true` 时单张分片的最大高度(css px) */
|
|
117
|
+
/** `multiPage: true` 时单张分片的最大高度(css px),上限 32000 */
|
|
118
118
|
autoMultiPageHeight: number;
|
|
119
|
-
/**
|
|
120
|
-
* 分片重新编码时的 deflate 级别 0-9
|
|
121
|
-
*
|
|
122
|
-
* 分片要把引擎给的整张 PNG 拆开重压一遍,这一步在大图上并不便宜:
|
|
123
|
-
* 同一份像素 level 9 要 1.3s、level 6 要 0.5s、level 3 只要 0.19s,
|
|
124
|
-
* 而体积差别只有个位数百分比。发到聊天里的图不值得为这点体积多等半秒。
|
|
125
|
-
*/
|
|
126
|
-
sliceCompression: number;
|
|
127
119
|
/** HTTP 磁盘缓存目录,留空使用引擎默认目录,填 `off` 关闭缓存 */
|
|
128
120
|
cacheDir: string;
|
|
129
121
|
/** HTTP 磁盘缓存上限(字节) */
|
|
@@ -186,39 +178,32 @@ declare const toScreenshotOptions: (options: Snapka, config: ShotiumConfig) => O
|
|
|
186
178
|
* @returns 选项名列表
|
|
187
179
|
*/
|
|
188
180
|
declare const pickUnsupported: (options: Snapka) => string[];
|
|
181
|
+
/**
|
|
182
|
+
* 引擎一次能从同一个滚动位置画出的最大 css 行数,也是 `tile.height` 的上限
|
|
183
|
+
*
|
|
184
|
+
* 超过这个值引擎会直接拒绝整个请求,所以这里夹住而不是原样传过去:
|
|
185
|
+
* 调用方要的是「一片别太高」,不是「宁可不出图」。
|
|
186
|
+
*/
|
|
187
|
+
declare const MAX_TILE_HEIGHT = 32000;
|
|
189
188
|
/**
|
|
190
189
|
* 计算每一片的高度(css px)
|
|
191
190
|
* @param multiPage karin 的 multiPage 参数
|
|
192
191
|
* @param autoHeight `true` 时使用的默认高度
|
|
193
|
-
* @returns
|
|
192
|
+
* @returns 分片高度,1 到 32000 之间的整数;不分片时返回 0
|
|
194
193
|
*/
|
|
195
194
|
declare const toSliceHeight: (multiPage: number | boolean | undefined, autoHeight: number) => number;
|
|
196
|
-
|
|
197
|
-
/** PNG 基本信息 */
|
|
198
|
-
interface PngInfo {
|
|
199
|
-
width: number;
|
|
200
|
-
height: number;
|
|
201
|
-
bitDepth: number;
|
|
202
|
-
colorType: number;
|
|
203
|
-
interlace: number;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* 读取 PNG 的宽高等基本信息
|
|
207
|
-
* @param buf PNG 数据
|
|
208
|
-
* @returns 基本信息,不是合法 PNG 时返回 null
|
|
209
|
-
*/
|
|
210
|
-
declare const readPngInfo: (buf: Buffer) => PngInfo | null;
|
|
211
195
|
/**
|
|
212
|
-
*
|
|
196
|
+
* 分片落盘时每一片的路径
|
|
213
197
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
198
|
+
* 分片模式下没有「整张图」这个东西可以往 `path` 上写,所以按片编号存:
|
|
199
|
+
* 路径里写了 `{n}` 就替换它,没写就把 1 起的序号插在扩展名前面。
|
|
200
|
+
* 只有一片时按调用方给的路径原样存,退化成不分片时的行为。
|
|
216
201
|
*
|
|
217
|
-
* @param
|
|
218
|
-
* @param
|
|
219
|
-
* @param
|
|
220
|
-
* @returns
|
|
202
|
+
* @param file 调用方给的路径
|
|
203
|
+
* @param index 第几片,从 1 开始
|
|
204
|
+
* @param total 一共几片
|
|
205
|
+
* @returns 这一片的路径
|
|
221
206
|
*/
|
|
222
|
-
declare const
|
|
207
|
+
declare const toTilePath: (file: string, index: number, total: number) => string;
|
|
223
208
|
|
|
224
|
-
export { HMR_KEY, type LocalAccess,
|
|
209
|
+
export { HMR_KEY, type LocalAccess, MAX_TILE_HEIGHT, type ShotiumConfig, UNSUPPORTED_KEYS, configPath, defaultConfig, getConfig, pickUnsupported, _package as pkg, pluginName, pluginVersion, saveConfig, toScreenshotOptions, toSliceHeight, toTilePath, toWaitUntil };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { getConfig, HMR_KEY, pluginName, pluginVersion } from './chunk-
|
|
2
|
-
export { HMR_KEY, configPath, defaultConfig, getConfig, package_default as pkg, pluginName, pluginVersion, saveConfig } from './chunk-
|
|
1
|
+
import { getConfig, HMR_KEY, pluginName, pluginVersion } from './chunk-YEDXVK5H.js';
|
|
2
|
+
export { HMR_KEY, configPath, defaultConfig, getConfig, package_default as pkg, pluginName, pluginVersion, saveConfig } from './chunk-YEDXVK5H.js';
|
|
3
3
|
import fs2 from 'node:fs';
|
|
4
|
-
import
|
|
4
|
+
import path3 from 'node:path';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
import { karin, logger, registerRender, renderTpl, app } from 'node-karin';
|
|
8
8
|
import { karinPathHtml } from 'node-karin/root';
|
|
9
|
-
import { start, status, stop, screenshot, daemon } from '@shotkit/shotium';
|
|
10
|
-
import zlib from 'node:zlib';
|
|
9
|
+
import { start, status, stop, screenshotTiles, screenshot, daemon } from '@shotkit/shotium';
|
|
11
10
|
|
|
12
11
|
var toStartOptions = (config) => {
|
|
13
12
|
const options = {
|
|
@@ -29,6 +28,7 @@ var createInprocessEngine = (config) => {
|
|
|
29
28
|
);
|
|
30
29
|
return {
|
|
31
30
|
screenshot: (options) => screenshot(options),
|
|
31
|
+
screenshotTiles: (options) => screenshotTiles(options),
|
|
32
32
|
close: async () => {
|
|
33
33
|
if (status().running) await stop();
|
|
34
34
|
}
|
|
@@ -57,15 +57,17 @@ var createDaemonEngine = (config) => {
|
|
|
57
57
|
}
|
|
58
58
|
return pending;
|
|
59
59
|
};
|
|
60
|
+
const call = async (run) => {
|
|
61
|
+
try {
|
|
62
|
+
return await run(await connect());
|
|
63
|
+
} catch {
|
|
64
|
+
client = null;
|
|
65
|
+
return await run(await connect());
|
|
66
|
+
}
|
|
67
|
+
};
|
|
60
68
|
return {
|
|
61
|
-
screenshot:
|
|
62
|
-
|
|
63
|
-
return await (await connect()).screenshot(options);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
client = null;
|
|
66
|
-
return await (await connect()).screenshot(options);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
+
screenshot: (options) => call((connected) => connected.screenshot(options)),
|
|
70
|
+
screenshotTiles: (options) => call((connected) => connected.screenshotTiles(options)),
|
|
69
71
|
close: async () => {
|
|
70
72
|
client?.close();
|
|
71
73
|
client = null;
|
|
@@ -75,165 +77,6 @@ var createDaemonEngine = (config) => {
|
|
|
75
77
|
var createEngine = (config) => {
|
|
76
78
|
return config.mode === "daemon" ? createDaemonEngine(config) : createInprocessEngine(config);
|
|
77
79
|
};
|
|
78
|
-
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
79
|
-
var CHANNELS = {
|
|
80
|
-
0: 1,
|
|
81
|
-
// 灰度
|
|
82
|
-
2: 3,
|
|
83
|
-
// RGB
|
|
84
|
-
4: 2,
|
|
85
|
-
// 灰度 + alpha
|
|
86
|
-
6: 4
|
|
87
|
-
// RGBA
|
|
88
|
-
};
|
|
89
|
-
var CRC_TABLE = (() => {
|
|
90
|
-
const table = new Int32Array(256);
|
|
91
|
-
for (let n = 0; n < 256; n++) {
|
|
92
|
-
let c = n;
|
|
93
|
-
for (let k = 0; k < 8; k++) {
|
|
94
|
-
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
95
|
-
}
|
|
96
|
-
table[n] = c;
|
|
97
|
-
}
|
|
98
|
-
return table;
|
|
99
|
-
})();
|
|
100
|
-
var crc32 = (buf) => {
|
|
101
|
-
let c = -1;
|
|
102
|
-
for (let i = 0; i < buf.length; i++) {
|
|
103
|
-
c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
|
|
104
|
-
}
|
|
105
|
-
return (c ^ -1) >>> 0;
|
|
106
|
-
};
|
|
107
|
-
var readChunks = (buf) => {
|
|
108
|
-
if (buf.length < 8 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
|
|
109
|
-
const chunks = [];
|
|
110
|
-
let offset = 8;
|
|
111
|
-
while (offset + 8 <= buf.length) {
|
|
112
|
-
const length = buf.readUInt32BE(offset);
|
|
113
|
-
const type = buf.toString("ascii", offset + 4, offset + 8);
|
|
114
|
-
const start2 = offset + 8;
|
|
115
|
-
const end = start2 + length;
|
|
116
|
-
if (end + 4 > buf.length) return null;
|
|
117
|
-
chunks.push({ type, data: buf.subarray(start2, end) });
|
|
118
|
-
offset = end + 4;
|
|
119
|
-
if (type === "IEND") break;
|
|
120
|
-
}
|
|
121
|
-
return chunks;
|
|
122
|
-
};
|
|
123
|
-
var writeChunk = (type, data) => {
|
|
124
|
-
const length = Buffer.alloc(4);
|
|
125
|
-
length.writeUInt32BE(data.length, 0);
|
|
126
|
-
const typeAndData = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
127
|
-
const crc = Buffer.alloc(4);
|
|
128
|
-
crc.writeUInt32BE(crc32(typeAndData), 0);
|
|
129
|
-
return Buffer.concat([length, typeAndData, crc]);
|
|
130
|
-
};
|
|
131
|
-
var readPngInfo = (buf) => {
|
|
132
|
-
if (buf.length < 33 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
|
|
133
|
-
if (buf.toString("ascii", 12, 16) !== "IHDR") return null;
|
|
134
|
-
return {
|
|
135
|
-
width: buf.readUInt32BE(16),
|
|
136
|
-
height: buf.readUInt32BE(20),
|
|
137
|
-
bitDepth: buf[24],
|
|
138
|
-
colorType: buf[25],
|
|
139
|
-
interlace: buf[28]
|
|
140
|
-
};
|
|
141
|
-
};
|
|
142
|
-
var paeth = (a, b, c) => {
|
|
143
|
-
const p = a + b - c;
|
|
144
|
-
const pa = Math.abs(p - a);
|
|
145
|
-
const pb = Math.abs(p - b);
|
|
146
|
-
const pc = Math.abs(p - c);
|
|
147
|
-
if (pa <= pb && pa <= pc) return a;
|
|
148
|
-
if (pb <= pc) return b;
|
|
149
|
-
return c;
|
|
150
|
-
};
|
|
151
|
-
var unfilter = (filtered, width, height, bpp) => {
|
|
152
|
-
const stride = width * bpp;
|
|
153
|
-
const out = Buffer.alloc(stride * height);
|
|
154
|
-
for (let y = 0; y < height; y++) {
|
|
155
|
-
const filterType = filtered[y * (stride + 1)];
|
|
156
|
-
const src = y * (stride + 1) + 1;
|
|
157
|
-
const dst = y * stride;
|
|
158
|
-
const up = dst - stride;
|
|
159
|
-
for (let x = 0; x < stride; x++) {
|
|
160
|
-
const raw = filtered[src + x];
|
|
161
|
-
const a = x >= bpp ? out[dst + x - bpp] : 0;
|
|
162
|
-
const b = y > 0 ? out[up + x] : 0;
|
|
163
|
-
const c = x >= bpp && y > 0 ? out[up + x - bpp] : 0;
|
|
164
|
-
let value;
|
|
165
|
-
switch (filterType) {
|
|
166
|
-
case 0:
|
|
167
|
-
value = raw;
|
|
168
|
-
break;
|
|
169
|
-
case 1:
|
|
170
|
-
value = raw + a;
|
|
171
|
-
break;
|
|
172
|
-
case 2:
|
|
173
|
-
value = raw + b;
|
|
174
|
-
break;
|
|
175
|
-
case 3:
|
|
176
|
-
value = raw + (a + b >> 1);
|
|
177
|
-
break;
|
|
178
|
-
case 4:
|
|
179
|
-
value = raw + paeth(a, b, c);
|
|
180
|
-
break;
|
|
181
|
-
default:
|
|
182
|
-
value = raw;
|
|
183
|
-
}
|
|
184
|
-
out[dst + x] = value & 255;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return out;
|
|
188
|
-
};
|
|
189
|
-
var splitPng = (buf, sliceHeight, level = 3) => {
|
|
190
|
-
const info = readPngInfo(buf);
|
|
191
|
-
if (!info) return null;
|
|
192
|
-
if (info.interlace !== 0 || info.bitDepth !== 8) return null;
|
|
193
|
-
const channels = CHANNELS[info.colorType];
|
|
194
|
-
if (!channels) return null;
|
|
195
|
-
const height = Math.max(1, Math.floor(sliceHeight));
|
|
196
|
-
if (height >= info.height) return [buf];
|
|
197
|
-
const chunks = readChunks(buf);
|
|
198
|
-
if (!chunks) return null;
|
|
199
|
-
const idat = chunks.filter((item) => item.type === "IDAT").map((item) => item.data);
|
|
200
|
-
if (idat.length === 0) return null;
|
|
201
|
-
let raw;
|
|
202
|
-
try {
|
|
203
|
-
raw = zlib.inflateSync(Buffer.concat(idat));
|
|
204
|
-
} catch {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
const stride = info.width * channels;
|
|
208
|
-
if (raw.length < (stride + 1) * info.height) return null;
|
|
209
|
-
const pixels = unfilter(raw, info.width, info.height, channels);
|
|
210
|
-
const extras = chunks.filter((item) => ["PLTE", "tRNS", "gAMA", "sRGB", "cHRM", "iCCP", "pHYs"].includes(item.type));
|
|
211
|
-
const list = [];
|
|
212
|
-
for (let top = 0; top < info.height; top += height) {
|
|
213
|
-
const sliceRows = Math.min(height, info.height - top);
|
|
214
|
-
const body = Buffer.alloc((stride + 1) * sliceRows);
|
|
215
|
-
for (let y = 0; y < sliceRows; y++) {
|
|
216
|
-
body[y * (stride + 1)] = 0;
|
|
217
|
-
pixels.copy(body, y * (stride + 1) + 1, (top + y) * stride, (top + y + 1) * stride);
|
|
218
|
-
}
|
|
219
|
-
const ihdr = Buffer.alloc(13);
|
|
220
|
-
ihdr.writeUInt32BE(info.width, 0);
|
|
221
|
-
ihdr.writeUInt32BE(sliceRows, 4);
|
|
222
|
-
ihdr[8] = info.bitDepth;
|
|
223
|
-
ihdr[9] = info.colorType;
|
|
224
|
-
ihdr[10] = 0;
|
|
225
|
-
ihdr[11] = 0;
|
|
226
|
-
ihdr[12] = 0;
|
|
227
|
-
list.push(Buffer.concat([
|
|
228
|
-
PNG_SIGNATURE,
|
|
229
|
-
writeChunk("IHDR", ihdr),
|
|
230
|
-
...extras.map((item) => writeChunk(item.type, item.data)),
|
|
231
|
-
writeChunk("IDAT", zlib.deflateSync(body, { level })),
|
|
232
|
-
writeChunk("IEND", Buffer.alloc(0))
|
|
233
|
-
]));
|
|
234
|
-
}
|
|
235
|
-
return list;
|
|
236
|
-
};
|
|
237
80
|
var ALLOWED_EXT = {
|
|
238
81
|
".html": "text/html; charset=utf-8",
|
|
239
82
|
".htm": "text/html; charset=utf-8",
|
|
@@ -262,7 +105,7 @@ var token = crypto.randomBytes(16).toString("hex");
|
|
|
262
105
|
var mountPath = `/shotium/${token}/fs`;
|
|
263
106
|
var mounted = false;
|
|
264
107
|
var toUrlPath = (file) => {
|
|
265
|
-
const posix =
|
|
108
|
+
const posix = path3.resolve(file).split(path3.sep).join("/").replace(/^\/+/, "");
|
|
266
109
|
return `${mountPath}/${posix.split("/").map(encodeURIComponent).join("/")}`;
|
|
267
110
|
};
|
|
268
111
|
var toFilePath = (urlPath) => {
|
|
@@ -279,8 +122,8 @@ var rewriteFileUrls = (text) => {
|
|
|
279
122
|
});
|
|
280
123
|
};
|
|
281
124
|
var handler = (req, res) => {
|
|
282
|
-
const file =
|
|
283
|
-
const ext =
|
|
125
|
+
const file = path3.normalize(toFilePath(req.path));
|
|
126
|
+
const ext = path3.extname(file).toLowerCase();
|
|
284
127
|
if (file.includes("..")) {
|
|
285
128
|
res.status(403).end("forbidden");
|
|
286
129
|
return;
|
|
@@ -315,9 +158,7 @@ var toBridgeUrl = (file) => {
|
|
|
315
158
|
const port = process.env.HTTP_PORT || "7777";
|
|
316
159
|
return `http://127.0.0.1:${port}${toUrlPath(file)}`;
|
|
317
160
|
};
|
|
318
|
-
var toFileUrl = (file) => pathToFileURL(
|
|
319
|
-
|
|
320
|
-
// src/convert.ts
|
|
161
|
+
var toFileUrl = (file) => pathToFileURL(path3.resolve(file)).href;
|
|
321
162
|
var UNSUPPORTED_KEYS = [
|
|
322
163
|
"waitForSelector",
|
|
323
164
|
"waitForFunction",
|
|
@@ -376,15 +217,22 @@ var pickUnsupported = (options) => {
|
|
|
376
217
|
return Array.isArray(value) ? value.length > 0 : !!value;
|
|
377
218
|
});
|
|
378
219
|
};
|
|
220
|
+
var MAX_TILE_HEIGHT = 32e3;
|
|
379
221
|
var toSliceHeight = (multiPage, autoHeight) => {
|
|
380
|
-
|
|
381
|
-
if (
|
|
382
|
-
return
|
|
222
|
+
const height = multiPage === true ? autoHeight : typeof multiPage === "number" ? multiPage : 0;
|
|
223
|
+
if (!(height > 0)) return 0;
|
|
224
|
+
return Math.min(MAX_TILE_HEIGHT, Math.max(1, Math.round(height)));
|
|
225
|
+
};
|
|
226
|
+
var toTilePath = (file, index, total) => {
|
|
227
|
+
if (file.includes("{n}")) return file.split("{n}").join(String(index));
|
|
228
|
+
if (total === 1) return file;
|
|
229
|
+
const ext = path3.extname(file);
|
|
230
|
+
return `${file.slice(0, file.length - ext.length)}-${index}${ext}`;
|
|
383
231
|
};
|
|
384
232
|
|
|
385
233
|
// src/index.ts
|
|
386
234
|
var RENDER_ID = "@karinjs/plugin-shotium";
|
|
387
|
-
var stringHtmlDir =
|
|
235
|
+
var stringHtmlDir = path3.join(karinPathHtml, "shotium");
|
|
388
236
|
var warned = /* @__PURE__ */ new Set();
|
|
389
237
|
var warnOnce = (key, message) => {
|
|
390
238
|
if (warned.has(key)) return;
|
|
@@ -394,7 +242,7 @@ var warnOnce = (key, message) => {
|
|
|
394
242
|
var writeHtmlString = (html, name) => {
|
|
395
243
|
fs2.mkdirSync(stringHtmlDir, { recursive: true });
|
|
396
244
|
const hash = crypto.createHash("md5").update(html).digest("hex").slice(0, 8);
|
|
397
|
-
const file =
|
|
245
|
+
const file = path3.join(stringHtmlDir, `${name || "render"}-${hash}.html`);
|
|
398
246
|
if (!fs2.existsSync(file)) fs2.writeFileSync(file, html);
|
|
399
247
|
return file;
|
|
400
248
|
};
|
|
@@ -402,7 +250,7 @@ var resolveTarget = (file, config, hasHeaders) => {
|
|
|
402
250
|
if (/^https?:\/\//.test(file)) {
|
|
403
251
|
return { target: file, allowFileAccess: false };
|
|
404
252
|
}
|
|
405
|
-
const local = file.startsWith("file:") ? fileURLToPath(file) :
|
|
253
|
+
const local = file.startsWith("file:") ? fileURLToPath(file) : path3.resolve(file);
|
|
406
254
|
const wantHttp = config.localAccess === "http" || config.localAccess === "auto" && hasHeaders;
|
|
407
255
|
if (wantHttp) {
|
|
408
256
|
if (isBridgeAvailable()) {
|
|
@@ -419,6 +267,15 @@ var formatBytes = (bytes) => {
|
|
|
419
267
|
const value = bytes / Math.pow(1024, i);
|
|
420
268
|
return `${i === 0 ? Math.round(value) : value.toFixed(2)} ${units[i]}`;
|
|
421
269
|
};
|
|
270
|
+
var formatStats = (stats, enabled) => {
|
|
271
|
+
if (!enabled) return "";
|
|
272
|
+
return ` \u7F51\u7EDC: ${stats.requests}(\u7F13\u5B58 ${stats.fromCache}/\u5931\u8D25 ${stats.failed}) \u5F15\u64CE: ${stats.timing.total.toFixed(1)}ms`;
|
|
273
|
+
};
|
|
274
|
+
var writeImage = (file, image) => {
|
|
275
|
+
const target = path3.resolve(file);
|
|
276
|
+
fs2.mkdirSync(path3.dirname(target), { recursive: true });
|
|
277
|
+
fs2.writeFileSync(target, image);
|
|
278
|
+
};
|
|
422
279
|
var main = async () => {
|
|
423
280
|
let config = getConfig();
|
|
424
281
|
let engine = createEngine(config);
|
|
@@ -456,38 +313,31 @@ var main = async () => {
|
|
|
456
313
|
const { target, allowFileAccess } = resolveTarget(data.file, config, hasHeaders);
|
|
457
314
|
const shot = toScreenshotOptions(data, config);
|
|
458
315
|
const sliceHeight = toSliceHeight(data.multiPage, config.autoMultiPageHeight);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
316
|
+
const name = path3.basename(data.file_name || data.file || "unknown");
|
|
317
|
+
const request = { ...shot, file: target, allowFileAccess };
|
|
318
|
+
if (sliceHeight > 0) {
|
|
319
|
+
const result2 = await engine.screenshotTiles({ ...request, tile: { height: sliceHeight } });
|
|
320
|
+
const images = result2.tiles.map((tile) => tile.image).filter((image2) => !!image2);
|
|
321
|
+
if (images.length === 0) throw new Error(`[${pluginName}] \u5F15\u64CE\u6CA1\u6709\u8FD4\u56DE\u56FE\u7247\u6570\u636E`);
|
|
322
|
+
if (data.path) {
|
|
323
|
+
images.forEach((image2, index) => {
|
|
324
|
+
writeImage(toTilePath(data.path, index + 1, images.length), image2);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const bytes = images.reduce((total, image2) => total + image2.length, 0);
|
|
328
|
+
logger.info(
|
|
329
|
+
`[${RENDER_ID}][${name}] \u5206\u7247\u622A\u56FE\u5B8C\u6210 ${images.length} \u5F20 \u5927\u5C0F: ${logger.green(formatBytes(bytes))} \u8017\u65F6: ${logger.green(String(Date.now() - time))} ms` + formatStats(result2.stats, config.logStats)
|
|
463
330
|
);
|
|
464
|
-
|
|
465
|
-
delete shot.quality;
|
|
331
|
+
return images.map((image2) => image2.toString("base64"));
|
|
466
332
|
}
|
|
467
|
-
const result = await engine.screenshot(
|
|
333
|
+
const result = await engine.screenshot(request);
|
|
468
334
|
const image = result.image;
|
|
469
335
|
if (!image) throw new Error(`[${pluginName}] \u5F15\u64CE\u6CA1\u6709\u8FD4\u56DE\u56FE\u7247\u6570\u636E`);
|
|
470
|
-
if (data.path)
|
|
471
|
-
fs2.mkdirSync(path2.dirname(path2.resolve(data.path)), { recursive: true });
|
|
472
|
-
fs2.writeFileSync(path2.resolve(data.path), image);
|
|
473
|
-
}
|
|
474
|
-
const name = path2.basename(data.file_name || data.file || "unknown");
|
|
475
|
-
const stats = config.logStats ? ` \u7F51\u7EDC: ${result.stats.requests}(\u7F13\u5B58 ${result.stats.fromCache}/\u5931\u8D25 ${result.stats.failed}) \u5F15\u64CE: ${result.stats.timing.total.toFixed(1)}ms` : "";
|
|
476
|
-
if (sliceHeight === 0) {
|
|
477
|
-
logger.info(
|
|
478
|
-
`[${RENDER_ID}][${name}] \u622A\u56FE\u5B8C\u6210 \u5927\u5C0F: ${logger.green(formatBytes(image.length))} \u8017\u65F6: ${logger.green(String(Date.now() - time))} ms${stats}`
|
|
479
|
-
);
|
|
480
|
-
return image.toString("base64");
|
|
481
|
-
}
|
|
482
|
-
const list = splitPng(image, Math.round(sliceHeight * (shot.scale ?? 1)), config.sliceCompression);
|
|
483
|
-
if (!list) {
|
|
484
|
-
warnOnce("split", "\u5F53\u524D\u56FE\u7247\u683C\u5F0F\u65E0\u6CD5\u5206\u7247\uFF0C\u5DF2\u6309\u6574\u5F20\u8FD4\u56DE");
|
|
485
|
-
return [image.toString("base64")];
|
|
486
|
-
}
|
|
336
|
+
if (data.path) writeImage(data.path, image);
|
|
487
337
|
logger.info(
|
|
488
|
-
`[${RENDER_ID}][${name}] \
|
|
338
|
+
`[${RENDER_ID}][${name}] \u622A\u56FE\u5B8C\u6210 \u5927\u5C0F: ${logger.green(formatBytes(image.length))} \u8017\u65F6: ${logger.green(String(Date.now() - time))} ms` + formatStats(result.stats, config.logStats)
|
|
489
339
|
);
|
|
490
|
-
return
|
|
340
|
+
return image.toString("base64");
|
|
491
341
|
});
|
|
492
342
|
logger.info(
|
|
493
343
|
`${logger.violet(`[\u63D2\u4EF6:${pluginVersion}]`)} ${logger.green(pluginName)} \u521D\u59CB\u5316\u5B8C\u6210~`
|
|
@@ -495,4 +345,4 @@ var main = async () => {
|
|
|
495
345
|
};
|
|
496
346
|
main();
|
|
497
347
|
|
|
498
|
-
export { UNSUPPORTED_KEYS, pickUnsupported,
|
|
348
|
+
export { MAX_TILE_HEIGHT, UNSUPPORTED_KEYS, pickUnsupported, toScreenshotOptions, toSliceHeight, toTilePath, toWaitUntil };
|
package/dist/web.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { package_default, getConfig, defaultConfig, saveConfig } from './chunk-
|
|
1
|
+
import { package_default, getConfig, defaultConfig, saveConfig } from './chunk-YEDXVK5H.js';
|
|
2
2
|
import { components } from 'node-karin';
|
|
3
3
|
|
|
4
4
|
var toNumber = (value, fallback) => {
|
|
@@ -86,14 +86,9 @@ var webConfig = {
|
|
|
86
86
|
}),
|
|
87
87
|
components.input.number("autoMultiPageHeight", {
|
|
88
88
|
label: "\u81EA\u52A8\u5206\u7247\u9AD8\u5EA6",
|
|
89
|
-
description: "\u8C03\u7528\u65B9\u4F20 multiPage: true \u65F6\uFF0C\u6BCF\u7247\u7684\u9AD8\u5EA6(css \u50CF\u7D20)",
|
|
89
|
+
description: "\u8C03\u7528\u65B9\u4F20 multiPage: true \u65F6\uFF0C\u6BCF\u7247\u7684\u9AD8\u5EA6(css \u50CF\u7D20)\uFF0C\u4E0A\u9650 32000",
|
|
90
90
|
defaultValue: String(config.autoMultiPageHeight)
|
|
91
91
|
}),
|
|
92
|
-
components.input.number("sliceCompression", {
|
|
93
|
-
label: "\u5206\u7247\u91CD\u65B0\u7F16\u7801\u7684\u538B\u7F29\u7EA7\u522B",
|
|
94
|
-
description: "0-9\u3002\u5206\u7247\u8981\u628A\u6574\u5F20\u56FE\u62C6\u5F00\u91CD\u538B\u4E00\u904D\uFF0C\u7EA7\u522B\u8D8A\u4F4E\u8D8A\u5FEB\u3001\u4F53\u79EF\u8D8A\u5927\uFF0C3 \u662F\u6298\u4E2D\u503C",
|
|
95
|
-
defaultValue: String(config.sliceCompression)
|
|
96
|
-
}),
|
|
97
92
|
components.divider.create("divider1"),
|
|
98
93
|
components.input.string("cacheDir", {
|
|
99
94
|
label: "HTTP \u7F13\u5B58\u76EE\u5F55",
|
|
@@ -140,7 +135,6 @@ var webConfig = {
|
|
|
140
135
|
scale: toNumber(form.scale, current.scale),
|
|
141
136
|
timeout: toNumber(form.timeout, current.timeout),
|
|
142
137
|
autoMultiPageHeight: toNumber(form.autoMultiPageHeight, defaultConfig.autoMultiPageHeight),
|
|
143
|
-
sliceCompression: Math.min(9, Math.max(0, toNumber(form.sliceCompression, defaultConfig.sliceCompression))),
|
|
144
138
|
cacheDir,
|
|
145
139
|
cacheMaxBytes: toNumber(form.cacheMaxBytes, current.cacheMaxBytes),
|
|
146
140
|
userAgent: String(form.userAgent ?? ""),
|