From: Bjørn Erik Pedersen Date: Tue, 16 Dec 2025 18:29:16 +0000 (+0100) Subject: Encode and Decode using the libwebp library via WASM with animation support X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=1b4514e0204f823251af347e6838a8333076737e;p=brevno-suite%2Fhugo Encode and Decode using the libwebp library via WASM with animation support Fixes #10030 Fixes #8500 Fixes #12843 Fixes #8879 Fixes #12842 --- diff --git a/common/himage/image.go b/common/himage/image.go new file mode 100644 index 000000000..d70252e7c --- /dev/null +++ b/common/himage/image.go @@ -0,0 +1,67 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package himage provides some high level image types and interfaces. +package himage + +import "image" + +// AnimatedImage represents an animated image. +// This is currently supported for GIF and WebP images. +type AnimatedImage interface { + image.Image // The first frame. + GetRaw() any // *gif.GIF or *WEBP. + GetLoopCount() int // Number of times to loop the animation. 0 means infinite. + ImageFrames +} + +// ImageFrames provides access to the frames of an animated image. +type ImageFrames interface { + GetFrames() []image.Image + + // Frame durations in milliseconds. + // Note that Gif frame durations are in 100ths of a second, + // so they need to be multiplied by 10 to get milliseconds and vice versa. + GetFrameDurations() []int + + SetFrames(frames []image.Image) + SetWidthHeight(width, height int) +} + +// ImageConfigProvider provides access to the image.Config of an image. +type ImageConfigProvider interface { + GetImageConfig() image.Config +} + +// FrameDurationsToGifDelays converts frame durations in milliseconds to +// GIF delays in 100ths of a second. +func FrameDurationsToGifDelays(frameDurations []int) []int { + delays := make([]int, len(frameDurations)) + for i, fd := range frameDurations { + delays[i] = fd / 10 + if delays[i] == 0 && fd > 0 { + delays[i] = 1 + } + } + return delays +} + +// GifDelaysToFrameDurations converts GIF delays in 100ths of a second to +// frame durations in milliseconds. +func GifDelaysToFrameDurations(delays []int) []int { + frameDurations := make([]int, len(delays)) + for i, d := range delays { + frameDurations[i] = d * 10 + } + return frameDurations +} diff --git a/common/hugio/readers.go b/common/hugio/readers.go index b1db09bac..aba54437a 100644 --- a/common/hugio/readers.go +++ b/common/hugio/readers.go @@ -32,6 +32,30 @@ type ReadSeekCloser interface { io.Closer } +// Sizer provides the size of, typically, a io.Reader. +// As implemented by e.g. os.File and io.SectionReader. +type Sizer interface { + Size() int64 +} + +type SizeReader interface { + io.Reader + Sizer +} + +// ToSizeReader converts the given io.Reader to a SizeReader. +// Note that if r is not a SizeReader, the entire content will be read into memory +func ToSizeReader(r io.Reader) (SizeReader, error) { + if sr, ok := r.(SizeReader); ok { + return sr, nil + } + b, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return bytes.NewReader(b), nil +} + // CloserFunc is an adapter to allow the use of ordinary functions as io.Closers. type CloserFunc func() error diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go index 173e9a66b..810ccc014 100644 --- a/common/hugo/hugo.go +++ b/common/hugo/hugo.go @@ -310,13 +310,12 @@ func GetDependencyList() []string { // GetDependencyListNonGo returns a list of non-Go dependencies. func GetDependencyListNonGo() []string { - var deps []string + deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup. if IsExtended { deps = append( deps, formatDep("github.com/sass/libsass", "3.6.6"), - formatDep("github.com/webmproject/libwebp", "v1.3.2"), ) } diff --git a/common/maps/map.go b/common/maps/map.go index d1db71a88..3d0a15053 100644 --- a/common/maps/map.go +++ b/common/maps/map.go @@ -74,10 +74,10 @@ func (m *Map[K, T]) Set(key K, value T) { } // WithWriteLock executes the given function with a write lock on the map. -func (m *Map[K, T]) WithWriteLock(f func(m map[K]T)) { +func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error { m.mu.Lock() defer m.mu.Unlock() - f(m.m) + return f(m.m) } // SetIfAbsent sets the given key to the given value if the key does not already exist in the map. diff --git a/common/maps/map_test.go b/common/maps/map_test.go index 55246a2f2..de5e980d7 100644 --- a/common/maps/map_test.go +++ b/common/maps/map_test.go @@ -62,8 +62,9 @@ func TestMap(t *testing.T) { c.Assert(found, qt.Equals, true) c.Assert(v, qt.Equals, 300) - m.WithWriteLock(func(m map[string]int) { + m.WithWriteLock(func(m map[string]int) error { m["f"] = 500 + return nil }) v, found = m.Lookup("f") c.Assert(found, qt.Equals, true) diff --git a/config/testconfig/testconfig.go b/config/testconfig/testconfig.go index 8f70e6cb7..596e0515d 100644 --- a/config/testconfig/testconfig.go +++ b/config/testconfig/testconfig.go @@ -22,6 +22,7 @@ import ( "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/internal/warpc" toml "github.com/pelletier/go-toml/v2" "github.com/spf13/afero" ) @@ -60,6 +61,14 @@ func GetTestDeps(fs afero.Fs, cfg config.Provider, beforeInit ...func(*deps.Deps d := &deps.Deps{ Conf: conf, Fs: hugofs.NewFrom(fs, conf.BaseConfig()), + WasmDispatchers: warpc.AllDispatchers( + warpc.Options{ + PoolSize: 1, + }, + warpc.Options{ + PoolSize: 1, + }, + ), } for _, f := range beforeInit { f(d) diff --git a/deps/deps.go b/deps/deps.go index 0d02bbda2..024b22ec8 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -1,3 +1,16 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package deps import ( @@ -252,7 +265,7 @@ func (d *Deps) Init() error { return fmt.Errorf("failed to create file caches from configuration: %w", err) } - resourceSpec, err := resources.NewSpec(d.PathSpec, common, fileCaches, d.MemCache, d.BuildState, d.Log, d, d.ExecHelper, d.BuildClosers, d.BuildState) + resourceSpec, err := resources.NewSpec(d.PathSpec, common, d.WasmDispatchers, fileCaches, d.MemCache, d.BuildState, d.Log, d, d.ExecHelper, d.BuildClosers, d.BuildState) if err != nil { return fmt.Errorf("failed to create resource spec: %w", err) } diff --git a/go.mod b/go.mod index b43b4f794..b57b8b0a6 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/bep/godartsass/v2 v2.5.0 github.com/bep/golibsass v1.2.0 github.com/bep/goportabletext v0.1.0 - github.com/bep/gowebp v0.3.0 github.com/bep/helpers v0.6.0 github.com/bep/imagemeta v0.12.0 github.com/bep/lazycache v0.8.0 @@ -21,6 +20,7 @@ require ( github.com/bep/mclib v1.20400.20402 github.com/bep/overlayfs v0.10.0 github.com/bep/simplecobra v0.6.1 + github.com/bep/textandbinarywriter v0.0.0-20251212174530-cd9f0732f60f github.com/bep/tmc v0.5.1 github.com/bits-and-blooms/bitset v1.24.4 github.com/cespare/xxhash/v2 v2.3.0 diff --git a/go.sum b/go.sum index 6e2cc5fe8..ade625472 100644 --- a/go.sum +++ b/go.sum @@ -158,8 +158,6 @@ github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw= github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg= -github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY= -github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= github.com/bep/helpers v0.6.0 h1:qtqMCK8XPFNM9hp5Ztu9piPjxNNkk8PIyUVjg6v8Bsw= github.com/bep/helpers v0.6.0/go.mod h1:IOZlgx5PM/R/2wgyCatfsgg5qQ6rNZJNDpWGXqDR044= github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k= @@ -174,6 +172,8 @@ github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE= github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro= github.com/bep/simplecobra v0.6.1 h1:ORBAC5CSar99/NPZ5fCthCx/uvlm7ry58wwDsZ23a20= github.com/bep/simplecobra v0.6.1/go.mod h1:hmtjyHv6xwD637ScIRP++0NKkR5szrHuMw5BxMUH66s= +github.com/bep/textandbinarywriter v0.0.0-20251212174530-cd9f0732f60f h1:NzhMpf5eis+w8bTbT1jqVz+gcMEBhcIPA/KRbYvX8+Y= +github.com/bep/textandbinarywriter v0.0.0-20251212174530-cd9f0732f60f/go.mod h1:vTWM9sqhanOWdo2B2NHwDQPuPmD/nCdMKDFPYxd4VKU= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg= @@ -584,7 +584,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/hugolib/content_map_page_assembler.go b/hugolib/content_map_page_assembler.go index e4589aef6..48c16c2a4 100644 --- a/hugolib/content_map_page_assembler.go +++ b/hugolib/content_map_page_assembler.go @@ -282,8 +282,9 @@ func (a *allPagesAssembler) doCreatePages(prefix string, depth int) error { default: // Skip this page. a.droppedPages.WithWriteLock( - func(m map[*Site][]string) { + func(m map[*Site][]string) error { m[site] = append(m[site], s) + return nil }, ) @@ -635,15 +636,16 @@ func (a *allPagesAssembler) doCreatePages(prefix string, depth int) error { continue } t := term{view: viewName, term: v} - a.seenTerms.WithWriteLock(func(m map[term]sitesmatrix.Vectors) { + a.seenTerms.WithWriteLock(func(m map[term]sitesmatrix.Vectors) error { vectors, found := m[t] if !found { m[t] = sitesmatrix.Vectors{ vec: struct{}{}, } - return + return nil } vectors[vec] = struct{}{} + return nil }) } } diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index 163f03665..bacd4ec9d 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "errors" "fmt" + "image" "io" "math/rand" "os" @@ -23,6 +24,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/himage" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" @@ -460,6 +462,63 @@ func (s *IntegrationTestBuilder) AssertNoRenderShortcodesArtifacts() { } } +type IntegrationTestImageHelper struct { + *qt.C + b *IntegrationTestBuilder + + img image.Image + formatName string + congig image.Config +} + +func (h *IntegrationTestImageHelper) AssertFormat(formatName string) *IntegrationTestImageHelper { + h.Assert(h.formatName, qt.Equals, formatName, qt.Commentf("Expected format %q, got %q", formatName, h.formatName)) + return h +} + +func (h *IntegrationTestImageHelper) AssertFrameDurations(expect []int) *IntegrationTestImageHelper { + anim, ok := h.img.(himage.AnimatedImage) + h.Assert(ok, qt.IsTrue, qt.Commentf("Image is not animated")) + h.Assert(anim.GetFrameDurations(), qt.DeepEquals, expect, qt.Commentf("Frame durations do not match")) + return h +} + +func (h *IntegrationTestImageHelper) AssertLoopCount(expect int) *IntegrationTestImageHelper { + anim, ok := h.img.(himage.AnimatedImage) + h.Assert(ok, qt.IsTrue, qt.Commentf("Image is not animated")) + h.Assert(anim.GetLoopCount(), qt.Equals, expect, qt.Commentf("Loop count does not match")) + return h +} + +func (h *IntegrationTestImageHelper) AssertIsAnimated(b bool) *IntegrationTestImageHelper { + _, ok := h.img.(himage.AnimatedImage) + if b { + h.Assert(ok, qt.IsTrue, qt.Commentf("Image is not animated")) + return h + } + h.Assert(ok, qt.IsFalse, qt.Commentf("Image is animated")) + return h +} + +func (s *IntegrationTestBuilder) ImageHelper(filename string) *IntegrationTestImageHelper { + filename = filepath.Clean(filename) + fs := s.fs.WorkingDirReadOnly + b, err := afero.ReadFile(fs, filename) + s.Assert(err, qt.IsNil) + conf, format, err := s.H.ResourceSpec.Imaging.Codec.DecodeConfig(bytes.NewReader(b)) + s.Assert(err, qt.IsNil) + img, err := s.H.ResourceSpec.Imaging.Codec.Decode(bytes.NewReader(b)) + s.Assert(err, qt.IsNil) + + return &IntegrationTestImageHelper{ + C: s.C, + b: s, + img: img, + formatName: format, + congig: conf, + } +} + func (s *IntegrationTestBuilder) AssertPublishDir(matches ...string) { s.AssertFs(s.fs.PublishDir, matches...) } @@ -503,7 +562,9 @@ func (s *IntegrationTestBuilder) printAndCheckFs(fs afero.Fs, path string, w io. if path == "" { path = "." } + var size int64 if !info.IsDir() { + size = info.Size() f, err := fs.Open(path) if err != nil { return fmt.Errorf("error: path %q: %s", path, err) @@ -513,7 +574,7 @@ func (s *IntegrationTestBuilder) printAndCheckFs(fs afero.Fs, path string, w io. var buf [1]byte io.ReadFull(f, buf[:]) } - fmt.Fprintln(w, path, info.IsDir()) + fmt.Fprintf(w, "%06d %s %t\n", size, path, info.IsDir()) return nil }) } diff --git a/hugolib/site.go b/hugolib/site.go index 5a2bcf16e..dd57d61cb 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -240,6 +240,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { } } + compilationCacheDir := filepath.Join(conf.Dirs().CacheDir, "_warpc") + firstSiteDeps := &deps.Deps{ Fs: cfg.Fs, Log: logger, @@ -251,13 +253,21 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { MemCache: memCache, TranslationProvider: i18n.NewTranslationProvider(), WasmDispatchers: warpc.AllDispatchers( + // Katex options. warpc.Options{ - CompilationCacheDir: filepath.Join(conf.Dirs().CacheDir, "_warpc"), + CompilationCacheDir: compilationCacheDir, // Katex is relatively slow. PoolSize: 8, - Infof: logger.InfoCommand("wasm").Logf, - Warnf: logger.WarnCommand("wasm").Logf, + Infof: logger.InfoCommand("katex").Logf, + Warnf: logger.WarnCommand("katex").Logf, + }, + // WebP options. + warpc.Options{ + CompilationCacheDir: compilationCacheDir, + PoolSize: 2, + Infof: logger.InfoCommand("webp").Logf, + Warnf: logger.WarnCommand("webp").Logf, }, ), } diff --git a/internal/warpc/build.sh b/internal/warpc/build.sh deleted file mode 100755 index 82c5f28d0..000000000 --- a/internal/warpc/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -go generate ./gen -javy compile js/greet.bundle.js -d -o wasm/greet.wasm -javy compile js/renderkatex.bundle.js -d -o wasm/renderkatex.wasm -touch warpc_test.go \ No newline at end of file diff --git a/internal/warpc/buildjs.sh b/internal/warpc/buildjs.sh new file mode 100755 index 000000000..79d9a3fd2 --- /dev/null +++ b/internal/warpc/buildjs.sh @@ -0,0 +1,4 @@ +go generate ./genjs +javy compile js/greet.bundle.js -d -o wasm/greet.wasm +javy compile js/renderkatex.bundle.js -d -o wasm/renderkatex.wasm +touch warpc_test.go \ No newline at end of file diff --git a/internal/warpc/buildwebp.sh b/internal/warpc/buildwebp.sh new file mode 100755 index 000000000..c8f2e429f --- /dev/null +++ b/internal/warpc/buildwebp.sh @@ -0,0 +1,4 @@ +pushd genwebp +make || exit 1 +popd +touch webp_integration_test.go \ No newline at end of file diff --git a/internal/warpc/gen/main.go b/internal/warpc/gen/main.go deleted file mode 100644 index d3d6562a9..000000000 --- a/internal/warpc/gen/main.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:generate go run main.go -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "github.com/evanw/esbuild/pkg/api" -) - -var scripts = []string{ - "greet.js", - "renderkatex.js", -} - -func main() { - for _, script := range scripts { - filename := filepath.Join("../js", script) - err := buildJSBundle(filename) - if err != nil { - log.Fatal(err) - } - } -} - -func buildJSBundle(filename string) error { - minify := true - result := api.Build( - api.BuildOptions{ - EntryPoints: []string{filename}, - Bundle: true, - MinifyWhitespace: minify, - MinifyIdentifiers: minify, - MinifySyntax: minify, - Target: api.ES2020, - Outfile: strings.Replace(filename, ".js", ".bundle.js", 1), - SourceRoot: "../js", - }) - - if len(result.Errors) > 0 { - return fmt.Errorf("build failed: %v", result.Errors) - } - if len(result.OutputFiles) != 1 { - return fmt.Errorf("expected 1 output file, got %d", len(result.OutputFiles)) - } - - of := result.OutputFiles[0] - if err := os.WriteFile(filepath.FromSlash(of.Path), of.Contents, 0o644); err != nil { - return fmt.Errorf("write file failed: %v", err) - } - return nil -} diff --git a/internal/warpc/genjs/main.go b/internal/warpc/genjs/main.go new file mode 100644 index 000000000..d3d6562a9 --- /dev/null +++ b/internal/warpc/genjs/main.go @@ -0,0 +1,68 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate go run main.go +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/evanw/esbuild/pkg/api" +) + +var scripts = []string{ + "greet.js", + "renderkatex.js", +} + +func main() { + for _, script := range scripts { + filename := filepath.Join("../js", script) + err := buildJSBundle(filename) + if err != nil { + log.Fatal(err) + } + } +} + +func buildJSBundle(filename string) error { + minify := true + result := api.Build( + api.BuildOptions{ + EntryPoints: []string{filename}, + Bundle: true, + MinifyWhitespace: minify, + MinifyIdentifiers: minify, + MinifySyntax: minify, + Target: api.ES2020, + Outfile: strings.Replace(filename, ".js", ".bundle.js", 1), + SourceRoot: "../js", + }) + + if len(result.Errors) > 0 { + return fmt.Errorf("build failed: %v", result.Errors) + } + if len(result.OutputFiles) != 1 { + return fmt.Errorf("expected 1 output file, got %d", len(result.OutputFiles)) + } + + of := result.OutputFiles[0] + if err := os.WriteFile(filepath.FromSlash(of.Path), of.Contents, 0o644); err != nil { + return fmt.Errorf("write file failed: %v", err) + } + return nil +} diff --git a/internal/warpc/genwebp/.gitignore b/internal/warpc/genwebp/.gitignore new file mode 100644 index 000000000..b8235143f --- /dev/null +++ b/internal/warpc/genwebp/.gitignore @@ -0,0 +1,2 @@ +build/ +libwebp/ \ No newline at end of file diff --git a/internal/warpc/genwebp/Makefile b/internal/warpc/genwebp/Makefile new file mode 100644 index 000000000..badd300cf --- /dev/null +++ b/internal/warpc/genwebp/Makefile @@ -0,0 +1,81 @@ +# The library version to use. +# Note that when updating this, you may also need to update the badge in README.md. +LIBWEBP_VERSION = v1.6.0 + +# The path to the WebAssembly SDK. Can be overridden from the command line. +# e.g., make WASI_SDK_PATH=/path/to/wasi-sdk +# Download releases from https://github.com/WebAssembly/wasi-sdk/releases. +# Note that on MacOS, you may need to add some whitelistings in Privacy & Security > Security. +WASI_SDK_PATH ?= /opt/wasi-sdk + +BUILD_DIR := build +BUILD_WASM := ${BUILD_DIR}/webp.wasm +TARGET_WASM_DIR := ../wasm +LIBWEBP_SRC := libwebp + +# Set the compiler and toolchain for WASI +CC := $(WASI_SDK_PATH)/bin/clang +CMAKE_TOOLCHAIN_FILE := $(WASI_SDK_PATH)/share/cmake/wasi-sdk.cmake + +# Source files for the final WASM binary +C_SOURCES := webp.c deps/parson/parson.c + +# Include paths +C_INCLUDES := -I$(LIBWEBP_SRC)/src -I$(BUILD_DIR)/src -I. + +# Libraries to link against +LD_LIBS := $(BUILD_DIR)/libwebpdemux.a $(BUILD_DIR)/libwebp.a $(BUILD_DIR)/libsharpyuv.a $(BUILD_DIR)/libwebpmux.a + +# Compiler flags for the WASM build +WASM_CFLAGS := \ + -O3 \ + -msimd128 \ + -mexec-model=command \ + -mnontrapping-fptoint \ + -Wl,--export=malloc \ + -Wl,--export=free \ + -Wall + +.PHONY: all clean + +# Default target: build the WebAssembly binary +all: $(LIBWEBP_SRC) $(BUILD_DIR) + @echo ">>> Configuring and building C libraries in $(BUILD_DIR)" + cd $(BUILD_DIR) && \ + cmake $(realpath $(LIBWEBP_SRC)) \ + -DCMAKE_TOOLCHAIN_FILE=$(CMAKE_TOOLCHAIN_FILE) \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=0 \ + -DWEBP_ENABLE_SIMD_DEFAULT=1 \ + -DWEBP_BUILD_EXTRAS=0 \ + -DWEBP_USE_THREAD=0 \ + -DWEBP_BUILD_ANIM_UTILS=0 \ + -DWEBP_BUILD_CWEBP=0 \ + -DWEBP_BUILD_DWEBP=0 \ + -DWEBP_BUILD_IMG2WEBP=0 \ + -DWEBP_BUILD_WEBPINFO=0 \ + -DWEBP_BUILD_WEBPMUX=1 && \ + make VERBOSE=1 + @echo ">>> Building WebAssembly binary: $(BUILD_WASM)" + $(CC) $(WASM_CFLAGS) --sysroot=$(WASI_SDK_PATH)/share/wasi-sysroot $(C_INCLUDES) -o $(BUILD_WASM) $(C_SOURCES) $(LD_LIBS) + @echo ">>> Moving the WebAssembly binary: $(TARGET_WASM_DIR)" + mv $(BUILD_WASM) $(TARGET_WASM_DIR) + + + +# Rule to create the build directory +$(BUILD_DIR): + @echo ">>> Creating build directory: $(BUILD_DIR)" + @mkdir -p $@ + +# Rule to clone the libwebp source code +$(LIBWEBP_SRC): + @echo ">>> Cloning libwebp source code (version: $(LIBWEBP_VERSION))" + git clone -b $(LIBWEBP_VERSION) --depth 1 --recursive https://github.com/webmproject/libwebp + test -d $@ + +# Rule to clean up build artifacts +clean: + @echo ">>> Cleaning build artifacts" + @rm -rf $(BUILD_DIR) + @rm -rf $(LIBWEBP_SRC) diff --git a/internal/warpc/genwebp/deps/clib/Makefile b/internal/warpc/genwebp/deps/clib/Makefile new file mode 100644 index 000000000..36f88edfb --- /dev/null +++ b/internal/warpc/genwebp/deps/clib/Makefile @@ -0,0 +1,97 @@ +CC ?= cc +PREFIX ?= /usr/local + +BINS = clib clib-install clib-search clib-init clib-configure clib-build clib-update clib-upgrade clib-uninstall + +ifdef EXE + BINS := $(addsuffix .exe,$(BINS)) +endif + +CP = cp -f +RM = rm -f +MKDIR = mkdir -p +INSTALL = install + +SRC = $(wildcard src/*.c) +COMMON_SRC = $(wildcard src/common/*.c) +ALL_SRC = $(wildcard src/*.c src/*.h src/common/*.c src/common/*.h test/package/*.c test/cache/*.c) +SDEPS = $(wildcard deps/*/*.c) +ODEPS = $(SDEPS:.c=.o) +DEPS = $(filter-out $(ODEPS), $(SDEPS)) +OBJS = $(DEPS:.c=.o) +MAKEFILES = $(wildcard deps/*/Makefile) +HEADERS_BINS = src/common/*.h src/version.h deps/logger/logger.h + +export CC + +CFLAGS += -std=c99 -Ideps -Wall -Wno-unused-function -U__STRICT_ANSI__ + +ifdef STATIC + CFLAGS += -DCURL_STATICLIB $(shell deps/curl/bin/curl-config --cflags) + LDFLAGS += -static $(shell deps/curl/bin/curl-config --static-libs) +else + CFLAGS += $(shell curl-config --cflags) + LDFLAGS += $(shell curl-config --libs) +endif + +ifneq (0,$(PTHREADS)) +ifndef NO_PTHREADS + CFLAGS += $(shell ./scripts/feature-test-pthreads && echo "-DHAVE_PTHREADS=1 -pthread") +endif +endif + +ifdef DEBUG + CFLAGS += -g -D CLIB_DEBUG=1 -D DEBUG="$(DEBUG)" +endif + +default: all + +all: $(BINS) + +build: $(BINS) + +$(BINS): $(SRC) $(COMMON_SRC) $(MAKEFILES) $(OBJS) $(HEADERS_BINS) + $(CC) $(CFLAGS) -o $@ $(COMMON_SRC) src/$(@:.exe=).c $(OBJS) $(LDFLAGS) + +$(MAKEFILES): + $(MAKE) -C $@ + +%.o: %.c + $(CC) $< -c -o $@ $(CFLAGS) -MMD + +clean: + $(foreach c, $(BINS), $(RM) $(c);) + $(RM) $(OBJS) + $(RM) $(AUTODEPS) + cd test/cache && make clean + cd test/package && make clean + +install: $(BINS) + $(MKDIR) $(PREFIX)/bin + $(foreach c, $(BINS), $(INSTALL) $(c) $(PREFIX)/bin/$(c);) + +uninstall: + $(foreach c, $(BINS), $(RM) $(PREFIX)/bin/$(c);) + +test: $(BINS) + @./test.sh + +# create a list of auto dependencies +AUTODEPS:= $(patsubst %.c,%.d, $(DEPS)) $(patsubst %.c,%.d, $(SRC)) + +# include by auto dependencies +-include $(AUTODEPS) + +# Format all source files in the repository. +fmt: + @if ! command -v clang-format &> /dev/null; then \ + echo "clang-format not found"; \ + exit; \ + fi + clang-format -i -style=LLVM $(ALL_SRC) + +# Install the commit hook. +commit-hook: scripts/pre-commit-hook.sh + cp -f scripts/pre-commit-hook.sh .git/hooks/pre-commit + +.PHONY: test all clean install uninstall fmt diff --git a/internal/warpc/genwebp/deps/parson/package.json b/internal/warpc/genwebp/deps/parson/package.json new file mode 100644 index 000000000..ee9265ce0 --- /dev/null +++ b/internal/warpc/genwebp/deps/parson/package.json @@ -0,0 +1,12 @@ +{ + "name": "parson", + "version": "1.0.2", + "repo": "clibs/parson", + "description": "Small json parser and reader", + "keywords": [ "json", "parser" ], + "license": "MIT", + "src": [ + "parson.c", + "parson.h" + ] +} diff --git a/internal/warpc/genwebp/deps/parson/parson.c b/internal/warpc/genwebp/deps/parson/parson.c new file mode 100644 index 000000000..bd5848247 --- /dev/null +++ b/internal/warpc/genwebp/deps/parson/parson.c @@ -0,0 +1,1759 @@ +/* + Parson ( http://kgabis.github.com/parson/ ) + Copyright (c) 2012 - 2015 Krzysztof Gabis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "parson.h" + +#include +#include +#include +#include +#include + +#define STARTING_CAPACITY 15 +#define ARRAY_MAX_CAPACITY 122880 /* 15*(2^13) */ +#define OBJECT_MAX_CAPACITY 960 /* 15*(2^6) */ +#define MAX_NESTING 19 +#define DOUBLE_SERIALIZATION_FORMAT "%f" + +#define SIZEOF_TOKEN(a) (sizeof(a) - 1) +#define SKIP_CHAR(str) ((*str)++) +#define SKIP_WHITESPACES(str) while (isspace(**str)) { SKIP_CHAR(str); } +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +#undef malloc +#undef free + +static JSON_Malloc_Function parson_malloc = malloc; +static JSON_Free_Function parson_free = free; + +#define IS_CONT(b) (((unsigned char)(b) & 0xC0) == 0x80) /* is utf-8 continuation byte */ + +/* Type definitions */ +typedef union json_value_value { + char *string; + double number; + JSON_Object *object; + JSON_Array *array; + int boolean; + int null; +} JSON_Value_Value; + +struct json_value_t { + JSON_Value_Type type; + JSON_Value_Value value; +}; + +struct json_object_t { + char **names; + JSON_Value **values; + size_t count; + size_t capacity; +}; + +struct json_array_t { + JSON_Value **items; + size_t count; + size_t capacity; +}; + +/* Various */ +static char * read_file(const char *filename); +static void remove_comments(char *string, const char *start_token, const char *end_token); +static char * parson_strndup(const char *string, size_t n); +static char * parson_strdup(const char *string); +static int is_utf16_hex(const unsigned char *string); +static int num_bytes_in_utf8_sequence(unsigned char c); +static int verify_utf8_sequence(const unsigned char *string, int *len); +static int is_valid_utf8(const char *string, size_t string_len); +static int is_decimal(const char *string, size_t length); + +/* JSON Object */ +static JSON_Object * json_object_init(void); +static JSON_Status json_object_add(JSON_Object *object, const char *name, JSON_Value *value); +static JSON_Status json_object_resize(JSON_Object *object, size_t new_capacity); +static JSON_Value * json_object_nget_value(const JSON_Object *object, const char *name, size_t n); +static void json_object_free(JSON_Object *object); + +/* JSON Array */ +static JSON_Array * json_array_init(void); +static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value); +static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity); +static void json_array_free(JSON_Array *array); + +/* JSON Value */ +static JSON_Value * json_value_init_string_no_copy(char *string); + +/* Parser */ +static void skip_quotes(const char **string); +static int parse_utf_16(const char **unprocessed, char **processed); +static char * process_string(const char *input, size_t len); +static char * get_quoted_string(const char **string); +static JSON_Value * parse_object_value(const char **string, size_t nesting); +static JSON_Value * parse_array_value(const char **string, size_t nesting); +static JSON_Value * parse_string_value(const char **string); +static JSON_Value * parse_boolean_value(const char **string); +static JSON_Value * parse_number_value(const char **string); +static JSON_Value * parse_null_value(const char **string); +static JSON_Value * parse_value(const char **string, size_t nesting); + +/* Serialization */ +static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, int is_pretty, char *num_buf); +static int json_serialize_string(const char *string, char *buf); +static int append_indent(char *buf, int level); +static int append_string(char *buf, const char *string); + +/* Various */ +static char * parson_strndup(const char *string, size_t n) { + char *output_string = (char*)parson_malloc(n + 1); + if (!output_string) + return NULL; + output_string[n] = '\0'; + strncpy(output_string, string, n); + return output_string; +} + +static char * parson_strdup(const char *string) { + return parson_strndup(string, strlen(string)); +} + +static int is_utf16_hex(const unsigned char *s) { + return isxdigit(s[0]) && isxdigit(s[1]) && isxdigit(s[2]) && isxdigit(s[3]); +} + +static int num_bytes_in_utf8_sequence(unsigned char c) { + if (c == 0xC0 || c == 0xC1 || c > 0xF4 || IS_CONT(c)) { + return 0; + } else if ((c & 0x80) == 0) { /* 0xxxxxxx */ + return 1; + } else if ((c & 0xE0) == 0xC0) { /* 110xxxxx */ + return 2; + } else if ((c & 0xF0) == 0xE0) { /* 1110xxxx */ + return 3; + } else if ((c & 0xF8) == 0xF0) { /* 11110xxx */ + return 4; + } + return 0; /* won't happen */ +} + +static int verify_utf8_sequence(const unsigned char *string, int *len) { + unsigned int cp = 0; + *len = num_bytes_in_utf8_sequence(string[0]); + + if (*len == 1) { + cp = string[0]; + } else if (*len == 2 && IS_CONT(string[1])) { + cp = string[0] & 0x1F; + cp = (cp << 6) | (string[1] & 0x3F); + } else if (*len == 3 && IS_CONT(string[1]) && IS_CONT(string[2])) { + cp = ((unsigned char)string[0]) & 0xF; + cp = (cp << 6) | (string[1] & 0x3F); + cp = (cp << 6) | (string[2] & 0x3F); + } else if (*len == 4 && IS_CONT(string[1]) && IS_CONT(string[2]) && IS_CONT(string[3])) { + cp = string[0] & 0x7; + cp = (cp << 6) | (string[1] & 0x3F); + cp = (cp << 6) | (string[2] & 0x3F); + cp = (cp << 6) | (string[3] & 0x3F); + } else { + return 0; + } + + /* overlong encodings */ + if ((cp < 0x80 && *len > 1) || + (cp < 0x800 && *len > 2) || + (cp < 0x10000 && *len > 3)) { + return 0; + } + + /* invalid unicode */ + if (cp > 0x10FFFF) { + return 0; + } + + /* surrogate halves */ + if (cp >= 0xD800 && cp <= 0xDFFF) { + return 0; + } + + return 1; +} + +static int is_valid_utf8(const char *string, size_t string_len) { + int len = 0; + const char *string_end = string + string_len; + while (string < string_end) { + if (!verify_utf8_sequence((const unsigned char*)string, &len)) { + return 0; + } + string += len; + } + return 1; +} + +static int is_decimal(const char *string, size_t length) { + if (length > 1 && string[0] == '0' && string[1] != '.') + return 0; + if (length > 2 && !strncmp(string, "-0", 2) && string[2] != '.') + return 0; + while (length--) + if (strchr("xX", string[length])) + return 0; + return 1; +} + +static char * read_file(const char * filename) { + FILE *fp = fopen(filename, "r"); + size_t file_size; + long pos; + char *file_contents; + if (!fp) + return NULL; + fseek(fp, 0L, SEEK_END); + pos = ftell(fp); + if (pos < 0) { + fclose(fp); + return NULL; + } + file_size = pos; + rewind(fp); + file_contents = (char*)parson_malloc(sizeof(char) * (file_size + 1)); + if (!file_contents) { + fclose(fp); + return NULL; + } + if (fread(file_contents, file_size, 1, fp) < 1) { + if (ferror(fp)) { + fclose(fp); + parson_free(file_contents); + return NULL; + } + } + fclose(fp); + file_contents[file_size] = '\0'; + return file_contents; +} + +static void remove_comments(char *string, const char *start_token, const char *end_token) { + int in_string = 0, escaped = 0; + size_t i; + char *ptr = NULL, current_char; + size_t start_token_len = strlen(start_token); + size_t end_token_len = strlen(end_token); + if (start_token_len == 0 || end_token_len == 0) + return; + while ((current_char = *string) != '\0') { + if (current_char == '\\' && !escaped) { + escaped = 1; + string++; + continue; + } else if (current_char == '\"' && !escaped) { + in_string = !in_string; + } else if (!in_string && strncmp(string, start_token, start_token_len) == 0) { + for(i = 0; i < start_token_len; i++) + string[i] = ' '; + string = string + start_token_len; + ptr = strstr(string, end_token); + if (!ptr) + return; + for (i = 0; i < (ptr - string) + end_token_len; i++) + string[i] = ' '; + string = ptr + end_token_len - 1; + } + escaped = 0; + string++; + } +} + +/* JSON Object */ +static JSON_Object * json_object_init(void) { + JSON_Object *new_obj = (JSON_Object*)parson_malloc(sizeof(JSON_Object)); + if (!new_obj) + return NULL; + new_obj->names = (char**)NULL; + new_obj->values = (JSON_Value**)NULL; + new_obj->capacity = 0; + new_obj->count = 0; + return new_obj; +} + +static JSON_Status json_object_add(JSON_Object *object, const char *name, JSON_Value *value) { + size_t index = 0; + if (object == NULL || name == NULL || value == NULL) { + return JSONFailure; + } + if (object->count >= object->capacity) { + size_t new_capacity = MAX(object->capacity * 2, STARTING_CAPACITY); + if (new_capacity > OBJECT_MAX_CAPACITY) + return JSONFailure; + if (json_object_resize(object, new_capacity) == JSONFailure) + return JSONFailure; + } + if (json_object_get_value(object, name) != NULL) + return JSONFailure; + index = object->count; + object->names[index] = parson_strdup(name); + if (object->names[index] == NULL) + return JSONFailure; + object->values[index] = value; + object->count++; + return JSONSuccess; +} + +static JSON_Status json_object_resize(JSON_Object *object, size_t new_capacity) { + char **temp_names = NULL; + JSON_Value **temp_values = NULL; + + if ((object->names == NULL && object->values != NULL) || + (object->names != NULL && object->values == NULL) || + new_capacity == 0) { + return JSONFailure; /* Shouldn't happen */ + } + + temp_names = (char**)parson_malloc(new_capacity * sizeof(char*)); + if (temp_names == NULL) + return JSONFailure; + + temp_values = (JSON_Value**)parson_malloc(new_capacity * sizeof(JSON_Value*)); + if (temp_names == NULL) { + parson_free(temp_names); + return JSONFailure; + } + + if (object->names != NULL && object->values != NULL && object->count > 0) { + memcpy(temp_names, object->names, object->count * sizeof(char*)); + memcpy(temp_values, object->values, object->count * sizeof(JSON_Value*)); + } + parson_free(object->names); + parson_free(object->values); + object->names = temp_names; + object->values = temp_values; + object->capacity = new_capacity; + return JSONSuccess; +} + +static JSON_Value * json_object_nget_value(const JSON_Object *object, const char *name, size_t n) { + size_t i, name_length; + for (i = 0; i < json_object_get_count(object); i++) { + name_length = strlen(object->names[i]); + if (name_length != n) + continue; + if (strncmp(object->names[i], name, n) == 0) + return object->values[i]; + } + return NULL; +} + +static void json_object_free(JSON_Object *object) { + while(object->count--) { + parson_free(object->names[object->count]); + json_value_free(object->values[object->count]); + } + parson_free(object->names); + parson_free(object->values); + parson_free(object); +} + +/* JSON Array */ +static JSON_Array * json_array_init(void) { + JSON_Array *new_array = (JSON_Array*)parson_malloc(sizeof(JSON_Array)); + if (!new_array) + return NULL; + new_array->items = (JSON_Value**)NULL; + new_array->capacity = 0; + new_array->count = 0; + return new_array; +} + +static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value) { + if (array->count >= array->capacity) { + size_t new_capacity = MAX(array->capacity * 2, STARTING_CAPACITY); + if (new_capacity > ARRAY_MAX_CAPACITY) + return JSONFailure; + if (json_array_resize(array, new_capacity) == JSONFailure) + return JSONFailure; + } + array->items[array->count] = value; + array->count++; + return JSONSuccess; +} + +static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity) { + JSON_Value **new_items = NULL; + if (new_capacity == 0) { + return JSONFailure; + } + new_items = (JSON_Value**)parson_malloc(new_capacity * sizeof(JSON_Value*)); + if (new_items == NULL) { + return JSONFailure; + } + if (array->items != NULL && array->count > 0) { + memcpy(new_items, array->items, array->count * sizeof(JSON_Value*)); + } + parson_free(array->items); + array->items = new_items; + array->capacity = new_capacity; + return JSONSuccess; +} + +static void json_array_free(JSON_Array *array) { + while (array->count--) + json_value_free(array->items[array->count]); + parson_free(array->items); + parson_free(array); +} + +/* JSON Value */ +static JSON_Value * json_value_init_string_no_copy(char *string) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) + return NULL; + new_value->type = JSONString; + new_value->value.string = string; + return new_value; +} + +/* Parser */ +static void skip_quotes(const char **string) { + SKIP_CHAR(string); + while (**string != '\"') { + if (**string == '\0') + return; + if (**string == '\\') { + SKIP_CHAR(string); + if (**string == '\0') + return; + } + SKIP_CHAR(string); + } + SKIP_CHAR(string); +} + +static int parse_utf_16(const char **unprocessed, char **processed) { + unsigned int cp, lead, trail; + char *processed_ptr = *processed; + const char *unprocessed_ptr = *unprocessed; + unprocessed_ptr++; /* skips u */ + if (!is_utf16_hex((const unsigned char*)unprocessed_ptr) || sscanf(unprocessed_ptr, "%4x", &cp) == EOF) + return JSONFailure; + if (cp < 0x80) { + *processed_ptr = cp; /* 0xxxxxxx */ + } else if (cp < 0x800) { + *processed_ptr++ = ((cp >> 6) & 0x1F) | 0xC0; /* 110xxxxx */ + *processed_ptr = ((cp ) & 0x3F) | 0x80; /* 10xxxxxx */ + } else if (cp < 0xD800 || cp > 0xDFFF) { + *processed_ptr++ = ((cp >> 12) & 0x0F) | 0xE0; /* 1110xxxx */ + *processed_ptr++ = ((cp >> 6) & 0x3F) | 0x80; /* 10xxxxxx */ + *processed_ptr = ((cp ) & 0x3F) | 0x80; /* 10xxxxxx */ + } else if (cp >= 0xD800 && cp <= 0xDBFF) { /* lead surrogate (0xD800..0xDBFF) */ + lead = cp; + unprocessed_ptr += 4; /* should always be within the buffer, otherwise previous sscanf would fail */ + if (*unprocessed_ptr++ != '\\' || *unprocessed_ptr++ != 'u' || /* starts with \u? */ + !is_utf16_hex((const unsigned char*)unprocessed_ptr) || + sscanf(unprocessed_ptr, "%4x", &trail) == EOF || + trail < 0xDC00 || trail > 0xDFFF) { /* valid trail surrogate? (0xDC00..0xDFFF) */ + return JSONFailure; + } + cp = ((((lead-0xD800)&0x3FF)<<10)|((trail-0xDC00)&0x3FF))+0x010000; + *processed_ptr++ = (((cp >> 18) & 0x07) | 0xF0); /* 11110xxx */ + *processed_ptr++ = (((cp >> 12) & 0x3F) | 0x80); /* 10xxxxxx */ + *processed_ptr++ = (((cp >> 6) & 0x3F) | 0x80); /* 10xxxxxx */ + *processed_ptr = (((cp ) & 0x3F) | 0x80); /* 10xxxxxx */ + } else { /* trail surrogate before lead surrogate */ + return JSONFailure; + } + unprocessed_ptr += 3; + *processed = processed_ptr; + *unprocessed = unprocessed_ptr; + return JSONSuccess; +} + + +/* Copies and processes passed string up to supplied length. +Example: "\u006Corem ipsum" -> lorem ipsum */ +static char* process_string(const char *input, size_t len) { + const char *input_ptr = input; + size_t initial_size = (len + 1) * sizeof(char); + size_t final_size = 0; + char *output = (char*)parson_malloc(initial_size); + char *output_ptr = output; + char *resized_output = NULL; + while ((*input_ptr != '\0') && (size_t)(input_ptr - input) < len) { + if (*input_ptr == '\\') { + input_ptr++; + switch (*input_ptr) { + case '\"': *output_ptr = '\"'; break; + case '\\': *output_ptr = '\\'; break; + case '/': *output_ptr = '/'; break; + case 'b': *output_ptr = '\b'; break; + case 'f': *output_ptr = '\f'; break; + case 'n': *output_ptr = '\n'; break; + case 'r': *output_ptr = '\r'; break; + case 't': *output_ptr = '\t'; break; + case 'u': + if (parse_utf_16(&input_ptr, &output_ptr) == JSONFailure) + goto error; + break; + default: + goto error; + } + } else if ((unsigned char)*input_ptr < 0x20) { + goto error; /* 0x00-0x19 are invalid characters for json string (http://www.ietf.org/rfc/rfc4627.txt) */ + } else { + *output_ptr = *input_ptr; + } + output_ptr++; + input_ptr++; + } + *output_ptr = '\0'; + /* resize to new length */ + final_size = (size_t)(output_ptr-output) + 1; + resized_output = (char*)parson_malloc(final_size); + if (resized_output == NULL) + goto error; + memcpy(resized_output, output, final_size); + parson_free(output); + return resized_output; +error: + parson_free(output); + return NULL; +} + +/* Return processed contents of a string between quotes and + skips passed argument to a matching quote. */ +static char * get_quoted_string(const char **string) { + const char *string_start = *string; + size_t string_len = 0; + skip_quotes(string); + if (**string == '\0') + return NULL; + string_len = *string - string_start - 2; /* length without quotes */ + return process_string(string_start + 1, string_len); +} + +static JSON_Value * parse_value(const char **string, size_t nesting) { + if (nesting > MAX_NESTING) + return NULL; + SKIP_WHITESPACES(string); + switch (**string) { + case '{': + return parse_object_value(string, nesting + 1); + case '[': + return parse_array_value(string, nesting + 1); + case '\"': + return parse_string_value(string); + case 'f': case 't': + return parse_boolean_value(string); + case '-': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return parse_number_value(string); + case 'n': + return parse_null_value(string); + default: + return NULL; + } +} + +static JSON_Value * parse_object_value(const char **string, size_t nesting) { + JSON_Value *output_value = json_value_init_object(), *new_value = NULL; + JSON_Object *output_object = json_value_get_object(output_value); + char *new_key = NULL; + if (output_value == NULL) + return NULL; + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + if (**string == '}') { /* empty object */ + SKIP_CHAR(string); + return output_value; + } + while (**string != '\0') { + new_key = get_quoted_string(string); + SKIP_WHITESPACES(string); + if (new_key == NULL || **string != ':') { + json_value_free(output_value); + return NULL; + } + SKIP_CHAR(string); + new_value = parse_value(string, nesting); + if (new_value == NULL) { + parson_free(new_key); + json_value_free(output_value); + return NULL; + } + if(json_object_add(output_object, new_key, new_value) == JSONFailure) { + parson_free(new_key); + parson_free(new_value); + json_value_free(output_value); + return NULL; + } + parson_free(new_key); + SKIP_WHITESPACES(string); + if (**string != ',') + break; + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + } + SKIP_WHITESPACES(string); + if (**string != '}' || /* Trim object after parsing is over */ + json_object_resize(output_object, json_object_get_count(output_object)) == JSONFailure) { + json_value_free(output_value); + return NULL; + } + SKIP_CHAR(string); + return output_value; +} + +static JSON_Value * parse_array_value(const char **string, size_t nesting) { + JSON_Value *output_value = json_value_init_array(), *new_array_value = NULL; + JSON_Array *output_array = json_value_get_array(output_value); + if (!output_value) + return NULL; + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + if (**string == ']') { /* empty array */ + SKIP_CHAR(string); + return output_value; + } + while (**string != '\0') { + new_array_value = parse_value(string, nesting); + if (!new_array_value) { + json_value_free(output_value); + return NULL; + } + if(json_array_add(output_array, new_array_value) == JSONFailure) { + parson_free(new_array_value); + json_value_free(output_value); + return NULL; + } + SKIP_WHITESPACES(string); + if (**string != ',') + break; + SKIP_CHAR(string); + SKIP_WHITESPACES(string); + } + SKIP_WHITESPACES(string); + if (**string != ']' || /* Trim array after parsing is over */ + json_array_resize(output_array, json_array_get_count(output_array)) == JSONFailure) { + json_value_free(output_value); + return NULL; + } + SKIP_CHAR(string); + return output_value; +} + +static JSON_Value * parse_string_value(const char **string) { + JSON_Value *value = NULL; + char *new_string = get_quoted_string(string); + if (new_string == NULL) + return NULL; + value = json_value_init_string_no_copy(new_string); + if (value == NULL) { + parson_free(new_string); + return NULL; + } + return value; +} + +static JSON_Value * parse_boolean_value(const char **string) { + size_t true_token_size = SIZEOF_TOKEN("true"); + size_t false_token_size = SIZEOF_TOKEN("false"); + if (strncmp("true", *string, true_token_size) == 0) { + *string += true_token_size; + return json_value_init_boolean(1); + } else if (strncmp("false", *string, false_token_size) == 0) { + *string += false_token_size; + return json_value_init_boolean(0); + } + return NULL; +} + +static JSON_Value * parse_number_value(const char **string) { + char *end; + double number = strtod(*string, &end); + JSON_Value *output_value; + if (is_decimal(*string, end - *string)) { + *string = end; + output_value = json_value_init_number(number); + } else { + output_value = NULL; + } + return output_value; +} + +static JSON_Value * parse_null_value(const char **string) { + size_t token_size = SIZEOF_TOKEN("null"); + if (strncmp("null", *string, token_size) == 0) { + *string += token_size; + return json_value_init_null(); + } + return NULL; +} + +/* Serialization */ +#define APPEND_STRING(str) do { written = append_string(buf, (str)); \ + if (written < 0) { return -1; } \ + if (buf != NULL) { buf += written; } \ + written_total += written; } while(0) + +#define APPEND_INDENT(level) do { written = append_indent(buf, (level)); \ + if (written < 0) { return -1; } \ + if (buf != NULL) { buf += written; } \ + written_total += written; } while(0) + +static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, int is_pretty, char *num_buf) +{ + const char *key = NULL, *string = NULL; + JSON_Value *temp_value = NULL; + JSON_Array *array = NULL; + JSON_Object *object = NULL; + size_t i = 0, count = 0; + double num = 0.0; + int written = -1, written_total = 0; + + switch (json_value_get_type(value)) { + case JSONArray: + array = json_value_get_array(value); + count = json_array_get_count(array); + APPEND_STRING("["); + if (count > 0 && is_pretty) + APPEND_STRING("\n"); + for (i = 0; i < count; i++) { + if (is_pretty) + APPEND_INDENT(level+1); + temp_value = json_array_get_value(array, i); + written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf); + if (written < 0) + return -1; + if (buf != NULL) + buf += written; + written_total += written; + if (i < (count - 1)) + APPEND_STRING(","); + if (is_pretty) + APPEND_STRING("\n"); + } + if (count > 0 && is_pretty) + APPEND_INDENT(level); + APPEND_STRING("]"); + return written_total; + case JSONObject: + object = json_value_get_object(value); + count = json_object_get_count(object); + APPEND_STRING("{"); + if (count > 0 && is_pretty) + APPEND_STRING("\n"); + for (i = 0; i < count; i++) { + key = json_object_get_name(object, i); + if (is_pretty) + APPEND_INDENT(level+1); + written = json_serialize_string(key, buf); + if (written < 0) + return -1; + if (buf != NULL) + buf += written; + written_total += written; + APPEND_STRING(":"); + if (is_pretty) + APPEND_STRING(" "); + temp_value = json_object_get_value(object, key); + written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf); + if (written < 0) + return -1; + if (buf != NULL) + buf += written; + written_total += written; + if (i < (count - 1)) + APPEND_STRING(","); + if (is_pretty) + APPEND_STRING("\n"); + } + if (count > 0 && is_pretty) + APPEND_INDENT(level); + APPEND_STRING("}"); + return written_total; + case JSONString: + string = json_value_get_string(value); + written = json_serialize_string(string, buf); + if (written < 0) + return -1; + if (buf != NULL) + buf += written; + written_total += written; + return written_total; + case JSONBoolean: + if (json_value_get_boolean(value)) + APPEND_STRING("true"); + else + APPEND_STRING("false"); + return written_total; + case JSONNumber: + num = json_value_get_number(value); + if (buf != NULL) + num_buf = buf; + if (num == ((double)(int)num)) /* check if num is integer */ + written = sprintf(num_buf, "%d", (int)num); + else + written = sprintf(num_buf, DOUBLE_SERIALIZATION_FORMAT, num); + if (written < 0) + return -1; + if (buf != NULL) + buf += written; + written_total += written; + return written_total; + case JSONNull: + APPEND_STRING("null"); + return written_total; + case JSONError: + return -1; + default: + return -1; + } +} + +static int json_serialize_string(const char *string, char *buf) { + size_t i = 0, len = strlen(string); + char c = '\0'; + int written = -1, written_total = 0; + APPEND_STRING("\""); + for (i = 0; i < len; i++) { + c = string[i]; + switch (c) { + case '\"': APPEND_STRING("\\\""); break; + case '\\': APPEND_STRING("\\\\"); break; + case '\b': APPEND_STRING("\\b"); break; + case '\f': APPEND_STRING("\\f"); break; + case '\n': APPEND_STRING("\\n"); break; + case '\r': APPEND_STRING("\\r"); break; + case '\t': APPEND_STRING("\\t"); break; + default: + if (buf != NULL) { + buf[0] = c; + buf += 1; + } + written_total += 1; + break; + } + } + APPEND_STRING("\""); + return written_total; +} + +static int append_indent(char *buf, int level) { + int i; + int written = -1, written_total = 0; + for (i = 0; i < level; i++) { + APPEND_STRING(" "); + } + return written_total; +} + +static int append_string(char *buf, const char *string) { + if (buf == NULL) { + return (int)strlen(string); + } + return sprintf(buf, "%s", string); +} + +#undef APPEND_STRING +#undef APPEND_INDENT + +/* Parser API */ +JSON_Value * json_parse_file(const char *filename) { + char *file_contents = read_file(filename); + JSON_Value *output_value = NULL; + if (file_contents == NULL) + return NULL; + output_value = json_parse_string(file_contents); + parson_free(file_contents); + return output_value; +} + +JSON_Value * json_parse_file_with_comments(const char *filename) { + char *file_contents = read_file(filename); + JSON_Value *output_value = NULL; + if (file_contents == NULL) + return NULL; + output_value = json_parse_string_with_comments(file_contents); + parson_free(file_contents); + return output_value; +} + +JSON_Value * json_parse_string(const char *string) { + if (string == NULL) + return NULL; + SKIP_WHITESPACES(&string); + if (*string != '{' && *string != '[') + return NULL; + return parse_value((const char**)&string, 0); +} + +JSON_Value * json_parse_string_with_comments(const char *string) { + JSON_Value *result = NULL; + char *string_mutable_copy = NULL, *string_mutable_copy_ptr = NULL; + string_mutable_copy = parson_strdup(string); + if (string_mutable_copy == NULL) + return NULL; + remove_comments(string_mutable_copy, "/*", "*/"); + remove_comments(string_mutable_copy, "//", "\n"); + string_mutable_copy_ptr = string_mutable_copy; + SKIP_WHITESPACES(&string_mutable_copy_ptr); + if (*string_mutable_copy_ptr != '{' && *string_mutable_copy_ptr != '[') { + parson_free(string_mutable_copy); + return NULL; + } + result = parse_value((const char**)&string_mutable_copy_ptr, 0); + parson_free(string_mutable_copy); + return result; +} + + +/* JSON Object API */ + +JSON_Value * json_object_get_value(const JSON_Object *object, const char *name) { + if (object == NULL || name == NULL) + return NULL; + return json_object_nget_value(object, name, strlen(name)); +} + +const char * json_object_get_string(const JSON_Object *object, const char *name) { + return json_value_get_string(json_object_get_value(object, name)); +} + +double json_object_get_number(const JSON_Object *object, const char *name) { + return json_value_get_number(json_object_get_value(object, name)); +} + +JSON_Object * json_object_get_object(const JSON_Object *object, const char *name) { + return json_value_get_object(json_object_get_value(object, name)); +} + +JSON_Array * json_object_get_array(const JSON_Object *object, const char *name) { + return json_value_get_array(json_object_get_value(object, name)); +} + +int json_object_get_boolean(const JSON_Object *object, const char *name) { + return json_value_get_boolean(json_object_get_value(object, name)); +} + +JSON_Value * json_object_dotget_value(const JSON_Object *object, const char *name) { + const char *dot_position = strchr(name, '.'); + if (!dot_position) + return json_object_get_value(object, name); + object = json_value_get_object(json_object_nget_value(object, name, dot_position - name)); + return json_object_dotget_value(object, dot_position + 1); +} + +const char * json_object_dotget_string(const JSON_Object *object, const char *name) { + return json_value_get_string(json_object_dotget_value(object, name)); +} + +double json_object_dotget_number(const JSON_Object *object, const char *name) { + return json_value_get_number(json_object_dotget_value(object, name)); +} + +JSON_Object * json_object_dotget_object(const JSON_Object *object, const char *name) { + return json_value_get_object(json_object_dotget_value(object, name)); +} + +JSON_Array * json_object_dotget_array(const JSON_Object *object, const char *name) { + return json_value_get_array(json_object_dotget_value(object, name)); +} + +int json_object_dotget_boolean(const JSON_Object *object, const char *name) { + return json_value_get_boolean(json_object_dotget_value(object, name)); +} + +size_t json_object_get_count(const JSON_Object *object) { + return object ? object->count : 0; +} + +const char * json_object_get_name(const JSON_Object *object, size_t index) { + if (index >= json_object_get_count(object)) + return NULL; + return object->names[index]; +} + +/* JSON Array API */ +JSON_Value * json_array_get_value(const JSON_Array *array, size_t index) { + if (index >= json_array_get_count(array)) + return NULL; + return array->items[index]; +} + +const char * json_array_get_string(const JSON_Array *array, size_t index) { + return json_value_get_string(json_array_get_value(array, index)); +} + +double json_array_get_number(const JSON_Array *array, size_t index) { + return json_value_get_number(json_array_get_value(array, index)); +} + +JSON_Object * json_array_get_object(const JSON_Array *array, size_t index) { + return json_value_get_object(json_array_get_value(array, index)); +} + +JSON_Array * json_array_get_array(const JSON_Array *array, size_t index) { + return json_value_get_array(json_array_get_value(array, index)); +} + +int json_array_get_boolean(const JSON_Array *array, size_t index) { + return json_value_get_boolean(json_array_get_value(array, index)); +} + +size_t json_array_get_count(const JSON_Array *array) { + return array ? array->count : 0; +} + +/* JSON Value API */ +JSON_Value_Type json_value_get_type(const JSON_Value *value) { + return value ? value->type : JSONError; +} + +JSON_Object * json_value_get_object(const JSON_Value *value) { + return json_value_get_type(value) == JSONObject ? value->value.object : NULL; +} + +JSON_Array * json_value_get_array(const JSON_Value *value) { + return json_value_get_type(value) == JSONArray ? value->value.array : NULL; +} + +const char * json_value_get_string(const JSON_Value *value) { + return json_value_get_type(value) == JSONString ? value->value.string : NULL; +} + +double json_value_get_number(const JSON_Value *value) { + return json_value_get_type(value) == JSONNumber ? value->value.number : 0; +} + +int json_value_get_boolean(const JSON_Value *value) { + return json_value_get_type(value) == JSONBoolean ? value->value.boolean : -1; +} + +void json_value_free(JSON_Value *value) { + switch (json_value_get_type(value)) { + case JSONObject: + json_object_free(value->value.object); + break; + case JSONString: + if (value->value.string) { parson_free(value->value.string); } + break; + case JSONArray: + json_array_free(value->value.array); + break; + default: + break; + } + parson_free(value); +} + +JSON_Value * json_value_init_object(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) + return NULL; + new_value->type = JSONObject; + new_value->value.object = json_object_init(); + if (!new_value->value.object) { + parson_free(new_value); + return NULL; + } + return new_value; +} + +JSON_Value * json_value_init_array(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) + return NULL; + new_value->type = JSONArray; + new_value->value.array = json_array_init(); + if (!new_value->value.array) { + parson_free(new_value); + return NULL; + } + return new_value; +} + +JSON_Value * json_value_init_string(const char *string) { + char *copy = NULL; + JSON_Value *value; + size_t string_len = 0; + if (string == NULL) + return NULL; + string_len = strlen(string); + if (!is_valid_utf8(string, string_len)) + return NULL; + copy = parson_strndup(string, string_len); + if (copy == NULL) + return NULL; + value = json_value_init_string_no_copy(copy); + if (value == NULL) + parson_free(copy); + return value; +} + +JSON_Value * json_value_init_number(double number) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) + return NULL; + new_value->type = JSONNumber; + new_value->value.number = number; + return new_value; +} + +JSON_Value * json_value_init_boolean(int boolean) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) + return NULL; + new_value->type = JSONBoolean; + new_value->value.boolean = boolean ? 1 : 0; + return new_value; +} + +JSON_Value * json_value_init_null(void) { + JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value)); + if (!new_value) + return NULL; + new_value->type = JSONNull; + return new_value; +} + +JSON_Value * json_value_deep_copy(const JSON_Value *value) { + size_t i = 0; + JSON_Value *return_value = NULL, *temp_value_copy = NULL, *temp_value = NULL; + const char *temp_string = NULL, *temp_key = NULL; + char *temp_string_copy = NULL; + JSON_Array *temp_array = NULL, *temp_array_copy = NULL; + JSON_Object *temp_object = NULL, *temp_object_copy = NULL; + + switch (json_value_get_type(value)) { + case JSONArray: + temp_array = json_value_get_array(value); + return_value = json_value_init_array(); + if (return_value == NULL) + return NULL; + temp_array_copy = json_value_get_array(return_value); + for (i = 0; i < json_array_get_count(temp_array); i++) { + temp_value = json_array_get_value(temp_array, i); + temp_value_copy = json_value_deep_copy(temp_value); + if (temp_value_copy == NULL) { + json_value_free(return_value); + return NULL; + } + if (json_array_add(temp_array_copy, temp_value_copy) == JSONFailure) { + json_value_free(return_value); + json_value_free(temp_value_copy); + return NULL; + } + } + return return_value; + case JSONObject: + temp_object = json_value_get_object(value); + return_value = json_value_init_object(); + if (return_value == NULL) + return NULL; + temp_object_copy = json_value_get_object(return_value); + for (i = 0; i < json_object_get_count(temp_object); i++) { + temp_key = json_object_get_name(temp_object, i); + temp_value = json_object_get_value(temp_object, temp_key); + temp_value_copy = json_value_deep_copy(temp_value); + if (temp_value_copy == NULL) { + json_value_free(return_value); + return NULL; + } + if (json_object_add(temp_object_copy, temp_key, temp_value_copy) == JSONFailure) { + json_value_free(return_value); + json_value_free(temp_value_copy); + return NULL; + } + } + return return_value; + case JSONBoolean: + return json_value_init_boolean(json_value_get_boolean(value)); + case JSONNumber: + return json_value_init_number(json_value_get_number(value)); + case JSONString: + temp_string = json_value_get_string(value); + temp_string_copy = parson_strdup(temp_string); + if (temp_string_copy == NULL) + return NULL; + return_value = json_value_init_string_no_copy(temp_string_copy); + if (return_value == NULL) + parson_free(temp_string_copy); + return return_value; + case JSONNull: + return json_value_init_null(); + case JSONError: + return NULL; + default: + return NULL; + } +} + +size_t json_serialization_size(const JSON_Value *value) { + char num_buf[1100]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */ + int res = json_serialize_to_buffer_r(value, NULL, 0, 0, num_buf); + return res < 0 ? 0 : (size_t)(res + 1); +} + +JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) { + int written = -1; + size_t needed_size_in_bytes = json_serialization_size(value); + if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) { + return JSONFailure; + } + written = json_serialize_to_buffer_r(value, buf, 0, 0, NULL); + if (written < 0) + return JSONFailure; + return JSONSuccess; +} + +JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename) { + JSON_Status return_code = JSONSuccess; + FILE *fp = NULL; + char *serialized_string = json_serialize_to_string(value); + if (serialized_string == NULL) { + return JSONFailure; + } + fp = fopen (filename, "w"); + if (fp != NULL) { + if (fputs (serialized_string, fp) == EOF) { + return_code = JSONFailure; + } + if (fclose (fp) == EOF) { + return_code = JSONFailure; + } + } + json_free_serialized_string(serialized_string); + return return_code; +} + +char * json_serialize_to_string(const JSON_Value *value) { + JSON_Status serialization_result = JSONFailure; + size_t buf_size_bytes = json_serialization_size(value); + char *buf = NULL; + if (buf_size_bytes == 0) { + return NULL; + } + buf = (char*)parson_malloc(buf_size_bytes); + if (buf == NULL) + return NULL; + serialization_result = json_serialize_to_buffer(value, buf, buf_size_bytes); + if (serialization_result == JSONFailure) { + json_free_serialized_string(buf); + return NULL; + } + return buf; +} + +size_t json_serialization_size_pretty(const JSON_Value *value) { + char num_buf[1100]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */ + int res = json_serialize_to_buffer_r(value, NULL, 0, 1, num_buf); + return res < 0 ? 0 : (size_t)(res + 1); +} + +JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) { + int written = -1; + size_t needed_size_in_bytes = json_serialization_size_pretty(value); + if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) + return JSONFailure; + written = json_serialize_to_buffer_r(value, buf, 0, 1, NULL); + if (written < 0) + return JSONFailure; + return JSONSuccess; +} + +JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename) { + JSON_Status return_code = JSONSuccess; + FILE *fp = NULL; + char *serialized_string = json_serialize_to_string_pretty(value); + if (serialized_string == NULL) { + return JSONFailure; + } + fp = fopen (filename, "w"); + if (fp != NULL) { + if (fputs (serialized_string, fp) == EOF) { + return_code = JSONFailure; + } + if (fclose (fp) == EOF) { + return_code = JSONFailure; + } + } + json_free_serialized_string(serialized_string); + return return_code; +} + +char * json_serialize_to_string_pretty(const JSON_Value *value) { + JSON_Status serialization_result = JSONFailure; + size_t buf_size_bytes = json_serialization_size_pretty(value); + char *buf = NULL; + if (buf_size_bytes == 0) { + return NULL; + } + buf = (char*)parson_malloc(buf_size_bytes); + if (buf == NULL) + return NULL; + serialization_result = json_serialize_to_buffer_pretty(value, buf, buf_size_bytes); + if (serialization_result == JSONFailure) { + json_free_serialized_string(buf); + return NULL; + } + return buf; +} + + +void json_free_serialized_string(char *string) { + parson_free(string); +} + +JSON_Status json_array_remove(JSON_Array *array, size_t ix) { + size_t last_element_ix = 0; + if (array == NULL || ix >= json_array_get_count(array)) { + return JSONFailure; + } + last_element_ix = json_array_get_count(array) - 1; + json_value_free(json_array_get_value(array, ix)); + array->count -= 1; + if (ix != last_element_ix) /* Replace value with one from the end of array */ + array->items[ix] = json_array_get_value(array, last_element_ix); + return JSONSuccess; +} + +JSON_Status json_array_replace_value(JSON_Array *array, size_t ix, JSON_Value *value) { + if (array == NULL || value == NULL || ix >= json_array_get_count(array)) { + return JSONFailure; + } + json_value_free(json_array_get_value(array, ix)); + array->items[ix] = value; + return JSONSuccess; +} + +JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string) { + JSON_Value *value = json_value_init_string(string); + if (value == NULL) + return JSONFailure; + if (json_array_replace_value(array, i, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number) { + JSON_Value *value = json_value_init_number(number); + if (value == NULL) + return JSONFailure; + if (json_array_replace_value(array, i, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean) { + JSON_Value *value = json_value_init_boolean(boolean); + if (value == NULL) + return JSONFailure; + if (json_array_replace_value(array, i, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_replace_null(JSON_Array *array, size_t i) { + JSON_Value *value = json_value_init_null(); + if (value == NULL) + return JSONFailure; + if (json_array_replace_value(array, i, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_clear(JSON_Array *array) { + size_t i = 0; + if (array == NULL) + return JSONFailure; + for (i = 0; i < json_array_get_count(array); i++) { + json_value_free(json_array_get_value(array, i)); + } + array->count = 0; + return JSONSuccess; +} + +JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value) { + if (array == NULL || value == NULL) + return JSONFailure; + return json_array_add(array, value); +} + +JSON_Status json_array_append_string(JSON_Array *array, const char *string) { + JSON_Value *value = json_value_init_string(string); + if (value == NULL) + return JSONFailure; + if (json_array_append_value(array, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_append_number(JSON_Array *array, double number) { + JSON_Value *value = json_value_init_number(number); + if (value == NULL) + return JSONFailure; + if (json_array_append_value(array, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_append_boolean(JSON_Array *array, int boolean) { + JSON_Value *value = json_value_init_boolean(boolean); + if (value == NULL) + return JSONFailure; + if (json_array_append_value(array, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_array_append_null(JSON_Array *array) { + JSON_Value *value = json_value_init_null(); + if (value == NULL) + return JSONFailure; + if (json_array_append_value(array, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value) { + size_t i = 0; + JSON_Value *old_value; + if (object == NULL || name == NULL || value == NULL) + return JSONFailure; + old_value = json_object_get_value(object, name); + if (old_value != NULL) { /* free and overwrite old value */ + json_value_free(old_value); + for (i = 0; i < json_object_get_count(object); i++) { + if (strcmp(object->names[i], name) == 0) { + object->values[i] = value; + return JSONSuccess; + } + } + } + /* add new key value pair */ + return json_object_add(object, name, value); +} + +JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string) { + return json_object_set_value(object, name, json_value_init_string(string)); +} + +JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number) { + return json_object_set_value(object, name, json_value_init_number(number)); +} + +JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean) { + return json_object_set_value(object, name, json_value_init_boolean(boolean)); +} + +JSON_Status json_object_set_null(JSON_Object *object, const char *name) { + return json_object_set_value(object, name, json_value_init_null()); +} + +JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value) { + const char *dot_pos = NULL; + char *current_name = NULL; + JSON_Object *temp_obj = NULL; + JSON_Value *new_value = NULL; + if (value == NULL || name == NULL || value == NULL) + return JSONFailure; + dot_pos = strchr(name, '.'); + if (dot_pos == NULL) { + return json_object_set_value(object, name, value); + } else { + current_name = parson_strndup(name, dot_pos - name); + temp_obj = json_object_get_object(object, current_name); + if (temp_obj == NULL) { + new_value = json_value_init_object(); + if (new_value == NULL) { + parson_free(current_name); + return JSONFailure; + } + if (json_object_add(object, current_name, new_value) == JSONFailure) { + json_value_free(new_value); + parson_free(current_name); + return JSONFailure; + } + temp_obj = json_object_get_object(object, current_name); + } + parson_free(current_name); + return json_object_dotset_value(temp_obj, dot_pos + 1, value); + } +} + +JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string) { + JSON_Value *value = json_value_init_string(string); + if (value == NULL) + return JSONFailure; + if (json_object_dotset_value(object, name, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number) { + JSON_Value *value = json_value_init_number(number); + if (value == NULL) + return JSONFailure; + if (json_object_dotset_value(object, name, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean) { + JSON_Value *value = json_value_init_boolean(boolean); + if (value == NULL) + return JSONFailure; + if (json_object_dotset_value(object, name, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_dotset_null(JSON_Object *object, const char *name) { + JSON_Value *value = json_value_init_null(); + if (value == NULL) + return JSONFailure; + if (json_object_dotset_value(object, name, value) == JSONFailure) { + json_value_free(value); + return JSONFailure; + } + return JSONSuccess; +} + +JSON_Status json_object_remove(JSON_Object *object, const char *name) { + size_t i = 0, last_item_index = 0; + if (object == NULL || json_object_get_value(object, name) == NULL) + return JSONFailure; + last_item_index = json_object_get_count(object) - 1; + for (i = 0; i < json_object_get_count(object); i++) { + if (strcmp(object->names[i], name) == 0) { + parson_free(object->names[i]); + json_value_free(object->values[i]); + if (i != last_item_index) { /* Replace key value pair with one from the end */ + object->names[i] = object->names[last_item_index]; + object->values[i] = object->values[last_item_index]; + } + object->count -= 1; + return JSONSuccess; + } + } + return JSONFailure; /* No execution path should end here */ +} + +JSON_Status json_object_dotremove(JSON_Object *object, const char *name) { + const char *dot_pos = strchr(name, '.'); + char *current_name = NULL; + JSON_Object *temp_obj = NULL; + if (dot_pos == NULL) { + return json_object_remove(object, name); + } else { + current_name = parson_strndup(name, dot_pos - name); + temp_obj = json_object_get_object(object, current_name); + if (temp_obj == NULL) { + parson_free(current_name); + return JSONFailure; + } + parson_free(current_name); + return json_object_dotremove(temp_obj, dot_pos + 1); + } +} + +JSON_Status json_object_clear(JSON_Object *object) { + size_t i = 0; + if (object == NULL) { + return JSONFailure; + } + for (i = 0; i < json_object_get_count(object); i++) { + parson_free(object->names[i]); + json_value_free(object->values[i]); + } + object->count = 0; + return JSONSuccess; +} + +JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value) { + JSON_Value *temp_schema_value = NULL, *temp_value = NULL; + JSON_Array *schema_array = NULL, *value_array = NULL; + JSON_Object *schema_object = NULL, *value_object = NULL; + JSON_Value_Type schema_type = JSONError, value_type = JSONError; + const char *key = NULL; + size_t i = 0, count = 0; + if (schema == NULL || value == NULL) + return JSONFailure; + schema_type = json_value_get_type(schema); + value_type = json_value_get_type(value); + if (schema_type != value_type && schema_type != JSONNull) /* null represents all values */ + return JSONFailure; + switch (schema_type) { + case JSONArray: + schema_array = json_value_get_array(schema); + value_array = json_value_get_array(value); + count = json_array_get_count(schema_array); + if (count == 0) + return JSONSuccess; /* Empty array allows all types */ + /* Get first value from array, rest is ignored */ + temp_schema_value = json_array_get_value(schema_array, 0); + for (i = 0; i < json_array_get_count(value_array); i++) { + temp_value = json_array_get_value(value_array, i); + if (json_validate(temp_schema_value, temp_value) == 0) { + return JSONFailure; + } + } + return JSONSuccess; + case JSONObject: + schema_object = json_value_get_object(schema); + value_object = json_value_get_object(value); + count = json_object_get_count(schema_object); + if (count == 0) + return JSONSuccess; /* Empty object allows all objects */ + else if (json_object_get_count(value_object) < count) + return JSONFailure; /* Tested object mustn't have less name-value pairs than schema */ + for (i = 0; i < count; i++) { + key = json_object_get_name(schema_object, i); + temp_schema_value = json_object_get_value(schema_object, key); + temp_value = json_object_get_value(value_object, key); + if (temp_value == NULL) + return JSONFailure; + if (json_validate(temp_schema_value, temp_value) == JSONFailure) + return JSONFailure; + } + return JSONSuccess; + case JSONString: case JSONNumber: case JSONBoolean: case JSONNull: + return JSONSuccess; /* equality already tested before switch */ + case JSONError: default: + return JSONFailure; + } +} + +JSON_Status json_value_equals(const JSON_Value *a, const JSON_Value *b) { + JSON_Object *a_object = NULL, *b_object = NULL; + JSON_Array *a_array = NULL, *b_array = NULL; + const char *a_string = NULL, *b_string = NULL; + const char *key = NULL; + size_t a_count = 0, b_count = 0, i = 0; + JSON_Value_Type a_type, b_type; + a_type = json_value_get_type(a); + b_type = json_value_get_type(b); + if (a_type != b_type) { + return 0; + } + switch (a_type) { + case JSONArray: + a_array = json_value_get_array(a); + b_array = json_value_get_array(b); + a_count = json_array_get_count(a_array); + b_count = json_array_get_count(b_array); + if (a_count != b_count) { + return 0; + } + for (i = 0; i < a_count; i++) { + if (!json_value_equals(json_array_get_value(a_array, i), + json_array_get_value(b_array, i))) { + return 0; + } + } + return 1; + case JSONObject: + a_object = json_value_get_object(a); + b_object = json_value_get_object(b); + a_count = json_object_get_count(a_object); + b_count = json_object_get_count(b_object); + if (a_count != b_count) { + return 0; + } + for (i = 0; i < a_count; i++) { + key = json_object_get_name(a_object, i); + if (!json_value_equals(json_object_get_value(a_object, key), + json_object_get_value(b_object, key))) { + return 0; + } + } + return 1; + case JSONString: + a_string = json_value_get_string(a); + b_string = json_value_get_string(b); + return strcmp(a_string, b_string) == 0; + case JSONBoolean: + return json_value_get_boolean(a) == json_value_get_boolean(b); + case JSONNumber: + return fabs(json_value_get_number(a) - json_value_get_number(b)) < 0.000001; /* EPSILON */ + case JSONError: + return 1; + case JSONNull: + return 1; + default: + return 1; + } +} + +JSON_Value_Type json_type(const JSON_Value *value) { + return json_value_get_type(value); +} + +JSON_Object * json_object (const JSON_Value *value) { + return json_value_get_object(value); +} + +JSON_Array * json_array (const JSON_Value *value) { + return json_value_get_array(value); +} + +const char * json_string (const JSON_Value *value) { + return json_value_get_string(value); +} + +double json_number (const JSON_Value *value) { + return json_value_get_number(value); +} + +int json_boolean(const JSON_Value *value) { + return json_value_get_boolean(value); +} + +void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun) { + parson_malloc = malloc_fun; + parson_free = free_fun; +} diff --git a/internal/warpc/genwebp/deps/parson/parson.h b/internal/warpc/genwebp/deps/parson/parson.h new file mode 100644 index 000000000..3c04edf18 --- /dev/null +++ b/internal/warpc/genwebp/deps/parson/parson.h @@ -0,0 +1,222 @@ +/* + Parson ( http://kgabis.github.com/parson/ ) + Copyright (c) 2012 - 2015 Krzysztof Gabis + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef parson_parson_h +#define parson_parson_h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include /* size_t */ + +/* Types and enums */ +typedef struct json_object_t JSON_Object; +typedef struct json_array_t JSON_Array; +typedef struct json_value_t JSON_Value; + +enum json_value_type { + JSONError = -1, + JSONNull = 1, + JSONString = 2, + JSONNumber = 3, + JSONObject = 4, + JSONArray = 5, + JSONBoolean = 6 +}; +typedef int JSON_Value_Type; + +enum json_result_t { + JSONSuccess = 0, + JSONFailure = -1 +}; +typedef int JSON_Status; + +typedef void * (*JSON_Malloc_Function)(size_t); +typedef void (*JSON_Free_Function)(void *); + +/* Call only once, before calling any other function from parson API. If not called, malloc and free + from stdlib will be used for all allocations */ +void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun); + +/* Parses first JSON value in a file, returns NULL in case of error */ +JSON_Value * json_parse_file(const char *filename); + +/* Parses first JSON value in a file and ignores comments (/ * * / and //), + returns NULL in case of error */ +JSON_Value * json_parse_file_with_comments(const char *filename); + +/* Parses first JSON value in a string, returns NULL in case of error */ +JSON_Value * json_parse_string(const char *string); + +/* Parses first JSON value in a string and ignores comments (/ * * / and //), + returns NULL in case of error */ +JSON_Value * json_parse_string_with_comments(const char *string); + +/* Serialization */ +size_t json_serialization_size(const JSON_Value *value); /* returns 0 on fail */ +JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes); +JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename); +char * json_serialize_to_string(const JSON_Value *value); + +/* Pretty serialization */ +size_t json_serialization_size_pretty(const JSON_Value *value); /* returns 0 on fail */ +JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes); +JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename); +char * json_serialize_to_string_pretty(const JSON_Value *value); + +void json_free_serialized_string(char *string); /* frees string from json_serialize_to_string and json_serialize_to_string_pretty */ + +/* Comparing */ +int json_value_equals(const JSON_Value *a, const JSON_Value *b); + +/* Validation + This is *NOT* JSON Schema. It validates json by checking if object have identically + named fields with matching types. + For example schema {"name":"", "age":0} will validate + {"name":"Joe", "age":25} and {"name":"Joe", "age":25, "gender":"m"}, + but not {"name":"Joe"} or {"name":"Joe", "age":"Cucumber"}. + In case of arrays, only first value in schema is checked against all values in tested array. + Empty objects ({}) validate all objects, empty arrays ([]) validate all arrays, + null validates values of every type. + */ +JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value); + +/* + * JSON Object + */ +JSON_Value * json_object_get_value (const JSON_Object *object, const char *name); +const char * json_object_get_string (const JSON_Object *object, const char *name); +JSON_Object * json_object_get_object (const JSON_Object *object, const char *name); +JSON_Array * json_object_get_array (const JSON_Object *object, const char *name); +double json_object_get_number (const JSON_Object *object, const char *name); /* returns 0 on fail */ +int json_object_get_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */ + +/* dotget functions enable addressing values with dot notation in nested objects, + just like in structs or c++/java/c# objects (e.g. objectA.objectB.value). + Because valid names in JSON can contain dots, some values may be inaccessible + this way. */ +JSON_Value * json_object_dotget_value (const JSON_Object *object, const char *name); +const char * json_object_dotget_string (const JSON_Object *object, const char *name); +JSON_Object * json_object_dotget_object (const JSON_Object *object, const char *name); +JSON_Array * json_object_dotget_array (const JSON_Object *object, const char *name); +double json_object_dotget_number (const JSON_Object *object, const char *name); /* returns 0 on fail */ +int json_object_dotget_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */ + +/* Functions to get available names */ +size_t json_object_get_count(const JSON_Object *object); +const char * json_object_get_name (const JSON_Object *object, size_t index); + +/* Creates new name-value pair or frees and replaces old value with a new one. + * json_object_set_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value); +JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string); +JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number); +JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean); +JSON_Status json_object_set_null(JSON_Object *object, const char *name); + +/* Works like dotget functions, but creates whole hierarchy if necessary. + * json_object_dotset_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value); +JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string); +JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number); +JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean); +JSON_Status json_object_dotset_null(JSON_Object *object, const char *name); + +/* Frees and removes name-value pair */ +JSON_Status json_object_remove(JSON_Object *object, const char *name); + +/* Works like dotget function, but removes name-value pair only on exact match. */ +JSON_Status json_object_dotremove(JSON_Object *object, const char *key); + +/* Removes all name-value pairs in object */ +JSON_Status json_object_clear(JSON_Object *object); + +/* + *JSON Array + */ +JSON_Value * json_array_get_value (const JSON_Array *array, size_t index); +const char * json_array_get_string (const JSON_Array *array, size_t index); +JSON_Object * json_array_get_object (const JSON_Array *array, size_t index); +JSON_Array * json_array_get_array (const JSON_Array *array, size_t index); +double json_array_get_number (const JSON_Array *array, size_t index); /* returns 0 on fail */ +int json_array_get_boolean(const JSON_Array *array, size_t index); /* returns -1 on fail */ +size_t json_array_get_count (const JSON_Array *array); + +/* Frees and removes value at given index, does nothing and returns JSONFailure if index doesn't exist. + * Order of values in array may change during execution. */ +JSON_Status json_array_remove(JSON_Array *array, size_t i); + +/* Frees and removes from array value at given index and replaces it with given one. + * Does nothing and returns JSONFailure if index doesn't exist. + * json_array_replace_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_array_replace_value(JSON_Array *array, size_t i, JSON_Value *value); +JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string); +JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number); +JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean); +JSON_Status json_array_replace_null(JSON_Array *array, size_t i); + +/* Frees and removes all values from array */ +JSON_Status json_array_clear(JSON_Array *array); + +/* Appends new value at the end of array. + * json_array_append_value does not copy passed value so it shouldn't be freed afterwards. */ +JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value); +JSON_Status json_array_append_string(JSON_Array *array, const char *string); +JSON_Status json_array_append_number(JSON_Array *array, double number); +JSON_Status json_array_append_boolean(JSON_Array *array, int boolean); +JSON_Status json_array_append_null(JSON_Array *array); + +/* + *JSON Value + */ +JSON_Value * json_value_init_object (void); +JSON_Value * json_value_init_array (void); +JSON_Value * json_value_init_string (const char *string); /* copies passed string */ +JSON_Value * json_value_init_number (double number); +JSON_Value * json_value_init_boolean(int boolean); +JSON_Value * json_value_init_null (void); +JSON_Value * json_value_deep_copy (const JSON_Value *value); +void json_value_free (JSON_Value *value); + +JSON_Value_Type json_value_get_type (const JSON_Value *value); +JSON_Object * json_value_get_object (const JSON_Value *value); +JSON_Array * json_value_get_array (const JSON_Value *value); +const char * json_value_get_string (const JSON_Value *value); +double json_value_get_number (const JSON_Value *value); +int json_value_get_boolean(const JSON_Value *value); + +/* Same as above, but shorter */ +JSON_Value_Type json_type (const JSON_Value *value); +JSON_Object * json_object (const JSON_Value *value); +JSON_Array * json_array (const JSON_Value *value); +const char * json_string (const JSON_Value *value); +double json_number (const JSON_Value *value); +int json_boolean(const JSON_Value *value); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/internal/warpc/genwebp/go.mod b/internal/warpc/genwebp/go.mod new file mode 100644 index 000000000..e69de29bb diff --git a/internal/warpc/genwebp/webp.c b/internal/warpc/genwebp/webp.c new file mode 100644 index 000000000..79dff6201 --- /dev/null +++ b/internal/warpc/genwebp/webp.c @@ -0,0 +1,782 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include "webp/demux.h" +#include +#include "deps/parson/parson.h" + +void handle_commands(FILE *stream); + +int main() +{ + // This will read commands from stdin and write responses to stdout + // and return 0 when stdin is closed. + // Any errors gets reported in the RPC response messages. + handle_commands(stdin); + return 0; +} + +// Translation table for WebPEncodingError to string. +// This translation table is replicated in all webp bindings on the web. Great design. +static const char *const kErrorMessages[VP8_ENC_ERROR_LAST] = { + "OK", + "OUT_OF_MEMORY: Out of memory allocating objects", + "BITSTREAM_OUT_OF_MEMORY: Out of memory re-allocating byte buffer", + "NULL_PARAMETER: NULL parameter passed to function", + "INVALID_CONFIGURATION: configuration is invalid", + "BAD_DIMENSION: Bad picture dimension. Maximum width and height " + "allowed is 16383 pixels.", + "PARTITION0_OVERFLOW: Partition #0 is too big to fit 512k.\n" + "To reduce the size of this partition, try using less segments " + "with the -segments option, and eventually reduce the number of " + "header bits using -partition_limit. More details are available " + "in the manual (`man cwebp`)", + "PARTITION_OVERFLOW: Partition is too big to fit 16M", + "BAD_WRITE: Picture writer returned an I/O error", + "FILE_TOO_BIG: File would be too big to fit in 4G", + "USER_ABORT: encoding abort requested by user"}; + +typedef struct +{ + int version; + int id; + char command[256]; + char err[256]; +} Header; + +typedef struct +{ + int width; + int height; + int stride; + int loopCount; + int frameCount; + int *frameDurations; + +} InputParams; + +typedef struct +{ + float quality; // between 0 and 100. Set to 0 for lossless. + char hint[64]; // drawing, icon, photo, picture, or text. Default is photo. + int preset; // preset to use; resolved from hint. + + bool useSharpYuv; // use sharp YUV for better quality. + +} InputOptions; + +typedef struct +{ + InputOptions options; + InputParams params; + +} InputData; + +typedef struct +{ + Header header; + InputData data; + +} InputMessage; + +typedef struct +{ + Header header; + InputData data; + +} OutputMessage; + +#define MAX_LINE_LENGTH 4096 + +static uint8_t *encodeNRGBA(WebPConfig *config, const uint8_t *rgba, int width, int height, int stride, size_t *output_size) +{ + WebPPicture pic; + WebPMemoryWriter wrt; + int ok; + if (!WebPPictureInit(&pic)) + { + fprintf(stderr, "WebPPictureInit failed\n"); + return NULL; + } + + pic.use_argb = 1; + pic.width = width; + pic.height = height; + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &wrt; + WebPMemoryWriterInit(&wrt); + ok = WebPPictureImportRGBA(&pic, rgba, stride); + if (ok) + { + ok = WebPEncode(config, &pic); + if (!ok) + { + fprintf(stderr, "WebPEncode failed: %d (%s)\n", pic.error_code, kErrorMessages[pic.error_code]); + } + } + else + { + fprintf(stderr, "WebPPictureImportRGBA failed: %d (%s)\n", pic.error_code, kErrorMessages[pic.error_code]); + } + WebPPictureFree(&pic); + if (!ok) + { + WebPMemoryWriterClear(&wrt); + return NULL; + } + *output_size = wrt.size; + return wrt.mem; +} + +static uint8_t *encodeGray(WebPConfig *config, uint8_t *y, int width, int height, int stride, size_t *output_size) +{ + WebPPicture pic; + WebPMemoryWriter wrt; + + int ok; + if (!WebPPictureInit(&pic)) + { + return NULL; + } + + pic.use_argb = 0; + pic.width = width; + pic.height = height; + pic.y_stride = stride; + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &wrt; + WebPMemoryWriterInit(&wrt); + + const int uvWidth = (int)(((int64_t)width + 1) >> 1); + const int uvHeight = (int)(((int64_t)height + 1) >> 1); + const int uvStride = uvWidth; + const int uvSize = uvStride * uvHeight; + const int gray = 128; + uint8_t *chroma; + + chroma = malloc(uvSize); + if (!chroma) + { + return 0; + } + memset(chroma, gray, uvSize); + + pic.y = y; + pic.u = chroma; + pic.v = chroma; + pic.uv_stride = uvStride; + + ok = WebPEncode(config, &pic); + + free(chroma); + + WebPPictureFree(&pic); + if (!ok) + { + WebPMemoryWriterClear(&wrt); + return NULL; + } + *output_size = wrt.size; + return wrt.mem; +} + +static uint8_t *encodeNRGBAAnimated(WebPConfig *config, InputParams params, const uint8_t *all_frames_data, size_t *output_size) +{ + WebPAnimEncoderOptions anim_options; + WebPAnimEncoderOptionsInit(&anim_options); + anim_options.anim_params.loop_count = params.loopCount; + + WebPAnimEncoder *enc = WebPAnimEncoderNew(params.width, params.height, &anim_options); + if (enc == NULL) + { + fprintf(stderr, "Error creating WebPAnimEncoder\n"); + return NULL; + } + + int timestamp = 0; + size_t frame_rgba_size = (size_t)params.stride * params.height; + + for (int i = 0; i < params.frameCount; i++) + { + WebPPicture pic; + if (!WebPPictureInit(&pic)) + { + fprintf(stderr, "WebPPictureInit failed\n"); + WebPAnimEncoderDelete(enc); + return NULL; + } + pic.use_argb = 1; + pic.width = params.width; + pic.height = params.height; + + const uint8_t *frame_rgba = all_frames_data + i * frame_rgba_size; + + if (!WebPPictureImportRGBA(&pic, frame_rgba, params.stride)) + { + fprintf(stderr, "WebPPictureImportRGBA failed\n"); + WebPPictureFree(&pic); + WebPAnimEncoderDelete(enc); + return NULL; + } + + if (!WebPAnimEncoderAdd(enc, &pic, timestamp, config)) + { + fprintf(stderr, "WebPAnimEncoderAdd failed\n"); + WebPPictureFree(&pic); + WebPAnimEncoderDelete(enc); + return NULL; + } + timestamp += params.frameDurations[i]; + WebPPictureFree(&pic); + } + + if (!WebPAnimEncoderAdd(enc, NULL, timestamp, config)) + { + fprintf(stderr, "WebPAnimEncoderAdd failed for final frame\n"); + WebPAnimEncoderDelete(enc); + return NULL; + } + + WebPData webp_data_out; + WebPDataInit(&webp_data_out); + if (!WebPAnimEncoderAssemble(enc, &webp_data_out)) + { + fprintf(stderr, "WebPAnimEncoderAssemble failed\n"); + WebPAnimEncoderDelete(enc); + return NULL; + } + WebPAnimEncoderDelete(enc); + + *output_size = webp_data_out.size; + uint8_t *webp_data = malloc(*output_size); + if (webp_data == NULL) + { + fprintf(stderr, "malloc failed for final webp data\n"); + WebPDataClear(&webp_data_out); + return NULL; + } + memcpy(webp_data, webp_data_out.bytes, *output_size); + WebPDataClear(&webp_data_out); + + return webp_data; +} + +static uint8_t initDecoderConfig(WebPDecoderConfig *config, WebPData data) +{ + if (!WebPInitDecoderConfig(config)) + { + return 0; + } + if (WebPGetFeatures(data.bytes, data.size, &config->input) != VP8_STATUS_OK) + { + return 0; + } + return 1; +} + +static uint8_t initEncoderConfig(WebPConfig *config, InputOptions opts) +{ + if (!WebPConfigInit(config)) + { + return 0; + } + + if (!WebPConfigPreset(config, opts.preset, opts.quality)) + { + return 0; + } + + if (opts.quality == 0) + { + // Activate the lossless compression mode with the desired efficiency level + // between 0 (fastest, lowest compression) and 9 (slower, best compression). + // A good default level is '6', providing a fair tradeoff between compression + // speed and final compressed size. + if (!WebPConfigLosslessPreset(config, 6)) + { + return 1; + } + } + + config->use_sharp_yuv = opts.useSharpYuv ? 1 : 0; + + return 1; +} + +InputMessage parse_input_message(const char *line) +{ + InputMessage msg = {0}; + + JSON_Value *root_value = json_parse_string(line); + if (root_value == NULL) + { + fprintf(stderr, "Error parsing JSON line\n"); + return msg; + } + + if (json_value_get_type(root_value) != JSONObject) + { + fprintf(stderr, "Error: Line did not parse to a valid JSON object\n"); + json_value_free(root_value); + return msg; + } + + JSON_Object *root_object = json_value_get_object(root_value); + + JSON_Object *header_object = json_object_get_object(root_object, "header"); + if (header_object != NULL) + { + msg.header.version = (int)json_object_get_number(header_object, "version"); + msg.header.id = (int)json_object_get_number(header_object, "id"); + const char *command_str = json_object_get_string(header_object, "command"); + if (command_str != NULL) + { + strncpy(msg.header.command, command_str, sizeof(msg.header.command) - 1); + msg.header.command[sizeof(msg.header.command) - 1] = '\0'; + } + const char *err_str = json_object_get_string(header_object, "err"); + if (err_str != NULL) + { + strncpy(msg.header.err, err_str, sizeof(msg.header.err) - 1); + msg.header.err[sizeof(msg.header.err) - 1] = '\0'; + } + } + + JSON_Object *data_object = json_object_get_object(root_object, "data"); + if (data_object != NULL) + { + JSON_Object *params_object = json_object_get_object(data_object, "params"); + if (params_object != NULL) + { + msg.data.params.width = (int)json_object_get_number(params_object, "width"); + msg.data.params.height = (int)json_object_get_number(params_object, "height"); + msg.data.params.stride = (int)json_object_get_number(params_object, "stride"); + msg.data.params.loopCount = (int)json_object_get_number(params_object, "loopCount"); + JSON_Array *durations_array = json_object_get_array(params_object, "frameDurations"); + if (durations_array != NULL) + { + size_t count = json_array_get_count(durations_array); + msg.data.params.frameCount = count; + + if (count > 0) + { + msg.data.params.frameDurations = malloc(sizeof(int) * count); + if (msg.data.params.frameDurations != NULL) + { + for (size_t i = 0; i < count; i++) + { + msg.data.params.frameDurations[i] = (int)json_array_get_number(durations_array, i); + } + } + else + { + // Malloc failed. + msg.data.params.frameCount = 0; + } + } + } + } + JSON_Object *options_object = json_object_get_object(data_object, "options"); + if (options_object != NULL) + { + msg.data.options.quality = (int)json_object_get_number(options_object, "quality"); + const char *hint_str = json_object_get_string(options_object, "hint"); + if (hint_str != NULL) + { + strncpy(msg.data.options.hint, hint_str, sizeof(msg.data.options.hint) - 1); + msg.data.options.hint[sizeof(msg.data.options.hint) - 1] = '\0'; + } + msg.data.options.useSharpYuv = json_object_get_number(options_object, "useSharpYuv") != 0; + if (msg.data.options.quality < 0 || msg.data.options.quality > 100) + { + msg.data.options.quality = 75; // default + } + if (strlen(msg.data.options.hint) == 0) + { + strncpy(msg.data.options.hint, "photo", sizeof(msg.data.options.hint) - 1); + } + + // Resolve preset from hint + if (strcmp(msg.data.options.hint, "picture") == 0) + { + msg.data.options.preset = WEBP_PRESET_PICTURE; + } + else if (strcmp(msg.data.options.hint, "photo") == 0) + { + msg.data.options.preset = WEBP_PRESET_PHOTO; + } + else if (strcmp(msg.data.options.hint, "drawing") == 0) + { + msg.data.options.preset = WEBP_PRESET_DRAWING; + } + else if (strcmp(msg.data.options.hint, "icon") == 0) + { + msg.data.options.preset = WEBP_PRESET_ICON; + } + else if (strcmp(msg.data.options.hint, "text") == 0) + { + msg.data.options.preset = WEBP_PRESET_TEXT; + } + else + { + // I'm not sure what webp's default is, but photo seems like a good choice. + msg.data.options.preset = WEBP_PRESET_PHOTO; + } + } + } + + json_value_free(root_value); + return msg; +} + +static void write_blob(uint32_t id, const uint8_t *data, uint32_t size) +{ + uint8_t output_blob_header[16]; + // See https://github.com/bep/textandbinarywriter + const char magic[] = {'T', 'A', 'K', '3', '5', 'E', 'M', '1'}; + memcpy(output_blob_header, magic, 8); + *(uint32_t *)&output_blob_header[8] = id; + *(uint32_t *)&output_blob_header[12] = size; + + fwrite(output_blob_header, 1, sizeof(output_blob_header), stdout); + fwrite(data, 1, (size_t)size, stdout); + fflush(stdout); +} + +void write_output_message(const OutputMessage *msg) +{ + JSON_Value *root_value = json_value_init_object(); + JSON_Object *root_object = json_value_get_object(root_value); + + // Header + JSON_Value *header_value = json_value_init_object(); + JSON_Object *header_object = json_value_get_object(header_value); + json_object_set_value(root_object, "header", header_value); + json_object_set_number(header_object, "version", msg->header.version); + json_object_set_number(header_object, "id", msg->header.id); + json_object_set_string(header_object, "err", msg->header.err); + + // Data + if (msg->data.params.width > 0) + { + JSON_Value *data_value = json_value_init_object(); + JSON_Object *data_object = json_value_get_object(data_value); + json_object_set_value(root_object, "data", data_value); + + JSON_Value *params_value = json_value_init_object(); + JSON_Object *params_object = json_value_get_object(params_value); + json_object_set_value(data_object, "params", params_value); + + json_object_set_number(params_object, "width", msg->data.params.width); + json_object_set_number(params_object, "height", msg->data.params.height); + json_object_set_number(params_object, "stride", msg->data.params.stride); + if (msg->data.params.frameDurations != NULL) + { + JSON_Value *durations_value = json_value_init_array(); + JSON_Array *durations_array = json_value_get_array(durations_value); + for (int i = 0; i < msg->data.params.frameCount; i++) + { + json_array_append_number(durations_array, msg->data.params.frameDurations[i]); + } + + json_object_set_value(params_object, "frameDurations", durations_value); + json_object_set_number(params_object, "loopCount", msg->data.params.loopCount); + } + } + + char *serialized_string = json_serialize_to_string(root_value); + fprintf(stdout, "%s\n", serialized_string); + fflush(stdout); + + json_free_serialized_string(serialized_string); + json_value_free(root_value); +} + +void handle_commands(FILE *stream) +{ + + char line[MAX_LINE_LENGTH]; + + while (fgets(line, sizeof(line), stream) != NULL) + { + InputMessage input = {0}; + uint8_t *blob_data = NULL; + uint32_t blob_size = 0; + + // Remove newline character if present + line[strcspn(line, "\n")] = 0; + + if (strlen(line) == 0) + { + continue; + } + + input = parse_input_message(line); + + // Next in stream is a blob header defined in https://github.com/bep/textandbinaryreader + // T', 'A', 'K', '3', '5', 'E', 'M', '1' id uint32, size uint32 + uint8_t blob_header[16]; + size_t read_bytes = fread(blob_header, 1, sizeof(blob_header), stream); + if (read_bytes != sizeof(blob_header)) + { + fprintf(stderr, "Error reading blob header\n"); + goto cleanup; + } + uint32_t blob_id = *(uint32_t *)&blob_header[8]; + blob_size = *(uint32_t *)&blob_header[12]; + blob_data = malloc((size_t)blob_size); + if (blob_data == NULL) + { + fprintf(stderr, "[%d] Error allocating memory for blob data\n", blob_id); + goto cleanup; + } + read_bytes = fread(blob_data, 1, (size_t)blob_size, stream); + if (read_bytes != (size_t)blob_size) + { + fprintf(stderr, "[%d] Error reading blob data (size: %llu read: %zu) \n", blob_id, (unsigned long long)blob_size, read_bytes); + goto cleanup; + } + + OutputMessage output = {0}; + output.header = input.header; + + if (strcmp(input.header.command, "decode") == 0) + { + + WebPData data; + data.bytes = blob_data; + data.size = (size_t)blob_size; + + WebPDecoderConfig config; + if (!initDecoderConfig(&config, data)) + { + strncpy(output.header.err, "Failed to initialize WebPDecoderConfig", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + output.data.params.width = config.input.width; + output.data.params.height = config.input.height; + output.data.params.stride = config.input.width * 4; + output.data.params.frameDurations = NULL; + + if (config.input.has_animation) + { + WebPAnimDecoderOptions dec_options; + WebPAnimDecoderOptionsInit(&dec_options); + dec_options.color_mode = MODE_RGBA; + + WebPAnimDecoder *dec = WebPAnimDecoderNew(&data, &dec_options); + if (dec == NULL) + { + strncpy(output.header.err, "Failed to create WebPAnimDecoder", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + WebPAnimInfo anim_info; + if (!WebPAnimDecoderGetInfo(dec, &anim_info)) + { + strncpy(output.header.err, "Failed to get animation info", sizeof(output.header.err) - 1); + write_output_message(&output); + WebPAnimDecoderDelete(dec); + goto cleanup; + } + + output.data.params.width = anim_info.canvas_width; + output.data.params.height = anim_info.canvas_height; + output.data.params.stride = anim_info.canvas_width * 4; + output.data.params.frameCount = anim_info.frame_count; + output.data.params.loopCount = anim_info.loop_count; + + int *frameDurations = malloc(sizeof(int) * anim_info.frame_count); + if (frameDurations == NULL) + { + strncpy(output.header.err, "Failed to allocate memory for frame durations", sizeof(output.header.err) - 1); + write_output_message(&output); + WebPAnimDecoderDelete(dec); + goto cleanup; + } + output.data.params.frameDurations = frameDurations; + + // Use a demuxer to get frame durations. + WebPDemuxer *demux = WebPDemux(&data); + if (demux != NULL) + { + WebPIterator iter; + for (uint32_t i = 0; i < anim_info.frame_count; ++i) + { + if (WebPDemuxGetFrame(demux, i + 1, &iter)) + { + frameDurations[i] = iter.duration; + WebPDemuxReleaseIterator(&iter); + } + else + { + frameDurations[i] = 0; + } + } + WebPDemuxDelete(demux); + } + + size_t frame_size = (size_t)anim_info.canvas_width * 4 * anim_info.canvas_height; + size_t all_frames_size = frame_size * anim_info.frame_count; + uint8_t *output_buffer = malloc(all_frames_size); + + if (output_buffer == NULL) + { + strncpy(output.header.err, "Failed to allocate memory for frames", sizeof(output.header.err) - 1); + write_output_message(&output); + WebPAnimDecoderDelete(dec); + free(frameDurations); + output.data.params.frameDurations = NULL; + goto cleanup; + } + + int frame_index = 0; + while (WebPAnimDecoderHasMoreFrames(dec)) + { + uint8_t *frame_rgba; + int timestamp; + if (!WebPAnimDecoderGetNext(dec, &frame_rgba, ×tamp)) + { + break; + } + memcpy(output_buffer + frame_index * frame_size, frame_rgba, frame_size); + frame_index++; + } + + write_output_message(&output); + + write_blob((uint32_t)input.header.id, output_buffer, (uint32_t)all_frames_size); + + WebPAnimDecoderDelete(dec); + free(output_buffer); + free(frameDurations); + output.data.params.frameDurations = NULL; + } + else + { + uint8_t *output_buffer = WebPDecodeRGBA(blob_data, (size_t)blob_size, &output.data.params.width, &output.data.params.height); + if (output_buffer == NULL) + { + strncpy(output.header.err, "Failed to decode WebP", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + output.data.params.stride = output.data.params.width * 4; + + write_output_message(&output); + + size_t output_size = (size_t)(output.data.params.stride * output.data.params.height); + write_blob((uint32_t)input.header.id, output_buffer, (uint32_t)output_size); + + WebPFree(output_buffer); + } + } + else if (strcmp(input.header.command, "config") == 0) + { + int width, height; + if (!WebPGetInfo(blob_data, (size_t)blob_size, &width, &height)) + { + strncpy(output.header.err, "Failed to get WebP info", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + output.data.params.width = width; + output.data.params.height = height; + + write_output_message(&output); + } + else if (strcmp(input.header.command, "encodeNRGBA") == 0) + { + WebPConfig config; + if (!initEncoderConfig(&config, input.data.options)) + { + strncpy(output.header.err, "Error initializing WebPConfig", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + size_t output_size = 0; + uint8_t *webp_data = NULL; + + if (input.data.params.frameDurations != NULL) + { + webp_data = encodeNRGBAAnimated(&config, input.data.params, blob_data, &output_size); + } + else + { + webp_data = encodeNRGBA(&config, blob_data, input.data.params.width, input.data.params.height, input.data.params.stride, &output_size); + } + + if (webp_data == NULL) + { + strncpy(output.header.err, "Error encoding NRGBA to WebP", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + write_output_message(&output); + write_blob((uint32_t)input.header.id, webp_data, (uint32_t)output_size); + free(webp_data); + } + else if (strcmp(input.header.command, "encodeGray") == 0) + { + WebPConfig config; + if (!initEncoderConfig(&config, input.data.options)) + { + strncpy(output.header.err, "Error initializing WebPConfig", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + size_t output_size = 0; + uint8_t *webp_data = encodeGray(&config, blob_data, input.data.params.width, input.data.params.height, input.data.params.stride, &output_size); + if (webp_data == NULL) + { + strncpy(output.header.err, "Error encoding Gray to WebP", sizeof(output.header.err) - 1); + write_output_message(&output); + goto cleanup; + } + + write_output_message(&output); + write_blob((uint32_t)input.header.id, webp_data, (uint32_t)output_size); + + free(webp_data); + } + else + { + snprintf(output.header.err, sizeof(output.header.err), "Unknown command: %s", input.header.command); + write_output_message(&output); + } + + cleanup: + if (blob_data != NULL) + { + free(blob_data); + } + if (input.data.params.frameDurations != NULL) + { + free(input.data.params.frameDurations); + } + } +} diff --git a/internal/warpc/warpc.go b/internal/warpc/warpc.go index 0d8d6a030..31f0a4807 100644 --- a/internal/warpc/warpc.go +++ b/internal/warpc/warpc.go @@ -21,12 +21,18 @@ import ( "errors" "fmt" "io" + "os" + "runtime" "strings" "sync" "sync/atomic" "time" + "github.com/bep/textandbinarywriter" + + "github.com/gohugoio/hugo/common/hstrings" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/common/maps" "golang.org/x/sync/errgroup" "github.com/tetratelabs/wazero" @@ -35,11 +41,19 @@ import ( "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" ) +const ( + MessageKindJSON string = "json" + MessageKindBlob string = "blob" +) + const currentVersion = 1 //go:embed wasm/quickjs.wasm var quickjsWasm []byte +//go:embed wasm/webp.wasm +var webpWasm []byte + // Header is in both the request and response. type Header struct { // Major version of the protocol. @@ -49,6 +63,16 @@ type Header struct { // Note that this only needs to be unique within the current request set time window. ID uint32 `json:"id"` + // Command is the command to execute. + Command string `json:"command"` + + // RequestKinds is a list of kinds in this RPC request, + // e.g. {"json", "blob"}, or {"json"}. + RequestKinds []string `json:"requestKinds"` + // ResponseKinds is a list of kinds expected in the response, + // e.g. {"json", "blob"}, or {"json"}. + ResponseKinds []string `json:"responseKinds"` + // Set in the response if there was an error. Err string `json:"err"` @@ -56,6 +80,37 @@ type Header struct { Warnings []string `json:"warnings,omitempty"` } +func (m *Header) init() error { + if m.ID == 0 { + return errors.New("ID must not be 0 (note that this must be unique within the current request set time window)") + } + if m.Version == 0 { + m.Version = currentVersion + } + if len(m.RequestKinds) == 0 { + m.RequestKinds = []string{string(MessageKindJSON)} + } + if len(m.ResponseKinds) == 0 { + m.ResponseKinds = []string{string(MessageKindJSON)} + } + if m.Version != currentVersion { + return fmt.Errorf("unsupported version: %d", m.Version) + } + for range 2 { + if len(m.RequestKinds) > 2 { + return fmt.Errorf("invalid number of request kinds: %d", len(m.RequestKinds)) + } + if len(m.ResponseKinds) > 2 { + return fmt.Errorf("invalid number of response kinds: %d", len(m.ResponseKinds)) + } + m.RequestKinds = hstrings.UniqueStringsReuse(m.RequestKinds) + m.ResponseKinds = hstrings.UniqueStringsReuse(m.ResponseKinds) + + } + + return nil +} + type Message[T any] struct { Header Header `json:"header"` Data T `json:"data"` @@ -65,6 +120,18 @@ func (m Message[T]) GetID() uint32 { return m.Header.ID } +func (m *Message[T]) init() error { + return m.Header.init() +} + +type SourceProvider interface { + GetSource() hugio.SizeReader +} + +type DestinationProvider interface { + GetDestination() io.Writer +} + type Dispatcher[Q, R any] interface { Execute(ctx context.Context, q Message[Q]) (Message[R], error) Close() error @@ -80,14 +147,17 @@ func (p *dispatcherPool[Q, R]) Close() error { } type dispatcher[Q, R any] struct { - zero Message[R] + zeroR Message[R] + + id atomic.Uint32 - mu sync.RWMutex - encMu sync.Mutex + mu sync.Mutex + encodeMu sync.Mutex pending map[uint32]*call[Q, R] - inOut *inOut + inOut *inOut + inGroup *errgroup.Group shutdown bool closing bool @@ -95,10 +165,23 @@ type dispatcher[Q, R any] struct { type inOut struct { sync.Mutex - stdin hugio.ReadWriteCloser - stdout hugio.ReadWriteCloser - dec *json.Decoder - enc *json.Encoder + stdin hugio.ReadWriteCloser + stdout io.WriteCloser + stdoutBinary hugio.ReadWriteCloser + dec *json.Decoder + enc *json.Encoder +} + +func (p *inOut) Close() error { + if err := p.stdin.Close(); err != nil { + return err + } + + // This will also close the underlying writers. + if err := p.stdout.Close(); err != nil { + return err + } + return nil } var ErrShutdown = fmt.Errorf("dispatcher is shutting down") @@ -127,17 +210,14 @@ func putTimer(t *time.Timer) { // Execute sends a request to the dispatcher and waits for the response. func (p *dispatcherPool[Q, R]) Execute(ctx context.Context, q Message[Q]) (Message[R], error) { d := p.getDispatcher() - if q.GetID() == 0 { - return d.zero, errors.New("ID must not be 0 (note that this must be unique within the current request set time window)") - } call, err := d.newCall(q) if err != nil { - return d.zero, err + return d.zeroR, err } if err := d.send(call); err != nil { - return d.zero, err + return d.zeroR, err } timer := getTimer(30 * time.Second) @@ -146,15 +226,15 @@ func (p *dispatcherPool[Q, R]) Execute(ctx context.Context, q Message[Q]) (Messa select { case call = <-call.donec: case <-p.donec: - return d.zero, p.Err() + return d.zeroR, p.Err() case <-ctx.Done(): - return d.zero, ctx.Err() + return d.zeroR, ctx.Err() case <-timer.C: - return d.zero, errors.New("timeout") + return d.zeroR, errors.New("timeout") } if call.err != nil { - return d.zero, call.err + return d.zeroR, call.err } resp, err := call.response, p.Err() @@ -162,13 +242,25 @@ func (p *dispatcherPool[Q, R]) Execute(ctx context.Context, q Message[Q]) (Messa if err == nil && resp.Header.Err != "" { err = errors.New(resp.Header.Err) } + return resp, err } func (d *dispatcher[Q, R]) newCall(q Message[Q]) (*call[Q, R], error) { + if q.Header.ID == 0 { + q.Header.ID = d.id.Add(1) + } + if err := q.init(); err != nil { + return nil, err + } + responseKinds := maps.NewMap[string, bool]() + for _, rk := range q.Header.ResponseKinds { + responseKinds.Set(rk, true) + } call := &call[Q, R]{ - donec: make(chan *call[Q, R], 1), - request: q, + donec: make(chan *call[Q, R], 1), + request: q, + responseKinds: responseKinds, } if d.shutdown || d.closing { @@ -185,48 +277,121 @@ func (d *dispatcher[Q, R]) newCall(q Message[Q]) (*call[Q, R], error) { } func (d *dispatcher[Q, R]) send(call *call[Q, R]) error { - d.mu.RLock() + d.mu.Lock() if d.closing || d.shutdown { - d.mu.RUnlock() + d.mu.Unlock() return ErrShutdown } - d.mu.RUnlock() + d.mu.Unlock() - d.encMu.Lock() - defer d.encMu.Unlock() + d.encodeMu.Lock() + defer d.encodeMu.Unlock() err := d.inOut.enc.Encode(call.request) if err != nil { return err } + if sp, ok := any(call.request.Data).(SourceProvider); ok { + source := sp.GetSource() + if source.Size() == 0 { + return errors.New("source size must be greater than 0") + } + + if err := textandbinarywriter.WriteBlobHeader(d.inOut.stdin, call.request.GetID(), uint32(source.Size())); err != nil { + return err + } + _, err := io.Copy(d.inOut.stdin, source) + if err != nil { + return err + } + } return nil } -func (d *dispatcher[Q, R]) input() { +func (d *dispatcher[Q, R]) inputBlobs() error { + var inputErr error + for { + id, length, err := textandbinarywriter.ReadBlobHeader(d.inOut.stdoutBinary) + if err != nil { + inputErr = err + break + } + + lr := &io.LimitedReader{ + R: d.inOut.stdoutBinary, + N: int64(length), + } + + call := d.pendingCall(id) + + if err := call.handleBlob(lr); err != nil { + inputErr = err + break + } + if lr.N != 0 { + inputErr = fmt.Errorf("blob %d: expected to read %d more bytes", id, lr.N) + break + } + if err := call.responseKinds.WithWriteLock( + func(m map[string]bool) error { + if _, ok := m[MessageKindBlob]; !ok { + return fmt.Errorf("unexpected blob response for %q call ID %d", call.request.Header.Command, id) + } + delete(m, MessageKindBlob) + if len(m) == 0 { + // Message exchange is complete. + d.mu.Lock() + delete(d.pending, id) + d.mu.Unlock() + call.done() + } + return nil + }); err != nil { + inputErr = err + break + } + } + + return inputErr +} + +func (d *dispatcher[Q, R]) inputJSON() error { var inputErr error for d.inOut.dec.More() { var r Message[R] if err := d.inOut.dec.Decode(&r); err != nil { - inputErr = fmt.Errorf("decoding response: %w", err) + inputErr = err break } - d.mu.Lock() - call, found := d.pending[r.GetID()] - if !found { - d.mu.Unlock() - panic(fmt.Errorf("call with ID %d not found", r.GetID())) + call := d.pendingCall(r.GetID()) + + if err := call.responseKinds.WithWriteLock( + func(m map[string]bool) error { + call.response = r + if _, ok := m[MessageKindJSON]; !ok { + return fmt.Errorf("unexpected JSON response for call ID %d", r.GetID()) + } + delete(m, MessageKindJSON) + if len(m) == 0 || r.Header.Err != "" { + // Message exchange is complete. + d.mu.Lock() + delete(d.pending, r.GetID()) + d.mu.Unlock() + call.done() + } + return nil + }); err != nil { + inputErr = err + break } - delete(d.pending, r.GetID()) - d.mu.Unlock() - call.response = r - call.done() + } // Terminate pending calls. d.shutdown = true if inputErr != nil { - isEOF := inputErr == io.EOF || strings.Contains(inputErr.Error(), "already closed") + isEOF := inputErr == io.EOF || inputErr == io.ErrClosedPipe || strings.Contains(inputErr.Error(), "already closed") if isEOF { if d.closing { inputErr = ErrShutdown @@ -242,13 +407,35 @@ func (d *dispatcher[Q, R]) input() { call.err = inputErr call.done() } + + return inputErr +} + +func (d *dispatcher[Q, R]) pendingCall(id uint32) *call[Q, R] { + d.mu.Lock() + defer d.mu.Unlock() + c, ok := d.pending[id] + if !ok { + panic(fmt.Errorf("call with ID %d not found", id)) + } + return c } type call[Q, R any] struct { - request Message[Q] - response Message[R] - err error - donec chan *call[Q, R] + request Message[Q] + response Message[R] + responseKinds *maps.Map[string, bool] + err error + donec chan *call[Q, R] +} + +func (c *call[Q, R]) handleBlob(r io.Reader) error { + dest := any(c.request.Data).(DestinationProvider).GetDestination() + if dest == nil { + panic("blob destination is not set") + } + _, err := io.Copy(dest, r) + return err } func (call *call[Q, R]) done() { @@ -269,6 +456,13 @@ type Binary struct { Data []byte } +type ProtocolType int + +const ( + ProtocolJSON ProtocolType = iota + 1 + ProtocolJSONAndBinary = iota +) + type Options struct { Ctx context.Context @@ -289,6 +483,18 @@ type Options struct { Memory int } +func (o *Options) init() error { + if o.Infof == nil { + o.Infof = func(format string, v ...any) { + } + } + if o.Warnf == nil { + o.Warnf = func(format string, v ...any) { + } + } + return nil +} + type CompileModuleContext struct { Opts Options Runtime wazero.Runtime @@ -395,18 +601,33 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { return nil, err } + dispatchers := make([]*dispatcher[Q, R], opts.PoolSize) + inOuts := make([]*inOut, opts.PoolSize) for i := range opts.PoolSize { - var stdin, stdout hugio.ReadWriteCloser - - stdin = hugio.NewPipeReadWriteCloser() - stdout = hugio.NewPipeReadWriteCloser() + var stdinPipe, stdoutBinary hugio.ReadWriteCloser + var stdout io.WriteCloser + var jsonr io.Reader + + stdinPipe = hugio.NewPipeReadWriteCloser() + stdoutPipe := hugio.NewPipeReadWriteCloser() + stdout = stdoutPipe + + var zero Q + if _, ok := any(zero).(DestinationProvider); ok { + stdoutBinary = hugio.NewPipeReadWriteCloser() + jsonr = stdoutPipe + stdout = textandbinarywriter.NewWriter(stdout, stdoutBinary) + } else { + jsonr = stdoutPipe + } inOuts[i] = &inOut{ - stdin: stdin, - stdout: stdout, - dec: json.NewDecoder(stdout), - enc: json.NewEncoder(stdin), + stdin: stdinPipe, + stdout: stdout, + stdoutBinary: stdoutBinary, + dec: json.NewDecoder(jsonr), + enc: json.NewEncoder(stdinPipe), } } @@ -417,13 +638,13 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { ) if opts.Runtime.Data != nil { - runtimeModule, err = r.CompileModule(ctx, opts.Runtime.Data) + runtimeModule, err = r.CompileModule(experimental.WithCompilationWorkers(ctx, runtime.GOMAXPROCS(0)/4), opts.Runtime.Data) if err != nil { return nil, err } } - mainModule, err = r.CompileModule(ctx, opts.Main.Data) + mainModule, err = r.CompileModule(experimental.WithCompilationWorkers(ctx, runtime.GOMAXPROCS(0)/4), opts.Main.Data) if err != nil { return nil, err } @@ -437,8 +658,10 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { for _, c := range inOuts { g.Go(func() error { var errBuff bytes.Buffer + stderr := io.MultiWriter(&errBuff, os.Stderr) ctx := context.WithoutCancel(ctx) - configBase := wazero.NewModuleConfig().WithStderr(&errBuff).WithStdout(c.stdout).WithStdin(c.stdin).WithStartFunctions() + // WithStartFunctions allows us to call the _start function manually. + configBase := wazero.NewModuleConfig().WithStderr(stderr).WithStdout(c.stdout).WithStdin(c.stdin).WithStartFunctions() if opts.Runtime.Data != nil { // This needs to be anonymous, it will be resolved in the import resolver below. runtimeInstance, err := r.InstantiateModule(ctx, runtimeModule, configBase.WithName("")) @@ -476,7 +699,7 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { } dp := &dispatcherPool[Q, R]{ - dispatchers: make([]*dispatcher[Q, R], len(inOuts)), + dispatchers: dispatchers, opts: opts, errc: make(chan error, 10), @@ -491,21 +714,34 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { }() for i := range inOuts { + ing, _ := errgroup.WithContext(ctx) d := &dispatcher[Q, R]{ pending: make(map[uint32]*call[Q, R]), inOut: inOuts[i], + inGroup: ing, + } + d.inGroup.Go(func() error { + return d.inputJSON() + }) + + if d.inOut.stdoutBinary != nil { + d.inGroup.Go(func() error { + return d.inputBlobs() + }) } - go d.input() dp.dispatchers[i] = d } dp.close = func() error { for _, d := range dp.dispatchers { d.closing = true - if err := d.inOut.stdin.Close(); err != nil { + if err := d.inOut.Close(); err != nil { return err } - if err := d.inOut.stdout.Close(); err != nil { + } + + for _, d := range dp.dispatchers { + if err := d.inGroup.Wait(); err != nil { return err } } @@ -546,12 +782,25 @@ func (d *lazyDispatcher[Q, R]) start() (Dispatcher[Q, R], error) { // Dispatchers holds all the dispatchers for the warpc package. type Dispatchers struct { katex *lazyDispatcher[KatexInput, KatexOutput] + webp *lazyDispatcher[WebpInput, WebpOutput] } func (d *Dispatchers) Katex() (Dispatcher[KatexInput, KatexOutput], error) { return d.katex.start() } +func (d *Dispatchers) Webp() (Dispatcher[WebpInput, WebpOutput], error) { + return d.webp.start() +} + +func (d *Dispatchers) NewWepCodec(quality int, hint string) (*WebpCodec, error) { + return &WebpCodec{ + d: d.Webp, + quality: quality, + hint: hint, + }, nil +} + func (d *Dispatchers) Close() error { var errs []error if d.katex.started { @@ -559,6 +808,11 @@ func (d *Dispatchers) Close() error { errs = append(errs, err) } } + if d.webp.started { + if err := d.webp.dispatcher.Close(); err != nil { + errs = append(errs, err) + } + } if len(errs) == 0 { return nil } @@ -568,7 +822,13 @@ func (d *Dispatchers) Close() error { // AllDispatchers creates all the dispatchers for the warpc package. // Note that the individual dispatchers are started lazily. // Remember to call Close on the returned Dispatchers when done. -func AllDispatchers(katexOpts Options) *Dispatchers { +func AllDispatchers(katexOpts, webpOpts Options) *Dispatchers { + if err := katexOpts.init(); err != nil { + panic(err) + } + if err := webpOpts.init(); err != nil { + panic(err) + } if katexOpts.Runtime.Data == nil { katexOpts.Runtime = Binary{Name: "javy_quickjs_provider_v2", Data: quickjsWasm} } @@ -576,13 +836,12 @@ func AllDispatchers(katexOpts Options) *Dispatchers { katexOpts.Main = Binary{Name: "renderkatex", Data: katexWasm} } - if katexOpts.Infof == nil { - katexOpts.Infof = func(format string, v ...any) { - // noop - } - } + webpOpts.Main = Binary{Name: "webp", Data: webpWasm} - return &Dispatchers{ + dispatchers := &Dispatchers{ katex: &lazyDispatcher[KatexInput, KatexOutput]{opts: katexOpts}, + webp: &lazyDispatcher[WebpInput, WebpOutput]{opts: webpOpts}, } + + return dispatchers } diff --git a/internal/warpc/wasm/webp.wasm b/internal/warpc/wasm/webp.wasm new file mode 100755 index 000000000..c99ae17cf Binary files /dev/null and b/internal/warpc/wasm/webp.wasm differ diff --git a/internal/warpc/watchjs.sh b/internal/warpc/watchjs.sh new file mode 100755 index 000000000..fbc90b648 --- /dev/null +++ b/internal/warpc/watchjs.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +trap exit SIGINT + +while true; do find . -type f -name "*.js" | entr -pd ./build.sh; done \ No newline at end of file diff --git a/internal/warpc/watchtestscripts.sh b/internal/warpc/watchtestscripts.sh deleted file mode 100755 index fbc90b648..000000000 --- a/internal/warpc/watchtestscripts.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -trap exit SIGINT - -while true; do find . -type f -name "*.js" | entr -pd ./build.sh; done \ No newline at end of file diff --git a/internal/warpc/watchwebp.sh b/internal/warpc/watchwebp.sh new file mode 100755 index 000000000..64d57e4c0 --- /dev/null +++ b/internal/warpc/watchwebp.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +trap exit SIGINT + + +while true; do find genwebp -type f -name "*.c" | entr -pd ./buildwebp.sh; done + diff --git a/internal/warpc/webp.go b/internal/warpc/webp.go new file mode 100644 index 000000000..f30588577 --- /dev/null +++ b/internal/warpc/webp.go @@ -0,0 +1,378 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package warpc + +import ( + "bytes" + "context" + "errors" + "fmt" + "image" + "image/color" + "image/draw" + "io" + + "github.com/gohugoio/hugo/common/himage" + "github.com/gohugoio/hugo/common/hugio" +) + +var ( + _ SourceProvider = WebpInput{} + _ DestinationProvider = WebpInput{} +) + +type CommonImageProcessingParams struct { + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Stride int `json:"stride,omitempty"` + + // For animated images. + FrameDurations []int `json:"frameDurations,omitempty"` + LoopCount int `json:"loopCount,omitempty"` +} + +/* +If you're reading this and questioning the protocol on top of WASM using JSON and streams instead of WAS Call's with pointers written to linear memory: + +- The goal of this is to eventually make it into one or more RPC plugin APIs. +- Passing pointers around has a number of challenges in that context. +- One would be that it's not possible to pass pointers to child processes (e.g. non-WASM plugins). + +Also, you would think that this JSON/streams approach would be significantly slower than using pointers directly, but in practice, +at least for WebP, the difference is negligible, see below output from a test run: + +[pointers] DecodeWebp took 18.168375ms +[pointers] EncodeWebp took 13.959458ms +[pointers] DecodeWebpConfig took 93.083µs + +[streams] DecodeWebp took 17.192917ms +[streams] EncodeWebp took 14.084792ms +[streams] DecodeWebpConfig took 54.334µs + +Also note that the placement of this code in this internal package is also temporary. We 1. Need to get the WASM RPC plugin infrastructure in place, and 2. Need to decide on the final API shape for image processing plugins. +*/ +type WebpInput struct { + Source hugio.SizeReader `json:"-"` // Will be sent in a separate stream. + Destination io.Writer `json:"-"` // Will be used to write the result to. + Options map[string]any `json:"options"` // Config options. + Params map[string]any `json:"params"` // Command params (width, height, etc.). +} + +func (w WebpInput) GetSource() hugio.SizeReader { + return w.Source +} + +func (w WebpInput) GetDestination() io.Writer { + return w.Destination +} + +type WebpOutput struct { + Params CommonImageProcessingParams `json:"params"` +} + +type WebpCodec struct { + d func() (Dispatcher[WebpInput, WebpOutput], error) + quality int + hint string +} + +// Decode reads a WEBP image from r and returns it as an image.Image. +// Note that animated WebP images are returnes as an himage.AnimatedImage. +func (d *WebpCodec) Decode(r io.Reader) (image.Image, error) { + dd, err := d.d() + if err != nil { + return nil, err + } + + source, err := hugio.ToSizeReader(r) + if err != nil { + return nil, err + } + + var destination bytes.Buffer + + // Commands: + // encodeNRGBA + // encodeGray + // decode + // config + message := Message[WebpInput]{ + Header: Header{ + Version: 1, + Command: "decode", + RequestKinds: []string{MessageKindJSON, MessageKindBlob}, + ResponseKinds: []string{MessageKindJSON, MessageKindBlob}, + }, + + Data: WebpInput{ + Source: source, + Destination: &destination, + Options: map[string]any{}, + }, + } + + out, err := dd.Execute(context.Background(), message) + if err != nil { + return nil, err + } + + w, h, stride := out.Data.Params.Width, out.Data.Params.Height, out.Data.Params.Stride + if w == 0 || h == 0 || stride == 0 { + return nil, fmt.Errorf("received invalid image dimensions: %dx%d stride %d", w, h, stride) + } + + if len(out.Data.Params.FrameDurations) > 0 { + // Animated WebP. + img := &WEBP{ + frameDurations: out.Data.Params.FrameDurations, + loopCount: out.Data.Params.LoopCount, + } + + frameSize := stride * h + frames := make([]image.Image, len(destination.Bytes())/frameSize) + for i := 0; i < len(frames); i++ { + frameBytes := destination.Bytes()[i*frameSize : (i+1)*frameSize] + frameImg := &image.RGBA{ + Pix: frameBytes, + Stride: stride, + Rect: image.Rect(0, 0, w, h), + } + frames[i] = frameImg + } + img.SetFrames(frames) + return img, nil + } + + img := &image.RGBA{ + Pix: destination.Bytes(), + Stride: stride, + Rect: image.Rect(0, 0, w, h), + } + + return img, nil +} + +func (d *WebpCodec) DecodeConfig(r io.Reader) (image.Config, error) { + dd, err := d.d() + if err != nil { + return image.Config{}, err + } + + // Avoid reading the entire image for config only. + const webpMaxHeaderSize = 32 + b := make([]byte, webpMaxHeaderSize) + _, err = r.Read(b) + if err != nil { + return image.Config{}, err + } + + message := Message[WebpInput]{ + Header: Header{ + Version: 1, + Command: "config", + RequestKinds: []string{MessageKindJSON, MessageKindBlob}, + ResponseKinds: []string{MessageKindJSON}, + }, + + Data: WebpInput{ + Source: bytes.NewReader(b), + }, + } + + out, err := dd.Execute(context.Background(), message) + if err != nil { + return image.Config{}, err + } + return image.Config{ + Width: out.Data.Params.Width, + Height: out.Data.Params.Height, + ColorModel: color.RGBAModel, + }, nil +} + +func (d *WebpCodec) Encode(w io.Writer, img image.Image) error { + b := img.Bounds() + if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 { + return errors.New("webp: image is too large to encode") + } + + dd, err := d.d() + if err != nil { + return err + } + + const ( + commandEncodeNRGBA = "encodeNRGBA" + commandEncodeGray = "encodeGray" + ) + + var ( + bounds = img.Bounds() + imageBytes []byte + stride int + frameDurations []int + loopCount int + command string + ) + + switch v := img.(type) { + case *image.RGBA: + imageBytes = v.Pix + stride = v.Stride + command = commandEncodeNRGBA + case *WEBP: + // Animated WebP. + frames := v.GetFrames() + if len(frames) == 0 { + return errors.New("webp: animated image has no frames") + } + firstFrame := frames[0] + bounds = firstFrame.Bounds() + nrgba := convertToNRGBA(firstFrame) + frameSize := nrgba.Stride * bounds.Dy() + imageBytes = make([]byte, frameSize*len(frames)) + stride = nrgba.Stride + for i, frame := range frames { + var nrgbaFrame *image.NRGBA + if i == 0 { + nrgbaFrame = nrgba + } else { + nrgbaFrame = convertToNRGBA(frame) + } + copy(imageBytes[i*frameSize:(i+1)*frameSize], nrgbaFrame.Pix) + } + frameDurations = v.GetFrameDurations() + loopCount = v.loopCount + command = commandEncodeNRGBA + case *image.Gray: + imageBytes = v.Pix + stride = v.Stride + command = commandEncodeGray + case himage.AnimatedImage: + frames := v.GetFrames() + if len(frames) == 0 { + return errors.New("webp: animated image has no frames") + } + firstFrame := frames[0] + bounds = firstFrame.Bounds() + nrgba := convertToNRGBA(firstFrame) + frameSize := nrgba.Stride * bounds.Dy() + imageBytes = make([]byte, frameSize*len(frames)) + stride = nrgba.Stride + for i, frame := range frames { + var nrgbaFrame *image.NRGBA + if i == 0 { + nrgbaFrame = nrgba + } else { + nrgbaFrame = convertToNRGBA(frame) + } + copy(imageBytes[i*frameSize:(i+1)*frameSize], nrgbaFrame.Pix) + } + frameDurations = v.GetFrameDurations() + loopCount = v.GetLoopCount() + command = commandEncodeNRGBA + default: + nrgba := convertToNRGBA(img) + imageBytes = nrgba.Pix + stride = nrgba.Stride + command = commandEncodeNRGBA + + } + + if len(imageBytes) == 0 { + return fmt.Errorf("no image bytes extracted from %T", img) + } + + // Commands: + // encodeNRGBA + // encodeGray + // decode + // config + message := Message[WebpInput]{ + Header: Header{ + Version: 1, + Command: command, + RequestKinds: []string{MessageKindJSON, MessageKindBlob}, + ResponseKinds: []string{MessageKindJSON, MessageKindBlob}, + }, + + Data: WebpInput{ + Source: bytes.NewReader(imageBytes), + Destination: w, + Options: map[string]any{ + "quality": d.quality, // a number between 0 and 100. Set to 0 for lossless. + "hint": d.hint, // drawing, icon, photo, picture, or text + "useSharpYuv": true, // Use sharp (and slow) RGB->YUV conversion. + }, + Params: map[string]any{ + "width": bounds.Max.X, + "height": bounds.Max.Y, + "stride": stride, + "frameDurations": frameDurations, + "loopCount": loopCount, + }, + }, + } + + _, err = dd.Execute(context.Background(), message) + if err != nil { + return err + } + return nil +} + +func convertToNRGBA(src image.Image) *image.NRGBA { + dst := image.NewNRGBA(src.Bounds()) + draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src) + return dst +} + +var _ himage.AnimatedImage = (*WEBP)(nil) + +// WEBP represents an animated WebP image. +// The naming deliberately matches the fields in the standard library image/gif package. +type WEBP struct { + image.Image // The first frame. + frames []image.Image + frameDurations []int + loopCount int +} + +func (w *WEBP) GetLoopCount() int { + return w.loopCount +} + +func (w *WEBP) GetFrames() []image.Image { + return w.frames +} + +func (w *WEBP) GetFrameDurations() []int { + return w.frameDurations +} + +func (w *WEBP) GetRaw() any { + return w +} + +func (w *WEBP) SetFrames(frames []image.Image) { + if len(frames) == 0 { + panic("frames cannot be empty") + } + w.frames = frames + w.Image = frames[0] +} + +func (w *WEBP) SetWidthHeight(width, height int) { + // No-op for WEBP. +} diff --git a/internal/warpc/webp_integration_test.go b/internal/warpc/webp_integration_test.go new file mode 100644 index 000000000..5e45e2491 --- /dev/null +++ b/internal/warpc/webp_integration_test.go @@ -0,0 +1,111 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package warpc_test + +import ( + "testing" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/hugolib" +) + +func TestWebPMisc(t *testing.T) { + files := ` +-- assets/sunrise.webp -- +sourcefilename: ../../resources/testdata/sunrise.webp +-- layouts/home.html -- +{{ $image := resources.Get "sunrise.webp" }} +{{ $resized := $image.Resize "123x456" }} +Original Width/Height: {{ $image.Width }}/{{ $image.Height }}| +Resized Width:/Height {{ $resized.Width }}/{{ $resized.Height }}| +Resized RelPermalink: {{ $resized.RelPermalink }}| +{{ $ic := images.Config "/assets/sunrise.webp" }} +ImageConfig: {{ printf "%d/%d" $ic.Width $ic.Height }}| +` + + b := hugolib.Test(t, files) + + b.ImageHelper("public/sunrise_hu_8dd1706a77fb35ce.webp").AssertFormat("webp").AssertIsAnimated(false) + + b.AssertFileContent("public/index.html", + "Original Width/Height: 1024/640|", + "Resized Width:/Height 123/456|", + "Resized RelPermalink: /sunrise_hu_8dd1706a77fb35ce.webp|", + "ImageConfig: 1024/640|", + ) +} + +func TestWebPEncodeGrayscale(t *testing.T) { + files := ` +-- assets/gopher.png -- +sourcefilename: ../../resources/testdata/bw-gopher.png +-- layouts/home.html -- +{{ $image := resources.Get "gopher.png" }} +{{ $resized := $image.Resize "123x456 webp" }} +Resized RelPermalink: {{ $resized.RelPermalink }}| +` + + b := hugolib.Test(t, files) + + b.ImageHelper("public/gopher_hu_f8d20fe200599f16.webp").AssertFormat("webp") +} + +func TestWebPInvalid(t *testing.T) { + files := ` +-- assets/invalid.webp -- +sourcefilename: ../../resources/testdata/webp/invalid.webp +-- layouts/home.html -- +{{ $image := resources.Get "invalid.webp" }} +{{ $resized := $image.Resize "123x456 webp" }} +Resized RelPermalink: {{ $resized.RelPermalink }}| +` + + b, err := hugolib.TestE(t, files) + + b.Assert(err, qt.IsNotNil) +} + +// This test isn't great, but we have golden tests to verify the output itself. +func TestWebPAnimation(t *testing.T) { + files := ` +-- hugo.toml -- +disableKinds = ["page", "section", "taxonomy", "term", "sitemap", "robotsTXT", "404"] +-- assets/anim.webp -- +sourcefilename: ../../resources/testdata/webp/anim.webp +-- assets/giphy.gif -- +sourcefilename: ../../resources/testdata/giphy.gif +-- layouts/home.html -- +{{ $webpAnim := resources.Get "anim.webp" }} +{{ $gifAnim := resources.Get "giphy.gif" }} +{{ ($webpAnim.Resize "100x100 webp").Publish }} +{{ ($webpAnim.Resize "100x100 gif").Publish }} +{{ ($gifAnim.Resize "100x100 gif").Publish }} +{{ ($gifAnim.Resize "100x100 webp").Publish }} + +` + + b := hugolib.Test(t, files) + + // Source animated gif: + // Frame durations in ms. + giphyFrameDurations := []int{200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200} + b.ImageHelper("public/giphy_hu_e4a5984f8835d617.webp").AssertFormat("webp").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(giphyFrameDurations) + b.ImageHelper("public/giphy_hu_87010e943ffb23b4.gif").AssertFormat("gif").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(giphyFrameDurations) + b.ImageHelper("public/giphy_hu_e4a5984f8835d617.webp").AssertFormat("webp").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(giphyFrameDurations) + + // Source animated webp: + animFrameDurations := []int{80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80} + b.ImageHelper("public/anim_hu_edc2f24aaad2cee6.webp").AssertFormat("webp").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(animFrameDurations) + b.ImageHelper("public/anim_hu_58eb49733894e7ce.gif").AssertFormat("gif").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(animFrameDurations) +} diff --git a/resources/image.go b/resources/image.go index c1f107b59..d487cd69b 100644 --- a/resources/image.go +++ b/resources/image.go @@ -19,8 +19,6 @@ import ( "image" "image/color" "image/draw" - "image/gif" - _ "image/png" "io" "os" "strings" @@ -30,6 +28,7 @@ import ( "github.com/gohugoio/hugo/cache/filecache" "github.com/gohugoio/hugo/common/hashing" + "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/paths" "github.com/disintegration/gift" @@ -40,9 +39,6 @@ import ( "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/images" - - // Blind import for image.Decode - _ "golang.org/x/image/webp" ) var ( @@ -115,7 +111,7 @@ func (i *imageResource) getExif() *exif.ExifInfo { defer f.Close() filename := i.getResourcePaths().Path() - x, err := i.getSpec().imaging.DecodeExif(filename, mf, f) + x, err := i.getSpec().Imaging.DecodeExif(filename, mf, f) if err != nil { i.getSpec().Logger.Warnf("Unable to decode Exif metadata from image: %s", i.Key()) return nil @@ -216,6 +212,7 @@ func (i *imageResource) Process(spec string) (images.ImageResource, error) { // filter and returns the transformed image. If one of width or height is 0, the image aspect // ratio is preserved. func (i *imageResource) Resize(spec string) (images.ImageResource, error) { + defer herrors.Recover() return i.processActionSpec(images.ActionResize, spec) } @@ -383,7 +380,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im } ci := i.clone(converted) - targetPath := i.relTargetPathFromConfig(conf, i.getSpec().imaging.Cfg.SourceHash) + targetPath := i.relTargetPathFromConfig(conf, i.getSpec().Imaging.Cfg.SourceHash) ci.setTargetPath(targetPath) ci.Format = conf.TargetFormat ci.setMediaType(conf.TargetFormat.MediaType()) @@ -396,15 +393,6 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im return img, nil } -type giphy struct { - image.Image - gif *gif.GIF -} - -func (g *giphy) GIF() *gif.GIF { - return g.gif -} - // DecodeImage decodes the image source into an Image. // This for internal use only. func (i *imageResource) DecodeImage() (image.Image, error) { @@ -414,15 +402,7 @@ func (i *imageResource) DecodeImage() (image.Image, error) { } defer f.Close() - if i.Format == images.GIF { - g, err := gif.DecodeAll(f) - if err != nil { - return nil, fmt.Errorf("failed to decode gif: %w", err) - } - return &giphy{gif: g, Image: g.Image[0]}, nil - } - img, _, err := image.Decode(f) - return img, err + return i.getSpec().Imaging.Codec.DecodeFormat(i.Format, f) } func (i *imageResource) clone(img image.Image) *imageResource { @@ -447,7 +427,7 @@ func (i *imageResource) getImageMetaCacheTargetPath() string { // Last increment: v0.130.0 when change to the new imagemeta library for Exif. const imageMetaVersionNumber = 2 - cfgHash := i.getSpec().imaging.Cfg.SourceHash + cfgHash := i.getSpec().Imaging.Cfg.SourceHash df := i.getResourcePaths() p1, _ := paths.FileAndExt(df.File) h := i.hash() diff --git a/resources/image_cache.go b/resources/image_cache.go index 1fc722609..ba0035b2c 100644 --- a/resources/image_cache.go +++ b/resources/image_cache.go @@ -37,7 +37,7 @@ func (c *ImageCache) getOrCreate( parent *imageResource, conf images.ImageConfig, createImage func() (*imageResource, image.Image, error), ) (*resourceAdapter, error) { - relTarget := parent.relTargetPathFromConfig(conf, parent.getSpec().imaging.Cfg.SourceHash) + relTarget := parent.relTargetPathFromConfig(conf, parent.getSpec().Imaging.Cfg.SourceHash) relTargetPath := relTarget.TargetPath() memKey := relTargetPath diff --git a/resources/image_extended_test.go b/resources/image_extended_test.go deleted file mode 100644 index 5865ce75b..000000000 --- a/resources/image_extended_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build extended - -package resources_test - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/htesting/hqt" - "github.com/gohugoio/hugo/media" -) - -func TestImageResizeWebP(t *testing.T) { - c := qt.New(t) - - _, image := fetchImage(c, "sunrise.webp") - - c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType) - c.Assert(image.RelPermalink(), qt.Equals, "/a/sunrise.webp") - c.Assert(image.ResourceType(), qt.Equals, "image") - exif := image.Exif() - c.Assert(exif, qt.Not(qt.IsNil)) - c.Assert(exif.Tags["Copyright"], qt.Equals, "Bjørn Erik Pedersen") - c.Assert(exif.Lat, hqt.IsSameFloat64, 36.59744166666667) - c.Assert(exif.Long, hqt.IsSameFloat64, -4.50846) - c.Assert(exif.Date.IsZero(), qt.Equals, false) - - resized, err := image.Resize("123x") - c.Assert(err, qt.IsNil) - c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType) - c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunrise_hu_a1deb893888915d9.webp") - c.Assert(resized.Width(), qt.Equals, 123) -} diff --git a/resources/images/codec.go b/resources/images/codec.go new file mode 100644 index 000000000..6d84a682f --- /dev/null +++ b/resources/images/codec.go @@ -0,0 +1,297 @@ +// Copyright 2025 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "bufio" + "errors" + "fmt" + "image" + "image/color/palette" + "image/draw" + "image/gif" + "image/jpeg" + "image/png" + "io" + + "github.com/gohugoio/hugo/common/himage" + "golang.org/x/image/bmp" + "golang.org/x/image/tiff" +) + +// Decoder defines the decoding of an image format. +// These matches the globalmpackage image functions. +type Decoder interface { + Decode(r io.Reader) (image.Image, error) + DecodeConfig(r io.Reader) (image.Config, error) +} + +// Encoder defines the encoding of an image format. +type Encoder interface { + Encode(w io.Writer, src image.Image) error +} + +type ToEncoder interface { + EncodeTo(conf ImageConfig, w io.Writer, src image.Image) error +} + +// CodecStdlib defines both decoding and encoding of an image format as defined by the standard library. +type CodecStdlib interface { + Decoder + Encoder +} + +// Codec is a generic image codec supporting multiple formats. +type Codec struct { + webp CodecStdlib +} + +func newCodec(webp CodecStdlib) *Codec { + return &Codec{webp: webp} +} + +func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error { + switch conf.TargetFormat { + case JPEG: + var rgba *image.RGBA + quality := conf.Quality + + if nrgba, ok := img.(*image.NRGBA); ok { + if nrgba.Opaque() { + rgba = &image.RGBA{ + Pix: nrgba.Pix, + Stride: nrgba.Stride, + Rect: nrgba.Rect, + } + } + } + if rgba != nil { + return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality}) + } + return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) + case PNG: + encoder := png.Encoder{CompressionLevel: png.DefaultCompression} + return encoder.Encode(w, img) + case GIF: + if anim, ok := img.(himage.AnimatedImage); ok { + if g, ok := anim.GetRaw().(*gif.GIF); ok { + return gif.EncodeAll(w, g) + } + + // Animated image, but not a GIF. Convert it. + frames := anim.GetFrames() + if len(frames) == 0 { + return gif.Encode(w, img, &gif.Options{NumColors: 256}) + } + + frameDurations := anim.GetFrameDurations() + if len(frameDurations) != len(frames) { + return errors.New("gif: number of frame durations does not match number of frames") + } + + outGif := &gif.GIF{ + Delay: himage.FrameDurationsToGifDelays(frameDurations), + } + + outGif.LoopCount = anim.GetLoopCount() + + for _, frame := range frames { + bounds := frame.Bounds() + palettedImage := image.NewPaletted(bounds, palette.Plan9) + draw.Draw(palettedImage, palettedImage.Rect, frame, bounds.Min, draw.Src) + outGif.Image = append(outGif.Image, palettedImage) + } + + return gif.EncodeAll(w, outGif) + } + return gif.Encode(w, img, &gif.Options{ + NumColors: 256, + }) + case TIFF: + return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true}) + case BMP: + return bmp.Encode(w, img) + case WEBP: + return d.webp.Encode(w, img) + default: + return errors.New("format not supported") + } +} + +func (d *Codec) DecodeFormat(f Format, r io.Reader) (image.Image, error) { + switch f { + case JPEG: + return jpeg.Decode(r) + case PNG: + return png.Decode(r) + case GIF: + g, err := gif.DecodeAll(r) + if err != nil { + return nil, fmt.Errorf("failed to decode gif: %w", err) + } + if len(g.Delay) > 1 { + return &giphy{gif: g, Image: g.Image[0]}, nil + } + return g.Image[0], nil + case TIFF: + return tiff.Decode(r) + case BMP: + return bmp.Decode(r) + case WEBP: + return d.webp.Decode(r) + default: + return nil, errors.New("format not supported") + } +} + +func (d *Codec) Decode(r io.Reader) (image.Image, error) { + rr := toPeekReader(r) + format, err := formatFromImage(rr) + if err != nil { + return nil, err + } + if format != 0 { + return d.DecodeFormat(format, rr) + } + + // Fallback to the standard image.Decode. + img, _, err := image.Decode(rr) + return img, err +} + +func (d *Codec) DecodeConfig(r io.Reader) (image.Config, string, error) { + rr := toPeekReader(r) + format, err := formatFromImage(rr) + if err != nil { + return image.Config{}, "", err + } + if format == WEBP { + cfg, err := d.webp.DecodeConfig(rr) + return cfg, "webp", err + } + + // Fallback to the standard image.DecodeConfig. + conf, name, err := image.DecodeConfig(rr) + return conf, name, err +} + +// toPeekReader converts an io.Reader to a peekReader. +func toPeekReader(r io.Reader) peekReader { + if rr, ok := r.(peekReader); ok { + return rr + } + return bufio.NewReader(r) +} + +// A peekReader is an io.Reader that can also peek ahead. +type peekReader interface { + io.Reader + Peek(int) ([]byte, error) +} + +const ( + // The WebP file header is 12 bytes long and starts with "RIFF" followed by + // 4 bytes indicating the file size, followed by "WEBP" and the VP8 chunk header. + // We use '?' as a wildcard for the 4 size bytes. + magicWebp = "RIFF????WEBPVP8" + // The GIF file header is 6 bytes long and starts with "GIF87a" or "GIF89a". + magicGif = "GIF8???" +) + +type magicFormat struct { + magic string + format Format +} + +var magicFormats = []magicFormat{ + {magic: magicWebp, format: WEBP}, + {magic: magicGif, format: GIF}, +} + +// formatFromImage determines the image format from the magic bytes. +// Note that this is only a partial implementation, +// as we currently only need WebP and GIF detection. +// The others can be handled by the standard library. +func formatFromImage(r peekReader) (Format, error) { + for _, mf := range magicFormats { + magicLen := len(mf.magic) + b, err := r.Peek(magicLen) + if err == nil && match(mf.magic, b) { + return mf.format, nil + } + } + return 0, nil +} + +func match(magic string, b []byte) bool { + if len(magic) != len(b) { + return false + } + for i := 0; i < len(magic); i++ { + if magic[i] != '?' && magic[i] != b[i] { + return false + } + } + return true +} + +var ( + _ himage.AnimatedImage = (*giphy)(nil) + _ himage.ImageConfigProvider = (*giphy)(nil) +) + +type giphy struct { + image.Image + gif *gif.GIF +} + +func (g *giphy) GetRaw() any { + return g.gif +} + +func (g *giphy) GetLoopCount() int { + return g.gif.LoopCount +} + +func (g *giphy) GetFrames() []image.Image { + frames := make([]image.Image, len(g.gif.Image)) + for i, frame := range g.gif.Image { + frames[i] = frame + } + return frames +} + +func (g *giphy) GetImageConfig() image.Config { + return g.gif.Config +} + +func (g *giphy) SetFrames(frames []image.Image) { + if len(frames) == 0 { + panic("frames cannot be empty") + } + g.gif.Image = make([]*image.Paletted, len(frames)) + for i, frame := range frames { + g.gif.Image[i] = frame.(*image.Paletted) + } + g.Image = g.gif.Image[0] +} + +func (g *giphy) SetWidthHeight(width, height int) { + g.gif.Config.Width = width + g.gif.Config.Height = height +} + +func (g *giphy) GetFrameDurations() []int { + return himage.GifDelaysToFrameDurations(g.gif.Delay) +} diff --git a/resources/images/config.go b/resources/images/config.go index 6fcd2e334..e2f0d5211 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -26,8 +26,6 @@ import ( "github.com/gohugoio/hugo/media" "github.com/mitchellh/mapstructure" - "github.com/bep/gowebp/libwebp/webpoptions" - "github.com/disintegration/gift" ) @@ -73,7 +71,7 @@ var ( // re-generation. imageFormatsVersions = map[Format]int{ PNG: 0, - WEBP: 0, + WEBP: 1, // Moved to WASM-based WebP encoder and decoder. GIF: 0, } @@ -96,12 +94,12 @@ var anchorPositions = map[string]gift.Anchor{ } // These encoding hints are currently only relevant for Webp. -var hints = map[string]webpoptions.EncodingPreset{ - "picture": webpoptions.EncodingPresetPicture, - "photo": webpoptions.EncodingPresetPhoto, - "drawing": webpoptions.EncodingPresetDrawing, - "icon": webpoptions.EncodingPresetIcon, - "text": webpoptions.EncodingPresetText, +var hints = map[string]bool{ + "picture": true, + "photo": true, + "drawing": true, + "icon": true, + "text": true, } var imageFilters = map[string]gift.Resampling{ @@ -234,8 +232,8 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin c.Anchor = pos } else if filter, ok := imageFilters[part]; ok { c.Filter = filter - } else if hint, ok := hints[part]; ok { - c.Hint = hint + } else if _, ok := hints[part]; ok { + c.Hint = part } else if part[0] == '#' { c.BgColor, err = hexStringToColorGo(part[1:]) if err != nil { @@ -302,8 +300,8 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin c.Filter = defaults.Config.ResampleFilter } - if c.Hint == 0 { - c.Hint = webpoptions.EncodingPresetPhoto + if c.Hint == "" { + c.Hint = "photo" } if c.Action != "" && c.Anchor == -1 { @@ -372,7 +370,7 @@ type ImageConfig struct { // Hint about what type of picture this is. Used to optimize encoding // when target is set to webp. - Hint webpoptions.EncodingPreset + Hint string Width int Height int @@ -390,7 +388,6 @@ func (cfg ImageConfig) Reanchor(a gift.Anchor) ImageConfig { type ImagingConfigInternal struct { BgColor color.Color - Hint webpoptions.EncodingPreset ResampleFilter gift.Resampling Anchor gift.Anchor @@ -424,7 +421,7 @@ func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error { // ImagingConfig contains default image processing configuration. This will be fetched // from site (or language) config. type ImagingConfig struct { - // Default image quality setting (1-100). Only used for JPEG images. + // Default image quality setting (1-100). Only used for JPEG and WebP images. Quality int // Resample filter to use in resize operations. diff --git a/resources/images/config_test.go b/resources/images/config_test.go index d3c9827bd..09aadeb62 100644 --- a/resources/images/config_test.go +++ b/resources/images/config_test.go @@ -131,7 +131,6 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a var c ImageConfig = GetDefaultImageConfig(nil) c.Action = action c.TargetFormat = PNG - c.Hint = 2 c.Width = width c.Height = height c.Quality = quality diff --git a/resources/images/image.go b/resources/images/image.go index c891b0168..ce9d5f4d5 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -19,25 +19,20 @@ import ( "image" "image/color" "image/draw" - "image/gif" - "image/jpeg" - "image/png" "io" "sync" - "github.com/bep/gowebp/libwebp/webpoptions" "github.com/bep/imagemeta" "github.com/bep/logg" "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/resources/images/webp" + "github.com/gohugoio/hugo/internal/warpc" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources/images/exif" "github.com/disintegration/gift" - "golang.org/x/image/bmp" - "golang.org/x/image/tiff" + "github.com/gohugoio/hugo/common/himage" "github.com/gohugoio/hugo/common/hugio" ) @@ -64,54 +59,7 @@ type Image struct { } func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error { - switch conf.TargetFormat { - case JPEG: - - var rgba *image.RGBA - quality := conf.Quality - - if nrgba, ok := img.(*image.NRGBA); ok { - if nrgba.Opaque() { - rgba = &image.RGBA{ - Pix: nrgba.Pix, - Stride: nrgba.Stride, - Rect: nrgba.Rect, - } - } - } - if rgba != nil { - return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality}) - } - return jpeg.Encode(w, img, &jpeg.Options{Quality: quality}) - case PNG: - encoder := png.Encoder{CompressionLevel: png.DefaultCompression} - return encoder.Encode(w, img) - - case GIF: - if giphy, ok := img.(Giphy); ok { - g := giphy.GIF() - return gif.EncodeAll(w, g) - } - return gif.Encode(w, img, &gif.Options{ - NumColors: 256, - }) - case TIFF: - return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true}) - - case BMP: - return bmp.Encode(w, img) - case WEBP: - return webp.Encode( - w, - img, webpoptions.EncodingOptions{ - Quality: conf.Quality, - EncodingPreset: webpoptions.EncodingPreset(conf.Hint), - UseSharpYuv: true, - }, - ) - default: - return errors.New("format not supported") - } + return i.Proc.Codec.EncodeTo(conf, w, img) } // Height returns i's height. @@ -146,7 +94,7 @@ func (i Image) WithSpec(s Spec) *Image { func (i *Image) InitConfig(r io.Reader) error { var err error i.configInit.Do(func() { - i.config, _, err = image.DecodeConfig(r) + i.config, _, err = i.Proc.Codec.DecodeConfig(r) }) return err } @@ -166,7 +114,7 @@ func (i *Image) initConfig() error { } defer f.Close() - i.config, _, err = image.DecodeConfig(f) + i.config, _, err = i.Proc.Codec.DecodeConfig(f) }) if err != nil { @@ -176,7 +124,7 @@ func (i *Image) initConfig() error { return nil } -func NewImageProcessor(warnl logg.LevelLogger, cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { +func NewImageProcessor(warnl logg.LevelLogger, wasmDispatchers *warpc.Dispatchers, cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { e := cfg.Config.Imaging.Exif exifDecoder, err := exif.NewDecoder( exif.WithDateDisabled(e.DisableDate), @@ -189,15 +137,26 @@ func NewImageProcessor(warnl logg.LevelLogger, cfg *config.ConfigNamespace[Imagi return nil, err } + webpCodec, err := wasmDispatchers.NewWepCodec(cfg.Config.Imaging.Quality, cfg.Config.Imaging.Hint) + if err != nil { + return nil, err + } + if webpCodec == nil { + return nil, errors.New("webp codec is not available") + } + imageCodec := newCodec(webpCodec) + return &ImageProcessor{ Cfg: cfg, exifDecoder: exifDecoder, + Codec: imageCodec, }, nil } type ImageProcessor struct { Cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] exifDecoder *exif.Decoder + Codec *Codec } // Filename is only used for logging. @@ -276,10 +235,11 @@ func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image. } func (p *ImageProcessor) resolveSrc(src image.Image, targetFormat Format) image.Image { - if giph, ok := src.(Giphy); ok { - g := giph.GIF() - if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) { - src = g.Image[0] + if animatedImage, ok := src.(himage.AnimatedImage); ok { + frames := animatedImage.GetFrames() + // If e.g. converting an animated GIF to JPEG, we only want the first frame. + if len(frames) < 2 || !targetFormat.SupportsAnimation() { + src = frames[0] } } return src @@ -288,25 +248,31 @@ func (p *ImageProcessor) resolveSrc(src image.Image, targetFormat Format) image. func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters ...gift.Filter) (image.Image, error) { filter := gift.New(filters...) - if giph, ok := src.(Giphy); ok { - g := giph.GIF() - if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) { - src = g.Image[0] + if anim, ok := src.(himage.AnimatedImage); ok { + frames := anim.GetFrames() + if len(frames) < 2 || !targetFormat.SupportsAnimation() { + src = frames[0] } else { var bounds image.Rectangle - firstFrame := g.Image[0] + firstFrame := frames[0] tmp := image.NewNRGBA(firstFrame.Bounds()) - for i := range g.Image { - gift.New().DrawAt(tmp, g.Image[i], g.Image[i].Bounds().Min, gift.OverOperator) + for i, frame := range frames { + gift.New().DrawAt(tmp, frame, frame.Bounds().Min, gift.OverOperator) bounds = filter.Bounds(tmp.Bounds()) - dst := image.NewPaletted(bounds, g.Image[i].Palette) + var dst draw.Image + if paletted, ok := frame.(*image.Paletted); ok { + // Gif. + dst = image.NewPaletted(bounds, paletted.Palette) + } else { + dst = image.NewNRGBA(bounds) + } filter.Draw(dst, tmp) - g.Image[i] = dst + frames[i] = dst } - g.Config.Width = bounds.Dx() - g.Config.Height = bounds.Dy() + anim.SetWidthHeight(bounds.Dx(), bounds.Dy()) + anim.SetFrames(frames) - return giph, nil + return anim, nil } } @@ -335,7 +301,7 @@ func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, Imagi } return ImageConfig{ Anchor: -1, // The real values start at 0. - Hint: defaults.Config.Hint, + Hint: "photo", Quality: defaults.Config.Imaging.Quality, } } @@ -383,6 +349,11 @@ func (f Format) SupportsTransparency() bool { return f != JPEG } +// SupportsAnimation reports whether the format supports animation. +func (f Format) SupportsAnimation() bool { + return f == GIF || f == WEBP +} + // DefaultExtension returns the default file extension of this format, starting with a dot. // For example: .jpg for JPEG func (f Format) DefaultExtension() string { @@ -416,8 +387,8 @@ type imageConfig struct { } func imageConfigFromImage(img image.Image) image.Config { - if giphy, ok := img.(Giphy); ok { - return giphy.GIF().Config + if cp, ok := img.(himage.ImageConfigProvider); ok { + return cp.GetImageConfig() } b := img.Bounds() return image.Config{Width: b.Max.X, Height: b.Max.Y} @@ -466,9 +437,3 @@ type ImageSource interface { DecodeImage() (image.Image, error) Key() string } - -// Giphy represents a GIF Image that may be animated. -type Giphy interface { - image.Image // The first frame. - GIF() *gif.GIF // All frames. -} diff --git a/resources/images/images_golden_integration_test.go b/resources/images/images_golden_integration_test.go index 751b3ce0d..96cdd2f79 100644 --- a/resources/images/images_golden_integration_test.go +++ b/resources/images/images_golden_integration_test.go @@ -342,6 +342,106 @@ Home. imagetesting.RunGolden(opts) } +func TestImagesGoldenProcessWebP(t *testing.T) { + t.Parallel() + + if imagetesting.SkipGoldenTests { + t.Skip("Skip golden test on this architecture") + } + + // Will be used as the base folder for generated images. + name := "process/webp" + + files := ` +-- hugo.toml -- +-- assets/highcontrast.webp -- +sourcefilename: ../testdata/webp/highcontrast.webp +-- assets/anim.webp -- +sourcefilename: ../testdata/webp/anim.webp +-- assets/fuzzycircle.webp -- +sourcefilename: ../testdata/webp/fuzzy-cirlcle-transparent-32.webp +-- assets/fuzzycircle.png -- +sourcefilename: ../testdata/fuzzy-cirlcle.png +-- assets/giphy.gif -- +sourcefilename: ../testdata/giphy.gif +-- assets/sunset.jpg -- +sourcefilename: ../testdata/sunset.jpg +-- layouts/home.html -- +Home. +{{ $fuzzyCircle := resources.Get "fuzzycircle.png" }} +{{ $highContrast := resources.Get "highcontrast.webp" }} +{{ $sunset := resources.Get "sunset.jpg" }} +{{ $sunsetGrayscale := $sunset.Filter (images.Grayscale) }} +{{ $animWebp := resources.Get "anim.webp" }} +{{ $giphy := resources.Get "giphy.gif" }} + +{{/* These are sorted. The end file name will be created from the spec + extension, so make sure these are unique. */}} +{{ template "process" (dict "spec" "crop 300x300 gif" "img" $animWebp) }} +{{ template "process" (dict "spec" "crop 300x300 smart" "img" $fuzzyCircle) }} +{{ template "process" (dict "spec" "crop 300x300" "img" $animWebp) }} +{{ template "process" (dict "spec" "crop 500x200 smart webp" "img" $sunset) }} +{{ template "process" (dict "spec" "crop 500x200 smart webp" "img" $sunset) }} +{{ template "process" (dict "spec" "fit 300x400 webp" "img" $sunsetGrayscale) }} +{{ template "process" (dict "spec" "fit 400x500 webp" "img" $sunset) }} +{{ template "process" (dict "spec" "gif" "img" $highContrast) }} +{{ template "process" (dict "spec" "png" "img" $highContrast) }} +{{ template "process" (dict "spec" "resize 300x300" "img" $giphy) }} +{{ template "process" (dict "spec" "resize 300x300 webp" "img" $giphy) }} +{{ template "process" (dict "spec" "resize 400x" "img" $highContrast) }} + +{{ define "process"}} +{{ $img := .img.Process .spec }} +{{ $ext := path.Ext $img.RelPermalink }} +{{ $name := printf "images/%s%s" (.spec | anchorize) $ext }} +{{ with $img | resources.Copy $name }} +{{ .Publish }} +{{ end }} +{{ end }} +` + + opts := imagetesting.DefaultGoldenOpts + opts.T = t + opts.Name = name + opts.Files = files + + imagetesting.RunGolden(opts) +} + +func TestImagesGoldenWebPAnimation(t *testing.T) { + t.Parallel() + + if imagetesting.SkipGoldenTests { + t.Skip("Skip golden test on this architecture") + } + + // Will be used as the base folder for generated images. + name := "webp/animation" + + files := ` +-- hugo.toml -- +disableKinds = ["page", "section", "taxonomy", "term", "sitemap", "robotsTXT", "404"] +-- assets/images/anim.webp -- +sourcefilename: ../testdata/webp/anim.webp +-- assets/images/giphy.gif -- +sourcefilename: ../testdata/giphy.gif +-- layouts/home.html -- +Home. +{{ $webpAnim := resources.Get "images/anim.webp" }} +{{ $gifAnim := resources.Get "images/giphy.gif" }} +{{ ($webpAnim.Resize "100x100 webp").Publish }} +{{ ($webpAnim.Resize "100x100 gif").Publish }} +{{ ($gifAnim.Resize "100x100 gif").Publish }} +{{ ($gifAnim.Resize "100x100 webp").Publish }} +` + + opts := imagetesting.DefaultGoldenOpts + opts.T = t + opts.Name = name + opts.Files = files + + imagetesting.RunGolden(opts) +} + func TestImagesGoldenMethods(t *testing.T) { t.Parallel() diff --git a/resources/images/imagetesting/testing.go b/resources/images/imagetesting/testing.go index f7a6af7ea..687cb6e83 100644 --- a/resources/images/imagetesting/testing.go +++ b/resources/images/imagetesting/testing.go @@ -15,12 +15,10 @@ package imagetesting import ( "image" - "image/gif" "io/fs" "os" "path/filepath" "runtime" - "strings" "testing" qt "github.com/frankban/quicktest" @@ -28,6 +26,7 @@ import ( "github.com/disintegration/gift" "github.com/gohugoio/hugo/common/hashing" + "github.com/gohugoio/hugo/common/himage" "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugofs" @@ -92,6 +91,9 @@ func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder { conf.NeedsOsFS = true conf.WorkingDir = opts.WorkingDir })) + + codec := c.H.ResourceSpec.Imaging.Codec + c.AssertFileContent("public/index.html", "Home.") outputDir := filepath.Join(c.H.Conf.WorkingDir(), "public", "images") @@ -118,20 +120,14 @@ func RunGolden(opts GoldenImageTestOpts) *hugolib.IntegrationTestBuilder { decodeAll := func(f *os.File) []image.Image { c.Helper() - var images []image.Image + v, err := codec.Decode(f) + c.Assert(err, qt.IsNil, qt.Commentf(f.Name())) - if strings.HasSuffix(f.Name(), ".gif") { - gif, err := gif.DecodeAll(f) - c.Assert(err, qt.IsNil, qt.Commentf(f.Name())) - images = make([]image.Image, len(gif.Image)) - for i, img := range gif.Image { - images[i] = img - } + if anim, ok := v.(himage.AnimatedImage); ok { + images = anim.GetFrames() } else { - img, _, err := image.Decode(f) - c.Assert(err, qt.IsNil, qt.Commentf(f.Name())) - images = append(images, img) + images = append(images, v) } return images } diff --git a/resources/images/testdata/images_golden/process/webp/crop-300x300-gif.gif b/resources/images/testdata/images_golden/process/webp/crop-300x300-gif.gif new file mode 100644 index 000000000..80e99d683 Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/crop-300x300-gif.gif differ diff --git a/resources/images/testdata/images_golden/process/webp/crop-300x300-smart.png b/resources/images/testdata/images_golden/process/webp/crop-300x300-smart.png new file mode 100644 index 000000000..95497d822 Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/crop-300x300-smart.png differ diff --git a/resources/images/testdata/images_golden/process/webp/crop-300x300.webp b/resources/images/testdata/images_golden/process/webp/crop-300x300.webp new file mode 100644 index 000000000..5904509df Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/crop-300x300.webp differ diff --git a/resources/images/testdata/images_golden/process/webp/crop-500x200-smart-webp.webp b/resources/images/testdata/images_golden/process/webp/crop-500x200-smart-webp.webp new file mode 100644 index 000000000..b608683ad Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/crop-500x200-smart-webp.webp differ diff --git a/resources/images/testdata/images_golden/process/webp/fit-300x400-webp.webp b/resources/images/testdata/images_golden/process/webp/fit-300x400-webp.webp new file mode 100644 index 000000000..e99f5805e Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/fit-300x400-webp.webp differ diff --git a/resources/images/testdata/images_golden/process/webp/fit-400x500-webp.webp b/resources/images/testdata/images_golden/process/webp/fit-400x500-webp.webp new file mode 100644 index 000000000..84de4c5dc Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/fit-400x500-webp.webp differ diff --git a/resources/images/testdata/images_golden/process/webp/gif.gif b/resources/images/testdata/images_golden/process/webp/gif.gif new file mode 100644 index 000000000..aadaf2ec2 Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/gif.gif differ diff --git a/resources/images/testdata/images_golden/process/webp/png.png b/resources/images/testdata/images_golden/process/webp/png.png new file mode 100644 index 000000000..b1f5eb89b Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/png.png differ diff --git a/resources/images/testdata/images_golden/process/webp/resize-300x300-webp.webp b/resources/images/testdata/images_golden/process/webp/resize-300x300-webp.webp new file mode 100644 index 000000000..bfc2050d4 Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/resize-300x300-webp.webp differ diff --git a/resources/images/testdata/images_golden/process/webp/resize-300x300.gif b/resources/images/testdata/images_golden/process/webp/resize-300x300.gif new file mode 100644 index 000000000..d887fc327 Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/resize-300x300.gif differ diff --git a/resources/images/testdata/images_golden/process/webp/resize-400x.webp b/resources/images/testdata/images_golden/process/webp/resize-400x.webp new file mode 100644 index 000000000..1b5136b23 Binary files /dev/null and b/resources/images/testdata/images_golden/process/webp/resize-400x.webp differ diff --git a/resources/images/testdata/images_golden/webp/animation/anim_hu_58eb49733894e7ce.gif b/resources/images/testdata/images_golden/webp/animation/anim_hu_58eb49733894e7ce.gif new file mode 100644 index 000000000..eeabfc836 Binary files /dev/null and b/resources/images/testdata/images_golden/webp/animation/anim_hu_58eb49733894e7ce.gif differ diff --git a/resources/images/testdata/images_golden/webp/animation/anim_hu_edc2f24aaad2cee6.webp b/resources/images/testdata/images_golden/webp/animation/anim_hu_edc2f24aaad2cee6.webp new file mode 100644 index 000000000..10ce746fe Binary files /dev/null and b/resources/images/testdata/images_golden/webp/animation/anim_hu_edc2f24aaad2cee6.webp differ diff --git a/resources/images/testdata/images_golden/webp/animation/giphy_hu_87010e943ffb23b4.gif b/resources/images/testdata/images_golden/webp/animation/giphy_hu_87010e943ffb23b4.gif new file mode 100644 index 000000000..66a6b73de Binary files /dev/null and b/resources/images/testdata/images_golden/webp/animation/giphy_hu_87010e943ffb23b4.gif differ diff --git a/resources/images/testdata/images_golden/webp/animation/giphy_hu_e4a5984f8835d617.webp b/resources/images/testdata/images_golden/webp/animation/giphy_hu_e4a5984f8835d617.webp new file mode 100644 index 000000000..cc11d24e5 Binary files /dev/null and b/resources/images/testdata/images_golden/webp/animation/giphy_hu_e4a5984f8835d617.webp differ diff --git a/resources/images/webp/webp.go b/resources/images/webp/webp.go deleted file mode 100644 index d7407214f..000000000 --- a/resources/images/webp/webp.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2021 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build extended - -package webp - -import ( - "image" - "io" - - "github.com/bep/gowebp/libwebp" - "github.com/bep/gowebp/libwebp/webpoptions" -) - -// Encode writes the Image m to w in Webp format with the given -// options. -func Encode(w io.Writer, m image.Image, o webpoptions.EncodingOptions) error { - return libwebp.Encode(w, m, o) -} - -// Supports returns whether webp encoding is supported in this build. -func Supports() bool { - return true -} diff --git a/resources/images/webp/webp_notavailable.go b/resources/images/webp/webp_notavailable.go deleted file mode 100644 index b617eeb51..000000000 --- a/resources/images/webp/webp_notavailable.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2021 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !extended - -package webp - -import ( - "image" - "io" - - "github.com/gohugoio/hugo/common/herrors" - - "github.com/bep/gowebp/libwebp/webpoptions" -) - -// Encode is only available in the extended version. -func Encode(w io.Writer, m image.Image, o webpoptions.EncodingOptions) error { - return herrors.ErrFeatureNotAvailable -} - -// Supports returns whether webp encoding is supported in this build. -func Supports() bool { - return false -} diff --git a/resources/resource_spec.go b/resources/resource_spec.go index 00fb637ae..c91166cc7 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -20,6 +20,7 @@ import ( "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/allconfig" + "github.com/gohugoio/hugo/internal/warpc" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/resources/internal" "github.com/gohugoio/hugo/resources/jsconfig" @@ -48,6 +49,7 @@ import ( func NewSpec( s *helpers.PathSpec, common *SpecCommon, // may be nil + wasmDispatchers *warpc.Dispatchers, fileCaches filecache.Caches, memCache *dynacache.Cache, incr identity.Incrementer, @@ -62,7 +64,7 @@ func NewSpec( imagesWarnl := logger.WarnCommand("images") - imaging, err := images.NewImageProcessor(imagesWarnl, imgConfig) + imaging, err := images.NewImageProcessor(imagesWarnl, wasmDispatchers, imgConfig) if err != nil { return nil, err } @@ -97,7 +99,7 @@ func NewSpec( ErrorSender: errorHandler, BuildClosers: buildClosers, Rebuilder: rebuilder, - imaging: imaging, + Imaging: imaging, ImageCache: newImageCache( fileCaches.ImageCache(), memCache, @@ -128,7 +130,7 @@ type Spec struct { ImageCache *ImageCache // Holds default filter settings etc. - imaging *images.ImageProcessor + Imaging *images.ImageProcessor ExecHelper *hexec.Exec @@ -203,7 +205,7 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro if isImage { ir := &imageResource{ - Image: images.NewImage(imgFormat, r.imaging, nil, gr), + Image: images.NewImage(imgFormat, r.Imaging, nil, gr), baseResource: gr, } ir.root = ir diff --git a/resources/testdata/bw-gopher.png b/resources/testdata/bw-gopher.png new file mode 100644 index 000000000..6e8e957ae Binary files /dev/null and b/resources/testdata/bw-gopher.png differ diff --git a/resources/testdata/webp/anim.webp b/resources/testdata/webp/anim.webp new file mode 100644 index 000000000..8a81c751b Binary files /dev/null and b/resources/testdata/webp/anim.webp differ diff --git a/resources/testdata/webp/bw-gopher.webp b/resources/testdata/webp/bw-gopher.webp new file mode 100644 index 000000000..188a0a0c5 Binary files /dev/null and b/resources/testdata/webp/bw-gopher.webp differ diff --git a/resources/testdata/webp/fuzzy-cirlcle-transparent-32.webp b/resources/testdata/webp/fuzzy-cirlcle-transparent-32.webp new file mode 100644 index 000000000..fa9d1720f Binary files /dev/null and b/resources/testdata/webp/fuzzy-cirlcle-transparent-32.webp differ diff --git a/resources/testdata/webp/highcontrast.webp b/resources/testdata/webp/highcontrast.webp new file mode 100644 index 000000000..df4915196 Binary files /dev/null and b/resources/testdata/webp/highcontrast.webp differ diff --git a/resources/testdata/webp/invalid.webp b/resources/testdata/webp/invalid.webp new file mode 100644 index 000000000..e69de29bb diff --git a/tpl/images/images.go b/tpl/images/images.go index 6296a7214..7c441fee0 100644 --- a/tpl/images/images.go +++ b/tpl/images/images.go @@ -29,14 +29,6 @@ import ( "github.com/mitchellh/mapstructure" "rsc.io/qr" - // Importing image codecs for image.DecodeConfig - _ "image/gif" - _ "image/jpeg" - _ "image/png" - - // Import webp codec - _ "golang.org/x/image/webp" - "github.com/gohugoio/hugo/deps" "github.com/spf13/afero" "github.com/spf13/cast" @@ -102,7 +94,7 @@ func (ns *Namespace) Config(path any) (image.Config, error) { } defer f.Close() - config, _, err = image.DecodeConfig(f) + config, _, err = ns.deps.ResourceSpec.Imaging.Codec.DecodeConfig(f) if err != nil { return config, err }