mirror of
https://github.com/gohugoio/hugo.git
synced 2025-09-02 22:52:51 +02:00
committed by
Bjørn Erik Pedersen
parent
5d2cbee989
commit
4ea94c451d
@@ -16,11 +16,18 @@ package images
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/overlayfs"
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/resources/images"
|
||||
"github.com/gohugoio/hugo/resources/resource_factories/create"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"rsc.io/qr"
|
||||
|
||||
// Importing image codecs for image.DecodeConfig
|
||||
_ "image/gif"
|
||||
@@ -50,21 +57,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
|
||||
@@ -117,3 +125,77 @@ 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,
|
||||
}
|
||||
|
||||
// QR encodes the given text into a QR code using the specified options,
|
||||
// returning an image resource.
|
||||
func (ns *Namespace) QR(options any) (images.ImageResource, error) {
|
||||
const (
|
||||
qrDefaultErrorCorrectionLevel = "medium"
|
||||
qrDefaultScale = 4
|
||||
)
|
||||
|
||||
opts := struct {
|
||||
Text string // text to encode
|
||||
Level string // error correction level; one of low, medium, quartile, or high
|
||||
Scale int // number of image pixels per QR code module
|
||||
TargetDir string // target directory relative to publishDir
|
||||
}{
|
||||
Level: qrDefaultErrorCorrectionLevel,
|
||||
Scale: qrDefaultScale,
|
||||
}
|
||||
|
||||
err := mapstructure.WeakDecode(options, &opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.Text == "" {
|
||||
return nil, errors.New("cannot encode an empty string")
|
||||
}
|
||||
|
||||
level, ok := qrErrorCorrectionLevels[opts.Level]
|
||||
if !ok {
|
||||
return nil, errors.New("error correction level must be one of low, medium, quartile, or high")
|
||||
}
|
||||
|
||||
if opts.Scale < 2 {
|
||||
return nil, errors.New("scale must be an integer greater than or equal to 2")
|
||||
}
|
||||
|
||||
targetPath := path.Join(opts.TargetDir, fmt.Sprintf("qr_%s.png", hashing.HashString(opts)))
|
||||
|
||||
r, err := ns.createClient.FromOpts(
|
||||
create.Options{
|
||||
TargetPath: targetPath,
|
||||
TargetPathHasHash: true,
|
||||
CreateContent: func() (func() (hugio.ReadSeekCloser, error), error) {
|
||||
code, err := qr.Encode(opts.Text, level)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code.Scale = opts.Scale
|
||||
png := code.PNG()
|
||||
return func() (hugio.ReadSeekCloser, error) {
|
||||
return hugio.NewReadSeekerNoOpCloserFromBytes(png), nil
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ir, ok := r.(images.ImageResource)
|
||||
if !ok {
|
||||
panic("bug: resource is not an image resource")
|
||||
}
|
||||
|
||||
return ir, nil
|
||||
}
|
||||
|
@@ -14,8 +14,10 @@
|
||||
package images_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
@@ -49,3 +51,52 @@ 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 --
|
||||
{{- $text := "https://gohugo.io" }}
|
||||
{{- $optionMaps := slice
|
||||
(dict "text" $text)
|
||||
(dict "text" $text "level" "medium")
|
||||
(dict "text" $text "level" "medium" "scale" 4)
|
||||
(dict "text" $text "level" "low" "scale" 2)
|
||||
(dict "text" $text "level" "medium" "scale" 3)
|
||||
(dict "text" $text "level" "quartile" "scale" 5)
|
||||
(dict "text" $text "level" "high" "scale" 6)
|
||||
(dict "text" $text "level" "high" "scale" 6 "targetDir" "foo/bar")
|
||||
}}
|
||||
{{- range $k, $opts := $optionMaps }}
|
||||
{{- with images.QR $opts }}
|
||||
<img data-id="{{ $k }}" data-img-hash="{{ .Content | hash.XxHash }}" data-level="{{ $opts.level }}" data-scale="{{ $opts.scale }}" data-targetDir="{{ $opts.targetDir }}" src="{{ .RelPermalink }}">
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
b.AssertFileContent("public/index.html",
|
||||
`<img data-id="0" data-img-hash="6ccacf8056c41475" data-level="" data-scale="" data-targetDir="" src="/qr_3891606335523452031.png">`,
|
||||
`<img data-id="1" data-img-hash="6ccacf8056c41475" data-level="medium" data-scale="" data-targetDir="" src="/qr_3891606335523452031.png">`,
|
||||
`<img data-id="2" data-img-hash="6ccacf8056c41475" data-level="medium" data-scale="4" data-targetDir="" src="/qr_3891606335523452031.png">`,
|
||||
`<img data-id="3" data-img-hash="c29338c3d105b156" data-level="low" data-scale="2" data-targetDir="" src="/qr_2532593520456080065.png">`,
|
||||
`<img data-id="4" data-img-hash="8f7a639cea917b0e" data-level="medium" data-scale="3" data-targetDir="" src="/qr_6630115934366238432.png">`,
|
||||
`<img data-id="5" data-img-hash="2d15d6dcb861b5da" data-level="quartile" data-scale="5" data-targetDir="" src="/qr_14167719016579496966.png">`,
|
||||
`<img data-id="6" data-img-hash="113c45f2c091bc4d" data-level="high" data-scale="6" data-targetDir="" src="/qr_1700475212608554630.png">`,
|
||||
`<img data-id="7" data-img-hash="113c45f2c091bc4d" data-level="high" data-scale="6" data-targetDir="foo/bar" src="/foo/bar/qr_12381230670040458986.png">`,
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
Reference in New Issue
Block a user