diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 295a3cda4..9123a9357 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -30,6 +30,7 @@ import ( "fmt" "hash" "io" + "log" "os" "reflect" "strconv" @@ -40,7 +41,9 @@ import ( "camlistore.org/pkg/blob" "camlistore.org/pkg/types" + "camlistore.org/third_party/github.com/bradfitz/latlong" "camlistore.org/third_party/github.com/camlistore/goexif/exif" + "camlistore.org/third_party/github.com/camlistore/goexif/tiff" ) // MaxSchemaBlobSize represents the upper bound for how large @@ -905,5 +908,69 @@ func FileTime(f io.ReaderAt) (time.Time, error) { if err != nil { return defaultTime() } + // If the EXIF file only had local timezone, but it did have + // GPS, then lookup the timezone and correct the time. + if ct.Location() == time.Local { + if lat, long, ok := ex.LatLong(); ok { + if loc := lookupLocation(latlong.LookupZoneName(lat, long)); loc != nil { + if t, err := exifDateTimeInLocation(ex, loc); err == nil { + return t, nil + } + } + } + } return ct, nil } + +// This is basically a copy of the exif.Exif.DateTime() method, except: +// * it takes a *time.Location to assume +// * the caller already assumes there's no timezone offset or GPS time +// in the EXIF, so any of that code can be ignored. +func exifDateTimeInLocation(x *exif.Exif, loc *time.Location) (time.Time, error) { + tag, err := x.Get(exif.DateTimeOriginal) + if err != nil { + tag, err = x.Get(exif.DateTime) + if err != nil { + return time.Time{}, err + } + } + if tag.Format() != tiff.StringVal { + return time.Time{}, errors.New("DateTime[Original] not in string format") + } + const exifTimeLayout = "2006:01:02 15:04:05" + dateStr := strings.TrimRight(string(tag.Val), "\x00") + return time.ParseInLocation(exifTimeLayout, dateStr, loc) +} + +var zoneCache struct { + sync.RWMutex + m map[string]*time.Location +} + +func lookupLocation(zone string) *time.Location { + if zone == "" { + return nil + } + zoneCache.RLock() + l, ok := zoneCache.m[zone] + zoneCache.RUnlock() + if ok { + return l + } + // could use singleflight here, but doesn't really + // matter if two callers both do this. + loc, err := time.LoadLocation(zone) + + zoneCache.Lock() + if zoneCache.m == nil { + zoneCache.m = make(map[string]*time.Location) + } + zoneCache.m[zone] = loc // even if nil + zoneCache.Unlock() + + if err != nil { + log.Printf("failed to lookup timezone %q: %v", zone, err) + return nil + } + return loc +} diff --git a/pkg/schema/schema_test.go b/pkg/schema/schema_test.go index dffd6988d..9436db9e6 100644 --- a/pkg/schema/schema_test.go +++ b/pkg/schema/schema_test.go @@ -18,6 +18,7 @@ package schema import ( "encoding/json" + "io" "io/ioutil" "os" "path/filepath" @@ -609,3 +610,34 @@ func TestStaticSocket(t *testing.T) { t.Fatalf("StaticFile.AsStaticSocket(): Expected true, got false") } } + +func TestTimezoneEXIFCorrection(t *testing.T) { + // Test that we get UTC times for photos taken in two + // different timezones. + // Both only have local time + GPS in the exif. + tests := []struct { + file, want, wantUTC string + }{ + {"coffee-sf.jpg", "2014-07-11 08:44:34 -0700 PDT", "2014-07-11 15:44:34 +0000 UTC"}, + {"gocon-tokyo.jpg", "2014-05-31 13:34:04 +0900 JST", "2014-05-31 04:34:04 +0000 UTC"}, + } + for _, tt := range tests { + f, err := os.Open("testdata/" + tt.file) + if err != nil { + t.Fatal(err) + } + // Hide *os.File type from FileTime, so it can't use modtime: + tm, err := FileTime(struct{ io.ReaderAt }{f}) + f.Close() + if err != nil { + t.Errorf("%s: %v", tt.file, err) + continue + } + if got := tm.String(); got != tt.want { + t.Errorf("%s: time = %q; want %q", tt.file, got, tt.want) + } + if got := tm.UTC().String(); got != tt.wantUTC { + t.Errorf("%s: utc time = %q; want %q", tt.file, got, tt.wantUTC) + } + } +} diff --git a/pkg/schema/testdata/coffee-sf.jpg b/pkg/schema/testdata/coffee-sf.jpg new file mode 100644 index 000000000..10c800fb1 Binary files /dev/null and b/pkg/schema/testdata/coffee-sf.jpg differ diff --git a/pkg/schema/testdata/gocon-tokyo.jpg b/pkg/schema/testdata/gocon-tokyo.jpg new file mode 100644 index 000000000..34f58fa28 Binary files /dev/null and b/pkg/schema/testdata/gocon-tokyo.jpg differ