tpl: Add imageConfig function

Add imageConfig function which calls image.DecodeConfig and returns the height, width and color mode of the image. (#2677)

This allows for more advanced image shortcodes and templates such as those required by AMP.

layouts/shortcodes/amp-img.html
```
{{ $src := .Get "src" }}
{{ $config := imageConfig (printf "/static/%s" $src) }}

<amp-img src="{{$src}}"
           height="{{$config.Height}}"
           width="{{$config.Width}}"
           layout="responsive">
</amp-img>
```
This commit is contained in:
Tristan Rice 2016-11-16 04:00:45 -08:00 committed by Bjørn Erik Pedersen
parent 950034db5c
commit a49f838cd0
4 changed files with 184 additions and 2 deletions

View file

@ -356,7 +356,7 @@ e.g.
{{ .Content }}
{{ end }}
## Files
## Files
### readDir
@ -372,6 +372,16 @@ Reads a file from disk and converts it into a string. Note that the filename mus
`{{readFile "README.txt"}}``"Hugo Rocks!"`
### imageConfig
Parses the image and returns the height, width and color model.
e.g.
```
{{ with (imageConfig "favicon.ico") }}
favicon.ico: {{.Width}} x {{.Height}}
{{ end }}
```
## Math
<table class="table table-bordered">

View file

@ -120,12 +120,14 @@ func (h *HugoSites) getNodes(nodeID string) Nodes {
return Nodes{}
}
// Reset resets the sites, making it ready for a full rebuild.
// Reset resets the sites and template caches, making it ready for a full rebuild.
func (h *HugoSites) reset() {
h.nodeMap = make(map[string]Nodes)
for i, s := range h.Sites {
h.Sites[i] = s.reset()
}
tpl.ResetCaches()
}
func (h *HugoSites) reCreateFromConfig() error {

View file

@ -26,6 +26,7 @@ import (
"fmt"
"html"
"html/template"
"image"
"math/rand"
"net/url"
"os"
@ -45,6 +46,11 @@ import (
"github.com/spf13/hugo/hugofs"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
// Importing image codecs for image.DecodeConfig
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)
var (
@ -364,6 +370,63 @@ func intersect(l1, l2 interface{}) (interface{}, error) {
}
}
// ResetCaches resets all caches that might be used during build.
func ResetCaches() {
resetImageConfigCache()
}
// imageConfigCache is a lockable cache for image.Config objects. It must be
// locked before reading or writing to config.
var imageConfigCache struct {
sync.RWMutex
config map[string]image.Config
}
// resetImageConfigCache initializes and resets the imageConfig cache for the
// imageConfig template function. This should be run once before every batch of
// template renderers so the cache is cleared for new data.
func resetImageConfigCache() {
imageConfigCache.Lock()
defer imageConfigCache.Unlock()
imageConfigCache.config = map[string]image.Config{}
}
// imageConfig returns the image.Config for the specified path relative to the
// working directory. resetImageConfigCache must be run beforehand.
func imageConfig(path interface{}) (image.Config, error) {
filename, err := cast.ToStringE(path)
if err != nil {
return image.Config{}, err
}
if filename == "" {
return image.Config{}, errors.New("imageConfig needs a filename")
}
// Check cache for image config.
imageConfigCache.RLock()
config, ok := imageConfigCache.config[filename]
imageConfigCache.RUnlock()
if ok {
return config, nil
}
f, err := hugofs.WorkingDir().Open(filename)
if err != nil {
return image.Config{}, err
}
config, _, err = image.DecodeConfig(f)
imageConfigCache.Lock()
imageConfigCache.config[filename] = config
imageConfigCache.Unlock()
return config, err
}
// in returns whether v is in the set l. l may be an array or slice.
func in(l interface{}, v interface{}) bool {
lv := reflect.ValueOf(l)
@ -1991,6 +2054,7 @@ func initFuncMap() {
"htmlEscape": htmlEscape,
"htmlUnescape": htmlUnescape,
"humanize": humanize,
"imageConfig": imageConfig,
"in": in,
"index": index,
"int": func(v interface{}) (int, error) { return cast.ToIntE(v) },

View file

@ -19,6 +19,9 @@ import (
"errors"
"fmt"
"html/template"
"image"
"image/color"
"image/png"
"math/rand"
"path"
"path/filepath"
@ -596,6 +599,109 @@ func TestDictionary(t *testing.T) {
}
}
func blankImage(width, height int) []byte {
var buf bytes.Buffer
img := image.NewRGBA(image.Rect(0, 0, width, height))
if err := png.Encode(&buf, img); err != nil {
panic(err)
}
return buf.Bytes()
}
func TestImageConfig(t *testing.T) {
viper.Reset()
defer viper.Reset()
workingDir := "/home/hugo"
viper.Set("workingDir", workingDir)
fs := &afero.MemMapFs{}
hugofs.InitFs(fs)
for i, this := range []struct {
resetCache bool
path string
input []byte
expected image.Config
}{
{
resetCache: true,
path: "a.png",
input: blankImage(10, 10),
expected: image.Config{
Width: 10,
Height: 10,
ColorModel: color.NRGBAModel,
},
},
{
resetCache: false,
path: "b.png",
input: blankImage(20, 15),
expected: image.Config{
Width: 20,
Height: 15,
ColorModel: color.NRGBAModel,
},
},
{
resetCache: false,
path: "a.png",
input: blankImage(20, 15),
expected: image.Config{
Width: 10,
Height: 10,
ColorModel: color.NRGBAModel,
},
},
{
resetCache: true,
path: "a.png",
input: blankImage(20, 15),
expected: image.Config{
Width: 20,
Height: 15,
ColorModel: color.NRGBAModel,
},
},
} {
afero.WriteFile(fs, filepath.Join(workingDir, this.path), this.input, 0755)
if this.resetCache {
resetImageConfigCache()
}
result, err := imageConfig(this.path)
if err != nil {
t.Errorf("imageConfig returned error: %s", err)
}
if !reflect.DeepEqual(result, this.expected) {
t.Errorf("[%d] imageConfig: expected '%v', got '%v'", i, this.expected, result)
}
if len(imageConfigCache.config) == 0 {
t.Error("imageConfigCache should have at least 1 item")
}
}
if _, err := imageConfig(t); err == nil {
t.Error("Expected error from imageConfig when passed invalid path")
}
if _, err := imageConfig("non-existant.png"); err == nil {
t.Error("Expected error from imageConfig when passed non-existant file")
}
// test cache clearing
ResetCaches()
if len(imageConfigCache.config) != 0 {
t.Error("ResetCaches should have cleared imageConfigCache")
}
}
func TestIn(t *testing.T) {
for i, this := range []struct {
v1 interface{}