Algorithms and design decisions
One parser for every format
JPEG (APP1 "Exif" segment), TIFF-based RAW (CR2, NEF, ARW, DNG), Canon CR3 (CMT1/CMT2/CMT4 boxes) and HEIC (via Pillow) all wrap the same TIFF structure. exif.parse_tiff reads the header (byte order, magic), IFD0, then follows the Exif (0x8769) and GPS (0x8825) pointers. Values larger than four bytes are read from offsets; rationals become floats (a zero denominator gives 0.0); truncated or malformed entries are skipped instead of failing the file. Format detection uses file content, not extension.
35mm equivalent
Order of trust: the EXIF FocalLengthIn35mmFilm tag; a crop factor from the camera table; a crop factor from the focal-plane resolution tags (36 / sensor width mm); otherwise full frame is assumed. Example: Fujifilm X-E4 at 35mm without the tag: table gives 1.5, so 52.5mm equivalent.
Prime recommendation
Per camera, over shots taken with zoom lenses (name contains a range like 24-70mm, or the lens was used at 3+ distinct focal lengths):
- Sort the equivalent focal lengths.
- Slide a window of ratio 1.25 (hi/lo) and keep the one holding the most shots.
- If it holds at least 40% of zoom shots (and there are 20+), recommend the standard prime nearest, in log distance, to the cluster median.
- If you already own a prime within 15% of that focal length, say so instead of recommending a purchase.
Worked example: 36 shots, 24x10, 26x10, 28x8, 50x4, 70x4. Window from 24 reaches 30mm and holds 28 shots, 78%. The median is 26; the log distances are 0.080 to 24 and 0.074 to 28, so: "78% ... at 24-28mm, consider a 28mm prime". On an APS-C body (crop 1.5) the native focal length is 28/1.5, about 19mm.
Reciprocal rule
Over-exposure in stops: log2(shutter * focal35). At 50mm, 1/50 s gives 0; 1/15 s gives 1.74. Stabilised shots (stabilised lens name or IBIS body) subtract --stab-stops (default 3). A shot is flagged when the margin exceeds 1/3 stop, which absorbs shutter-speed rounding. Both the plain-rule count and the stabilisation-adjusted count are reported. Flash shots and exposures of 1 s or longer are excluded.
ISO comfort limits
Rule-of-thumb maximum clean ISO by sensor class (derived from the crop factor): medium format 12800, full frame 6400, APS-C 3200, Micro Four Thirds 1600, 1-inch/compact 800.
Sun position and golden hour
EXIF times are local. UTC is local time minus the EXIF offset (OffsetTimeOriginal), or minus round(lon / 15) hours when absent. Elevation comes from the NOAA algorithm (equation of time, declination, hour angle). Phases: day above 6 degrees, golden -4 to 6, blue -8 to -4, night below. Check: Berlin on 2024-06-21 at 11:08 UTC gives about 60.9 degrees, matching 90 - 52.5 + 23.4. Photos without GPS use --fallback-location or are left out of the light shares.
Gear usage and trips
Per lens, monthly shot counts feed the timeline; a lens is dormant when its last use is at least --unused-days before the newest photo in the library. Trips: sort by capture time, start a new cluster whenever the gap to the previous shot exceeds --gap-hours (default 36), and show clusters with 10+ shots.
Cache
SQLite table files(path, mtime_ns, size, side_ns, data). A hit requires all three of mtime, size and sidecar mtime to match, so editing an .xmp rating invalidates only that file. Derived values (crop, equivalents, stabilisation) are recomputed on load, so table changes never serve stale results. Workers only extract; the main process writes the cache.