@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,432 @@
1
+ /**
2
+ * @afixt/screenshot-utils — public TypeScript type definitions.
3
+ *
4
+ * Mirror of the contract in `spec.md` §§ 8, 9, 10, 11, 14. All coordinates
5
+ * are CSS pixels unless explicitly labelled "device pixels".
6
+ *
7
+ * Implementation files reference these via JSDoc `@typedef` import:
8
+ *
9
+ * /** @typedef {import('./types').Rect} Rect *\/
10
+ */
11
+
12
+ import type { Readable } from 'node:stream';
13
+
14
+ // ─── Geometry & primitives ────────────────────────────────────────────────────
15
+
16
+ export interface Rect {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ height: number;
21
+ }
22
+
23
+ export interface Point {
24
+ x: number;
25
+ y: number;
26
+ }
27
+
28
+ export interface Viewport {
29
+ width: number;
30
+ height: number;
31
+ /** Default 1. */
32
+ deviceScaleFactor?: number;
33
+ isMobile?: boolean;
34
+ hasTouch?: boolean;
35
+ }
36
+
37
+ export interface PageMetrics {
38
+ /** `document.documentElement.scrollWidth`. */
39
+ width: number;
40
+ /** `document.documentElement.scrollHeight`. */
41
+ height: number;
42
+ viewport: Viewport;
43
+ devicePixelRatio: number;
44
+ url: string;
45
+ /** ISO-8601 UTC. */
46
+ capturedAt: string;
47
+ }
48
+
49
+ export type Format = 'png' | 'jpeg' | 'webp';
50
+ export type Severity = 'critical' | 'serious' | 'moderate' | 'minor';
51
+
52
+ // ─── Capture ──────────────────────────────────────────────────────────────────
53
+
54
+ export type Encoding = 'buffer' | 'stream';
55
+ export type AnimationsPolicy = 'reduce' | 'wait' | 'ignore';
56
+
57
+ export interface WaitForStableOptions {
58
+ fonts?: boolean;
59
+ images?: boolean;
60
+ animations?: AnimationsPolicy;
61
+ /** ms; default 5000. */
62
+ maxWait?: number;
63
+ /** ms; default 200. */
64
+ idleMs?: number;
65
+ }
66
+
67
+ export interface CaptureOptions {
68
+ format?: Format;
69
+ /** 1-100; ignored for png. */
70
+ quality?: number;
71
+ fullPage?: boolean;
72
+ omitBackground?: boolean;
73
+ encoding?: Encoding;
74
+ /** ms; default 30000. */
75
+ timeout?: number;
76
+ waitForStable?: WaitForStableOptions | false;
77
+ /** Optional EventEmitter-shaped sink for progress events. */
78
+ events?: { emit(name: string, payload?: unknown): void } | null;
79
+ }
80
+
81
+ export interface CaptureTiming {
82
+ settleMs: number;
83
+ captureMs: number;
84
+ encodeMs: number;
85
+ totalMs: number;
86
+ }
87
+
88
+ export interface CaptureResult {
89
+ format: Format;
90
+ bytes: Buffer | Readable;
91
+ page: PageMetrics;
92
+ timing: CaptureTiming;
93
+ /** Non-fatal warnings (e.g., font-load timeout). */
94
+ warnings: string[];
95
+ }
96
+
97
+ export interface ElementCaptureOptions extends CaptureOptions {
98
+ selector?: string;
99
+ xpath?: string;
100
+ /** Optional outerHTML verification snippet. */
101
+ snippet?: string;
102
+ /** Default 10. */
103
+ padding?: number;
104
+ maxDimensions?: { width: number; height: number };
105
+ scrollIntoView?: boolean;
106
+ }
107
+
108
+ export interface ElementCaptureResult extends CaptureResult {
109
+ element: {
110
+ selector: string;
111
+ xpath: string;
112
+ documentRect: Rect;
113
+ viewportRect: Rect;
114
+ verified: boolean;
115
+ verificationReason?: string;
116
+ };
117
+ /** Per spec § 8.1.3, failure mode: bytes=null + reason populated. */
118
+ reason?: string;
119
+ }
120
+
121
+ export interface LongPageCaptureOptions extends CaptureOptions {
122
+ /** null → use viewport height. */
123
+ stripHeight?: number | null;
124
+ overlap?: number;
125
+ maxHeight?: number;
126
+ /** Pause between scroll and capture, ms; default 100. */
127
+ scrollSettleMs?: number;
128
+ }
129
+
130
+ export type ResponsiveCaptureMode = 'fullPage' | 'viewport' | 'longPage';
131
+
132
+ export interface NamedViewport extends Viewport {
133
+ name: string;
134
+ }
135
+
136
+ export interface ResponsiveCaptureOptions extends CaptureOptions {
137
+ viewports: NamedViewport[];
138
+ reloadBetween?: boolean;
139
+ captureMode?: ResponsiveCaptureMode;
140
+ /** Default 1. >1 requires multiple browser contexts. */
141
+ parallelism?: number;
142
+ }
143
+
144
+ export interface ResponsiveCaptureResult {
145
+ captures: Array<CaptureResult & { viewportName: string }>;
146
+ capturedAt: string;
147
+ totalMs: number;
148
+ }
149
+
150
+ export interface StabilityReport {
151
+ fonts: 'ready' | 'timeout' | 'skipped';
152
+ images: { total: number; loaded: number; failed: number };
153
+ animations: 'none' | 'reduced' | 'settled' | 'timeout' | 'skipped';
154
+ totalMs: number;
155
+ }
156
+
157
+ // ─── Transform ────────────────────────────────────────────────────────────────
158
+
159
+ export interface CropOptions {
160
+ padding?: number;
161
+ format?: Format;
162
+ quality?: number;
163
+ }
164
+
165
+ export interface CropManyEntry {
166
+ id: string;
167
+ rect: Rect;
168
+ }
169
+
170
+ export type CropManyOptions = CropOptions & {
171
+ /** Default 4. */
172
+ parallelism?: number;
173
+ };
174
+
175
+ export interface ResizeOptions {
176
+ width?: number;
177
+ height?: number;
178
+ fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
179
+ position?: string;
180
+ kernel?: 'nearest' | 'cubic' | 'mitchell' | 'lanczos2' | 'lanczos3';
181
+ withoutEnlargement?: boolean;
182
+ format?: Format;
183
+ quality?: number;
184
+ }
185
+
186
+ export interface ConvertOptions {
187
+ format: Format;
188
+ quality?: number;
189
+ lossless?: boolean;
190
+ /** sharp webp/avif effort, 0-6. */
191
+ effort?: number;
192
+ }
193
+
194
+ export interface NormalizeOptions {
195
+ /** Normalize from this DPR; default: image's intrinsic DPR. */
196
+ fromDpr?: number;
197
+ /** Target DPR; default 1 (CSS pixel space). */
198
+ toDpr?: number;
199
+ }
200
+
201
+ export interface NormalizeResult {
202
+ buffer: Buffer;
203
+ dimensions: { width: number; height: number };
204
+ }
205
+
206
+ // ─── Compose ──────────────────────────────────────────────────────────────────
207
+
208
+ export interface AnnotationStyle {
209
+ stroke: string;
210
+ strokeWidth: number;
211
+ fill?: string;
212
+ badge?: { bg: string; fg: string };
213
+ }
214
+
215
+ export type AnnotationTheme = Record<string, AnnotationStyle>;
216
+
217
+ export interface Annotation {
218
+ rect: Rect;
219
+ /** Theme key, e.g., 'severity-critical'. */
220
+ style: string;
221
+ /** Optional letter/number badge. */
222
+ label?: string;
223
+ /** Not rendered to raster; appears in SVG output only. */
224
+ tooltip?: string;
225
+ }
226
+
227
+ export interface AnnotateOptions {
228
+ theme?: AnnotationTheme;
229
+ output?: 'png' | 'svg';
230
+ legend?: 'none' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
231
+ }
232
+
233
+ export interface HeatmapPoint {
234
+ x: number;
235
+ y: number;
236
+ /** Default 1. */
237
+ weight?: number;
238
+ }
239
+
240
+ export interface HeatmapOptions {
241
+ /** Kernel radius in CSS px; default 40. */
242
+ radius?: number;
243
+ /** Alpha multiplier; default 0.7. */
244
+ intensity?: number;
245
+ colorScale?: 'viridis' | 'magma' | 'severity' | ((t: number) => [number, number, number]);
246
+ blendMode?: 'overlay' | 'screen' | 'multiply';
247
+ }
248
+
249
+ export interface DiffOptions {
250
+ /** pixelmatch threshold; default 0.1. */
251
+ threshold?: number;
252
+ includeAA?: boolean;
253
+ alpha?: number;
254
+ diffColor?: [number, number, number];
255
+ output?: 'overlay' | 'side-by-side' | 'mask-only';
256
+ }
257
+
258
+ export interface DiffResult {
259
+ diffBuffer: Buffer;
260
+ changedPixels: number;
261
+ totalPixels: number;
262
+ percentChanged: number;
263
+ bbox?: Rect;
264
+ /** Optional and expensive; per-cluster bboxes. */
265
+ regions?: Rect[];
266
+ }
267
+
268
+ // ─── Tile (DZI) ───────────────────────────────────────────────────────────────
269
+
270
+ export type DziTileFormat = 'png' | 'jpeg' | 'webp';
271
+ export type DziOutput = 'buffers' | 'directory' | 'stream';
272
+
273
+ export interface TileDziOptions {
274
+ /** 256 or 512. Default 256. */
275
+ tileSize?: 256 | 512;
276
+ /** Px overlap between tiles; default 1. */
277
+ overlap?: number;
278
+ tileFormat?: DziTileFormat;
279
+ quality?: number;
280
+ output?: DziOutput;
281
+ /** When output: 'directory', subdir name in the temp dir. */
282
+ name?: string;
283
+ }
284
+
285
+ export interface DziTile {
286
+ level: number;
287
+ col: number;
288
+ row: number;
289
+ /** Relative path within the DZI tree. */
290
+ path: string;
291
+ bytes?: Buffer;
292
+ stream?: Readable;
293
+ }
294
+
295
+ export interface DziDescriptor {
296
+ /** Serialized .dzi XML. */
297
+ xml: string;
298
+ format: DziTileFormat;
299
+ width: number;
300
+ height: number;
301
+ tileSize: number;
302
+ overlap: number;
303
+ }
304
+
305
+ export interface DziResult {
306
+ descriptor: DziDescriptor;
307
+ tiles: DziTile[];
308
+ outputDir?: string;
309
+ totalBytes: number;
310
+ totalTiles: number;
311
+ }
312
+
313
+ // ─── Redaction ────────────────────────────────────────────────────────────────
314
+
315
+ export type RedactMode = 'blur' | 'mask' | 'pixelate';
316
+
317
+ export interface RedactRegion {
318
+ rect: Rect;
319
+ mode?: RedactMode;
320
+ /** For mode: 'blur'. */
321
+ radius?: number;
322
+ /** For mode: 'mask'. CSS color string. */
323
+ color?: string;
324
+ /** For mode: 'pixelate'. */
325
+ cellSize?: number;
326
+ }
327
+
328
+ export interface RedactOptions {
329
+ default?: RedactMode;
330
+ defaultRadius?: number;
331
+ }
332
+
333
+ export interface PiiPolicy {
334
+ inputTypes?: string[];
335
+ customSelectors?: string[];
336
+ mode: RedactMode;
337
+ radius?: number;
338
+ cellSize?: number;
339
+ color?: string;
340
+ }
341
+
342
+ // ─── Adapter contract (spec § 10) ─────────────────────────────────────────────
343
+
344
+ export interface AdapterCapabilities {
345
+ /** Browser limit, e.g., 16384 for Chrome. */
346
+ screenshotMaxHeight: number;
347
+ supportsViewportEmulation: boolean;
348
+ supportsCDP: boolean;
349
+ }
350
+
351
+ export interface ScreenshotOptions {
352
+ fullPage?: boolean;
353
+ clip?: Rect;
354
+ format?: Format;
355
+ quality?: number;
356
+ omitBackground?: boolean;
357
+ encoding?: 'binary' | 'base64';
358
+ }
359
+
360
+ export interface PageAdapter {
361
+ screenshot(opts: ScreenshotOptions): Promise<Buffer>;
362
+ evaluate<T>(fn: (...args: unknown[]) => T, ...args: unknown[]): Promise<T>;
363
+ scrollTo(point: Point): Promise<void>;
364
+ getMetrics(): Promise<PageMetrics>;
365
+ setViewport(viewport: Viewport): Promise<void>;
366
+ reload(opts?: { waitUntil?: string; timeout?: number }): Promise<void>;
367
+ readonly capabilities: AdapterCapabilities;
368
+ readonly label?: string;
369
+ }
370
+
371
+ // ─── Defaults & utilities ─────────────────────────────────────────────────────
372
+
373
+ export interface ScreenshotUtilsDefaults {
374
+ readonly format: Format;
375
+ readonly quality: number;
376
+ readonly fullPage: boolean;
377
+ readonly timeout: number;
378
+ readonly padding: number;
379
+ readonly stripHeight: number | null;
380
+ readonly tileSize: number;
381
+ readonly tileOverlap: number;
382
+ readonly tileFormat: DziTileFormat;
383
+ readonly tileQuality: number;
384
+ readonly waitForStable: Required<WaitForStableOptions>;
385
+ }
386
+
387
+ export declare const defaults: ScreenshotUtilsDefaults;
388
+ export declare const version: string;
389
+
390
+ // ─── Error shape (spec § 14.2) ────────────────────────────────────────────────
391
+
392
+ export interface ScreenshotUtilsErrorShape extends Error {
393
+ name: string;
394
+ /** Stable, machine-readable. */
395
+ code: string;
396
+ /** Sub-classification enum. */
397
+ reason?: string;
398
+ cause?: Error;
399
+ /** Structured debug data — never includes PII. */
400
+ context?: Record<string, unknown>;
401
+ retriable: boolean;
402
+ }
403
+
404
+ // Concrete error class names. Importers may use these as type guards via instanceof.
405
+ export type ScreenshotUtilsErrorName =
406
+ | 'ScreenshotUtilsError'
407
+ | 'CaptureError'
408
+ | 'BrowserUnavailableError'
409
+ | 'PageTooTallError'
410
+ | 'ElementNotFoundError'
411
+ | 'ElementNotVisibleError'
412
+ | 'ElementMismatchError'
413
+ | 'ElementTooLargeError'
414
+ | 'StabilityTimeoutError'
415
+ | 'CaptureTimeoutError'
416
+ | 'TransformError'
417
+ | 'InvalidRectError'
418
+ | 'UnsupportedFormatError'
419
+ | 'BufferCorruptError'
420
+ | 'ComposeError'
421
+ | 'DimensionMismatchError'
422
+ | 'ThemeError'
423
+ | 'HeatmapPointError'
424
+ | 'TileError'
425
+ | 'TileSizeError'
426
+ | 'TileWriteError'
427
+ | 'RedactionError'
428
+ | 'PolicyError'
429
+ | 'AdapterError'
430
+ | 'AdapterContractError'
431
+ | 'BrowserApiError'
432
+ | 'OptionalDependencyMissingError';