package format import ( "encoding/json" "errors" "fmt" "math" "strconv" "strings" "time" ) type exifRecord struct { ImageWidth int ImageHeight int Orientation int DateTimeOriginal string CreateDate string Make string Model string LensModel string FNumber float64 ExposureTime float64 ISO int } // parseExif reads `exiftool -json -n` output for a single file. func parseExif(data []byte) (Meta, error) { var recs []exifRecord if err := json.Unmarshal(data, &recs); err != nil { return Meta{}, fmt.Errorf("exiftool output: %w", err) } if len(recs) != 1 { return Meta{}, fmt.Errorf("exiftool output: %d records, want 1", len(recs)) } e := recs[0] if e.ImageWidth == 0 || e.ImageHeight == 0 { return Meta{}, errors.New("exiftool output: no image dimensions") } m := Meta{Width: e.ImageWidth, Height: e.ImageHeight, Lens: e.LensModel, ISO: e.ISO} if e.Orientation >= 5 && e.Orientation <= 8 { m.Width, m.Height = m.Height, m.Width } m.Taken = exifTime(e.DateTimeOriginal) if m.Taken.IsZero() { m.Taken = exifTime(e.CreateDate) } m.Camera = camera(e.Make, e.Model) if e.FNumber > 0 { // Two significant figures, as f-numbers are written (0.95, 1.8, 5.6, 22); // exiftool -n reports the raw value, e.g. 1.779999971. m.Aperture = "f/" + strconv.FormatFloat(e.FNumber, 'g', 2, 64) } m.Shutter = shutter(e.ExposureTime) return m, nil } // exifTime parses "YYYY:MM:DD HH:MM:SS", ignoring any trailing subseconds or zone. func exifTime(s string) time.Time { if len(s) < 19 { return time.Time{} } t, err := time.Parse("2006:01:02 15:04:05", s[:19]) if err != nil { return time.Time{} } return t } func camera(maker, model string) string { maker, model = strings.TrimSpace(maker), strings.TrimSpace(model) if maker == "" { return model } if model == "" { return maker } brand := strings.ToLower(strings.Fields(maker)[0]) if strings.Contains(strings.ToLower(model), brand) { return model } return maker + " " + model } func shutter(sec float64) string { switch { case sec <= 0: return "" case sec >= 1: return strconv.FormatFloat(sec, 'f', -1, 64) + "s" default: return "1/" + strconv.Itoa(int(math.Round(1/sec))) } }