]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Switch EXIF library
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 7 Jul 2024 10:54:30 +0000 (12:54 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 20 Jul 2024 13:56:10 +0000 (15:56 +0200)
Closes #10855
Closes #8586
Closes #8996

12 files changed:
go.mod
go.sum
hugolib/image_test.go
resources/image.go
resources/image_extended_test.go
resources/image_test.go
resources/images/exif/exif.go
resources/images/exif/exif_test.go
resources/images/image.go
resources/resource_spec.go
resources/resource_transformers/cssjs/tailwindcss.go
resources/testdata/sunrise.webp [new file with mode: 0644]

diff --git a/go.mod b/go.mod
index 2b7327aa33655f64c6efeb8c26307a144a91b68d..d1d4da6d84a89384faf775500c2e7d597adc86f0 100644 (file)
--- 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.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
diff --git a/go.sum b/go.sum
index 801a32cd2110d19d1f989a706cda7ffc3d598420..b4250b46b3fcaf7c12cb7e2ff3e27aabb35ac233 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -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=
index b3b93371177cefe5f3e03be08de154ebb8ecdbd9..d58008512224bbe2bbd0ba3f2f621a17dc6fdd96 100644 (file)
@@ -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()
 }
index 8f70a665ae7e3b10aebb9bbd9f4505774deb81cf..188f4962460dd24cca0fca6c46fb69c7c2696851 100644 (file)
@@ -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()
index 429e51fb6148edb55a2c597e2739c35a548a9723..2f1f4f4a487f75cd38807ca07cf2e2631f565b1e 100644 (file)
@@ -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)
 }
index 7e26c1f55e0ee5b90ab1bd4bfa4591f395891dc1..0a53b41177b795cf8b61dbf06155c39116463ff8 100644 (file)
@@ -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)
        }
 
index 0374cdc961b612b18340a49ce247b39d86a5be90..a7f0e0757a7cbfddf31207fc56378db99e8f8bad 100644 (file)
 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)
        }
index 64c5a39e303101f8cb082feb36aad358664f62d8..278bc761aefa4526c8ca9aea583a64d91a58d1c6 100644 (file)
@@ -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)
                        }
index 1637d0cf8b09aa4ef11e81552383ee699caf72ed..4d5285acdd2f79fabe609adbd2e44ad35a3af67f 100644 (file)
@@ -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 {
index ef76daa1af2726bed512ca963c013f1c80af5653..d50edeb73ce38154d25cbd7a7270e2f72b8cd984 100644 (file)
@@ -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
        }
index f3195c5960a62c78534f3fb3a8a39b6b9f8b267a..beda7a646fe56a2499a35fa5bdb7fc351cc7457d 100644 (file)
@@ -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
new file mode 100644 (file)
index 0000000..25ea7b0
Binary files /dev/null and b/resources/testdata/sunrise.webp differ