├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── lib ├── LICENSE.libwebp ├── Makefile ├── webp.c └── webp.wasm.gz ├── purego_darwin.go ├── purego_other.go ├── purego_unix.go ├── purego_windows.go ├── testdata ├── anim.webp ├── test.png └── test.webp ├── webp.go ├── webp_dynamic.go ├── webp_test.go └── webp_wazero.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.23.x] 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.os }} 10 | env: 11 | CGO_ENABLED: 0 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | - name: Install Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Test 20 | run: go test 21 | 22 | test-dynamic: 23 | strategy: 24 | matrix: 25 | go-version: [1.23.x] 26 | os: [ubuntu-latest] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | - name: Install Go 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: ${{ matrix.go-version }} 35 | - name: Install package 36 | run: | 37 | sudo apt-get update -y; sudo apt-get -y install libwebp-dev 38 | - name: Test 39 | run: go test 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 gen2brain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## webp 2 | [![Status](https://github.com/gen2brain/webp/actions/workflows/test.yml/badge.svg)](https://github.com/gen2brain/webp/actions) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/gen2brain/webp.svg)](https://pkg.go.dev/github.com/gen2brain/webp) 4 | 5 | Go encoder/decoder for [WebP Image File Format](https://en.wikipedia.org/wiki/WebP) with support for animated WebP images (decode only). 6 | 7 | Based on [libwebp](https://github.com/webmproject/libwebp) compiled to [WASM](https://en.wikipedia.org/wiki/WebAssembly) and used with [wazero](https://wazero.io/) runtime (CGo-free). 8 | 9 | The library will first try to use a dynamic/shared library (if installed) via [purego](https://github.com/ebitengine/purego) and will fall back to WASM. 10 | 11 | ### Build tags 12 | 13 | * `nodynamic` - do not use dynamic/shared library (use only WASM) 14 | 15 | ### Benchmark 16 | 17 | ``` 18 | goos: linux 19 | goarch: amd64 20 | pkg: github.com/gen2brain/webp 21 | cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz 22 | 23 | BenchmarkDecodeStd-8 157 7639585 ns/op 473683 B/op 13 allocs/op 24 | BenchmarkDecodeWasm-8 156 7799653 ns/op 2614793 B/op 316 allocs/op 25 | BenchmarkDecodeDynamic-8 344 3497863 ns/op 943356 B/op 58 allocs/op 26 | BenchmarkDecodeTranspiled-8 (1) 138 8562133 ns/op 1335622 B/op 52 allocs/op 27 | BenchmarkDecodeCGo1-8 (2) 300 3897300 ns/op 1333630 B/op 21 allocs/op 28 | BenchmarkDecodeCGo2-8 (3) 314 3801195 ns/op 1334020 B/op 22 allocs/op 29 | 30 | BenchmarkEncodeWasm-8 12 96123599 ns/op 4857356 B/op 298 allocs/op 31 | BenchmarkEncodeDynamic-8 55 19022243 ns/op 19888 B/op 42 allocs/op 32 | BenchmarkEncodeTranspiled-8 (1) 18 60042805 ns/op 76104 B/op 36 allocs/op 33 | BenchmarkEncodeCGo1-8 (2) 31 32538122 ns/op 3213497 B/op 524294 allocs/op 34 | BenchmarkEncodeCGo2-8 (3) 52 22482704 ns/op 26043 B/op 5 allocs/op 35 | ``` 36 | 37 | - `(1)` [git.sr.ht/~jackmordaunt/go-libwebp](https://git.sr.ht/~jackmordaunt/go-libwebp) 38 | - `(2)` [github.com/chai2010/webp](https://github.com/chai2010/webp) 39 | - `(3)` [github.com/kolesa-team/go-webp](https://github.com/kolesa-team/go-webp) 40 | 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gen2brain/webp 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/ebitengine/purego v0.8.3 7 | github.com/tetratelabs/wazero v1.9.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc= 2 | github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 3 | github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= 4 | github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 5 | -------------------------------------------------------------------------------- /lib/LICENSE.libwebp: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Google Inc. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | * Neither the name of Google nor the names of its contributors may 16 | be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /lib/Makefile: -------------------------------------------------------------------------------- 1 | LIBWEBP_VERSION = v1.4.0 2 | 3 | LIBWEBP_SRC = $(PWD)/libwebp 4 | LIBWEBP_BUILD = $(LIBWEBP_SRC)/build 5 | 6 | WASI_SDK_PATH ?= /opt/wasi-sdk 7 | export CC = $(WASI_SDK_PATH)/bin/clang --sysroot=$(WASI_SDK_PATH)/share/wasi-sysroot 8 | export CFLAGS = -msimd128 9 | 10 | CMAKE_TOOLCHAIN_FILE=$(WASI_SDK_PATH)/share/cmake/wasi-sdk.cmake 11 | 12 | BIN := webp.wasm 13 | 14 | all: $(BIN) 15 | 16 | $(LIBWEBP_SRC): 17 | git clone -b $(LIBWEBP_VERSION) --depth 1 --recursive --jobs `nproc` https://github.com/webmproject/libwebp 18 | mkdir -p $(LIBWEBP_BUILD) 19 | test -d $@ 20 | 21 | $(LIBWEBP_BUILD)/libwebp.a: $(LIBWEBP_SRC) 22 | cd $(LIBWEBP_BUILD); \ 23 | cmake $(LIBWEBP_SRC) \ 24 | -DCMAKE_BUILD_TYPE=Release \ 25 | -DBUILD_SHARED_LIBS=0 \ 26 | -DWEBP_ENABLE_SIMD_DEFAULT=1 \ 27 | -DWEBP_BUILD_EXTRAS=0 \ 28 | -DWEBP_USE_THREAD=0 \ 29 | -DWEBP_BUILD_ANIM_UTILS=0 \ 30 | -DWEBP_BUILD_CWEBP=0 \ 31 | -DWEBP_BUILD_DWEBP=0 \ 32 | -DWEBP_BUILD_IMG2WEBP=0 \ 33 | -DWEBP_BUILD_WEBPINFO=0 \ 34 | -DWEBP_BUILD_WEBPMUX=0 \ 35 | -DCMAKE_TOOLCHAIN_FILE=$(CMAKE_TOOLCHAIN_FILE) 36 | 37 | cd $(LIBWEBP_BUILD); \ 38 | $(MAKE) -j$(shell nproc) VERBOSE=1 39 | 40 | $(BIN): $(LIBWEBP_BUILD)/libwebp.a 41 | $(CC) \ 42 | -O3 \ 43 | -Wl,--no-entry \ 44 | -Wl,--export=malloc \ 45 | -Wl,--export=free \ 46 | -Wl,--export=decode \ 47 | -Wl,--export=encode \ 48 | -mexec-model=reactor \ 49 | -mnontrapping-fptoint \ 50 | -I${LIBWEBP_SRC}/src \ 51 | -I${LIBWEBP_BUILD}/src \ 52 | -o $@ \ 53 | -Wall \ 54 | webp.c \ 55 | ${LIBWEBP_BUILD}/libwebpdemux.a \ 56 | ${LIBWEBP_BUILD}/libwebp.a \ 57 | ${LIBWEBP_BUILD}/libsharpyuv.a 58 | 59 | .PHONY: clean 60 | 61 | clean: 62 | -rm -rf $(LIBWEBP_SRC) 63 | -------------------------------------------------------------------------------- /lib/webp.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "webp/decode.h" 5 | #include "webp/encode.h" 6 | #include "webp/demux.h" 7 | 8 | int decode(uint8_t *webp_in, int webp_in_size, int config_only, int decode_all, uint32_t *width, uint32_t *height, uint32_t *count, uint32_t *animation, uint8_t *delay, uint8_t *out); 9 | uint8_t* encode(uint8_t *rgb_in, int width, int height, size_t *size, int colorspace, int quality, int method, int lossless, int exact); 10 | 11 | int decode(uint8_t *webp_in, int webp_in_size, int config_only, int decode_all, uint32_t *width, uint32_t *height, uint32_t *count, uint32_t *animation, uint8_t *delay, uint8_t *out) { 12 | 13 | WebPData data; 14 | data.bytes = webp_in; 15 | data.size = webp_in_size; 16 | 17 | WebPDecoderConfig config; 18 | if(!WebPInitDecoderConfig(&config)) { 19 | return 0; 20 | } 21 | 22 | if(WebPGetFeatures(data.bytes, data.size, &config.input) != VP8_STATUS_OK) { 23 | return 0; 24 | } 25 | 26 | *width = config.input.width; 27 | *height = config.input.height; 28 | *animation = config.input.has_animation; 29 | 30 | if(config_only && !decode_all) { 31 | *count = 1; 32 | 33 | return 1; 34 | } 35 | 36 | if(decode_all || config.input.has_animation) { 37 | WebPAnimDecoderOptions options; 38 | WebPAnimDecoderOptionsInit(&options); 39 | options.color_mode = MODE_rgbA; 40 | 41 | WebPAnimDecoder* dec = WebPAnimDecoderNew(&data, &options); 42 | 43 | WebPAnimInfo info; 44 | WebPAnimDecoderGetInfo(dec, &info); 45 | 46 | *count = info.frame_count; 47 | 48 | if(config_only) { 49 | WebPAnimDecoderDelete(dec); 50 | return 1; 51 | } 52 | 53 | int frame = 0, timestamp = 0, timestampPrev = 0, duration = 0; 54 | 55 | uint8_t* buf; 56 | int buf_size = info.canvas_width * info.canvas_height * 4; 57 | 58 | while(WebPAnimDecoderHasMoreFrames(dec)) { 59 | if(!WebPAnimDecoderGetNext(dec, &buf, ×tamp)) { 60 | WebPAnimDecoderDelete(dec); 61 | return 0; 62 | } 63 | 64 | memcpy(out + buf_size*frame, buf, buf_size); 65 | 66 | duration = timestamp - timestampPrev; 67 | memcpy(delay + sizeof(int)*frame, &duration, sizeof(int)); 68 | timestampPrev = timestamp; 69 | 70 | if(!decode_all) { 71 | break; 72 | } 73 | 74 | frame++; 75 | } 76 | 77 | WebPAnimDecoderDelete(dec); 78 | return 1; 79 | } 80 | 81 | int w = *width; 82 | int h = *height; 83 | int cw = (w+1)/2; 84 | int ch = (h+1)/2; 85 | 86 | int i0 = 1*w*h + 0*cw*ch; 87 | int i1 = 1*w*h + 1*cw*ch; 88 | int i2 = 1*w*h + 2*cw*ch; 89 | int i3 = 2*w*h + 2*cw*ch; 90 | 91 | config.output.colorspace = MODE_YUVA; 92 | config.output.is_external_memory = 1; 93 | 94 | config.output.u.YUVA.y = &out[0]; 95 | config.output.u.YUVA.y_size = i0; 96 | config.output.u.YUVA.y_stride = w; 97 | 98 | config.output.u.YUVA.u = &out[i0]; 99 | config.output.u.YUVA.u_size = i1; 100 | config.output.u.YUVA.u_stride = cw; 101 | 102 | config.output.u.YUVA.v = &out[i1]; 103 | config.output.u.YUVA.v_size = i2; 104 | config.output.u.YUVA.v_stride = cw; 105 | 106 | config.output.u.YUVA.a = &out[i2]; 107 | config.output.u.YUVA.a_size = i3; 108 | config.output.u.YUVA.a_stride = w; 109 | 110 | if(WebPDecode(data.bytes, data.size, &config) != VP8_STATUS_OK) { 111 | WebPFreeDecBuffer(&config.output); 112 | return 0; 113 | } 114 | 115 | WebPFreeDecBuffer(&config.output); 116 | return 1; 117 | } 118 | 119 | uint8_t* encode(uint8_t *in, int w, int h, size_t *size, int colorspace, int quality, int method, int lossless, int exact) { 120 | uint8_t *out = NULL; 121 | 122 | WebPConfig config; 123 | if(!WebPConfigInit(&config)) { 124 | return out; 125 | } 126 | 127 | config.quality = quality; 128 | config.method = method; 129 | config.lossless = lossless; 130 | config.exact = exact; 131 | 132 | int cw = (w+1)/2; 133 | int ch = (h+1)/2; 134 | 135 | int i0 = 1*w*h + 0*cw*ch; 136 | int i1 = 1*w*h + 1*cw*ch; 137 | int i2 = 1*w*h + 2*cw*ch; 138 | 139 | WebPPicture picture; 140 | if(!WebPPictureInit(&picture)) { 141 | return out; 142 | } 143 | 144 | picture.width = w; 145 | picture.height = h; 146 | 147 | if(colorspace == WEBP_YUV420A) { 148 | picture.use_argb = 0; 149 | picture.colorspace = colorspace; 150 | picture.y = &in[0]; 151 | picture.u = &in[i0]; 152 | picture.v = &in[i1]; 153 | picture.a = &in[i2]; 154 | picture.y_stride = w; 155 | picture.uv_stride = cw; 156 | picture.a_stride = w; 157 | } else { 158 | picture.use_argb = 1; 159 | picture.argb_stride = w * 4; 160 | 161 | if(!WebPPictureImportRGBA(&picture, in, picture.argb_stride)) { 162 | WebPPictureFree(&picture); 163 | return out; 164 | } 165 | } 166 | 167 | WebPMemoryWriter writer; 168 | picture.writer = WebPMemoryWrite; 169 | picture.custom_ptr = &writer; 170 | WebPMemoryWriterInit(&writer); 171 | 172 | if(!WebPEncode(&config, &picture)) { 173 | WebPPictureFree(&picture); 174 | WebPMemoryWriterClear(&writer); 175 | return out; 176 | } 177 | 178 | *size = writer.size; 179 | out = writer.mem; 180 | 181 | WebPPictureFree(&picture); 182 | writer.mem = NULL; 183 | WebPMemoryWriterClear(&writer); 184 | 185 | return out; 186 | } 187 | -------------------------------------------------------------------------------- /lib/webp.wasm.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gen2brain/webp/3a7e6722e66f8f12f363995208b887acee9d0122/lib/webp.wasm.gz -------------------------------------------------------------------------------- /purego_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin && !nodynamic 2 | 3 | package webp 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/ebitengine/purego" 9 | ) 10 | 11 | const ( 12 | libname = "libwebp.dylib" 13 | libnameDemux = "libwebpdemux.dylib" 14 | ) 15 | 16 | func loadLibrary(name string) (uintptr, error) { 17 | handle, err := purego.Dlopen(name, purego.RTLD_NOW|purego.RTLD_GLOBAL) 18 | if err != nil { 19 | return 0, fmt.Errorf("cannot load library: %w", err) 20 | } 21 | 22 | return uintptr(handle), nil 23 | } 24 | -------------------------------------------------------------------------------- /purego_other.go: -------------------------------------------------------------------------------- 1 | //go:build (!unix && !darwin && !windows) || nodynamic 2 | 3 | package webp 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "io" 9 | ) 10 | 11 | var ( 12 | dynamic = false 13 | dynamicErr = fmt.Errorf("webp: dynamic disabled") 14 | ) 15 | 16 | func decodeDynamic(r io.Reader, configOnly, decodeAll bool) (*WEBP, image.Config, error) { 17 | return nil, image.Config{}, dynamicErr 18 | } 19 | 20 | func encodeDynamic(w io.Writer, m image.Image, quality, method int, lossless, exact bool) error { 21 | return dynamicErr 22 | } 23 | 24 | func loadLibrary(name string) (uintptr, error) { 25 | return 0, dynamicErr 26 | } 27 | -------------------------------------------------------------------------------- /purego_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix && !darwin && !nodynamic 2 | 3 | package webp 4 | 5 | import ( 6 | "debug/elf" 7 | "fmt" 8 | "os" 9 | "runtime" 10 | 11 | "github.com/ebitengine/purego" 12 | ) 13 | 14 | const ( 15 | libname = "libwebp.so" 16 | libnameDemux = "libwebpdemux.so" 17 | ) 18 | 19 | func loadLibrary(name string) (uintptr, error) { 20 | if runtime.GOOS == "linux" && !isDynamicBinary() { 21 | return 0, fmt.Errorf("not a dynamic binary") 22 | } 23 | 24 | handle, err := purego.Dlopen(name, purego.RTLD_NOW|purego.RTLD_GLOBAL) 25 | if err != nil { 26 | return 0, fmt.Errorf("cannot load library: %w", err) 27 | } 28 | 29 | return handle, nil 30 | } 31 | 32 | func isDynamicBinary() bool { 33 | fileName, err := os.Executable() 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | fl, err := elf.Open(fileName) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | defer fl.Close() 44 | 45 | _, err = fl.DynamicSymbols() 46 | if err == nil { 47 | return true 48 | } 49 | 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /purego_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && !nodynamic 2 | 3 | package webp 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | const ( 11 | libname = "libwebp.dll" 12 | libnameDemux = "libwebpdemux.dll" 13 | ) 14 | 15 | func loadLibrary(name string) (uintptr, error) { 16 | handle, err := syscall.LoadLibrary(name) 17 | if err != nil { 18 | return 0, fmt.Errorf("cannot load library %s: %w", libname, err) 19 | } 20 | 21 | return uintptr(handle), nil 22 | } 23 | -------------------------------------------------------------------------------- /testdata/anim.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gen2brain/webp/3a7e6722e66f8f12f363995208b887acee9d0122/testdata/anim.webp -------------------------------------------------------------------------------- /testdata/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gen2brain/webp/3a7e6722e66f8f12f363995208b887acee9d0122/testdata/test.png -------------------------------------------------------------------------------- /testdata/test.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gen2brain/webp/3a7e6722e66f8f12f363995208b887acee9d0122/testdata/test.webp -------------------------------------------------------------------------------- /webp.go: -------------------------------------------------------------------------------- 1 | // Package webp implements an WEBP image decoder based on libwebp compiled to WASM. 2 | package webp 3 | 4 | import ( 5 | "errors" 6 | "image" 7 | "image/draw" 8 | "io" 9 | ) 10 | 11 | // Errors . 12 | var ( 13 | ErrMemRead = errors.New("webp: mem read failed") 14 | ErrMemWrite = errors.New("webp: mem write failed") 15 | ErrDecode = errors.New("webp: decode failed") 16 | ErrEncode = errors.New("webp: encode failed") 17 | ) 18 | 19 | const ( 20 | webpMaxHeaderSize = 32 21 | webpDemuxABIVersion = 0x0107 22 | webpDecoderABIVersion = 0x0209 23 | webpEncoderABIVersion = 0x020f 24 | ) 25 | 26 | // WEBP represents the possibly multiple images stored in a WEBP file. 27 | type WEBP struct { 28 | // Decoded images. 29 | Image []image.Image 30 | // Delay times, one per frame, in milliseconds. 31 | Delay []int 32 | } 33 | 34 | // DefaultQuality is the default quality encoding parameter. 35 | const DefaultQuality = 75 36 | 37 | // DefaultMethod is the default method encoding parameter. 38 | const DefaultMethod = 4 39 | 40 | // Options are the encoding parameters. 41 | type Options struct { 42 | // Quality in the range [0,100]. Quality of 100 implies Lossless. Default is 75. 43 | Quality int 44 | // Lossless indicates whether to use the lossless compression. Lossless will ignore quality. 45 | Lossless bool 46 | // Method is quality/speed trade-off (0=fast, 6=slower-better). Default is 4. 47 | Method int 48 | // Exact preserve the exact RGB values in transparent area. 49 | Exact bool 50 | } 51 | 52 | // Decode reads a WEBP image from r and returns it as an image.Image. 53 | func Decode(r io.Reader) (image.Image, error) { 54 | var err error 55 | var ret *WEBP 56 | 57 | if dynamic { 58 | ret, _, err = decodeDynamic(r, false, false) 59 | if err != nil { 60 | return nil, err 61 | } 62 | } else { 63 | ret, _, err = decode(r, false, false) 64 | if err != nil { 65 | return nil, err 66 | } 67 | } 68 | 69 | return ret.Image[0], nil 70 | } 71 | 72 | // DecodeConfig returns the color model and dimensions of a WEBP image without decoding the entire image. 73 | func DecodeConfig(r io.Reader) (image.Config, error) { 74 | var err error 75 | var cfg image.Config 76 | 77 | if dynamic { 78 | _, cfg, err = decodeDynamic(r, true, false) 79 | if err != nil { 80 | return image.Config{}, err 81 | } 82 | } else { 83 | _, cfg, err = decode(r, true, false) 84 | if err != nil { 85 | return image.Config{}, err 86 | } 87 | } 88 | 89 | return cfg, nil 90 | } 91 | 92 | // DecodeAll reads a WEBP image from r and returns the sequential frames and timing information. 93 | func DecodeAll(r io.Reader) (*WEBP, error) { 94 | var err error 95 | var ret *WEBP 96 | 97 | if dynamic { 98 | ret, _, err = decodeDynamic(r, false, true) 99 | if err != nil { 100 | return nil, err 101 | } 102 | } else { 103 | ret, _, err = decode(r, false, true) 104 | if err != nil { 105 | return nil, err 106 | } 107 | } 108 | 109 | return ret, nil 110 | } 111 | 112 | // Encode writes the image m to w with the given options. 113 | func Encode(w io.Writer, m image.Image, o ...Options) error { 114 | lossless := false 115 | quality := DefaultQuality 116 | method := DefaultMethod 117 | exact := false 118 | 119 | if o != nil { 120 | opt := o[0] 121 | lossless = opt.Lossless 122 | quality = opt.Quality 123 | method = opt.Method 124 | exact = opt.Exact 125 | 126 | if quality <= 0 { 127 | quality = DefaultQuality 128 | } else if quality > 100 { 129 | quality = 100 130 | } 131 | 132 | if method < 0 { 133 | method = DefaultMethod 134 | } else if method > 6 { 135 | method = 6 136 | } 137 | } 138 | 139 | if dynamic { 140 | err := encodeDynamic(w, m, quality, method, lossless, exact) 141 | if err != nil { 142 | return err 143 | } 144 | } else { 145 | err := encode(w, m, quality, method, lossless, exact) 146 | if err != nil { 147 | return err 148 | } 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // Dynamic returns error (if there was any) during opening dynamic/shared library. 155 | func Dynamic() error { 156 | return dynamicErr 157 | } 158 | 159 | // Init initializes wazero runtime and compiles the module. 160 | // This function does nothing if a dynamic/shared library is used and Dynamic() returns nil. 161 | // There is no need to explicitly call this function, first Decode/Encode will initialize the runtime. 162 | func Init() { 163 | if dynamic && dynamicErr == nil { 164 | return 165 | } 166 | 167 | initOnce() 168 | } 169 | 170 | func imageToNRGBA(src image.Image) *image.NRGBA { 171 | if dst, ok := src.(*image.NRGBA); ok { 172 | return dst 173 | } 174 | 175 | b := src.Bounds() 176 | dst := image.NewNRGBA(b) 177 | draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src) 178 | 179 | return dst 180 | } 181 | 182 | func init() { 183 | image.RegisterFormat("webp", "RIFF????WEBPVP8", Decode, DecodeConfig) 184 | } 185 | -------------------------------------------------------------------------------- /webp_dynamic.go: -------------------------------------------------------------------------------- 1 | //go:build (unix || darwin || windows) && !nodynamic 2 | 3 | package webp 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "io" 10 | "runtime" 11 | "unsafe" 12 | 13 | "github.com/ebitengine/purego" 14 | ) 15 | 16 | func decodeDynamic(r io.Reader, configOnly, decodeAll bool) (*WEBP, image.Config, error) { 17 | var cfg image.Config 18 | 19 | var err error 20 | var data []byte 21 | 22 | if configOnly { 23 | data = make([]byte, webpMaxHeaderSize) 24 | _, err = r.Read(data) 25 | if err != nil { 26 | return nil, cfg, err 27 | } 28 | } else { 29 | data, err = io.ReadAll(r) 30 | if err != nil { 31 | return nil, cfg, err 32 | } 33 | } 34 | 35 | var wpData webpData 36 | wpData.Size = uint64(len(data)) 37 | wpData.Bytes = &data[0] 38 | 39 | var config webpDecoderConfig 40 | if !webpInitDecoderConfig(&config) { 41 | return nil, cfg, ErrDecode 42 | } 43 | defer webpFreeDecBuffer(&config.Output) 44 | 45 | if !webpGetFeatures(wpData.Bytes, wpData.Size, &config.Input) { 46 | return nil, cfg, ErrDecode 47 | } 48 | 49 | hasAnimation := config.Input.Animation != 0 50 | 51 | cfg.Width = int(config.Input.Width) 52 | cfg.Height = int(config.Input.Height) 53 | 54 | cfg.ColorModel = color.NYCbCrAModel 55 | if hasAnimation { 56 | cfg.ColorModel = color.RGBAModel 57 | } 58 | 59 | if configOnly { 60 | return nil, cfg, nil 61 | } 62 | 63 | delay := make([]int, 0) 64 | images := make([]image.Image, 0) 65 | 66 | rect := image.Rect(0, 0, cfg.Width, cfg.Height) 67 | 68 | if decodeAll || hasAnimation { 69 | var options webpAnimDecoderOptions 70 | webpAnimDecoderOptionsInit(&options) 71 | options.ColorMode = modeRgbA 72 | options.UseThreads = 1 73 | 74 | decoder := webpAnimDecoderNew(&wpData, &options) 75 | defer webpAnimDecoderDelete(decoder) 76 | 77 | var timestamp, timestampPrev int 78 | out := new(uint8) 79 | 80 | for webpAnimDecoderHasMoreFrames(decoder) { 81 | if !webpAnimDecoderGetNext(decoder, &out, ×tamp) { 82 | return nil, cfg, ErrDecode 83 | } 84 | 85 | img := image.NewRGBA(rect) 86 | copy(img.Pix, unsafe.Slice(out, cfg.Width*cfg.Height*4)) 87 | 88 | images = append(images, img) 89 | delay = append(delay, timestamp-timestampPrev) 90 | 91 | timestampPrev = timestamp 92 | 93 | if !decodeAll { 94 | break 95 | } 96 | } 97 | 98 | ret := &WEBP{ 99 | Image: images, 100 | Delay: delay, 101 | } 102 | 103 | runtime.KeepAlive(data) 104 | 105 | return ret, cfg, nil 106 | } 107 | 108 | config.Output.Colorspace = modeYUVA 109 | config.Options.UseThreads = 1 110 | 111 | if !webpDecode(wpData.Bytes, wpData.Size, &config) { 112 | return nil, cfg, ErrDecode 113 | } 114 | 115 | img := image.NewNYCbCrA(rect, image.YCbCrSubsampleRatio420) 116 | out := *(*webpYUVABuffer)(unsafe.Pointer(&config.Output.U)) 117 | 118 | copy(img.Y, unsafe.Slice(out.Y, out.YSize)) 119 | copy(img.Cb, unsafe.Slice(out.U, out.USize)) 120 | copy(img.Cr, unsafe.Slice(out.V, out.VSize)) 121 | copy(img.A, unsafe.Slice(out.A, out.ASize)) 122 | 123 | images = append(images, img) 124 | 125 | runtime.KeepAlive(data) 126 | 127 | ret := &WEBP{ 128 | Image: images, 129 | Delay: delay, 130 | } 131 | 132 | return ret, cfg, nil 133 | } 134 | 135 | func encodeDynamic(w io.Writer, m image.Image, quality, method int, lossless, exact bool) error { 136 | var config webpConfig 137 | if !webpConfigInit(&config) { 138 | return ErrEncode 139 | } 140 | 141 | config.Quality = float32(quality) 142 | config.ThreadLevel = 1 143 | config.Method = int32(method) 144 | 145 | config.Lossless = 0 146 | if lossless { 147 | config.Lossless = 1 148 | } 149 | 150 | config.Exact = 0 151 | if exact { 152 | config.Exact = 1 153 | } 154 | 155 | var picture webpPicture 156 | if !webpPictureInit(&picture) { 157 | return ErrEncode 158 | } 159 | defer webpPictureFree(&picture) 160 | 161 | picture.Width = int32(m.Bounds().Dx()) 162 | picture.Height = int32(m.Bounds().Dy()) 163 | 164 | var data []byte 165 | 166 | switch img := m.(type) { 167 | case *image.YCbCr: 168 | i := imageToNRGBA(img) 169 | data = i.Pix 170 | picture.UseArgb = 1 171 | picture.ArgbStride = int32(i.Stride) 172 | case *image.NYCbCrA: 173 | if img.SubsampleRatio == image.YCbCrSubsampleRatio420 { 174 | picture.Y = unsafe.SliceData(img.Y) 175 | picture.U = unsafe.SliceData(img.Cb) 176 | picture.V = unsafe.SliceData(img.Cr) 177 | picture.A = unsafe.SliceData(img.A) 178 | picture.YStride = int32(img.YStride) 179 | picture.UvStride = int32(img.CStride) 180 | picture.AStride = int32(img.AStride) 181 | picture.UseArgb = 0 182 | picture.Colorspace = 4 // WEBP_YUV420A 183 | } else { 184 | i := imageToNRGBA(img) 185 | data = i.Pix 186 | picture.UseArgb = 1 187 | picture.ArgbStride = int32(i.Stride) 188 | } 189 | case *image.RGBA: 190 | data = img.Pix 191 | picture.UseArgb = 1 192 | picture.ArgbStride = int32(img.Stride) 193 | case *image.NRGBA: 194 | data = img.Pix 195 | picture.UseArgb = 1 196 | picture.ArgbStride = int32(img.Stride) 197 | default: 198 | i := imageToNRGBA(img) 199 | data = i.Pix 200 | picture.UseArgb = 1 201 | picture.ArgbStride = int32(i.Stride) 202 | } 203 | 204 | if picture.UseArgb == 1 { 205 | if !webpPictureImportRGBA(&picture, unsafe.SliceData(data), int(picture.ArgbStride)) { 206 | return ErrEncode 207 | } 208 | } 209 | 210 | picture.Writer = writeCallback 211 | picture.CustomPtr = (*byte)(unsafe.Pointer(&w)) 212 | 213 | if !webpEncode(&config, &picture) { 214 | return ErrEncode 215 | } 216 | 217 | return nil 218 | } 219 | 220 | func write(d *uint8, size uint64, picture *webpPicture) int { 221 | w := *(*io.Writer)(unsafe.Pointer(picture.CustomPtr)) 222 | 223 | _, err := w.Write(unsafe.Slice(d, size)) 224 | if err != nil { 225 | return 0 226 | } 227 | 228 | return 1 229 | } 230 | 231 | func init() { 232 | var err error 233 | defer func() { 234 | if r := recover(); r != nil { 235 | dynamic = false 236 | dynamicErr = fmt.Errorf("%v", r) 237 | } 238 | }() 239 | 240 | libwebp, err = loadLibrary(libname) 241 | if err == nil { 242 | libwebpDemux, err = loadLibrary(libnameDemux) 243 | if err == nil { 244 | dynamic = true 245 | } else { 246 | dynamicErr = err 247 | } 248 | } else { 249 | dynamicErr = err 250 | } 251 | 252 | if !dynamic { 253 | return 254 | } 255 | 256 | purego.RegisterLibFunc(&_webpAnimDecoderOptionsInit, libwebpDemux, "WebPAnimDecoderOptionsInitInternal") 257 | purego.RegisterLibFunc(&_webpAnimDecoderNew, libwebpDemux, "WebPAnimDecoderNewInternal") 258 | purego.RegisterLibFunc(&_webpAnimDecoderGetNext, libwebpDemux, "WebPAnimDecoderGetNext") 259 | purego.RegisterLibFunc(&_webpAnimDecoderHasMoreFrames, libwebpDemux, "WebPAnimDecoderHasMoreFrames") 260 | purego.RegisterLibFunc(&_webpAnimDecoderDelete, libwebpDemux, "WebPAnimDecoderDelete") 261 | purego.RegisterLibFunc(&_webpDecode, libwebp, "WebPDecode") 262 | purego.RegisterLibFunc(&_webpInitDecoderConfig, libwebp, "WebPInitDecoderConfigInternal") 263 | purego.RegisterLibFunc(&_webpGetFeatures, libwebp, "WebPGetFeaturesInternal") 264 | purego.RegisterLibFunc(&_webpPictureImportRGBA, libwebp, "WebPPictureImportRGBA") 265 | purego.RegisterLibFunc(&_webpConfigInit, libwebp, "WebPConfigInitInternal") 266 | purego.RegisterLibFunc(&_webpPictureInit, libwebp, "WebPPictureInitInternal") 267 | purego.RegisterLibFunc(&_webpPictureFree, libwebp, "WebPPictureFree") 268 | purego.RegisterLibFunc(&_webpFreeDecBuffer, libwebp, "WebPFreeDecBuffer") 269 | purego.RegisterLibFunc(&_webpEncode, libwebp, "WebPEncode") 270 | } 271 | 272 | var ( 273 | libwebp uintptr 274 | libwebpDemux uintptr 275 | dynamic bool 276 | dynamicErr error 277 | 278 | writeCallback = purego.NewCallback(write) 279 | ) 280 | 281 | var ( 282 | _webpAnimDecoderOptionsInit func(*webpAnimDecoderOptions, int) int 283 | _webpAnimDecoderNew func(*webpData, *webpAnimDecoderOptions, int) *webpAnimDecoder 284 | _webpAnimDecoderGetNext func(*webpAnimDecoder, **uint8, *int) int 285 | _webpAnimDecoderHasMoreFrames func(*webpAnimDecoder) int 286 | _webpAnimDecoderDelete func(*webpAnimDecoder) 287 | _webpDecode func(*uint8, uint64, *webpDecoderConfig) int 288 | _webpInitDecoderConfig func(*webpDecoderConfig) int 289 | _webpGetFeatures func(*uint8, uint64, *webpBitstreamFeatures, int) int 290 | _webpPictureImportRGBA func(*webpPicture, *uint8, int) int 291 | _webpConfigInit func(*webpConfig, int, float32, int) int 292 | _webpPictureInit func(*webpPicture, int) int 293 | _webpPictureFree func(*webpPicture) 294 | _webpFreeDecBuffer func(*webpDecBuffer) 295 | _webpEncode func(*webpConfig, *webpPicture) int 296 | ) 297 | 298 | func webpAnimDecoderOptionsInit(options *webpAnimDecoderOptions) { 299 | _webpAnimDecoderOptionsInit(options, webpDemuxABIVersion) 300 | } 301 | 302 | func webpAnimDecoderNew(data *webpData, options *webpAnimDecoderOptions) *webpAnimDecoder { 303 | return _webpAnimDecoderNew(data, options, webpDemuxABIVersion) 304 | } 305 | 306 | func webpAnimDecoderGetNext(decoder *webpAnimDecoder, buf **uint8, duration *int) bool { 307 | ret := _webpAnimDecoderGetNext(decoder, buf, duration) 308 | 309 | return ret != 0 310 | } 311 | 312 | func webpAnimDecoderHasMoreFrames(decoder *webpAnimDecoder) bool { 313 | ret := _webpAnimDecoderHasMoreFrames(decoder) 314 | 315 | return ret != 0 316 | } 317 | 318 | func webpAnimDecoderDelete(decoder *webpAnimDecoder) { 319 | _webpAnimDecoderDelete(decoder) 320 | } 321 | 322 | func webpDecode(data *uint8, size uint64, config *webpDecoderConfig) bool { 323 | ret := _webpDecode(data, size, config) 324 | 325 | return ret == 0 326 | } 327 | 328 | func webpInitDecoderConfig(config *webpDecoderConfig) bool { 329 | ret := _webpInitDecoderConfig(config) 330 | 331 | return ret == 0 332 | } 333 | 334 | func webpGetFeatures(data *uint8, size uint64, features *webpBitstreamFeatures) bool { 335 | ret := _webpGetFeatures(data, size, features, webpDecoderABIVersion) 336 | 337 | return ret == 0 338 | } 339 | 340 | func webpPictureImportRGBA(picture *webpPicture, in *uint8, stride int) bool { 341 | ret := _webpPictureImportRGBA(picture, in, stride) 342 | 343 | return ret != 0 344 | } 345 | 346 | func webpConfigInit(config *webpConfig) bool { 347 | ret := _webpConfigInit(config, 0, DefaultQuality, webpEncoderABIVersion) 348 | 349 | return ret != 0 350 | } 351 | 352 | func webpPictureInit(picture *webpPicture) bool { 353 | ret := _webpPictureInit(picture, webpEncoderABIVersion) 354 | 355 | return ret != 0 356 | } 357 | 358 | func webpPictureFree(picture *webpPicture) { 359 | _webpPictureFree(picture) 360 | } 361 | 362 | func webpFreeDecBuffer(p *webpDecBuffer) { 363 | _webpFreeDecBuffer(p) 364 | } 365 | 366 | func webpEncode(config *webpConfig, picture *webpPicture) bool { 367 | ret := _webpEncode(config, picture) 368 | 369 | return ret != 0 370 | } 371 | 372 | const ( 373 | modeRgbA = 7 374 | modeYUVA = 12 375 | ) 376 | 377 | type webpAnimDecoder struct{} 378 | 379 | type webpData struct { 380 | Bytes *uint8 381 | Size uint64 382 | } 383 | 384 | type webpDecoderOptions struct { 385 | BypassFiltering int32 386 | NoFancyUpsampling int32 387 | UseCropping int32 388 | CropLeft int32 389 | CropTop int32 390 | CropWidth int32 391 | CropHeight int32 392 | UseScaling int32 393 | ScaledWidth int32 394 | ScaledHeight int32 395 | UseThreads int32 396 | DitheringStrength int32 397 | Flip int32 398 | AlphaDitheringStrength int32 399 | _ [5]uint32 400 | } 401 | 402 | type webpDecoderConfig struct { 403 | Input webpBitstreamFeatures 404 | Output webpDecBuffer 405 | Options webpDecoderOptions 406 | _ [4]byte 407 | } 408 | 409 | type webpDecBuffer struct { 410 | Colorspace uint32 411 | Width int32 412 | Height int32 413 | IsExternalMemory int32 414 | U [80]byte 415 | _ [4]uint32 416 | PrivateMemory *uint8 417 | } 418 | 419 | type webpBitstreamFeatures struct { 420 | Width int32 421 | Height int32 422 | Alpha int32 423 | Animation int32 424 | Format int32 425 | _ [5]uint32 426 | } 427 | 428 | type webpYUVABuffer struct { 429 | Y *uint8 430 | U *uint8 431 | V *uint8 432 | A *uint8 433 | YStride int32 434 | UStride int32 435 | VStride int32 436 | AStride int32 437 | YSize uint64 438 | USize uint64 439 | VSize uint64 440 | ASize uint64 441 | } 442 | 443 | type webpPicture struct { 444 | UseArgb int32 445 | Colorspace uint32 446 | Width int32 447 | Height int32 448 | Y *uint8 449 | U *uint8 450 | V *uint8 451 | YStride int32 452 | UvStride int32 453 | A *uint8 454 | AStride int32 455 | Pad1 [2]uint32 456 | Argb *uint32 457 | ArgbStride int32 458 | Pad2 [3]uint32 459 | Writer uintptr 460 | CustomPtr *byte 461 | ExtraInfoType int32 462 | ExtraInfo *uint8 463 | Stats *webpAuxStats 464 | ErrorCode uint32 465 | ProgressHook *[0]byte 466 | UserData *byte 467 | Pad3 [3]uint32 468 | Pad4 *uint8 469 | Pad5 *uint8 470 | Pad6 [8]uint32 471 | Memory_ *byte 472 | MemoryArgb *byte 473 | Pad7 [2]*byte 474 | } 475 | 476 | type webpConfig struct { 477 | Lossless int32 478 | Quality float32 479 | Method int32 480 | ImageHint uint32 481 | TargetSize int32 482 | TargetPsnr float32 483 | Segments int32 484 | SnsStrength int32 485 | FilterStrength int32 486 | FilterSharpness int32 487 | FilterType int32 488 | Autofilter int32 489 | AlphaCompression int32 490 | AlphaFiltering int32 491 | AlphaQuality int32 492 | Pass int32 493 | ShowCompressed int32 494 | Preprocessing int32 495 | Partitions int32 496 | PartitionLimit int32 497 | EmulateJpegSize int32 498 | ThreadLevel int32 499 | LowMemory int32 500 | NearLossless int32 501 | Exact int32 502 | UseDeltaPalette int32 503 | UseSharpYuv int32 504 | Qmin int32 505 | Qmax int32 506 | } 507 | 508 | type webpAuxStats struct { 509 | CodedSize int32 510 | PSNR [5]float32 511 | BlockCount [3]int32 512 | HeaderBytes [2]int32 513 | ResidualBytes [3][4]int32 514 | SegmentSize [4]int32 515 | SegmentQuant [4]int32 516 | SegmentLevel [4]int32 517 | AlphaDataSize int32 518 | LayerDataSize int32 519 | LosslessFeatures uint32 520 | HistogramBits int32 521 | TransformBits int32 522 | CacheBits int32 523 | PaletteSize int32 524 | LosslessSize int32 525 | LosslessHdrSize int32 526 | LosslessDataSize int32 527 | Pad [2]uint32 528 | } 529 | 530 | type webpAnimDecoderOptions struct { 531 | ColorMode uint32 532 | UseThreads int32 533 | Padding [7]uint32 534 | } 535 | -------------------------------------------------------------------------------- /webp_test.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "fmt" 7 | "image" 8 | "image/jpeg" 9 | "image/png" 10 | "io" 11 | "os" 12 | "sync" 13 | "testing" 14 | ) 15 | 16 | //go:embed testdata/test.webp 17 | var testWebp []byte 18 | 19 | //go:embed testdata/test.png 20 | var testPng []byte 21 | 22 | //go:embed testdata/anim.webp 23 | var testWebpAnim []byte 24 | 25 | func TestDecode(t *testing.T) { 26 | img, err := Decode(bytes.NewReader(testWebp)) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | w, err := writeCloser() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | err = jpeg.Encode(w, img, nil) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | } 41 | 42 | func TestDecodeWasm(t *testing.T) { 43 | img, _, err := decode(bytes.NewReader(testWebp), false, false) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | w, err := writeCloser() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | err = jpeg.Encode(w, img.Image[0], nil) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | } 58 | 59 | func TestDecodeDynamic(t *testing.T) { 60 | if err := Dynamic(); err != nil { 61 | fmt.Println(err) 62 | t.Skip() 63 | } 64 | 65 | img, _, err := decodeDynamic(bytes.NewReader(testWebp), false, false) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | w, err := writeCloser() 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | err = jpeg.Encode(w, img.Image[0], nil) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | } 80 | 81 | func TestDecodeAnimWasm(t *testing.T) { 82 | ret, _, err := decode(bytes.NewReader(testWebpAnim), false, true) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | if len(ret.Image) != len(ret.Delay) { 88 | t.Errorf("got %d, want %d", len(ret.Delay), len(ret.Image)) 89 | } 90 | 91 | if len(ret.Image) != 17 { 92 | t.Errorf("got %d, want %d", len(ret.Image), 17) 93 | } 94 | 95 | for _, img := range ret.Image { 96 | w, err := writeCloser() 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | err = jpeg.Encode(w, img, nil) 102 | if err != nil { 103 | t.Error(err) 104 | } 105 | 106 | err = w.Close() 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | } 111 | } 112 | 113 | func TestDecodeAnimDynamic(t *testing.T) { 114 | if err := Dynamic(); err != nil { 115 | fmt.Println(err) 116 | t.Skip() 117 | } 118 | 119 | ret, _, err := decodeDynamic(bytes.NewReader(testWebpAnim), false, true) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | if len(ret.Image) != len(ret.Delay) { 125 | t.Errorf("got %d, want %d", len(ret.Delay), len(ret.Image)) 126 | } 127 | 128 | if len(ret.Image) != 17 { 129 | t.Errorf("got %d, want %d", len(ret.Image), 17) 130 | } 131 | 132 | for _, img := range ret.Image { 133 | w, err := writeCloser() 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | err = jpeg.Encode(w, img, nil) 139 | if err != nil { 140 | t.Error(err) 141 | } 142 | 143 | err = w.Close() 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | } 148 | } 149 | 150 | func TestImageDecode(t *testing.T) { 151 | img, _, err := image.Decode(bytes.NewReader(testWebp)) 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | w, err := writeCloser() 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | err = jpeg.Encode(w, img, nil) 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | } 166 | 167 | func TestImageDecodeAnim(t *testing.T) { 168 | img, _, err := image.Decode(bytes.NewReader(testWebpAnim)) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | 173 | w, err := writeCloser() 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | 178 | err = jpeg.Encode(w, img, nil) 179 | if err != nil { 180 | t.Error(err) 181 | } 182 | } 183 | 184 | func TestDecodeConfig(t *testing.T) { 185 | cfg, err := DecodeConfig(bytes.NewReader(testWebp)) 186 | if err != nil { 187 | t.Fatal(err) 188 | } 189 | 190 | if cfg.Width != 512 { 191 | t.Errorf("width: got %d, want %d", cfg.Width, 512) 192 | } 193 | 194 | if cfg.Height != 512 { 195 | t.Errorf("height: got %d, want %d", cfg.Height, 512) 196 | } 197 | } 198 | 199 | func TestImageDecodeConfig(t *testing.T) { 200 | cfg, _, err := image.DecodeConfig(bytes.NewReader(testWebp)) 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | 205 | if cfg.Width != 512 { 206 | t.Errorf("width: got %d, want %d", cfg.Width, 512) 207 | } 208 | 209 | if cfg.Height != 512 { 210 | t.Errorf("height: got %d, want %d", cfg.Height, 512) 211 | } 212 | } 213 | 214 | func TestEncodeRGBA(t *testing.T) { 215 | img, err := png.Decode(bytes.NewReader(testPng)) 216 | if err != nil { 217 | t.Fatal(err) 218 | } 219 | 220 | w, err := writeCloser() 221 | if err != nil { 222 | t.Fatal(err) 223 | } 224 | 225 | err = Encode(w, img) 226 | if err != nil { 227 | t.Fatal(err) 228 | } 229 | } 230 | 231 | func TestEncodeWasm(t *testing.T) { 232 | img, err := Decode(bytes.NewReader(testWebp)) 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | w, err := writeCloser() 238 | if err != nil { 239 | t.Fatal(err) 240 | } 241 | 242 | err = encode(w, img, DefaultQuality, DefaultMethod, false, false) 243 | if err != nil { 244 | t.Fatal(err) 245 | } 246 | } 247 | 248 | func TestEncodeWasmSync(t *testing.T) { 249 | wg := sync.WaitGroup{} 250 | ch := make(chan bool, 2) 251 | 252 | img, err := Decode(bytes.NewReader(testWebp)) 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | 257 | for i := 0; i < 10; i++ { 258 | wg.Add(1) 259 | go func() { 260 | ch <- true 261 | defer func() { <-ch; wg.Done() }() 262 | 263 | err = encode(io.Discard, img, DefaultQuality, DefaultMethod, false, false) 264 | if err != nil { 265 | t.Error(err) 266 | } 267 | }() 268 | } 269 | 270 | wg.Wait() 271 | } 272 | 273 | func TestEncodeDynamic(t *testing.T) { 274 | if err := Dynamic(); err != nil { 275 | fmt.Println(err) 276 | t.Skip() 277 | } 278 | 279 | img, err := Decode(bytes.NewReader(testWebp)) 280 | if err != nil { 281 | t.Fatal(err) 282 | } 283 | 284 | w, err := writeCloser() 285 | if err != nil { 286 | t.Fatal(err) 287 | } 288 | 289 | err = encodeDynamic(w, img, DefaultQuality, DefaultMethod, false, false) 290 | if err != nil { 291 | t.Fatal(err) 292 | } 293 | } 294 | 295 | func BenchmarkDecodeWasm(b *testing.B) { 296 | for i := 0; i < b.N; i++ { 297 | _, _, err := decode(bytes.NewReader(testWebp), false, false) 298 | if err != nil { 299 | b.Error(err) 300 | } 301 | } 302 | } 303 | 304 | func BenchmarkDecodeDynamic(b *testing.B) { 305 | if err := Dynamic(); err != nil { 306 | fmt.Println(err) 307 | b.Skip() 308 | } 309 | 310 | for i := 0; i < b.N; i++ { 311 | _, _, err := decodeDynamic(bytes.NewReader(testWebp), false, false) 312 | if err != nil { 313 | b.Error(err) 314 | } 315 | } 316 | } 317 | 318 | func BenchmarkEncodeWasm(b *testing.B) { 319 | img, err := Decode(bytes.NewReader(testWebp)) 320 | if err != nil { 321 | b.Fatal(err) 322 | } 323 | 324 | for i := 0; i < b.N; i++ { 325 | err = encode(io.Discard, img, DefaultQuality, DefaultMethod, false, false) 326 | if err != nil { 327 | b.Error(err) 328 | } 329 | } 330 | } 331 | 332 | func BenchmarkEncodeDynamic(b *testing.B) { 333 | if err := Dynamic(); err != nil { 334 | fmt.Println(err) 335 | b.Skip() 336 | } 337 | 338 | img, err := Decode(bytes.NewReader(testWebp)) 339 | if err != nil { 340 | b.Fatal(err) 341 | } 342 | 343 | for i := 0; i < b.N; i++ { 344 | err = encodeDynamic(io.Discard, img, DefaultQuality, DefaultMethod, false, false) 345 | if err != nil { 346 | b.Error(err) 347 | } 348 | } 349 | } 350 | 351 | type discard struct{} 352 | 353 | func (d discard) Close() error { 354 | return nil 355 | } 356 | 357 | func (discard) Write(p []byte) (int, error) { 358 | return len(p), nil 359 | } 360 | 361 | var discardCloser io.WriteCloser = discard{} 362 | 363 | func writeCloser(s ...string) (io.WriteCloser, error) { 364 | if len(s) > 0 { 365 | f, err := os.Create(s[0]) 366 | if err != nil { 367 | return nil, err 368 | } 369 | 370 | return f, nil 371 | } 372 | 373 | return discardCloser, nil 374 | } 375 | -------------------------------------------------------------------------------- /webp_wazero.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "debug/pe" 8 | _ "embed" 9 | "fmt" 10 | "image" 11 | "image/color" 12 | "io" 13 | "os" 14 | "runtime" 15 | "sync" 16 | "unsafe" 17 | 18 | "github.com/tetratelabs/wazero" 19 | "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 20 | ) 21 | 22 | //go:embed lib/webp.wasm.gz 23 | var webpWasm []byte 24 | 25 | func decode(r io.Reader, configOnly, decodeAll bool) (*WEBP, image.Config, error) { 26 | initOnce() 27 | 28 | var cfg image.Config 29 | var data []byte 30 | 31 | ctx := context.Background() 32 | 33 | mod, err := rt.InstantiateModule(ctx, cm, mc) 34 | if err != nil { 35 | return nil, cfg, err 36 | } 37 | 38 | defer mod.Close(ctx) 39 | 40 | _alloc := mod.ExportedFunction("malloc") 41 | _free := mod.ExportedFunction("free") 42 | _decode := mod.ExportedFunction("decode") 43 | 44 | if configOnly { 45 | data = make([]byte, webpMaxHeaderSize) 46 | _, err = r.Read(data) 47 | if err != nil { 48 | return nil, cfg, fmt.Errorf("read: %w", err) 49 | } 50 | } else { 51 | data, err = io.ReadAll(r) 52 | if err != nil { 53 | return nil, cfg, fmt.Errorf("read: %w", err) 54 | } 55 | } 56 | 57 | inSize := len(data) 58 | 59 | res, err := _alloc.Call(ctx, uint64(inSize)) 60 | if err != nil { 61 | return nil, cfg, fmt.Errorf("alloc: %w", err) 62 | } 63 | inPtr := res[0] 64 | defer _free.Call(ctx, inPtr) 65 | 66 | ok := mod.Memory().Write(uint32(inPtr), data) 67 | if !ok { 68 | return nil, cfg, ErrMemWrite 69 | } 70 | 71 | res, err = _alloc.Call(ctx, 4*4) 72 | if err != nil { 73 | return nil, cfg, fmt.Errorf("alloc: %w", err) 74 | } 75 | defer _free.Call(ctx, res[0]) 76 | 77 | widthPtr := res[0] 78 | heightPtr := res[0] + 4 79 | countPtr := res[0] + 8 80 | animPtr := res[0] + 12 81 | 82 | all := 0 83 | if decodeAll { 84 | all = 1 85 | } 86 | 87 | res, err = _decode.Call(ctx, inPtr, uint64(inSize), 1, uint64(all), widthPtr, heightPtr, countPtr, animPtr, 0, 0) 88 | if err != nil { 89 | return nil, cfg, fmt.Errorf("decode: %w", err) 90 | } 91 | 92 | if res[0] == 0 { 93 | return nil, cfg, ErrDecode 94 | } 95 | 96 | width, ok := mod.Memory().ReadUint32Le(uint32(widthPtr)) 97 | if !ok { 98 | return nil, cfg, ErrMemRead 99 | } 100 | 101 | height, ok := mod.Memory().ReadUint32Le(uint32(heightPtr)) 102 | if !ok { 103 | return nil, cfg, ErrMemRead 104 | } 105 | 106 | anim, ok := mod.Memory().ReadUint32Le(uint32(animPtr)) 107 | if !ok { 108 | return nil, cfg, ErrMemRead 109 | } 110 | 111 | hasAnimation := anim != 0 112 | 113 | cfg.Width = int(width) 114 | cfg.Height = int(height) 115 | 116 | cfg.ColorModel = color.NYCbCrAModel 117 | if hasAnimation { 118 | cfg.ColorModel = color.RGBAModel 119 | } 120 | 121 | if configOnly { 122 | return nil, cfg, nil 123 | } 124 | 125 | delay := make([]int, 0) 126 | images := make([]image.Image, 0) 127 | 128 | if decodeAll || hasAnimation { 129 | count, ok := mod.Memory().ReadUint32Le(uint32(countPtr)) 130 | if !ok { 131 | return nil, cfg, ErrMemRead 132 | } 133 | 134 | size := cfg.Width * cfg.Height * 4 135 | 136 | res, err = _alloc.Call(ctx, uint64(size*int(count))) 137 | if err != nil { 138 | return nil, cfg, fmt.Errorf("alloc: %w", err) 139 | } 140 | outPtr := res[0] 141 | defer _free.Call(ctx, outPtr) 142 | 143 | res, err = _alloc.Call(ctx, uint64(4*int(count))) 144 | if err != nil { 145 | return nil, cfg, fmt.Errorf("alloc: %w", err) 146 | } 147 | delayPtr := res[0] 148 | defer _free.Call(ctx, delayPtr) 149 | 150 | res, err = _decode.Call(ctx, inPtr, uint64(inSize), 0, uint64(all), widthPtr, heightPtr, countPtr, animPtr, delayPtr, outPtr) 151 | if err != nil { 152 | return nil, cfg, fmt.Errorf("decode: %w", err) 153 | } 154 | 155 | if res[0] == 0 { 156 | return nil, cfg, ErrDecode 157 | } 158 | 159 | for i := 0; i < int(count); i++ { 160 | out, ok := mod.Memory().Read(uint32(outPtr)+uint32(i*size), uint32(size)) 161 | if !ok { 162 | return nil, cfg, ErrMemRead 163 | } 164 | 165 | img := image.NewRGBA(image.Rect(0, 0, cfg.Width, cfg.Height)) 166 | img.Pix = out 167 | 168 | images = append(images, img) 169 | 170 | d, ok := mod.Memory().ReadUint32Le(uint32(delayPtr) + uint32(i*4)) 171 | if !ok { 172 | return nil, cfg, ErrMemRead 173 | } 174 | 175 | delay = append(delay, int(d)) 176 | } 177 | 178 | ret := &WEBP{ 179 | Image: images, 180 | Delay: delay, 181 | } 182 | 183 | return ret, cfg, nil 184 | } 185 | 186 | rect := image.Rect(0, 0, cfg.Width, cfg.Height) 187 | w, h := rect.Dx(), rect.Dy() 188 | cw := (rect.Max.X+1)/2 - rect.Min.X/2 189 | ch := (rect.Max.Y+1)/2 - rect.Min.Y/2 190 | 191 | i0 := 1*w*h + 0*cw*ch 192 | i1 := 1*w*h + 1*cw*ch 193 | i2 := 1*w*h + 2*cw*ch 194 | i3 := 2*w*h + 2*cw*ch 195 | 196 | size := i3 197 | 198 | res, err = _alloc.Call(ctx, uint64(size)) 199 | if err != nil { 200 | return nil, cfg, fmt.Errorf("alloc: %w", err) 201 | } 202 | outPtr := res[0] 203 | defer _free.Call(ctx, outPtr) 204 | 205 | res, err = _decode.Call(ctx, inPtr, uint64(inSize), 0, uint64(all), widthPtr, heightPtr, countPtr, animPtr, 0, outPtr) 206 | if err != nil { 207 | return nil, cfg, fmt.Errorf("decode: %w", err) 208 | } 209 | 210 | if res[0] == 0 { 211 | return nil, cfg, ErrDecode 212 | } 213 | 214 | out, ok := mod.Memory().Read(uint32(outPtr), uint32(size)) 215 | if !ok { 216 | return nil, cfg, ErrMemRead 217 | } 218 | 219 | img := &image.NYCbCrA{ 220 | YCbCr: image.YCbCr{ 221 | Y: out[:i0:i0], 222 | Cb: out[i0:i1:i1], 223 | Cr: out[i1:i2:i2], 224 | SubsampleRatio: image.YCbCrSubsampleRatio420, 225 | YStride: w, 226 | CStride: cw, 227 | Rect: rect, 228 | }, 229 | A: out[i2:], 230 | AStride: w, 231 | } 232 | 233 | images = append(images, img) 234 | 235 | ret := &WEBP{ 236 | Image: images, 237 | Delay: delay, 238 | } 239 | 240 | return ret, cfg, nil 241 | } 242 | 243 | func encode(w io.Writer, m image.Image, quality, method int, lossless, exact bool) error { 244 | initOnce() 245 | 246 | ctx := context.Background() 247 | 248 | mod, err := rt.InstantiateModule(ctx, cm, mc) 249 | if err != nil { 250 | return err 251 | } 252 | 253 | defer mod.Close(ctx) 254 | 255 | _alloc := mod.ExportedFunction("malloc") 256 | _free := mod.ExportedFunction("free") 257 | _encode := mod.ExportedFunction("encode") 258 | 259 | var data []byte 260 | var colorspace int 261 | 262 | var width = m.Bounds().Dx() 263 | var height = m.Bounds().Dy() 264 | 265 | switch img := m.(type) { 266 | case *image.YCbCr: 267 | i := imageToNRGBA(img) 268 | data = i.Pix 269 | case *image.NYCbCrA: 270 | if img.SubsampleRatio == image.YCbCrSubsampleRatio420 { 271 | length := len(img.Y) + len(img.Cb) + len(img.Cr) + len(img.A) 272 | var b = struct { 273 | addr *uint8 274 | len int 275 | cap int 276 | }{&img.Y[0], length, length} 277 | data = *(*[]byte)(unsafe.Pointer(&b)) 278 | colorspace = 4 // WEBP_YUV420A 279 | } else { 280 | i := imageToNRGBA(img) 281 | data = i.Pix 282 | } 283 | case *image.RGBA: 284 | data = img.Pix 285 | case *image.NRGBA: 286 | data = img.Pix 287 | default: 288 | i := imageToNRGBA(img) 289 | data = i.Pix 290 | } 291 | 292 | res, err := _alloc.Call(ctx, uint64(len(data))) 293 | if err != nil { 294 | return fmt.Errorf("alloc: %w", err) 295 | } 296 | inPtr := res[0] 297 | defer _free.Call(ctx, inPtr) 298 | 299 | ok := mod.Memory().Write(uint32(inPtr), data) 300 | if !ok { 301 | return ErrMemWrite 302 | } 303 | 304 | res, err = _alloc.Call(ctx, 8) 305 | if err != nil { 306 | return fmt.Errorf("alloc: %w", err) 307 | } 308 | sizePtr := res[0] 309 | defer _free.Call(ctx, sizePtr) 310 | 311 | losslessVal := 0 312 | if lossless { 313 | losslessVal = 1 314 | } 315 | 316 | exactVal := 0 317 | if exact { 318 | exactVal = 1 319 | } 320 | 321 | res, err = _encode.Call(ctx, inPtr, uint64(width), uint64(height), sizePtr, uint64(colorspace), uint64(quality), 322 | uint64(method), uint64(losslessVal), uint64(exactVal)) 323 | if err != nil { 324 | return fmt.Errorf("encode: %w", err) 325 | } 326 | defer _free.Call(ctx, res[0]) 327 | 328 | size, ok := mod.Memory().ReadUint64Le(uint32(sizePtr)) 329 | if !ok { 330 | return ErrMemRead 331 | } 332 | 333 | if size == 0 { 334 | return ErrEncode 335 | } 336 | 337 | out, ok := mod.Memory().Read(uint32(res[0]), uint32(size)) 338 | if !ok { 339 | return ErrMemRead 340 | } 341 | 342 | _, err = w.Write(out) 343 | if err != nil { 344 | return fmt.Errorf("write: %w", err) 345 | } 346 | 347 | return nil 348 | } 349 | 350 | var ( 351 | rt wazero.Runtime 352 | cm wazero.CompiledModule 353 | mc wazero.ModuleConfig 354 | 355 | initOnce = sync.OnceFunc(initialize) 356 | ) 357 | 358 | func initialize() { 359 | ctx := context.Background() 360 | rt = wazero.NewRuntime(ctx) 361 | 362 | r, err := gzip.NewReader(bytes.NewReader(webpWasm)) 363 | if err != nil { 364 | panic(err) 365 | } 366 | defer r.Close() 367 | 368 | var data bytes.Buffer 369 | _, err = data.ReadFrom(r) 370 | if err != nil { 371 | panic(err) 372 | } 373 | 374 | cm, err = rt.CompileModule(ctx, data.Bytes()) 375 | if err != nil { 376 | panic(err) 377 | } 378 | 379 | wasi_snapshot_preview1.MustInstantiate(ctx, rt) 380 | 381 | if runtime.GOOS == "windows" && isWindowsGUI() { 382 | mc = wazero.NewModuleConfig().WithStderr(io.Discard).WithStdout(io.Discard) 383 | } else { 384 | mc = wazero.NewModuleConfig().WithStderr(os.Stderr).WithStdout(os.Stdout) 385 | } 386 | } 387 | 388 | func isWindowsGUI() bool { 389 | const imageSubsystemWindowsGui = 2 390 | 391 | fileName, err := os.Executable() 392 | if err != nil { 393 | return false 394 | } 395 | 396 | fl, err := pe.Open(fileName) 397 | if err != nil { 398 | return false 399 | } 400 | 401 | defer fl.Close() 402 | 403 | var subsystem uint16 404 | if header, ok := fl.OptionalHeader.(*pe.OptionalHeader64); ok { 405 | subsystem = header.Subsystem 406 | } else if header, ok := fl.OptionalHeader.(*pe.OptionalHeader32); ok { 407 | subsystem = header.Subsystem 408 | } 409 | 410 | if subsystem == imageSubsystemWindowsGui { 411 | return true 412 | } 413 | 414 | return false 415 | } 416 | --------------------------------------------------------------------------------