shithub: hugo

Download patch

ref: a49f838cd03f89aa9eb61584c510177fa5cbd3bd
parent: 950034db5cca6df75a2cb39c5e2c2e66dc41c534
author: Tristan Rice <[email protected]>
date: Tue Nov 15 23:00:45 EST 2016

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>
```

--- a/docs/content/templates/functions.md
+++ b/docs/content/templates/functions.md
@@ -356,7 +356,7 @@
        {{ .Content }}
     {{ end }}
 
-## Files    
+## Files
 
 ### readDir
 
@@ -371,6 +371,16 @@
  So, if you have a file with the name `README.txt` in the root of your project with the content `Hugo Rocks!`:
 
  `{{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
 
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -120,12 +120,14 @@
 	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 {
--- a/tpl/template_funcs.go
+++ b/tpl/template_funcs.go
@@ -26,6 +26,7 @@
 	"fmt"
 	"html"
 	"html/template"
+	"image"
 	"math/rand"
 	"net/url"
 	"os"
@@ -45,6 +46,11 @@
 	"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 @@
 	}
 }
 
+// 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 @@
 		"htmlEscape":    htmlEscape,
 		"htmlUnescape":  htmlUnescape,
 		"humanize":      humanize,
+		"imageConfig":   imageConfig,
 		"in":            in,
 		"index":         index,
 		"int":           func(v interface{}) (int, error) { return cast.ToIntE(v) },
--- a/tpl/template_funcs_test.go
+++ b/tpl/template_funcs_test.go
@@ -19,6 +19,9 @@
 	"errors"
 	"fmt"
 	"html/template"
+	"image"
+	"image/color"
+	"image/png"
 	"math/rand"
 	"path"
 	"path/filepath"
@@ -593,6 +596,109 @@
 				t.Errorf("[%d] got %v but expected %v", i, r, this.expectedValue)
 			}
 		}
+	}
+}
+
+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")
 	}
 }