Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tpl: Adds imageConfig function which calls image.DecodeConfig and returns the height, width and color mode of the image. #2677

Merged
merged 1 commit into from Nov 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/content/templates/functions.md
Expand Up @@ -356,7 +356,7 @@ e.g.
{{ .Content }}
{{ end }}

## Files
## Files

### readDir

Expand All @@ -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">
Expand Down
4 changes: 3 additions & 1 deletion hugolib/hugo_sites.go
Expand Up @@ -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 {
Expand Down
64 changes: 64 additions & 0 deletions tpl/template_funcs.go
Expand Up @@ -26,6 +26,7 @@ import (
"fmt"
"html"
"html/template"
"image"
"math/rand"
"net/url"
"os"
Expand All @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) },
Expand Down
106 changes: 106 additions & 0 deletions tpl/template_funcs_test.go
Expand Up @@ -19,6 +19,9 @@ import (
"errors"
"fmt"
"html/template"
"image"
"image/color"
"image/png"
"math/rand"
"path"
"path/filepath"
Expand Down Expand Up @@ -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{}
Expand Down