@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
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const { ScreenshotUtilsError } = require('./base.js');
4
+
5
+ /**
6
+ * Base for errors arising during composition (annotate, heatmap, diff, composite).
7
+ *
8
+ * @public
9
+ */
10
+ class ComposeError extends ScreenshotUtilsError {
11
+ /**
12
+ * @param {string} message
13
+ * @param {object} [options]
14
+ */
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.code = 'COMPOSE_ERROR';
18
+ this.retriable = false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * `diff()` was called with two images whose dimensions don't match.
24
+ *
25
+ * @public
26
+ */
27
+ class DimensionMismatchError extends ComposeError {
28
+ /**
29
+ * @param {string} message
30
+ * @param {object} [options]
31
+ */
32
+ constructor(message, options) {
33
+ super(message, options);
34
+ this.code = 'DIMENSION_MISMATCH';
35
+ this.retriable = false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * An annotation theme is missing a required key or has invalid values.
41
+ *
42
+ * @public
43
+ */
44
+ class ThemeError extends ComposeError {
45
+ /**
46
+ * @param {string} message
47
+ * @param {object} [options]
48
+ */
49
+ constructor(message, options) {
50
+ super(message, options);
51
+ this.code = 'THEME_ERROR';
52
+ this.retriable = false;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * A heatmap point is malformed (e.g., missing x/y, negative weight, NaN).
58
+ *
59
+ * @public
60
+ */
61
+ class HeatmapPointError extends ComposeError {
62
+ /**
63
+ * @param {string} message
64
+ * @param {object} [options]
65
+ */
66
+ constructor(message, options) {
67
+ super(message, options);
68
+ this.code = 'HEATMAP_POINT';
69
+ this.retriable = false;
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ ComposeError,
75
+ DimensionMismatchError,
76
+ ThemeError,
77
+ HeatmapPointError,
78
+ };
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Barrel for the public error hierarchy. Import from here, not from the
5
+ * individual files — the indirection keeps instanceof chains stable when
6
+ * the package is consumed via either CJS or ESM.
7
+ *
8
+ * @module screenshot-utils/errors
9
+ */
10
+
11
+ const { ScreenshotUtilsError } = require('./base.js');
12
+ const {
13
+ CaptureError,
14
+ BrowserUnavailableError,
15
+ PageTooTallError,
16
+ ElementNotFoundError,
17
+ ElementNotVisibleError,
18
+ ElementMismatchError,
19
+ ElementTooLargeError,
20
+ StabilityTimeoutError,
21
+ CaptureTimeoutError,
22
+ } = require('./capture.js');
23
+ const {
24
+ TransformError,
25
+ InvalidRectError,
26
+ UnsupportedFormatError,
27
+ BufferCorruptError,
28
+ } = require('./transform.js');
29
+ const {
30
+ ComposeError,
31
+ DimensionMismatchError,
32
+ ThemeError,
33
+ HeatmapPointError,
34
+ } = require('./compose.js');
35
+ const { TileError, TileSizeError, TileWriteError } = require('./tile.js');
36
+ const { RedactionError, PolicyError } = require('./redaction.js');
37
+ const { AdapterError, AdapterContractError, BrowserApiError } = require('./adapter.js');
38
+ const { OptionalDependencyMissingError } = require('./optional.js');
39
+
40
+ module.exports = {
41
+ ScreenshotUtilsError,
42
+ // capture
43
+ CaptureError,
44
+ BrowserUnavailableError,
45
+ PageTooTallError,
46
+ ElementNotFoundError,
47
+ ElementNotVisibleError,
48
+ ElementMismatchError,
49
+ ElementTooLargeError,
50
+ StabilityTimeoutError,
51
+ CaptureTimeoutError,
52
+ // transform
53
+ TransformError,
54
+ InvalidRectError,
55
+ UnsupportedFormatError,
56
+ BufferCorruptError,
57
+ // compose
58
+ ComposeError,
59
+ DimensionMismatchError,
60
+ ThemeError,
61
+ HeatmapPointError,
62
+ // tile
63
+ TileError,
64
+ TileSizeError,
65
+ TileWriteError,
66
+ // redaction
67
+ RedactionError,
68
+ PolicyError,
69
+ // adapter
70
+ AdapterError,
71
+ AdapterContractError,
72
+ BrowserApiError,
73
+ // optional peers
74
+ OptionalDependencyMissingError,
75
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { ScreenshotUtilsError } = require('./base.js');
4
+
5
+ /**
6
+ * An operation needs an optional peer dependency that isn't installed
7
+ * (e.g., calling `diff()` without `pixelmatch`). The error message MUST
8
+ * include the install command for the missing package.
9
+ *
10
+ * @public
11
+ */
12
+ class OptionalDependencyMissingError extends ScreenshotUtilsError {
13
+ /**
14
+ * @param {string} message
15
+ * @param {object} [options]
16
+ * @param {string} [options.packageName] The npm package the caller must install.
17
+ * @param {string} [options.installCommand] Suggested install command (default: derived).
18
+ */
19
+ constructor(message, options = {}) {
20
+ const { packageName, installCommand, ...rest } = options;
21
+ super(message, rest);
22
+ this.code = 'OPTIONAL_DEPENDENCY_MISSING';
23
+ this.retriable = false;
24
+ if (packageName !== undefined) {
25
+ this.packageName = packageName;
26
+ }
27
+ if (installCommand !== undefined) {
28
+ this.installCommand = installCommand;
29
+ } else if (packageName !== undefined) {
30
+ this.installCommand = `npm install ${packageName}`;
31
+ }
32
+ }
33
+ }
34
+
35
+ module.exports = {
36
+ OptionalDependencyMissingError,
37
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const { ScreenshotUtilsError } = require('./base.js');
4
+
5
+ /**
6
+ * Base for errors arising during redaction (blur / mask / pixelate, PII policies).
7
+ *
8
+ * @public
9
+ */
10
+ class RedactionError extends ScreenshotUtilsError {
11
+ /**
12
+ * @param {string} message
13
+ * @param {object} [options]
14
+ */
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.code = 'REDACTION_ERROR';
18
+ this.retriable = false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * A PII policy object is malformed or references unknown rule keys.
24
+ *
25
+ * @public
26
+ */
27
+ class PolicyError extends RedactionError {
28
+ /**
29
+ * @param {string} message
30
+ * @param {object} [options]
31
+ */
32
+ constructor(message, options) {
33
+ super(message, options);
34
+ this.code = 'POLICY_ERROR';
35
+ this.retriable = false;
36
+ }
37
+ }
38
+
39
+ module.exports = {
40
+ RedactionError,
41
+ PolicyError,
42
+ };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const { ScreenshotUtilsError } = require('./base.js');
4
+
5
+ /**
6
+ * Base for errors arising during DZI tile generation.
7
+ *
8
+ * @public
9
+ */
10
+ class TileError extends ScreenshotUtilsError {
11
+ /**
12
+ * @param {string} message
13
+ * @param {object} [options]
14
+ */
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.code = 'TILE_ERROR';
18
+ this.retriable = false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * `tileSize` is invalid (must be 256 or 512 per spec § 8.4.1).
24
+ *
25
+ * @public
26
+ */
27
+ class TileSizeError extends TileError {
28
+ /**
29
+ * @param {string} message
30
+ * @param {object} [options]
31
+ */
32
+ constructor(message, options) {
33
+ super(message, options);
34
+ this.code = 'TILE_SIZE';
35
+ this.retriable = false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Writing tiles to the output directory failed (only in `output: 'directory'` mode).
41
+ * Retriable because the underlying cause is usually transient (disk full, permissions race).
42
+ *
43
+ * @public
44
+ */
45
+ class TileWriteError extends TileError {
46
+ /**
47
+ * @param {string} message
48
+ * @param {object} [options]
49
+ */
50
+ constructor(message, options) {
51
+ super(message, options);
52
+ this.code = 'TILE_WRITE';
53
+ this.retriable = true;
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ TileError,
59
+ TileSizeError,
60
+ TileWriteError,
61
+ };
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const { ScreenshotUtilsError } = require('./base.js');
4
+
5
+ /**
6
+ * Base for errors arising during image-buffer transforms (crop, resize, convert, normalize).
7
+ *
8
+ * @public
9
+ */
10
+ class TransformError extends ScreenshotUtilsError {
11
+ /**
12
+ * @param {string} message
13
+ * @param {object} [options]
14
+ */
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.code = 'TRANSFORM_ERROR';
18
+ this.retriable = false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * A rect is malformed or lies outside the source image bounds.
24
+ *
25
+ * @public
26
+ */
27
+ class InvalidRectError extends TransformError {
28
+ /**
29
+ * @param {string} message
30
+ * @param {object} [options]
31
+ */
32
+ constructor(message, options) {
33
+ super(message, options);
34
+ this.code = 'INVALID_RECT';
35
+ this.retriable = false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * The requested output format is not supported (e.g., AVIF in v1.0).
41
+ *
42
+ * @public
43
+ */
44
+ class UnsupportedFormatError extends TransformError {
45
+ /**
46
+ * @param {string} message
47
+ * @param {object} [options]
48
+ */
49
+ constructor(message, options) {
50
+ super(message, options);
51
+ this.code = 'UNSUPPORTED_FORMAT';
52
+ this.retriable = false;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Sharp could not decode the input buffer (truncated, wrong format, corrupt).
58
+ *
59
+ * @public
60
+ */
61
+ class BufferCorruptError extends TransformError {
62
+ /**
63
+ * @param {string} message
64
+ * @param {object} [options]
65
+ */
66
+ constructor(message, options) {
67
+ super(message, options);
68
+ this.code = 'BUFFER_CORRUPT';
69
+ this.retriable = false;
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ TransformError,
75
+ InvalidRectError,
76
+ UnsupportedFormatError,
77
+ BufferCorruptError,
78
+ };
package/src/index.js ADDED
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Public entrypoint for `@afixt/screenshot-utils`.
5
+ *
6
+ * This is the only semver-protected surface. Deep imports
7
+ * (e.g. `require('@afixt/screenshot-utils/src/...')`) are internal and
8
+ * may change without a major version bump.
9
+ *
10
+ * @module screenshot-utils
11
+ */
12
+
13
+ const pkg = require('../package.json');
14
+ const errors = require('./errors/index.js');
15
+ const adapters = require('./adapters/index.js');
16
+ const capture = require('./capture/index.js');
17
+ const transform = require('./transform/index.js');
18
+ const tile = require('./tile/index.js');
19
+ const compose = require('./compose/index.js');
20
+
21
+ /**
22
+ * Library defaults. Per-call options shallow-merge over this object.
23
+ * Frozen — never mutate at runtime.
24
+ *
25
+ * Mirrors `spec.md` § 11.1 exactly.
26
+ *
27
+ * @public
28
+ * @type {Readonly<import('./types/index.d.ts').ScreenshotUtilsDefaults>}
29
+ */
30
+ const defaults = Object.freeze({
31
+ format: 'png',
32
+ quality: 90,
33
+ fullPage: true,
34
+ timeout: 30_000,
35
+ padding: 10,
36
+ stripHeight: null,
37
+ tileSize: 256,
38
+ tileOverlap: 1,
39
+ tileFormat: 'webp',
40
+ tileQuality: 80,
41
+ waitForStable: Object.freeze({
42
+ fonts: true,
43
+ images: true,
44
+ animations: 'reduce',
45
+ maxWait: 5000,
46
+ idleMs: 200,
47
+ }),
48
+ });
49
+
50
+ /**
51
+ * Resolved package version from package.json. Useful for diagnostic logging
52
+ * by consumers; never use for feature gating (consumers should pin the range
53
+ * they need via semver).
54
+ *
55
+ * @public
56
+ * @type {string}
57
+ */
58
+ const version = pkg.version;
59
+
60
+ module.exports = {
61
+ defaults,
62
+ version,
63
+ ...errors,
64
+ ...adapters,
65
+ ...capture,
66
+ ...transform,
67
+ ...tile,
68
+ ...compose,
69
+ };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const { redact } = require('./redact.js');
4
+ const { applyPiiPolicy, defaultPiiPolicy } = require('./policy.js');
5
+
6
+ module.exports = { redact, applyPiiPolicy, defaultPiiPolicy };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const { PolicyError } = require('../errors/index.js');
4
+ const { redact } = require('./redact.js');
5
+
6
+ /**
7
+ * Default PII policy bundled with the package. Consumers tighten it per
8
+ * their privacy posture; this is the conservative starting point per
9
+ * `spec.md` § 8.5.2.
10
+ *
11
+ * @public
12
+ * @type {Readonly<import('../types/index.d.ts').PiiPolicy>}
13
+ */
14
+ const defaultPiiPolicy = Object.freeze({
15
+ inputTypes: Object.freeze(['email', 'password', 'tel', 'ssn-pattern']),
16
+ customSelectors: Object.freeze(['[data-pii="true"]', '[data-sensitive]']),
17
+ mode: 'blur',
18
+ radius: 12,
19
+ });
20
+
21
+ /**
22
+ * Apply a PII policy to a screenshot using a pre-collected list of element
23
+ * rects + classification metadata.
24
+ *
25
+ * Implements `spec.md` § 8.5.2. The function does NOT inspect a DOM
26
+ * directly — callers run a page-context evaluator that produces the
27
+ * `regions` array (an array of `{ rect, classification? }` objects) and
28
+ * pass it here. This keeps the package DOM-agnostic.
29
+ *
30
+ * @public
31
+ * @param {Buffer} input
32
+ * @param {Array<{rect: import('../types/index.d.ts').Rect, classification?: string}>} regions
33
+ * @param {import('../types/index.d.ts').PiiPolicy} [policy]
34
+ * @returns {Promise<Buffer>}
35
+ */
36
+ async function applyPiiPolicy(input, regions, policy = defaultPiiPolicy) {
37
+ if (!policy || typeof policy !== 'object') {
38
+ throw new PolicyError('policy must be an object', {
39
+ context: { received: typeof policy },
40
+ });
41
+ }
42
+ if (typeof policy.mode !== 'string') {
43
+ throw new PolicyError('policy.mode is required', { context: { policy } });
44
+ }
45
+ if (!Array.isArray(regions)) {
46
+ throw new PolicyError('regions must be an array', {
47
+ context: { received: typeof regions },
48
+ });
49
+ }
50
+
51
+ const redactRegions = regions.map(r => ({
52
+ rect: r.rect,
53
+ mode: policy.mode,
54
+ ...(policy.radius !== undefined && policy.mode === 'blur' ? { radius: policy.radius } : {}),
55
+ ...(policy.cellSize !== undefined && policy.mode === 'pixelate'
56
+ ? { cellSize: policy.cellSize }
57
+ : {}),
58
+ ...(policy.color !== undefined && policy.mode === 'mask' ? { color: policy.color } : {}),
59
+ }));
60
+
61
+ return redact(input, redactRegions, {
62
+ default: policy.mode,
63
+ defaultRadius: policy.radius ?? 8,
64
+ });
65
+ }
66
+
67
+ module.exports = { applyPiiPolicy, defaultPiiPolicy };