← Files stark AI DeveloperARCHIVED FILE

skills/animated-readme-logo/scripts/inspect-animated-image.mjs

4.6 KB · Oct 5, 2026 · 18:31 UTC

↓ Download file

#!/usr/bin/env node
import {
  AnimatedImageError,
  DEFAULT_ANIMATED_IMAGE_LIMITS,
  inspectAnimatedImageFile,
} from "./lib/animated-image.mjs";

function usage() {
  return `Usage: node inspect-animated-image.mjs [options] <image>

Validate that a local GIF, APNG, or WebP is structurally animated and free of hidden metadata.

Options:
  --json                 Emit one deterministic JSON object
  --max-file-bytes N     Maximum input size (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxFileBytes})
  --max-chunk-bytes N    Maximum logical chunk size (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxChunkBytes})
  --max-frames N         Maximum frame count (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxFrames})
  --max-dimension N      Maximum width or height (default: ${DEFAULT_ANIMATED_IMAGE_LIMITS.maxDimension})
  -h, --help             Show this help

Exit codes: 0=valid animated image, 1=invalid/static/limit failure, 2=usage/I/O.`;
}

function positiveInteger(option, value) {
  if (!/^[1-9]\d*$/.test(value ?? "")) {
    throw new AnimatedImageError(
      "INVALID_OPTION",
      `${option} requires a positive integer`,
      "usage",
    );
  }
  const parsed = Number(value);
  if (!Number.isSafeInteger(parsed)) {
    throw new AnimatedImageError(
      "INVALID_OPTION",
      `${option} exceeds the safe integer range`,
      "usage",
    );
  }
  return parsed;
}

function parseArgs(argv) {
  const args = { limits: {} };
  const limitOptions = new Map([
    ["--max-file-bytes", "maxFileBytes"],
    ["--max-chunk-bytes", "maxChunkBytes"],
    ["--max-frames", "maxFrames"],
    ["--max-dimension", "maxDimension"],
  ]);
  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index];
    if (argument === "--help" || argument === "-h") {
      args.help = true;
      continue;
    }
    if (argument === "--json") {
      args.json = true;
      continue;
    }
    if (limitOptions.has(argument)) {
      args.limits[limitOptions.get(argument)] = positiveInteger(argument, argv[index + 1]);
      index += 1;
      continue;
    }
    if (argument.startsWith("-")) {
      throw new AnimatedImageError("UNKNOWN_OPTION", `unknown option: ${argument}`, "usage");
    }
    if (args.file) {
      throw new AnimatedImageError("TOO_MANY_INPUTS", "provide exactly one image path", "usage");
    }
    args.file = argument;
  }
  return args;
}

function outputSuccess(file, inspected, jsonOutput) {
  const payload = { valid: true, path: file, ...inspected, errors: [] };
  if (jsonOutput) {
    console.log(JSON.stringify(payload));
    return;
  }
  console.log(`VALID ANIMATED IMAGE: ${file}`);
  console.log(`Format: ${inspected.format}`);
  console.log(`Dimensions: ${inspected.width} x ${inspected.height}`);
  console.log(`Frames: ${inspected.frameCount}`);
  console.log(`Animated: yes`);
  console.log(`Loop: ${inspected.loop}`);
}

function outputFailure(file, error, jsonOutput) {
  const payload = {
    valid: false,
    path: file ?? null,
    errors: [{ code: error.code ?? "UNEXPECTED_ERROR", message: error.message }],
  };
  if (jsonOutput) {
    console.log(JSON.stringify(payload));
    return;
  }
  console.error(`INVALID ANIMATED IMAGE: ${file ?? "(no input)"}`);
  console.error(`- [${payload.errors[0].code}] ${payload.errors[0].message}`);
}

function main() {
  let args;
  try {
    args = parseArgs(process.argv.slice(2));
  } catch (error) {
    const normalized =
      error instanceof AnimatedImageError
        ? error
        : new AnimatedImageError("INVALID_ARGUMENT", error.message, "usage");
    outputFailure(null, normalized, process.argv.includes("--json"));
    if (!process.argv.includes("--json")) console.error(usage());
    process.exitCode = 2;
    return;
  }
  if (args.help) {
    console.log(usage());
    return;
  }
  if (!args.file) {
    const error = new AnimatedImageError("MISSING_INPUT", "provide one local image path", "usage");
    outputFailure(null, error, args.json);
    if (!args.json) console.error(usage());
    process.exitCode = 2;
    return;
  }

  try {
    const inspected = inspectAnimatedImageFile(args.file, args.limits);
    if (!inspected.animated) {
      throw new AnimatedImageError(
        "STATIC_IMAGE",
        `${inspected.format} contains ${inspected.frameCount} frame and is not animated`,
      );
    }
    outputSuccess(args.file, inspected, args.json);
  } catch (error) {
    const normalized =
      error instanceof AnimatedImageError
        ? error
        : new AnimatedImageError("UNEXPECTED_ERROR", error.message);
    outputFailure(args.file, normalized, args.json);
    process.exitCode = ["usage", "io"].includes(normalized.category) ? 2 : 1;
  }
}

main();

SHA-256: 7c2fc6a9c57ca53147358e42a6601a37e1dc71f18c77a1d281f6d97b9e6bd575