Skip to content

Commit

Permalink
Switch EXIF library
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Jul 7, 2024
1 parent fb8909d commit 24d3dc1
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 155 deletions.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/bep/golibsass v1.1.1
github.com/bep/gowebp v0.3.0
github.com/bep/helpers v0.4.0
github.com/bep/imagemeta v0.0.0-20240707104023-e2e8168238fe
github.com/bep/lazycache v0.4.0
github.com/bep/logg v0.4.0
github.com/bep/mclib v1.20400.20402
Expand Down Expand Up @@ -60,7 +61,6 @@ require (
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pelletier/go-toml/v2 v2.2.2
github.com/rogpeppe/go-internal v1.12.0
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sanity-io/litter v1.5.5
github.com/spf13/afero v1.11.0
github.com/spf13/cast v1.6.0
Expand Down Expand Up @@ -162,3 +162,5 @@ require (
)

go 1.20

replace github.com/bep/imagemeta => /Users/bep/dev/go/bep/imagemeta
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,6 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/shogo82148/go-shuffle v0.0.0-20180218125048-27e6095f230d/go.mod h1:2htx6lmL0NGLHlO8ZCf+lQBGBHIbEujyywxJArf+2Yc=
Expand Down
7 changes: 4 additions & 3 deletions resources/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ func (i *imageResource) Exif() *exif.ExifInfo {

func (i *imageResource) getExif() *exif.ExifInfo {
i.metaInit.Do(func() {
supportsExif := i.Format == images.JPEG || i.Format == images.TIFF
if !supportsExif {
mf := i.Format.ToImageMetaImageFormatFormat()
if mf == -1 {
// No Exif support for this format.
return
}

Expand Down Expand Up @@ -114,7 +115,7 @@ func (i *imageResource) getExif() *exif.ExifInfo {
}
defer f.Close()

x, err := i.getSpec().imaging.DecodeExif(f)
x, err := i.getSpec().imaging.DecodeExif(mf, f)
if err != nil {
i.getSpec().Logger.Warnf("Unable to decode Exif metadata from image: %s", i.Key())
return nil
Expand Down
150 changes: 31 additions & 119 deletions resources/images/exif/exif.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,14 @@
package exif

import (
"bytes"
"fmt"
"io"
"math"
"math/big"
"regexp"
"strings"
"time"
"unicode"
"unicode/utf8"

"github.com/bep/imagemeta"
"github.com/bep/tmc"

_exif "github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/tiff"
)

const exifTimeLayout = "2006:01:02 15:04:05"
Expand Down Expand Up @@ -115,143 +108,62 @@ func NewDecoder(options ...func(*Decoder) error) (*Decoder, error) {
return d, nil
}

func (d *Decoder) Decode(r io.Reader) (ex *ExifInfo, err error) {
func (d *Decoder) Decode(format imagemeta.ImageFormat, r io.Reader) (ex *ExifInfo, 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
tagInfos := make(imagemeta.Tags)
handleTag := func(ti imagemeta.TagInfo) error {
tagInfos[ti.Tag] = ti
return nil
}

// var format imagemeta.ImageFormat

err = imagemeta.Decode(
imagemeta.Options{
R: r.(imagemeta.Reader),
ImageFormat: format,
HandleTag: handleTag,
Sources: imagemeta.TagSourceEXIF | imagemeta.TagSourceXMP,
},
)

var tm time.Time
var lat, long float64

if !d.noDate {
tm, _ = x.DateTime()
tm, _ = tagInfos.GetDateTime()
}

if !d.noLatLong {
lat, long, _ = x.LatLong()
if math.IsNaN(lat) {
lat = 0
}
if math.IsNaN(long) {
long = 0
}
}

walker := &exifWalker{x: x, vals: make(map[string]any), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe}
if err = x.Walk(walker); err != nil {
return
lat, long, _ = tagInfos.GetLatLong()
}

ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: walker.vals}

return
}

func decodeTag(x *_exif.Exif, f _exif.FieldName, t *tiff.Tag) (any, 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
}
tags := make(map[string]any)
for k, v := range tagInfos {
if d.excludeFieldsrRe != nil && d.excludeFieldsrRe.MatchString(k) {
continue
}
return s, nil
case tiff.OtherVal:
return "unknown", nil
}

var rv []any

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 t is int or t > 1, use float64
if rat.IsInt() || rat.Cmp(big.NewRat(1, 1)) == 1 {
f, _ := rat.Float64()
rv = append(rv, f)
} else {
rv = append(rv, rat)
}

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
if d.includeFieldsRe != nil && !d.includeFieldsRe.MatchString(k) {
continue
}
tags[k] = v.Value
}

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)
}
ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: tags}

type exifWalker struct {
x *_exif.Exif
vals map[string]any
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
return
}

func nullString(in []byte) string {
var rv bytes.Buffer
for len(in) > 0 {
r, size := utf8.DecodeRune(in)
if unicode.IsGraphic(r) {
rv.WriteRune(r)
}
in = in[size:]
}
return rv.String()
func getDateTime(tags map[string]imagemeta.TagInfo) time.Time {
return time.Time{}
}

// Code borrowed f
var tcodec *tmc.Codec

func init() {
Expand Down
42 changes: 13 additions & 29 deletions resources/images/exif/exif_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"testing"
"time"

"github.com/gohugoio/hugo/htesting/hqt"
"github.com/bep/imagemeta"
"github.com/google/go-cmp/cmp"

qt "github.com/frankban/quicktest"
Expand All @@ -35,11 +35,12 @@ func TestExif(t *testing.T) {

d, err := NewDecoder(IncludeFields("Lens|Date"))
c.Assert(err, qt.IsNil)
x, err := d.Decode(f)
x, err := d.Decode(imagemeta.ImageFormatJPEG, f)
c.Assert(err, qt.IsNil)
c.Assert(x.Date.Format("2006-01-02"), qt.Equals, "2017-10-27")

// Malaga: https://goo.gl/taazZy

c.Assert(x.Lat, qt.Equals, float64(36.59744166666667))
c.Assert(x.Long, qt.Equals, float64(-4.50846))

Expand All @@ -51,7 +52,7 @@ func TestExif(t *testing.T) {

v, found = x.Tags["DateTime"]
c.Assert(found, qt.Equals, true)
c.Assert(v, hqt.IsSameType, time.Time{})
c.Assert(v, qt.Equals, "2017:11:23 09:56:54")

// Verify that it survives a round-trip to JSON and back.
data, err := json.Marshal(x)
Expand All @@ -72,8 +73,8 @@ func TestExifPNG(t *testing.T) {

d, err := NewDecoder()
c.Assert(err, qt.IsNil)
_, err = d.Decode(f)
c.Assert(err, qt.Not(qt.IsNil))
_, err = d.Decode(imagemeta.ImageFormatPNG, f)
c.Assert(err, qt.IsNil)
}

func TestIssue8079(t *testing.T) {
Expand All @@ -85,28 +86,11 @@ func TestIssue8079(t *testing.T) {

d, err := NewDecoder()
c.Assert(err, qt.IsNil)
x, err := d.Decode(f)
x, err := d.Decode(imagemeta.ImageFormatJPEG, f)
c.Assert(err, qt.IsNil)
c.Assert(x.Tags["ImageDescription"], qt.Equals, "Città del Vaticano #nanoblock #vatican #vaticancity")
}

func TestNullString(t *testing.T) {
c := qt.New(t)

for _, test := range []struct {
in string
expect string
}{
{"foo", "foo"},
{"\x20", "\x20"},
{"\xc4\x81", "\xc4\x81"}, // \u0101
{"\u0160", "\u0160"}, // non-breaking space
} {
res := nullString([]byte(test.in))
c.Assert(res, qt.Equals, test.expect)
}
}

func BenchmarkDecodeExif(b *testing.B) {
c := qt.New(b)
f, err := os.Open(filepath.FromSlash("../../testdata/sunset.jpg"))
Expand All @@ -118,7 +102,7 @@ func BenchmarkDecodeExif(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err = d.Decode(f)
_, err = d.Decode(imagemeta.ImageFormatJPEG, f)
c.Assert(err, qt.IsNil)
f.Seek(0, 0)
}
Expand All @@ -145,7 +129,7 @@ func TestIssue10738(t *testing.T) {

d, err := NewDecoder(IncludeFields(include))
c.Assert(err, qt.IsNil)
x, err := d.Decode(f)
x, err := d.Decode(imagemeta.ImageFormatJPEG, f)
c.Assert(err, qt.IsNil)

// Verify that it survives a round-trip to JSON and back.
Expand Down Expand Up @@ -194,7 +178,7 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
10,
0,
1,
},
},
{
Expand All @@ -221,7 +205,7 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
1,
0,
1,
},
},
{
Expand Down Expand Up @@ -266,7 +250,7 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
30,
0,
1,
},
},
{
Expand All @@ -293,7 +277,7 @@ func TestIssue10738(t *testing.T) {
include: "Lens|Date|ExposureTime",
}, want{
4,
0,
1,
},
},
}
Expand Down
Loading

0 comments on commit 24d3dc1

Please sign in to comment.