@afixt/screenshot-utils 0.9.0 → 1.1.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 (86) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +20 -20
  3. package/dist/index.js +172 -16
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +172 -16
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/types/adapters/cdp.d.ts +28 -0
  8. package/dist/types/adapters/cdp.d.ts.map +1 -0
  9. package/dist/types/adapters/index.d.ts +5 -0
  10. package/dist/types/adapters/index.d.ts.map +1 -0
  11. package/dist/types/adapters/playwright.d.ts +28 -0
  12. package/dist/types/adapters/playwright.d.ts.map +1 -0
  13. package/dist/types/adapters/puppeteer.d.ts +20 -0
  14. package/dist/types/adapters/puppeteer.d.ts.map +1 -0
  15. package/dist/types/capture/element.d.ts +20 -0
  16. package/dist/types/capture/element.d.ts.map +1 -0
  17. package/dist/types/capture/index.d.ts +8 -0
  18. package/dist/types/capture/index.d.ts.map +1 -0
  19. package/dist/types/capture/long-page.d.ts +16 -0
  20. package/dist/types/capture/long-page.d.ts.map +1 -0
  21. package/dist/types/capture/page.d.ts +14 -0
  22. package/dist/types/capture/page.d.ts.map +1 -0
  23. package/dist/types/capture/responsive.d.ts +20 -0
  24. package/dist/types/capture/responsive.d.ts.map +1 -0
  25. package/dist/types/capture/settle.d.ts +14 -0
  26. package/dist/types/capture/settle.d.ts.map +1 -0
  27. package/dist/types/capture/viewport.d.ts +14 -0
  28. package/dist/types/capture/viewport.d.ts.map +1 -0
  29. package/dist/types/cli/index.d.ts +10 -0
  30. package/dist/types/cli/index.d.ts.map +1 -0
  31. package/dist/types/compose/annotate.d.ts +16 -0
  32. package/dist/types/compose/annotate.d.ts.map +1 -0
  33. package/dist/types/compose/diff.d.ts +15 -0
  34. package/dist/types/compose/diff.d.ts.map +1 -0
  35. package/dist/types/compose/heatmap.d.ts +17 -0
  36. package/dist/types/compose/heatmap.d.ts.map +1 -0
  37. package/dist/types/compose/index.d.ts +6 -0
  38. package/dist/types/compose/index.d.ts.map +1 -0
  39. package/dist/types/compose/theme.d.ts +17 -0
  40. package/dist/types/compose/theme.d.ts.map +1 -0
  41. package/dist/types/errors/adapter.d.ts +33 -0
  42. package/dist/types/errors/adapter.d.ts.map +1 -0
  43. package/dist/types/errors/base.d.ts +37 -0
  44. package/dist/types/errors/base.d.ts.map +1 -0
  45. package/dist/types/errors/capture.d.ts +109 -0
  46. package/dist/types/errors/capture.d.ts.map +1 -0
  47. package/dist/types/errors/compose.d.ts +35 -0
  48. package/dist/types/errors/compose.d.ts.map +1 -0
  49. package/dist/types/errors/index.d.ts +29 -0
  50. package/dist/types/errors/index.d.ts.map +1 -0
  51. package/dist/types/errors/optional.d.ts +23 -0
  52. package/dist/types/errors/optional.d.ts.map +1 -0
  53. package/dist/types/errors/redaction.d.ts +21 -0
  54. package/dist/types/errors/redaction.d.ts.map +1 -0
  55. package/dist/types/errors/tile.d.ts +29 -0
  56. package/dist/types/errors/tile.d.ts.map +1 -0
  57. package/dist/types/errors/transform.d.ts +35 -0
  58. package/dist/types/errors/transform.d.ts.map +1 -0
  59. package/dist/types/index.d.ts +58 -0
  60. package/dist/types/index.d.ts.map +1 -0
  61. package/dist/types/redact/index.d.ts +5 -0
  62. package/dist/types/redact/index.d.ts.map +1 -0
  63. package/dist/types/redact/policy.d.ts +29 -0
  64. package/dist/types/redact/policy.d.ts.map +1 -0
  65. package/dist/types/redact/redact.d.ts +29 -0
  66. package/dist/types/redact/redact.d.ts.map +1 -0
  67. package/dist/types/tile/dzi.d.ts +18 -0
  68. package/dist/types/tile/dzi.d.ts.map +1 -0
  69. package/dist/types/tile/index.d.ts +3 -0
  70. package/dist/types/tile/index.d.ts.map +1 -0
  71. package/dist/types/transform/convert.d.ts +13 -0
  72. package/dist/types/transform/convert.d.ts.map +1 -0
  73. package/dist/types/transform/crop.d.ts +33 -0
  74. package/dist/types/transform/crop.d.ts.map +1 -0
  75. package/dist/types/transform/index.d.ts +7 -0
  76. package/dist/types/transform/index.d.ts.map +1 -0
  77. package/dist/types/transform/normalize.d.ts +14 -0
  78. package/dist/types/transform/normalize.d.ts.map +1 -0
  79. package/dist/types/transform/resize.d.ts +14 -0
  80. package/dist/types/transform/resize.d.ts.map +1 -0
  81. package/package.json +5 -4
  82. package/src/capture/element.js +48 -6
  83. package/src/capture/page.js +31 -10
  84. package/src/capture/settle.js +139 -4
  85. package/src/index.js +3 -0
  86. package/src/types/index.d.ts +10 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * An operation needs an optional peer dependency that isn't installed
3
+ * (e.g., calling `diff()` without `pixelmatch`). The error message MUST
4
+ * include the install command for the missing package.
5
+ *
6
+ * @public
7
+ */
8
+ export class OptionalDependencyMissingError extends ScreenshotUtilsError {
9
+ /**
10
+ * @param {string} message
11
+ * @param {object} [options]
12
+ * @param {string} [options.packageName] The npm package the caller must install.
13
+ * @param {string} [options.installCommand] Suggested install command (default: derived).
14
+ */
15
+ constructor(message: string, options?: {
16
+ packageName?: string | undefined;
17
+ installCommand?: string | undefined;
18
+ });
19
+ packageName: string | undefined;
20
+ installCommand: string | undefined;
21
+ }
22
+ import { ScreenshotUtilsError } from "./base.js";
23
+ //# sourceMappingURL=optional.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optional.d.ts","sourceRoot":"","sources":["../../../src/errors/optional.js"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH;IACI;;;;;OAKG;IACH,qBALW,MAAM,YAEd;QAAyB,WAAW;QACX,cAAc;KACzC,EAcA;IAPO,gCAA8B;IAG9B,mCAAoC;CAK/C"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Base for errors arising during redaction (blur / mask / pixelate, PII policies).
3
+ *
4
+ * @public
5
+ */
6
+ export class RedactionError extends ScreenshotUtilsError {
7
+ /**
8
+ * @param {string} message
9
+ * @param {object} [options]
10
+ */
11
+ constructor(message: string, options?: object);
12
+ }
13
+ /**
14
+ * A PII policy object is malformed or references unknown rule keys.
15
+ *
16
+ * @public
17
+ */
18
+ export class PolicyError extends RedactionError {
19
+ }
20
+ import { ScreenshotUtilsError } from "./base.js";
21
+ //# sourceMappingURL=redaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../../src/errors/redaction.js"],"names":[],"mappings":"AAIA;;;;GAIG;AACH;IACI;;;OAGG;IACH,qBAHW,MAAM,YACN,MAAM,EAMhB;CACJ;AAED;;;;GAIG;AACH;CAUC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Base for errors arising during DZI tile generation.
3
+ *
4
+ * @public
5
+ */
6
+ export class TileError extends ScreenshotUtilsError {
7
+ /**
8
+ * @param {string} message
9
+ * @param {object} [options]
10
+ */
11
+ constructor(message: string, options?: object);
12
+ }
13
+ /**
14
+ * `tileSize` is invalid (must be 256 or 512 per spec § 8.4.1).
15
+ *
16
+ * @public
17
+ */
18
+ export class TileSizeError extends TileError {
19
+ }
20
+ /**
21
+ * Writing tiles to the output directory failed (only in `output: 'directory'` mode).
22
+ * Retriable because the underlying cause is usually transient (disk full, permissions race).
23
+ *
24
+ * @public
25
+ */
26
+ export class TileWriteError extends TileError {
27
+ }
28
+ import { ScreenshotUtilsError } from "./base.js";
29
+ //# sourceMappingURL=tile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tile.d.ts","sourceRoot":"","sources":["../../../src/errors/tile.js"],"names":[],"mappings":"AAIA;;;;GAIG;AACH;IACI;;;OAGG;IACH,qBAHW,MAAM,YACN,MAAM,EAMhB;CACJ;AAED;;;;GAIG;AACH;CAUC;AAED;;;;;GAKG;AACH;CAUC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Base for errors arising during image-buffer transforms (crop, resize, convert, normalize).
3
+ *
4
+ * @public
5
+ */
6
+ export class TransformError extends ScreenshotUtilsError {
7
+ /**
8
+ * @param {string} message
9
+ * @param {object} [options]
10
+ */
11
+ constructor(message: string, options?: object);
12
+ }
13
+ /**
14
+ * A rect is malformed or lies outside the source image bounds.
15
+ *
16
+ * @public
17
+ */
18
+ export class InvalidRectError extends TransformError {
19
+ }
20
+ /**
21
+ * The requested output format is not supported (e.g., AVIF in v1.0).
22
+ *
23
+ * @public
24
+ */
25
+ export class UnsupportedFormatError extends TransformError {
26
+ }
27
+ /**
28
+ * Sharp could not decode the input buffer (truncated, wrong format, corrupt).
29
+ *
30
+ * @public
31
+ */
32
+ export class BufferCorruptError extends TransformError {
33
+ }
34
+ import { ScreenshotUtilsError } from "./base.js";
35
+ //# sourceMappingURL=transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/errors/transform.js"],"names":[],"mappings":"AAIA;;;;GAIG;AACH;IACI;;;OAGG;IACH,qBAHW,MAAM,YACN,MAAM,EAMhB;CACJ;AAED;;;;GAIG;AACH;CAUC;AAED;;;;GAIG;AACH;CAUC;AAED;;;;GAIG;AACH;CAUC"}
@@ -0,0 +1,58 @@
1
+ declare const _exports: {
2
+ annotate: typeof compose.annotate;
3
+ defaultTheme: Readonly<import("./types/index.d.ts").AnnotationTheme>;
4
+ diff: typeof compose.diff;
5
+ heatmap: typeof compose.heatmap;
6
+ tileDzi: typeof tile.tileDzi;
7
+ crop: typeof transform.crop;
8
+ cropMany: typeof transform.cropMany;
9
+ convert: typeof transform.convert;
10
+ resize: typeof transform.resize;
11
+ normalize: typeof transform.normalize;
12
+ capturePage: typeof capture.capturePage;
13
+ waitForStable: typeof capture.waitForStable;
14
+ captureLongPage: typeof capture.captureLongPage;
15
+ captureElement: typeof capture.captureElement;
16
+ captureViewport: typeof capture.captureViewport;
17
+ captureResponsive: typeof capture.captureResponsive;
18
+ fromPuppeteerPage: typeof adapters.fromPuppeteerPage;
19
+ fromPlaywrightPage: typeof adapters.fromPlaywrightPage;
20
+ fromCDPSession: typeof adapters.fromCDPSession;
21
+ ScreenshotUtilsError: typeof errors.ScreenshotUtilsError;
22
+ CaptureError: typeof errors.CaptureError;
23
+ BrowserUnavailableError: typeof errors.BrowserUnavailableError;
24
+ PageTooTallError: typeof errors.PageTooTallError;
25
+ ElementNotFoundError: typeof errors.ElementNotFoundError;
26
+ ElementNotVisibleError: typeof errors.ElementNotVisibleError;
27
+ ElementMismatchError: typeof errors.ElementMismatchError;
28
+ ElementTooLargeError: typeof errors.ElementTooLargeError;
29
+ StabilityTimeoutError: typeof errors.StabilityTimeoutError;
30
+ CaptureTimeoutError: typeof errors.CaptureTimeoutError;
31
+ TransformError: typeof errors.TransformError;
32
+ InvalidRectError: typeof errors.InvalidRectError;
33
+ UnsupportedFormatError: typeof errors.UnsupportedFormatError;
34
+ BufferCorruptError: typeof errors.BufferCorruptError;
35
+ ComposeError: typeof errors.ComposeError;
36
+ DimensionMismatchError: typeof errors.DimensionMismatchError;
37
+ ThemeError: typeof errors.ThemeError;
38
+ HeatmapPointError: typeof errors.HeatmapPointError;
39
+ TileError: typeof errors.TileError;
40
+ TileSizeError: typeof errors.TileSizeError;
41
+ TileWriteError: typeof errors.TileWriteError;
42
+ RedactionError: typeof errors.RedactionError;
43
+ PolicyError: typeof errors.PolicyError;
44
+ AdapterError: typeof errors.AdapterError;
45
+ AdapterContractError: typeof errors.AdapterContractError;
46
+ BrowserApiError: typeof errors.BrowserApiError;
47
+ OptionalDependencyMissingError: typeof errors.OptionalDependencyMissingError;
48
+ defaults: Readonly<import("./types/index.d.ts").ScreenshotUtilsDefaults>;
49
+ version: string;
50
+ };
51
+ export = _exports;
52
+ import compose = require("./compose/index.js");
53
+ import tile = require("./tile/index.js");
54
+ import transform = require("./transform/index.js");
55
+ import capture = require("./capture/index.js");
56
+ import adapters = require("./adapters/index.js");
57
+ import errors = require("./errors/index.js");
58
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import { redact } from "./redact.js";
2
+ import { applyPiiPolicy } from "./policy.js";
3
+ import { defaultPiiPolicy } from "./policy.js";
4
+ export { redact, applyPiiPolicy, defaultPiiPolicy };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/redact/index.js"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Apply a PII policy to a screenshot using a pre-collected list of element
3
+ * rects + classification metadata.
4
+ *
5
+ * Implements `spec.md` § 8.5.2. The function does NOT inspect a DOM
6
+ * directly — callers run a page-context evaluator that produces the
7
+ * `regions` array (an array of `{ rect, classification? }` objects) and
8
+ * pass it here. This keeps the package DOM-agnostic.
9
+ *
10
+ * @public
11
+ * @param {Buffer} input
12
+ * @param {Array<{rect: import('../types/index.d.ts').Rect, classification?: string}>} regions
13
+ * @param {import('../types/index.d.ts').PiiPolicy} [policy]
14
+ * @returns {Promise<Buffer>}
15
+ */
16
+ export function applyPiiPolicy(input: Buffer, regions: Array<{
17
+ rect: import("../types/index.d.ts").Rect;
18
+ classification?: string;
19
+ }>, policy?: import("../types/index.d.ts").PiiPolicy): Promise<Buffer>;
20
+ /**
21
+ * Default PII policy bundled with the package. Consumers tighten it per
22
+ * their privacy posture; this is the conservative starting point per
23
+ * `spec.md` § 8.5.2.
24
+ *
25
+ * @public
26
+ * @type {Readonly<import('../types/index.d.ts').PiiPolicy>}
27
+ */
28
+ export const defaultPiiPolicy: Readonly<import("../types/index.d.ts").PiiPolicy>;
29
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../../src/redact/policy.js"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;GAcG;AACH,sCALW,MAAM,WACN,KAAK,CAAC;IAAC,IAAI,EAAE,OAAO,qBAAqB,EAAE,IAAI,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAC,CAAC,WAC1E,OAAO,qBAAqB,EAAE,SAAS,GACrC,OAAO,CAAC,MAAM,CAAC,CA+B3B;AA3DD;;;;;;;GAOG;AACH,+BAFU,QAAQ,CAAC,OAAO,qBAAqB,EAAE,SAAS,CAAC,CAOxD"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Apply one of three redaction modes to specified regions of an image.
3
+ *
4
+ * Implements `spec.md` § 8.5.1.
5
+ *
6
+ * - `mode: 'blur'` — Gaussian blur with the given `radius`.
7
+ * - `mode: 'mask'` — solid-color fill (`color`, default `#000`).
8
+ * - `mode: 'pixelate'` — downscale-then-upscale with the given `cellSize`.
9
+ *
10
+ * @public
11
+ * @param {Buffer} input
12
+ * @param {Array<import('../types/index.d.ts').RedactRegion>} regions
13
+ * @param {import('../types/index.d.ts').RedactOptions} [options]
14
+ * @returns {Promise<Buffer>}
15
+ */
16
+ export function redact(input: Buffer, regions: Array<import("../types/index.d.ts").RedactRegion>, options?: import("../types/index.d.ts").RedactOptions): Promise<Buffer>;
17
+ /**
18
+ * Parse a CSS color string (`#rgb`, `#rrggbb`, `#rrggbbaa`, or named black).
19
+ *
20
+ * @param {string} color
21
+ * @returns {{r: number, g: number, b: number, alpha: number}}
22
+ */
23
+ export function parseColor(color: string): {
24
+ r: number;
25
+ g: number;
26
+ b: number;
27
+ alpha: number;
28
+ };
29
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../../src/redact/redact.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AACH,8BALW,MAAM,WACN,KAAK,CAAC,OAAO,qBAAqB,EAAE,YAAY,CAAC,YACjD,OAAO,qBAAqB,EAAE,aAAa,GACzC,OAAO,CAAC,MAAM,CAAC,CA6C3B;AA4ID;;;;;GAKG;AACH,kCAHW,MAAM,GACJ;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,CAgC5D"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Generate a Deep Zoom Image pyramid from a source buffer using
3
+ * `sharp().tile({ layout: 'dz' })`.
4
+ *
5
+ * Implements `spec.md` § 8.4.1. Three output modes:
6
+ * - `'buffers'` (default): returns tile bytes in memory, cleans up the temp dir.
7
+ * - `'directory'`: returns the path to the temp dir; the caller is responsible
8
+ * for moving / uploading / deleting it.
9
+ * - `'stream'`: NOT IMPLEMENTED in v1.0 (per spec § 22 — deferred to v1.1).
10
+ *
11
+ * @public
12
+ * @param {Buffer} input
13
+ * @param {import('../types/index.d.ts').TileDziOptions} [options]
14
+ * @returns {Promise<import('../types/index.d.ts').DziResult>}
15
+ */
16
+ export function tileDzi(input: Buffer, options?: import("../types/index.d.ts").TileDziOptions): Promise<import("../types/index.d.ts").DziResult>;
17
+ export const MAX_TILES: 10000;
18
+ //# sourceMappingURL=dzi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dzi.d.ts","sourceRoot":"","sources":["../../../src/tile/dzi.js"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;;GAcG;AACH,+BAJW,MAAM,YACN,OAAO,qBAAqB,EAAE,cAAc,GAC1C,OAAO,CAAC,OAAO,qBAAqB,EAAE,SAAS,CAAC,CA6F5D;AA5GD,wBAAkB,KAAM,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { tileDzi };
2
+ import { tileDzi } from "./dzi.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tile/index.js"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Re-encode a buffer to a different image format.
3
+ *
4
+ * Implements `spec.md` § 8.2.4. Thin wrapper over `sharp(buf).toFormat()`
5
+ * with safer defaults and the package's error contract.
6
+ *
7
+ * @public
8
+ * @param {Buffer} input
9
+ * @param {import('../types/index.d.ts').ConvertOptions} options
10
+ * @returns {Promise<Buffer>}
11
+ */
12
+ export function convert(input: Buffer, options: import("../types/index.d.ts").ConvertOptions): Promise<Buffer>;
13
+ //# sourceMappingURL=convert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../../../src/transform/convert.js"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,+BAJW,MAAM,WACN,OAAO,qBAAqB,EAAE,cAAc,GAC1C,OAAO,CAAC,MAAM,CAAC,CA8B3B"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Extract a single rectangle from a source image buffer.
3
+ *
4
+ * Implements `spec.md` § 8.2.1. All coordinates are CSS pixels. Padding
5
+ * expands the rect in all four directions and is then clamped to the
6
+ * image bounds (we never error on padding causing under/overflow — the
7
+ * caller asked for slack, not arithmetic).
8
+ *
9
+ * @public
10
+ * @param {Buffer} input
11
+ * @param {import('../types/index.d.ts').Rect} rect
12
+ * @param {import('../types/index.d.ts').CropOptions} [options]
13
+ * @returns {Promise<Buffer>}
14
+ */
15
+ export function crop(input: Buffer, rect: import("../types/index.d.ts").Rect, options?: import("../types/index.d.ts").CropOptions): Promise<Buffer>;
16
+ /**
17
+ * Extract many rectangles in parallel from one source. Used by the engine's
18
+ * `_captureIssueScreenshots` migration path (one full-page capture + many
19
+ * crops beats N per-element captures by ~20×).
20
+ *
21
+ * Implements `spec.md` § 8.2.2.
22
+ *
23
+ * @public
24
+ * @param {Buffer} input
25
+ * @param {Array<{ id: string, rect: import('../types/index.d.ts').Rect }>} entries
26
+ * @param {import('../types/index.d.ts').CropManyOptions} [options]
27
+ * @returns {Promise<Map<string, Buffer>>}
28
+ */
29
+ export function cropMany(input: Buffer, entries: Array<{
30
+ id: string;
31
+ rect: import("../types/index.d.ts").Rect;
32
+ }>, options?: import("../types/index.d.ts").CropManyOptions): Promise<Map<string, Buffer>>;
33
+ //# sourceMappingURL=crop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop.d.ts","sourceRoot":"","sources":["../../../src/transform/crop.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AACH,4BALW,MAAM,QACN,OAAO,qBAAqB,EAAE,IAAI,YAClC,OAAO,qBAAqB,EAAE,WAAW,GACvC,OAAO,CAAC,MAAM,CAAC,CAoB3B;AAED;;;;;;;;;;;;GAYG;AACH,gCALW,MAAM,WACN,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,qBAAqB,EAAE,IAAI,CAAA;CAAE,CAAC,YAC/D,OAAO,qBAAqB,EAAE,eAAe,GAC3C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoDxC"}
@@ -0,0 +1,7 @@
1
+ import { crop } from "./crop.js";
2
+ import { cropMany } from "./crop.js";
3
+ import { convert } from "./convert.js";
4
+ import { resize } from "./resize.js";
5
+ import { normalize } from "./normalize.js";
6
+ export { crop, cropMany, convert, resize, normalize };
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transform/index.js"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalize an image buffer to a target device-pixel ratio so its coordinate
3
+ * space is CSS pixels regardless of the source DPR.
4
+ *
5
+ * Implements `spec.md` § 8.2.5. Default behavior preserves the image's
6
+ * intrinsic DPR (caller opts in to normalization for cross-DPR comparison).
7
+ *
8
+ * @public
9
+ * @param {Buffer} input
10
+ * @param {import('../types/index.d.ts').NormalizeOptions} [options]
11
+ * @returns {Promise<import('../types/index.d.ts').NormalizeResult>}
12
+ */
13
+ export function normalize(input: Buffer, options?: import("../types/index.d.ts").NormalizeOptions): Promise<import("../types/index.d.ts").NormalizeResult>;
14
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/transform/normalize.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,iCAJW,MAAM,YACN,OAAO,qBAAqB,EAAE,gBAAgB,GAC5C,OAAO,CAAC,OAAO,qBAAqB,EAAE,eAAe,CAAC,CAiClE"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resize an image buffer with sharp-native semantics.
3
+ *
4
+ * Implements `spec.md` § 8.2.3. Honors `width`, `height`, `fit`, `position`,
5
+ * `kernel`, and `withoutEnlargement`. At least one of `width` or `height`
6
+ * must be supplied.
7
+ *
8
+ * @public
9
+ * @param {Buffer} input
10
+ * @param {import('../types/index.d.ts').ResizeOptions} options
11
+ * @returns {Promise<Buffer>}
12
+ */
13
+ export function resize(input: Buffer, options: import("../types/index.d.ts").ResizeOptions): Promise<Buffer>;
14
+ //# sourceMappingURL=resize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../../src/transform/resize.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,8BAJW,MAAM,WACN,OAAO,qBAAqB,EAAE,aAAa,GACzC,OAAO,CAAC,MAAM,CAAC,CA0E3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afixt/screenshot-utils",
3
- "version": "0.9.0",
3
+ "version": "1.1.0",
4
4
  "description": "Screenshot capture, transform, compose, tile, and redact utilities for the AFixt/Revenant accessibility platform",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",
@@ -83,7 +83,7 @@
83
83
  "access": "restricted"
84
84
  },
85
85
  "engines": {
86
- "node": ">=22.22.1"
86
+ "node": ">=20.9.0"
87
87
  },
88
88
  "keywords": [
89
89
  "screenshot",
@@ -101,7 +101,7 @@
101
101
  "revenant"
102
102
  ],
103
103
  "dependencies": {
104
- "sharp": "0.34.5"
104
+ "sharp": "^0.35.3"
105
105
  },
106
106
  "peerDependencies": {
107
107
  "@napi-rs/canvas": "^0.1.0",
@@ -132,7 +132,7 @@
132
132
  "@commitlint/config-conventional": "20.5.3",
133
133
  "@eslint/js": "10.0.1",
134
134
  "@types/node": "22.19.19",
135
- "@vitest/coverage-v8": "4.1.5",
135
+ "@vitest/coverage-istanbul": "4.1.5",
136
136
  "clean-jsdoc-theme": "4.3.2",
137
137
  "eslint": "10.3.0",
138
138
  "eslint-plugin-import-x": "4.16.2",
@@ -151,6 +151,7 @@
151
151
  "lint-staged": "17.0.2",
152
152
  "markdownlint-cli": "0.48.0",
153
153
  "npm-run-all2": "8.0.4",
154
+ "openseadragon": "6.0.2",
154
155
  "pixelmatch": "7.1.0",
155
156
  "playwright": "1.60.0",
156
157
  "pngjs": "7.0.0",
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { CaptureTimeoutError } = require('../errors/index.js');
4
+ const { waitForStable } = require('./settle.js');
4
5
 
5
6
  /**
6
7
  * Capture a single element by selector or xpath.
@@ -31,6 +32,7 @@ async function captureElement(adapter, options = {}) {
31
32
  timeout = 5000,
32
33
  format = 'png',
33
34
  quality = 90,
35
+ waitForStable: settleOpts = false,
34
36
  events = null,
35
37
  } = options;
36
38
 
@@ -77,6 +79,7 @@ async function captureElement(adapter, options = {}) {
77
79
  }
78
80
 
79
81
  let viewportRect = lookup.viewportRect;
82
+ let documentRect = lookup.documentRect;
80
83
  if (scrollIntoView) {
81
84
  await adapter.scrollTo({
82
85
  x: 0,
@@ -93,6 +96,17 @@ async function captureElement(adapter, options = {}) {
93
96
  }
94
97
  }
95
98
 
99
+ // Optional render-stability wait. Critically, we re-read the element rect
100
+ // *after* settling: late reflow (lazy images, web fonts, JS layout) is the
101
+ // cause of blank/wrong-region element crops, so coordinates captured before
102
+ // settle cannot be trusted for the clip.
103
+ const settled = await settleAndReread(adapter, { selector, xpath, timeout, settleOpts });
104
+ const settleMs = settled.settleMs;
105
+ if (settled.rect) {
106
+ viewportRect = settled.rect.viewportRect;
107
+ documentRect = settled.rect.documentRect;
108
+ }
109
+
96
110
  const clip = {
97
111
  x: Math.max(0, Math.round(viewportRect.x - padding)),
98
112
  y: Math.max(0, Math.round(viewportRect.y - padding)),
@@ -117,12 +131,12 @@ async function captureElement(adapter, options = {}) {
117
131
  format,
118
132
  bytes,
119
133
  page: metrics,
120
- timing: { settleMs: 0, captureMs, encodeMs: 0, totalMs },
134
+ timing: { settleMs, captureMs, encodeMs: 0, totalMs },
121
135
  warnings: [],
122
136
  element: {
123
137
  selector: selector ?? '',
124
138
  xpath: xpath ?? '',
125
- documentRect: lookup.documentRect,
139
+ documentRect,
126
140
  viewportRect,
127
141
  verified: Boolean(snippet),
128
142
  },
@@ -131,6 +145,35 @@ async function captureElement(adapter, options = {}) {
131
145
  return result;
132
146
  }
133
147
 
148
+ /**
149
+ * Run waitForStable (when requested) and re-read the element rect afterwards.
150
+ * Kept out of `captureElement` so the main flow stays flat.
151
+ *
152
+ * @param {import('../types/index.d.ts').PageAdapter} adapter
153
+ * @param {{selector?: string, xpath?: string, timeout: number, settleOpts: import('../types/index.d.ts').WaitForStableOptions|boolean}} ctx
154
+ * @returns {Promise<{settleMs: number, rect: {viewportRect: import('../types/index.d.ts').Rect, documentRect: import('../types/index.d.ts').Rect}|null}>}
155
+ */
156
+ async function settleAndReread(adapter, { selector, xpath, timeout, settleOpts }) {
157
+ if (!settleOpts) {
158
+ return { settleMs: 0, rect: null };
159
+ }
160
+ const settleStart = nowMs();
161
+ await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
162
+ const settleMs = nowMs() - settleStart;
163
+ const reread = await withTimeout(
164
+ adapter.evaluate(findElementInPage, { selector, xpath }),
165
+ timeout,
166
+ { operation: 'adapter.evaluate(findElement after settle)' }
167
+ );
168
+ if (reread && reread.found) {
169
+ return {
170
+ settleMs,
171
+ rect: { viewportRect: reread.viewportRect, documentRect: reread.documentRect },
172
+ };
173
+ }
174
+ return { settleMs, rect: null };
175
+ }
176
+
134
177
  /**
135
178
  * Page-context element lookup. Shipped to the browser via adapter.evaluate.
136
179
  * Coverage suppressed for the same reason as `settle.js#runInPage` —
@@ -139,9 +182,9 @@ async function captureElement(adapter, options = {}) {
139
182
  * @param {{selector?: string, xpath?: string}} args
140
183
  * @returns {{found: false} | {found: true, outerHTML: string, documentRect: import('../types/index.d.ts').Rect, viewportRect: import('../types/index.d.ts').Rect}}
141
184
  */
142
- /* v8 ignore start -- runs in the browser via adapter.evaluate(); not
143
- instrumentable by Node-side coverage. Exercised by the puppeteer/playwright
144
- element-capture integration tests. */
185
+ /* istanbul ignore next -- runs in the browser via adapter.evaluate(); must NOT
186
+ be instrumented (injected counters don't exist in the page context) and is
187
+ exercised by the puppeteer/playwright element-capture integration tests. */
145
188
  function findElementInPage(args) {
146
189
  const doc = globalThis.document;
147
190
  let element;
@@ -184,7 +227,6 @@ function findElementInPage(args) {
184
227
  },
185
228
  };
186
229
  }
187
- /* v8 ignore stop */
188
230
 
189
231
  /**
190
232
  * Build a failure-shaped ElementCaptureResult that preserves the engine's
@@ -33,20 +33,12 @@ async function capturePage(adapter, options = {}) {
33
33
  emit(events, 'capture:start', { format, fullPage });
34
34
 
35
35
  let settleMs = 0;
36
- const warnings = [];
36
+ let warnings = [];
37
37
  if (settleOpts) {
38
38
  const settleStart = nowMs();
39
39
  const report = await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
40
40
  settleMs = nowMs() - settleStart;
41
- if (report.fonts === 'timeout') {
42
- warnings.push('fonts did not settle within maxWait');
43
- }
44
- if (report.animations === 'timeout') {
45
- warnings.push('animations did not settle within maxWait');
46
- }
47
- if (report.images.failed > 0) {
48
- warnings.push(`${report.images.failed} image(s) failed to load`);
49
- }
41
+ warnings = settleWarnings(report);
50
42
  }
51
43
 
52
44
  const metrics = await adapter.getMetrics();
@@ -97,6 +89,35 @@ async function capturePage(adapter, options = {}) {
97
89
  return result;
98
90
  }
99
91
 
92
+ /**
93
+ * Translate a StabilityReport into non-fatal warning strings.
94
+ *
95
+ * @param {import('../types/index.d.ts').StabilityReport} report
96
+ * @returns {string[]}
97
+ */
98
+ function settleWarnings(report) {
99
+ const warnings = [];
100
+ if (report.fonts === 'timeout') {
101
+ warnings.push('fonts did not settle within maxWait');
102
+ }
103
+ if (report.animations === 'timeout') {
104
+ warnings.push('animations did not settle within maxWait');
105
+ }
106
+ if (report.images.failed > 0) {
107
+ warnings.push(`${report.images.failed} image(s) failed to load`);
108
+ }
109
+ if (report.backgroundImages && report.backgroundImages.failed > 0) {
110
+ warnings.push(`${report.backgroundImages.failed} background image(s) failed to load`);
111
+ }
112
+ if (report.network === 'timeout') {
113
+ warnings.push('network did not reach idle within maxWait');
114
+ }
115
+ if (report.layoutStable === 'timeout') {
116
+ warnings.push('layout did not stabilize within maxWait');
117
+ }
118
+ return warnings;
119
+ }
120
+
100
121
  /**
101
122
  * Map the user's desired output format to the format the browser should
102
123
  * produce. Returns either the same format (no re-encode) or 'png' (when