"strings"
)
-// ReadSeeker wraps io.Reader and io.Seeker.
-type ReadSeeker interface {
- io.Reader
- io.Seeker
-}
-
// ReadSeekCloser is implemented by afero.File. We use this as the common type for
// content in Resource objects, even for strings.
type ReadSeekCloser interface {
- ReadSeeker
+ io.ReadSeeker
io.Closer
}
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
type readSeekerNopCloser struct {
- ReadSeeker
+ io.ReadSeeker
}
// Close does nothing.
}
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
-func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
+func NewReadSeekerNoOpCloser(r io.ReadSeeker) ReadSeekCloser {
return readSeekerNopCloser{r}
}
return readSeekerNopCloser{bytes.NewReader(content)}
}
+// NewReadSeekerNoOpCloserFromReader creates a new ReadSeekerNoOpCloser from the given io.Reader.
+// If the given io.Reader is not an io.ReadSeeker, the entire content will be read into memory.
+func NewReadSeekerNoOpCloserFromReader(r io.Reader) (readSeekerNopCloser, error) {
+ var rs io.ReadSeeker
+ if s, ok := r.(io.ReadSeeker); ok {
+ rs = s
+ } else {
+ b, err := io.ReadAll(r)
+ if err != nil {
+ return readSeekerNopCloser{rs}, err
+ }
+ rs = bytes.NewReader(b)
+ }
+ return readSeekerNopCloser{rs}, nil
+}
+
// NewOpenReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
// The ReadSeeker will be seeked to the beginning before returned.
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
fs := s.fs.WorkingDirReadOnly
b, err := afero.ReadFile(fs, filename)
s.Assert(err, qt.IsNil)
- conf, format, err := s.H.ResourceSpec.Imaging.Codec.DecodeConfig(bytes.NewReader(b))
+ conf, format, err := s.H.ResourceSpec.Imaging.Codec.DecodeConfig(0, bytes.NewReader(b))
s.Assert(err, qt.IsNil)
img, err := s.H.ResourceSpec.Imaging.Codec.Decode(bytes.NewReader(b))
s.Assert(err, qt.IsNil)
TIFFType Type
BMPType Type
WEBPType Type
+ AVIFType Type
+ HEIFType Type
+ HEICType Type
// Common font types
TrueTypeFontType Type
TIFFType: Type{Type: "image/tiff"},
BMPType: Type{Type: "image/bmp"},
WEBPType: Type{Type: "image/webp"},
+ AVIFType: Type{Type: "image/avif"},
+ HEIFType: Type{Type: "image/heif"},
+ HEICType: Type{Type: "image/heic"},
// Common font types
TrueTypeFontType: Type{Type: "font/ttf"},
"image/tiff": map[string]any{"suffixes": []string{"tif", "tiff"}},
"image/bmp": map[string]any{"suffixes": []string{"bmp"}},
"image/webp": map[string]any{"suffixes": []string{"webp"}},
+ "image/avif": map[string]any{"suffixes": []string{"avif"}},
+ "image/heif": map[string]any{"suffixes": []string{"heif"}},
+ "image/heic": map[string]any{"suffixes": []string{"heic"}},
// Common font types
"font/ttf": map[string]any{"suffixes": []string{"ttf"}},
}
- c.Assert(len(DefaultTypes), qt.Equals, 41)
+ c.Assert(len(DefaultTypes), qt.Equals, 44)
}
"image/jpeg"
"image/png"
"io"
+ "strings"
"github.com/bep/imagemeta"
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/himage"
+ "github.com/gohugoio/hugo/common/hugio"
"golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
return img, err
}
-func (d *Codec) DecodeConfig(r io.Reader) (image.Config, string, error) {
+func (d *Codec) DecodeConfig(f Format, r io.Reader) (image.Config, string, error) {
rr := toPeekReader(r)
format, err := formatFromImage(rr)
if err != nil {
return image.Config{}, "", err
}
+ if format == 0 {
+ format = f
+ }
r = rr
- if format == WEBP {
- if rs, ok := r.(io.ReadSeeker); ok {
- rs.Seek(0, 0)
- // Avoid spinning up a WASM runtime if we don't have to.
- res, err := imagemeta.Decode(
- imagemeta.Options{
- R: rs,
- ImageFormat: imagemeta.WebP,
- Sources: imagemeta.CONFIG,
- },
- )
- if err == nil {
- return image.Config{
- Width: res.ImageConfig.Width,
- Height: res.ImageConfig.Height,
- ColorModel: color.RGBAModel,
- }, "webp", nil
- }
- rs.Seek(0, 0)
- r = rs
+ if format.UseImageMetaConfigDecoder() {
+ rs, err := hugio.NewReadSeekerNoOpCloserFromReader(r)
+ if err != nil {
+ return image.Config{}, "", err
}
- // Fallback to the webp codec config decode.
- cfg, err := d.webp.DecodeConfig(r)
- return cfg, "webp", err
+ rs.Seek(0, 0)
+ res, err := imagemeta.Decode(
+ imagemeta.Options{
+ R: rs,
+ ImageFormat: format.ToImageMetaImageFormatFormat(),
+ Sources: imagemeta.CONFIG,
+ },
+ )
+ if err == nil {
+ return image.Config{
+ Width: res.ImageConfig.Width,
+ Height: res.ImageConfig.Height,
+ ColorModel: color.RGBAModel,
+ }, strings.ToLower(format.String()), nil
+ }
+
+ // Fallback to the standard image.DecodeConfig.
+ rs.Seek(0, 0)
+ r = rs
}
- // Fallback to the standard image.DecodeConfig.
- conf, name, err := image.DecodeConfig(rr)
+ conf, name, err := image.DecodeConfig(r)
+
return conf, name, err
}
".bmp": BMP,
".gif": GIF,
".webp": WEBP,
+ ".avif": AVIF,
+ ".heif": HEIF,
+ ".heic": HEIC,
}
// These are the image types we can process.
media.Builtin.WEBPType.SubType: WEBP,
}
+ // We cannot process these formats, but we can provide metadata support for them (including width/height).
+ metaOnlyImageSubTypes = map[string]Format{
+ media.Builtin.AVIFType.SubType: AVIF,
+ media.Builtin.HEIFType.SubType: HEIF,
+ media.Builtin.HEICType.SubType: HEIC,
+ }
+
// Increment to mark all processed images as stale. Only use when absolutely needed.
// See the finer grained smartCropVersionNumber.
mainImageVersionNumber = 1
return f, found
}
-func ImageFormatFromMediaSubType(sub string) (Format, bool) {
+type ImageResourceType int
+
+const (
+ // ImageResourceTypeNone means that the resource is not an image, and thus does not support any image operations.
+ ImageResourceTypeNone ImageResourceType = iota
+ // This is an image, but with no support for any image operations.
+ ImageResourceTypeBasic
+ // ImageResourceTypeMetaOnly means that only metadata operations (e.g. getting width/height and other metadata) are supported for this format.
+ ImageResourceTypeMetaOnly
+ // ImageResourceTypeProcessable means that all image operations (resizing, cropping, etc.) are supported for this format.
+ ImageResourceTypeProcessable
+)
+
+// ImageFormatFromMediaSubType returns the image format for the given media subtype, and how much image processing operations are supported for this format.
+func ImageFormatFromMediaSubType(sub string) (Format, ImageResourceType) {
f, found := processableImageSubTypes[sub]
- return f, found
+ if found {
+ return f, ImageResourceTypeProcessable
+ }
+ if f, found = metaOnlyImageSubTypes[sub]; found {
+ return f, ImageResourceTypeMetaOnly
+ }
+ return f, ImageResourceTypeBasic
}
const (
func (i *Image) InitConfig(r io.Reader) error {
var err error
i.configInit.Do(func() {
- i.config, _, err = i.Proc.Codec.DecodeConfig(r)
+ i.config, _, err = i.Proc.Codec.DecodeConfig(i.Format, r)
})
return err
}
}
defer f.Close()
- i.config, _, err = i.Proc.Codec.DecodeConfig(f)
+ i.config, _, err = i.Proc.Codec.DecodeConfig(i.Format, f)
})
if err != nil {
TIFF
BMP
WEBP
+
+ // Below: We have no encoder/decoder for these, but we can provide metadata support for them (including width/height).
+ AVIF
+ HEIF
+ HEIC
)
+// Whether to use imagemeta to decode image config (width/height ).
+func (f Format) UseImageMetaConfigDecoder() bool {
+ return f == WEBP || f == AVIF || f == HEIF || f == HEIC
+}
+
func (f Format) ToImageMetaImageFormatFormat() imagemeta.ImageFormat {
switch f {
case JPEG:
return imagemeta.TIFF
case WEBP:
return imagemeta.WebP
+ case AVIF:
+ return imagemeta.AVIF
+ case HEIF:
+ return imagemeta.HEIF
+ case HEIC:
+ return imagemeta.HEIF
default:
return -1
}
return media.Builtin.BMPType
case WEBP:
return media.Builtin.WEBPType
+ case AVIF:
+ return media.Builtin.AVIFType
+ case HEIF:
+ return media.Builtin.HEIFType
+ case HEIC:
+ return media.Builtin.HEICType
default:
panic(fmt.Sprintf("%d is not a valid image format", f))
}
return "BMP"
case WEBP:
return "WEBP"
+ case AVIF:
+ return "AVIF"
+ case HEIF:
+ return "HEIF"
+ case HEIC:
+ return "HEIC"
default:
return "Unknown"
}
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, `invalid metadata source "foo" in imaging.meta.sources config; must be one of [exif iptc xmp]`)
}
+
+func TestAVIFMetaWidthAndHeight(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[imaging.meta]
+fields = ['**']
+sources = ['exif', 'iptc', 'xmp']
+-- assets/sunset.avif --
+sourcefilename: ../../testdata/sunset.avif
+-- assets/sunset.jpg --
+sourcefilename: ../../testdata/sunset.jpg
+-- assets/mytext.txt --
+This is a text file, not an image.
+-- assets/mysvg.svg --
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <rect width="100" height="100" fill="blue" />
+</svg>
+-- layouts/home.html --
+{{ $txt := resources.Get "mytext.txt" }}
+{{ $svg := resources.Get "mysvg.svg" }}
+{{ $avif := resources.Get "sunset.avif" }}
+{{ $jpg := resources.Get "sunset.jpg" }}
+{{ $ic := images.Config "/assets/sunset.avif" }}
+$avif.Width/Height: {{ $avif.Width }}x{{ $avif.Height }}
+$ic.Width/Height: {{ $ic.Width }}x{{ $ic.Height }}
+
+{{ template "is-meta-etc" dict "what" "AVIF" "dot" $avif -}}
+{{ template "is-meta-etc" dict "what" "JPG" "dot" $jpg -}}
+{{ template "is-meta-etc" dict "what" "TXT" "dot" $txt -}}
+{{ template "is-meta-etc" dict "what" "SVG" "dot" $svg -}}
+
+{{ $meta := $avif.Meta }}
+Num Exif tags: {{ $meta.Exif | len }}|
+{{ define "is-meta-etc"}}
+IsImageResource {{ .what }}: {{ if reflect.IsImageResource .dot }}true{{ else }}false{{ end }}
+IsImageResourceWithMeta {{ .what }}: {{ if reflect.IsImageResourceWithMeta .dot }}true{{ else }}false{{ end }}
+IsImageResourceProcessable {{ .what }}: {{ if reflect.IsImageResourceProcessable .dot }}true{{ else }}false{{ end }}
+{{ end }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ `
+$avif.Width/Height: 900x562
+$ic.Width/Height: 900x562
+
+
+IsImageResource AVIF: true
+IsImageResourceWithMeta AVIF: true
+IsImageResourceProcessable AVIF: false
+
+IsImageResource JPG: true
+IsImageResourceWithMeta JPG: true
+IsImageResourceProcessable JPG: true
+
+IsImageResource TXT: false
+IsImageResourceWithMeta TXT: false
+IsImageResourceProcessable TXT: false
+
+IsImageResource SVG: true
+IsImageResourceWithMeta SVG: false
+IsImageResourceProcessable SVG: false
+
+Num Exif tags: 52|
+`,
+ )
+}
isImage := rd.MediaType.MainType == "image"
var imgFormat images.Format
+ var imgOpsSupport images.ImageResourceType
if isImage {
- imgFormat, isImage = images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
+ imgFormat, imgOpsSupport = images.ImageFormatFromMediaSubType(rd.MediaType.SubType)
}
gr := &genericResource{
title: rd.Title,
}
- if isImage {
+ if imgOpsSupport >= images.ImageResourceTypeMetaOnly {
ir := newImageResource(images.NewImage(imgFormat, r.Imaging, nil, gr), gr)
return newResourceAdapter(gr.spec, rd.LazyPublish, ir), nil
}
return img
}
-// IsImage reports whether the given resource is an image that can be processed.
-func IsImage(v any) bool {
+// ResolveImageOpsSupport reports the ImageOpsSupport for the given resource. This can be used to determine if a resource supports image operations like Resize, Crop, etc.
+func ResolveImageOpsSupport(v any) images.ImageResourceType {
r, ok := v.(resource.Resource)
if !ok {
- return false
+ return images.ImageResourceTypeNone
}
mt := r.MediaType()
if mt.MainType != "image" {
- return false
+ return images.ImageResourceTypeNone
}
- _, isImage := images.ImageFormatFromMediaSubType(mt.SubType)
- return isImage
+ _, support := images.ImageFormatFromMediaSubType(mt.SubType)
+ return support
}
func (r *resourceAdapter) publish() {
"fmt"
"image"
"path"
+ "path/filepath"
"sync"
"github.com/bep/overlayfs"
}
defer f.Close()
- config, _, err = ns.deps.ResourceSpec.Imaging.Codec.DecodeConfig(f)
+ ext := filepath.Ext(filename)
+ format, _ := images.ImageFormatFromExt(ext)
+
+ config, _, err = ns.deps.ResourceSpec.Imaging.Codec.DecodeConfig(format, f)
if err != nil {
return config, err
}
import (
"github.com/gohugoio/hugo/common/hreflect"
"github.com/gohugoio/hugo/resources"
+ "github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
)
return ok
}
-// IsImageResource reports whether v is a Hugo Image Resource.
-// If this returns true, you may process it and get information about its width, height, etc.
+// IsImageResource reports whether v is a Image Resource.
func (ns *Namespace) IsImageResource(v any) bool {
- return resources.IsImage(v)
+ return resources.ResolveImageOpsSupport(v) > images.ImageResourceTypeNone
+}
+
+// IsImageResourceWithMeta reports whether v is a Image Resource that supports at least the image metadata operations Width and Height and Meta (for e.g. Exif).
+// This will return true for AVIF, HEIF and HEIC image resources, even if we don't yet support image operations like Resize, Crop, etc. on these formats.
+func (ns *Namespace) IsImageResourceWithMeta(v any) bool {
+ return resources.ResolveImageOpsSupport(v) >= images.ImageResourceTypeMetaOnly
+}
+
+// IsImageResourceProcessable reports whether v is a Image Resource that supports all image processing operations like Resize, Crop, etc. in addition to the metadata operations.
+func (ns *Namespace) IsImageResourceProcessable(v any) bool {
+ return resources.ResolveImageOpsSupport(v) >= images.ImageResourceTypeProcessable
}
IsSite: false: {{ reflect.IsSite . }}|true: {{ reflect.IsSite .Site }}|true: {{ reflect.IsSite site }}
IsPage: true: {{ reflect.IsPage . }}|false: {{ reflect.IsPage .Site }}|false: {{ reflect.IsPage site }}
IsResource: true: {{ reflect.IsResource . }}|true: {{ reflect.IsResource $a }}|true: {{ reflect.IsResource $b }}|true: {{ reflect.IsResource $c }}
-IsImageResource: false: {{ reflect.IsImageResource . }}|true: {{ reflect.IsImageResource $a }}|true: {{ reflect.IsImageResource $a10 }}|false: {{ reflect.IsImageResource $b }}|false: {{ reflect.IsImageResource $c }}|false: {{ reflect.IsImageResource $d }}
IsSite: false: false|true: true|true: true
IsPage: true: true|false: false|false: false
IsResource: true: true|true: true|true: true|true: true
-IsImageResource: false: false|true: true|true: true|false: false|false: false|false: false
`)
}