├── examples ├── out │ └── .keep ├── images │ ├── cosmos.png │ ├── cosmos.webp │ ├── kinkaku.png │ ├── kinkaku.webp │ ├── butterfly.png │ ├── butterfly.webp │ ├── checkerboard.png │ ├── fizyplankton.png │ ├── fizyplankton.webp │ ├── yellow-rose-3.png │ ├── yellow-rose-3.webp │ └── README.md ├── README.md ├── decode │ └── decode.go └── encode │ └── encode.go ├── .gitignore ├── go.mod ├── Dockerfile ├── Makefile ├── test └── util │ ├── util_test.go │ └── util.go ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── webp ├── rgb_image_test.go ├── yuva_image.go ├── rgb_image.go ├── webp.go ├── decode.go ├── webp_test.go └── encode.go └── README.md /examples/out/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /examples/out/* 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pixiv/go-libwebp 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /examples/images/cosmos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/cosmos.png -------------------------------------------------------------------------------- /examples/images/cosmos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/cosmos.webp -------------------------------------------------------------------------------- /examples/images/kinkaku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/kinkaku.png -------------------------------------------------------------------------------- /examples/images/kinkaku.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/kinkaku.webp -------------------------------------------------------------------------------- /examples/images/butterfly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/butterfly.png -------------------------------------------------------------------------------- /examples/images/butterfly.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/butterfly.webp -------------------------------------------------------------------------------- /examples/images/checkerboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/checkerboard.png -------------------------------------------------------------------------------- /examples/images/fizyplankton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/fizyplankton.png -------------------------------------------------------------------------------- /examples/images/fizyplankton.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/fizyplankton.webp -------------------------------------------------------------------------------- /examples/images/yellow-rose-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/yellow-rose-3.png -------------------------------------------------------------------------------- /examples/images/yellow-rose-3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixiv/go-libwebp/HEAD/examples/images/yellow-rose-3.webp -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Example Codes and Images 2 | ======================== 3 | 4 | - [decode/decode.go](./decode/decode.go) -- a example code to decoding WebP image into image.RGBA. 5 | - [encode/encode.go](./encode/encode.go) -- a example code to encoding and writing image.RGBA into WebP file. 6 | -------------------------------------------------------------------------------- /examples/decode/decode.go: -------------------------------------------------------------------------------- 1 | // Package main is an example implementation of WebP decoder. 2 | package main 3 | 4 | import ( 5 | "github.com/pixiv/go-libwebp/test/util" 6 | "github.com/pixiv/go-libwebp/webp" 7 | ) 8 | 9 | func main() { 10 | var err error 11 | 12 | // Read binary data 13 | data := util.ReadFile("cosmos.webp") 14 | 15 | // Decode 16 | options := &webp.DecoderOptions{} 17 | img, err := webp.DecodeRGBA(data, options) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | util.WritePNG(img, "encoded_cosmos.png") 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.4 2 | 3 | RUN apk add --no-cache g++ make go 4 | 5 | RUN mkdir -p /tmp/go-libwebp 6 | COPY Makefile /tmp/go-libwebp/Makefile 7 | 8 | ENV LIBWEBP_PREFIX="/usr/local" \ 9 | LIBWEBP_VERSION="1.3.2" 10 | RUN cd /tmp/go-libwebp && make libwebp 11 | 12 | ENV GOPATH="/go" \ 13 | WORKDIR="/go/src/github.com/pixiv/go-libwebp" \ 14 | PATH="/go/bin:/usr/local/go/bin:$PATH" \ 15 | CGO_CFLAGS="-I /usr/local/include" \ 16 | CGO_LDFLAGS="-L /usr/local/lib" \ 17 | LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH" 18 | 19 | RUN mkdir -p $WORKDIR 20 | VOLUME $WORKDIR 21 | WORKDIR $WORKDIR 22 | 23 | CMD ["make", "test"] 24 | -------------------------------------------------------------------------------- /examples/encode/encode.go: -------------------------------------------------------------------------------- 1 | // Package main is an example implementation of WebP encoder. 2 | package main 3 | 4 | import ( 5 | "bufio" 6 | "image" 7 | 8 | "github.com/pixiv/go-libwebp/test/util" 9 | "github.com/pixiv/go-libwebp/webp" 10 | ) 11 | 12 | func main() { 13 | img := util.ReadPNG("cosmos.png") 14 | 15 | // Create file and buffered writer 16 | io := util.CreateFile("encoded_cosmos.webp") 17 | w := bufio.NewWriter(io) 18 | defer func() { 19 | w.Flush() 20 | io.Close() 21 | }() 22 | 23 | config, err := webp.ConfigPreset(webp.PresetDefault, 90) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | // Encode into WebP 29 | if err := webp.EncodeRGBA(w, img.(*image.RGBA), config); err != nil { 30 | panic(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | repo = github.com/pixiv/go-libwebp 2 | build_dir = /tmp 3 | cur_dir = $(shell pwd) 4 | libwebp_so = ${LIBWEBP_PREFIX}/lib/libwebp.so 5 | LIBWEBP_VERSION ?= 1.3.2 6 | 7 | all: test 8 | 9 | test: 10 | go test -v --ldflags "-extldflags '$(GOLIBWEBP_EXTLDFLAGS)'" ./... 11 | 12 | libwebp: $(libwebp_so) 13 | 14 | $(libwebp_so): 15 | cd $(build_dir) \ 16 | && wget http://downloads.webmproject.org/releases/webp/libwebp-$(LIBWEBP_VERSION).tar.gz \ 17 | && tar xf libwebp-$(LIBWEBP_VERSION).tar.gz \ 18 | && cd libwebp-$(LIBWEBP_VERSION) \ 19 | && ./configure --prefix=$(LIBWEBP_PREFIX) \ 20 | && make \ 21 | && make install 22 | 23 | docker-test: 24 | docker run -v $(cur_dir):/go/src/$(repo) -it go-libwebp:$(LIBWEBP_VERSION) 25 | 26 | docker-sh: 27 | docker run -v $(cur_dir):/go/src/$(repo) -it go-libwebp:$(LIBWEBP_VERSION) sh 28 | 29 | docker-build: 30 | docker build -t go-libwebp:$(LIBWEBP_VERSION) . 31 | 32 | docker-clean: 33 | docker rm $$(docker ps -a -q -f "ancestor=go-libwebp") 34 | 35 | .PHONY: \ 36 | all \ 37 | test \ 38 | libwebp \ 39 | docker-test \ 40 | docker-sh \ 41 | docker-build \ 42 | docker-clean 43 | -------------------------------------------------------------------------------- /test/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pixiv/go-libwebp/test/util" 7 | ) 8 | 9 | var PNGFiles = []string{ 10 | "butterfly.png", 11 | "cosmos.png", 12 | "fizyplankton.png", 13 | "kinkaku.png", 14 | "yellow-rose-3.png", 15 | } 16 | 17 | var WebPFiles = []string{ 18 | "butterfly.webp", 19 | "cosmos.webp", 20 | "fizyplankton.webp", 21 | "kinkaku.webp", 22 | "yellow-rose-3.webp", 23 | } 24 | 25 | func TestOpenFile(t *testing.T) { 26 | for _, file := range PNGFiles { 27 | util.OpenFile(file) 28 | } 29 | for _, file := range WebPFiles { 30 | util.OpenFile(file) 31 | } 32 | } 33 | 34 | func TestReadFile(t *testing.T) { 35 | for _, file := range PNGFiles { 36 | util.ReadFile(file) 37 | } 38 | for _, file := range WebPFiles { 39 | util.ReadFile(file) 40 | } 41 | } 42 | 43 | func TestCreateFile(t *testing.T) { 44 | f := util.CreateFile("util_test") 45 | f.Write([]byte{'o', 'k'}) 46 | f.Close() 47 | } 48 | 49 | func TestReadWritePNG(t *testing.T) { 50 | for _, file := range PNGFiles { 51 | png := util.ReadPNG(file) 52 | util.WritePNG(png, "util_test_"+file) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Shunsuke MICHII 2 | Copyright (c) 2023, pixiv Inc. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 18 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | test: 14 | name: test 15 | strategy: 16 | matrix: 17 | libwebp-version: ['1.3.2', '1.4.0', '1.5.0', '1.6.0'] 18 | go-version: ['~1.23', '~1.24', '~1.25'] 19 | extldflags: ['', '-static'] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@v4 24 | - uses: actions/cache@v4 25 | with: 26 | path: ~/cache 27 | key: "1-${{ runner.os }}-${{ matrix.libwebp-version }}" 28 | - name: setup Go 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: ${{ matrix.go-version }} 32 | cache: false 33 | - name: set environment variables 34 | shell: bash 35 | run: | 36 | mkdir -p $HOME/cache 37 | echo "CGO_CFLAGS=-I $HOME/cache/libwebp-${{ matrix.libwebp-version }}/include" >> $GITHUB_ENV 38 | echo "CGO_LDFLAGS=-L $HOME/cache/libwebp-${{ matrix.libwebp-version }}/lib" >> $GITHUB_ENV 39 | echo "LD_LIBRARY_PATH=$HOME/cache/libwebp-${{ matrix.libwebp-version }}/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV 40 | echo "GOLIBWEBP_EXTLDFLAGS=${{ matrix.extldflags }}" >> $GITHUB_ENV 41 | echo "LIBWEBP_VERSION=${{ matrix.libwebp-version }}" >> $GITHUB_ENV 42 | - name: build libwebp 43 | run: LIBWEBP_PREFIX=$HOME/cache/libwebp-${{ matrix.libwebp-version }} make libwebp 44 | - name: test 45 | run: make test 46 | -------------------------------------------------------------------------------- /webp/rgb_image_test.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | ) 8 | 9 | func TestConvertFromRGBA(t *testing.T) { 10 | rgba := color.RGBA{0x11, 0x22, 0x33, 0xFF} 11 | expect := RGB{0x11, 0x22, 0x33} 12 | if got := RGBModel.Convert(rgba); got != expect { 13 | t.Errorf("got: %v, expect: %v", got, expect) 14 | } 15 | } 16 | 17 | func TestConvertFromRGB(t *testing.T) { 18 | c := RGB{0x11, 0x22, 0x33} 19 | if got := RGBModel.Convert(c); got != c { 20 | t.Errorf("got: %v, expect: %v", got, c) 21 | } 22 | } 23 | 24 | func TestColorRGBA(t *testing.T) { 25 | c := RGB{0x11, 0x22, 0x33} 26 | r, g, b, a := uint32(0x1111), uint32(0x2222), uint32(0x3333), uint32(0xFFFF) 27 | 28 | gotR, gotG, gotB, gotA := c.RGBA() 29 | if gotR != r { 30 | t.Errorf("got R: %v, expect R: %v", gotR, r) 31 | } 32 | if gotG != g { 33 | t.Errorf("got G: %v, expect G: %v", gotG, g) 34 | } 35 | if gotB != b { 36 | t.Errorf("got B: %v, expect B: %v", gotB, b) 37 | } 38 | if gotA != a { 39 | t.Errorf("got A: %v, expect A: %v", gotA, a) 40 | } 41 | } 42 | 43 | func TestImageInterface(t *testing.T) { 44 | rect := image.Rect(0, 0, 100, 100) 45 | img := NewRGBImage(rect) 46 | 47 | if got := img.ColorModel(); got != RGBModel { 48 | t.Errorf("ColorModel() should return rgb.ColorModel, got: %v", got) 49 | } 50 | 51 | if got := img.Bounds(); got != rect { 52 | t.Errorf("Bounds() should return %v, got: %v", rect, got) 53 | } 54 | 55 | black := color.RGBA{0x00, 0x00, 0x00, 0xFF} 56 | if got := img.At(0, 0); got != black { 57 | t.Errorf("At(0, 0) should return %v, got: %v", black, got) 58 | } 59 | 60 | blank := color.RGBA{} 61 | if got := img.At(-1, -1); got != blank { 62 | t.Errorf("At(0, 0) should return %v, got: %v", blank, got) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /webp/yuva_image.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | import "image" 4 | 5 | // YUVAImage represents a image of YUV colors with alpha channel image. 6 | // 7 | // YUVAImage contains decoded YCbCr image data with alpha channel, 8 | // but it is not compatible with image.YCbCr. Because, the RGB-YCbCr conversion 9 | // that used in WebP is following to ITU-R BT.601 standard. 10 | // In contrast, the conversion of Image.YCbCr (and color.YCbCrModel) is following 11 | // to the JPEG standard (JFIF). If you need the image as image.YCBCr, you will 12 | // first convert from WebP to RGB image, then convert from RGB image to JPEG's 13 | // YCbCr image. 14 | // 15 | // See: http://en.wikipedia.org/wiki/YCbCr 16 | type YUVAImage struct { 17 | Y, Cb, Cr, A []uint8 18 | YStride int 19 | CStride int 20 | AStride int 21 | ColorSpace ColorSpace 22 | Rect image.Rectangle 23 | } 24 | 25 | // NewYUVAImage creates and allocates image buffer. 26 | func NewYUVAImage(r image.Rectangle, c ColorSpace) (image *YUVAImage) { 27 | yw, yh := r.Dx(), r.Dy() 28 | cw, ch := ((r.Max.X+1)/2 - r.Min.X/2), ((r.Max.Y+1)/2 - r.Min.Y/2) 29 | 30 | switch c { 31 | case YUV420: 32 | b := make([]byte, yw*yh+2*cw*ch) 33 | image = &YUVAImage{ 34 | Y: b[:yw*yh], 35 | Cb: b[yw*yh+0*cw*ch : yw*yh+1*cw*ch], 36 | Cr: b[yw*yh+1*cw*ch : yw*yh+2*cw*ch], 37 | A: nil, 38 | YStride: yw, 39 | CStride: cw, 40 | AStride: 0, 41 | ColorSpace: c, 42 | Rect: r, 43 | } 44 | 45 | case YUV420A: 46 | b := make([]byte, 2*yw*yh+2*cw*ch) 47 | image = &YUVAImage{ 48 | Y: b[:yw*yh], 49 | Cb: b[yw*yh+0*cw*ch : yw*yh+1*cw*ch], 50 | Cr: b[yw*yh+1*cw*ch : yw*yh+2*cw*ch], 51 | A: b[yw*yh+2*cw*ch:], 52 | YStride: yw, 53 | CStride: cw, 54 | AStride: yw, 55 | ColorSpace: c, 56 | Rect: r, 57 | } 58 | } 59 | 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-libwebp 2 | ========== 3 | 4 | [![ci](https://github.com/pixiv/go-libwebp/actions/workflows/ci.yml/badge.svg)](https://github.com/pixiv/go-libwebp/actions/workflows/ci.yml) 5 | [![GoDoc](https://godoc.org/github.com/pixiv/go-libwebp/webp?status.svg)](https://godoc.org/github.com/pixiv/go-libwebp/webp) 6 | 7 | A implementation of Go binding for [libwebp](https://developers.google.com/speed/webp/docs/api). 8 | 9 | ## Dependencies 10 | 11 | - libwebp 1.3.2 and above 12 | 13 | ## Usage 14 | 15 | The [examples](./examples) directory contains example codes and images. 16 | 17 | ### Decoding WebP into image.RGBA 18 | 19 | ``` 20 | package main 21 | 22 | import ( 23 | "github.com/pixiv/go-libwebp/test/util" 24 | "github.com/pixiv/go-libwebp/webp" 25 | ) 26 | 27 | func main() { 28 | var err error 29 | 30 | // Read binary data 31 | data := util.ReadFile("cosmos.webp") 32 | 33 | // Decode 34 | options := &webp.DecoderOptions{} 35 | img, err := webp.DecodeRGBA(data, options) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | util.WritePNG(img, "encoded_cosmos.png") 41 | } 42 | ``` 43 | 44 | You can set more decoding options such as cropping, flipping and scaling. 45 | 46 | ### Encoding WebP from image.RGBA 47 | 48 | ``` 49 | package main 50 | 51 | import ( 52 | "bufio" 53 | "image" 54 | 55 | "github.com/pixiv/go-libwebp/test/util" 56 | "github.com/pixiv/go-libwebp/webp" 57 | ) 58 | 59 | func main() { 60 | img := util.ReadPNG("cosmos.png") 61 | 62 | // Create file and buffered writer 63 | io := util.CreateFile("encoded_cosmos.webp") 64 | w := bufio.NewWriter(io) 65 | defer func() { 66 | w.Flush() 67 | io.Close() 68 | }() 69 | 70 | config := webp.ConfigPreset(webp.PresetDefault, 90) 71 | 72 | // Encode into WebP 73 | if err := webp.EncodeRGBA(w, img.(*image.RGBA), config); err != nil { 74 | panic(err) 75 | } 76 | } 77 | ``` 78 | 79 | ## TODO 80 | 81 | - Incremental decoding API 82 | - Container API (Animation) 83 | 84 | ## License 85 | 86 | This library is released under The BSD 2-Clause License. 87 | See [LICENSE](./LICENSE). 88 | -------------------------------------------------------------------------------- /webp/rgb_image.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | // RGBImage represent image data which has RGB colors. 9 | // RGBImage is compatible with image.RGBA, but does not have alpha channel to reduce using memory. 10 | type RGBImage struct { 11 | // Pix holds the image's stream, in R, G, B order. 12 | Pix []uint8 13 | // Stride is the Pix stride (in bytes) between vertically adjacent pixels. 14 | Stride int 15 | // Rect is the image's bounds. 16 | Rect image.Rectangle 17 | } 18 | 19 | // NewRGBImage allocates and returns RGB image 20 | func NewRGBImage(r image.Rectangle) *RGBImage { 21 | w, h := r.Dx(), r.Dy() 22 | return &RGBImage{Pix: make([]uint8, 3*w*h), Stride: 3 * w, Rect: r} 23 | } 24 | 25 | // ColorModel returns RGB color model. 26 | func (p *RGBImage) ColorModel() color.Model { 27 | return RGBModel 28 | } 29 | 30 | // Bounds implements image.Image.At 31 | func (p *RGBImage) Bounds() image.Rectangle { 32 | return p.Rect 33 | } 34 | 35 | // At implements image.Image.At 36 | func (p *RGBImage) At(x, y int) color.Color { 37 | return p.RGBAAt(x, y) 38 | } 39 | 40 | // RGBAAt returns the color of the pixel at (x, y) as RGBA. 41 | func (p *RGBImage) RGBAAt(x, y int) color.RGBA { 42 | if !(image.Point{x, y}.In(p.Rect)) { 43 | return color.RGBA{} 44 | } 45 | i := (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*3 46 | return color.RGBA{p.Pix[i+0], p.Pix[i+1], p.Pix[i+2], 0xFF} 47 | } 48 | 49 | // RGBModel is RGB color model instance 50 | var RGBModel = color.ModelFunc(rgbModel) 51 | 52 | func rgbModel(c color.Color) color.Color { 53 | if _, ok := c.(RGB); ok { 54 | return c 55 | } 56 | r, g, b, _ := c.RGBA() 57 | return RGB{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)} 58 | } 59 | 60 | // RGB color 61 | type RGB struct { 62 | R, G, B uint8 63 | } 64 | 65 | // RGBA implements Color.RGBA 66 | func (c RGB) RGBA() (r, g, b, a uint32) { 67 | r = uint32(c.R) 68 | r |= r << 8 69 | g = uint32(c.G) 70 | g |= g << 8 71 | b = uint32(c.B) 72 | b |= b << 8 73 | a = uint32(0xFFFF) 74 | return 75 | } 76 | 77 | // Make sure RGBImage implements image.Image. 78 | // See https://golang.org/doc/effective_go.html#blank_implements. 79 | var _ image.Image = new(RGBImage) 80 | -------------------------------------------------------------------------------- /webp/webp.go: -------------------------------------------------------------------------------- 1 | // Package webp provides an interface to libwebp library to decoding/encoding 2 | // WebP image. 3 | package webp 4 | 5 | /* 6 | #cgo LDFLAGS: -lwebp -lsharpyuv -lm 7 | 8 | #include 9 | #include 10 | 11 | */ 12 | import "C" 13 | 14 | // ColorSpace represents encoding color space in WebP 15 | type ColorSpace int 16 | 17 | const ( 18 | // YUV420 specifies YUV4:2:0 19 | YUV420 ColorSpace = C.WEBP_YUV420 20 | // YUV420A specifies YUV4:2:0 with alpha channel 21 | YUV420A ColorSpace = C.WEBP_YUV420A 22 | ) 23 | 24 | // ImageHint corresponds to C.WebPImageHint. 25 | type ImageHint int 26 | 27 | const ( 28 | HintDefault ImageHint = C.WEBP_HINT_DEFAULT 29 | HintPicture ImageHint = C.WEBP_HINT_PICTURE 30 | HintPhoto ImageHint = C.WEBP_HINT_PHOTO 31 | HintGraph ImageHint = C.WEBP_HINT_GRAPH 32 | HintLast ImageHint = C.WEBP_HINT_LAST 33 | ) 34 | 35 | // Preset corresponds to C.WebPPreset. 36 | type Preset int 37 | 38 | const ( 39 | // PresetDefault corresponds to WEBP_PRESET_DEFAULT, for default preset. 40 | PresetDefault Preset = C.WEBP_PRESET_DEFAULT 41 | // PresetPicture corresponds to WEBP_PRESET_PICTURE, for digital picture, like portrait, inner shot 42 | PresetPicture Preset = C.WEBP_PRESET_PICTURE 43 | // PresetPhoto corresponds to WEBP_PRESET_PHOTO, for outdoor photograph, with natural lighting 44 | PresetPhoto Preset = C.WEBP_PRESET_PHOTO 45 | // PresetDrawing corresponds to WEBP_PRESET_DRAWING, for hand or line drawing, with high-contrast details 46 | PresetDrawing Preset = C.WEBP_PRESET_DRAWING 47 | // PresetIcon corresponds to WEBP_PRESET_ICON, for small-sized colorful images 48 | PresetIcon Preset = C.WEBP_PRESET_ICON 49 | // PresetText corresponds to WEBP_PRESET_TEXT, for text-like 50 | PresetText Preset = C.WEBP_PRESET_TEXT 51 | ) 52 | 53 | // FilterType corresponds to filter types in compression parameters. 54 | type FilterType int 55 | 56 | const ( 57 | // SimpleFilter (=0, default) 58 | SimpleFilter FilterType = iota 59 | // StrongFilter (=1) 60 | StrongFilter 61 | ) 62 | 63 | // Preprocessing corresponds to preprocessing filter parameter. 64 | type Preprocessing int 65 | 66 | const ( 67 | // PreprocessingNone specifies to disable preprocessing filter. 68 | PreprocessingNone = 0 69 | // PreprocessingSegmentSmooth specifies segment-smooth filter. 70 | PreprocessingSegmentSmooth = 1 71 | //PreprocessingPseudoRandomDithering specifies pseudo-random dithering filter. 72 | PreprocessingPseudoRandomDithering = 2 73 | ) 74 | -------------------------------------------------------------------------------- /test/util/util.go: -------------------------------------------------------------------------------- 1 | // Package util contains utility code for demosntration of go-libwebp. 2 | package util 3 | 4 | import ( 5 | "bufio" 6 | "errors" 7 | "fmt" 8 | "image" 9 | "image/png" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "runtime" 15 | ) 16 | 17 | func examplesDir() string { 18 | _, file, _, ok := runtime.Caller(0) 19 | if !ok { 20 | panic(errors.New("could not retrieve the directory")) 21 | } 22 | result, err := filepath.Abs(filepath.Join(filepath.Dir(file), "..", "..", "examples")) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return result 27 | } 28 | 29 | // GetExFilePath returns the path of specified example file. 30 | func GetExFilePath(name string) string { 31 | path := filepath.Join(examplesDir(), "images", name) 32 | if _, err := os.Stat(path); err == nil { 33 | return path 34 | } 35 | panic(fmt.Errorf("%v does not exist in any directory which contains in $GOPATH", name)) 36 | } 37 | 38 | // GetOutFilePath returns the path of specified out file. 39 | func GetOutFilePath(name string) string { 40 | path := filepath.Join(examplesDir(), "out") 41 | if _, err := os.Stat(path); err == nil { 42 | return filepath.Join(path, name) 43 | } 44 | panic(fmt.Errorf("out directory does not exist in any directory which contains in $GOPATH")) 45 | } 46 | 47 | // OpenFile opens specified example file 48 | func OpenFile(name string) (io io.Reader) { 49 | io, err := os.Open(GetExFilePath(name)) 50 | if err != nil { 51 | panic(err) 52 | } 53 | return 54 | } 55 | 56 | // ReadFile reads and returns data bytes of specified example file. 57 | func ReadFile(name string) (data []byte) { 58 | data, err := ioutil.ReadFile(GetExFilePath(name)) 59 | if err != nil { 60 | panic(err) 61 | } 62 | return 63 | } 64 | 65 | // CreateFile opens specified example file 66 | func CreateFile(name string) (f *os.File) { 67 | f, err := os.Create(GetOutFilePath(name)) 68 | if err != nil { 69 | panic(err) 70 | } 71 | return 72 | } 73 | 74 | // WritePNG encodes and writes image into PNG file. 75 | func WritePNG(img image.Image, name string) { 76 | f, err := os.Create(GetOutFilePath(name)) 77 | if err != nil { 78 | panic(err) 79 | } 80 | b := bufio.NewWriter(f) 81 | defer func() { 82 | b.Flush() 83 | f.Close() 84 | }() 85 | 86 | if err := png.Encode(b, img); err != nil { 87 | panic(err) 88 | } 89 | return 90 | } 91 | 92 | // ReadPNG reads and decodes png data into image.Image 93 | func ReadPNG(name string) (img image.Image) { 94 | io, err := os.Open(GetExFilePath(name)) 95 | if err != nil { 96 | panic(err) 97 | } 98 | img, err = png.Decode(io) 99 | if err != nil { 100 | panic(err) 101 | } 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /examples/images/README.md: -------------------------------------------------------------------------------- 1 | Example Images 2 | ============== 3 | 4 | This directory contains example WebP encoded files and PNG source files. 5 | 6 | ## Image Credits 7 | 8 | ### Photos by Author 9 | 10 | These images are taken by author. 11 | These photos are licensed under the Creative Commons Attribution 3.0. 12 | You can also use these images under the same as [go-libwebp's license](../LICENSE). 13 | 14 | #### cosmos.png 15 | 16 | The cosmos taken in Nokono-shima (Nokono Island), Fukuoka, JAPAN. 17 | 18 | ![cosmos.png](cosmos.png) 19 | 20 | WebP file is generated by following command: 21 | 22 | ```sh 23 | $ cwebp -q 90 cosmos.png -o cosmos.webp 24 | Saving file 'cosmos.webp' 25 | File: cosmos.png 26 | Dimension: 1024 x 768 27 | Output: 76954 bytes Y-U-V-All-PSNR 45.87 47.63 48.10 46.44 dB 28 | block count: intra4: 2972 29 | intra16: 100 (-> 3.26%) 30 | skipped block: 1 (0.03%) 31 | bytes used: header: 249 (0.3%) 32 | mode-partition: 15161 (19.7%) 33 | Residuals bytes |segment 1|segment 2|segment 3|segment 4| total 34 | macroblocks: | 0%| 11%| 33%| 54%| 3072 35 | quantizer: | 12 | 11 | 9 | 8 | 36 | filter level: | 4 | 2 | 2 | 4 | 37 | ``` 38 | 39 | #### butterfly.png 40 | 41 | The butterfly taken in Ishigaki-jima (Ishigaki Island) in Okinawa, JAPAN. 42 | 43 | ![butterfly.png](butterfly.png) 44 | 45 | WebP file is generated by following command: 46 | 47 | ```sh 48 | $ cwebp -q 90 butterfly.png -o butterfly.webp 49 | Saving file 'butterfly.webp' 50 | File: butterfly.png 51 | Dimension: 1024 x 768 52 | Output: 79198 bytes Y-U-V-All-PSNR 45.50 48.71 49.90 46.44 dB 53 | block count: intra4: 2775 54 | intra16: 297 (-> 9.67%) 55 | skipped block: 0 (0.00%) 56 | bytes used: header: 383 (0.5%) 57 | mode-partition: 13227 (16.7%) 58 | Residuals bytes |segment 1|segment 2|segment 3|segment 4| total 59 | macroblocks: | 1%| 7%| 14%| 75%| 3072 60 | quantizer: | 12 | 12 | 10 | 8 | 61 | filter level: | 4 | 3 | 2 | 6 | 62 | ``` 63 | 64 | #### kinkaku.png 65 | 66 | Kinkaku taken in Kyoto, JAPAN. 67 | 68 | ![kinkaku.png](kinkaku.png) 69 | 70 | WebP file is generated by following command: 71 | 72 | ```sh 73 | $ cwebp -q 90 kinkaku.png -o kinkaku.webp 74 | Saving file 'kinkaku.webp' 75 | File: kinkaku.png 76 | Dimension: 1024 x 768 77 | Output: 186300 bytes Y-U-V-All-PSNR 43.86 47.25 48.26 44.81 dB 78 | block count: intra4: 2775 79 | intra16: 297 (-> 9.67%) 80 | skipped block: 243 (7.91%) 81 | bytes used: header: 425 (0.2%) 82 | mode-partition: 16737 (9.0%) 83 | Residuals bytes |segment 1|segment 2|segment 3|segment 4| total 84 | macroblocks: | 2%| 28%| 40%| 28%| 3072 85 | quantizer: | 12 | 11 | 9 | 6 | 86 | filter level: | 4 | 2 | 2 | 1 | 87 | ``` 88 | 89 | ### From Google WebP Gallery 90 | 91 | These images picked up from [WebP Gallery](https://developers.google.com/speed/webp/gallery) in Google Developers. 92 | 93 | #### yellow-rose-3.png 94 | 95 | "Free Stock Photo in High Resolution - Yellow Rose 3 - Flowers" by Jon Sullivan 96 | 97 | This image is clipped by Google and licensed under [Creative Commons Attribution 3.0](https://creativecommons.org/licenses/by/3.0/). The Source of this image is in the public domain. 98 | 99 |
100 | 101 |
102 | 103 | #### fizyplankton.png 104 | 105 | "baby tux for my user page" by Fizyplankton. 106 | 107 | This image file is in the public domain. 108 | 109 |
110 | 111 |
112 | -------------------------------------------------------------------------------- /webp/decode.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | /* 4 | #include 5 | #include 6 | 7 | static VP8StatusCode CheckDecBuffer(const WebPDecBuffer* const buffer); 8 | 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | "image" 16 | "unsafe" 17 | ) 18 | 19 | // DecoderOptions specifies decoding options of WebP. 20 | type DecoderOptions struct { 21 | BypassFiltering bool // If true, bypass filtering process 22 | NoFancyUpsampling bool // If true, do not fancy upsampling 23 | Crop image.Rectangle // Do cropping if image.Rectangle is not empty. 24 | Scale image.Rectangle // Do scaling if image.Rectangle is not empty. 25 | UseThreads bool // If true, use multi threads 26 | DitheringStrength int // Specify dithering strength [0=Off .. 100=full] 27 | Flip bool // If true, flip output vertically 28 | AlphaDitheringStrength int // Specify alpha dithering strength in [0..100] 29 | } 30 | 31 | // BitstreamFeatures represents the image properties which are retrived from 32 | // data stream. 33 | type BitstreamFeatures struct { 34 | Width int // Image width in pixels 35 | Height int // Image height in pixles 36 | HasAlpha bool // True if data stream contains a alpha channel. 37 | HasAnimation bool // True if data stream is an animation 38 | Format int // Image compression format 39 | } 40 | 41 | // GetDecoderVersion returns decoder's version number, packed in hexadecimal. 42 | // e.g; v0.4.2 is 0x000402 43 | func GetDecoderVersion() (v int) { 44 | return int(C.WebPGetDecoderVersion()) 45 | } 46 | 47 | // GetInfo retrives width/height from data bytes. 48 | func GetInfo(data []byte) (width, height int) { 49 | var w, h C.int 50 | C.WebPGetInfo((*C.uint8_t)(&data[0]), (C.size_t)(len(data)), &w, &h) 51 | return int(w), int(h) 52 | } 53 | 54 | // GetFeatures returns features as BitstreamFeatures retrived from data stream. 55 | func GetFeatures(data []byte) (f *BitstreamFeatures, err error) { 56 | var cf C.WebPBitstreamFeatures 57 | status := C.WebPGetFeatures((*C.uint8_t)(&data[0]), (C.size_t)(len(data)), &cf) 58 | 59 | if status != C.VP8_STATUS_OK { 60 | return nil, fmt.Errorf("WebPGetFeatures returns unexpected status: %s", statusString(status)) 61 | } 62 | 63 | f = &BitstreamFeatures{ 64 | Width: int(cf.width), // TODO: use Rectangle instaed? 65 | Height: int(cf.height), 66 | HasAlpha: cf.has_alpha > 0, 67 | HasAnimation: cf.has_animation > 0, 68 | Format: int(cf.format), 69 | } 70 | return 71 | } 72 | 73 | // DecodeYUVA decodes WebP image into YUV image with alpha channel, and returns 74 | // it as *YUVAImage. 75 | func DecodeYUVA(data []byte, options *DecoderOptions) (img *YUVAImage, err error) { 76 | config, err := initDecoderConfig(options) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | cDataPtr := (*C.uint8_t)(&data[0]) 82 | cDataSize := (C.size_t)(len(data)) 83 | 84 | // Retrive WebP features from data stream 85 | if status := C.WebPGetFeatures(cDataPtr, cDataSize, &config.input); status != C.VP8_STATUS_OK { 86 | return nil, fmt.Errorf("Could not get features from the data stream, return %s", statusString(status)) 87 | } 88 | 89 | outWidth, outHeight := calcOutputSize(config) 90 | buf := (*C.WebPYUVABuffer)(unsafe.Pointer(&config.output.u[0])) 91 | 92 | // Set up output configurations 93 | colorSpace := YUV420 94 | config.output.colorspace = C.MODE_YUV 95 | if config.input.has_alpha > 0 { 96 | colorSpace = YUV420A 97 | config.output.colorspace = C.MODE_YUVA 98 | } 99 | config.output.is_external_memory = 1 100 | 101 | // Allocate image and fill into buffer 102 | img = NewYUVAImage(image.Rect(0, 0, outWidth, outHeight), colorSpace) 103 | buf.y = (*C.uint8_t)(&img.Y[0]) 104 | buf.u = (*C.uint8_t)(&img.Cb[0]) 105 | buf.v = (*C.uint8_t)(&img.Cr[0]) 106 | buf.a = nil 107 | buf.y_stride = C.int(img.YStride) 108 | buf.u_stride = C.int(img.CStride) 109 | buf.v_stride = C.int(img.CStride) 110 | buf.a_stride = 0 111 | buf.y_size = C.size_t(len(img.Y)) 112 | buf.u_size = C.size_t(len(img.Cb)) 113 | buf.v_size = C.size_t(len(img.Cr)) 114 | buf.a_size = 0 115 | 116 | if config.input.has_alpha > 0 { 117 | buf.a = (*C.uint8_t)(&img.A[0]) 118 | buf.a_stride = C.int(img.AStride) 119 | buf.a_size = C.size_t(len(img.A)) 120 | } 121 | 122 | if status := C.WebPDecode(cDataPtr, cDataSize, config); status != C.VP8_STATUS_OK { 123 | return nil, fmt.Errorf("Could not decode data stream, return %s", statusString(status)) 124 | } 125 | 126 | return 127 | } 128 | 129 | // DecodeRGBA decodes WebP image into rgbA image and returns it as an *image.RGBA. 130 | func DecodeRGBA(data []byte, options *DecoderOptions) (img *image.RGBA, err error) { 131 | config, err := initDecoderConfig(options) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | cDataPtr := (*C.uint8_t)(&data[0]) 137 | cDataSize := (C.size_t)(len(data)) 138 | 139 | // Retrive WebP features 140 | if status := C.WebPGetFeatures(cDataPtr, cDataSize, &config.input); status != C.VP8_STATUS_OK { 141 | return nil, fmt.Errorf("Could not get features from the data stream, return %s", statusString(status)) 142 | } 143 | 144 | // Allocate output image 145 | outWidth, outHeight := calcOutputSize(config) 146 | img = image.NewRGBA(image.Rect(0, 0, outWidth, outHeight)) 147 | 148 | // Set up output configurations 149 | config.output.colorspace = C.MODE_rgbA 150 | config.output.is_external_memory = 1 151 | 152 | // Allocate WebPRGBABuffer and fill in the pointers to output image 153 | buf := (*C.WebPRGBABuffer)(unsafe.Pointer(&config.output.u[0])) 154 | buf.rgba = (*C.uint8_t)(&img.Pix[0]) 155 | buf.stride = C.int(img.Stride) 156 | buf.size = (C.size_t)(len(img.Pix)) 157 | 158 | // Decode 159 | if status := C.WebPDecode(cDataPtr, cDataSize, config); status != C.VP8_STATUS_OK { 160 | return nil, fmt.Errorf("Could not decode data stream, return %s", statusString(status)) 161 | } 162 | 163 | return 164 | } 165 | 166 | // DecodeNRGBA decodes WebP image into RGBA image and returns it as an *image.NRGBA. 167 | func DecodeNRGBA(data []byte, options *DecoderOptions) (img *image.NRGBA, err error) { 168 | config, err := initDecoderConfig(options) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | cDataPtr := (*C.uint8_t)(&data[0]) 174 | cDataSize := (C.size_t)(len(data)) 175 | 176 | // Retrive WebP features 177 | if status := C.WebPGetFeatures(cDataPtr, cDataSize, &config.input); status != C.VP8_STATUS_OK { 178 | return nil, fmt.Errorf("Could not get features from the data stream, return %s", statusString(status)) 179 | } 180 | 181 | // Allocate output image 182 | outWidth, outHeight := calcOutputSize(config) 183 | img = image.NewNRGBA(image.Rect(0, 0, outWidth, outHeight)) 184 | 185 | // Set up output configurations 186 | config.output.colorspace = C.MODE_RGBA 187 | config.output.is_external_memory = 1 188 | 189 | // Allocate WebPRGBABuffer and fill in the pointers to output image 190 | buf := (*C.WebPRGBABuffer)(unsafe.Pointer(&config.output.u[0])) 191 | buf.rgba = (*C.uint8_t)(&img.Pix[0]) 192 | buf.stride = C.int(img.Stride) 193 | buf.size = (C.size_t)(len(img.Pix)) 194 | 195 | // Decode 196 | if status := C.WebPDecode(cDataPtr, cDataSize, config); status != C.VP8_STATUS_OK { 197 | return nil, fmt.Errorf("Could not decode data stream, return %s", statusString(status)) 198 | } 199 | 200 | return 201 | } 202 | 203 | // sattusString convert the VP8StatsCode to string. 204 | func statusString(status C.VP8StatusCode) string { 205 | switch status { 206 | case C.VP8_STATUS_OK: 207 | return "VP8_STATUS_OK" 208 | case C.VP8_STATUS_OUT_OF_MEMORY: 209 | return "VP8_STATUS_OUT_OF_MEMORY" 210 | case C.VP8_STATUS_INVALID_PARAM: 211 | return "VP8_STATUS_INVALID_PARAM" 212 | case C.VP8_STATUS_BITSTREAM_ERROR: 213 | return "VP8_STATUS_BITSTREAM_ERROR" 214 | case C.VP8_STATUS_UNSUPPORTED_FEATURE: 215 | return "VP8_STATUS_UNSUPPORTED_FEATURE" 216 | case C.VP8_STATUS_SUSPENDED: 217 | return "VP8_STATUS_SUSPENDED" 218 | case C.VP8_STATUS_USER_ABORT: 219 | return "VP8_STATUS_USER_ABORT" 220 | case C.VP8_STATUS_NOT_ENOUGH_DATA: 221 | return "VP8_STATUS_NOT_ENOUGH_DATA" 222 | } 223 | return "Unexpected Status Code" 224 | } 225 | 226 | // initDecoderConfing initializes a decoder configration and sets up the options. 227 | func initDecoderConfig(options *DecoderOptions) (config *C.WebPDecoderConfig, err error) { 228 | // Initialize decoder config 229 | config = &C.WebPDecoderConfig{} 230 | if C.WebPInitDecoderConfig(config) == 0 { 231 | return nil, errors.New("Could not initialize decoder config") 232 | } 233 | 234 | // Set up decoder options 235 | if options.BypassFiltering { 236 | config.options.bypass_filtering = 1 237 | } 238 | if options.NoFancyUpsampling { 239 | config.options.no_fancy_upsampling = 1 240 | } 241 | if options.Crop.Max.X > 0 && options.Crop.Max.Y > 0 { 242 | config.options.use_cropping = 1 243 | config.options.crop_left = C.int(options.Crop.Min.X) 244 | config.options.crop_top = C.int(options.Crop.Min.Y) 245 | config.options.crop_width = C.int(options.Crop.Dx()) 246 | config.options.crop_height = C.int(options.Crop.Dy()) 247 | } 248 | if options.Scale.Max.X > 0 && options.Scale.Max.Y > 0 { 249 | config.options.use_scaling = 1 250 | config.options.scaled_width = C.int(options.Scale.Max.X) 251 | config.options.scaled_height = C.int(options.Scale.Max.Y) 252 | } 253 | if options.UseThreads { 254 | config.options.use_threads = 1 255 | } 256 | config.options.dithering_strength = C.int(options.DitheringStrength) 257 | 258 | return 259 | } 260 | 261 | // calcOutputSize retrives width and height of output image from the decoder configuration. 262 | func calcOutputSize(config *C.WebPDecoderConfig) (width, height int) { 263 | options := config.options 264 | if options.use_scaling > 0 { 265 | width = int(config.options.scaled_width) 266 | height = int(config.options.scaled_height) 267 | return 268 | } 269 | if config.options.use_cropping > 0 { 270 | width = int(config.options.crop_width) 271 | height = int(config.options.crop_height) 272 | return 273 | } 274 | 275 | width = int(config.input.width) 276 | height = int(config.input.height) 277 | return 278 | } 279 | -------------------------------------------------------------------------------- /webp/webp_test.go: -------------------------------------------------------------------------------- 1 | package webp_test 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "os" 10 | "reflect" 11 | "testing" 12 | 13 | "github.com/pixiv/go-libwebp/test/util" 14 | "github.com/pixiv/go-libwebp/webp" 15 | ) 16 | 17 | func TestMain(m *testing.M) { 18 | result := m.Run() 19 | if webp.GetDestinationManagerMapLen() > 0 { 20 | fmt.Println("destinationManager leaked") 21 | result = 2 22 | } 23 | os.Exit(result) 24 | } 25 | 26 | // 27 | // Decode 28 | // 29 | 30 | // Test Get Decoder Version 31 | func TestGetDecoderVersion(t *testing.T) { 32 | v := webp.GetDecoderVersion() 33 | if v < 0 { 34 | t.Errorf("GetDecoderVersion should returns positive version number, got %v\n", v) 35 | } 36 | } 37 | 38 | func TestGetInfo(t *testing.T) { 39 | data := util.ReadFile("cosmos.webp") 40 | width, height := webp.GetInfo(data) 41 | 42 | if width != 1024 { 43 | t.Errorf("Expected width: %d, but got %d", 1024, width) 44 | } 45 | if height != 768 { 46 | t.Errorf("Expected height: %d, but got %d", 768, height) 47 | } 48 | } 49 | 50 | func TestGetFeatures(t *testing.T) { 51 | data := util.ReadFile("cosmos.webp") 52 | f, err := webp.GetFeatures(data) 53 | if err != nil { 54 | t.Errorf("Got Error: %v", err) 55 | return 56 | } 57 | if got := f.Width; got != 1024 { 58 | t.Errorf("Expected Width: %v, but got %v", 1024, got) 59 | } 60 | if got := f.Height; got != 768 { 61 | t.Errorf("Expected Width: %v, but got %v", 768, got) 62 | } 63 | if got := f.HasAlpha; got != false { 64 | t.Errorf("Expected HasAlpha: %v, but got %v", false, got) 65 | } 66 | if got := f.HasAnimation; got != false { 67 | t.Errorf("Expected HasAlpha: %v, but got %v", false, got) 68 | } 69 | if got := f.Format; got != 1 { 70 | t.Errorf("Expected Format: %v, but got %v", 1, got) 71 | } 72 | } 73 | 74 | func TestDecodeYUV(t *testing.T) { 75 | files := []string{ 76 | "cosmos.webp", 77 | "butterfly.webp", 78 | "kinkaku.webp", 79 | "yellow-rose-3.webp", 80 | } 81 | 82 | for _, file := range files { 83 | data := util.ReadFile(file) 84 | options := &webp.DecoderOptions{} 85 | 86 | _, err := webp.DecodeYUVA(data, options) 87 | if err != nil { 88 | t.Errorf("Got Error: %v", err) 89 | return 90 | } 91 | } 92 | } 93 | 94 | func TestDecodeRGBA(t *testing.T) { 95 | files := []string{ 96 | "cosmos.webp", 97 | "butterfly.webp", 98 | "kinkaku.webp", 99 | "yellow-rose-3.webp", 100 | } 101 | 102 | for _, file := range files { 103 | data := util.ReadFile(file) 104 | options := &webp.DecoderOptions{} 105 | 106 | _, err := webp.DecodeRGBA(data, options) 107 | if err != nil { 108 | t.Errorf("Got Error: %v", err) 109 | return 110 | } 111 | } 112 | } 113 | 114 | func TestDecodeNRGBA(t *testing.T) { 115 | files := []string{ 116 | "cosmos.webp", 117 | "butterfly.webp", 118 | "kinkaku.webp", 119 | "yellow-rose-3.webp", 120 | } 121 | 122 | for _, file := range files { 123 | data := util.ReadFile(file) 124 | options := &webp.DecoderOptions{} 125 | 126 | _, err := webp.DecodeNRGBA(data, options) 127 | if err != nil { 128 | t.Errorf("Got Error: %v", err) 129 | return 130 | } 131 | } 132 | } 133 | 134 | func TestDecodeRGBAWithCropping(t *testing.T) { 135 | data := util.ReadFile("cosmos.webp") 136 | crop := image.Rect(100, 100, 300, 200) 137 | 138 | options := &webp.DecoderOptions{ 139 | Crop: crop, 140 | } 141 | 142 | img, err := webp.DecodeRGBA(data, options) 143 | if err != nil { 144 | t.Errorf("Got Error: %v", err) 145 | return 146 | } 147 | if img.Rect.Dx() != crop.Dx() || img.Rect.Dy() != crop.Dy() { 148 | t.Errorf("Decoded image should cropped to %v, but got %v", crop, img.Rect) 149 | } 150 | } 151 | 152 | func TestDecodeRGBAWithScaling(t *testing.T) { 153 | data := util.ReadFile("cosmos.webp") 154 | scale := image.Rect(0, 0, 640, 480) 155 | 156 | options := &webp.DecoderOptions{ 157 | Scale: scale, 158 | } 159 | 160 | img, err := webp.DecodeRGBA(data, options) 161 | if err != nil { 162 | t.Errorf("Got Error: %v", err) 163 | return 164 | } 165 | if img.Rect.Dx() != scale.Dx() || img.Rect.Dy() != scale.Dy() { 166 | t.Errorf("Decoded image should scaled to %v, but got %v", scale, img.Rect) 167 | } 168 | } 169 | 170 | // 171 | // Encoding 172 | // 173 | 174 | func TestEncodeRGBA(t *testing.T) { 175 | img := util.ReadPNG("yellow-rose-3.png") 176 | if _, ok := img.(*image.NRGBA); !ok { 177 | t.Fatalf("image is not NRGBA: %v", reflect.TypeOf(img)) 178 | } 179 | 180 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 181 | if err != nil { 182 | t.Fatalf("got error: %v", err) 183 | } 184 | 185 | f := util.CreateFile("TestEncodeRGBA.webp") 186 | w := bufio.NewWriter(f) 187 | defer func() { 188 | w.Flush() 189 | f.Close() 190 | }() 191 | 192 | if err := webp.EncodeRGBA(w, img, config); err != nil { 193 | t.Errorf("Got Error: %v", err) 194 | return 195 | } 196 | } 197 | 198 | func TestEncodeRGBAWithProgress(t *testing.T) { 199 | img := util.ReadPNG("yellow-rose-3.png") 200 | if _, ok := img.(*image.NRGBA); !ok { 201 | t.Fatalf("image is not NRGBA: %v", reflect.TypeOf(img)) 202 | } 203 | 204 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 205 | if err != nil { 206 | t.Fatalf("got error: %v", err) 207 | } 208 | 209 | f := util.CreateFile("TestEncodeRGBAWithProgress.webp") 210 | w := bufio.NewWriter(f) 211 | defer func() { 212 | w.Flush() 213 | f.Close() 214 | }() 215 | 216 | if err := webp.EncodeRGBAWithProgress(w, img, config, func(i int) bool { 217 | t.Logf("Progress: %v", i) 218 | return true 219 | }); err != nil { 220 | t.Errorf("Got Error: %v", err) 221 | return 222 | } 223 | } 224 | 225 | func TestEncodeRGBAWithProgressCanceled(t *testing.T) { 226 | img := util.ReadPNG("yellow-rose-3.png") 227 | if _, ok := img.(*image.NRGBA); !ok { 228 | t.Fatalf("image is not NRGBA: %v", reflect.TypeOf(img)) 229 | } 230 | 231 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 232 | if err != nil { 233 | t.Fatalf("got error: %v", err) 234 | } 235 | 236 | f := util.CreateFile("TestEncodeRGBAWithProgress.webp") 237 | w := bufio.NewWriter(f) 238 | defer func() { 239 | w.Flush() 240 | f.Close() 241 | }() 242 | 243 | var encodeErr *webp.EncodeError 244 | if err := webp.EncodeRGBAWithProgress(w, img, config, func(i int) bool { 245 | t.Logf("Progress: %v", i) 246 | return false 247 | }); !errors.As(err, &encodeErr) || encodeErr.EncodeErrorCode() != webp.EncodeErrorCodeVP8EncErrorUserAbort { 248 | t.Errorf("Expected UserAbort (%v) but received: %v", webp.EncodeErrorCodeVP8EncErrorUserAbort, err) 249 | return 250 | } 251 | } 252 | 253 | func convertToRGBImage(t *testing.T, origImg image.Image) *webp.RGBImage { 254 | bounds := origImg.Bounds() 255 | img := webp.NewRGBImage(bounds) 256 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 257 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 258 | rgba := origImg.At(x, y) 259 | r, g, b, _ := rgba.RGBA() 260 | img.Pix[y*img.Stride+x*3+0] = uint8(r >> 8) 261 | img.Pix[y*img.Stride+x*3+1] = uint8(g >> 8) 262 | img.Pix[y*img.Stride+x*3+2] = uint8(b >> 8) 263 | } 264 | } 265 | 266 | return img 267 | } 268 | 269 | func TestEncodeRGB(t *testing.T) { 270 | origImg := util.ReadPNG("yellow-rose-3.png") 271 | img := convertToRGBImage(t, origImg) 272 | 273 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 274 | if err != nil { 275 | t.Fatalf("got error: %v", err) 276 | } 277 | 278 | f := util.CreateFile("TestEncodeRGB.webp") 279 | w := bufio.NewWriter(f) 280 | defer func() { 281 | w.Flush() 282 | f.Close() 283 | }() 284 | 285 | if err := webp.EncodeRGBA(w, img, config); err != nil { 286 | t.Errorf("Got Error: %v", err) 287 | return 288 | } 289 | } 290 | 291 | func TestEncodeRGBWithProgress(t *testing.T) { 292 | origImg := util.ReadPNG("yellow-rose-3.png") 293 | img := convertToRGBImage(t, origImg) 294 | 295 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 296 | if err != nil { 297 | t.Fatalf("got error: %v", err) 298 | } 299 | 300 | f := util.CreateFile("TestEncodeRGBWithProgress.webp") 301 | w := bufio.NewWriter(f) 302 | defer func() { 303 | w.Flush() 304 | f.Close() 305 | }() 306 | 307 | if err := webp.EncodeRGBAWithProgress(w, img, config, func(i int) bool { 308 | t.Logf("Progress: %v", i) 309 | return true 310 | }); err != nil { 311 | t.Errorf("Got Error: %v", err) 312 | return 313 | } 314 | } 315 | 316 | func TestEncodeRGBWithProgressCanceled(t *testing.T) { 317 | origImg := util.ReadPNG("yellow-rose-3.png") 318 | img := convertToRGBImage(t, origImg) 319 | 320 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 321 | if err != nil { 322 | t.Fatalf("got error: %v", err) 323 | } 324 | 325 | f := util.CreateFile("TestEncodeRGBWithProgress.webp") 326 | w := bufio.NewWriter(f) 327 | defer func() { 328 | w.Flush() 329 | f.Close() 330 | }() 331 | 332 | var encodeErr *webp.EncodeError 333 | if err := webp.EncodeRGBAWithProgress(w, img, config, func(i int) bool { 334 | t.Logf("Progress: %v", i) 335 | return false 336 | }); !errors.As(err, &encodeErr) || encodeErr.EncodeErrorCode() != webp.EncodeErrorCodeVP8EncErrorUserAbort { 337 | t.Errorf("Expected UserAbort (%v) but received: %v", webp.EncodeErrorCodeVP8EncErrorUserAbort, err) 338 | return 339 | } 340 | } 341 | 342 | func TestEncodeYUVA(t *testing.T) { 343 | data := util.ReadFile("cosmos.webp") 344 | options := &webp.DecoderOptions{} 345 | 346 | img, err := webp.DecodeYUVA(data, options) 347 | if err != nil { 348 | t.Errorf("Got Error: %v in decoding", err) 349 | return 350 | } 351 | 352 | f := util.CreateFile("TestEncodeYUVA.webp") 353 | w := bufio.NewWriter(f) 354 | defer func() { 355 | w.Flush() 356 | f.Close() 357 | }() 358 | 359 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 360 | if err != nil { 361 | t.Fatalf("got error: %v", err) 362 | } 363 | 364 | if err := webp.EncodeYUVA(w, img, config); err != nil { 365 | t.Errorf("Got Error: %v", err) 366 | return 367 | } 368 | } 369 | 370 | func TestEncodeYUVAWithProgress(t *testing.T) { 371 | data := util.ReadFile("cosmos.webp") 372 | options := &webp.DecoderOptions{} 373 | 374 | img, err := webp.DecodeYUVA(data, options) 375 | if err != nil { 376 | t.Errorf("Got Error: %v in decoding", err) 377 | return 378 | } 379 | 380 | f := util.CreateFile("TestEncodeYUVA.webp") 381 | w := bufio.NewWriter(f) 382 | defer func() { 383 | w.Flush() 384 | f.Close() 385 | }() 386 | 387 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 388 | if err != nil { 389 | t.Fatalf("got error: %v", err) 390 | } 391 | 392 | if err := webp.EncodeYUVAWithProgress(w, img, config, func(i int) bool { 393 | t.Logf("Progress: %v", i) 394 | return true 395 | }); err != nil { 396 | t.Errorf("Got Error: %v", err) 397 | return 398 | } 399 | } 400 | 401 | func TestEncodeYUVAWithProgressCanceled(t *testing.T) { 402 | data := util.ReadFile("cosmos.webp") 403 | options := &webp.DecoderOptions{} 404 | 405 | img, err := webp.DecodeYUVA(data, options) 406 | if err != nil { 407 | t.Errorf("Got Error: %v in decoding", err) 408 | return 409 | } 410 | 411 | f := util.CreateFile("TestEncodeYUVA.webp") 412 | w := bufio.NewWriter(f) 413 | defer func() { 414 | w.Flush() 415 | f.Close() 416 | }() 417 | 418 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 419 | if err != nil { 420 | t.Fatalf("got error: %v", err) 421 | } 422 | 423 | var encodeErr *webp.EncodeError 424 | if err := webp.EncodeYUVAWithProgress(w, img, config, func(i int) bool { 425 | t.Logf("Progress: %v", i) 426 | return false 427 | }); !errors.As(err, &encodeErr) || encodeErr.EncodeErrorCode() != webp.EncodeErrorCodeVP8EncErrorUserAbort { 428 | t.Errorf("Expected UserAbort (%v) but received: %v", webp.EncodeErrorCodeVP8EncErrorUserAbort, err) 429 | return 430 | } 431 | } 432 | 433 | func TestEncodeGray(t *testing.T) { 434 | p := image.NewGray(image.Rect(0, 0, 1, 10)) 435 | for i := 0; i < 10; i++ { 436 | p.SetGray(0, i, color.Gray{uint8(float32(i) / 10 * 255)}) 437 | } 438 | 439 | f := util.CreateFile("TestEncodeGray.webp") 440 | w := bufio.NewWriter(f) 441 | defer func() { 442 | w.Flush() 443 | f.Close() 444 | }() 445 | 446 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 447 | if err != nil { 448 | t.Fatalf("got error: %v", err) 449 | } 450 | 451 | if err := webp.EncodeGray(w, p, config); err != nil { 452 | t.Errorf("Got Error: %v", err) 453 | return 454 | } 455 | } 456 | 457 | func TestEncodeGrayWithProgress(t *testing.T) { 458 | p := image.NewGray(image.Rect(0, 0, 1, 10)) 459 | for i := 0; i < 10; i++ { 460 | p.SetGray(0, i, color.Gray{uint8(float32(i) / 10 * 255)}) 461 | } 462 | 463 | f := util.CreateFile("TestEncodeGray.webp") 464 | w := bufio.NewWriter(f) 465 | defer func() { 466 | w.Flush() 467 | f.Close() 468 | }() 469 | 470 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 471 | if err != nil { 472 | t.Fatalf("got error: %v", err) 473 | } 474 | 475 | if err := webp.EncodeGrayWithProgress(w, p, config, func(i int) bool { 476 | t.Logf("Progress: %v", i) 477 | return true 478 | }); err != nil { 479 | t.Errorf("Got Error: %v", err) 480 | return 481 | } 482 | } 483 | 484 | func TestEncodeGrayWithProgressCanceled(t *testing.T) { 485 | p := image.NewGray(image.Rect(0, 0, 1, 10)) 486 | for i := 0; i < 10; i++ { 487 | p.SetGray(0, i, color.Gray{uint8(float32(i) / 10 * 255)}) 488 | } 489 | 490 | f := util.CreateFile("TestEncodeGray.webp") 491 | w := bufio.NewWriter(f) 492 | defer func() { 493 | w.Flush() 494 | f.Close() 495 | }() 496 | 497 | config, err := webp.ConfigPreset(webp.PresetDefault, 100) 498 | if err != nil { 499 | t.Fatalf("got error: %v", err) 500 | } 501 | 502 | var encodeErr *webp.EncodeError 503 | if err := webp.EncodeGrayWithProgress(w, p, config, func(i int) bool { 504 | t.Logf("Progress: %v", i) 505 | return false 506 | }); !errors.As(err, &encodeErr) || encodeErr.EncodeErrorCode() != webp.EncodeErrorCodeVP8EncErrorUserAbort { 507 | t.Errorf("Expected UserAbort (%v) but received: %v", webp.EncodeErrorCodeVP8EncErrorUserAbort, err) 508 | return 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /webp/encode.go: -------------------------------------------------------------------------------- 1 | package webp 2 | 3 | /* 4 | #include 5 | #include 6 | #include 7 | 8 | int golibwebpWriteWebP(uint8_t*, size_t, struct WebPPicture*); 9 | int golibwebpProgressHook(int, struct WebPPicture*); 10 | 11 | static WebPPicture *calloc_WebPPicture(void) { 12 | return calloc(sizeof(WebPPicture), 1); 13 | } 14 | 15 | static void free_WebPPicture(WebPPicture* webpPicture) { 16 | free(webpPicture); 17 | } 18 | 19 | static int webpEncodeYUVA(const WebPConfig *config, WebPPicture *picture, uint8_t *y, uint8_t *u, uint8_t *v, uint8_t *a) { 20 | picture->y = y; 21 | picture->u = u; 22 | picture->v = v; 23 | if (picture->colorspace == WEBP_YUV420A) { 24 | picture->a = a; 25 | } 26 | picture->writer = (WebPWriterFunction)golibwebpWriteWebP; 27 | picture->progress_hook = (WebPProgressHook)golibwebpProgressHook; 28 | 29 | return WebPEncode(config, picture); 30 | } 31 | 32 | static int webpEncodeGray(const WebPConfig *config, WebPPicture *picture, uint8_t *y) { 33 | int ok = 0; 34 | const int c_width = (picture->width + 1) >> 1; 35 | const int c_height = (picture->height + 1) >> 1; 36 | const int c_stride = c_width; 37 | const int c_size = c_stride * c_height; 38 | const int gray = 128; 39 | uint8_t* chroma; 40 | 41 | chroma = malloc(c_size); 42 | if (!chroma) { 43 | return 0; 44 | } 45 | memset(chroma, gray, c_size); 46 | 47 | picture->y = y; 48 | picture->u = chroma; 49 | picture->v = chroma; 50 | picture->uv_stride = c_stride; 51 | picture->writer = (WebPWriterFunction)golibwebpWriteWebP; 52 | picture->progress_hook = (WebPProgressHook)golibwebpProgressHook; 53 | 54 | ok = WebPEncode(config, picture); 55 | 56 | free(chroma); 57 | 58 | return ok; 59 | } 60 | */ 61 | import "C" 62 | 63 | import ( 64 | "errors" 65 | "fmt" 66 | "image" 67 | "io" 68 | "sync" 69 | "unsafe" 70 | ) 71 | 72 | // Config specifies WebP encoding configuration. 73 | type Config struct { 74 | c C.WebPConfig 75 | } 76 | 77 | type ProgressHook func(int) bool 78 | 79 | type EncodeError struct { 80 | encodeErrorCode EncodeErrorCode 81 | } 82 | 83 | func (e *EncodeError) Error() string { 84 | return fmt.Sprintf("Encoding error: %d", e.encodeErrorCode) 85 | } 86 | 87 | func (e *EncodeError) EncodeErrorCode() EncodeErrorCode { 88 | return e.encodeErrorCode 89 | } 90 | 91 | var _ error = &EncodeError{} 92 | 93 | type EncodeErrorCode int 94 | 95 | const ( 96 | EncodeErrorCodeVP8EncOK EncodeErrorCode = C.VP8_ENC_OK 97 | EncodeErrorCodeVP8EncErrorOutOfMemory EncodeErrorCode = C.VP8_ENC_ERROR_OUT_OF_MEMORY 98 | EncodeErrorCodeVP8EncErrorBitstreamOutOfMemory EncodeErrorCode = C.VP8_ENC_ERROR_BITSTREAM_OUT_OF_MEMORY 99 | EncodeErrorCodeVP8EncErrorNullParameter EncodeErrorCode = C.VP8_ENC_ERROR_NULL_PARAMETER 100 | EncodeErrorCodeVP8EncErrorInvalidConfiguration EncodeErrorCode = C.VP8_ENC_ERROR_INVALID_CONFIGURATION 101 | EncodeErrorCodeVP8EncErrorBadDimension EncodeErrorCode = C.VP8_ENC_ERROR_BAD_DIMENSION 102 | EncodeErrorCodeVP8EncErrorPartition0Overflow EncodeErrorCode = C.VP8_ENC_ERROR_PARTITION0_OVERFLOW 103 | EncodeErrorCodeVP8EncErrorPartitionOverflow EncodeErrorCode = C.VP8_ENC_ERROR_PARTITION_OVERFLOW 104 | EncodeErrorCodeVP8EncErrorBadWrite EncodeErrorCode = C.VP8_ENC_ERROR_BAD_WRITE 105 | EncodeErrorCodeVP8EncErrorFileTooBig EncodeErrorCode = C.VP8_ENC_ERROR_FILE_TOO_BIG 106 | EncodeErrorCodeVP8EncErrorUserAbort EncodeErrorCode = C.VP8_ENC_ERROR_USER_ABORT 107 | EncodeErrorCodeVP8ErrorLast EncodeErrorCode = C.VP8_ENC_ERROR_LAST 108 | ) 109 | 110 | var errWebPPictureAllocate = errors.New("Could not allocate webp picture") 111 | var errWebPPictureInitialize = errors.New("Could not initialize webp picture") 112 | var errUnsupportedImageType = errors.New("unsupported image type") 113 | var errInvalidConfiguration = errors.New("invalid configuration") 114 | var errInitializeWebPConfig = errors.New("failed to initialize webp config") 115 | 116 | // ConfigPreset returns initialized configuration with given preset and quality 117 | // factor. 118 | func ConfigPreset(preset Preset, quality float32) (*Config, error) { 119 | c := &Config{} 120 | if C.WebPConfigPreset(&c.c, C.WebPPreset(preset), C.float(quality)) == 0 { 121 | return nil, errInitializeWebPConfig 122 | } 123 | return c, nil 124 | } 125 | 126 | // ConfigLosslessPreset returns initialized configuration for lossless encoding. 127 | // Given level specifies desired efficiency level between 0 (fastest, lowest 128 | // compression) and 9 (slower, best compression). 129 | func ConfigLosslessPreset(level int) (*Config, error) { 130 | c := &Config{} 131 | if C.WebPConfigPreset(&c.c, C.WebPPreset(PresetDefault), C.float(0)) == 0 { 132 | return nil, errInitializeWebPConfig 133 | } 134 | if C.WebPConfigLosslessPreset(&c.c, C.int(level)) == 0 { 135 | return nil, errInitializeWebPConfig 136 | } 137 | return c, nil 138 | } 139 | 140 | // SetLossless sets lossless parameter that specifies whether to enable lossless 141 | // encoding. 142 | func (c *Config) SetLossless(v bool) { 143 | c.c.lossless = boolToValue(v) 144 | } 145 | 146 | // Lossless returns lossless parameter flag whether to enable lossless encoding. 147 | func (c *Config) Lossless() bool { 148 | return valueToBool(c.c.lossless) 149 | } 150 | 151 | // SetQuality sets encoding quality factor between 0 (smallest file) and 100 152 | // (biggest). 153 | func (c *Config) SetQuality(v float32) { 154 | c.c.quality = C.float(v) 155 | } 156 | 157 | // Quality returns encoding quality factor. 158 | func (c *Config) Quality() float32 { 159 | return float32(c.c.quality) 160 | } 161 | 162 | // SetMethod sets method parameter that specifies quality/speed trade-off 163 | // (0=fast, 6=slower-better). 164 | func (c *Config) SetMethod(v int) { 165 | c.c.method = C.int(v) 166 | } 167 | 168 | // Method returns method parameter. 169 | func (c *Config) Method() int { 170 | return int(c.c.method) 171 | } 172 | 173 | // SetImageHint sets hint for image type. It is used to only lossless encoding 174 | // for now. 175 | func (c *Config) SetImageHint(v ImageHint) { 176 | c.c.image_hint = C.WebPImageHint(v) 177 | } 178 | 179 | // ImageHint returns hint parameter for image type. 180 | func (c *Config) ImageHint() ImageHint { 181 | return ImageHint(c.c.image_hint) 182 | } 183 | 184 | // SetTargetPSNR sets target PSNR value that specifies the minimal distortion to 185 | // try to achieve. If it sets 0, disable target PSNR. 186 | func (c *Config) SetTargetPSNR(v float32) { 187 | c.c.target_PSNR = C.float(v) 188 | } 189 | 190 | // TargetPSNR returns target PSNR value. 191 | func (c *Config) TargetPSNR() float32 { 192 | return float32(c.c.target_PSNR) 193 | } 194 | 195 | // SetSegments sets segments parameter that specifies the maximum number of 196 | // segments to use, in [1..4]. 197 | func (c *Config) SetSegments(v int) { 198 | c.c.segments = C.int(v) 199 | } 200 | 201 | // Segments returns segments parameter. 202 | func (c *Config) Segments() int { 203 | return int(c.c.segments) 204 | } 205 | 206 | // SetSNSStrength sets SNS strength parameter between 0 (off) and 100 (maximum). 207 | func (c *Config) SetSNSStrength(v int) { 208 | c.c.sns_strength = C.int(v) 209 | } 210 | 211 | // SNSStrength returns SNS strength parameter. 212 | func (c *Config) SNSStrength() int { 213 | return int(c.c.sns_strength) 214 | } 215 | 216 | // SetFilterStrength sets filter strength parameter between 0 (off) and 100 217 | // (strongest). 218 | func (c *Config) SetFilterStrength(v int) { 219 | c.c.filter_strength = C.int(v) 220 | } 221 | 222 | // FilterStrength returns filter strength parameter. 223 | func (c *Config) FilterStrength() int { 224 | return int(c.c.filter_strength) 225 | } 226 | 227 | // SetFilterSharpness sets filter sharpness parameter between 0 (off) and 7 228 | // (least sharp). 229 | func (c *Config) SetFilterSharpness(v int) { 230 | c.c.filter_sharpness = C.int(v) 231 | } 232 | 233 | // FilterSharpness returns filter sharpness parameter. 234 | func (c *Config) FilterSharpness() int { 235 | return int(c.c.filter_sharpness) 236 | } 237 | 238 | // SetFilterType sets filter type parameter. 239 | func (c *Config) SetFilterType(v FilterType) { 240 | c.c.filter_type = C.int(v) 241 | } 242 | 243 | // FilterType returns filter type parameter. 244 | func (c *Config) FilterType() FilterType { 245 | return FilterType(c.c.filter_type) 246 | } 247 | 248 | // SetAutoFilter sets auto filter flag that specifies whether to auto adjust 249 | // filter strength. 250 | func (c *Config) SetAutoFilter(v bool) { 251 | c.c.autofilter = boolToValue(v) 252 | } 253 | 254 | // AutoFilter returns auto filter flag. 255 | func (c *Config) AutoFilter() bool { 256 | return valueToBool(c.c.autofilter) 257 | } 258 | 259 | // SetAlphaCompression sets alpha compression parameter. 260 | func (c *Config) SetAlphaCompression(v int) { 261 | c.c.alpha_compression = C.int(v) 262 | } 263 | 264 | // AlphaCompression returns alpha compression parameter. 265 | func (c *Config) AlphaCompression() int { 266 | return int(c.c.alpha_compression) 267 | } 268 | 269 | // SetAlphaFiltering sets alpha filtering parameter. 270 | func (c *Config) SetAlphaFiltering(v int) { 271 | c.c.alpha_filtering = C.int(v) 272 | } 273 | 274 | // AlphaFiltering returns alpha filtering parameter. 275 | func (c *Config) AlphaFiltering() int { 276 | return int(c.c.alpha_filtering) 277 | } 278 | 279 | // SetAlphaQuality sets alpha quality parameter. 280 | func (c *Config) SetAlphaQuality(v int) { 281 | c.c.alpha_quality = C.int(v) 282 | } 283 | 284 | // AlphaQuality returns alpha quality parameter. 285 | func (c *Config) AlphaQuality() int { 286 | return int(c.c.alpha_quality) 287 | } 288 | 289 | // SetPass sets pass parameter that specifies number of entropy-analysis passes 290 | // between 1 and 10. 291 | func (c *Config) SetPass(v int) { 292 | c.c.pass = C.int(v) 293 | } 294 | 295 | // Pass returns pass parameter. 296 | func (c *Config) Pass() int { 297 | return int(c.c.pass) 298 | } 299 | 300 | // SetPreprocessing sets preprocessing filter. 301 | func (c *Config) SetPreprocessing(v Preprocessing) { 302 | c.c.preprocessing = C.int(v) 303 | } 304 | 305 | // Preprocessing returns preprocessing filter. 306 | func (c *Config) Preprocessing() Preprocessing { 307 | return Preprocessing(c.c.preprocessing) 308 | } 309 | 310 | // SetPartitions sets partitions parameter. 311 | func (c *Config) SetPartitions(v int) { 312 | c.c.partitions = C.int(v) 313 | } 314 | 315 | // Partitions returns partitions parameter. 316 | func (c *Config) Partitions() int { 317 | return int(c.c.partitions) 318 | } 319 | 320 | // SetPartitionLimit returns partition limit parameter. 321 | func (c *Config) SetPartitionLimit(v int) { 322 | c.c.partition_limit = C.int(v) 323 | } 324 | 325 | // PartitionLimit returns partition limit parameter. 326 | func (c *Config) PartitionLimit() int { 327 | return int(c.c.partition_limit) 328 | } 329 | 330 | // SetEmulateJPEGSize sets flag whether the compression parameters remaps to 331 | // match the expected output size from JPEG compression. 332 | func (c *Config) SetEmulateJPEGSize(v bool) { 333 | c.c.emulate_jpeg_size = boolToValue(v) 334 | } 335 | 336 | // EmulateJPEGSize returns the flag whether to enable emulating JPEG size. 337 | func (c *Config) EmulateJPEGSize() bool { 338 | return valueToBool(c.c.emulate_jpeg_size) 339 | } 340 | 341 | // SetThreadLevel sets thread level parameter. If non-zero value is specified, 342 | // try and use multi-threaded encoding. 343 | func (c *Config) SetThreadLevel(v int) { 344 | c.c.thread_level = C.int(v) 345 | } 346 | 347 | // ThreadLevel returns thread level parameter. 348 | func (c *Config) ThreadLevel() int { 349 | return int(c.c.thread_level) 350 | } 351 | 352 | // SetLowMemory sets flag whether to reduce memory usage. 353 | func (c *Config) SetLowMemory(v bool) { 354 | c.c.low_memory = boolToValue(v) 355 | } 356 | 357 | // LowMemory returns low memory flag. 358 | func (c *Config) LowMemory() bool { 359 | return valueToBool(c.c.low_memory) 360 | } 361 | 362 | // SetNearLossless sets near lossless encoding factor between 0 (max loss) and 363 | // 100 (disable near lossless encoding, default). 364 | func (c *Config) SetNearLossless(v int) { 365 | c.c.near_lossless = C.int(v) 366 | } 367 | 368 | // NearLossless returns near lossless encoding factor. 369 | func (c *Config) NearLossless() int { 370 | return int(c.c.near_lossless) 371 | } 372 | 373 | // SetExact sets the flag whether to preserve the exact RGB values under 374 | // transparent area. 375 | func (c *Config) SetExact(v bool) { 376 | c.c.exact = boolToValue(v) 377 | } 378 | 379 | // Exact returns exact flag. 380 | func (c *Config) Exact() bool { 381 | return valueToBool(c.c.exact) 382 | } 383 | 384 | func boolToValue(v bool) C.int { 385 | if v { 386 | return 1 387 | } 388 | return 0 389 | } 390 | 391 | func valueToBool(v C.int) bool { 392 | if v > 0 || v < 0 { 393 | return true 394 | } 395 | return false 396 | } 397 | 398 | type destinationManager struct { 399 | writer io.Writer 400 | progressHook ProgressHook 401 | } 402 | 403 | var destinationManagerMapMutex sync.RWMutex 404 | var destinationManagerMap = make(map[uintptr]*destinationManager) 405 | 406 | // GetDestinationManagerMapLen returns the number of globally working sourceManagers for debug 407 | func GetDestinationManagerMapLen() int { 408 | destinationManagerMapMutex.RLock() 409 | defer destinationManagerMapMutex.RUnlock() 410 | return len(destinationManagerMap) 411 | } 412 | 413 | func makeDestinationManager(w io.Writer, progressHook ProgressHook, pic *C.WebPPicture) (mgr *destinationManager) { 414 | mgr = &destinationManager{writer: w, progressHook: progressHook} 415 | destinationManagerMapMutex.Lock() 416 | defer destinationManagerMapMutex.Unlock() 417 | destinationManagerMap[uintptr(unsafe.Pointer(pic))] = mgr 418 | return 419 | } 420 | 421 | func releaseDestinationManager(pic *C.WebPPicture) { 422 | destinationManagerMapMutex.Lock() 423 | defer destinationManagerMapMutex.Unlock() 424 | delete(destinationManagerMap, uintptr(unsafe.Pointer(pic))) 425 | } 426 | 427 | func getDestinationManager(pic *C.WebPPicture) *destinationManager { 428 | destinationManagerMapMutex.RLock() 429 | defer destinationManagerMapMutex.RUnlock() 430 | return destinationManagerMap[uintptr(unsafe.Pointer(pic))] 431 | } 432 | 433 | //export golibwebpWriteWebP 434 | func golibwebpWriteWebP(data *C.uint8_t, size C.size_t, pic *C.WebPPicture) C.int { 435 | mgr := getDestinationManager(pic) 436 | bytes := C.GoBytes(unsafe.Pointer(data), C.int(size)) 437 | _, err := mgr.writer.Write(bytes) 438 | if err != nil { 439 | return 0 // TODO: can't pass error message 440 | } 441 | return 1 442 | } 443 | 444 | //export golibwebpProgressHook 445 | func golibwebpProgressHook(percent C.int, pic *C.WebPPicture) C.int { 446 | mgr := getDestinationManager(pic) 447 | shouldContinue := true 448 | if mgr.progressHook != nil { 449 | func() { 450 | defer func() { 451 | if r := recover(); r != nil { 452 | shouldContinue = false 453 | } 454 | }() 455 | shouldContinue = mgr.progressHook(int(percent)) 456 | }() 457 | } 458 | 459 | return boolToValue(shouldContinue) 460 | } 461 | 462 | // EncodeRGBA encodes and writes image.Image into the writer as WebP. 463 | // Now supports image.RGBA or image.NRGBA. 464 | func EncodeRGBA(w io.Writer, img image.Image, c *Config) (err error) { 465 | return EncodeRGBAWithProgress(w, img, c, nil) 466 | } 467 | 468 | // EncodeRGBAWithProgress encodes and writes image.Image into the writer as WebP. 469 | // Now supports image.RGBA or image.NRGBA. 470 | // This function accepts progress hook function and supports cancellation. 471 | func EncodeRGBAWithProgress(w io.Writer, img image.Image, c *Config, progressHook ProgressHook) (err error) { 472 | if err = ValidateConfig(c); err != nil { 473 | return 474 | } 475 | 476 | pic := C.calloc_WebPPicture() 477 | if pic == nil { 478 | return errWebPPictureAllocate 479 | } 480 | defer C.free_WebPPicture(pic) 481 | 482 | makeDestinationManager(w, progressHook, pic) 483 | defer releaseDestinationManager(pic) 484 | 485 | if C.WebPPictureInit(pic) == 0 { 486 | return errWebPPictureInitialize 487 | } 488 | defer C.WebPPictureFree(pic) 489 | 490 | pic.use_argb = 1 491 | 492 | pic.width = C.int(img.Bounds().Dx()) 493 | pic.height = C.int(img.Bounds().Dy()) 494 | 495 | pic.progress_hook = C.WebPProgressHook(C.golibwebpProgressHook) 496 | pic.writer = C.WebPWriterFunction(C.golibwebpWriteWebP) 497 | 498 | switch p := img.(type) { 499 | case *RGBImage: 500 | C.WebPPictureImportRGB(pic, (*C.uint8_t)(&p.Pix[0]), C.int(p.Stride)) 501 | case *image.RGBA: 502 | C.WebPPictureImportRGBA(pic, (*C.uint8_t)(&p.Pix[0]), C.int(p.Stride)) 503 | case *image.NRGBA: 504 | C.WebPPictureImportRGBA(pic, (*C.uint8_t)(&p.Pix[0]), C.int(p.Stride)) 505 | default: 506 | return errUnsupportedImageType 507 | } 508 | 509 | if C.WebPEncode(&c.c, pic) == 0 { 510 | return &EncodeError{encodeErrorCode: EncodeErrorCode(pic.error_code)} 511 | } 512 | 513 | return 514 | } 515 | 516 | // EncodeGray encodes and writes Gray Image data into the writer as WebP. 517 | func EncodeGray(w io.Writer, p *image.Gray, c *Config) (err error) { 518 | return EncodeGrayWithProgress(w, p, c, nil) 519 | } 520 | 521 | // EncodeGrayWithProgress encodes and writes Gray Image data into the writer as WebP. 522 | // This function accepts progress hook function and supports cancellation. 523 | func EncodeGrayWithProgress(w io.Writer, p *image.Gray, c *Config, progressHook ProgressHook) (err error) { 524 | if err = ValidateConfig(c); err != nil { 525 | return 526 | } 527 | 528 | pic := C.calloc_WebPPicture() 529 | if pic == nil { 530 | return errWebPPictureAllocate 531 | } 532 | defer C.free_WebPPicture(pic) 533 | 534 | makeDestinationManager(w, progressHook, pic) 535 | defer releaseDestinationManager(pic) 536 | 537 | if C.WebPPictureInit(pic) == 0 { 538 | return errWebPPictureInitialize 539 | } 540 | defer C.WebPPictureFree(pic) 541 | 542 | pic.use_argb = 0 543 | pic.width = C.int(p.Rect.Dx()) 544 | pic.height = C.int(p.Rect.Dy()) 545 | pic.y_stride = C.int(p.Stride) 546 | 547 | if C.webpEncodeGray(&c.c, pic, (*C.uint8_t)(&p.Pix[0])) == 0 { 548 | return &EncodeError{encodeErrorCode: EncodeErrorCode(pic.error_code)} 549 | } 550 | 551 | return 552 | } 553 | 554 | // EncodeYUVA encodes and writes YUVA Image data into the writer as WebP. 555 | func EncodeYUVA(w io.Writer, img *YUVAImage, c *Config) (err error) { 556 | return EncodeYUVAWithProgress(w, img, c, nil) 557 | } 558 | 559 | // EncodeYUVAWithProgress encodes and writes YUVA Image data into the writer as WebP. 560 | // This function accepts progress hook function and supports cancellation. 561 | func EncodeYUVAWithProgress(w io.Writer, img *YUVAImage, c *Config, progressHook ProgressHook) (err error) { 562 | if err = ValidateConfig(c); err != nil { 563 | return 564 | } 565 | 566 | pic := C.calloc_WebPPicture() 567 | if pic == nil { 568 | return errWebPPictureAllocate 569 | } 570 | defer C.free_WebPPicture(pic) 571 | 572 | makeDestinationManager(w, progressHook, pic) 573 | defer releaseDestinationManager(pic) 574 | 575 | if C.WebPPictureInit(pic) == 0 { 576 | return errWebPPictureInitialize 577 | } 578 | defer C.WebPPictureFree(pic) 579 | 580 | pic.use_argb = 0 581 | pic.colorspace = C.WebPEncCSP(img.ColorSpace) 582 | pic.width = C.int(img.Rect.Dx()) 583 | pic.height = C.int(img.Rect.Dy()) 584 | pic.y_stride = C.int(img.YStride) 585 | pic.uv_stride = C.int(img.CStride) 586 | var a *C.uint8_t 587 | y, u, v := (*C.uint8_t)(&img.Y[0]), (*C.uint8_t)(&img.Cb[0]), (*C.uint8_t)(&img.Cr[0]) 588 | if img.ColorSpace == YUV420A { 589 | pic.a_stride = C.int(img.AStride) 590 | a = (*C.uint8_t)(&img.A[0]) 591 | } 592 | 593 | if C.webpEncodeYUVA(&c.c, pic, y, u, v, a) == 0 { 594 | return &EncodeError{encodeErrorCode: EncodeErrorCode(pic.error_code)} 595 | } 596 | return 597 | } 598 | 599 | func ValidateConfig(c *Config) error { 600 | if C.WebPValidateConfig(&c.c) == 0 { 601 | return errInvalidConfiguration 602 | } 603 | return nil 604 | } 605 | --------------------------------------------------------------------------------