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