diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md index 417e2cdb2..bee82e21f 100644 --- a/docs/content/templates/functions.md +++ b/docs/content/templates/functions.md @@ -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 diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 27add9976..2ae58854b 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -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 { diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go index d9b4be990..7aa90e18b 100644 --- a/tpl/template_funcs.go +++ b/tpl/template_funcs.go @@ -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) }, diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go index 16325a75d..720f04067 100644 --- a/tpl/template_funcs_test.go +++ b/tpl/template_funcs_test.go @@ -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{}