imgtune 0.1.0 → 0.3.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 (63) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +109 -5
  3. package/dist/animated.js +176 -0
  4. package/dist/audit.js +1 -0
  5. package/dist/backgrounds.js +42 -7
  6. package/dist/broken.js +5 -1
  7. package/dist/browser.js +4 -4
  8. package/dist/candidates.js +25 -0
  9. package/dist/check-report.js +56 -6
  10. package/dist/check.js +26 -5
  11. package/dist/cli.js +86 -8
  12. package/dist/egress-proxy.js +2 -1
  13. package/dist/fetch.js +9 -4
  14. package/dist/ffmpeg.js +530 -0
  15. package/dist/gpu.js +12 -0
  16. package/dist/hints.js +704 -16
  17. package/dist/image-file.js +154 -34
  18. package/dist/image-open.js +9 -0
  19. package/dist/images.js +142 -8
  20. package/dist/layout.js +7 -7
  21. package/dist/lcp.js +2 -1
  22. package/dist/locate-join.js +86 -0
  23. package/dist/locate-only.js +14 -3
  24. package/dist/locate.js +6 -2
  25. package/dist/measure.js +39 -1
  26. package/dist/media-encode.js +1054 -0
  27. package/dist/media-file.js +284 -0
  28. package/dist/media-limits.js +31 -0
  29. package/dist/media-model.js +251 -0
  30. package/dist/media-phase.js +675 -0
  31. package/dist/media-prepare.js +31 -0
  32. package/dist/px.js +11 -0
  33. package/dist/recheck.js +12 -4
  34. package/dist/remote.js +1065 -107
  35. package/dist/replay.js +20 -0
  36. package/dist/resources/audio.js +248 -0
  37. package/dist/resources/brotli.js +22 -0
  38. package/dist/resources/cache.js +59 -0
  39. package/dist/resources/capture.js +225 -0
  40. package/dist/resources/compression.js +122 -0
  41. package/dist/resources/favicon.js +130 -0
  42. package/dist/resources/font.js +440 -0
  43. package/dist/resources/index.js +8 -0
  44. package/dist/resources/lottie.js +491 -0
  45. package/dist/resources/model.js +478 -0
  46. package/dist/resources/phase.js +171 -0
  47. package/dist/resources/pool.js +47 -0
  48. package/dist/resources/report.js +57 -0
  49. package/dist/resources/sfnt.js +305 -0
  50. package/dist/resources/svg.js +178 -0
  51. package/dist/resources/types.js +1 -0
  52. package/dist/resources/zip.js +67 -0
  53. package/dist/result-cache.js +56 -3
  54. package/dist/run.js +32 -8
  55. package/dist/sizes.js +7 -5
  56. package/dist/summary.js +69 -3
  57. package/dist/vector-art.js +294 -0
  58. package/dist/vector-phase.js +222 -0
  59. package/dist/video-analysis.js +371 -0
  60. package/dist/webp-animation.js +283 -0
  61. package/package.json +25 -4
  62. package/skills/imgtune/SKILL.md +8 -4
  63. package/skills/imgtune/reference.md +1 -1
@@ -0,0 +1,31 @@
1
+ // Makes a media file readable by the pipeline and probes it: the shared step of `imgtune image` (media-file.ts) and the scan's
2
+ // media phase (media-phase.ts). Kept apart from media-file.ts, which imports the scan's remote.ts helpers, so the phase can use
3
+ // it without a cycle. A plain file is probed with ffprobe; an animated WebP is read frame by frame (webp-animation.ts), since
4
+ // ffmpeg cannot read it, and its probe is taken from the animation.
5
+ import { animationOf } from "./animated.js";
6
+ import { probeMedia } from "./ffmpeg.js";
7
+ import { WebpAnimation } from "./webp-animation.js";
8
+ import { MAX_INPUT_PIXELS_FFMPEG } from "./media-limits.js";
9
+ /** Longest a probe of one file may take, in ms. */
10
+ export const PROBE_MS = 30_000;
11
+ /** The frame rate of an animated WebP's constant-rate timeline. */
12
+ export const WEBP_FPS = 30;
13
+ /**
14
+ * Makes a file readable and probes it. An animated WebP is opened as a WebpAnimation (no frame is decoded here): its probe is its
15
+ * canvas size and its length. `framePixelLimit` is the per-frame pixel limit (MAX_INPUT_PIXELS_FFMPEG by default; a test lowers
16
+ * it). An APNG reports no length to ffprobe, so its animation length stands in. Throws with the reason when the file cannot be
17
+ * read, or is too large to score.
18
+ */
19
+ export async function prepareMedia(path, buffer, _dir, framePixelLimit = MAX_INPUT_PIXELS_FFMPEG, _signal) {
20
+ const animation = animationOf(buffer);
21
+ if (animation?.format === "webp") {
22
+ const webp = new WebpAnimation(buffer, WEBP_FPS, framePixelLimit);
23
+ const probe = { durationS: webp.durationMs / 1000, width: webp.width, height: webp.height, fps: WEBP_FPS, videoCodec: "webp" };
24
+ return { source: webp, probe, animation };
25
+ }
26
+ const probe = await probeMedia(path, PROBE_MS);
27
+ if (!probe)
28
+ throw new Error("ffprobe could not read this file");
29
+ const sized = animation && !(probe.durationS > 0) ? { ...probe, durationS: animation.durationMs / 1000 } : probe;
30
+ return { source: path, probe: sized, animation };
31
+ }
package/dist/px.js ADDED
@@ -0,0 +1,11 @@
1
+ // Whole-px rounding of a proposed sizes value. A fitted length is rounded in the direction that keeps the chosen srcset
2
+ // candidate covering the need: a length up, a plus offset up, and a minus offset down in magnitude (so the value is not
3
+ // below the fit). A `sizes` value like 190.04px is never printed.
4
+ /** A px length as whole px, rounded up (190.04 -> 191). */
5
+ export function wholePx(px) {
6
+ return Math.ceil(px - 1e-9);
7
+ }
8
+ /** A signed px offset of a calc(): a plus offset rounded up, a minus offset rounded down in magnitude. */
9
+ export function offsetPx(b) {
10
+ return b < 0 ? -Math.floor(-b + 1e-9) : Math.ceil(b - 1e-9);
11
+ }
package/dist/recheck.js CHANGED
@@ -6,11 +6,12 @@ import { resolve } from "node:path";
6
6
  import { launchChromium } from "./browser.js";
7
7
  import { DEFAULT_VIEWPORTS } from "./config.js";
8
8
  import { hasFix } from "./images.js";
9
- import { parseGpuDevice } from "./gpu.js";
9
+ import { gpuSettingOf, gpuSettingOne } from "./gpu.js";
10
10
  import { parseViewports, safeText, scanOnce, scorerNote } from "./remote.js";
11
11
  import { settleLimit } from "./concurrency.js";
12
12
  import { cleanRecord } from "./locate-join.js";
13
13
  import { batchLine, rowLine } from "./recheck-rows.js";
14
+ import { hiddenSummaryLine } from "./hints.js";
14
15
  import { parseAllowHosts, thirdPartyFromFlags } from "./third-party.js";
15
16
  /** Browser contexts (one per viewport loaded) a recheck keeps in flight over all its pages together. */
16
17
  export const RECHECK_CONTEXTS = 3;
@@ -46,7 +47,7 @@ export function parseRecheckArgs(argv) {
46
47
  if (arg === "--allow-third-party")
47
48
  continue;
48
49
  if (arg === "--gpu") {
49
- gpu ??= { device: 0 };
50
+ gpu ??= gpuSettingOne(0);
50
51
  continue;
51
52
  }
52
53
  if (arg === "--no-cache") {
@@ -61,7 +62,7 @@ export function parseRecheckArgs(argv) {
61
62
  if (arg === "--recheck")
62
63
  files.push(value);
63
64
  else if (arg === "--gpu-device")
64
- gpu = { device: parseGpuDevice(value) };
65
+ gpu = gpuSettingOf(value);
65
66
  else if (arg === "--viewports")
66
67
  viewports = parseViewports(value);
67
68
  else if (arg === "--allow-host")
@@ -210,7 +211,11 @@ export function compareImage(before, after, unchangedSrc, how = { ambiguous: 0 }
210
211
  const reused = unchangedSrc ? " (src unchanged: estimate reused)" : "";
211
212
  lines.push(` hint sizes ${sizesBefore} -> ${sizesAfter} saves ${kb(savedBefore)} -> ${kb(savedAfter)}${reused}`);
212
213
  // Predicted (the hint, per view, averaged over viewports) next to measured (served bytes at the same viewports).
213
- const split = before.fix.bytes.sizes !== undefined ? ` (sizes ${kb(before.fix.bytes.sizes)}, quality ${kb(before.fix.bytes.quality ?? 0)})` : "";
214
+ const split = before.fix.bytes.resize !== undefined
215
+ ? ` (sizes ${kb(before.fix.bytes.sizes ?? 0)}, resize ${kb(before.fix.bytes.resize)}, quality ${kb(before.fix.bytes.quality ?? 0)})`
216
+ : before.fix.bytes.sizes !== undefined
217
+ ? ` (sizes ${kb(before.fix.bytes.sizes)}, quality ${kb(before.fix.bytes.quality ?? 0)})`
218
+ : "";
214
219
  const predicted = `predicted ${saving(savedBefore)}/view${split}`;
215
220
  const mean = measured.length > 0 ? measured.reduce((sum, b) => sum + b, 0) / measured.length : undefined;
216
221
  if (mean !== undefined)
@@ -369,6 +374,9 @@ async function recheckPage(file, args, cwd, log, options) {
369
374
  if (details.length > 0)
370
375
  out.push("", "not passed:", ...details);
371
376
  out.push(batchLine(rows, seconds) + ` on ${safeText(target.href)} at ${viewports.length} viewport(s)`);
377
+ const hiddenLine = hiddenSummaryLine(fresh.hidden);
378
+ if (hiddenLine !== undefined)
379
+ out.push(hiddenLine);
372
380
  out.push(failures.length === 0 ? "ok: no image became undersized, and no bytes rose without a sharpness fix" : `FAILED: ${failures.length} check(s)`);
373
381
  for (const failure of failures)
374
382
  out.push(` ${safeText(failure)}`);