@karinjs/plugin-shotium 0.2.1 → 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 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
- 这里换成了「截一次,再把 PNG 按行切开」:不重新布局,也就不会出现分片之间样式对不上的情况。
46
- 切割直接在 zlib 层面做,不依赖任何图像库。
44
+ `multiPage` 在 puppeteer 那边是「改视窗高度重截几次」,这里交给引擎的
45
+ `screenshotTiles()`:文档只加载、布局、光栅化一次,引擎在光栅化的过程中按行切开、
46
+ 逐片编码,同一时刻只存在一片的位图。每一片都来自同一次布局,不会出现分片之间样式对不上的情况。
47
47
 
48
- 因为切割器只认 PNG,`multiPage` 配上 jpeg/webp 时会自动切到 png,并在日志里提示一次。
48
+ `multiPage: true` 用配置里的 `autoMultiPageHeight`,传数字就是这个数字,
49
+ 单位是 css 像素,上限 32000(引擎从一个滚动位置能画出的最大行数),超过会被夹到上限。
50
+ 最后一片是余数,所以片数是 `ceil(高度 / 单片高度)`。
49
51
 
50
- 切割要把整张图重压一遍,这一步在大图上并不便宜。1440x2541 那张图上,
51
- 同一份像素 deflate level 9 要 1.3s、level 6 要 0.48s、level 3 只要 0.19s,
52
- 而体积差别只有个位数百分比,所以默认用 level 3,可以通过 `sliceCompression` 调。
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.2.1",
9
+ version: "0.3.0",
10
10
  description: "karin \u7684 shotium \u622A\u56FE\u3001\u6E32\u67D3\u63D2\u4EF6",
11
11
  keywords: [
12
12
  "karin",
@@ -39,7 +39,7 @@ var package_default = {
39
39
  "test:watch": "vitest"
40
40
  },
41
41
  dependencies: {
42
- "@shotkit/shotium": "^0.3.3"
42
+ "@shotkit/shotium": "^0.4.0"
43
43
  },
44
44
  devDependencies: {
45
45
  "@types/express": "5.0.6",
@@ -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.2.1";
5
+ var version = "0.3.0";
6
6
  var description = "karin 的 shotium 截图、渲染插件";
7
7
  var keywords = [
8
8
  "karin",
@@ -35,7 +35,7 @@ var scripts = {
35
35
  "test:watch": "vitest"
36
36
  };
37
37
  var dependencies = {
38
- "@shotkit/shotium": "^0.3.3"
38
+ "@shotkit/shotium": "^0.4.0"
39
39
  };
40
40
  var devDependencies = {
41
41
  "@types/express": "5.0.6",
@@ -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 分片高度,不分片时返回 0
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
- * 把一张 PNG 按高度切成若干张 PNG
196
+ * 分片落盘时每一片的路径
213
197
  *
214
- * 与「改视窗高度重新截图」相比,这里是对同一次渲染结果做无损切割:
215
- * 不会重新布局,也就不会出现分片之间样式对不上的问题。
198
+ * 分片模式下没有「整张图」这个东西可以往 `path` 上写,所以按片编号存:
199
+ * 路径里写了 `{n}` 就替换它,没写就把 1 起的序号插在扩展名前面。
200
+ * 只有一片时按调用方给的路径原样存,退化成不分片时的行为。
216
201
  *
217
- * @param buf 原始 PNG
218
- * @param sliceHeight 每片高度(设备像素)
219
- * @param level deflate 级别 0-9,越低越快、体积越大
220
- * @returns 切好的 PNG 列表;无法处理时返回 null,由调用方回退
202
+ * @param file 调用方给的路径
203
+ * @param index 第几片,从 1 开始
204
+ * @param total 一共几片
205
+ * @returns 这一片的路径
221
206
  */
222
- declare const splitPng: (buf: Buffer, sliceHeight: number, level?: number) => Buffer[] | null;
207
+ declare const toTilePath: (file: string, index: number, total: number) => string;
223
208
 
224
- export { HMR_KEY, type LocalAccess, type PngInfo, type ShotiumConfig, UNSUPPORTED_KEYS, configPath, defaultConfig, getConfig, pickUnsupported, _package as pkg, pluginName, pluginVersion, readPngInfo, saveConfig, splitPng, toScreenshotOptions, toSliceHeight, toWaitUntil };
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-76RKNDUT.js';
2
- export { HMR_KEY, configPath, defaultConfig, getConfig, package_default as pkg, pluginName, pluginVersion, saveConfig } from './chunk-76RKNDUT.js';
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 path2 from 'node:path';
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: async (options) => {
62
- try {
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 = path2.resolve(file).split(path2.sep).join("/").replace(/^\/+/, "");
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 = path2.normalize(toFilePath(req.path));
283
- const ext = path2.extname(file).toLowerCase();
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(path2.resolve(file)).href;
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
- if (multiPage === true) return autoHeight;
381
- if (typeof multiPage === "number" && multiPage > 0) return multiPage;
382
- return 0;
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 = path2.join(karinPathHtml, "shotium");
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 = path2.join(stringHtmlDir, `${name || "render"}-${hash}.html`);
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) : path2.resolve(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
- if (sliceHeight > 0 && shot.type !== "png") {
460
- warnOnce(
461
- "multiPageType",
462
- `\u5206\u7247\u6E32\u67D3\u53EA\u80FD\u8F93\u51FA png\uFF0C\u672C\u6B21\u5DF2\u628A ${shot.type} \u5207\u6362\u4E3A png`
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
- shot.type = "png";
465
- delete shot.quality;
331
+ return images.map((image2) => image2.toString("base64"));
466
332
  }
467
- const result = await engine.screenshot({ ...shot, file: target, allowFileAccess });
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}] \u5206\u7247\u622A\u56FE\u5B8C\u6210 ${list.length} \u5F20 \u5927\u5C0F: ${logger.green(formatBytes(image.length))} \u8017\u65F6: ${logger.green(String(Date.now() - time))} ms${stats}`
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 list.map((item) => item.toString("base64"));
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, readPngInfo, splitPng, toScreenshotOptions, toSliceHeight, toWaitUntil };
348
+ export { MAX_TILE_HEIGHT, UNSUPPORTED_KEYS, pickUnsupported, toScreenshotOptions, toSliceHeight, toTilePath, toWaitUntil };
@@ -1,4 +1,4 @@
1
- import { package_default, getConfig, defaultConfig, saveConfig } from './chunk-76RKNDUT.js';
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 ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karinjs/plugin-shotium",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "karin 的 shotium 截图、渲染插件",
5
5
  "keywords": [
6
6
  "karin",
@@ -23,7 +23,7 @@
23
23
  "url": "git+https://github.com/KarinJS/plugin-shotium.git"
24
24
  },
25
25
  "dependencies": {
26
- "@shotkit/shotium": "^0.3.3"
26
+ "@shotkit/shotium": "^0.4.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/express": "5.0.6",