diff options
author | Bjørn Erik Pedersen <[email protected]> | 2024-07-07 12:54:30 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2024-07-20 15:56:10 +0200 |
commit | 72ff937e11a6375da5b13788f855eafcc2452b85 (patch) | |
tree | cfda30d19de010b2ebf654b25f4958c65588510d | |
parent | a28bed0817b99eafde8486427eda10a3357ab7b8 (diff) | |
download | hugo-72ff937e11a6375da5b13788f855eafcc2452b85.tar.gz hugo-72ff937e11a6375da5b13788f855eafcc2452b85.zip |
Switch EXIF library
Closes #10855
Closes #8586
Closes #8996
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 7 | ||||
-rw-r--r-- | hugolib/image_test.go | 9 | ||||
-rw-r--r-- | resources/image.go | 12 | ||||
-rw-r--r-- | resources/image_extended_test.go | 14 | ||||
-rw-r--r-- | resources/image_test.go | 14 | ||||
-rw-r--r-- | resources/images/exif/exif.go | 307 | ||||
-rw-r--r-- | resources/images/exif/exif_test.go | 65 | ||||
-rw-r--r-- | resources/images/image.go | 25 | ||||
-rw-r--r-- | resources/resource_spec.go | 4 | ||||
-rw-r--r-- | resources/resource_transformers/cssjs/tailwindcss.go | 6 | ||||
-rw-r--r-- | resources/testdata/sunrise.webp | bin | 0 -> 95652 bytes |
12 files changed, 284 insertions, 181 deletions
@@ -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.7.0 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 @@ -118,10 +118,6 @@ github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/bep/gitmap v1.4.0 h1:GeWbPb2QDTfcZLBQmCB693N3sJmPQfeu81fDrD5r8x8= -github.com/bep/gitmap v1.4.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw= -github.com/bep/gitmap v1.5.0 h1:ExDl7HeDaRDG8FXFRTnv20qzbyJlC6ivdOboMYFvrms= -github.com/bep/gitmap v1.5.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw= github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA= github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw= github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= @@ -136,6 +132,8 @@ 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.7.0 h1:I6Ve/UToNRdnh8qOlpuiR8dX56q6qi97hOqReaMsLMk= +github.com/bep/imagemeta v0.7.0/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s= 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= @@ -409,7 +407,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/hugolib/image_test.go b/hugolib/image_test.go index b3b933711..d58008512 100644 --- a/hugolib/image_test.go +++ b/hugolib/image_test.go @@ -82,12 +82,13 @@ SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Ex // Check the file cache 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") + b.AssertFileContent("resources/_gen/images/bundle/sunset_9750822043026343402.json", + "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") + + b.AssertFileContent("resources/_gen/images/images/sunset_9750822043026343402.json", + "FocalLengthIn35mmFormat|uint16", "PENTAX") b.AssertNoDuplicateWrites() } diff --git a/resources/image.go b/resources/image.go index 8f70a665a..188f49624 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,8 @@ func (i *imageResource) getExif() *exif.ExifInfo { } defer f.Close() - x, err := i.getSpec().imaging.DecodeExif(f) + filename := i.getResourcePaths().Path() + x, err := i.getSpec().imaging.DecodeExif(filename, mf, f) if err != nil { i.getSpec().Logger.Warnf("Unable to decode Exif metadata from image: %s", i.Key()) return nil @@ -471,7 +473,9 @@ func (i *imageResource) clone(img image.Image) *imageResource { } func (i *imageResource) getImageMetaCacheTargetPath() string { - const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache + // Increment to invalidate the meta cache + // Last increment: v0.130.0 when change to the new imagemeta library for Exif. + const imageMetaVersionNumber = 2 cfgHash := i.getSpec().imaging.Cfg.SourceHash df := i.getResourcePaths() diff --git a/resources/image_extended_test.go b/resources/image_extended_test.go index 429e51fb6..2f1f4f4a4 100644 --- a/resources/image_extended_test.go +++ b/resources/image_extended_test.go @@ -20,22 +20,28 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/htesting/hqt" "github.com/gohugoio/hugo/media" ) func TestImageResizeWebP(t *testing.T) { c := qt.New(t) - _, image := fetchImage(c, "sunset.webp") + _, image := fetchImage(c, "sunrise.webp") c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType) - c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.webp") + c.Assert(image.RelPermalink(), qt.Equals, "/a/sunrise.webp") c.Assert(image.ResourceType(), qt.Equals, "image") - c.Assert(image.Exif(), qt.IsNil) + exif := image.Exif() + c.Assert(exif, qt.Not(qt.IsNil)) + c.Assert(exif.Tags["Copyright"], qt.Equals, "Bjørn Erik Pedersen") + c.Assert(exif.Lat, hqt.IsSameFloat64, 36.59744166666667) + c.Assert(exif.Long, hqt.IsSameFloat64, -4.50846) + c.Assert(exif.Date.IsZero(), qt.Equals, false) resized, err := image.Resize("123x") c.Assert(err, qt.IsNil) c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType) - c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu36ee0b61ba924719ad36da960c273f96_59826_123x0_resize_q68_h2_linear_2.webp") + c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunrise_hu6ad68bcbae1b79cbc2f6b451894deaf6_95652_123x0_resize_q68_h2_linear_2.webp") c.Assert(resized.Width(), qt.Equals, 123) } diff --git a/resources/image_test.go b/resources/image_test.go index 7e26c1f55..0a53b4117 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -19,7 +19,6 @@ import ( "image" "image/gif" "io/fs" - "math/big" "math/rand" "os" "path/filepath" @@ -30,6 +29,7 @@ import ( "testing" "time" + "github.com/bep/imagemeta" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/resources/images/webp" @@ -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 { @@ -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) @@ -443,6 +448,7 @@ func TestImageExif(t *testing.T) { c.Assert(lensModel, qt.Equals, "smc PENTAX-DA* 16-50mm F2.8 ED AL [IF] SDM") resized, _ := image.Resize("300x200") x2 := resized.Exif() + c.Assert(x2, eq, x) } diff --git a/resources/images/exif/exif.go b/resources/images/exif/exif.go index 0374cdc96..a7f0e0757 100644 --- a/resources/images/exif/exif.go +++ b/resources/images/exif/exif.go @@ -14,25 +14,18 @@ package exif import ( - "bytes" "fmt" "io" - "math" - "math/big" "regexp" + "strconv" "strings" "time" - "unicode" - "unicode/utf8" + "github.com/bep/imagemeta" + "github.com/bep/logg" "github.com/bep/tmc" - - _exif "github.com/rwcarlsen/goexif/exif" - "github.com/rwcarlsen/goexif/tiff" ) -const exifTimeLayout = "2006:01:02 15:04:05" - // ExifInfo holds the decoded Exif data for an Image. type ExifInfo struct { // GPS latitude in degrees. @@ -53,6 +46,15 @@ type Decoder struct { excludeFieldsrRe *regexp.Regexp noDate bool noLatLong bool + warnl logg.LevelLogger +} + +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 { @@ -91,6 +93,13 @@ func WithDateDisabled(disabled bool) func(*Decoder) error { } } +func WithWarnLogger(warnl logg.LevelLogger) func(*Decoder) error { + return func(d *Decoder) error { + d.warnl = warnl + return nil + } +} + func compileRegexp(expression string) (*regexp.Regexp, error) { expression = strings.TrimSpace(expression) if expression == "" { @@ -115,148 +124,222 @@ 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") + } +) + +// Filename is only used for logging. +func (d *Decoder) Decode(filename string, 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 + var tagInfos imagemeta.Tags + handleTag := func(ti imagemeta.TagInfo) error { + tagInfos.Add(ti) + return nil + } + + 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 !strings.HasPrefix(ti.Namespace, "IFD0") { + // Drop thumbnail tags. + return false + } + } + + if d.shouldExclude(ti.Tag) { + return false } - return + + return d.shouldInclude(ti.Tag) } + var warnf func(string, ...any) + if d.warnl != nil { + // There should be very little warnings (fingers crossed!), + // but this will typically be unrecognized formats. + // To be able to possibly get rid of these warnings, + // we need to know what images are causing them. + warnf = func(format string, args ...any) { + format = fmt.Sprintf("%q: %s: ", filename, format) + d.warnl.Logf(format, args...) + } + } + + err = imagemeta.Decode( + imagemeta.Options{ + R: r.(io.ReadSeeker), + ImageFormat: format, + ShouldHandleTag: shouldInclude, + HandleTag: handleTag, + Sources: imagemeta.EXIF, // For now. TODO(bep) + Warnf: warnf, + }, + ) + 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 - } + lat, long, _ = tagInfos.GetLatLong() } - walker := &exifWalker{x: x, vals: make(map[string]any), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe} - if err = x.Walk(walker); err != nil { - return + 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 } - ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: walker.vals} + ex = &ExifInfo{Lat: lat, Long: long, Date: tm, Tags: tags} 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 - } +var tcodec *tmc.Codec + +func init() { + newIntadapter := func(target any) tmc.Adapter { + var bitSize int + var isSigned bool + + switch target.(type) { + case int: + bitSize = 0 + isSigned = true + case int8: + bitSize = 8 + isSigned = true + case int16: + bitSize = 16 + isSigned = true + case int32: + bitSize = 32 + isSigned = true + case int64: + bitSize = 64 + isSigned = true + case uint: + bitSize = 0 + case uint8: + bitSize = 8 + case uint16: + bitSize = 16 + case uint32: + bitSize = 32 + case uint64: + bitSize = 64 } - 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) + intFromString := func(s string) (any, error) { + if bitSize == 0 { + return strconv.Atoi(s) + } + + var v any + var err error + + if isSigned { + v, err = strconv.ParseInt(s, 10, bitSize) } else { - rv = append(rv, rat) + v, err = strconv.ParseUint(s, 10, bitSize) } - case tiff.FloatVal: - v, _ := t.Float(i) - rv = append(rv, v) - case tiff.IntVal: - v, _ := t.Int(i) - rv = append(rv, v) - } - } + if err != nil { + return 0, err + } - if t.Count == 1 { - if len(rv) == 1 { - return rv[0], nil - } - } + if isSigned { + i := v.(int64) + switch target.(type) { + case int: + return int(i), nil + case int8: + return int8(i), nil + case int16: + return int16(i), nil + case int32: + return int32(i), nil + case int64: + return i, nil + } + } - return rv, nil -} + i := v.(uint64) + switch target.(type) { + case uint: + return uint(i), nil + case uint8: + return uint8(i), nil + case uint16: + return uint16(i), nil + case uint32: + return uint32(i), nil + case uint64: + return i, 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]any - includeMatcher *regexp.Regexp - excludeMatcher *regexp.Regexp -} + return 0, fmt.Errorf("unsupported target type %T", target) + } -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 + intToString := func(v any) (string, error) { + return fmt.Sprintf("%d", v), nil + } + + return tmc.NewAdapter(target, intFromString, intToString) } - e.vals[name] = val - return nil -} -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:] + ru, _ := imagemeta.NewRat[uint32](1, 2) + ri, _ := imagemeta.NewRat[int32](1, 2) + tmcAdapters := []tmc.Adapter{ + tmc.NewAdapter(ru, nil, nil), + tmc.NewAdapter(ri, nil, nil), + newIntadapter(int(1)), + newIntadapter(int8(1)), + newIntadapter(int16(1)), + newIntadapter(int32(1)), + newIntadapter(int64(1)), + newIntadapter(uint(1)), + newIntadapter(uint8(1)), + newIntadapter(uint16(1)), + newIntadapter(uint32(1)), + newIntadapter(uint64(1)), } - 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) } diff --git a/resources/images/exif/exif_test.go b/resources/images/exif/exif_test.go index 64c5a39e3..278bc761a 100644 --- a/resources/images/exif/exif_test.go +++ b/resources/images/exif/exif_test.go @@ -15,13 +15,12 @@ package exif import ( "encoding/json" - "math/big" "os" "path/filepath" "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 +34,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.JPEG, 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)) @@ -49,9 +49,9 @@ func TestExif(t *testing.T) { c.Assert(ok, qt.Equals, true) c.Assert(lensModel, qt.Equals, "smc PENTAX-DA* 16-50mm F2.8 ED AL [IF] SDM") - v, found = x.Tags["DateTime"] + v, found = x.Tags["ModifyDate"] 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 +72,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.PNG, f) + c.Assert(err, qt.IsNil) } func TestIssue8079(t *testing.T) { @@ -85,28 +85,11 @@ func TestIssue8079(t *testing.T) { d, err := NewDecoder() c.Assert(err, qt.IsNil) - x, err := d.Decode(f) + x, err := d.Decode("", imagemeta.JPEG, 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 +101,7 @@ func BenchmarkDecodeExif(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err = d.Decode(f) + _, err = d.Decode("", imagemeta.JPEG, f) c.Assert(err, qt.IsNil) f.Seek(0, 0) } @@ -126,8 +109,13 @@ func BenchmarkDecodeExif(b *testing.B) { var eq = qt.CmpEquals( 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 { @@ -138,14 +126,15 @@ var eq = qt.CmpEquals( func TestIssue10738(t *testing.T) { c := qt.New(t) - testFunc := func(path, include string) any { + testFunc := func(c *qt.C, path, include string) any { + c.Helper() f, err := os.Open(filepath.FromSlash(path)) c.Assert(err, qt.IsNil) defer f.Close() d, err := NewDecoder(IncludeFields(include)) c.Assert(err, qt.IsNil) - x, err := d.Decode(f) + x, err := d.Decode("", imagemeta.JPEG, f) c.Assert(err, qt.IsNil) // Verify that it survives a round-trip to JSON and back. @@ -194,7 +183,7 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 10, - 0, + 1, }, }, { @@ -221,7 +210,7 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 1, - 0, + 1, }, }, { @@ -266,7 +255,7 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 30, - 0, + 1, }, }, { @@ -293,19 +282,21 @@ func TestIssue10738(t *testing.T) { include: "Lens|Date|ExposureTime", }, want{ 4, - 0, + 1, }, }, } for _, tt := range tests { c.Run(tt.name, func(c *qt.C) { - got := testFunc(tt.args.path, tt.args.include) + got := testFunc(c, tt.args.path, tt.args.include) switch v := got.(type) { case float64: c.Assert(v, qt.Equals, float64(tt.want.vN)) - case *big.Rat: - c.Assert(v, eq, big.NewRat(tt.want.vN, tt.want.vD)) + case imagemeta.Rat[uint32]: + r, err := imagemeta.NewRat[uint32](uint32(tt.want.vN), uint32(tt.want.vD)) + c.Assert(err, qt.IsNil) + c.Assert(v, eq, r) default: c.Fatalf("unexpected type: %T", got) } diff --git a/resources/images/image.go b/resources/images/image.go index 1637d0cf8..4d5285acd 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -26,6 +26,8 @@ import ( "sync" "github.com/bep/gowebp/libwebp/webpoptions" + "github.com/bep/imagemeta" + "github.com/bep/logg" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/resources/images/webp" @@ -174,13 +176,14 @@ func (i *Image) initConfig() error { return nil } -func NewImageProcessor(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { +func NewImageProcessor(warnl logg.LevelLogger, cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { e := cfg.Config.Imaging.Exif exifDecoder, err := exif.NewDecoder( exif.WithDateDisabled(e.DisableDate), exif.WithLatLongDisabled(e.DisableLatLong), exif.ExcludeFields(e.ExcludeFields), exif.IncludeFields(e.IncludeFields), + exif.WithWarnLogger(warnl), ) if err != nil { return nil, err @@ -197,8 +200,9 @@ type ImageProcessor struct { exifDecoder *exif.Decoder } -func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) { - return p.exifDecoder.Decode(r) +// Filename is only used for logging. +func (p *ImageProcessor) DecodeExif(filename string, format imagemeta.ImageFormat, r io.Reader) (*exif.ExifInfo, error) { + return p.exifDecoder.Decode(filename, format, r) } func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([]gift.Filter, error) { @@ -353,6 +357,21 @@ const ( WEBP ) +func (f Format) ToImageMetaImageFormatFormat() imagemeta.ImageFormat { + switch f { + case JPEG: + return imagemeta.JPEG + case PNG: + return imagemeta.PNG + case TIFF: + return imagemeta.TIFF + case WEBP: + return imagemeta.WebP + default: + return -1 + } +} + // RequiresDefaultQuality returns if the default quality needs to be applied to // images of this format. func (f Format) RequiresDefaultQuality() bool { diff --git a/resources/resource_spec.go b/resources/resource_spec.go index ef76daa1a..d50edeb73 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -60,7 +60,9 @@ func NewSpec( conf := s.Cfg.GetConfig().(*allconfig.Config) imgConfig := conf.Imaging - imaging, err := images.NewImageProcessor(imgConfig) + imagesWarnl := logger.WarnCommand("images") + + imaging, err := images.NewImageProcessor(imagesWarnl, imgConfig) if err != nil { return nil, err } diff --git a/resources/resource_transformers/cssjs/tailwindcss.go b/resources/resource_transformers/cssjs/tailwindcss.go index f3195c596..beda7a646 100644 --- a/resources/resource_transformers/cssjs/tailwindcss.go +++ b/resources/resource_transformers/cssjs/tailwindcss.go @@ -99,11 +99,6 @@ func (t *tailwindcssTransformation) Transform(ctx *resources.ResourceTransformat cmdArgs = append(cmdArgs, options.toArgs()...) - // TODO1 - // npm i tailwindcss @tailwindcss/cli - // npm i tailwindcss@next @tailwindcss/cli@next - // npx tailwindcss -h - var errBuf bytes.Buffer stderr := io.MultiWriter(infow, &errBuf) @@ -134,7 +129,6 @@ func (t *tailwindcssTransformation) Transform(ctx *resources.ResourceTransformat t.rs.Assets.Fs, t.rs.Logger, ctx.DependencyManager, ) - // TODO1 option { src, err = imp.resolve() if err != nil { return err diff --git a/resources/testdata/sunrise.webp b/resources/testdata/sunrise.webp Binary files differnew file mode 100644 index 000000000..25ea7b046 --- /dev/null +++ b/resources/testdata/sunrise.webp |