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

exiftool-arg-diff

v1.0.0

Published

Diff old/new metadata into the minimal exiftool CLI args needed to apply the change.

Downloads

186

Readme

exiftool-arg-diff

npm version

Your app reads a file's metadata, the user edits two fields, and you write the result. Write the whole record and you push eleven unchanged tags back to disk, along with whatever another tool changed while you were holding the data.

diffMetadataArgs takes what you read and what you now want, and returns the exiftool args that apply the difference. When nothing changed it returns null, so you skip the subprocess.

When this helps

You hold metadata in memory between reading a file and writing it: an editor, a tagging UI, a sync job, anything with a model the user edits. exiftool has no memory of what you read ten minutes ago, so it cannot separate your edit from one another tool made in the meantime. Your app knows both, and this turns that into the smallest correct write.

  • List fields diff as multisets, so Keywords emits -Keywords+=added and -Keywords-=removed instead of replacing the list. Keywords a different tool added survive.
  • Unchanged fields produce no args, and an unchanged record produces no call.
  • Zero runtime dependencies. It computes strings, and you keep whatever already runs exiftool (exiftool-vendored, a raw child_process call).

The payoff scales with how long you hold the data. A tagging UI left open for an hour has an hour of drift to guard against. A script that reads and writes in the same breath has almost none, and plain exiftool will serve it better.

Install

npm install exiftool-arg-diff

Usage

Declare 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.

API

diffMetadataArgs(schema, oldMetadata, newMetadata)

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.

MetadataSchema

Record<string, FieldStrategy>, keyed by exiftool tag name.

FieldStrategy

How a field's changes are translated into args:

  • "overwrite": scalar field, replaced wholesale (-field=value). A field removed in newMetadata clears 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 like Keywords on 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.

Metadata

Record<string, FieldValue>. Scalar fields hold a single value, list fields hold an array, and a missing/undefined value means the tag is absent.

FieldValue

MetadataValue | MetadataValue[] | undefined, one field's value.

MetadataValue

string | number, a scalar exiftool tag value.

Example schema

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",
};

Composing with photo-metadata-replicate

Diffing needs a new metadata state to compare against. To build one, pair this with photo-metadata-replicate, which merges one item's metadata onto a set of targets in memory: keywords union, and an empty source field never blanks a target.

Its metadata shape matches this package's, so a merged result goes straight into diffMetadataArgs. Replicate to get the new metadata, then diff it against the original for the args to run.

Integrating with exiftool-vendored

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 });
  }
}