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}