npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

aeo-image

v1.2.0

Published

Write descriptive + rights metadata (captions, keywords, alt text, creator, license) into WebP, AVIF, HEIC, JPEG & PNG — self-describing images that Google Images reads and recommends embedding, built for the AI-search era. The only pure-JS, zero-dependen

Downloads

1,221

Readme

aeo-image

Write descriptive + rights metadata — captions, keywords, alt text, creator, license — into WebP, AVIF, HEIC, JPEG & PNG so your images are self-describing: Google Images reads embedded IPTC metadata (and recommends embedding it), and the description travels with the file as images get downloaded, indexed, and ingested by AI pipelines. The only pure-JS, zero-dependency library that writes XMP to AVIF/HEIC. Byte-preserving (never re-encodes). Runs on Node, Bun, Deno & edge.

CI npm install size dependencies provenance Socket Badge types license

import { writeMetadata, readMetadata } from "aeo-image";

const tagged = writeMetadata(webpBytes, {
  description: "A golden retriever catching a frisbee on a beach at sunset",
  keywords: ["dog", "beach", "sunset"],
  altText: "Brown dog mid-jump catching an orange frisbee",
});

readMetadata(tagged);
// → { description: "...", keywords: ["dog","beach","sunset"], altText: "..." }

The image pixels are never re-encoded. Only the metadata block is spliced in.


Why this exists

Metadata embedded inside an image file travels with it — when the file is downloaded, hot-linked, indexed by image search, or ingested by an AI pipeline as a file, the page's HTML context is gone but the embedded description, attribution, and license remain. That metadata lives in XMP (and IPTC), a packet inside the container. See What Google actually documents below for the evidence-backed specifics.

Today, writing XMP into modern web image formats from JavaScript means one of:

| Option | Problem | | --- | --- | | exiftool (Perl) / wrappers | Requires a binary; won't run in a sandboxed cloud function | | sharp (libvips) | Native dependency and re-encodes your pixels — quality loss; cannot even write XMP to AVIF | | piexifjs | JPEG-first; WebP write is buggy; no AVIF | | exifr / ExifReader | Read-only |

No other pure-JS, zero-dependency library writes descriptive metadata into WebP and AVIF without re-encoding — not even sharp can write XMP to AVIF. aeo-image does. See docs/landscape.md for the full competitive analysis.

Features

  • 📦 Zero runtime dependencies — pure TypeScript over Uint8Array/DataView.
  • 🖼️ Byte-preserving — splices metadata only; compressed image data is copied verbatim, never re-encoded.
  • 🧠 Semantic, AEO-oriented API — you write description/keywords/altText, not raw tag IDs. We map them onto the correct XMP namespaces (dc:, photoshop:, Iptc4xmpCore:, Iptc4xmpExt:, xmpRights:).
  • ☁️ Runs anywhere — Node, Deno, Bun, Cloudflare Workers, Vercel/Netlify/Lambda edge functions. No fs required; operates on buffers.
  • 🔒 Privacy-friendlyremoveMetadata() strips XMP/EXIF in one call (keeps ICC colour profile).
  • 🧩 ESM, fully typed — and require()-able on Node ≥ 20.19 / 22.12.

Format support

| Format | Read | Write | Status | | --- | :---: | :---: | --- | | WebP | ✅ | ✅ | Implemented & tested (simple + extended) | | AVIF | ✅ | ✅ | Implemented & tested (ISOBMFF item + full iloc offset recalculation) | | HEIC | ✅ | ✅ | Implemented & tested (shares AVIF's ISOBMFF engine; validated against the Nokia HEIF conformance suite) | | JPEG | ✅ | ✅ | Implemented & tested (APP1 segment splice) | | PNG | ✅ | ✅ | Implemented & tested (standard iTXt, CRC-correct) |

All four major web image formats are supported. An unrecognized format throws a typed UnsupportedFormatError rather than risking silent corruption. See docs/roadmap.md.

What Google actually documents

Being precise about what's spec-backed vs. forward-looking:

  • Google Images reads embedded IPTC photo metadata — creator, credit line, copyright, and licensing — and shows it in results (including the Licensable badge). Google recommends embedding metadata in the file (over sidecars) so it isn't lost. → Image metadata in Google Images · IPTC's quick guide aeo-image writes exactly these fieldscreator, credit, copyrightNotice, licenseUrl (xmpRights:WebStatement), and licensor (IPTC PLUS plus:Licensor).
  • ℹ️ For image understanding/ranking, Google primarily uses the HTML alt attribute, page context, and computer vision — not embedded metadata. → Image SEO best practices. So embedding alt text in the file complements (doesn't replace) your HTML alt; its value is durability, portability, accessibility, and attribution.
  • 🔭 AI answer engines reading embedded metadata is plausible and increasingly likely as they consume files directly, but is not a published spec today. Treat it as forward-looking.

In short: the documented, here-today win is portable, machine-readable attribution + licensing (which Google reads and recommends embedding) plus accessibility; the AI-search upside is a bet on where file-level metadata is heading.

Standards & conformance

aeo-image writes metadata as an Adobe XMP packet (the modern serialization) — not the legacy IPTC-IIM binary block. This is what Google and current tooling read.

Fields conform to the IPTC Photo Metadata Standard 2025.1 (the current revision), specifically the descriptive, accessibility, rights/licensing, and AI-provenance subset, across these namespaces:

| Namespace | Prefix | Used for | | --- | --- | --- | | Dublin Core | dc: | description, title, subject/keywords, creator, rights | | IPTC Core | Iptc4xmpCore: | AltTextAccessibility (IPTC 2021.1+) | | IPTC Extension | Iptc4xmpExt: | digital source type + AI-generation provenance (IPTC 2025.1) | | Adobe Photoshop | photoshop: | credit, copyright | | XMP Rights | xmpRights: | web statement (license URL) | | PLUS | plus: (ns 1.0) | licensor (license-acquisition link) |

This includes IPTC 2025.1's four AI-generation provenance properties (AIPromptInformation, AIPromptWriterName, AISystemUsed, AISystemVersionUsed) plus DigitalSourceType — see Label AI-generated images. Readable by exiftool (named tags from 13.40). C2PA / Content Credentials (cryptographically signed manifests) are a separate standard and out of scope.

Install

npm install aeo-image

Requires Node ≥ 20 (or any modern runtime with TextEncoder/Uint8Array).

Usage

Read

import { readMetadata } from "aeo-image";
import { readFileSync } from "node:fs";

const meta = readMetadata(new Uint8Array(readFileSync("photo.webp")));
console.log(meta.description, meta.keywords);

Write (tag for AEO)

import { writeMetadata } from "aeo-image";
import { readFileSync, writeFileSync } from "node:fs";

const input = new Uint8Array(readFileSync("photo.webp"));
const output = writeMetadata(input, {
  description: "Solar panels on a barn roof in rural Vermont",
  title: "Rural Solar Install",
  keywords: ["solar", "renewable energy", "Vermont", "agrivoltaics"],
  altText: "Rows of black solar panels mounted on a red barn roof",
  creator: "Jane Doe",
  credit: "Example Studio",
  rights: "© 2026 Example Studio",
});
writeFileSync("photo.tagged.webp", output);

Label AI-generated images

IPTC Photo Metadata 2025.1 added four AI-provenance fields, which pair with DigitalSourceType — the field ecosystems read to label an image as AI-generated:

import { writeMetadata, DIGITAL_SOURCE_TYPE } from "aeo-image";

const output = writeMetadata(input, {
  description: "A neon-lit street market at night in the rain",
  digitalSourceType: DIGITAL_SOURCE_TYPE.trainedAlgorithmicMedia,
  ai: {
    prompt: "neon street market, rain reflections, cinematic 35mm",
    promptWriter: "Jane Doe",
    system: "DALL-E via Bing Image Creator",
    systemVersion: "3",
  },
  // Per IPTC guidance, leave `creator` empty for fully AI-generated images —
  // the prompt writer is explicitly not the image creator.
});

Strip (privacy)

import { removeMetadata } from "aeo-image";
const clean = removeMetadata(input); // removes XMP/EXIF, keeps pixels + ICC

Detect format

import { detectFormat } from "aeo-image";
detectFormat(buf); // "webp" | "jpeg" | "png" | "avif" | "heic" | "unknown"

API

| Function | Signature | Description | | --- | --- | --- | | readMetadata | (buf: Uint8Array) => ImageMetadata | Read semantic metadata. | | writeMetadata | (buf: Uint8Array, meta: ImageMetadata) => Uint8Array | Return a new buffer with metadata written; pixels preserved. | | removeMetadata | (buf: Uint8Array) => Uint8Array | Return a new buffer with XMP/EXIF stripped. | | detectFormat | (buf: Uint8Array) => ImageFormat | Identify the container by magic bytes. | | serializeXmp | (meta: ImageMetadata) => string | Build a standalone XMP packet (advanced). | | parseXmp | (xmp: string) => ImageMetadata | Parse a standalone XMP packet (advanced). |

ImageMetadata

| Field | Type | Maps to | | --- | --- | --- | | description | string | dc:description (x-default) | | title | string | dc:title (x-default) | | keywords | string[] | dc:subject (rdf:Bag) | | creator | string | dc:creator (rdf:Seq) | | rights | string | dc:rights (x-default) | | altText | string | Iptc4xmpCore:AltTextAccessibility | | credit | string | photoshop:Credit | | copyrightNotice | string | photoshop:Copyright | | licenseUrl | string | xmpRights:WebStatementGoogle Licensable | | licensor | { url, name? } | IPTC PLUS plus:LicensorGoogle "Get this image" link | | digitalSourceType | string (IRI) | Iptc4xmpExt:DigitalSourceTypeAI-generated disclosure; use DIGITAL_SOURCE_TYPE.* | | ai | { prompt?, promptWriter?, system?, systemVersion? } | IPTC 2025.1 Iptc4xmpExt:AIPromptInformation / AIPromptWriterName / AISystemUsed / AISystemVersionUsed |

The last three implement the fields Google Images reads for the Licensable badge and license link. All functions return a new buffer and never mutate the input. See docs/xmp-fields.md for the complete field/namespace reference and AEO rationale.

How it works

WebP is a RIFF container: a 12-byte header followed by a flat list of chunks. Metadata lives in a dedicated XMP chunk. Writing it means:

  1. Parse the chunk list (no decoding of image data).
  2. If the file is "simple" (VP8 /VP8L), synthesize the extended-format VP8X header it needs to carry metadata — reading canvas dimensions straight from the bitstream.
  3. Set the XMP presence flag bit in VP8X.
  4. Splice in the XMP chunk and recompute the RIFF size.

The compressed image chunk is copied byte-for-byte.

JPEG stores XMP in an APP1 marker segment (signature http://ns.adobe.com/xap/1.0/\0); PNG uses a standard iTXt chunk (XML:com.adobe.xmp, CRC-32 recomputed). Both follow the same splice pattern — locate/replace the metadata block, copy everything else (including the entropy-coded scan / IDAT data) byte-for-byte.

AVIF and HEIC are harder: they're ISOBMFF box trees (same container, different codec) where XMP is an item whose bytes are located via absolute file offsets in the iloc box. Inserting metadata shifts mdat, invalidating every offset — so aeo-image reads each item's bytes, emits a fresh meta (regenerated iinf/iloc/iref) and mdat, and recomputes all offsets from the new layout. The compressed image data is relocated verbatim (verified: decoded pixels are byte-identical before and after). The same engine handles HEIC, and was validated against the full Nokia HEIF conformance suite — including grid-tiled, overlay, thumbnail, and multi-item files.

Read docs/architecture.md, docs/webp-format.md, and docs/avif-format.md for the deep dives.

Examples

Runnable scripts in examples/:

node examples/01-read-write.mjs

Development

npm test          # run the test suite (Node's built-in runner, no install needed on Node 22+)
npm run typecheck # type-check without emitting
npm run build     # emit ESM + .d.ts to dist/

Tests run real .webp fixtures through full round-trips and validate RIFF framing, flag bits, and byte-level pixel preservation. Output is independently verified to parse in exiftool (in CI), and has been checked against ImageMagick and Apple's imaging stack.

Contributing

Implementing JPEG, PNG, and AVIF is the active roadmap — see CONTRIBUTING.md and docs/roadmap.md. The architecture is designed so each new format is a thin adapter over a shared container/splice core.

License

MIT