Skip to content
/ hugo Public
forked from gohugoio/hugo

Commit

Permalink
tpl/images: Add images.QR function
Browse files Browse the repository at this point in the history
  • Loading branch information
jmooring committed Jan 2, 2025
1 parent d913f46 commit 98c9ccc
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 9 deletions.
44 changes: 44 additions & 0 deletions docs/content/en/functions/images/QR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: images.QR
description: Encodes the given text into a QR code, returning an image resource.
categories: []
keywords: []
action:
aliases: []
related: []
returnType: images.ImageResource
signatures: ['images.QR TEXT [LEVEL]']
---

{{< new-in 0.141.0 >}}

The `images.QR` function encodes the given text to a [QR code] using the given error correction level, returning an image resource.

[QR code]: https://en.wikipedia.org/wiki/QR_code

The default error correction level is sufficient for most applications.

Error correction level|Image size (pixels)|Redundancy
:--|:--|:--
low|232x232|20%
medium (default)|264x264|38%
quartile|264x264|55%
high|296x296|65%

To render a QR code with the default error correction level:

```go-html-template
{{ with images.QR "https://gohugo.io" }}
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
{{ end }}
```

To render a QR code with a "high" error correction level:

```go-html-template
{{ with images.QR "https://gohugo.io" "high" }}
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
{{ end }}
```

Always test the rendered QR code after resizing, both on-screen and in print.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ require (
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
rsc.io/qr v0.2.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,8 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
Expand Down
85 changes: 76 additions & 9 deletions tpl/images/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ package images

import (
"errors"
"fmt"
"image"
"sync"

"github.com/bep/overlayfs"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/resource_factories/create"
"rsc.io/qr"

// Importing image codecs for image.DecodeConfig
_ "image/gif"
Expand Down Expand Up @@ -50,21 +54,22 @@ func New(d *deps.Deps) *Namespace {
}

return &Namespace{
readFileFs: readFileFs,
Filters: &images.Filters{},
cache: map[string]image.Config{},
deps: d,
readFileFs: readFileFs,
Filters: &images.Filters{},
cache: map[string]image.Config{},
deps: d,
createClient: create.New(d.ResourceSpec),
}
}

// Namespace provides template functions for the "images" namespace.
type Namespace struct {
*images.Filters
readFileFs afero.Fs
cacheMu sync.RWMutex
cache map[string]image.Config

deps *deps.Deps
readFileFs afero.Fs
cacheMu sync.RWMutex
cache map[string]image.Config
deps *deps.Deps
createClient *create.Client
}

// Config returns the image.Config for the specified path relative to the
Expand Down Expand Up @@ -117,3 +122,65 @@ func (ns *Namespace) Filter(args ...any) (images.ImageResource, error) {

return img.Filter(filtersv...)
}

var (
qrErrorCorrectionLevels = map[string]qr.Level{
"low": qr.L,
"medium": qr.M,
"quartile": qr.Q,
"high": qr.H,
}
qrDefaultErrorCorrectLevel = "medium"
)

// QR Encodes the given text to a QR code using the given error correction
// level, returning an image resource.
func (ns *Namespace) QR(args ...any) (images.ImageResource, error) {
if len(args) == 0 || len(args) > 2 {
return nil, errors.New("requires one or two arguments")
}

text, err := cast.ToStringE(args[0])
if err != nil {
return nil, err
}
if text == "" {
return nil, errors.New("cannot encode an empty string")
}

levels := qrDefaultErrorCorrectLevel
if len(args) == 2 {
levels, err = cast.ToStringE(args[1])
if err != nil {
return nil, err
}
if levels == "" {
levels = qrDefaultErrorCorrectLevel
}
}

level, ok := qrErrorCorrectionLevels[levels]
if !ok {
return nil, errors.New("error correction level must be one of low, medium, quartile, or high")
}

code, err := qr.Encode(text, level)
if err != nil {
return nil, err
}

png := code.PNG()
targetPath := fmt.Sprintf("qr_%v.png", hashing.XxHashFromStringHexEncoded(text+levels))

r, err := ns.createClient.FromString(targetPath, string(png))
if err != nil {
return nil, err
}

ir, ok := r.(images.ImageResource)
if !ok {
return nil, errors.New("bug: resource is not an image resource")
}

return ir, nil
}
34 changes: 34 additions & 0 deletions tpl/images/images_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
package images_test

import (
"strings"
"testing"

qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
)

Expand Down Expand Up @@ -49,3 +51,35 @@ fileExists2 OK: true|
imageConfig2 OK: 1|
`)
}

func TestQR(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
-- layouts/index.html --
{{ $u := "https://gohugo.io" }}
{{ images.QR $u | hash.XxHash }}|
{{ images.QR $u "low" | hash.XxHash }}|
{{ images.QR $u "medium" | hash.XxHash }}|
{{ images.QR $u "quartile" | hash.XxHash }}|
{{ images.QR $u "high" | hash.XxHash}}|
`

b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
"1a9c0fbd502c7d70|\ndd8ef3a47a7f70ed|\n1a9c0fbd502c7d70|\n3c471aab148b1e3c|\n9cb4dc156db3dfdf|",
)

files = strings.ReplaceAll(files, "low", "foo")

b, err := hugolib.TestE(t, files)
b.Assert(err.Error(), qt.Contains, "error correction level must be one of low, medium, quartile, or high")

files = strings.ReplaceAll(files, "foo", "low")
files = strings.ReplaceAll(files, "https://gohugo.io", "")

b, err = hugolib.TestE(t, files)
b.Assert(err.Error(), qt.Contains, "cannot encode an empty string")
}

0 comments on commit 98c9ccc

Please sign in to comment.