summaryrefslogtreecommitdiffstats
path: root/resources/images/exif/exif.go
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-08-29 10:18:51 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-08-31 19:04:56 +0200
commit28143397d625cce1f89f4161cba97c0dddd9004c (patch)
treea0d04fd2c6d04b82bcc1060b97c8f803378da779 /resources/images/exif/exif.go
parent8a8d4a6d97d181f1aaee639d35b198a27bb788e2 (diff)
Add image.Exif
Note that we will probably need to add some metadata cache for this to scale. Fixes #4600
Diffstat (limited to 'resources/images/exif/exif.go')
-rw-r--r--resources/images/exif/exif.go242
1 files changed, 242 insertions, 0 deletions
diff --git a/resources/images/exif/exif.go b/resources/images/exif/exif.go
new file mode 100644
index 000000000..7a3c982c1
--- /dev/null
+++ b/resources/images/exif/exif.go
@@ -0,0 +1,242 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package exif
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "math/big"
+ "regexp"
+ "strings"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ _exif "github.com/rwcarlsen/goexif/exif"
+ "github.com/rwcarlsen/goexif/tiff"
+)
+
+const exifTimeLayout = "2006:01:02 15:04:05"
+
+type Exif struct {
+ Lat float64
+ Long float64
+ Date time.Time
+ Values map[string]interface{}
+}
+
+type Decoder struct {
+ includeFieldsRe *regexp.Regexp
+ excludeFieldsrRe *regexp.Regexp
+ noDate bool
+ noLatLong bool
+}
+
+func IncludeFields(expression string) func(*Decoder) error {
+ return func(d *Decoder) error {
+ re, err := compileRegexp(expression)
+ if err != nil {
+ return err
+ }
+ d.includeFieldsRe = re
+ return nil
+ }
+}
+
+func ExcludeFields(expression string) func(*Decoder) error {
+ return func(d *Decoder) error {
+ re, err := compileRegexp(expression)
+ if err != nil {
+ return err
+ }
+ d.excludeFieldsrRe = re
+ return nil
+ }
+}
+
+func WithLatLongDisabled(disabled bool) func(*Decoder) error {
+ return func(d *Decoder) error {
+ d.noLatLong = disabled
+ return nil
+ }
+}
+
+func WithDateDisabled(disabled bool) func(*Decoder) error {
+ return func(d *Decoder) error {
+ d.noDate = disabled
+ return nil
+ }
+}
+
+func compileRegexp(expression string) (*regexp.Regexp, error) {
+ expression = strings.TrimSpace(expression)
+ if expression == "" {
+ return nil, nil
+ }
+ if !strings.HasPrefix(expression, "(") {
+ // Make it case insensitive
+ expression = "(?i)" + expression
+ }
+
+ return regexp.Compile(expression)
+
+}
+
+func NewDecoder(options ...func(*Decoder) error) (*Decoder, error) {
+ d := &Decoder{}
+ for _, opt := range options {
+ if err := opt(d); err != nil {
+ return nil, err
+ }
+ }
+
+ return d, nil
+}
+
+func (d *Decoder) Decode(r io.Reader) (ex *Exif, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("Exif failed: %v", r)
+ }
+ }()
+
+ var x *_exif.Exif
+ x, err = _exif.Decode(r)
+ if err != nil {
+ if err.Error() == "EOF" {
+
+ // Found no Exif
+ return nil, nil
+ }
+ return
+ }
+
+ var tm time.Time
+ var lat, long float64
+
+ if !d.noDate {
+ tm, _ = x.DateTime()
+ }
+
+ if !d.noLatLong {
+ lat, long, _ = x.LatLong()
+ }
+
+ walker := &exifWalker{x: x, vals: make(map[string]interface{}), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe}
+ if err = x.Walk(walker); err != nil {
+ return
+ }
+
+ ex = &Exif{Lat: lat, Long: long, Date: tm, Values: walker.vals}
+
+ return
+}
+
+func decodeTag(x *_exif.Exif, f _exif.FieldName, t *tiff.Tag) (interface{}, error) {
+ switch t.Format() {
+ case tiff.StringVal, tiff.UndefVal:
+ s := nullString(t.Val)
+ if strings.Contains(string(f), "DateTime") {
+ if d, err := tryParseDate(x, s); err == nil {
+ return d, nil
+ }
+ }
+ return s, nil
+ case tiff.OtherVal:
+ return "unknown", nil
+ }
+
+ var rv []interface{}
+
+ for i := 0; i < int(t.Count); i++ {
+ switch t.Format() {
+ case tiff.RatVal:
+ n, d, _ := t.Rat2(i)
+ rat := big.NewRat(n, d)
+ if n == 1 {
+ rv = append(rv, rat)
+ } else {
+ f, _ := rat.Float64()
+ rv = append(rv, f)
+ }
+
+ case tiff.FloatVal:
+ v, _ := t.Float(i)
+ rv = append(rv, v)
+ case tiff.IntVal:
+ v, _ := t.Int(i)
+ rv = append(rv, v)
+ }
+ }
+
+ if t.Count == 1 {
+ if len(rv) == 1 {
+ return rv[0], nil
+ }
+ }
+
+ return rv, nil
+
+}
+
+// Code borrowed from exif.DateTime and adjusted.
+func tryParseDate(x *_exif.Exif, s string) (time.Time, error) {
+ dateStr := strings.TrimRight(s, "\x00")
+ // TODO(bep): look for timezone offset, GPS time, etc.
+ timeZone := time.Local
+ if tz, _ := x.TimeZone(); tz != nil {
+ timeZone = tz
+ }
+ return time.ParseInLocation(exifTimeLayout, dateStr, timeZone)
+
+}
+
+type exifWalker struct {
+ x *_exif.Exif
+ vals map[string]interface{}
+ includeMatcher *regexp.Regexp
+ excludeMatcher *regexp.Regexp
+}
+
+func (e *exifWalker) Walk(f _exif.FieldName, tag *tiff.Tag) error {
+ name := string(f)
+ if e.excludeMatcher != nil && e.excludeMatcher.MatchString(name) {
+ return nil
+ }
+ if e.includeMatcher != nil && !e.includeMatcher.MatchString(name) {
+ return nil
+ }
+ val, err := decodeTag(e.x, f, tag)
+ if err != nil {
+ return err
+ }
+ e.vals[name] = val
+ return nil
+}
+
+func nullString(in []byte) string {
+ var rv bytes.Buffer
+ for _, b := range in {
+ if unicode.IsPrint(rune(b)) {
+ rv.WriteByte(b)
+ }
+ }
+ rvs := rv.String()
+ if utf8.ValidString(rvs) {
+ return rvs
+ }
+
+ return ""
+}