From 24d3dc16b7cabddfdba0b278936334aad342aa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 7 Jul 2024 12:54:30 +0200 Subject: [PATCH] Switch EXIF library Closes #10855 Closes #8586 --- go.mod | 4 +- go.sum | 1 - resources/image.go | 7 +- resources/images/exif/exif.go | 150 ++++++----------------------- resources/images/exif/exif_test.go | 42 +++----- resources/images/image.go | 20 +++- 6 files changed, 69 insertions(+), 155 deletions(-) diff --git a/go.mod b/go.mod index c2aabcf0605..4b0ccd30f1f 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -162,3 +162,5 @@ require ( ) go 1.20 + +replace github.com/bep/imagemeta => /Users/bep/dev/go/bep/imagemeta diff --git a/go.sum b/go.sum index 5e94def05af..97053a9de1e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/resources/image.go b/resources/image.go index 8f70a665ae7..71980766d2c 100644 --- a/resources/image.go +++ b/resources/image.go @@ -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 } @@ -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 diff --git a/resources/images/exif/exif.go b/resources/images/exif/exif.go index 0374cdc961b..97049df2433 100644 --- a/resources/images/exif/exif.go +++ b/resources/images/exif/exif.go @@ -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" @@ -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() { diff --git a/resources/images/exif/exif_test.go b/resources/images/exif/exif_test.go index 64c5a39e303..190df582156 100644 --- a/resources/images/exif/exif_test.go +++ b/resources/images/exif/exif_test.go @@ -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" @@ -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)) @@ -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) @@ -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) { @@ -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")) @@ -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) } @@ -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. @@ -194,7 +178,7 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 10, - 0, + 1, }, }, { @@ -221,7 +205,7 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 1, - 0, + 1, }, }, { @@ -266,7 +250,7 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 30, - 0, + 1, }, }, { @@ -293,7 +277,7 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 4, - 0, + 1, }, }, } diff --git a/resources/images/image.go b/resources/images/image.go index 1637d0cf8b0..6521e60c9a6 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -26,6 +26,7 @@ import ( "sync" "github.com/bep/gowebp/libwebp/webpoptions" + "github.com/bep/imagemeta" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/resources/images/webp" @@ -197,8 +198,8 @@ type ImageProcessor struct { exifDecoder *exif.Decoder } -func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) { - return p.exifDecoder.Decode(r) +func (p *ImageProcessor) DecodeExif(format imagemeta.ImageFormat, r io.Reader) (*exif.ExifInfo, error) { + return p.exifDecoder.Decode(format, r) } func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([]gift.Filter, error) { @@ -353,6 +354,21 @@ const ( WEBP ) +func (f Format) ToImageMetaImageFormatFormat() imagemeta.ImageFormat { + switch f { + case JPEG: + return imagemeta.ImageFormatJPEG + case PNG: + return imagemeta.ImageFormatPNG + case TIFF: + return imagemeta.ImageFormatTIFF + case WEBP: + return imagemeta.ImageFormatWebP + default: + return -1 + } +} + // RequiresDefaultQuality returns if the default quality needs to be applied to // images of this format. func (f Format) RequiresDefaultQuality() bool {