Skip to main content

README

exiftool-ts v0.1.3


exiftool-ts

TypeScript rewrite of ExifTool for Node.js. Read and write metadata from JPEG, PNG, WebP, AVIF/HEIF, and TIFF-family images — with a fully typed library API and a drop-in CLI.

Why

ExifTool is the gold standard for metadata — but it's a 30k-line Perl program. Every invocation pays a Perl startup cost, embedding it in a JS/TS service means shelling out or managing sidecars, and there are no types.

exiftool exists to bring that capability natively into the TypeScript ecosystem:

  • Typed end to endread() returns an inferred FileInfo; no parsing strings.
  • Embeddable — zero runtime dependencies; import the library directly or ship the CLI alongside your service. No child processes.
  • Trust ExifTool as ground truth — our parity suite diffs our output against the real exiftool binary on every run; formatting bugs get caught, not shipped.
  • Auto-generated tag database — tag definitions are parsed from the ExifTool Perl source at build time (~thousands of tags), not hand-maintained.

Status & parity

Early development, moving fast. The full, maintained status matrix lives in the parity docs and the working register in DIVERGENCES.md.

Value-level parity is enforced by src/cli/exiftool-parity.test.ts, which runs the real exiftool binary on shared fixtures and fails on any undocumented divergence. Remaining gaps are registered in KNOWN_DIVERGENCES inside that file with reasons (file-date timezone offsets, makernote lens lookups, …).

Install

npm (CLI + library, Node ≥ 18):

npm i -g @woss/exiftool
exiftool-ts photo.jpg

The package ships compiled JS with full .d.ts types and has zero runtime dependencies.

JSR (for Deno consumers, runs from TypeScript source):

deno install -A -n exiftool-ts jsr:@woss/exiftool

Library usage

The package ships .d.ts declarations, so consumers get full types and autocomplete out of the box:

import { ExifTool } from "@woss/exiftool";

const tool = new ExifTool();

const info = await tool.read("photo.jpg");
info.tags.Make; // "Canon" — typed as TagValue
info.tags.ExposureTime; // "1/200" — formatted like exiftool
Object.keys(info.tags); // browse everything

// In-memory buffers work too:
const meta = await tool.readBytes(imageBuffer);

// Writing (creates photo.jpg_original unless suppressed):
await tool.write("photo.jpg", { Artist: "me", Copyright: "(c)" });

Exported surface: ExifTool, TagDb, writeTags, UnsupportedFormatError, and the types FileInfo, TagValue, WriteResult, ParseHints, ReadOptions, TagEntry, TagGroups.

Plugins

Formats are plugins. By default ExifTool loads every built-in one, but you can restrict an instance to a set — the bundler then ships only those parsers:

import { ExifTool } from "@woss/exiftool";
import { MODERN_PLUGINS } from "@woss/exiftool/plugins";

const tool = new ExifTool({ plugins: MODERN_PLUGINS }); // JPEG, PNG, WebP, AVIF, TIFF-family RAW (read-only)

MODERN_PLUGINS / ALL_PLUGINS cover the shipped formats; individual parsers (jpegParser, pngParser, webpParser, avifParser, tiffRawParser) and fully custom plugins ({ format, extensions, canParse, parse, writeBytes? }) come from the same subpath. A plugin without writeBytes is read-only. The default — every built-in — loads lazily, so new ExifTool() keeps working unchanged.

Browser

The parser core is platform-free. The browser export condition resolves to a bundle with no Node builtins; reads and writes work on in-memory buffers:

import { ExifTool, MODERN_PLUGINS } from "@woss/exiftool/browser";

const tool = new ExifTool({ plugins: MODERN_PLUGINS });
const { bytes, written } = await tool.writeBytes(imageBuffer, { Artist: "me" });
const info = await tool.readBytes(bytes);

Path-based read/write stay Node-only (main entry); browser consumers get readBytes/writeBytes.

CLI usage

exiftool-ts photo.jpg # default tabular dump
exiftool-ts -j photo.jpg # JSON
exiftool-ts -X photo.jpg > out.xml # XML
exiftool-ts -csv *.jpg > out.csv # CSV
exiftool-ts -r -ext jpg . # recurse a directory tree
exiftool-ts -if '$Make eq Canon' *.jpg # condition filter
exiftool-ts -ee -j multi-picture.jpg # embedded images as extra docs
exiftool-ts '-Artist=me' photo.jpg # write a tag (_original backup)
exiftool-ts --overwrite-original '-Software=x' photo.jpg
printf -- '-j\nphoto.jpg\n-execute\n-stay_open\nFalse\n' \
| exiftool-ts -stay_open True # persistent daemon

Architecture

mod.ts → JSR barrel (re-exports the public API)
src/
cli.ts → CLI entry point (arg normalization → main())
exiftool.ts → Node ExifTool class (path-based read / write via fs)
exiftool-core.ts → Platform-free core (readBytes / writeBytes / plugins)
browser.ts → Browser entry (platform-free bundle)
plugins.ts → Plugin presets (./plugins subpath)
tag-db.ts → Tag database (name/id/group lookups)
types.ts → Core types (FileInfo, TagEntry, TagValue, …)
cli/
args.ts → ExifTool-style argument parser (normalizeArgs + parseCliArgs)
filestat.ts → File-stat tag overlay (FileSize, FileModifyDate, …)
filter.ts → -if condition evaluation
glob.ts → directory recursion / extension filters
output.ts → JSON / XML / CSV / tabular formatters
stay-open.ts → -stay_open daemon command loop
verbosity.ts → -v/-q rendering helpers
format/
mod.ts → Plugin contract + detection (FormatParser, detectParser)
all.ts → Built-in plugin set (lazy default)
jpeg.ts → JPEG segment walk (EXIF/XMP/IPTC/ICC/MPF/Adobe)
png.ts → PNG chunk walk (eXIf/iTXt/zTXt/tEXt/iCCP…)
webp.ts → RIFF/VP8X chunk walk
avif.ts → ISOBMFF box walk (meta items, colr, pixi)
exif/
ifd.ts → Bounds-checked IFD structure parser
tiff.ts → Shared TIFF engine (both endians, GPS/sub-IFDs)
tiff-builder.ts → TIFF serializer for the write path
values.ts → PrintConv value formatting
composite.ts → Composite tag derivation (35mm equiv, LightValue…)
xmp.ts → XMP/RDF extraction
app13.ts → Photoshop IRB + IPTC IIM extraction
icc.ts → ICC profile header/tag parsing
write/
pipeline.ts → Safe-overwrite pipeline (temp swap + _original backup)
writers.ts → Per-container writers (JPEG APP1 / PNG eXIf / WebP / AVIF)
utils/
crc32.ts → CRC-32 (PNG chunks)
encoding.ts → String encoding/escaping helpers
scripts/
generate-tags.ts → Generate tag DB from ExifTool Perl source
test-harness.ts → Compare output against real exiftool
coverage-audit.ts → Enforce 100% line/function coverage gate
.github/workflows/
release.yml → npm + JSR publish, dist tarball on tags

Development

pnpm test # run the full test suite
pnpm check # typecheck
pnpm coverage-audit # tests + enforce 100% line/function coverage
pnpm generate-tags # regenerate tag DB (requires exiftool source)

# Parity vs real exiftool (needs `exiftool` on PATH):
npx vitest run src/cli/exiftool-parity.test.ts

Project scope

Near term: deepen EXIF/XMP/IPTC writing, add maker-note decoding, extend the container list toward the formats exiftool covers. The long-term target remains parity with ExifTool's reading surface across its supported formats — tracked in the parity docs.

License

MIT