@afixt/screenshot-utils 0.9.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/CHANGELOG.md +39 -0
- package/LICENSE +13 -0
- package/README.md +132 -0
- package/bin/afixt-screenshot +14 -0
- package/dist/index.js +2797 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2795 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +164 -0
- package/src/adapters/cdp.js +116 -0
- package/src/adapters/index.js +17 -0
- package/src/adapters/playwright.js +150 -0
- package/src/adapters/puppeteer.js +128 -0
- package/src/capture/element.js +257 -0
- package/src/capture/index.js +17 -0
- package/src/capture/long-page.js +267 -0
- package/src/capture/page.js +207 -0
- package/src/capture/responsive.js +94 -0
- package/src/capture/settle.js +182 -0
- package/src/capture/viewport.js +21 -0
- package/src/cli/index.js +266 -0
- package/src/compose/annotate.js +283 -0
- package/src/compose/diff.js +173 -0
- package/src/compose/heatmap.js +159 -0
- package/src/compose/index.js +8 -0
- package/src/compose/theme.js +45 -0
- package/src/errors/adapter.js +65 -0
- package/src/errors/base.js +48 -0
- package/src/errors/capture.js +175 -0
- package/src/errors/compose.js +78 -0
- package/src/errors/index.js +75 -0
- package/src/errors/optional.js +37 -0
- package/src/errors/redaction.js +42 -0
- package/src/errors/tile.js +61 -0
- package/src/errors/transform.js +78 -0
- package/src/index.js +69 -0
- package/src/redact/index.js +6 -0
- package/src/redact/policy.js +67 -0
- package/src/redact/redact.js +268 -0
- package/src/tile/dzi.js +268 -0
- package/src/tile/index.js +5 -0
- package/src/transform/convert.js +54 -0
- package/src/transform/crop.js +251 -0
- package/src/transform/index.js +8 -0
- package/src/transform/normalize.js +83 -0
- package/src/transform/resize.js +99 -0
- package/src/types/index.d.ts +432 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2795 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// package.json
|
|
13
|
+
var require_package = __commonJS({
|
|
14
|
+
"package.json"(exports, module) {
|
|
15
|
+
module.exports = {
|
|
16
|
+
name: "@afixt/screenshot-utils",
|
|
17
|
+
version: "0.9.0",
|
|
18
|
+
description: "Screenshot capture, transform, compose, tile, and redact utilities for the AFixt/Revenant accessibility platform",
|
|
19
|
+
type: "commonjs",
|
|
20
|
+
main: "./dist/index.js",
|
|
21
|
+
module: "./dist/index.mjs",
|
|
22
|
+
types: "./dist/types/index.d.ts",
|
|
23
|
+
exports: {
|
|
24
|
+
".": {
|
|
25
|
+
types: "./dist/types/index.d.ts",
|
|
26
|
+
import: "./dist/index.mjs",
|
|
27
|
+
require: "./dist/index.js",
|
|
28
|
+
default: "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
files: [
|
|
33
|
+
"bin",
|
|
34
|
+
"src",
|
|
35
|
+
"dist",
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
bin: {
|
|
40
|
+
"afixt-screenshot": "./bin/afixt-screenshot"
|
|
41
|
+
},
|
|
42
|
+
scripts: {
|
|
43
|
+
build: "npm-run-all -p build:bundle build:types",
|
|
44
|
+
"build:bundle": "tsup",
|
|
45
|
+
"build:types": "tsc -p tsconfig.json",
|
|
46
|
+
"build:clean": "rm -rf dist",
|
|
47
|
+
prebuild: "npm run build:clean",
|
|
48
|
+
test: "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"test:coverage": "vitest run --coverage",
|
|
51
|
+
"test:single": "vitest run --testNamePattern",
|
|
52
|
+
"test:perf": "vitest run --config vitest.perf.config.js",
|
|
53
|
+
lint: "eslint . --max-warnings 0",
|
|
54
|
+
"lint:fix": "eslint . --fix --max-warnings 0",
|
|
55
|
+
format: "prettier --write .",
|
|
56
|
+
"format:check": "prettier --check .",
|
|
57
|
+
"lint:md": "markdownlint '**/*.md' --ignore node_modules --ignore scratchpads --ignore dist --ignore docs --ignore CHANGELOG.md --ignore spec.md",
|
|
58
|
+
"lint:cpd": "jscpd . --config .jscpd.json",
|
|
59
|
+
"lint:cpd:ci": "jscpd . --config .jscpd.json --threshold 5",
|
|
60
|
+
"security:audit": "npm audit",
|
|
61
|
+
"security:check": "npm audit --audit-level=high",
|
|
62
|
+
"security:check:prod": "npm audit --omit=dev --audit-level=high",
|
|
63
|
+
"security:osv": "osv-scanner --config=.osv-scanner.toml --lockfile=package-lock.json",
|
|
64
|
+
"security:semgrep": "semgrep --config=p/javascript --config=p/nodejsscan --config=p/owasp-top-ten --config=p/secrets --error --metrics=off",
|
|
65
|
+
"security:secrets": "trufflehog git file://. --only-verified --fail --no-update",
|
|
66
|
+
"security:secrets:staged": "trufflehog git file://. --since-commit HEAD --only-verified --fail --no-update",
|
|
67
|
+
"license:check": "license-checker-rseidelsohn --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD-2-Clause;BSD-3-Clause;0BSD;CC0-1.0;Unlicense;Python-2.0;WTFPL;BlueOak-1.0.0;LGPL-3.0-or-later;LGPL-3.0' --excludePackagesStartingWith '@afixt/' --summary",
|
|
68
|
+
"license:report": "license-checker-rseidelsohn --production --csv --out reports/licenses.csv --excludePackagesStartingWith '@afixt/'",
|
|
69
|
+
links: "lychee --no-progress --exclude 'github\\.com/AFixt/' '**/*.md' --exclude-path 'node_modules/**' --exclude-path 'dist/**' --exclude-path 'docs/**' --exclude-path 'scratchpads/**'",
|
|
70
|
+
docs: "jsdoc -c jsdoc.json",
|
|
71
|
+
"docs:serve": "npx http-server ./docs",
|
|
72
|
+
check: "npm-run-all -p lint format:check lint:md lint:cpd:ci",
|
|
73
|
+
"check:all": "npm-run-all -s check test security:check:prod license:check",
|
|
74
|
+
release: "standard-version",
|
|
75
|
+
prepare: "husky"
|
|
76
|
+
},
|
|
77
|
+
"lint-staged": {
|
|
78
|
+
"*.{js,mjs,cjs}": [
|
|
79
|
+
"eslint --fix",
|
|
80
|
+
"prettier --write"
|
|
81
|
+
],
|
|
82
|
+
"*.{json,md,yml,yaml}": [
|
|
83
|
+
"prettier --write"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
repository: {
|
|
87
|
+
type: "git",
|
|
88
|
+
url: "git+ssh://git@github.com/AFixt/screenshot-utils.git"
|
|
89
|
+
},
|
|
90
|
+
bugs: {
|
|
91
|
+
url: "https://github.com/AFixt/screenshot-utils/issues"
|
|
92
|
+
},
|
|
93
|
+
homepage: "https://github.com/AFixt/screenshot-utils",
|
|
94
|
+
author: "Karl Groves <karl.groves@afixt.com>",
|
|
95
|
+
license: "UNLICENSED",
|
|
96
|
+
publishConfig: {
|
|
97
|
+
access: "restricted"
|
|
98
|
+
},
|
|
99
|
+
engines: {
|
|
100
|
+
node: ">=22.22.1"
|
|
101
|
+
},
|
|
102
|
+
keywords: [
|
|
103
|
+
"screenshot",
|
|
104
|
+
"accessibility",
|
|
105
|
+
"puppeteer",
|
|
106
|
+
"playwright",
|
|
107
|
+
"sharp",
|
|
108
|
+
"dzi",
|
|
109
|
+
"tile",
|
|
110
|
+
"diff",
|
|
111
|
+
"annotate",
|
|
112
|
+
"redact",
|
|
113
|
+
"a11y",
|
|
114
|
+
"afixt",
|
|
115
|
+
"revenant"
|
|
116
|
+
],
|
|
117
|
+
dependencies: {
|
|
118
|
+
sharp: "0.34.5"
|
|
119
|
+
},
|
|
120
|
+
peerDependencies: {
|
|
121
|
+
"@napi-rs/canvas": "^0.1.0",
|
|
122
|
+
pixelmatch: "^7.1.0",
|
|
123
|
+
playwright: ">=1.40.0",
|
|
124
|
+
pngjs: "^7.0.0",
|
|
125
|
+
puppeteer: ">=23.0.0"
|
|
126
|
+
},
|
|
127
|
+
peerDependenciesMeta: {
|
|
128
|
+
"@napi-rs/canvas": {
|
|
129
|
+
optional: true
|
|
130
|
+
},
|
|
131
|
+
pixelmatch: {
|
|
132
|
+
optional: true
|
|
133
|
+
},
|
|
134
|
+
playwright: {
|
|
135
|
+
optional: true
|
|
136
|
+
},
|
|
137
|
+
pngjs: {
|
|
138
|
+
optional: true
|
|
139
|
+
},
|
|
140
|
+
puppeteer: {
|
|
141
|
+
optional: true
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
devDependencies: {
|
|
145
|
+
"@commitlint/cli": "20.5.3",
|
|
146
|
+
"@commitlint/config-conventional": "20.5.3",
|
|
147
|
+
"@eslint/js": "10.0.1",
|
|
148
|
+
"@types/node": "22.19.19",
|
|
149
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
150
|
+
"clean-jsdoc-theme": "4.3.2",
|
|
151
|
+
eslint: "10.3.0",
|
|
152
|
+
"eslint-plugin-import-x": "4.16.2",
|
|
153
|
+
"eslint-plugin-jsdoc": "62.9.0",
|
|
154
|
+
"eslint-plugin-n": "18.0.1",
|
|
155
|
+
"eslint-plugin-no-secrets": "2.3.3",
|
|
156
|
+
"eslint-plugin-promise": "7.3.0",
|
|
157
|
+
"eslint-plugin-security": "4.0.0",
|
|
158
|
+
"eslint-plugin-sonarjs": "4.0.3",
|
|
159
|
+
"eslint-plugin-unicorn": "64.0.0",
|
|
160
|
+
globals: "17.6.0",
|
|
161
|
+
husky: "9.1.7",
|
|
162
|
+
jscpd: "4.0.9",
|
|
163
|
+
jsdoc: "4.0.4",
|
|
164
|
+
"license-checker-rseidelsohn": "4.4.2",
|
|
165
|
+
"lint-staged": "17.0.2",
|
|
166
|
+
"markdownlint-cli": "0.48.0",
|
|
167
|
+
"npm-run-all2": "8.0.4",
|
|
168
|
+
pixelmatch: "7.1.0",
|
|
169
|
+
playwright: "1.60.0",
|
|
170
|
+
pngjs: "7.0.0",
|
|
171
|
+
prettier: "3.8.3",
|
|
172
|
+
puppeteer: "25.0.2",
|
|
173
|
+
"standard-version": "9.5.0",
|
|
174
|
+
tsup: "8.5.1",
|
|
175
|
+
typescript: "6.0.3",
|
|
176
|
+
vitest: "4.1.5"
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// src/errors/base.js
|
|
183
|
+
var require_base = __commonJS({
|
|
184
|
+
"src/errors/base.js"(exports, module) {
|
|
185
|
+
var ScreenshotUtilsError = class extends Error {
|
|
186
|
+
/**
|
|
187
|
+
* Construct an error. Subclasses set `code` and `retriable`; callers may
|
|
188
|
+
* supply `reason`, `cause`, and `context`.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} message Human-readable summary. Should not include PII.
|
|
191
|
+
* @param {object} [options]
|
|
192
|
+
* @param {string} [options.reason] Sub-classification (enum string) — see spec § 14.3.
|
|
193
|
+
* @param {Error} [options.cause] Underlying error being wrapped, preserved for stack traces.
|
|
194
|
+
* @param {Record<string, unknown>} [options.context] Structured debug data. NO PII.
|
|
195
|
+
*/
|
|
196
|
+
constructor(message, { reason, cause, context } = {}) {
|
|
197
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
198
|
+
this.name = new.target.name;
|
|
199
|
+
this.code = "SCREENSHOT_UTILS_ERROR";
|
|
200
|
+
this.retriable = false;
|
|
201
|
+
if (reason !== void 0) {
|
|
202
|
+
this.reason = reason;
|
|
203
|
+
}
|
|
204
|
+
if (context !== void 0) {
|
|
205
|
+
this.context = context;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
module.exports = { ScreenshotUtilsError };
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// src/errors/capture.js
|
|
214
|
+
var require_capture = __commonJS({
|
|
215
|
+
"src/errors/capture.js"(exports, module) {
|
|
216
|
+
var { ScreenshotUtilsError } = require_base();
|
|
217
|
+
var CaptureError = class extends ScreenshotUtilsError {
|
|
218
|
+
/**
|
|
219
|
+
* @param {string} message
|
|
220
|
+
* @param {object} [options]
|
|
221
|
+
* @param {string} [options.reason]
|
|
222
|
+
* @param {Error} [options.cause]
|
|
223
|
+
* @param {Record<string, unknown>} [options.context]
|
|
224
|
+
*/
|
|
225
|
+
constructor(message, options) {
|
|
226
|
+
super(message, options);
|
|
227
|
+
this.code = "CAPTURE_ERROR";
|
|
228
|
+
this.retriable = false;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var BrowserUnavailableError = class extends CaptureError {
|
|
232
|
+
/**
|
|
233
|
+
* @param {string} message
|
|
234
|
+
* @param {object} [options]
|
|
235
|
+
*/
|
|
236
|
+
constructor(message, options) {
|
|
237
|
+
super(message, options);
|
|
238
|
+
this.code = "BROWSER_UNAVAILABLE";
|
|
239
|
+
this.retriable = true;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
var PageTooTallError = class extends CaptureError {
|
|
243
|
+
/**
|
|
244
|
+
* @param {string} message
|
|
245
|
+
* @param {object} [options]
|
|
246
|
+
*/
|
|
247
|
+
constructor(message, options) {
|
|
248
|
+
super(message, options);
|
|
249
|
+
this.code = "PAGE_TOO_TALL";
|
|
250
|
+
this.retriable = false;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var ElementNotFoundError = class extends CaptureError {
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} message
|
|
256
|
+
* @param {object} [options]
|
|
257
|
+
*/
|
|
258
|
+
constructor(message, options) {
|
|
259
|
+
super(message, options);
|
|
260
|
+
this.code = "ELEMENT_NOT_FOUND";
|
|
261
|
+
this.retriable = false;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var ElementNotVisibleError = class extends CaptureError {
|
|
265
|
+
/**
|
|
266
|
+
* @param {string} message
|
|
267
|
+
* @param {object} [options]
|
|
268
|
+
*/
|
|
269
|
+
constructor(message, options) {
|
|
270
|
+
super(message, options);
|
|
271
|
+
this.code = "ELEMENT_NOT_VISIBLE";
|
|
272
|
+
this.retriable = false;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var ElementMismatchError = class extends CaptureError {
|
|
276
|
+
/**
|
|
277
|
+
* @param {string} message
|
|
278
|
+
* @param {object} [options]
|
|
279
|
+
*/
|
|
280
|
+
constructor(message, options) {
|
|
281
|
+
super(message, options);
|
|
282
|
+
this.code = "ELEMENT_MISMATCH";
|
|
283
|
+
this.retriable = false;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
var ElementTooLargeError = class extends CaptureError {
|
|
287
|
+
/**
|
|
288
|
+
* @param {string} message
|
|
289
|
+
* @param {object} [options]
|
|
290
|
+
*/
|
|
291
|
+
constructor(message, options) {
|
|
292
|
+
super(message, options);
|
|
293
|
+
this.code = "ELEMENT_TOO_LARGE";
|
|
294
|
+
this.retriable = false;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var StabilityTimeoutError = class extends CaptureError {
|
|
298
|
+
/**
|
|
299
|
+
* @param {string} message
|
|
300
|
+
* @param {object} [options]
|
|
301
|
+
*/
|
|
302
|
+
constructor(message, options) {
|
|
303
|
+
super(message, options);
|
|
304
|
+
this.code = "STABILITY_TIMEOUT";
|
|
305
|
+
this.retriable = true;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var CaptureTimeoutError = class extends CaptureError {
|
|
309
|
+
/**
|
|
310
|
+
* @param {string} message
|
|
311
|
+
* @param {object} [options]
|
|
312
|
+
*/
|
|
313
|
+
constructor(message, options) {
|
|
314
|
+
super(message, options);
|
|
315
|
+
this.code = "CAPTURE_TIMEOUT";
|
|
316
|
+
this.retriable = true;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
module.exports = {
|
|
320
|
+
CaptureError,
|
|
321
|
+
BrowserUnavailableError,
|
|
322
|
+
PageTooTallError,
|
|
323
|
+
ElementNotFoundError,
|
|
324
|
+
ElementNotVisibleError,
|
|
325
|
+
ElementMismatchError,
|
|
326
|
+
ElementTooLargeError,
|
|
327
|
+
StabilityTimeoutError,
|
|
328
|
+
CaptureTimeoutError
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// src/errors/transform.js
|
|
334
|
+
var require_transform = __commonJS({
|
|
335
|
+
"src/errors/transform.js"(exports, module) {
|
|
336
|
+
var { ScreenshotUtilsError } = require_base();
|
|
337
|
+
var TransformError = class extends ScreenshotUtilsError {
|
|
338
|
+
/**
|
|
339
|
+
* @param {string} message
|
|
340
|
+
* @param {object} [options]
|
|
341
|
+
*/
|
|
342
|
+
constructor(message, options) {
|
|
343
|
+
super(message, options);
|
|
344
|
+
this.code = "TRANSFORM_ERROR";
|
|
345
|
+
this.retriable = false;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
var InvalidRectError = class extends TransformError {
|
|
349
|
+
/**
|
|
350
|
+
* @param {string} message
|
|
351
|
+
* @param {object} [options]
|
|
352
|
+
*/
|
|
353
|
+
constructor(message, options) {
|
|
354
|
+
super(message, options);
|
|
355
|
+
this.code = "INVALID_RECT";
|
|
356
|
+
this.retriable = false;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var UnsupportedFormatError = class extends TransformError {
|
|
360
|
+
/**
|
|
361
|
+
* @param {string} message
|
|
362
|
+
* @param {object} [options]
|
|
363
|
+
*/
|
|
364
|
+
constructor(message, options) {
|
|
365
|
+
super(message, options);
|
|
366
|
+
this.code = "UNSUPPORTED_FORMAT";
|
|
367
|
+
this.retriable = false;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var BufferCorruptError = class extends TransformError {
|
|
371
|
+
/**
|
|
372
|
+
* @param {string} message
|
|
373
|
+
* @param {object} [options]
|
|
374
|
+
*/
|
|
375
|
+
constructor(message, options) {
|
|
376
|
+
super(message, options);
|
|
377
|
+
this.code = "BUFFER_CORRUPT";
|
|
378
|
+
this.retriable = false;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
module.exports = {
|
|
382
|
+
TransformError,
|
|
383
|
+
InvalidRectError,
|
|
384
|
+
UnsupportedFormatError,
|
|
385
|
+
BufferCorruptError
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// src/errors/compose.js
|
|
391
|
+
var require_compose = __commonJS({
|
|
392
|
+
"src/errors/compose.js"(exports, module) {
|
|
393
|
+
var { ScreenshotUtilsError } = require_base();
|
|
394
|
+
var ComposeError = class extends ScreenshotUtilsError {
|
|
395
|
+
/**
|
|
396
|
+
* @param {string} message
|
|
397
|
+
* @param {object} [options]
|
|
398
|
+
*/
|
|
399
|
+
constructor(message, options) {
|
|
400
|
+
super(message, options);
|
|
401
|
+
this.code = "COMPOSE_ERROR";
|
|
402
|
+
this.retriable = false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
var DimensionMismatchError = class extends ComposeError {
|
|
406
|
+
/**
|
|
407
|
+
* @param {string} message
|
|
408
|
+
* @param {object} [options]
|
|
409
|
+
*/
|
|
410
|
+
constructor(message, options) {
|
|
411
|
+
super(message, options);
|
|
412
|
+
this.code = "DIMENSION_MISMATCH";
|
|
413
|
+
this.retriable = false;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var ThemeError = class extends ComposeError {
|
|
417
|
+
/**
|
|
418
|
+
* @param {string} message
|
|
419
|
+
* @param {object} [options]
|
|
420
|
+
*/
|
|
421
|
+
constructor(message, options) {
|
|
422
|
+
super(message, options);
|
|
423
|
+
this.code = "THEME_ERROR";
|
|
424
|
+
this.retriable = false;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var HeatmapPointError = class extends ComposeError {
|
|
428
|
+
/**
|
|
429
|
+
* @param {string} message
|
|
430
|
+
* @param {object} [options]
|
|
431
|
+
*/
|
|
432
|
+
constructor(message, options) {
|
|
433
|
+
super(message, options);
|
|
434
|
+
this.code = "HEATMAP_POINT";
|
|
435
|
+
this.retriable = false;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
module.exports = {
|
|
439
|
+
ComposeError,
|
|
440
|
+
DimensionMismatchError,
|
|
441
|
+
ThemeError,
|
|
442
|
+
HeatmapPointError
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// src/errors/tile.js
|
|
448
|
+
var require_tile = __commonJS({
|
|
449
|
+
"src/errors/tile.js"(exports, module) {
|
|
450
|
+
var { ScreenshotUtilsError } = require_base();
|
|
451
|
+
var TileError = class extends ScreenshotUtilsError {
|
|
452
|
+
/**
|
|
453
|
+
* @param {string} message
|
|
454
|
+
* @param {object} [options]
|
|
455
|
+
*/
|
|
456
|
+
constructor(message, options) {
|
|
457
|
+
super(message, options);
|
|
458
|
+
this.code = "TILE_ERROR";
|
|
459
|
+
this.retriable = false;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
var TileSizeError = class extends TileError {
|
|
463
|
+
/**
|
|
464
|
+
* @param {string} message
|
|
465
|
+
* @param {object} [options]
|
|
466
|
+
*/
|
|
467
|
+
constructor(message, options) {
|
|
468
|
+
super(message, options);
|
|
469
|
+
this.code = "TILE_SIZE";
|
|
470
|
+
this.retriable = false;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
var TileWriteError = class extends TileError {
|
|
474
|
+
/**
|
|
475
|
+
* @param {string} message
|
|
476
|
+
* @param {object} [options]
|
|
477
|
+
*/
|
|
478
|
+
constructor(message, options) {
|
|
479
|
+
super(message, options);
|
|
480
|
+
this.code = "TILE_WRITE";
|
|
481
|
+
this.retriable = true;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
module.exports = {
|
|
485
|
+
TileError,
|
|
486
|
+
TileSizeError,
|
|
487
|
+
TileWriteError
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// src/errors/redaction.js
|
|
493
|
+
var require_redaction = __commonJS({
|
|
494
|
+
"src/errors/redaction.js"(exports, module) {
|
|
495
|
+
var { ScreenshotUtilsError } = require_base();
|
|
496
|
+
var RedactionError = class extends ScreenshotUtilsError {
|
|
497
|
+
/**
|
|
498
|
+
* @param {string} message
|
|
499
|
+
* @param {object} [options]
|
|
500
|
+
*/
|
|
501
|
+
constructor(message, options) {
|
|
502
|
+
super(message, options);
|
|
503
|
+
this.code = "REDACTION_ERROR";
|
|
504
|
+
this.retriable = false;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var PolicyError = class extends RedactionError {
|
|
508
|
+
/**
|
|
509
|
+
* @param {string} message
|
|
510
|
+
* @param {object} [options]
|
|
511
|
+
*/
|
|
512
|
+
constructor(message, options) {
|
|
513
|
+
super(message, options);
|
|
514
|
+
this.code = "POLICY_ERROR";
|
|
515
|
+
this.retriable = false;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
module.exports = {
|
|
519
|
+
RedactionError,
|
|
520
|
+
PolicyError
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// src/errors/adapter.js
|
|
526
|
+
var require_adapter = __commonJS({
|
|
527
|
+
"src/errors/adapter.js"(exports, module) {
|
|
528
|
+
var { ScreenshotUtilsError } = require_base();
|
|
529
|
+
var AdapterError = class extends ScreenshotUtilsError {
|
|
530
|
+
/**
|
|
531
|
+
* @param {string} message
|
|
532
|
+
* @param {object} [options]
|
|
533
|
+
*/
|
|
534
|
+
constructor(message, options) {
|
|
535
|
+
super(message, options);
|
|
536
|
+
this.code = "ADAPTER_ERROR";
|
|
537
|
+
this.retriable = false;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
var AdapterContractError = class extends AdapterError {
|
|
541
|
+
/**
|
|
542
|
+
* @param {string} message
|
|
543
|
+
* @param {object} [options]
|
|
544
|
+
*/
|
|
545
|
+
constructor(message, options) {
|
|
546
|
+
super(message, options);
|
|
547
|
+
this.code = "ADAPTER_CONTRACT";
|
|
548
|
+
this.retriable = false;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
var BrowserApiError = class extends AdapterError {
|
|
552
|
+
/**
|
|
553
|
+
* @param {string} message
|
|
554
|
+
* @param {object} [options]
|
|
555
|
+
*/
|
|
556
|
+
constructor(message, options) {
|
|
557
|
+
super(message, options);
|
|
558
|
+
this.code = "BROWSER_API";
|
|
559
|
+
this.retriable = true;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
module.exports = {
|
|
563
|
+
AdapterError,
|
|
564
|
+
AdapterContractError,
|
|
565
|
+
BrowserApiError
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// src/errors/optional.js
|
|
571
|
+
var require_optional = __commonJS({
|
|
572
|
+
"src/errors/optional.js"(exports, module) {
|
|
573
|
+
var { ScreenshotUtilsError } = require_base();
|
|
574
|
+
var OptionalDependencyMissingError = class extends ScreenshotUtilsError {
|
|
575
|
+
/**
|
|
576
|
+
* @param {string} message
|
|
577
|
+
* @param {object} [options]
|
|
578
|
+
* @param {string} [options.packageName] The npm package the caller must install.
|
|
579
|
+
* @param {string} [options.installCommand] Suggested install command (default: derived).
|
|
580
|
+
*/
|
|
581
|
+
constructor(message, options = {}) {
|
|
582
|
+
const { packageName, installCommand, ...rest } = options;
|
|
583
|
+
super(message, rest);
|
|
584
|
+
this.code = "OPTIONAL_DEPENDENCY_MISSING";
|
|
585
|
+
this.retriable = false;
|
|
586
|
+
if (packageName !== void 0) {
|
|
587
|
+
this.packageName = packageName;
|
|
588
|
+
}
|
|
589
|
+
if (installCommand !== void 0) {
|
|
590
|
+
this.installCommand = installCommand;
|
|
591
|
+
} else if (packageName !== void 0) {
|
|
592
|
+
this.installCommand = `npm install ${packageName}`;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
module.exports = {
|
|
597
|
+
OptionalDependencyMissingError
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// src/errors/index.js
|
|
603
|
+
var require_errors = __commonJS({
|
|
604
|
+
"src/errors/index.js"(exports, module) {
|
|
605
|
+
var { ScreenshotUtilsError } = require_base();
|
|
606
|
+
var {
|
|
607
|
+
CaptureError,
|
|
608
|
+
BrowserUnavailableError,
|
|
609
|
+
PageTooTallError,
|
|
610
|
+
ElementNotFoundError,
|
|
611
|
+
ElementNotVisibleError,
|
|
612
|
+
ElementMismatchError,
|
|
613
|
+
ElementTooLargeError,
|
|
614
|
+
StabilityTimeoutError,
|
|
615
|
+
CaptureTimeoutError
|
|
616
|
+
} = require_capture();
|
|
617
|
+
var {
|
|
618
|
+
TransformError,
|
|
619
|
+
InvalidRectError,
|
|
620
|
+
UnsupportedFormatError,
|
|
621
|
+
BufferCorruptError
|
|
622
|
+
} = require_transform();
|
|
623
|
+
var {
|
|
624
|
+
ComposeError,
|
|
625
|
+
DimensionMismatchError,
|
|
626
|
+
ThemeError,
|
|
627
|
+
HeatmapPointError
|
|
628
|
+
} = require_compose();
|
|
629
|
+
var { TileError, TileSizeError, TileWriteError } = require_tile();
|
|
630
|
+
var { RedactionError, PolicyError } = require_redaction();
|
|
631
|
+
var { AdapterError, AdapterContractError, BrowserApiError } = require_adapter();
|
|
632
|
+
var { OptionalDependencyMissingError } = require_optional();
|
|
633
|
+
module.exports = {
|
|
634
|
+
ScreenshotUtilsError,
|
|
635
|
+
// capture
|
|
636
|
+
CaptureError,
|
|
637
|
+
BrowserUnavailableError,
|
|
638
|
+
PageTooTallError,
|
|
639
|
+
ElementNotFoundError,
|
|
640
|
+
ElementNotVisibleError,
|
|
641
|
+
ElementMismatchError,
|
|
642
|
+
ElementTooLargeError,
|
|
643
|
+
StabilityTimeoutError,
|
|
644
|
+
CaptureTimeoutError,
|
|
645
|
+
// transform
|
|
646
|
+
TransformError,
|
|
647
|
+
InvalidRectError,
|
|
648
|
+
UnsupportedFormatError,
|
|
649
|
+
BufferCorruptError,
|
|
650
|
+
// compose
|
|
651
|
+
ComposeError,
|
|
652
|
+
DimensionMismatchError,
|
|
653
|
+
ThemeError,
|
|
654
|
+
HeatmapPointError,
|
|
655
|
+
// tile
|
|
656
|
+
TileError,
|
|
657
|
+
TileSizeError,
|
|
658
|
+
TileWriteError,
|
|
659
|
+
// redaction
|
|
660
|
+
RedactionError,
|
|
661
|
+
PolicyError,
|
|
662
|
+
// adapter
|
|
663
|
+
AdapterError,
|
|
664
|
+
AdapterContractError,
|
|
665
|
+
BrowserApiError,
|
|
666
|
+
// optional peers
|
|
667
|
+
OptionalDependencyMissingError
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// src/adapters/puppeteer.js
|
|
673
|
+
var require_puppeteer = __commonJS({
|
|
674
|
+
"src/adapters/puppeteer.js"(exports, module) {
|
|
675
|
+
var { AdapterContractError, BrowserApiError } = require_errors();
|
|
676
|
+
function fromPuppeteerPage(page, options = {}) {
|
|
677
|
+
assertPuppeteerPage(page);
|
|
678
|
+
const label = options.label;
|
|
679
|
+
const screenshotMaxHeight = Number.isFinite(options.screenshotMaxHeight) ? options.screenshotMaxHeight : 16384;
|
|
680
|
+
return {
|
|
681
|
+
async screenshot(opts) {
|
|
682
|
+
return wrapBrowserCall("page.screenshot", () => page.screenshot(opts), label);
|
|
683
|
+
},
|
|
684
|
+
async evaluate(fn, ...args) {
|
|
685
|
+
return wrapBrowserCall("page.evaluate", () => page.evaluate(fn, ...args), label);
|
|
686
|
+
},
|
|
687
|
+
async scrollTo(point) {
|
|
688
|
+
await wrapBrowserCall(
|
|
689
|
+
"page.evaluate(scrollTo)",
|
|
690
|
+
// Runs in the browser context; `globalThis` === window.
|
|
691
|
+
() => page.evaluate(({ x, y }) => globalThis.scrollTo(x, y), point),
|
|
692
|
+
label
|
|
693
|
+
);
|
|
694
|
+
},
|
|
695
|
+
async getMetrics() {
|
|
696
|
+
const metrics = await wrapBrowserCall(
|
|
697
|
+
"page.evaluate(getMetrics)",
|
|
698
|
+
() => (
|
|
699
|
+
// Runs in the browser context; `globalThis` === window.
|
|
700
|
+
page.evaluate(() => {
|
|
701
|
+
const doc = globalThis.document;
|
|
702
|
+
return {
|
|
703
|
+
width: doc.documentElement.scrollWidth,
|
|
704
|
+
height: doc.documentElement.scrollHeight,
|
|
705
|
+
viewport: {
|
|
706
|
+
width: globalThis.innerWidth,
|
|
707
|
+
height: globalThis.innerHeight
|
|
708
|
+
},
|
|
709
|
+
devicePixelRatio: globalThis.devicePixelRatio,
|
|
710
|
+
url: globalThis.location.href
|
|
711
|
+
};
|
|
712
|
+
})
|
|
713
|
+
),
|
|
714
|
+
label
|
|
715
|
+
);
|
|
716
|
+
return { ...metrics, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
717
|
+
},
|
|
718
|
+
async setViewport(viewport) {
|
|
719
|
+
await wrapBrowserCall("page.setViewport", () => page.setViewport(viewport), label);
|
|
720
|
+
},
|
|
721
|
+
async reload(opts) {
|
|
722
|
+
await wrapBrowserCall("page.reload", () => page.reload(opts), label);
|
|
723
|
+
},
|
|
724
|
+
get capabilities() {
|
|
725
|
+
return {
|
|
726
|
+
screenshotMaxHeight,
|
|
727
|
+
supportsViewportEmulation: true,
|
|
728
|
+
supportsCDP: true
|
|
729
|
+
};
|
|
730
|
+
},
|
|
731
|
+
get label() {
|
|
732
|
+
return label;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function assertPuppeteerPage(page) {
|
|
737
|
+
if (page === null || typeof page !== "object") {
|
|
738
|
+
throw new AdapterContractError("fromPuppeteerPage(page) requires a Puppeteer Page object", {
|
|
739
|
+
context: { received: typeof page }
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
const required = ["screenshot", "evaluate", "setViewport", "reload"];
|
|
743
|
+
const missing = required.filter((name) => typeof page[name] !== "function");
|
|
744
|
+
if (missing.length > 0) {
|
|
745
|
+
throw new AdapterContractError(
|
|
746
|
+
`The provided page object is missing required Puppeteer methods: ${missing.join(", ")}`,
|
|
747
|
+
{ context: { missing } }
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function wrapBrowserCall(operation, fn, label) {
|
|
752
|
+
try {
|
|
753
|
+
return await fn();
|
|
754
|
+
} catch (error) {
|
|
755
|
+
throw new BrowserApiError(`Puppeteer call failed: ${operation}`, {
|
|
756
|
+
cause: error,
|
|
757
|
+
context: { operation, label }
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
module.exports = { fromPuppeteerPage };
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// src/adapters/playwright.js
|
|
766
|
+
var require_playwright = __commonJS({
|
|
767
|
+
"src/adapters/playwright.js"(exports, module) {
|
|
768
|
+
var { AdapterContractError, BrowserApiError } = require_errors();
|
|
769
|
+
function fromPlaywrightPage(page, options = {}) {
|
|
770
|
+
assertPlaywrightPage(page);
|
|
771
|
+
const label = options.label;
|
|
772
|
+
const screenshotMaxHeight = Number.isFinite(options.screenshotMaxHeight) ? options.screenshotMaxHeight : 16384;
|
|
773
|
+
return {
|
|
774
|
+
async screenshot(opts) {
|
|
775
|
+
return wrapBrowserCall("page.screenshot", () => page.screenshot(opts), label);
|
|
776
|
+
},
|
|
777
|
+
async evaluate(fn, ...args) {
|
|
778
|
+
return wrapBrowserCall(
|
|
779
|
+
"page.evaluate",
|
|
780
|
+
// Playwright expects evaluate(fn, arg) — single arg position.
|
|
781
|
+
// Forward exactly what the caller passed.
|
|
782
|
+
() => args.length === 0 ? page.evaluate(fn) : page.evaluate(fn, ...args),
|
|
783
|
+
label
|
|
784
|
+
);
|
|
785
|
+
},
|
|
786
|
+
async scrollTo(point) {
|
|
787
|
+
await wrapBrowserCall(
|
|
788
|
+
"page.evaluate(scrollTo)",
|
|
789
|
+
() => page.evaluate(({ x, y }) => globalThis.scrollTo(x, y), point),
|
|
790
|
+
label
|
|
791
|
+
);
|
|
792
|
+
},
|
|
793
|
+
async getMetrics() {
|
|
794
|
+
const metrics = await wrapBrowserCall(
|
|
795
|
+
"page.evaluate(getMetrics)",
|
|
796
|
+
() => page.evaluate(() => {
|
|
797
|
+
const doc = globalThis.document;
|
|
798
|
+
return {
|
|
799
|
+
width: doc.documentElement.scrollWidth,
|
|
800
|
+
height: doc.documentElement.scrollHeight,
|
|
801
|
+
viewport: {
|
|
802
|
+
width: globalThis.innerWidth,
|
|
803
|
+
height: globalThis.innerHeight
|
|
804
|
+
},
|
|
805
|
+
devicePixelRatio: globalThis.devicePixelRatio,
|
|
806
|
+
url: globalThis.location.href
|
|
807
|
+
};
|
|
808
|
+
}),
|
|
809
|
+
label
|
|
810
|
+
);
|
|
811
|
+
return { ...metrics, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
812
|
+
},
|
|
813
|
+
async setViewport(viewport) {
|
|
814
|
+
await wrapBrowserCall(
|
|
815
|
+
"page.setViewportSize",
|
|
816
|
+
() => page.setViewportSize({
|
|
817
|
+
width: viewport.width,
|
|
818
|
+
height: viewport.height
|
|
819
|
+
}),
|
|
820
|
+
label
|
|
821
|
+
);
|
|
822
|
+
},
|
|
823
|
+
async reload(opts) {
|
|
824
|
+
await wrapBrowserCall("page.reload", () => page.reload(opts), label);
|
|
825
|
+
},
|
|
826
|
+
get capabilities() {
|
|
827
|
+
return {
|
|
828
|
+
screenshotMaxHeight,
|
|
829
|
+
supportsViewportEmulation: true,
|
|
830
|
+
supportsCDP: false
|
|
831
|
+
// Playwright's CDP requires explicit session setup
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
get label() {
|
|
835
|
+
return label;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function assertPlaywrightPage(page) {
|
|
840
|
+
if (page === null || typeof page !== "object") {
|
|
841
|
+
throw new AdapterContractError(
|
|
842
|
+
"fromPlaywrightPage(page) requires a Playwright Page object",
|
|
843
|
+
{ context: { received: typeof page } }
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const required = ["screenshot", "evaluate", "setViewportSize", "reload"];
|
|
847
|
+
const missing = required.filter((name) => typeof page[name] !== "function");
|
|
848
|
+
if (missing.length > 0) {
|
|
849
|
+
throw new AdapterContractError(
|
|
850
|
+
`The provided page object is missing required Playwright methods: ${missing.join(", ")}`,
|
|
851
|
+
{ context: { missing } }
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async function wrapBrowserCall(operation, fn, label) {
|
|
856
|
+
try {
|
|
857
|
+
return await fn();
|
|
858
|
+
} catch (error) {
|
|
859
|
+
throw new BrowserApiError(`Playwright call failed: ${operation}`, {
|
|
860
|
+
cause: error,
|
|
861
|
+
context: { operation, label }
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
module.exports = { fromPlaywrightPage };
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// src/adapters/cdp.js
|
|
870
|
+
var require_cdp = __commonJS({
|
|
871
|
+
"src/adapters/cdp.js"(exports, module) {
|
|
872
|
+
var { AdapterContractError, BrowserApiError } = require_errors();
|
|
873
|
+
var { fromPuppeteerPage } = require_puppeteer();
|
|
874
|
+
function fromCDPSession(session, page, options = {}) {
|
|
875
|
+
assertSession(session);
|
|
876
|
+
const base = fromPuppeteerPage(page, options);
|
|
877
|
+
const label = options.label;
|
|
878
|
+
const screenshotMaxHeight = Number.isFinite(options.screenshotMaxHeight) ? options.screenshotMaxHeight : 16384;
|
|
879
|
+
return {
|
|
880
|
+
async screenshot(opts) {
|
|
881
|
+
const cdpOpts = translateScreenshotOpts(opts);
|
|
882
|
+
try {
|
|
883
|
+
const response = await session.send("Page.captureScreenshot", cdpOpts);
|
|
884
|
+
return Buffer.from(response.data, "base64");
|
|
885
|
+
} catch (error) {
|
|
886
|
+
throw new BrowserApiError("CDP Page.captureScreenshot failed", {
|
|
887
|
+
cause: error,
|
|
888
|
+
context: { operation: "CDP.Page.captureScreenshot", label }
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
// Delegate every non-screenshot method to the wrapped Puppeteer adapter.
|
|
893
|
+
evaluate: base.evaluate.bind(base),
|
|
894
|
+
scrollTo: base.scrollTo.bind(base),
|
|
895
|
+
getMetrics: base.getMetrics.bind(base),
|
|
896
|
+
setViewport: base.setViewport.bind(base),
|
|
897
|
+
reload: base.reload.bind(base),
|
|
898
|
+
get capabilities() {
|
|
899
|
+
return {
|
|
900
|
+
screenshotMaxHeight,
|
|
901
|
+
supportsViewportEmulation: true,
|
|
902
|
+
supportsCDP: true
|
|
903
|
+
};
|
|
904
|
+
},
|
|
905
|
+
get label() {
|
|
906
|
+
return label;
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function translateScreenshotOpts(opts = {}) {
|
|
911
|
+
const out = {
|
|
912
|
+
format: opts.type === "jpeg" ? "jpeg" : "png"
|
|
913
|
+
};
|
|
914
|
+
if (opts.type === "jpeg" && Number.isFinite(opts.quality)) {
|
|
915
|
+
out.quality = opts.quality;
|
|
916
|
+
}
|
|
917
|
+
if (opts.clip) {
|
|
918
|
+
out.clip = {
|
|
919
|
+
x: opts.clip.x,
|
|
920
|
+
y: opts.clip.y,
|
|
921
|
+
width: opts.clip.width,
|
|
922
|
+
height: opts.clip.height,
|
|
923
|
+
scale: 1
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
if (opts.fullPage) {
|
|
927
|
+
out.captureBeyondViewport = true;
|
|
928
|
+
}
|
|
929
|
+
if (opts.omitBackground) {
|
|
930
|
+
out.optimizeForSpeed = false;
|
|
931
|
+
}
|
|
932
|
+
return out;
|
|
933
|
+
}
|
|
934
|
+
function assertSession(session) {
|
|
935
|
+
if (session === null || typeof session !== "object" || typeof session.send !== "function") {
|
|
936
|
+
throw new AdapterContractError(
|
|
937
|
+
"fromCDPSession(session, page) requires a CDP session with a .send() method",
|
|
938
|
+
{ context: { received: typeof session } }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
module.exports = { fromCDPSession };
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// src/adapters/index.js
|
|
947
|
+
var require_adapters = __commonJS({
|
|
948
|
+
"src/adapters/index.js"(exports, module) {
|
|
949
|
+
var { fromPuppeteerPage } = require_puppeteer();
|
|
950
|
+
var { fromPlaywrightPage } = require_playwright();
|
|
951
|
+
var { fromCDPSession } = require_cdp();
|
|
952
|
+
module.exports = { fromPuppeteerPage, fromPlaywrightPage, fromCDPSession };
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// src/capture/settle.js
|
|
957
|
+
var require_settle = __commonJS({
|
|
958
|
+
"src/capture/settle.js"(exports, module) {
|
|
959
|
+
async function waitForStable(adapter, options = {}) {
|
|
960
|
+
const {
|
|
961
|
+
fonts = true,
|
|
962
|
+
images = true,
|
|
963
|
+
animations = "reduce",
|
|
964
|
+
maxWait = 5e3,
|
|
965
|
+
idleMs = 200
|
|
966
|
+
} = options;
|
|
967
|
+
const start = nowMs();
|
|
968
|
+
const report = await adapter.evaluate(runInPage, {
|
|
969
|
+
fonts,
|
|
970
|
+
images,
|
|
971
|
+
animations,
|
|
972
|
+
maxWait,
|
|
973
|
+
idleMs
|
|
974
|
+
});
|
|
975
|
+
const totalMs = nowMs() - start;
|
|
976
|
+
return { ...report, totalMs };
|
|
977
|
+
}
|
|
978
|
+
async function runInPage(opts) {
|
|
979
|
+
const doc = globalThis.document;
|
|
980
|
+
const report = {
|
|
981
|
+
fonts: "skipped",
|
|
982
|
+
images: { total: 0, loaded: 0, failed: 0 },
|
|
983
|
+
animations: "skipped"
|
|
984
|
+
};
|
|
985
|
+
const withTimeout = (promise, ms, label) => Promise.race([
|
|
986
|
+
promise,
|
|
987
|
+
new Promise((_resolve, reject) => {
|
|
988
|
+
setTimeout(() => reject(new Error(`${label} timeout`)), ms);
|
|
989
|
+
})
|
|
990
|
+
]);
|
|
991
|
+
const settleFonts = async () => {
|
|
992
|
+
if (!opts.fonts || !doc || !doc.fonts || !doc.fonts.ready) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
await withTimeout(doc.fonts.ready, opts.maxWait, "fonts.ready");
|
|
997
|
+
report.fonts = "ready";
|
|
998
|
+
} catch {
|
|
999
|
+
report.fonts = "timeout";
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const settleOneImage = (img) => new Promise((resolve) => {
|
|
1003
|
+
if (img.complete) {
|
|
1004
|
+
if (img.naturalHeight > 0) {
|
|
1005
|
+
report.images.loaded += 1;
|
|
1006
|
+
} else {
|
|
1007
|
+
report.images.failed += 1;
|
|
1008
|
+
}
|
|
1009
|
+
resolve();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
img.addEventListener(
|
|
1013
|
+
"load",
|
|
1014
|
+
() => {
|
|
1015
|
+
report.images.loaded += 1;
|
|
1016
|
+
resolve();
|
|
1017
|
+
},
|
|
1018
|
+
{ once: true }
|
|
1019
|
+
);
|
|
1020
|
+
img.addEventListener(
|
|
1021
|
+
"error",
|
|
1022
|
+
() => {
|
|
1023
|
+
report.images.failed += 1;
|
|
1024
|
+
resolve();
|
|
1025
|
+
},
|
|
1026
|
+
{ once: true }
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
const settleImages = async () => {
|
|
1030
|
+
if (!opts.images || !doc) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const imgs = Array.from(doc.images || doc.querySelectorAll("img") || []);
|
|
1034
|
+
report.images.total = imgs.length;
|
|
1035
|
+
await Promise.all(imgs.map(settleOneImage));
|
|
1036
|
+
};
|
|
1037
|
+
const settleAnimations = async () => {
|
|
1038
|
+
if (opts.animations === "ignore" || !doc) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const reduced = globalThis.matchMedia && globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1042
|
+
if (reduced) {
|
|
1043
|
+
report.animations = "reduced";
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const running = doc.getAnimations ? doc.getAnimations().filter((a) => a.playState === "running") : [];
|
|
1047
|
+
if (opts.animations === "reduce") {
|
|
1048
|
+
report.animations = running.length > 0 ? "timeout" : "none";
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (opts.animations === "wait") {
|
|
1052
|
+
if (running.length === 0) {
|
|
1053
|
+
report.animations = "none";
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
await withTimeout(
|
|
1058
|
+
Promise.all(running.map((a) => a.finished.catch(() => {
|
|
1059
|
+
}))),
|
|
1060
|
+
opts.maxWait,
|
|
1061
|
+
"animations"
|
|
1062
|
+
);
|
|
1063
|
+
report.animations = "settled";
|
|
1064
|
+
} catch {
|
|
1065
|
+
report.animations = "timeout";
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
const settleIdle = async () => {
|
|
1070
|
+
if (opts.idleMs <= 0) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (typeof globalThis.requestIdleCallback === "function") {
|
|
1074
|
+
await new Promise((resolve) => {
|
|
1075
|
+
globalThis.requestIdleCallback(() => resolve(), {
|
|
1076
|
+
timeout: opts.idleMs * 2
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
await new Promise((resolve) => {
|
|
1081
|
+
setTimeout(resolve, opts.idleMs);
|
|
1082
|
+
});
|
|
1083
|
+
};
|
|
1084
|
+
await settleFonts();
|
|
1085
|
+
await settleImages();
|
|
1086
|
+
await settleAnimations();
|
|
1087
|
+
await settleIdle();
|
|
1088
|
+
return report;
|
|
1089
|
+
}
|
|
1090
|
+
function nowMs() {
|
|
1091
|
+
return Number(process.hrtime.bigint() / 1000000n);
|
|
1092
|
+
}
|
|
1093
|
+
module.exports = { waitForStable };
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// src/capture/page.js
|
|
1098
|
+
var require_page = __commonJS({
|
|
1099
|
+
"src/capture/page.js"(exports, module) {
|
|
1100
|
+
var { CaptureTimeoutError, UnsupportedFormatError } = require_errors();
|
|
1101
|
+
var { waitForStable } = require_settle();
|
|
1102
|
+
async function capturePage(adapter, options = {}) {
|
|
1103
|
+
const {
|
|
1104
|
+
format = "png",
|
|
1105
|
+
quality = 90,
|
|
1106
|
+
fullPage = true,
|
|
1107
|
+
omitBackground = false,
|
|
1108
|
+
encoding = "buffer",
|
|
1109
|
+
timeout = 3e4,
|
|
1110
|
+
waitForStable: settleOpts = false,
|
|
1111
|
+
events = null
|
|
1112
|
+
} = options;
|
|
1113
|
+
validateFormat(format);
|
|
1114
|
+
const start = nowMs();
|
|
1115
|
+
emit(events, "capture:start", { format, fullPage });
|
|
1116
|
+
let settleMs = 0;
|
|
1117
|
+
const warnings = [];
|
|
1118
|
+
if (settleOpts) {
|
|
1119
|
+
const settleStart = nowMs();
|
|
1120
|
+
const report = await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
|
|
1121
|
+
settleMs = nowMs() - settleStart;
|
|
1122
|
+
if (report.fonts === "timeout") {
|
|
1123
|
+
warnings.push("fonts did not settle within maxWait");
|
|
1124
|
+
}
|
|
1125
|
+
if (report.animations === "timeout") {
|
|
1126
|
+
warnings.push("animations did not settle within maxWait");
|
|
1127
|
+
}
|
|
1128
|
+
if (report.images.failed > 0) {
|
|
1129
|
+
warnings.push(`${report.images.failed} image(s) failed to load`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
const metrics = await adapter.getMetrics();
|
|
1133
|
+
const browserFormat = decideBrowserFormat(format);
|
|
1134
|
+
const browserOpts = buildBrowserOpts({
|
|
1135
|
+
format: browserFormat,
|
|
1136
|
+
quality,
|
|
1137
|
+
fullPage,
|
|
1138
|
+
omitBackground
|
|
1139
|
+
});
|
|
1140
|
+
const captureStart = nowMs();
|
|
1141
|
+
const rawBuffer = await withTimeout(adapter.screenshot(browserOpts), timeout, {
|
|
1142
|
+
operation: "adapter.screenshot"
|
|
1143
|
+
});
|
|
1144
|
+
const captureMs = nowMs() - captureStart;
|
|
1145
|
+
let bytes = rawBuffer;
|
|
1146
|
+
let encodeMs = 0;
|
|
1147
|
+
if (browserFormat !== format) {
|
|
1148
|
+
const encodeStart = nowMs();
|
|
1149
|
+
bytes = await reencode(rawBuffer, { format, quality });
|
|
1150
|
+
encodeMs = nowMs() - encodeStart;
|
|
1151
|
+
}
|
|
1152
|
+
if (encoding === "stream") {
|
|
1153
|
+
bytes = bufferToStream(bytes);
|
|
1154
|
+
}
|
|
1155
|
+
const totalMs = nowMs() - start;
|
|
1156
|
+
const result = {
|
|
1157
|
+
format,
|
|
1158
|
+
bytes,
|
|
1159
|
+
page: metrics,
|
|
1160
|
+
timing: { settleMs, captureMs, encodeMs, totalMs },
|
|
1161
|
+
warnings
|
|
1162
|
+
};
|
|
1163
|
+
emit(events, "capture:complete", {
|
|
1164
|
+
format,
|
|
1165
|
+
bytes: typeof bytes === "object" && Buffer.isBuffer(bytes) ? bytes.length : null,
|
|
1166
|
+
totalMs
|
|
1167
|
+
});
|
|
1168
|
+
return result;
|
|
1169
|
+
}
|
|
1170
|
+
function decideBrowserFormat(format) {
|
|
1171
|
+
return format === "webp" ? "png" : format;
|
|
1172
|
+
}
|
|
1173
|
+
function buildBrowserOpts({ format, quality, fullPage, omitBackground }) {
|
|
1174
|
+
const out = { fullPage, omitBackground, type: format };
|
|
1175
|
+
if (format === "jpeg") {
|
|
1176
|
+
out.quality = quality;
|
|
1177
|
+
}
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
async function reencode(input, { format, quality }) {
|
|
1181
|
+
const sharp = __require("sharp");
|
|
1182
|
+
let pipeline = sharp(input);
|
|
1183
|
+
switch (format) {
|
|
1184
|
+
case "webp":
|
|
1185
|
+
pipeline = pipeline.webp({ quality });
|
|
1186
|
+
break;
|
|
1187
|
+
case "jpeg":
|
|
1188
|
+
pipeline = pipeline.jpeg({ quality });
|
|
1189
|
+
break;
|
|
1190
|
+
case "png":
|
|
1191
|
+
pipeline = pipeline.png();
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
return pipeline.toBuffer();
|
|
1195
|
+
}
|
|
1196
|
+
function withTimeout(promise, ms, ctx) {
|
|
1197
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
1198
|
+
return promise;
|
|
1199
|
+
}
|
|
1200
|
+
let timer;
|
|
1201
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1202
|
+
timer = setTimeout(() => {
|
|
1203
|
+
reject(
|
|
1204
|
+
new CaptureTimeoutError(`${ctx.operation} exceeded ${ms}ms timeout`, {
|
|
1205
|
+
context: { operation: ctx.operation, timeoutMs: ms }
|
|
1206
|
+
})
|
|
1207
|
+
);
|
|
1208
|
+
}, ms);
|
|
1209
|
+
if (typeof timer.unref === "function") {
|
|
1210
|
+
timer.unref();
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
1214
|
+
}
|
|
1215
|
+
function validateFormat(format) {
|
|
1216
|
+
if (!["png", "jpeg", "webp"].includes(format)) {
|
|
1217
|
+
throw new UnsupportedFormatError(
|
|
1218
|
+
`format must be 'png', 'jpeg', or 'webp'; received ${JSON.stringify(format)}`,
|
|
1219
|
+
{ context: { received: format } }
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
function emit(events, name, payload) {
|
|
1224
|
+
if (events && typeof events.emit === "function") {
|
|
1225
|
+
events.emit(name, payload);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
function nowMs() {
|
|
1229
|
+
return Number(process.hrtime.bigint() / 1000000n);
|
|
1230
|
+
}
|
|
1231
|
+
function bufferToStream(buf) {
|
|
1232
|
+
const { Readable } = __require("stream");
|
|
1233
|
+
return Readable.from(buf);
|
|
1234
|
+
}
|
|
1235
|
+
module.exports = { capturePage };
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// src/capture/long-page.js
|
|
1240
|
+
var require_long_page = __commonJS({
|
|
1241
|
+
"src/capture/long-page.js"(exports, module) {
|
|
1242
|
+
var { PageTooTallError, CaptureTimeoutError } = require_errors();
|
|
1243
|
+
async function captureLongPage(adapter, options = {}) {
|
|
1244
|
+
const {
|
|
1245
|
+
stripHeight = null,
|
|
1246
|
+
overlap = 0,
|
|
1247
|
+
maxHeight = 32768,
|
|
1248
|
+
format = "png",
|
|
1249
|
+
quality = 90,
|
|
1250
|
+
omitBackground = false,
|
|
1251
|
+
scrollSettleMs = 100,
|
|
1252
|
+
timeout = 3e4,
|
|
1253
|
+
events = null
|
|
1254
|
+
} = options;
|
|
1255
|
+
const start = nowMs();
|
|
1256
|
+
emit(events, "capture:start", { format, fullPage: true, mode: "long-page" });
|
|
1257
|
+
const metrics = await adapter.getMetrics();
|
|
1258
|
+
if (metrics.height > maxHeight) {
|
|
1259
|
+
throw new PageTooTallError(
|
|
1260
|
+
`Page height ${metrics.height}px exceeds maxHeight ${maxHeight}px`,
|
|
1261
|
+
{ context: { documentHeight: metrics.height, maxHeight } }
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const capabilities = adapter.capabilities ?? { screenshotMaxHeight: 16384 };
|
|
1265
|
+
const browserMax = capabilities.screenshotMaxHeight;
|
|
1266
|
+
const captureStart = nowMs();
|
|
1267
|
+
const { bytes: stitched, encodeMs } = metrics.height <= browserMax ? await singleShotCapture(adapter, {
|
|
1268
|
+
format,
|
|
1269
|
+
quality,
|
|
1270
|
+
omitBackground,
|
|
1271
|
+
timeout
|
|
1272
|
+
}) : await stripCapture(adapter, {
|
|
1273
|
+
metrics,
|
|
1274
|
+
stripHeight,
|
|
1275
|
+
overlap,
|
|
1276
|
+
scrollSettleMs,
|
|
1277
|
+
timeout,
|
|
1278
|
+
format,
|
|
1279
|
+
quality,
|
|
1280
|
+
omitBackground,
|
|
1281
|
+
events
|
|
1282
|
+
});
|
|
1283
|
+
const captureMs = nowMs() - captureStart - encodeMs;
|
|
1284
|
+
const totalMs = nowMs() - start;
|
|
1285
|
+
const result = {
|
|
1286
|
+
format,
|
|
1287
|
+
bytes: stitched,
|
|
1288
|
+
page: metrics,
|
|
1289
|
+
timing: { settleMs: 0, captureMs, encodeMs, totalMs },
|
|
1290
|
+
warnings: []
|
|
1291
|
+
};
|
|
1292
|
+
emit(events, "capture:complete", { format, totalMs });
|
|
1293
|
+
return result;
|
|
1294
|
+
}
|
|
1295
|
+
async function singleShotCapture(adapter, opts) {
|
|
1296
|
+
const { format, quality, omitBackground, timeout } = opts;
|
|
1297
|
+
const bytes = await withTimeout(
|
|
1298
|
+
adapter.screenshot({
|
|
1299
|
+
fullPage: true,
|
|
1300
|
+
type: format === "webp" ? "png" : format,
|
|
1301
|
+
omitBackground,
|
|
1302
|
+
...format === "jpeg" ? { quality } : {}
|
|
1303
|
+
}),
|
|
1304
|
+
timeout,
|
|
1305
|
+
{ operation: "adapter.screenshot(fullPage)" }
|
|
1306
|
+
);
|
|
1307
|
+
return { bytes, encodeMs: 0 };
|
|
1308
|
+
}
|
|
1309
|
+
async function stripCapture(adapter, opts) {
|
|
1310
|
+
const {
|
|
1311
|
+
metrics,
|
|
1312
|
+
stripHeight,
|
|
1313
|
+
overlap,
|
|
1314
|
+
scrollSettleMs,
|
|
1315
|
+
timeout,
|
|
1316
|
+
format,
|
|
1317
|
+
quality,
|
|
1318
|
+
omitBackground,
|
|
1319
|
+
events
|
|
1320
|
+
} = opts;
|
|
1321
|
+
const sh = Math.max(1, stripHeight ?? metrics.viewport.height);
|
|
1322
|
+
const stride = Math.max(1, sh - overlap);
|
|
1323
|
+
const uniqueYs = planStripYs(metrics.height, sh, stride);
|
|
1324
|
+
const strips = [];
|
|
1325
|
+
for (let i = 0; i < uniqueYs.length; i += 1) {
|
|
1326
|
+
const y = uniqueYs[i];
|
|
1327
|
+
await adapter.scrollTo({ x: 0, y });
|
|
1328
|
+
if (scrollSettleMs > 0) {
|
|
1329
|
+
await sleep(scrollSettleMs);
|
|
1330
|
+
}
|
|
1331
|
+
const buffer = await withTimeout(
|
|
1332
|
+
adapter.screenshot({ fullPage: false, type: "png", omitBackground }),
|
|
1333
|
+
timeout,
|
|
1334
|
+
{ operation: `adapter.screenshot(strip ${i + 1}/${uniqueYs.length})` }
|
|
1335
|
+
);
|
|
1336
|
+
strips.push({ y, buffer });
|
|
1337
|
+
emit(events, "strip:captured", { index: i, total: uniqueYs.length, y });
|
|
1338
|
+
}
|
|
1339
|
+
const stitchStart = nowMs();
|
|
1340
|
+
const bytes = await stitchStrips(strips, {
|
|
1341
|
+
width: metrics.viewport.width,
|
|
1342
|
+
height: metrics.height,
|
|
1343
|
+
format,
|
|
1344
|
+
quality
|
|
1345
|
+
});
|
|
1346
|
+
return { bytes, encodeMs: nowMs() - stitchStart };
|
|
1347
|
+
}
|
|
1348
|
+
function planStripYs(height, sh, stride) {
|
|
1349
|
+
const ys = [];
|
|
1350
|
+
for (let y = 0; y < height; y += stride) {
|
|
1351
|
+
ys.push(Math.min(y, height - sh));
|
|
1352
|
+
if (y + sh >= height) {
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return Array.from(new Set(ys));
|
|
1357
|
+
}
|
|
1358
|
+
async function stitchStrips(strips, { width, height, format, quality }) {
|
|
1359
|
+
const sharp = __require("sharp");
|
|
1360
|
+
const canvas = sharp({
|
|
1361
|
+
create: {
|
|
1362
|
+
width: Math.round(width),
|
|
1363
|
+
height: Math.round(height),
|
|
1364
|
+
channels: 4,
|
|
1365
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
1366
|
+
}
|
|
1367
|
+
}).png();
|
|
1368
|
+
const composites = strips.map(({ y, buffer }) => ({
|
|
1369
|
+
input: buffer,
|
|
1370
|
+
top: Math.round(y),
|
|
1371
|
+
left: 0
|
|
1372
|
+
}));
|
|
1373
|
+
let pipeline = canvas.composite(composites);
|
|
1374
|
+
switch (format) {
|
|
1375
|
+
case "webp":
|
|
1376
|
+
pipeline = pipeline.webp({ quality });
|
|
1377
|
+
break;
|
|
1378
|
+
case "jpeg":
|
|
1379
|
+
pipeline = pipeline.jpeg({ quality });
|
|
1380
|
+
break;
|
|
1381
|
+
case "png":
|
|
1382
|
+
pipeline = pipeline.png();
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
return pipeline.toBuffer();
|
|
1386
|
+
}
|
|
1387
|
+
function sleep(ms) {
|
|
1388
|
+
return new Promise((resolve) => {
|
|
1389
|
+
setTimeout(resolve, ms);
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
function emit(events, name, payload) {
|
|
1393
|
+
if (events && typeof events.emit === "function") {
|
|
1394
|
+
events.emit(name, payload);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function nowMs() {
|
|
1398
|
+
return Number(process.hrtime.bigint() / 1000000n);
|
|
1399
|
+
}
|
|
1400
|
+
function withTimeout(promise, ms, ctx) {
|
|
1401
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
1402
|
+
return promise;
|
|
1403
|
+
}
|
|
1404
|
+
let timer;
|
|
1405
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1406
|
+
timer = setTimeout(() => {
|
|
1407
|
+
reject(
|
|
1408
|
+
new CaptureTimeoutError(`${ctx.operation} exceeded ${ms}ms timeout`, {
|
|
1409
|
+
context: { operation: ctx.operation, timeoutMs: ms }
|
|
1410
|
+
})
|
|
1411
|
+
);
|
|
1412
|
+
}, ms);
|
|
1413
|
+
if (typeof timer.unref === "function") {
|
|
1414
|
+
timer.unref();
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
1418
|
+
}
|
|
1419
|
+
module.exports = { captureLongPage };
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// src/capture/element.js
|
|
1424
|
+
var require_element = __commonJS({
|
|
1425
|
+
"src/capture/element.js"(exports, module) {
|
|
1426
|
+
var { CaptureTimeoutError } = require_errors();
|
|
1427
|
+
async function captureElement(adapter, options = {}) {
|
|
1428
|
+
const {
|
|
1429
|
+
selector,
|
|
1430
|
+
xpath,
|
|
1431
|
+
snippet,
|
|
1432
|
+
padding = 10,
|
|
1433
|
+
maxDimensions = { width: 4e3, height: 4e3 },
|
|
1434
|
+
scrollIntoView = true,
|
|
1435
|
+
timeout = 5e3,
|
|
1436
|
+
format = "png",
|
|
1437
|
+
quality = 90,
|
|
1438
|
+
events = null
|
|
1439
|
+
} = options;
|
|
1440
|
+
if (!selector && !xpath) {
|
|
1441
|
+
return failure(
|
|
1442
|
+
{ selector, xpath, format },
|
|
1443
|
+
"element_not_found",
|
|
1444
|
+
null,
|
|
1445
|
+
"No selector or xpath provided"
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
const start = nowMs();
|
|
1449
|
+
emit(events, "capture:start", { format, mode: "element", selector, xpath });
|
|
1450
|
+
const lookup = await withTimeout(
|
|
1451
|
+
adapter.evaluate(findElementInPage, { selector, xpath }),
|
|
1452
|
+
timeout,
|
|
1453
|
+
{ operation: "adapter.evaluate(findElement)" }
|
|
1454
|
+
);
|
|
1455
|
+
if (!lookup || !lookup.found) {
|
|
1456
|
+
return failure({ selector, xpath, format }, "element_not_found", null, null);
|
|
1457
|
+
}
|
|
1458
|
+
if (lookup.documentRect.width === 0 || lookup.documentRect.height === 0) {
|
|
1459
|
+
return failure({ selector, xpath, format }, "element_not_visible", lookup, null);
|
|
1460
|
+
}
|
|
1461
|
+
if (lookup.documentRect.width > maxDimensions.width || lookup.documentRect.height > maxDimensions.height) {
|
|
1462
|
+
return failure({ selector, xpath, format }, "element_too_large", lookup, null);
|
|
1463
|
+
}
|
|
1464
|
+
if (snippet) {
|
|
1465
|
+
const normalize = (s) => String(s).replaceAll(/\s+/g, " ").trim();
|
|
1466
|
+
if (normalize(lookup.outerHTML) !== normalize(snippet)) {
|
|
1467
|
+
return failure(
|
|
1468
|
+
{ selector, xpath, format },
|
|
1469
|
+
"element_mismatch",
|
|
1470
|
+
lookup,
|
|
1471
|
+
"Provided snippet did not match element outerHTML"
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
let viewportRect = lookup.viewportRect;
|
|
1476
|
+
if (scrollIntoView) {
|
|
1477
|
+
await adapter.scrollTo({
|
|
1478
|
+
x: 0,
|
|
1479
|
+
y: Math.max(0, lookup.documentRect.y - padding)
|
|
1480
|
+
});
|
|
1481
|
+
const reread = await withTimeout(
|
|
1482
|
+
adapter.evaluate(findElementInPage, { selector, xpath }),
|
|
1483
|
+
timeout,
|
|
1484
|
+
{ operation: "adapter.evaluate(findElement after scroll)" }
|
|
1485
|
+
);
|
|
1486
|
+
if (reread && reread.found) {
|
|
1487
|
+
viewportRect = reread.viewportRect;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const clip = {
|
|
1491
|
+
x: Math.max(0, Math.round(viewportRect.x - padding)),
|
|
1492
|
+
y: Math.max(0, Math.round(viewportRect.y - padding)),
|
|
1493
|
+
width: Math.round(viewportRect.width + 2 * padding),
|
|
1494
|
+
height: Math.round(viewportRect.height + 2 * padding)
|
|
1495
|
+
};
|
|
1496
|
+
const screenshotOpts = { fullPage: false, type: format, clip };
|
|
1497
|
+
if (format === "jpeg") {
|
|
1498
|
+
screenshotOpts.quality = quality;
|
|
1499
|
+
}
|
|
1500
|
+
const captureStart = nowMs();
|
|
1501
|
+
const bytes = await withTimeout(adapter.screenshot(screenshotOpts), timeout, {
|
|
1502
|
+
operation: "adapter.screenshot(element)"
|
|
1503
|
+
});
|
|
1504
|
+
const captureMs = nowMs() - captureStart;
|
|
1505
|
+
const metrics = await adapter.getMetrics();
|
|
1506
|
+
const totalMs = nowMs() - start;
|
|
1507
|
+
const result = {
|
|
1508
|
+
format,
|
|
1509
|
+
bytes,
|
|
1510
|
+
page: metrics,
|
|
1511
|
+
timing: { settleMs: 0, captureMs, encodeMs: 0, totalMs },
|
|
1512
|
+
warnings: [],
|
|
1513
|
+
element: {
|
|
1514
|
+
selector: selector ?? "",
|
|
1515
|
+
xpath: xpath ?? "",
|
|
1516
|
+
documentRect: lookup.documentRect,
|
|
1517
|
+
viewportRect,
|
|
1518
|
+
verified: Boolean(snippet)
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
emit(events, "capture:complete", { format, totalMs, mode: "element" });
|
|
1522
|
+
return result;
|
|
1523
|
+
}
|
|
1524
|
+
function findElementInPage(args) {
|
|
1525
|
+
const doc = globalThis.document;
|
|
1526
|
+
let element;
|
|
1527
|
+
if (args.selector) {
|
|
1528
|
+
try {
|
|
1529
|
+
element = doc.querySelector(args.selector);
|
|
1530
|
+
} catch {
|
|
1531
|
+
element = null;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (!element && args.xpath) {
|
|
1535
|
+
try {
|
|
1536
|
+
const FIRST = 9;
|
|
1537
|
+
const res = doc.evaluate(args.xpath, doc, null, FIRST, null);
|
|
1538
|
+
element = res.singleNodeValue;
|
|
1539
|
+
} catch {
|
|
1540
|
+
element = null;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (!element) {
|
|
1544
|
+
return { found: false };
|
|
1545
|
+
}
|
|
1546
|
+
const rect = element.getBoundingClientRect();
|
|
1547
|
+
const sx = globalThis.scrollX || 0;
|
|
1548
|
+
const sy = globalThis.scrollY || 0;
|
|
1549
|
+
return {
|
|
1550
|
+
found: true,
|
|
1551
|
+
outerHTML: element.outerHTML,
|
|
1552
|
+
documentRect: {
|
|
1553
|
+
x: rect.x + sx,
|
|
1554
|
+
y: rect.y + sy,
|
|
1555
|
+
width: rect.width,
|
|
1556
|
+
height: rect.height
|
|
1557
|
+
},
|
|
1558
|
+
viewportRect: {
|
|
1559
|
+
x: rect.x,
|
|
1560
|
+
y: rect.y,
|
|
1561
|
+
width: rect.width,
|
|
1562
|
+
height: rect.height
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function failure(input, reason, lookup, verificationReason) {
|
|
1567
|
+
return {
|
|
1568
|
+
format: input.format,
|
|
1569
|
+
bytes: null,
|
|
1570
|
+
page: null,
|
|
1571
|
+
timing: { settleMs: 0, captureMs: 0, encodeMs: 0, totalMs: 0 },
|
|
1572
|
+
warnings: [],
|
|
1573
|
+
element: {
|
|
1574
|
+
selector: input.selector ?? "",
|
|
1575
|
+
xpath: input.xpath ?? "",
|
|
1576
|
+
documentRect: lookup?.documentRect ?? { x: 0, y: 0, width: 0, height: 0 },
|
|
1577
|
+
viewportRect: lookup?.viewportRect ?? { x: 0, y: 0, width: 0, height: 0 },
|
|
1578
|
+
verified: false,
|
|
1579
|
+
...verificationReason ? { verificationReason } : {}
|
|
1580
|
+
},
|
|
1581
|
+
reason
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function emit(events, name, payload) {
|
|
1585
|
+
if (events && typeof events.emit === "function") {
|
|
1586
|
+
events.emit(name, payload);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function nowMs() {
|
|
1590
|
+
return Number(process.hrtime.bigint() / 1000000n);
|
|
1591
|
+
}
|
|
1592
|
+
function withTimeout(promise, ms, ctx) {
|
|
1593
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
1594
|
+
return promise;
|
|
1595
|
+
}
|
|
1596
|
+
let timer;
|
|
1597
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1598
|
+
timer = setTimeout(() => {
|
|
1599
|
+
reject(
|
|
1600
|
+
new CaptureTimeoutError(`${ctx.operation} exceeded ${ms}ms timeout`, {
|
|
1601
|
+
context: { operation: ctx.operation, timeoutMs: ms }
|
|
1602
|
+
})
|
|
1603
|
+
);
|
|
1604
|
+
}, ms);
|
|
1605
|
+
if (typeof timer.unref === "function") {
|
|
1606
|
+
timer.unref();
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
1610
|
+
}
|
|
1611
|
+
module.exports = { captureElement };
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
// src/capture/viewport.js
|
|
1616
|
+
var require_viewport = __commonJS({
|
|
1617
|
+
"src/capture/viewport.js"(exports, module) {
|
|
1618
|
+
var { capturePage } = require_page();
|
|
1619
|
+
async function captureViewport(adapter, options = {}) {
|
|
1620
|
+
return capturePage(adapter, { ...options, fullPage: false });
|
|
1621
|
+
}
|
|
1622
|
+
module.exports = { captureViewport };
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// src/capture/responsive.js
|
|
1627
|
+
var require_responsive = __commonJS({
|
|
1628
|
+
"src/capture/responsive.js"(exports, module) {
|
|
1629
|
+
var { capturePage } = require_page();
|
|
1630
|
+
var { captureLongPage } = require_long_page();
|
|
1631
|
+
var { captureViewport } = require_viewport();
|
|
1632
|
+
async function captureResponsive(adapter, options) {
|
|
1633
|
+
if (!options || !Array.isArray(options.viewports) || options.viewports.length === 0) {
|
|
1634
|
+
const { InvalidRectError } = require_errors();
|
|
1635
|
+
throw new InvalidRectError(
|
|
1636
|
+
"captureResponsive requires options.viewports as a non-empty array",
|
|
1637
|
+
{ context: { received: options?.viewports } }
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
const { viewports, reloadBetween = true, captureMode = "fullPage", ...rest } = options;
|
|
1641
|
+
delete rest.parallelism;
|
|
1642
|
+
const captureOptions = rest;
|
|
1643
|
+
const start = nowMs();
|
|
1644
|
+
const captures = [];
|
|
1645
|
+
for (const viewport of viewports) {
|
|
1646
|
+
if (!viewport || typeof viewport.name !== "string") {
|
|
1647
|
+
const { InvalidRectError } = require_errors();
|
|
1648
|
+
throw new InvalidRectError("Each viewport must have a string name", {
|
|
1649
|
+
context: { viewport }
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
await adapter.setViewport(viewport);
|
|
1653
|
+
if (reloadBetween) {
|
|
1654
|
+
await adapter.reload({ waitUntil: "networkidle0" }).catch(() => adapter.reload());
|
|
1655
|
+
}
|
|
1656
|
+
const capture = await captureForMode(adapter, captureMode, captureOptions);
|
|
1657
|
+
captures.push({ ...capture, viewportName: viewport.name });
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
captures,
|
|
1661
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1662
|
+
totalMs: nowMs() - start
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
async function captureForMode(adapter, mode, captureOptions) {
|
|
1666
|
+
switch (mode) {
|
|
1667
|
+
case "viewport":
|
|
1668
|
+
return captureViewport(adapter, captureOptions);
|
|
1669
|
+
case "longPage":
|
|
1670
|
+
return captureLongPage(adapter, captureOptions);
|
|
1671
|
+
default:
|
|
1672
|
+
return capturePage(adapter, { ...captureOptions, fullPage: true });
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
function nowMs() {
|
|
1676
|
+
return Number(process.hrtime.bigint() / 1000000n);
|
|
1677
|
+
}
|
|
1678
|
+
module.exports = { captureResponsive };
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// src/capture/index.js
|
|
1683
|
+
var require_capture2 = __commonJS({
|
|
1684
|
+
"src/capture/index.js"(exports, module) {
|
|
1685
|
+
var { capturePage } = require_page();
|
|
1686
|
+
var { waitForStable } = require_settle();
|
|
1687
|
+
var { captureLongPage } = require_long_page();
|
|
1688
|
+
var { captureElement } = require_element();
|
|
1689
|
+
var { captureViewport } = require_viewport();
|
|
1690
|
+
var { captureResponsive } = require_responsive();
|
|
1691
|
+
module.exports = {
|
|
1692
|
+
capturePage,
|
|
1693
|
+
waitForStable,
|
|
1694
|
+
captureLongPage,
|
|
1695
|
+
captureElement,
|
|
1696
|
+
captureViewport,
|
|
1697
|
+
captureResponsive
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
// src/transform/crop.js
|
|
1703
|
+
var require_crop = __commonJS({
|
|
1704
|
+
"src/transform/crop.js"(exports, module) {
|
|
1705
|
+
var { InvalidRectError, BufferCorruptError } = require_errors();
|
|
1706
|
+
async function crop(input, rect, options = {}) {
|
|
1707
|
+
assertBuffer(input);
|
|
1708
|
+
assertRect(rect);
|
|
1709
|
+
const { padding = 0, format, quality = 90 } = options;
|
|
1710
|
+
const sharp = __require("sharp");
|
|
1711
|
+
const meta = await readMeta(sharp, input);
|
|
1712
|
+
const extract = computeExtract(rect, padding, meta);
|
|
1713
|
+
if (extract.width <= 0 || extract.height <= 0) {
|
|
1714
|
+
throw new InvalidRectError(
|
|
1715
|
+
`Rect resolves to 0\xD70 after clamping to image bounds (${meta.width}\xD7${meta.height})`,
|
|
1716
|
+
{ context: { rect, padding, imageWidth: meta.width, imageHeight: meta.height } }
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
let pipeline = sharp(input).extract(extract);
|
|
1720
|
+
pipeline = applyFormat(pipeline, format, quality);
|
|
1721
|
+
return pipeline.toBuffer();
|
|
1722
|
+
}
|
|
1723
|
+
async function cropMany(input, entries, options = {}) {
|
|
1724
|
+
assertBuffer(input);
|
|
1725
|
+
if (!Array.isArray(entries)) {
|
|
1726
|
+
throw new InvalidRectError("cropMany entries must be an array", {
|
|
1727
|
+
context: { received: typeof entries }
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
const { padding = 0, format, quality = 90, parallelism = 4 } = options;
|
|
1731
|
+
const sharp = __require("sharp");
|
|
1732
|
+
const meta = await readMeta(sharp, input);
|
|
1733
|
+
const planned = entries.map((entry) => {
|
|
1734
|
+
if (!entry || typeof entry.id !== "string") {
|
|
1735
|
+
throw new InvalidRectError("Each cropMany entry must have a string id and a rect", {
|
|
1736
|
+
context: { entry }
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
assertRect(entry.rect);
|
|
1740
|
+
const extract = computeExtract(entry.rect, padding, meta);
|
|
1741
|
+
if (extract.width <= 0 || extract.height <= 0) {
|
|
1742
|
+
throw new InvalidRectError(
|
|
1743
|
+
`Rect for id ${JSON.stringify(entry.id)} resolves to 0\xD70 after clamping`,
|
|
1744
|
+
{
|
|
1745
|
+
context: {
|
|
1746
|
+
id: entry.id,
|
|
1747
|
+
rect: entry.rect,
|
|
1748
|
+
padding,
|
|
1749
|
+
imageWidth: meta.width,
|
|
1750
|
+
imageHeight: meta.height
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
return { id: entry.id, extract };
|
|
1756
|
+
});
|
|
1757
|
+
const results = /* @__PURE__ */ new Map();
|
|
1758
|
+
await parallelEach(
|
|
1759
|
+
planned,
|
|
1760
|
+
async ({ id, extract }) => {
|
|
1761
|
+
let pipeline = sharp(input).extract(extract);
|
|
1762
|
+
pipeline = applyFormat(pipeline, format, quality);
|
|
1763
|
+
const buffer = await pipeline.toBuffer();
|
|
1764
|
+
results.set(id, buffer);
|
|
1765
|
+
},
|
|
1766
|
+
Math.max(1, parallelism)
|
|
1767
|
+
);
|
|
1768
|
+
return results;
|
|
1769
|
+
}
|
|
1770
|
+
async function parallelEach(items, fn, limit) {
|
|
1771
|
+
let cursor = 0;
|
|
1772
|
+
const workers = Array.from({ length: Math.min(limit, items.length || 1) }, async () => {
|
|
1773
|
+
while (cursor < items.length) {
|
|
1774
|
+
const i = cursor;
|
|
1775
|
+
cursor += 1;
|
|
1776
|
+
await fn(items[i], i);
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
await Promise.all(workers);
|
|
1780
|
+
}
|
|
1781
|
+
function assertRect(rect) {
|
|
1782
|
+
if (rect === null || typeof rect !== "object") {
|
|
1783
|
+
throw new InvalidRectError("rect must be an object", {
|
|
1784
|
+
context: { received: typeof rect }
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
const { x, y, width, height } = (
|
|
1788
|
+
/** @type {Record<string, unknown>} */
|
|
1789
|
+
rect
|
|
1790
|
+
);
|
|
1791
|
+
for (const [key, value] of [
|
|
1792
|
+
["x", x],
|
|
1793
|
+
["y", y],
|
|
1794
|
+
["width", width],
|
|
1795
|
+
["height", height]
|
|
1796
|
+
]) {
|
|
1797
|
+
if (!Number.isFinite(value)) {
|
|
1798
|
+
throw new InvalidRectError(`rect.${key} must be a finite number`, {
|
|
1799
|
+
context: { rect }
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (width <= 0 || height <= 0) {
|
|
1804
|
+
throw new InvalidRectError("rect.width and rect.height must be > 0", {
|
|
1805
|
+
context: { rect }
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function assertBuffer(input) {
|
|
1810
|
+
if (!Buffer.isBuffer(input)) {
|
|
1811
|
+
throw new BufferCorruptError("input must be a Buffer", {
|
|
1812
|
+
context: { received: typeof input }
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function computeExtract(rect, padding, meta) {
|
|
1817
|
+
const left = clamp(Math.round(rect.x - padding), 0, meta.width);
|
|
1818
|
+
const top = clamp(Math.round(rect.y - padding), 0, meta.height);
|
|
1819
|
+
const right = clamp(Math.round(rect.x + rect.width + padding), 0, meta.width);
|
|
1820
|
+
const bottom = clamp(Math.round(rect.y + rect.height + padding), 0, meta.height);
|
|
1821
|
+
return {
|
|
1822
|
+
left,
|
|
1823
|
+
top,
|
|
1824
|
+
width: Math.max(0, right - left),
|
|
1825
|
+
height: Math.max(0, bottom - top)
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function clamp(n, lo, hi) {
|
|
1829
|
+
if (n < lo) {
|
|
1830
|
+
return lo;
|
|
1831
|
+
}
|
|
1832
|
+
if (n > hi) {
|
|
1833
|
+
return hi;
|
|
1834
|
+
}
|
|
1835
|
+
return n;
|
|
1836
|
+
}
|
|
1837
|
+
async function readMeta(sharp, input) {
|
|
1838
|
+
try {
|
|
1839
|
+
const meta = await sharp(input).metadata();
|
|
1840
|
+
if (!meta.width || !meta.height) {
|
|
1841
|
+
throw new BufferCorruptError("Image has no detectable dimensions", {
|
|
1842
|
+
context: { meta }
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
return { width: meta.width, height: meta.height };
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
if (error instanceof BufferCorruptError) {
|
|
1848
|
+
throw error;
|
|
1849
|
+
}
|
|
1850
|
+
throw new BufferCorruptError("sharp could not read input buffer metadata", {
|
|
1851
|
+
cause: error
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
function applyFormat(pipeline, format, quality) {
|
|
1856
|
+
if (!format) {
|
|
1857
|
+
return pipeline;
|
|
1858
|
+
}
|
|
1859
|
+
switch (format) {
|
|
1860
|
+
case "png":
|
|
1861
|
+
return pipeline.png();
|
|
1862
|
+
case "jpeg":
|
|
1863
|
+
return pipeline.jpeg({ quality });
|
|
1864
|
+
case "webp":
|
|
1865
|
+
return pipeline.webp({ quality });
|
|
1866
|
+
default:
|
|
1867
|
+
return pipeline;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
module.exports = { crop, cropMany };
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
// src/transform/convert.js
|
|
1875
|
+
var require_convert = __commonJS({
|
|
1876
|
+
"src/transform/convert.js"(exports, module) {
|
|
1877
|
+
var { BufferCorruptError, UnsupportedFormatError } = require_errors();
|
|
1878
|
+
async function convert(input, options) {
|
|
1879
|
+
assertBuffer(input);
|
|
1880
|
+
if (!options || typeof options !== "object") {
|
|
1881
|
+
throw new UnsupportedFormatError("convert requires an options object with a format", {
|
|
1882
|
+
context: { received: typeof options }
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
const { format, quality = 90, lossless = false, effort = 4 } = options;
|
|
1886
|
+
if (!["png", "jpeg", "webp"].includes(format)) {
|
|
1887
|
+
throw new UnsupportedFormatError(
|
|
1888
|
+
`format must be 'png', 'jpeg', or 'webp'; received ${JSON.stringify(format)}`,
|
|
1889
|
+
{ context: { received: format } }
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
const sharp = __require("sharp");
|
|
1893
|
+
let pipeline = sharp(input);
|
|
1894
|
+
switch (format) {
|
|
1895
|
+
case "png":
|
|
1896
|
+
pipeline = pipeline.png();
|
|
1897
|
+
break;
|
|
1898
|
+
case "jpeg":
|
|
1899
|
+
pipeline = pipeline.jpeg({ quality });
|
|
1900
|
+
break;
|
|
1901
|
+
case "webp":
|
|
1902
|
+
pipeline = pipeline.webp({ quality, lossless, effort });
|
|
1903
|
+
break;
|
|
1904
|
+
}
|
|
1905
|
+
return pipeline.toBuffer();
|
|
1906
|
+
}
|
|
1907
|
+
function assertBuffer(input) {
|
|
1908
|
+
if (!Buffer.isBuffer(input)) {
|
|
1909
|
+
throw new BufferCorruptError("input must be a Buffer", {
|
|
1910
|
+
context: { received: typeof input }
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
module.exports = { convert };
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
// src/transform/resize.js
|
|
1919
|
+
var require_resize = __commonJS({
|
|
1920
|
+
"src/transform/resize.js"(exports, module) {
|
|
1921
|
+
var { BufferCorruptError, InvalidRectError } = require_errors();
|
|
1922
|
+
async function resize(input, options) {
|
|
1923
|
+
assertBuffer(input);
|
|
1924
|
+
if (!options || typeof options !== "object") {
|
|
1925
|
+
throw new InvalidRectError("resize requires an options object", {
|
|
1926
|
+
context: { received: typeof options }
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
const {
|
|
1930
|
+
width,
|
|
1931
|
+
height,
|
|
1932
|
+
fit,
|
|
1933
|
+
position,
|
|
1934
|
+
kernel,
|
|
1935
|
+
withoutEnlargement,
|
|
1936
|
+
format,
|
|
1937
|
+
quality = 90
|
|
1938
|
+
} = options;
|
|
1939
|
+
if (width === void 0 && height === void 0) {
|
|
1940
|
+
throw new InvalidRectError("resize requires at least one of width or height", {
|
|
1941
|
+
context: { options }
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
if (width !== void 0 && (!Number.isFinite(width) || width <= 0)) {
|
|
1945
|
+
throw new InvalidRectError("resize.width must be a positive finite number", {
|
|
1946
|
+
context: { width }
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
if (height !== void 0 && (!Number.isFinite(height) || height <= 0)) {
|
|
1950
|
+
throw new InvalidRectError("resize.height must be a positive finite number", {
|
|
1951
|
+
context: { height }
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
const sharp = __require("sharp");
|
|
1955
|
+
const resizeOpts = {};
|
|
1956
|
+
if (width !== void 0) {
|
|
1957
|
+
resizeOpts.width = Math.round(width);
|
|
1958
|
+
}
|
|
1959
|
+
if (height !== void 0) {
|
|
1960
|
+
resizeOpts.height = Math.round(height);
|
|
1961
|
+
}
|
|
1962
|
+
if (fit) {
|
|
1963
|
+
resizeOpts.fit = fit;
|
|
1964
|
+
}
|
|
1965
|
+
if (position) {
|
|
1966
|
+
resizeOpts.position = position;
|
|
1967
|
+
}
|
|
1968
|
+
if (kernel) {
|
|
1969
|
+
resizeOpts.kernel = kernel;
|
|
1970
|
+
}
|
|
1971
|
+
if (withoutEnlargement !== void 0) {
|
|
1972
|
+
resizeOpts.withoutEnlargement = withoutEnlargement;
|
|
1973
|
+
}
|
|
1974
|
+
let pipeline = sharp(input).resize(resizeOpts);
|
|
1975
|
+
switch (format) {
|
|
1976
|
+
case "png":
|
|
1977
|
+
pipeline = pipeline.png();
|
|
1978
|
+
break;
|
|
1979
|
+
case "jpeg":
|
|
1980
|
+
pipeline = pipeline.jpeg({ quality });
|
|
1981
|
+
break;
|
|
1982
|
+
case "webp":
|
|
1983
|
+
pipeline = pipeline.webp({ quality });
|
|
1984
|
+
break;
|
|
1985
|
+
}
|
|
1986
|
+
return pipeline.toBuffer();
|
|
1987
|
+
}
|
|
1988
|
+
function assertBuffer(input) {
|
|
1989
|
+
if (!Buffer.isBuffer(input)) {
|
|
1990
|
+
throw new BufferCorruptError("input must be a Buffer", {
|
|
1991
|
+
context: { received: typeof input }
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
module.exports = { resize };
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
// src/transform/normalize.js
|
|
2000
|
+
var require_normalize = __commonJS({
|
|
2001
|
+
"src/transform/normalize.js"(exports, module) {
|
|
2002
|
+
var { BufferCorruptError } = require_errors();
|
|
2003
|
+
async function normalize(input, options = {}) {
|
|
2004
|
+
assertBuffer(input);
|
|
2005
|
+
const sharp = __require("sharp");
|
|
2006
|
+
const meta = await readMeta(sharp, input);
|
|
2007
|
+
const { fromDpr = inferDpr(meta), toDpr = 1 } = options;
|
|
2008
|
+
if (!Number.isFinite(fromDpr) || fromDpr <= 0) {
|
|
2009
|
+
throw new BufferCorruptError("Could not infer source DPR and no fromDpr supplied", {
|
|
2010
|
+
context: { meta, fromDpr }
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
const scale = toDpr / fromDpr;
|
|
2014
|
+
if (scale === 1) {
|
|
2015
|
+
return {
|
|
2016
|
+
buffer: input,
|
|
2017
|
+
dimensions: {
|
|
2018
|
+
width: Math.round(meta.width / fromDpr),
|
|
2019
|
+
height: Math.round(meta.height / fromDpr)
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
const targetWidth = Math.max(1, Math.round(meta.width * scale));
|
|
2024
|
+
const targetHeight = Math.max(1, Math.round(meta.height * scale));
|
|
2025
|
+
const buffer = await sharp(input).resize({ width: targetWidth, height: targetHeight }).toBuffer();
|
|
2026
|
+
return {
|
|
2027
|
+
buffer,
|
|
2028
|
+
dimensions: { width: targetWidth, height: targetHeight }
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
function inferDpr(meta) {
|
|
2032
|
+
if (meta && Number.isFinite(meta.density) && meta.density > 0) {
|
|
2033
|
+
return meta.density / 96;
|
|
2034
|
+
}
|
|
2035
|
+
return 1;
|
|
2036
|
+
}
|
|
2037
|
+
function assertBuffer(input) {
|
|
2038
|
+
if (!Buffer.isBuffer(input)) {
|
|
2039
|
+
throw new BufferCorruptError("input must be a Buffer", {
|
|
2040
|
+
context: { received: typeof input }
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
async function readMeta(sharp, input) {
|
|
2045
|
+
try {
|
|
2046
|
+
return await sharp(input).metadata();
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
throw new BufferCorruptError("sharp could not read input buffer metadata", {
|
|
2049
|
+
cause: error
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
module.exports = { normalize };
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
// src/transform/index.js
|
|
2058
|
+
var require_transform2 = __commonJS({
|
|
2059
|
+
"src/transform/index.js"(exports, module) {
|
|
2060
|
+
var { crop, cropMany } = require_crop();
|
|
2061
|
+
var { convert } = require_convert();
|
|
2062
|
+
var { resize } = require_resize();
|
|
2063
|
+
var { normalize } = require_normalize();
|
|
2064
|
+
module.exports = { crop, cropMany, convert, resize, normalize };
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
// src/tile/dzi.js
|
|
2069
|
+
var require_dzi = __commonJS({
|
|
2070
|
+
"src/tile/dzi.js"(exports, module) {
|
|
2071
|
+
var fs = __require("fs/promises");
|
|
2072
|
+
var path = __require("path");
|
|
2073
|
+
var os = __require("os");
|
|
2074
|
+
var { randomUUID } = __require("crypto");
|
|
2075
|
+
var { TileSizeError, TileWriteError, BufferCorruptError } = require_errors();
|
|
2076
|
+
var MAX_TILES = 1e4;
|
|
2077
|
+
async function tileDzi(input, options = {}) {
|
|
2078
|
+
assertBuffer(input);
|
|
2079
|
+
const {
|
|
2080
|
+
tileSize = 256,
|
|
2081
|
+
overlap = 1,
|
|
2082
|
+
tileFormat = "webp",
|
|
2083
|
+
quality = 80,
|
|
2084
|
+
output = "buffers",
|
|
2085
|
+
name
|
|
2086
|
+
} = options;
|
|
2087
|
+
if (tileSize !== 256 && tileSize !== 512) {
|
|
2088
|
+
throw new TileSizeError(`tileSize must be 256 or 512; received ${tileSize}`, {
|
|
2089
|
+
context: { tileSize }
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
if (output === "stream") {
|
|
2093
|
+
throw new TileSizeError(
|
|
2094
|
+
"output: 'stream' is deferred to v1.1; use 'buffers' or 'directory'",
|
|
2095
|
+
{ context: { output } }
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
if (!["buffers", "directory"].includes(output)) {
|
|
2099
|
+
throw new TileSizeError(`output must be 'buffers' or 'directory'; received ${output}`, {
|
|
2100
|
+
context: { output }
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
const sharp = __require("sharp");
|
|
2104
|
+
const meta = await readMeta(sharp, input);
|
|
2105
|
+
const totalEstimate = estimateTileCount(meta.width, meta.height, tileSize);
|
|
2106
|
+
if (totalEstimate > MAX_TILES) {
|
|
2107
|
+
throw new TileSizeError(
|
|
2108
|
+
`Estimated tile count ${totalEstimate} exceeds the ${MAX_TILES} ceiling for a single tileDzi() call`,
|
|
2109
|
+
{
|
|
2110
|
+
context: {
|
|
2111
|
+
width: meta.width,
|
|
2112
|
+
height: meta.height,
|
|
2113
|
+
tileSize,
|
|
2114
|
+
estimate: totalEstimate
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
const dirName = name ?? `dzi-${randomUUID()}`;
|
|
2120
|
+
let tempDir;
|
|
2121
|
+
try {
|
|
2122
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "screenshot-utils-dzi-"));
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
throw new TileWriteError("Failed to create temp directory for DZI output", {
|
|
2125
|
+
cause: error
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
const outputBase = path.join(tempDir, dirName);
|
|
2129
|
+
try {
|
|
2130
|
+
await applyTileFormat(sharp(input), tileFormat, quality).tile({ size: tileSize, overlap, layout: "dz" }).toFile(outputBase);
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2133
|
+
});
|
|
2134
|
+
throw new TileWriteError("sharp().tile().toFile() failed", { cause: error });
|
|
2135
|
+
}
|
|
2136
|
+
const dziXml = await readDziXml(outputBase);
|
|
2137
|
+
const tiles = await collectTiles({
|
|
2138
|
+
outputBase,
|
|
2139
|
+
dirName,
|
|
2140
|
+
output
|
|
2141
|
+
});
|
|
2142
|
+
if (output === "buffers") {
|
|
2143
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
descriptor: {
|
|
2148
|
+
xml: dziXml,
|
|
2149
|
+
format: tileFormat,
|
|
2150
|
+
width: meta.width,
|
|
2151
|
+
height: meta.height,
|
|
2152
|
+
tileSize,
|
|
2153
|
+
overlap
|
|
2154
|
+
},
|
|
2155
|
+
tiles,
|
|
2156
|
+
outputDir: output === "directory" ? tempDir : void 0,
|
|
2157
|
+
totalBytes: tiles.reduce((sum, t) => sum + (t.bytes ? t.bytes.length : 0), 0),
|
|
2158
|
+
totalTiles: tiles.length
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function estimateTileCount(width, height, tileSize) {
|
|
2162
|
+
let total = 0;
|
|
2163
|
+
let w = width;
|
|
2164
|
+
let h = height;
|
|
2165
|
+
while (w >= 1 && h >= 1) {
|
|
2166
|
+
const cols = Math.ceil(w / tileSize);
|
|
2167
|
+
const rows = Math.ceil(h / tileSize);
|
|
2168
|
+
total += cols * rows;
|
|
2169
|
+
if (w === 1 && h === 1) {
|
|
2170
|
+
break;
|
|
2171
|
+
}
|
|
2172
|
+
w = Math.max(1, Math.ceil(w / 2));
|
|
2173
|
+
h = Math.max(1, Math.ceil(h / 2));
|
|
2174
|
+
}
|
|
2175
|
+
return total;
|
|
2176
|
+
}
|
|
2177
|
+
function applyTileFormat(pipeline, format, quality) {
|
|
2178
|
+
switch (format) {
|
|
2179
|
+
case "png":
|
|
2180
|
+
return pipeline.png();
|
|
2181
|
+
case "jpeg":
|
|
2182
|
+
return pipeline.jpeg({ quality });
|
|
2183
|
+
case "webp":
|
|
2184
|
+
return pipeline.webp({ quality });
|
|
2185
|
+
default:
|
|
2186
|
+
throw new TileSizeError(`tileFormat must be png, jpeg, or webp; received ${format}`, {
|
|
2187
|
+
context: { tileFormat: format }
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
async function readDziXml(outputBase) {
|
|
2192
|
+
try {
|
|
2193
|
+
return await fs.readFile(`${outputBase}.dzi`, "utf8");
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
throw new TileWriteError("Sharp did not produce the .dzi descriptor file", {
|
|
2196
|
+
cause: error
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
async function collectTiles({ outputBase, dirName, output }) {
|
|
2201
|
+
const filesDir = `${outputBase}_files`;
|
|
2202
|
+
const tiles = [];
|
|
2203
|
+
let levels;
|
|
2204
|
+
try {
|
|
2205
|
+
levels = await fs.readdir(filesDir);
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
throw new TileWriteError("Sharp did not produce the _files directory", {
|
|
2208
|
+
cause: error
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
const levelNums = levels.map(Number).filter((n) => Number.isFinite(n)).toSorted((a, b) => a - b);
|
|
2212
|
+
for (const level of levelNums) {
|
|
2213
|
+
const levelDir = path.join(filesDir, String(level));
|
|
2214
|
+
const dirEntries = await fs.readdir(levelDir);
|
|
2215
|
+
const fileNames = dirEntries.toSorted();
|
|
2216
|
+
for (const fileName of fileNames) {
|
|
2217
|
+
const match = /^(\d+)_(\d+)\.\w+$/.exec(fileName);
|
|
2218
|
+
if (!match) {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
const col = Number(match[1]);
|
|
2222
|
+
const row = Number(match[2]);
|
|
2223
|
+
const relPath = `${dirName}_files/${level}/${fileName}`;
|
|
2224
|
+
const entry = {
|
|
2225
|
+
level,
|
|
2226
|
+
col,
|
|
2227
|
+
row,
|
|
2228
|
+
path: relPath
|
|
2229
|
+
};
|
|
2230
|
+
if (output === "buffers") {
|
|
2231
|
+
entry.bytes = await fs.readFile(path.join(levelDir, fileName));
|
|
2232
|
+
}
|
|
2233
|
+
tiles.push(entry);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
return tiles;
|
|
2237
|
+
}
|
|
2238
|
+
async function readMeta(sharp, input) {
|
|
2239
|
+
try {
|
|
2240
|
+
const meta = await sharp(input).metadata();
|
|
2241
|
+
if (!meta.width || !meta.height) {
|
|
2242
|
+
throw new BufferCorruptError("Input image has no detectable dimensions");
|
|
2243
|
+
}
|
|
2244
|
+
return { width: meta.width, height: meta.height };
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
if (error instanceof BufferCorruptError) {
|
|
2247
|
+
throw error;
|
|
2248
|
+
}
|
|
2249
|
+
throw new BufferCorruptError("sharp could not read tile input buffer", {
|
|
2250
|
+
cause: error
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function assertBuffer(input) {
|
|
2255
|
+
if (!Buffer.isBuffer(input)) {
|
|
2256
|
+
throw new BufferCorruptError("input must be a Buffer", {
|
|
2257
|
+
context: { received: typeof input }
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
module.exports = { tileDzi, MAX_TILES };
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
// src/tile/index.js
|
|
2266
|
+
var require_tile2 = __commonJS({
|
|
2267
|
+
"src/tile/index.js"(exports, module) {
|
|
2268
|
+
var { tileDzi } = require_dzi();
|
|
2269
|
+
module.exports = { tileDzi };
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
|
|
2273
|
+
// src/compose/theme.js
|
|
2274
|
+
var require_theme = __commonJS({
|
|
2275
|
+
"src/compose/theme.js"(exports, module) {
|
|
2276
|
+
var defaultTheme = Object.freeze({
|
|
2277
|
+
"severity-critical": Object.freeze({
|
|
2278
|
+
stroke: "#D32F2F",
|
|
2279
|
+
strokeWidth: 3,
|
|
2280
|
+
fill: "#D32F2F22",
|
|
2281
|
+
badge: Object.freeze({ bg: "#D32F2F", fg: "#fff" })
|
|
2282
|
+
}),
|
|
2283
|
+
"severity-serious": Object.freeze({
|
|
2284
|
+
stroke: "#F57C00",
|
|
2285
|
+
strokeWidth: 3,
|
|
2286
|
+
fill: "#F57C0022",
|
|
2287
|
+
badge: Object.freeze({ bg: "#F57C00", fg: "#fff" })
|
|
2288
|
+
}),
|
|
2289
|
+
"severity-moderate": Object.freeze({
|
|
2290
|
+
stroke: "#FBC02D",
|
|
2291
|
+
strokeWidth: 2,
|
|
2292
|
+
fill: "#FBC02D22",
|
|
2293
|
+
badge: Object.freeze({ bg: "#FBC02D", fg: "#000" })
|
|
2294
|
+
}),
|
|
2295
|
+
"severity-minor": Object.freeze({
|
|
2296
|
+
stroke: "#1976D2",
|
|
2297
|
+
strokeWidth: 2,
|
|
2298
|
+
fill: "#1976D222",
|
|
2299
|
+
badge: Object.freeze({ bg: "#1976D2", fg: "#fff" })
|
|
2300
|
+
})
|
|
2301
|
+
});
|
|
2302
|
+
module.exports = { defaultTheme };
|
|
2303
|
+
}
|
|
2304
|
+
});
|
|
2305
|
+
|
|
2306
|
+
// src/compose/annotate.js
|
|
2307
|
+
var require_annotate = __commonJS({
|
|
2308
|
+
"src/compose/annotate.js"(exports, module) {
|
|
2309
|
+
var {
|
|
2310
|
+
BufferCorruptError,
|
|
2311
|
+
ThemeError,
|
|
2312
|
+
HeatmapPointError,
|
|
2313
|
+
ComposeError
|
|
2314
|
+
} = require_errors();
|
|
2315
|
+
var { defaultTheme } = require_theme();
|
|
2316
|
+
var MAX_ANNOTATIONS = 5e3;
|
|
2317
|
+
var VALID_LEGEND_POSITIONS = /* @__PURE__ */ new Set([
|
|
2318
|
+
"none",
|
|
2319
|
+
"top-left",
|
|
2320
|
+
"top-right",
|
|
2321
|
+
"bottom-left",
|
|
2322
|
+
"bottom-right"
|
|
2323
|
+
]);
|
|
2324
|
+
async function annotate(input, annotations, options = {}) {
|
|
2325
|
+
assertBuffer(input);
|
|
2326
|
+
if (!Array.isArray(annotations)) {
|
|
2327
|
+
throw new HeatmapPointError("annotations must be an array", {
|
|
2328
|
+
context: { received: typeof annotations }
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
if (annotations.length > MAX_ANNOTATIONS) {
|
|
2332
|
+
throw new ComposeError(
|
|
2333
|
+
`Cannot annotate more than ${MAX_ANNOTATIONS} regions in a single call`,
|
|
2334
|
+
{ context: { received: annotations.length, max: MAX_ANNOTATIONS } }
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
const { theme: callerTheme, output = "png", legend = "none" } = options;
|
|
2338
|
+
if (!VALID_LEGEND_POSITIONS.has(legend)) {
|
|
2339
|
+
throw new ThemeError(
|
|
2340
|
+
`legend must be one of ${[...VALID_LEGEND_POSITIONS].join(", ")}; received ${JSON.stringify(legend)}`,
|
|
2341
|
+
{ context: { received: legend } }
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
if (output !== "png" && output !== "svg") {
|
|
2345
|
+
throw new ComposeError(
|
|
2346
|
+
`annotate output must be 'png' or 'svg'; received ${JSON.stringify(output)}`,
|
|
2347
|
+
{ context: { received: output } }
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
const theme = mergeTheme(defaultTheme, callerTheme);
|
|
2351
|
+
for (const [i, ann] of annotations.entries()) {
|
|
2352
|
+
assertAnnotation(ann, i, theme);
|
|
2353
|
+
}
|
|
2354
|
+
const sharp = __require("sharp");
|
|
2355
|
+
const meta = await readMeta(sharp, input);
|
|
2356
|
+
const svg = buildSvg(meta, annotations, theme, legend);
|
|
2357
|
+
if (output === "svg") {
|
|
2358
|
+
return Buffer.from(svg, "utf8");
|
|
2359
|
+
}
|
|
2360
|
+
return sharp(input).composite([{ input: Buffer.from(svg, "utf8"), top: 0, left: 0 }]).toBuffer();
|
|
2361
|
+
}
|
|
2362
|
+
function mergeTheme(base, extra) {
|
|
2363
|
+
if (!extra) {
|
|
2364
|
+
return base;
|
|
2365
|
+
}
|
|
2366
|
+
const merged = { ...base };
|
|
2367
|
+
for (const [key, style] of Object.entries(extra)) {
|
|
2368
|
+
if (!style || typeof style !== "object") {
|
|
2369
|
+
throw new ThemeError(`Theme key '${key}' must be an object`, {
|
|
2370
|
+
context: { key, received: typeof style }
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
if (typeof style.stroke !== "string") {
|
|
2374
|
+
throw new ThemeError(`Theme key '${key}' must have a 'stroke' color string`, {
|
|
2375
|
+
context: { key }
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
merged[key] = { ...base[key], ...style };
|
|
2379
|
+
}
|
|
2380
|
+
return merged;
|
|
2381
|
+
}
|
|
2382
|
+
function assertAnnotation(ann, index, theme) {
|
|
2383
|
+
if (!ann || typeof ann !== "object") {
|
|
2384
|
+
throw new HeatmapPointError(`annotations[${index}] must be an object`, {
|
|
2385
|
+
context: { index, received: typeof ann }
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
const a = (
|
|
2389
|
+
/** @type {Record<string, unknown>} */
|
|
2390
|
+
ann
|
|
2391
|
+
);
|
|
2392
|
+
const rect = (
|
|
2393
|
+
/** @type {Record<string, unknown> | undefined} */
|
|
2394
|
+
a.rect
|
|
2395
|
+
);
|
|
2396
|
+
if (!rect || !Number.isFinite(rect.x) || !Number.isFinite(rect.y) || !Number.isFinite(rect.width) || !Number.isFinite(rect.height)) {
|
|
2397
|
+
throw new HeatmapPointError(`annotations[${index}].rect must be a finite Rect`, {
|
|
2398
|
+
context: { index, rect }
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
if (typeof a.style !== "string") {
|
|
2402
|
+
throw new HeatmapPointError(`annotations[${index}].style must be a theme key string`, {
|
|
2403
|
+
context: { index, style: a.style }
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
if (!theme[a.style]) {
|
|
2407
|
+
throw new ThemeError(
|
|
2408
|
+
`annotations[${index}].style '${a.style}' has no matching theme entry`,
|
|
2409
|
+
{ context: { index, style: a.style, available: Object.keys(theme) } }
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
function buildSvg(meta, annotations, theme, legend) {
|
|
2414
|
+
const layers = annotations.map((ann) => svgLayerForAnnotation(ann, theme[ann.style])).join("\n");
|
|
2415
|
+
const legendSvg = legend === "none" ? "" : svgLegend(meta, annotations, theme, legend);
|
|
2416
|
+
return [
|
|
2417
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${meta.width}" height="${meta.height}" viewBox="0 0 ${meta.width} ${meta.height}">`,
|
|
2418
|
+
'<g font-family="system-ui, -apple-system, Segoe UI, sans-serif" font-size="14" font-weight="bold">',
|
|
2419
|
+
layers,
|
|
2420
|
+
legendSvg,
|
|
2421
|
+
"</g>",
|
|
2422
|
+
"</svg>"
|
|
2423
|
+
].join("\n");
|
|
2424
|
+
}
|
|
2425
|
+
function svgLayerForAnnotation(ann, style) {
|
|
2426
|
+
const { x, y, width, height } = ann.rect;
|
|
2427
|
+
const parts = [
|
|
2428
|
+
`<rect x="${x}" y="${y}" width="${width}" height="${height}" stroke="${escapeAttr(style.stroke)}" stroke-width="${style.strokeWidth}" fill="${escapeAttr(style.fill ?? "none")}"/>`
|
|
2429
|
+
];
|
|
2430
|
+
if (ann.label && style.badge) {
|
|
2431
|
+
const badgeSize = 20;
|
|
2432
|
+
const badgeX = x - badgeSize / 2;
|
|
2433
|
+
const badgeY = y - badgeSize / 2;
|
|
2434
|
+
parts.push(
|
|
2435
|
+
`<rect x="${badgeX}" y="${badgeY}" width="${badgeSize}" height="${badgeSize}" rx="${badgeSize / 2}" fill="${escapeAttr(style.badge.bg)}"/>`,
|
|
2436
|
+
`<text x="${badgeX + badgeSize / 2}" y="${badgeY + badgeSize / 2 + 4}" text-anchor="middle" fill="${escapeAttr(style.badge.fg)}">${escapeText(String(ann.label))}</text>`
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
return parts.join("");
|
|
2440
|
+
}
|
|
2441
|
+
function svgLegend(meta, annotations, theme, position) {
|
|
2442
|
+
const usedKeys = [...new Set(annotations.map((a) => a.style))];
|
|
2443
|
+
if (usedKeys.length === 0) {
|
|
2444
|
+
return "";
|
|
2445
|
+
}
|
|
2446
|
+
const rowHeight = 22;
|
|
2447
|
+
const padding = 10;
|
|
2448
|
+
const swatchSize = 14;
|
|
2449
|
+
const charWidth = 8;
|
|
2450
|
+
const labelWidths = usedKeys.map((k) => k.length * charWidth);
|
|
2451
|
+
const width = padding * 2 + swatchSize + 6 + Math.max(...labelWidths);
|
|
2452
|
+
const height = padding * 2 + usedKeys.length * rowHeight;
|
|
2453
|
+
let x;
|
|
2454
|
+
let y;
|
|
2455
|
+
switch (position) {
|
|
2456
|
+
case "top-left":
|
|
2457
|
+
x = padding;
|
|
2458
|
+
y = padding;
|
|
2459
|
+
break;
|
|
2460
|
+
case "top-right":
|
|
2461
|
+
x = meta.width - width - padding;
|
|
2462
|
+
y = padding;
|
|
2463
|
+
break;
|
|
2464
|
+
case "bottom-left":
|
|
2465
|
+
x = padding;
|
|
2466
|
+
y = meta.height - height - padding;
|
|
2467
|
+
break;
|
|
2468
|
+
default:
|
|
2469
|
+
x = meta.width - width - padding;
|
|
2470
|
+
y = meta.height - height - padding;
|
|
2471
|
+
}
|
|
2472
|
+
const rows = usedKeys.map((key, i) => {
|
|
2473
|
+
const style = theme[key];
|
|
2474
|
+
const rowY = y + padding + i * rowHeight;
|
|
2475
|
+
return `<rect x="${x + padding}" y="${rowY}" width="${swatchSize}" height="${swatchSize}" fill="${escapeAttr(style.fill ?? "none")}" stroke="${escapeAttr(style.stroke)}" stroke-width="2"/><text x="${x + padding + swatchSize + 6}" y="${rowY + swatchSize - 2}" fill="#222">${escapeText(key)}</text>`;
|
|
2476
|
+
}).join("");
|
|
2477
|
+
return `<g><rect x="${x}" y="${y}" width="${width}" height="${height}" fill="rgba(255,255,255,0.95)" stroke="#222" stroke-width="1"/>` + rows + "</g>";
|
|
2478
|
+
}
|
|
2479
|
+
function escapeAttr(s) {
|
|
2480
|
+
return String(s).replaceAll('"', """).replaceAll("<", "<");
|
|
2481
|
+
}
|
|
2482
|
+
function escapeText(s) {
|
|
2483
|
+
return String(s).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
2484
|
+
}
|
|
2485
|
+
function assertBuffer(input) {
|
|
2486
|
+
if (!Buffer.isBuffer(input)) {
|
|
2487
|
+
throw new BufferCorruptError("input must be a Buffer", {
|
|
2488
|
+
context: { received: typeof input }
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
async function readMeta(sharp, input) {
|
|
2493
|
+
try {
|
|
2494
|
+
const meta = await sharp(input).metadata();
|
|
2495
|
+
if (!meta.width || !meta.height) {
|
|
2496
|
+
throw new BufferCorruptError("Image has no detectable dimensions");
|
|
2497
|
+
}
|
|
2498
|
+
return { width: meta.width, height: meta.height };
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
if (error instanceof BufferCorruptError) {
|
|
2501
|
+
throw error;
|
|
2502
|
+
}
|
|
2503
|
+
throw new BufferCorruptError("sharp could not read annotate input buffer", {
|
|
2504
|
+
cause: error
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
module.exports = { annotate, MAX_ANNOTATIONS };
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
// src/compose/diff.js
|
|
2513
|
+
var require_diff = __commonJS({
|
|
2514
|
+
"src/compose/diff.js"(exports, module) {
|
|
2515
|
+
var {
|
|
2516
|
+
BufferCorruptError,
|
|
2517
|
+
DimensionMismatchError,
|
|
2518
|
+
OptionalDependencyMissingError
|
|
2519
|
+
} = require_errors();
|
|
2520
|
+
async function diff(bufferA, bufferB, options = {}) {
|
|
2521
|
+
assertBuffer(bufferA, "bufferA");
|
|
2522
|
+
assertBuffer(bufferB, "bufferB");
|
|
2523
|
+
const {
|
|
2524
|
+
threshold = 0.1,
|
|
2525
|
+
includeAA = false,
|
|
2526
|
+
alpha = 0.5,
|
|
2527
|
+
diffColor = [255, 0, 0],
|
|
2528
|
+
output = "overlay"
|
|
2529
|
+
} = options;
|
|
2530
|
+
if (!["overlay", "side-by-side", "mask-only"].includes(output)) {
|
|
2531
|
+
throw new DimensionMismatchError(
|
|
2532
|
+
`diff output must be 'overlay', 'side-by-side', or 'mask-only'; received ${output}`,
|
|
2533
|
+
{ context: { output } }
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
const pixelmatch = await loadPixelmatch();
|
|
2537
|
+
const sharp = __require("sharp");
|
|
2538
|
+
const a = await rawRgba(sharp, bufferA);
|
|
2539
|
+
const b = await rawRgba(sharp, bufferB);
|
|
2540
|
+
if (a.width !== b.width || a.height !== b.height) {
|
|
2541
|
+
throw new DimensionMismatchError(
|
|
2542
|
+
`diff requires equal dimensions: bufferA is ${a.width}x${a.height}, bufferB is ${b.width}x${b.height}`,
|
|
2543
|
+
{
|
|
2544
|
+
context: {
|
|
2545
|
+
a: { width: a.width, height: a.height },
|
|
2546
|
+
b: { width: b.width, height: b.height }
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
const totalPixels = a.width * a.height;
|
|
2552
|
+
const diffData = Buffer.alloc(a.data.length);
|
|
2553
|
+
const changedPixels = pixelmatch(a.data, b.data, diffData, a.width, a.height, {
|
|
2554
|
+
threshold,
|
|
2555
|
+
includeAA,
|
|
2556
|
+
alpha,
|
|
2557
|
+
diffColor
|
|
2558
|
+
});
|
|
2559
|
+
const diffBuffer = await renderOutput(sharp, output, {
|
|
2560
|
+
b,
|
|
2561
|
+
diffData,
|
|
2562
|
+
width: a.width,
|
|
2563
|
+
height: a.height
|
|
2564
|
+
});
|
|
2565
|
+
return {
|
|
2566
|
+
diffBuffer,
|
|
2567
|
+
changedPixels,
|
|
2568
|
+
totalPixels,
|
|
2569
|
+
percentChanged: totalPixels === 0 ? 0 : changedPixels / totalPixels * 100
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
async function loadPixelmatch() {
|
|
2573
|
+
try {
|
|
2574
|
+
const mod = await import('pixelmatch');
|
|
2575
|
+
return mod.default ?? mod;
|
|
2576
|
+
} catch (error) {
|
|
2577
|
+
throw new OptionalDependencyMissingError(
|
|
2578
|
+
"diff() requires the optional peer dependency `pixelmatch`. Install it before calling diff().",
|
|
2579
|
+
{ packageName: "pixelmatch", cause: error }
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
async function rawRgba(sharp, buffer) {
|
|
2584
|
+
try {
|
|
2585
|
+
const { data, info } = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
2586
|
+
return { data, width: info.width, height: info.height };
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
throw new BufferCorruptError("sharp could not decode diff input buffer", {
|
|
2589
|
+
cause: error
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
async function renderOutput(sharp, mode, ctx) {
|
|
2594
|
+
const diffPng = await sharp(ctx.diffData, {
|
|
2595
|
+
raw: { width: ctx.width, height: ctx.height, channels: 4 }
|
|
2596
|
+
}).png().toBuffer();
|
|
2597
|
+
if (mode === "mask-only") {
|
|
2598
|
+
return diffPng;
|
|
2599
|
+
}
|
|
2600
|
+
if (mode === "side-by-side") {
|
|
2601
|
+
const bImage = await sharp(ctx.b.data, {
|
|
2602
|
+
raw: { width: ctx.b.width, height: ctx.b.height, channels: 4 }
|
|
2603
|
+
}).png().toBuffer();
|
|
2604
|
+
return sharp({
|
|
2605
|
+
create: {
|
|
2606
|
+
width: ctx.width * 2,
|
|
2607
|
+
height: ctx.height,
|
|
2608
|
+
channels: 4,
|
|
2609
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
2610
|
+
}
|
|
2611
|
+
}).composite([
|
|
2612
|
+
{ input: bImage, top: 0, left: 0 },
|
|
2613
|
+
{ input: diffPng, top: 0, left: ctx.width }
|
|
2614
|
+
]).png().toBuffer();
|
|
2615
|
+
}
|
|
2616
|
+
return diffPng;
|
|
2617
|
+
}
|
|
2618
|
+
function assertBuffer(buffer, name) {
|
|
2619
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
2620
|
+
throw new BufferCorruptError(`${name} must be a Buffer`, {
|
|
2621
|
+
context: { received: typeof buffer, name }
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
module.exports = { diff };
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
// src/compose/heatmap.js
|
|
2630
|
+
var require_heatmap = __commonJS({
|
|
2631
|
+
"src/compose/heatmap.js"(exports, module) {
|
|
2632
|
+
var { BufferCorruptError, ComposeError, HeatmapPointError } = require_errors();
|
|
2633
|
+
var MAX_POINTS = 1e5;
|
|
2634
|
+
var VALID_BLEND_MODES = /* @__PURE__ */ new Set(["overlay", "screen", "multiply"]);
|
|
2635
|
+
async function heatmap(input, points, options = {}) {
|
|
2636
|
+
assertBuffer(input);
|
|
2637
|
+
if (!Array.isArray(points)) {
|
|
2638
|
+
throw new HeatmapPointError("points must be an array", {
|
|
2639
|
+
context: { received: typeof points }
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
if (points.length > MAX_POINTS) {
|
|
2643
|
+
throw new ComposeError(
|
|
2644
|
+
`Cannot render a heatmap with more than ${MAX_POINTS} points in a single call`,
|
|
2645
|
+
{ context: { received: points.length, max: MAX_POINTS } }
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
const { radius = 40, intensity = 0.7, colorScale = "viridis", blendMode = "overlay" } = options;
|
|
2649
|
+
if (!VALID_BLEND_MODES.has(blendMode)) {
|
|
2650
|
+
throw new ComposeError(
|
|
2651
|
+
`blendMode must be one of ${[...VALID_BLEND_MODES].join(", ")}; received ${blendMode}`,
|
|
2652
|
+
{ context: { received: blendMode } }
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
2656
|
+
throw new HeatmapPointError("radius must be a positive number", {
|
|
2657
|
+
context: { received: radius }
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
for (const [i, point] of points.entries()) {
|
|
2661
|
+
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
2662
|
+
throw new HeatmapPointError(`points[${i}] must have finite x and y`, {
|
|
2663
|
+
context: { index: i, point }
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
if (point.weight !== void 0 && !Number.isFinite(point.weight)) {
|
|
2667
|
+
throw new HeatmapPointError(`points[${i}].weight must be a finite number`, {
|
|
2668
|
+
context: { index: i, weight: point.weight }
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
const sharp = __require("sharp");
|
|
2673
|
+
const meta = await readMeta(sharp, input);
|
|
2674
|
+
if (points.length === 0) {
|
|
2675
|
+
return input;
|
|
2676
|
+
}
|
|
2677
|
+
const color = resolveColor(colorScale);
|
|
2678
|
+
const svg = buildSvg(points, radius, intensity, color, meta);
|
|
2679
|
+
const blurred = await sharp(Buffer.from(svg, "utf8")).resize(meta.width, meta.height, { fit: "fill" }).blur(Math.min(50, radius / 2)).toBuffer();
|
|
2680
|
+
return sharp(input).composite([{ input: blurred, blend: blendMode }]).toBuffer();
|
|
2681
|
+
}
|
|
2682
|
+
function buildSvg(points, radius, intensity, color, meta) {
|
|
2683
|
+
const opacity = Math.max(0, Math.min(1, intensity));
|
|
2684
|
+
const circles = points.map((p) => {
|
|
2685
|
+
const w = Math.max(0.1, Number(p.weight ?? 1));
|
|
2686
|
+
const r = radius * Math.sqrt(w);
|
|
2687
|
+
return `<circle cx="${p.x}" cy="${p.y}" r="${r}" fill="${color}" fill-opacity="${opacity}"/>`;
|
|
2688
|
+
}).join("");
|
|
2689
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${meta.width}" height="${meta.height}"><g>${circles}</g></svg>`;
|
|
2690
|
+
}
|
|
2691
|
+
function resolveColor(colorScale) {
|
|
2692
|
+
if (typeof colorScale === "function") {
|
|
2693
|
+
try {
|
|
2694
|
+
const result = colorScale(1);
|
|
2695
|
+
if (Array.isArray(result) && result.length >= 3) {
|
|
2696
|
+
return `rgb(${result[0]}, ${result[1]}, ${result[2]})`;
|
|
2697
|
+
}
|
|
2698
|
+
} catch {
|
|
2699
|
+
}
|
|
2700
|
+
return "#FF0000";
|
|
2701
|
+
}
|
|
2702
|
+
switch (colorScale) {
|
|
2703
|
+
case "magma":
|
|
2704
|
+
return "#FB8861";
|
|
2705
|
+
case "severity":
|
|
2706
|
+
return "#D32F2F";
|
|
2707
|
+
default:
|
|
2708
|
+
return "#FDE725";
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
function assertBuffer(input) {
|
|
2712
|
+
if (!Buffer.isBuffer(input)) {
|
|
2713
|
+
throw new BufferCorruptError("input must be a Buffer", {
|
|
2714
|
+
context: { received: typeof input }
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
async function readMeta(sharp, input) {
|
|
2719
|
+
try {
|
|
2720
|
+
const meta = await sharp(input).metadata();
|
|
2721
|
+
if (!meta.width || !meta.height) {
|
|
2722
|
+
throw new BufferCorruptError("Image has no detectable dimensions");
|
|
2723
|
+
}
|
|
2724
|
+
return { width: meta.width, height: meta.height };
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
if (error instanceof BufferCorruptError) {
|
|
2727
|
+
throw error;
|
|
2728
|
+
}
|
|
2729
|
+
throw new BufferCorruptError("sharp could not read heatmap input buffer", {
|
|
2730
|
+
cause: error
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
module.exports = { heatmap, MAX_POINTS };
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
// src/compose/index.js
|
|
2739
|
+
var require_compose2 = __commonJS({
|
|
2740
|
+
"src/compose/index.js"(exports, module) {
|
|
2741
|
+
var { annotate } = require_annotate();
|
|
2742
|
+
var { defaultTheme } = require_theme();
|
|
2743
|
+
var { diff } = require_diff();
|
|
2744
|
+
var { heatmap } = require_heatmap();
|
|
2745
|
+
module.exports = { annotate, defaultTheme, diff, heatmap };
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
// src/index.js
|
|
2750
|
+
var require_index = __commonJS({
|
|
2751
|
+
"src/index.js"(exports, module) {
|
|
2752
|
+
var pkg = require_package();
|
|
2753
|
+
var errors = require_errors();
|
|
2754
|
+
var adapters = require_adapters();
|
|
2755
|
+
var capture = require_capture2();
|
|
2756
|
+
var transform = require_transform2();
|
|
2757
|
+
var tile = require_tile2();
|
|
2758
|
+
var compose = require_compose2();
|
|
2759
|
+
var defaults = Object.freeze({
|
|
2760
|
+
format: "png",
|
|
2761
|
+
quality: 90,
|
|
2762
|
+
fullPage: true,
|
|
2763
|
+
timeout: 3e4,
|
|
2764
|
+
padding: 10,
|
|
2765
|
+
stripHeight: null,
|
|
2766
|
+
tileSize: 256,
|
|
2767
|
+
tileOverlap: 1,
|
|
2768
|
+
tileFormat: "webp",
|
|
2769
|
+
tileQuality: 80,
|
|
2770
|
+
waitForStable: Object.freeze({
|
|
2771
|
+
fonts: true,
|
|
2772
|
+
images: true,
|
|
2773
|
+
animations: "reduce",
|
|
2774
|
+
maxWait: 5e3,
|
|
2775
|
+
idleMs: 200
|
|
2776
|
+
})
|
|
2777
|
+
});
|
|
2778
|
+
var version = pkg.version;
|
|
2779
|
+
module.exports = {
|
|
2780
|
+
defaults,
|
|
2781
|
+
version,
|
|
2782
|
+
...errors,
|
|
2783
|
+
...adapters,
|
|
2784
|
+
...capture,
|
|
2785
|
+
...transform,
|
|
2786
|
+
...tile,
|
|
2787
|
+
...compose
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
var index = require_index();
|
|
2792
|
+
|
|
2793
|
+
export { index as default };
|
|
2794
|
+
//# sourceMappingURL=index.mjs.map
|
|
2795
|
+
//# sourceMappingURL=index.mjs.map
|