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

photo-date-fix

v0.1.0

Published

Work out when archive photos were really taken (unset camera clocks, cameras hours off at one event, burned-in datestamps, scans posing as captures) and write a date-evidence manifest. Never edits originals.

Readme

photo-date-fix

Work out when archive photos were really taken, and say how sure you are. For a folder of pictures (or a manifest from near-dupe-pick, or from photo-face-find, which is not published yet) it writes a date-evidence manifest: the best date for each picture, where that date came from, a confidence, and the reasons in plain language. It catches the failures that wreck a date-ordered slideshow or timeline: camera clocks that were never set, two cameras at one wedding wrong in different ways, a datestamp burned into the frame that contradicts the file dates, and scans whose only dates are when they were digitised. Originals are never modified.

macOS only. Node 20+, with a small Swift helper (Apple Vision and ImageIO) compiled on first run.

Install

npm install -g photo-date-fix    # one runtime dependency: exifr

The Swift helper needs the Xcode command-line tools (swiftc). It compiles into the work dir on first use and is cached by its source hash. Without it the tool still runs, but it cannot read datestamps or fall back to ImageIO, and it says so on stderr.

Hardened: 2026-10-30 · b860c45 · gate CLOSED WITH FOLLOW-UPS

Usage

# Dry run: weigh the evidence, report, write nothing to --out
photo-date-fix infer ~/Pictures/wedding --event ~/Pictures/wedding

# Write dates.jsonl and offsets.json
photo-date-fix infer ~/Pictures/wedding --event ~/Pictures/wedding \
  --out wedding-dates --go

# Review wedding-dates/offsets.json: approve, reject or edit each proposed
# correction. Then recompute with your decisions:
photo-date-fix apply --out wedding-dates          # dry run
photo-date-fix apply --out wedding-dates --go     # rewrites dates.jsonl

--event <dir> makes every picture under that folder one event, which is how the guest camera in wedding/guest/ gets compared with the pro cameras in wedding/hi-res/. Without it, each folder is its own event. Near-duplicate pairs from a near-dupe-pick manifest anchor cameras to each other within an event; they don’t join folders into one.

Run it from outside the photos: infer will not make its default --out or --work inside an input folder (exit 2). A folder you name with --out or --work is your call. apply reuses the work dir recorded in offsets.json.

Other flags: --reference "<camera>" names the camera whose clock is right; --ocr auto|all|off controls datestamp reading only (auto, the default, reads stamps only where there is no trustworthy camera date; the ImageIO fallback for EXIF runs whatever the mode); --min-bytes sets the size floor for folder files (4096 by default); --work <dir> sets where the fact cache and helper live. photo-date-fix --help is the full reference.

What it weighs

Strongest first. Each source is its own module under lib/sources/.

| Evidence | src | Confidence | |---|---|---| | A datestamp printed in the frame (“9 14 ’03”, “’03 9 14”, “2003.09.14”), read by Apple Vision on the four corners | datestamp-ocr | from the OCR’s own confidence, raised when several passes agree | | EXIF DateTimeOriginal (CreateDate, IPTC DateCreated after it), read by exifr from a Buffer, ImageIO when exifr finds nothing | exif-original, exif-digitized, iptc-created | high for a camera original | | The same, corrected for a camera whose clock was wrong at its event, once a person approves the correction (until then it rides in proposed) | event-date-swap, event-clock-offset | estimated; approval raises the proposal’s confidence one step (medium → high, low → medium) | | The earliest capture date in a near-duplicate group (a resave, a crop, a rescan) | resave-chain | one step below the donor’s | | A folder or caption hint (“from J. Smith ’83”, “June 6 1989”, “Summer camp! 1971”) | hint-folder, hint-caption, hint-filename | low, estimated | | A file date (TIFF ModifyDate, the earliest mtime across copies) | file-date | low, estimated: an upper bound |

Some dates count as no date at all:

  • Unset clocks. 0000:00:00 00:00:00, a blank camera date (spaces or NULs), a 2000-01-01 reading, and a 1999-12-31 file date are factory defaults, not dates. Such a picture is undated (a skip, not a failure) and is never dated from its file date. A 1999-12-31 camera reading is a real one, and a camera that shot on 1999-12-31 had its clock set, so its 2000-01-01 frames keep their dates (a millennium party).

  • Scans. A scanner make (EPSON Perfection, Nikon LS-5000 and the like), scanning software, scanner-grade resolution with no camera tags, or a scans/slides folder marks a scan. Its dates record digitisation; the evidence says so and nothing sorts by them.

Cameras at one event

At each event the tool groups frames by camera (make, model and serial) and picks a reference clock: the camera whose median reading is latest, because an unset or reset clock falls back to a past date. Then, for each other camera:

  • Near-duplicate pairs across cameras (one moment, two cameras), from a near-dupe-pick manifest, set the offset to the minute.

  • A date-only error is a camera whose first session starts at the same time of day as a session on a trusted camera, on another date. Say a pro camera at a wedding reads 2006-02-11 18:37, and the guest camera’s evening session begins at 2015-08-22 18:39 (an invented example). The date shifts by whole days and the time of day is kept. It is proposed only when a near-duplicate pair backs it, or when all three of these hold: a session start on a camera placed independently of it (the reference, a camera consistent by overlap, or one placed by near-duplicate pairs; never one placed by consensus or by another swap) falls within 5 minutes of its corrected first frame; the swap is more than two days (a day or two could be the neighbouring day’s event at the same hour, such as a rehearsal dinner shot the evening before); and it is the only camera whose readings sit more than two days from the reference’s (more than one, and the folder probably holds several events). Otherwise it is a low-confidence hint, not a correction. The trade-off: a wedding with two cameras whose dates were never set gives two hints, not two proposals; approve them by hand in offsets.json, or add near-duplicate pairs.

  • A clock offset comes from session-start consensus: the camera’s first frame lines up with session starts on two or more trusted cameras that agree. A second body about 5 h 47 m slow would line up with both the corrected pro camera and the guest camera. That offset is then checked against the others that put any of its session starts on a trusted camera’s session start: they are ranked by how many of its session starts fall inside a trusted camera’s session, then by how many cameras back them. A session no other camera covered (an after-party only it shot) counts for none of them, so it neither supports nor demotes an offset. If another offset aligns more of its sessions than the consensus (it also shot an earlier session that only one camera covered, say), that offset is given, as a hint, not a correction. If another offset, more than 5 minutes away, aligns as many of its sessions, whatever the cameras behind each, the first frame has not settled it (a camera that missed the ceremony matches the ceremony’s start and the reception’s alike, and how many trusted cameras shot each says nothing about this one): the better-backed offset, the consensus when they are even, is given as a hint that names the others.

  • Not corrections. A clock offset of more than two days, a camera whose frames overlap no other camera’s in a folder that was not named with --event, and a camera that only its first frame could place are listed as hints: “probably a separate event or a wrong clock; set --event/--reference”, with the numbers. A hinted camera’s pictures keep their camera reading, at low confidence, with the hint in their evidence; they stay ok, since the hint itself is the warning.

A camera whose frames already overlap a trusted camera’s is left as it is: without pairs, the tool cannot see an error that small. When the reference is the latest-readings guess rather than --reference, its own frames say so in their evidence and are at most medium confidence. A --reference that names no camera is refused, by name.

The manifest

dates.jsonl has one row per picture, in the shape the photo tools share: id (the first 16 hex of the sha256; byte-identical copies share a row and list every path), kind, paths, and the date block this tool owns. Until a person approves a correction, the camera reading stays in iso and the correction rides in proposed:

{"iso":"2006-02-11T18:37:15","label":"2006-02-11","src":"exif-original","confidence":"low",
 "estimated":false,"cameraDate":"2006-02-11T18:37:15","camera":"Canon EOS 20D",
 "event":"/Users/me/Pictures/wedding","origin":"capture",
 "evidence":["camera Canon EOS 20D reads 2006-02-11 18:37:15; date never set (reads 2006-02-11), time of day right: …",
             "proposed: offset +3479d 00:00:00 would make it 2015-08-22 18:37:15; not yet approved in offsets.json, so the camera reading stands"],
 "proposed":{"iso":"2015-08-22T18:37:15","offset":"+3479d 00:00:00","kind":"date-only","confidence":"medium",
             "event":"/Users/me/Pictures/wedding","camera":"Canon EOS 20D"}}

cameraDate is the camera’s own reading in the same ISO form (also for a reading set aside as implausible, so it may not be a calendar date: 2010-02-30T12:00:00, 0000-00-00T00:00:00), or null when there is none, it is blank, or it is not a date at all. Once approved, apply moves it into iso with src event-date-swap (or event-clock-offset) and "estimated": true.

An undated picture has "iso": null and "label": "undated". A year-only hint sorts at July 1 and a month-only hint at mid-month, so neither leads its era. Rows from an input manifest keep every other block (face, group, anything else) untouched; video frame and range rows, and any row without an id, pass through as they are. A file that a manifest row and a folder both hold is one row, the manifest’s. A file with no extension is read only if its bytes say it is an image.

offsets.json

Every proposed correction ("status": "proposed") and every hint ("hint") is written here for a person to judge. Set a camera’s "status" to "approved" or "rejected". Edit its "offset" (+05:47:00, +3479d 00:00:00, added to the camera clock) to correct the estimate. A camera marked consistent, or a hint, can also be given an offset and approved. apply re-reads the file, along with the inputs, their real paths, the options and the work dir recorded in it, and recomputes. It refuses, by name, a decision that matches no camera and a status it does not know ("Approved" is not "approved"). If the inputs are no longer where infer found them, it refuses the run and says how to point the file at the new location. infer will not overwrite a file a person has edited, one it cannot parse, or one that is not its own, nor the manifest apply wrote from it.

Output and exit codes

Built on mjs-tool-skeleton (contract mjs-tool/3). Every run renders one item list four ways: human (the default), --brief (one line per item), --json, and --quiet. Each text mode ends with a next: line. Both commands are dry runs unless --go; a dry run writes only to the work dir (the fact cache that makes long runs resumable, the compiled helper, and progress.json, which says the phase a long run is in and how far along it is).

Exit 0 means every item is ok or skipped. An undated picture is a skip: a folder whose clocks were never set exits 0. 1 means some item warned (a datestamp contradicting the camera, a correction or hint awaiting a person, a picture whose correction is pending), refused, or failed. 2 is a usage error, a missing input included. 3 means the run could not complete. Inputs with no pictures in them (an empty or mistyped folder, a --min-bytes set too high) say nothing to do as a warn, so they exit 1; so does a file whose bytes are not a readable image, and a missing Swift helper when a picture needed it. A camera date in the future, before 1826, or not on the calendar (30 February) is not a capture date: the evidence says why, and the picture falls back as if it had none. An --event folder that does not exist, or holds no picture from the inputs, is a usage error. A dry run’s next counts the files --go would write, as the applied run counts what it wrote. Agents: use --brief or --json, treat every non-ok line as a finding, and never edit offsets.json yourself: approving a correction is a person’s call.

Tests

npm test

There are 74 tests. sources.test.mjs and cli.test.mjs hold one per behaviour: each evidence source, the unset-clock forms, date-only versus clock-offset detection, the offsets.json round trip, datestamp OCR on a synthetic seven-segment stamp, the manifest schema with pass-through of unknown blocks, the ImageIO fallback, and the CLI contract. runtime-fence.test.mjs is the mjs-tool-skeleton runtime suite, ported. Fixtures are synthetic; test/fixtures/SOURCES.md records how each one was made.

Outside comparison

As of 2026-10-08 (not rechecked since). Checked 2026-10-07: digiKam’s time adjust, Photo Mechanic’s capture-time adjustment, and epdoc/exif. All three apply an offset you already know.

Rechecked 2026-10-08 with one search:

  • AGG (ALVA Generic Gallery) computes a camera’s offset from a pair of same-moment pictures that you pick by hand. That is this tool’s near-duplicate anchor, without the finding.

  • flashvideosynchronization and Syncalize synchronise video, from camera flashes and from embedded timecodes respectively.

None of them finds its own anchors in a still-photo archive, tells a date-only error from a clock offset, recognises an unset clock or a scan, reads a burned-in datestamp, or keeps per-picture evidence and confidence. That gap is this tool.

Known limits

  • A clock offset rests on session starts, not on every frame. The tool counts how many of a camera’s session starts fall inside other cameras’ sessions, and holds the offset back as a hint when another offset aligns more of them or ties it, but it does not score offsets frame by frame. Two cameras with wrong clocks that happen to line up with each other can still pass; near-duplicate pairs from near-dupe-pick settle an offset to the minute.

  • An offset that aligns every one of a camera’s sessions can still be a whole-session shift. When the gaps between its sessions match the gaps between the trusted cameras’ (it shot getting ready alone at 13:00 and the ceremony at 16:00, and the trusted cameras shot the ceremony at 16:00 and the reception at 19:00), the offset three hours later aligns both its sessions and the right offset only one, so the shifted offset is given at medium confidence. No session-start rule tells the two apart; near-duplicate pairs from near-dupe-pick settle it.

  • Datestamp OCR on real frames is partial: on a real roll, about half the frames read, and the rest fall back to their file dates. Frames of one roll can carry different stamps, so there is no folder vote.

  • A --reference that names no camera is refused, but with --go both outputs are still written under the latest-readings reference.

  • A manifest input run from the folder its pictures sit in can make the default work dir among the photos: a manifest’s picture folders are known only once it is read. Pass --work.

  • A fact cache made by an earlier version keeps what that version read: delete the work dir after upgrading.

  • infer re-hashes every file on a first run (the contract’s id is a full sha256); the fact cache makes reruns cheap.

  • Video frames are passed through undated.

  • Tested on Apple Silicon with Node 26 only, not yet on Node 20 or an Intel Mac.

License

MIT © Danniel T. Gaidula