Skip to content

Commit

Permalink
Switch EXIF library
Browse files Browse the repository at this point in the history
Closes #10855
Closes #8586
Closes #8996
  • Loading branch information
bep committed Jul 12, 2024
1 parent fb8909d commit 1a8a771
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 167 deletions.
1 change: 1 addition & 0 deletions common/hugio/readers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
type ReadSeeker interface {
io.Reader
io.Seeker
io.ReaderAt
}

// ReadSeekCloser is implemented by afero.File. We use this as the common type for
Expand Down
2 changes: 1 addition & 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.3.0
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
7 changes: 6 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY=
github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
github.com/bep/helpers v0.4.0 h1:ab9veaAiWY4ST48Oxp5usaqivDmYdB744fz+tcZ3Ifs=
github.com/bep/helpers v0.4.0/go.mod h1:/QpHdmcPagDw7+RjkLFCvnlUc8lQ5kg4KDrEkb2Yyco=
github.com/bep/imagemeta v0.1.0 h1:Y/bE6q5HYgF3tEYL1dF2oprEYiuyCiADokp1Yc+pvDo=
github.com/bep/imagemeta v0.1.0/go.mod h1:FAcTxuAx8JxWVelC6VTK4GKxsHUzXEtR5lwKZG3Oupk=
github.com/bep/imagemeta v0.2.0 h1:938HCN0vtTpFW/Rs+HYiqkjFbLDojeR2VJPp/S+hIRU=
github.com/bep/imagemeta v0.2.0/go.mod h1:FAcTxuAx8JxWVelC6VTK4GKxsHUzXEtR5lwKZG3Oupk=
github.com/bep/imagemeta v0.3.0 h1:2dSTgp0RA8s8KUA8PKlnczVyOuvJ5bd553V1VijUXTo=
github.com/bep/imagemeta v0.3.0/go.mod h1:FAcTxuAx8JxWVelC6VTK4GKxsHUzXEtR5lwKZG3Oupk=
github.com/bep/lazycache v0.4.0 h1:X8yVyWNVupPd4e1jV7efi3zb7ZV/qcjKQgIQ5aPbkYI=
github.com/bep/lazycache v0.4.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc=
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
Expand Down Expand Up @@ -405,7 +411,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
4 changes: 2 additions & 2 deletions hugolib/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Ex
b.AssertImage(200, 200, "resources/_gen/images/bundle/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_resize_q75_box.jpg")

b.AssertFileContent("resources/_gen/images/bundle/sunset_3166614710256882113.json",
"DateTimeDigitized|time.Time", "PENTAX")
"FocalLengthIn35mmFormat|uint16", "PENTAX")

b.AssertImage(123, 234, "resources/_gen/images/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_123x234_resize_q75_box.jpg")
b.AssertFileContent("resources/_gen/images/images/sunset_3166614710256882113.json",
"DateTimeDigitized|time.Time", "PENTAX")
"FocalLengthIn35mmFormat|uint16", "PENTAX")

b.AssertNoDuplicateWrites()
}
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
13 changes: 9 additions & 4 deletions resources/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"image"
"image/gif"
"io/fs"
"math/big"
"math/rand"
"os"
"path/filepath"
Expand All @@ -30,6 +29,7 @@ import (
"testing"
"time"

"github.com/bep/imagemeta"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/resources/images/webp"

Expand Down Expand Up @@ -67,8 +67,13 @@ var eq = qt.CmpEquals(
return m1.Type == m2.Type
}),
cmp.Comparer(
func(v1, v2 *big.Rat) bool {
return v1.RatString() == v2.RatString()
func(v1, v2 imagemeta.Rat[uint32]) bool {
return v1.String() == v2.String()
},
),
cmp.Comparer(
func(v1, v2 imagemeta.Rat[int32]) bool {
return v1.String() == v2.String()
},
),
cmp.Comparer(func(v1, v2 time.Time) bool {
Expand Down Expand Up @@ -392,7 +397,7 @@ func TestImageResize8BitPNG(t *testing.T) {
c.Assert(image.MediaType().Type, qt.Equals, "image/png")
c.Assert(image.RelPermalink(), qt.Equals, "/a/gohugoio.png")
c.Assert(image.ResourceType(), qt.Equals, "image")
c.Assert(image.Exif(), qt.IsNil)
c.Assert(image.Exif(), qt.IsNotNil)

resized, err := image.Resize("800x")
c.Assert(err, qt.IsNil)
Expand Down
208 changes: 91 additions & 117 deletions resources/images/exif/exif.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,15 @@
package exif

import (
"bytes"
"fmt"
"io"
"math"
"math/big"
"regexp"
"strconv"
"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"

Check failure on line 28 in resources/images/exif/exif.go

View workflow job for this annotation

GitHub Actions / test (1.22.x, ubuntu-latest)

const exifTimeLayout is unused (U1000)
Expand All @@ -55,6 +49,14 @@ type Decoder struct {
noLatLong bool
}

func (d *Decoder) shouldInclude(s string) bool {
return (d.includeFieldsRe == nil || d.includeFieldsRe.MatchString(s))
}

func (d *Decoder) shouldExclude(s string) bool {
return d.excludeFieldsrRe != nil && d.excludeFieldsrRe.MatchString(s)
}

func IncludeFields(expression string) func(*Decoder) error {
return func(d *Decoder) error {
re, err := compileRegexp(expression)
Expand Down Expand Up @@ -115,148 +117,120 @@ func NewDecoder(options ...func(*Decoder) error) (*Decoder, error) {
return d, nil
}

func (d *Decoder) Decode(r io.Reader) (ex *ExifInfo, err error) {
var (
isTimeTag = func(s string) bool {
return strings.Contains(s, "Time")
}
isGPSTag = func(s string) bool {
return strings.HasPrefix(s, "GPS")
}
)

// TODO1 bump the exif cache version number.
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
var tagInfos imagemeta.Tags
handleTag := func(ti imagemeta.TagInfo) error {
tagInfos.Add(ti)
return nil
}

var tm time.Time
var lat, long float64

if !d.noDate {
tm, _ = x.DateTime()
}
shouldInclude := func(ti imagemeta.TagInfo) bool {
if ti.Source == imagemeta.EXIF {
if !d.noDate {
// We need the time tags to calculate the date.
if isTimeTag(ti.Tag) {
return true
}
}
if !d.noLatLong {
// We need to GPS tags to calculate the lat/long.
if isGPSTag(ti.Tag) {
return true
}
}

if !d.noLatLong {
lat, long, _ = x.LatLong()
if math.IsNaN(lat) {
lat = 0
if !strings.HasPrefix(ti.Namespace, "IFD0") {
// Drop thumbnail tags.
return false
}
}
if math.IsNaN(long) {
long = 0

if d.shouldExclude(ti.Tag) {
return false
}
}

walker := &exifWalker{x: x, vals: make(map[string]any), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe}
if err = x.Walk(walker); err != nil {
return
return d.shouldInclude(ti.Tag)
}

ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: walker.vals}
err = imagemeta.Decode(
imagemeta.Options{
R: r.(imagemeta.Reader),
ImageFormat: format,
ShouldHandleTag: shouldInclude,
HandleTag: handleTag,
Sources: imagemeta.EXIF, // For now. TODO1.
},
)

return
}
var tm time.Time
var lat, long float64

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
}
}
return s, nil
case tiff.OtherVal:
return "unknown", nil
if !d.noDate {
tm, _ = tagInfos.GetDateTime()
}

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 !d.noLatLong {
lat, long, _ = tagInfos.GetLatLong()
}

if t.Count == 1 {
if len(rv) == 1 {
return rv[0], nil
tags := make(map[string]any)
for k, v := range tagInfos.All() {
if d.shouldExclude(k) {
continue
}
if !d.shouldInclude(k) {
continue
}
tags[k] = v.Value
}

return rv, nil
}
ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: tags}

// 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)
return
}

type exifWalker struct {
x *_exif.Exif
vals map[string]any
includeMatcher *regexp.Regexp
excludeMatcher *regexp.Regexp
func getDateTime(tags map[string]imagemeta.TagInfo) time.Time {

Check failure on line 208 in resources/images/exif/exif.go

View workflow job for this annotation

GitHub Actions / test (1.22.x, ubuntu-latest)

func getDateTime is unused (U1000)
return time.Time{}
}

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
}
var tcodec *tmc.Codec

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:]
func init() {
tmcAdapters := []tmc.Adapter{
tmc.NewAdapter(imagemeta.NewRat[uint32](1, 2), nil, nil),
tmc.NewAdapter(imagemeta.NewRat[int32](1, 2), nil, nil),
// TODO1, if we don't remove the cache, this needs to be expanded to support other
tmc.NewAdapter(uint16(1),
func(s string) (interface{}, error) {
i, err := strconv.ParseUint(s, 10, 16)
return uint16(i), err
},
func(v interface{}) (string, error) {
vv := v.(uint16)
return strconv.Itoa(int(vv)), nil
}),
}
return rv.String()
}

var tcodec *tmc.Codec
tmcAdapters = append(tmc.DefaultTypeAdapters, tmcAdapters...)

func init() {
var err error
tcodec, err = tmc.New()
tcodec, err = tmc.New(tmc.WithTypeAdapters(tmcAdapters))
if err != nil {
panic(err)
}
Expand Down
Loading

0 comments on commit 1a8a771

Please sign in to comment.