mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 07:18:54 +00:00
Add AVIF, HEIF and HEIC partial support (only metadata for now)
* Add AVIF, HEIF and HEIC partial support * Add them as media types. * Support reading metadata (Width, Height, Exif, etc.) from these formats. * Add a new template function IsImageResourceMeta to check if a resource supports image metadata operations, which will return true for AVIF, HEIF and HEIC resources even if they don't support full image operations yet. Fixes #14549
This commit is contained in:
committed by
GitHub
parent
b7203bbb3a
commit
49bfb1070b
+19
-9
@@ -19,16 +19,10 @@ import (
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -70,7 +64,7 @@ type ReadSeekCloserProvider interface {
|
||||
|
||||
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
type readSeekerNopCloser struct {
|
||||
ReadSeeker
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
// Close does nothing.
|
||||
@@ -79,7 +73,7 @@ func (r readSeekerNopCloser) Close() error {
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
|
||||
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
|
||||
func NewReadSeekerNoOpCloser(r io.ReadSeeker) ReadSeekCloser {
|
||||
return readSeekerNopCloser{r}
|
||||
}
|
||||
|
||||
@@ -111,6 +105,22 @@ func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
|
||||
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 {
|
||||
|
||||
@@ -506,7 +506,7 @@ func (s *IntegrationTestBuilder) ImageHelper(filename string) *IntegrationTestIm
|
||||
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)
|
||||
|
||||
@@ -29,6 +29,9 @@ type BuiltinTypes struct {
|
||||
TIFFType Type
|
||||
BMPType Type
|
||||
WEBPType Type
|
||||
AVIFType Type
|
||||
HEIFType Type
|
||||
HEICType Type
|
||||
|
||||
// Common font types
|
||||
TrueTypeFontType Type
|
||||
@@ -85,6 +88,9 @@ var Builtin = BuiltinTypes{
|
||||
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"},
|
||||
@@ -141,6 +147,9 @@ var defaultMediaTypesConfig = map[string]any{
|
||||
"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"}},
|
||||
|
||||
@@ -151,5 +151,5 @@ func TestDefaultTypes(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
c.Assert(len(DefaultTypes), qt.Equals, 41)
|
||||
c.Assert(len(DefaultTypes), qt.Equals, 44)
|
||||
}
|
||||
|
||||
+31
-26
@@ -25,10 +25,12 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -230,41 +232,44 @@ func (d *Codec) Decode(r io.Reader) (image.Image, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ var (
|
||||
".bmp": BMP,
|
||||
".gif": GIF,
|
||||
".webp": WEBP,
|
||||
".avif": AVIF,
|
||||
".heif": HEIF,
|
||||
".heic": HEIC,
|
||||
}
|
||||
|
||||
// These are the image types we can process.
|
||||
@@ -70,6 +73,13 @@ var (
|
||||
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
|
||||
@@ -125,9 +135,29 @@ func ImageFormatFromExt(ext string) (Format, bool) {
|
||||
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 (
|
||||
|
||||
@@ -94,7 +94,7 @@ func (i Image) WithSpec(s Spec) *Image {
|
||||
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
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func (i *Image) initConfig() error {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
i.config, _, err = i.Proc.Codec.DecodeConfig(f)
|
||||
i.config, _, err = i.Proc.Codec.DecodeConfig(i.Format, f)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -342,8 +342,18 @@ const (
|
||||
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:
|
||||
@@ -354,6 +364,12 @@ func (f Format) ToImageMetaImageFormatFormat() imagemeta.ImageFormat {
|
||||
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
|
||||
}
|
||||
@@ -396,6 +412,12 @@ func (f Format) MediaType() media.Type {
|
||||
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))
|
||||
}
|
||||
@@ -415,6 +437,12 @@ func (f Format) String() string {
|
||||
return "BMP"
|
||||
case WEBP:
|
||||
return "WEBP"
|
||||
case AVIF:
|
||||
return "AVIF"
|
||||
case HEIF:
|
||||
return "HEIF"
|
||||
case HEIC:
|
||||
return "HEIC"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -229,3 +229,74 @@ Home.
|
||||
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|
|
||||
`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,8 +186,9 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro
|
||||
|
||||
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{
|
||||
@@ -204,7 +205,7 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro
|
||||
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
|
||||
}
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -404,18 +404,18 @@ func (r *resourceAdapter) getImageOps() images.ImageResourceOps {
|
||||
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() {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/overlayfs"
|
||||
@@ -94,7 +95,10 @@ func (ns *Namespace) Config(path any) (image.Config, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
+14
-3
@@ -16,6 +16,7 @@ package reflect
|
||||
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"
|
||||
)
|
||||
@@ -56,8 +57,18 @@ func (ns *Namespace) IsSite(v any) bool {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ AVIF.ResourceType: {{ $d.ResourceType }}
|
||||
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 }}
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +62,5 @@ AVIF.ResourceType: image
|
||||
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
|
||||
`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user