
Product
Introducing Repository Access Permissions and Custom Roles
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.
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
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.
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.
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.
Uint8Array/DataView.description/keywords/altText, not raw tag IDs. We map them onto the correct XMP namespaces (dc:, photoshop:, Iptc4xmpCore:, xmpRights:).fs required; operates on buffers.removeMetadata() strips XMP/EXIF in one call (keeps ICC colour profile).require()-able on Node ≥ 20.19 / 22.12.| 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.
Being precise about what's spec-backed vs. forward-looking:
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.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.
npm install aeo-image
Requires Node ≥ 20 (or any modern runtime with TextEncoder/Uint8Array).
import { readMetadata } from "aeo-image";
import { readFileSync } from "node:fs";
const meta = readMetadata(new Uint8Array(readFileSync("photo.webp")));
console.log(meta.description, meta.keywords);
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);
import { removeMetadata } from "aeo-image";
const clean = removeMetadata(input); // removes XMP/EXIF, keeps pixels + ICC
import { detectFormat } from "aeo-image";
detectFormat(buf); // "webp" | "jpeg" | "png" | "avif" | "heic" | "unknown"
| 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 — the field Google reads |
credit | string | photoshop:Credit |
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.
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:
VP8 /VP8L), synthesize the extended-format VP8X header it needs to carry metadata — reading canvas dimensions straight from the bitstream.VP8X.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.
Runnable scripts in examples/:
01-read-write.mjs — tag an image and read it back.02-aeo-batch.mjs — batch-tag a folder of images for AEO.03-strip-metadata.mjs — strip metadata for privacy.04-cloud-function.mjs — request handler shape for edge/serverless.node examples/01-read-write.mjs
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.
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.
FAQs
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
The npm package aeo-image receives a total of 266 weekly downloads. As such, aeo-image popularity was classified as not popular.
We found that aeo-image demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.

Product
Socket Firewall blocks malicious VS Code and Open VSX extensions before install, protecting developers from compromised editor marketplaces.