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.
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 end —
read()returns an inferredFileInfo; 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
exiftoolbinary 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.
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, …).
npm (CLI + library, Node ≥ 18):
npm i -g @woss/exiftool
exiftool-ts photo.jpgThe 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/exiftoolThe 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.
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.
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.
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 daemonmod.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
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.tsNear term: deepen XMP/IPTC writing, extend the container list toward the formats exiftool covers. EXIF (IFD0/ExifIFD/GPS) writing and MakerNote decoding for the core 6 vendors are shipped. The long-term target remains parity with ExifTool's reading surface across its supported formats — tracked in the parity docs.
MIT — © woss (woss.io), covering this repository's TypeScript code only.
ExifTool was created by Phil Harvey, who maintains it single-handedly since 2003. This project exists because of his work and would not be possible without it. Phil's ExifTool is licensed under the same terms as Perl (Artistic License or GPL); nothing from his codebase is used here — this is an independent clean-room TypeScript implementation that uses his tool only as a behavioral reference for output parity. All credit for the ExifTool concept, design, and decades of metadata expertise belongs to him. Consider supporting his work.
Made with Human and AI effort — woss.io