--- /dev/null
+// 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
+}
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
// 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"),
)
}
}
// 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.
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)
"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"
)
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)
+// 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 (
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)
}
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
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
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=
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=
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=
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
},
)
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
})
}
}
"encoding/base64"
"errors"
"fmt"
+ "image"
"io"
"math/rand"
"os"
"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"
}
}
+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...)
}
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)
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
})
}
}
}
+ compilationCacheDir := filepath.Join(conf.Dirs().CacheDir, "_warpc")
+
firstSiteDeps := &deps.Deps{
Fs: cfg.Fs,
Log: logger,
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,
},
),
}
+++ /dev/null
-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
--- /dev/null
+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
--- /dev/null
+pushd genwebp
+make || exit 1
+popd
+touch webp_integration_test.go
\ No newline at end of file
+++ /dev/null
-// 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
-}
--- /dev/null
+// 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
+}
--- /dev/null
+build/
+libwebp/
\ No newline at end of file
--- /dev/null
+# 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)
--- /dev/null
+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
--- /dev/null
+{
+ "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"
+ ]
+}
--- /dev/null
+/*
+ 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <math.h>
+
+#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;
+}
--- /dev/null
+/*
+ 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 <stddef.h> /* 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
--- /dev/null
+// 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 <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <webp/encode.h>
+#include <webp/decode.h>
+#include "webp/demux.h"
+#include <webp/mux.h>
+#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);
+ }
+ }
+}
"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"
"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.
// 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"`
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"`
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
}
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
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")
// 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)
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()
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 {
}
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
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() {
Data []byte
}
+type ProtocolType int
+
+const (
+ ProtocolJSON ProtocolType = iota + 1
+ ProtocolJSONAndBinary = iota
+)
+
type Options struct {
Ctx context.Context
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
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),
}
}
)
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
}
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(""))
}
dp := &dispatcherPool[Q, R]{
- dispatchers: make([]*dispatcher[Q, R], len(inOuts)),
+ dispatchers: dispatchers,
opts: opts,
errc: make(chan error, 10),
}()
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
}
}
// 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 {
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
}
// 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}
}
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
}
--- /dev/null
+#!/bin/bash
+
+trap exit SIGINT
+
+while true; do find . -type f -name "*.js" | entr -pd ./build.sh; done
\ No newline at end of file
+++ /dev/null
-#!/bin/bash
-
-trap exit SIGINT
-
-while true; do find . -type f -name "*.js" | entr -pd ./build.sh; done
\ No newline at end of file
--- /dev/null
+#!/bin/bash
+
+trap exit SIGINT
+
+
+while true; do find genwebp -type f -name "*.c" | entr -pd ./buildwebp.sh; done
+
--- /dev/null
+// 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.
+}
--- /dev/null
+// 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)
+}
"image"
"image/color"
"image/draw"
- "image/gif"
- _ "image/png"
"io"
"os"
"strings"
"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"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/resources/images"
-
- // Blind import for image.Decode
- _ "golang.org/x/image/webp"
)
var (
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
// 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)
}
}
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())
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) {
}
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 {
// 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()
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
+++ /dev/null
-// 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)
-}
--- /dev/null
+// 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)
+}
"github.com/gohugoio/hugo/media"
"github.com/mitchellh/mapstructure"
- "github.com/bep/gowebp/libwebp/webpoptions"
-
"github.com/disintegration/gift"
)
// re-generation.
imageFormatsVersions = map[Format]int{
PNG: 0,
- WEBP: 0,
+ WEBP: 1, // Moved to WASM-based WebP encoder and decoder.
GIF: 0,
}
}
// 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{
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 {
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 {
// 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
type ImagingConfigInternal struct {
BgColor color.Color
- Hint webpoptions.EncodingPreset
ResampleFilter gift.Resampling
Anchor gift.Anchor
// 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.
var c ImageConfig = GetDefaultImageConfig(nil)
c.Action = action
c.TargetFormat = PNG
- c.Hint = 2
c.Width = width
c.Height = height
c.Quality = quality
"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"
)
}
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.
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
}
}
defer f.Close()
- i.config, _, err = image.DecodeConfig(f)
+ i.config, _, err = i.Proc.Codec.DecodeConfig(f)
})
if err != nil {
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),
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.
}
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
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
}
}
}
return ImageConfig{
Anchor: -1, // The real values start at 0.
- Hint: defaults.Config.Hint,
+ Hint: "photo",
Quality: defaults.Config.Imaging.Quality,
}
}
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 {
}
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}
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.
-}
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()
import (
"image"
- "image/gif"
"io/fs"
"os"
"path/filepath"
"runtime"
- "strings"
"testing"
qt "github.com/frankban/quicktest"
"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"
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")
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
}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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
-}
"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"
func NewSpec(
s *helpers.PathSpec,
common *SpecCommon, // may be nil
+ wasmDispatchers *warpc.Dispatchers,
fileCaches filecache.Caches,
memCache *dynacache.Cache,
incr identity.Incrementer,
imagesWarnl := logger.WarnCommand("images")
- imaging, err := images.NewImageProcessor(imagesWarnl, imgConfig)
+ imaging, err := images.NewImageProcessor(imagesWarnl, wasmDispatchers, imgConfig)
if err != nil {
return nil, err
}
ErrorSender: errorHandler,
BuildClosers: buildClosers,
Rebuilder: rebuilder,
- imaging: imaging,
+ Imaging: imaging,
ImageCache: newImageCache(
fileCaches.ImageCache(),
memCache,
ImageCache *ImageCache
// Holds default filter settings etc.
- imaging *images.ImageProcessor
+ Imaging *images.ImageProcessor
ExecHelper *hexec.Exec
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
"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"
}
defer f.Close()
- config, _, err = image.DecodeConfig(f)
+ config, _, err = ns.deps.ResourceSpec.Imaging.Codec.DecodeConfig(f)
if err != nil {
return config, err
}