schema: use GPS location to find timezone from EXIF when UTC offset is unknown

Change-Id: Ia3590424db36508b491d8f19829738fe102e5c3d
This commit is contained in:
Brad Fitzpatrick 2014-07-13 10:26:01 -07:00
parent 286b53f119
commit 077763fcbb
4 changed files with 99 additions and 0 deletions

View File

@ -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
}

View File

@ -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)
}
}
}

BIN
pkg/schema/testdata/coffee-sf.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
pkg/schema/testdata/gocon-tokyo.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB