Diff old/new metadata into the minimal exiftool CLI args needed to apply
the change. Compute-only: this library doesn't spawn exiftool itself, and
never invokes it when nothing changed.
- Writes only the fields that changed, not the whole record every save.
- Diffs list fields as sets (
additive-list), so keywords added by other tools between reads aren't clobbered. - Zero runtime dependencies: a thin compute-only layer to drop next to
whatever runs
exiftoolfor you (exiftool-vendored, a rawchild_processcall, etc.).
Not a fit for batch/-stay_open workflows (out of scope for now), or if
you're not diffing against a prior metadata state at all.
npm install exiftool-arg-diffDeclare a strategy per field, then diff old vs new metadata:
import { diffMetadataArgs, type MetadataSchema } from "exiftool-arg-diff";
const schema: MetadataSchema = {
Description: "overwrite",
Keywords: "additive-list",
Rating: "overwrite",
};
const oldMetadata = { Description: "Sunset", Keywords: ["beach", "sunset"] };
const newMetadata = {
Description: "Sunset over the bay",
Keywords: ["beach", "dusk"],
Rating: 5, // absent from oldMetadata: diffed as newly set, not skipped
};
diffMetadataArgs(schema, oldMetadata, newMetadata);
// => ["-Description=Sunset over the bay", "-Keywords+=dusk", "-Keywords-=sunset", "-Rating=5"]If nothing changed, diffMetadataArgs returns null instead of [], so
callers can skip running exiftool entirely.
Diffs oldMetadata against newMetadata per schema and returns the
exiftool CLI args needed to apply the change, or null if nothing changed.
Fields not present in schema are ignored. A field missing from
oldMetadata or newMetadata is treated as undefined (tag absent), not
skipped, so e.g. a field present only in newMetadata is diffed as newly
added.
Record<string, FieldStrategy>, keyed by exiftool tag name.
How a field's changes are translated into args:
"overwrite": scalar field, replaced wholesale (-field=value). A field removed innewMetadataclears it (-field=)."additive-list": list field with incremental add/remove support (-field+=x,-field-=y), diffed as a multiset (order-independent). Use this for fields likeKeywordson formats that support incremental list edits (e.g. JPEG/IPTC)."list-overwrite": list field without incremental support, replaced wholesale (-field=a,b,c) whenever its multiset of values changes. Use this for containers that don't support incremental list edits (e.g. video).
Don't use "overwrite" on a list field: it compares with === (reference
equality), so it fires on every diff regardless of order or actual change.
Use "additive-list" or "list-overwrite" for array-valued fields.
Record<string, MetadataValue | MetadataValue[] | undefined>. Scalar
fields hold a single value, list fields hold an array, and a
missing/undefined value means the tag is absent.
string | number, a scalar exiftool tag value.
There's no shipped default schema: which strategy fits a field depends on your workflow. Here's a starting point for photo metadata to copy and adjust:
const photoSchema: MetadataSchema = {
Title: "overwrite",
Description: "overwrite",
Rating: "overwrite",
GPSLatitude: "overwrite",
GPSLongitude: "overwrite",
Keywords: "additive-list", // JPEG/IPTC keywords support incremental edits
Subject: "additive-list",
};This library only computes args; it's a natural companion to
exiftool-vendored,
which runs exiftool for you. Pass the computed args as write()'s
writeArgs option, and skip the call entirely when there's nothing to do:
import { exiftool } from "exiftool-vendored";
import {
diffMetadataArgs,
type Metadata,
type MetadataSchema,
} from "exiftool-arg-diff";
const schema: MetadataSchema = { Description: "overwrite" };
async function applyMetadataChange(
file: string,
oldMetadata: Metadata,
newMetadata: Metadata,
): Promise<void> {
const args = diffMetadataArgs(schema, oldMetadata, newMetadata);
if (args !== null) {
await exiftool.write(file, {}, { writeArgs: args });
}
}