@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
package/dist/index.mjs CHANGED
@@ -14,7 +14,7 @@ var require_package = __commonJS({
14
14
  "package.json"(exports, module) {
15
15
  module.exports = {
16
16
  name: "@afixt/screenshot-utils",
17
- version: "0.9.0",
17
+ version: "1.0.0",
18
18
  description: "Screenshot capture, transform, compose, tile, and redact utilities for the AFixt/Revenant accessibility platform",
19
19
  type: "commonjs",
20
20
  main: "./dist/index.js",
@@ -97,7 +97,7 @@ var require_package = __commonJS({
97
97
  access: "restricted"
98
98
  },
99
99
  engines: {
100
- node: ">=22.22.1"
100
+ node: ">=20.9.0"
101
101
  },
102
102
  keywords: [
103
103
  "screenshot",
@@ -115,7 +115,7 @@ var require_package = __commonJS({
115
115
  "revenant"
116
116
  ],
117
117
  dependencies: {
118
- sharp: "0.34.5"
118
+ sharp: "^0.35.3"
119
119
  },
120
120
  peerDependencies: {
121
121
  "@napi-rs/canvas": "^0.1.0",
@@ -146,7 +146,7 @@ var require_package = __commonJS({
146
146
  "@commitlint/config-conventional": "20.5.3",
147
147
  "@eslint/js": "10.0.1",
148
148
  "@types/node": "22.19.19",
149
- "@vitest/coverage-v8": "4.1.5",
149
+ "@vitest/coverage-istanbul": "4.1.5",
150
150
  "clean-jsdoc-theme": "4.3.2",
151
151
  eslint: "10.3.0",
152
152
  "eslint-plugin-import-x": "4.16.2",
@@ -165,6 +165,7 @@ var require_package = __commonJS({
165
165
  "lint-staged": "17.0.2",
166
166
  "markdownlint-cli": "0.48.0",
167
167
  "npm-run-all2": "8.0.4",
168
+ openseadragon: "6.0.2",
168
169
  pixelmatch: "7.1.0",
169
170
  playwright: "1.60.0",
170
171
  pngjs: "7.0.0",
@@ -960,7 +961,10 @@ var require_settle = __commonJS({
960
961
  const {
961
962
  fonts = true,
962
963
  images = true,
964
+ backgroundImages = true,
963
965
  animations = "reduce",
966
+ network = "idle",
967
+ layoutStableMs = 250,
964
968
  maxWait = 5e3,
965
969
  idleMs = 200
966
970
  } = options;
@@ -968,7 +972,10 @@ var require_settle = __commonJS({
968
972
  const report = await adapter.evaluate(runInPage, {
969
973
  fonts,
970
974
  images,
975
+ backgroundImages,
971
976
  animations,
977
+ network,
978
+ layoutStableMs,
972
979
  maxWait,
973
980
  idleMs
974
981
  });
@@ -980,6 +987,9 @@ var require_settle = __commonJS({
980
987
  const report = {
981
988
  fonts: "skipped",
982
989
  images: { total: 0, loaded: 0, failed: 0 },
990
+ backgroundImages: "skipped",
991
+ network: "skipped",
992
+ layoutStable: "skipped",
983
993
  animations: "skipped"
984
994
  };
985
995
  const withTimeout = (promise, ms, label) => Promise.race([
@@ -1034,6 +1044,103 @@ var require_settle = __commonJS({
1034
1044
  report.images.total = imgs.length;
1035
1045
  await Promise.all(imgs.map(settleOneImage));
1036
1046
  };
1047
+ const collectBackgroundUrls = () => {
1048
+ const CAP = 200;
1049
+ const urls = /* @__PURE__ */ new Set();
1050
+ const els = doc.querySelectorAll("*");
1051
+ for (let i = 0; i < els.length && urls.size < CAP; i += 1) {
1052
+ const bg = globalThis.getComputedStyle(els[i]).backgroundImage;
1053
+ if (!bg || bg === "none") {
1054
+ continue;
1055
+ }
1056
+ for (const match of bg.matchAll(/url\((['"]?)([^'")]+)\1\)/g)) {
1057
+ const url = match[2];
1058
+ if (url && !url.startsWith("data:")) {
1059
+ urls.add(url);
1060
+ }
1061
+ }
1062
+ }
1063
+ return Array.from(urls).slice(0, CAP);
1064
+ };
1065
+ const settleOneBackgroundUrl = (url) => new Promise((resolve) => {
1066
+ const img = new globalThis.Image();
1067
+ const onload = () => {
1068
+ report.backgroundImages.loaded += 1;
1069
+ resolve();
1070
+ };
1071
+ const onerror = () => {
1072
+ report.backgroundImages.failed += 1;
1073
+ resolve();
1074
+ };
1075
+ img.addEventListener("load", onload, { once: true });
1076
+ img.addEventListener("error", onerror, { once: true });
1077
+ img.src = url;
1078
+ });
1079
+ const settleBackgroundImages = async () => {
1080
+ if (!opts.backgroundImages || !doc) {
1081
+ return;
1082
+ }
1083
+ const list = collectBackgroundUrls();
1084
+ report.backgroundImages = { total: list.length, loaded: 0, failed: 0 };
1085
+ if (list.length === 0) {
1086
+ return;
1087
+ }
1088
+ await withTimeout(
1089
+ Promise.all(list.map(settleOneBackgroundUrl)),
1090
+ opts.maxWait,
1091
+ "backgroundImages"
1092
+ ).catch(() => {
1093
+ });
1094
+ };
1095
+ const settleNetwork = async () => {
1096
+ if (opts.network !== "idle" || !doc) {
1097
+ return;
1098
+ }
1099
+ const perf = globalThis.performance;
1100
+ const countResources = () => perf && perf.getEntriesByType ? perf.getEntriesByType("resource").length : 0;
1101
+ const quietMs = 200;
1102
+ const deadline = Date.now() + opts.maxWait;
1103
+ let lastCount = countResources();
1104
+ let lastChange = Date.now();
1105
+ while (Date.now() < deadline) {
1106
+ const count = countResources();
1107
+ if (count !== lastCount) {
1108
+ lastCount = count;
1109
+ lastChange = Date.now();
1110
+ }
1111
+ if (doc.readyState === "complete" && Date.now() - lastChange >= quietMs) {
1112
+ report.network = "idle";
1113
+ return;
1114
+ }
1115
+ await new Promise((resolve) => {
1116
+ setTimeout(resolve, 50);
1117
+ });
1118
+ }
1119
+ report.network = "timeout";
1120
+ };
1121
+ const settleLayout = async () => {
1122
+ if (!opts.layoutStableMs || opts.layoutStableMs <= 0 || !doc) {
1123
+ return;
1124
+ }
1125
+ const el = doc.documentElement;
1126
+ const deadline = Date.now() + opts.maxWait;
1127
+ let lastHeight = el.scrollHeight;
1128
+ let stableSince = Date.now();
1129
+ while (Date.now() < deadline) {
1130
+ await new Promise((resolve) => {
1131
+ setTimeout(resolve, 50);
1132
+ });
1133
+ const height = el.scrollHeight;
1134
+ if (height !== lastHeight) {
1135
+ lastHeight = height;
1136
+ stableSince = Date.now();
1137
+ } else if (Date.now() - stableSince >= opts.layoutStableMs) {
1138
+ report.layoutStable = "stable";
1139
+ return;
1140
+ }
1141
+ }
1142
+ report.layoutStable = "timeout";
1143
+ };
1037
1144
  const settleAnimations = async () => {
1038
1145
  if (opts.animations === "ignore" || !doc) {
1039
1146
  return;
@@ -1081,9 +1188,12 @@ var require_settle = __commonJS({
1081
1188
  setTimeout(resolve, opts.idleMs);
1082
1189
  });
1083
1190
  };
1191
+ await settleNetwork();
1084
1192
  await settleFonts();
1085
1193
  await settleImages();
1194
+ await settleBackgroundImages();
1086
1195
  await settleAnimations();
1196
+ await settleLayout();
1087
1197
  await settleIdle();
1088
1198
  return report;
1089
1199
  }
@@ -1114,20 +1224,12 @@ var require_page = __commonJS({
1114
1224
  const start = nowMs();
1115
1225
  emit(events, "capture:start", { format, fullPage });
1116
1226
  let settleMs = 0;
1117
- const warnings = [];
1227
+ let warnings = [];
1118
1228
  if (settleOpts) {
1119
1229
  const settleStart = nowMs();
1120
1230
  const report = await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
1121
1231
  settleMs = nowMs() - settleStart;
1122
- if (report.fonts === "timeout") {
1123
- warnings.push("fonts did not settle within maxWait");
1124
- }
1125
- if (report.animations === "timeout") {
1126
- warnings.push("animations did not settle within maxWait");
1127
- }
1128
- if (report.images.failed > 0) {
1129
- warnings.push(`${report.images.failed} image(s) failed to load`);
1130
- }
1232
+ warnings = settleWarnings(report);
1131
1233
  }
1132
1234
  const metrics = await adapter.getMetrics();
1133
1235
  const browserFormat = decideBrowserFormat(format);
@@ -1167,6 +1269,28 @@ var require_page = __commonJS({
1167
1269
  });
1168
1270
  return result;
1169
1271
  }
1272
+ function settleWarnings(report) {
1273
+ const warnings = [];
1274
+ if (report.fonts === "timeout") {
1275
+ warnings.push("fonts did not settle within maxWait");
1276
+ }
1277
+ if (report.animations === "timeout") {
1278
+ warnings.push("animations did not settle within maxWait");
1279
+ }
1280
+ if (report.images.failed > 0) {
1281
+ warnings.push(`${report.images.failed} image(s) failed to load`);
1282
+ }
1283
+ if (report.backgroundImages && report.backgroundImages.failed > 0) {
1284
+ warnings.push(`${report.backgroundImages.failed} background image(s) failed to load`);
1285
+ }
1286
+ if (report.network === "timeout") {
1287
+ warnings.push("network did not reach idle within maxWait");
1288
+ }
1289
+ if (report.layoutStable === "timeout") {
1290
+ warnings.push("layout did not stabilize within maxWait");
1291
+ }
1292
+ return warnings;
1293
+ }
1170
1294
  function decideBrowserFormat(format) {
1171
1295
  return format === "webp" ? "png" : format;
1172
1296
  }
@@ -1424,6 +1548,7 @@ var require_long_page = __commonJS({
1424
1548
  var require_element = __commonJS({
1425
1549
  "src/capture/element.js"(exports, module) {
1426
1550
  var { CaptureTimeoutError } = require_errors();
1551
+ var { waitForStable } = require_settle();
1427
1552
  async function captureElement(adapter, options = {}) {
1428
1553
  const {
1429
1554
  selector,
@@ -1435,6 +1560,7 @@ var require_element = __commonJS({
1435
1560
  timeout = 5e3,
1436
1561
  format = "png",
1437
1562
  quality = 90,
1563
+ waitForStable: settleOpts = false,
1438
1564
  events = null
1439
1565
  } = options;
1440
1566
  if (!selector && !xpath) {
@@ -1473,6 +1599,7 @@ var require_element = __commonJS({
1473
1599
  }
1474
1600
  }
1475
1601
  let viewportRect = lookup.viewportRect;
1602
+ let documentRect = lookup.documentRect;
1476
1603
  if (scrollIntoView) {
1477
1604
  await adapter.scrollTo({
1478
1605
  x: 0,
@@ -1487,6 +1614,12 @@ var require_element = __commonJS({
1487
1614
  viewportRect = reread.viewportRect;
1488
1615
  }
1489
1616
  }
1617
+ const settled = await settleAndReread(adapter, { selector, xpath, timeout, settleOpts });
1618
+ const settleMs = settled.settleMs;
1619
+ if (settled.rect) {
1620
+ viewportRect = settled.rect.viewportRect;
1621
+ documentRect = settled.rect.documentRect;
1622
+ }
1490
1623
  const clip = {
1491
1624
  x: Math.max(0, Math.round(viewportRect.x - padding)),
1492
1625
  y: Math.max(0, Math.round(viewportRect.y - padding)),
@@ -1508,12 +1641,12 @@ var require_element = __commonJS({
1508
1641
  format,
1509
1642
  bytes,
1510
1643
  page: metrics,
1511
- timing: { settleMs: 0, captureMs, encodeMs: 0, totalMs },
1644
+ timing: { settleMs, captureMs, encodeMs: 0, totalMs },
1512
1645
  warnings: [],
1513
1646
  element: {
1514
1647
  selector: selector ?? "",
1515
1648
  xpath: xpath ?? "",
1516
- documentRect: lookup.documentRect,
1649
+ documentRect,
1517
1650
  viewportRect,
1518
1651
  verified: Boolean(snippet)
1519
1652
  }
@@ -1521,6 +1654,26 @@ var require_element = __commonJS({
1521
1654
  emit(events, "capture:complete", { format, totalMs, mode: "element" });
1522
1655
  return result;
1523
1656
  }
1657
+ async function settleAndReread(adapter, { selector, xpath, timeout, settleOpts }) {
1658
+ if (!settleOpts) {
1659
+ return { settleMs: 0, rect: null };
1660
+ }
1661
+ const settleStart = nowMs();
1662
+ await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
1663
+ const settleMs = nowMs() - settleStart;
1664
+ const reread = await withTimeout(
1665
+ adapter.evaluate(findElementInPage, { selector, xpath }),
1666
+ timeout,
1667
+ { operation: "adapter.evaluate(findElement after settle)" }
1668
+ );
1669
+ if (reread && reread.found) {
1670
+ return {
1671
+ settleMs,
1672
+ rect: { viewportRect: reread.viewportRect, documentRect: reread.documentRect }
1673
+ };
1674
+ }
1675
+ return { settleMs, rect: null };
1676
+ }
1524
1677
  function findElementInPage(args) {
1525
1678
  const doc = globalThis.document;
1526
1679
  let element;
@@ -2770,7 +2923,10 @@ var require_index = __commonJS({
2770
2923
  waitForStable: Object.freeze({
2771
2924
  fonts: true,
2772
2925
  images: true,
2926
+ backgroundImages: true,
2773
2927
  animations: "reduce",
2928
+ network: "idle",
2929
+ layoutStableMs: 250,
2774
2930
  maxWait: 5e3,
2775
2931
  idleMs: 200
2776
2932
  })