internal/format/exif.go

94 lines · 2276 bytes

 1package format
 2
 3import (
 4	"encoding/json"
 5	"errors"
 6	"fmt"
 7	"math"
 8	"strconv"
 9	"strings"
10	"time"
11)
12
13type exifRecord struct {
14	ImageWidth       int
15	ImageHeight      int
16	Orientation      int
17	DateTimeOriginal string
18	CreateDate       string
19	Make             string
20	Model            string
21	LensModel        string
22	FNumber          float64
23	ExposureTime     float64
24	ISO              int
25}
26
27// parseExif reads `exiftool -json -n` output for a single file.
28func parseExif(data []byte) (Meta, error) {
29	var recs []exifRecord
30	if err := json.Unmarshal(data, &recs); err != nil {
31		return Meta{}, fmt.Errorf("exiftool output: %w", err)
32	}
33	if len(recs) != 1 {
34		return Meta{}, fmt.Errorf("exiftool output: %d records, want 1", len(recs))
35	}
36	e := recs[0]
37	if e.ImageWidth == 0 || e.ImageHeight == 0 {
38		return Meta{}, errors.New("exiftool output: no image dimensions")
39	}
40	m := Meta{Width: e.ImageWidth, Height: e.ImageHeight, Lens: e.LensModel, ISO: e.ISO}
41	if e.Orientation >= 5 && e.Orientation <= 8 {
42		m.Width, m.Height = m.Height, m.Width
43	}
44	m.Taken = exifTime(e.DateTimeOriginal)
45	if m.Taken.IsZero() {
46		m.Taken = exifTime(e.CreateDate)
47	}
48	m.Camera = camera(e.Make, e.Model)
49	if e.FNumber > 0 {
50		// Two significant figures, as f-numbers are written (0.95, 1.8, 5.6, 22);
51		// exiftool -n reports the raw value, e.g. 1.779999971.
52		m.Aperture = "f/" + strconv.FormatFloat(e.FNumber, 'g', 2, 64)
53	}
54	m.Shutter = shutter(e.ExposureTime)
55	return m, nil
56}
57
58// exifTime parses "YYYY:MM:DD HH:MM:SS", ignoring any trailing subseconds or zone.
59func exifTime(s string) time.Time {
60	if len(s) < 19 {
61		return time.Time{}
62	}
63	t, err := time.Parse("2006:01:02 15:04:05", s[:19])
64	if err != nil {
65		return time.Time{}
66	}
67	return t
68}
69
70func camera(maker, model string) string {
71	maker, model = strings.TrimSpace(maker), strings.TrimSpace(model)
72	if maker == "" {
73		return model
74	}
75	if model == "" {
76		return maker
77	}
78	brand := strings.ToLower(strings.Fields(maker)[0])
79	if strings.Contains(strings.ToLower(model), brand) {
80		return model
81	}
82	return maker + " " + model
83}
84
85func shutter(sec float64) string {
86	switch {
87	case sec <= 0:
88		return ""
89	case sec >= 1:
90		return strconv.FormatFloat(sec, 'f', -1, 64) + "s"
91	default:
92		return "1/" + strconv.Itoa(int(math.Round(1/sec)))
93	}
94}