commit 5af6d93b3ba64d22b0988740a5d68e6e9a7db47e Author: harukasan Date: Fri Jan 9 12:51:34 2015 +0900 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de78a86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store + +/examples/out/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2f8c88a --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Shunsuke MICHII +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5bf9e0 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +go-libwebp +========== + +A implementation of Go binding for [libwebp](https://developers.google.com/speed/webp/docs/api) written with cgo. + +## Usage + +The [examples](./examples) directory contains example codes and images. + +### Decoding WebP into image.RGBA + +``` +import ( + "io/ioutil" + + "github.com/harukasan/go-libwebp/examples/util" + "github.com/harukasan/go-libwebp/webp" +) + +func main() { + var err error + + // Read binary data + data, err := ioutil.ReadFile("examples/cosmos.webp") + if err != nil { + panic(err) + } + + // Decode + options := &webp.DecoderOptions{} + img, err := webp.DecodeRGBA(data, options) + if err != nil { + panic(err) + } + + err = util.WritePNG(img, "out/encoded_cosmos.png") + if err != nil { + panic(err) + } +} +``` + +You can set more decoding options such as cropping, flipping and scaling. + +### Encoding WebP from image.RGBA + +``` +package main + +import ( + "bufio" + "image" + + "github.com/harukasan/go-libwebp/examples/util" + "github.com/harukasan/go-libwebp/webp" +) + +func main() { + err := util.ReadPNG("examples/cosmos.png") + if err != nil { + panic() + } + + // Encode to WebP + io := ("out.webp") + w := bufio.NewWriter(f) + defer func() { + w.Flush() + f.Close() + }() + + if err := webp.EncodeRGBA(w, img.(*image.RGBA), config); err != nil { + panic(err) + } +} +``` + +## TODO + +- Incremental decoding API +- Container API (Animation) + +## License + +Copyright (c) 2014 MICHII Shunsuke. All rights reserved. + +This library is released under The BSD 2-Clause License. +See [LICENSE](./LICENSE). diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..2d62565 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +Example Codes and Images +======================== + +- [decode/decode.go](./decode/decode.go) -- a example code to decoding WebP image into image.RGBA. +- [encode/encode.go](./encode/encode.go) -- a example code to encoding and writing image.RGBA into WebP file. diff --git a/examples/decode/decode.go b/examples/decode/decode.go new file mode 100644 index 0000000..0c29b6e --- /dev/null +++ b/examples/decode/decode.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/harukasan/go-libwebp/examples/util" + "github.com/harukasan/go-libwebp/webp" +) + +func main() { + var err error + + // Read binary data + data := util.ReadFile("cosmos.webp") + + // Decode + options := &webp.DecoderOptions{} + img, err := webp.DecodeRGBA(data, options) + if err != nil { + panic(err) + } + + util.WritePNG(img, "encoded_cosmos.png") +} diff --git a/examples/encode/encode.go b/examples/encode/encode.go new file mode 100644 index 0000000..f7f7c63 --- /dev/null +++ b/examples/encode/encode.go @@ -0,0 +1,32 @@ +package main + +import ( + "bufio" + "image" + + "github.com/harukasan/go-libwebp/examples/util" + "github.com/harukasan/go-libwebp/webp" +) + +func main() { + img := util.ReadPNG("cosmos.png") + + // Create file and buffered writer + io := util.CreateFile("encoded_cosmos.webp") + w := bufio.NewWriter(io) + defer func() { + w.Flush() + io.Close() + }() + + config := webp.Config{ + Preset: webp.PresetDefault, + Quality: 90, + Method: 6, + } + + // Encode into WebP + if err := webp.EncodeRGBA(w, img.(*image.RGBA), config); err != nil { + panic(err) + } +} diff --git a/examples/images/README.md b/examples/images/README.md new file mode 100644 index 0000000..ec276b6 --- /dev/null +++ b/examples/images/README.md @@ -0,0 +1,111 @@ +Example Images +============== + +This directory contains example WebP encoded files and PNG source files. + +## Image Credits + +### Photos by Author + +These images are taken by author. +These photos are licensed under the Creative Commons Attribution 3.0. +You can also use these images under the same as [go-libwebp's license](../LICENSE). + +#### cosmos.png + +The cosmos taken in Nokono-shima (Nokono Island), Fukuoka, JAPAN. + +![cosmos.png](cosmos.png) + +WebP file is generated by following command: + +```sh +$ cwebp -q 90 cosmos.png -o cosmos.webp +Saving file 'cosmos.webp' +File: cosmos.png +Dimension: 1024 x 768 +Output: 76954 bytes Y-U-V-All-PSNR 45.87 47.63 48.10 46.44 dB +block count: intra4: 2972 +intra16: 100 (-> 3.26%) +skipped block: 1 (0.03%) +bytes used: header: 249 (0.3%) +mode-partition: 15161 (19.7%) +Residuals bytes |segment 1|segment 2|segment 3|segment 4| total +macroblocks: | 0%| 11%| 33%| 54%| 3072 +quantizer: | 12 | 11 | 9 | 8 | +filter level: | 4 | 2 | 2 | 4 | +``` + +#### butterfly.png + +The butterfly taken in Ishigaki-jima (Ishigaki Island) in Okinawa, JAPAN. + +![butterfly.png](butterfly.png) + +WebP file is generated by following command: + +```sh +$ cwebp -q 90 butterfly.png -o butterfly.webp +Saving file 'butterfly.webp' +File: butterfly.png +Dimension: 1024 x 768 +Output: 79198 bytes Y-U-V-All-PSNR 45.50 48.71 49.90 46.44 dB +block count: intra4: 2775 +intra16: 297 (-> 9.67%) +skipped block: 0 (0.00%) +bytes used: header: 383 (0.5%) +mode-partition: 13227 (16.7%) +Residuals bytes |segment 1|segment 2|segment 3|segment 4| total +macroblocks: | 1%| 7%| 14%| 75%| 3072 +quantizer: | 12 | 12 | 10 | 8 | +filter level: | 4 | 3 | 2 | 6 | +``` + +#### kinkaku.png + +Kinkaku taken in Kyoto, JAPAN. + +![kinkaku.png](kinkaku.png) + +WebP file is generated by following command: + +```sh +$ cwebp -q 90 kinkaku.png -o kinkaku.webp +Saving file 'kinkaku.webp' +File: kinkaku.png +Dimension: 1024 x 768 +Output: 186300 bytes Y-U-V-All-PSNR 43.86 47.25 48.26 44.81 dB +block count: intra4: 2775 +intra16: 297 (-> 9.67%) +skipped block: 243 (7.91%) +bytes used: header: 425 (0.2%) +mode-partition: 16737 (9.0%) +Residuals bytes |segment 1|segment 2|segment 3|segment 4| total +macroblocks: | 2%| 28%| 40%| 28%| 3072 +quantizer: | 12 | 11 | 9 | 6 | +filter level: | 4 | 2 | 2 | 1 | +``` + +### From Google WebP Gallery + +These images picked up from [WebP Gallery](https://developers.google.com/speed/webp/gallery) in Google Developers. + +#### yellow-rose-3.png + +"Free Stock Photo in High Resolution - Yellow Rose 3 - Flowers" by Jon Sullivan + +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. + +
+ +
+ +#### fizyplankton.png + +"baby tux for my user page" by Fizyplankton. + +This image file is in the public domain. + +
+ +
diff --git a/examples/images/butterfly.png b/examples/images/butterfly.png new file mode 100644 index 0000000..d327f48 Binary files /dev/null and b/examples/images/butterfly.png differ diff --git a/examples/images/butterfly.webp b/examples/images/butterfly.webp new file mode 100644 index 0000000..be2ea34 Binary files /dev/null and b/examples/images/butterfly.webp differ diff --git a/examples/images/checkerboard.png b/examples/images/checkerboard.png new file mode 100644 index 0000000..0441308 Binary files /dev/null and b/examples/images/checkerboard.png differ diff --git a/examples/images/cosmos.png b/examples/images/cosmos.png new file mode 100644 index 0000000..b85931f Binary files /dev/null and b/examples/images/cosmos.png differ diff --git a/examples/images/cosmos.webp b/examples/images/cosmos.webp new file mode 100644 index 0000000..e255af4 Binary files /dev/null and b/examples/images/cosmos.webp differ diff --git a/examples/images/fizyplankton.png b/examples/images/fizyplankton.png new file mode 100644 index 0000000..c75b1fc Binary files /dev/null and b/examples/images/fizyplankton.png differ diff --git a/examples/images/fizyplankton.webp b/examples/images/fizyplankton.webp new file mode 100644 index 0000000..81bc7db Binary files /dev/null and b/examples/images/fizyplankton.webp differ diff --git a/examples/images/kinkaku.png b/examples/images/kinkaku.png new file mode 100644 index 0000000..6624107 Binary files /dev/null and b/examples/images/kinkaku.png differ diff --git a/examples/images/kinkaku.webp b/examples/images/kinkaku.webp new file mode 100644 index 0000000..0f54af6 Binary files /dev/null and b/examples/images/kinkaku.webp differ diff --git a/examples/images/yellow-rose-3.png b/examples/images/yellow-rose-3.png new file mode 100644 index 0000000..140c0b9 Binary files /dev/null and b/examples/images/yellow-rose-3.png differ diff --git a/examples/images/yellow-rose-3.webp b/examples/images/yellow-rose-3.webp new file mode 100644 index 0000000..dfd5d18 Binary files /dev/null and b/examples/images/yellow-rose-3.webp differ diff --git a/examples/out/.keep b/examples/out/.keep new file mode 100644 index 0000000..e69de29 diff --git a/examples/util/util.go b/examples/util/util.go new file mode 100644 index 0000000..2dc2398 --- /dev/null +++ b/examples/util/util.go @@ -0,0 +1,80 @@ +// Package util contains utility code for demosntration of go-libwebp. +package util + +import ( + "bufio" + "image" + "image/png" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +// GetExFilePath returns the path of specified example file. +func GetExFilePath(name string) string { + return filepath.Join(os.Getenv("GOPATH"), "src/github.com/harukasan/go-libwebp/examples/images", name) +} + +// GetOutFilePath returns the path of specified out file. +func GetOutFilePath(name string) string { + return filepath.Join(os.Getenv("GOPATH"), "src/github.com/harukasan/go-libwebp/examples/out", name) +} + +// OpenFile opens specified example file +func OpenFile(name string) (io io.Reader) { + io, err := os.Open(GetExFilePath(name)) + if err != nil { + panic(err) + } + return +} + +// ReadFile reads and returns data bytes of specified example file. +func ReadFile(name string) (data []byte) { + data, err := ioutil.ReadFile(GetExFilePath(name)) + if err != nil { + panic(err) + } + return +} + +// CreateFile opens specified example file +func CreateFile(name string) (f *os.File) { + f, err := os.Create(GetOutFilePath(name)) + if err != nil { + panic(err) + } + return +} + +// WritePNG encodes and writes image into PNG file. +func WritePNG(img image.Image, name string) { + f, err := os.Create(GetOutFilePath(name)) + if err != nil { + panic(err) + } + b := bufio.NewWriter(f) + defer func() { + b.Flush() + f.Close() + }() + + if err := png.Encode(b, img); err != nil { + panic(err) + } + return +} + +// ReadPNG reads and decodes png data into image.Image +func ReadPNG(name string) (img image.Image) { + io, err := os.Open(GetExFilePath(name)) + if err != nil { + panic(err) + } + img, err = png.Decode(io) + if err != nil { + panic(err) + } + return +} diff --git a/webp/cover.out b/webp/cover.out new file mode 100644 index 0000000..3bfcf32 --- /dev/null +++ b/webp/cover.out @@ -0,0 +1,80 @@ +mode: count +github.com/harukasan/go-libwebp/webp/decode.go:41.34,43.2 1 1 +github.com/harukasan/go-libwebp/webp/decode.go:46.47,50.2 3 1 +github.com/harukasan/go-libwebp/webp/decode.go:53.65,57.37 3 1 +github.com/harukasan/go-libwebp/webp/decode.go:61.2,69.8 2 1 +github.com/harukasan/go-libwebp/webp/decode.go:57.37,59.13 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:73.83,75.16 2 2 +github.com/harukasan/go-libwebp/webp/decode.go:79.2,83.107 3 2 +github.com/harukasan/go-libwebp/webp/decode.go:87.2,96.42 6 2 +github.com/harukasan/go-libwebp/webp/decode.go:100.2,114.32 14 2 +github.com/harukasan/go-libwebp/webp/decode.go:120.2,120.95 1 2 +github.com/harukasan/go-libwebp/webp/decode.go:124.2,124.8 1 2 +github.com/harukasan/go-libwebp/webp/decode.go:75.16,77.3 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:83.107,85.13 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:96.42,98.3 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:114.32,118.3 3 0 +github.com/harukasan/go-libwebp/webp/decode.go:120.95,122.13 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:128.84,130.16 2 3 +github.com/harukasan/go-libwebp/webp/decode.go:134.2,138.107 3 3 +github.com/harukasan/go-libwebp/webp/decode.go:143.2,157.95 9 3 +github.com/harukasan/go-libwebp/webp/decode.go:161.2,161.8 1 3 +github.com/harukasan/go-libwebp/webp/decode.go:130.16,132.3 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:138.107,140.13 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:157.95,159.13 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:165.55,166.16 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:184.2,184.33 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:167.2,168.25 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:169.2,170.36 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:171.2,172.36 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:173.2,174.38 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:175.2,176.42 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:177.2,178.32 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:179.2,180.33 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:181.2,182.38 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:188.102,191.57 2 5 +github.com/harukasan/go-libwebp/webp/decode.go:196.2,196.29 1 5 +github.com/harukasan/go-libwebp/webp/decode.go:199.2,199.31 1 5 +github.com/harukasan/go-libwebp/webp/decode.go:202.2,202.54 1 5 +github.com/harukasan/go-libwebp/webp/decode.go:209.2,209.56 1 5 +github.com/harukasan/go-libwebp/webp/decode.go:214.2,214.24 1 5 +github.com/harukasan/go-libwebp/webp/decode.go:217.12,219.8 2 5 +github.com/harukasan/go-libwebp/webp/decode.go:191.57,193.3 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:196.29,198.3 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:199.31,201.3 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:202.54,208.3 5 1 +github.com/harukasan/go-libwebp/webp/decode.go:209.56,213.3 3 1 +github.com/harukasan/go-libwebp/webp/decode.go:214.24,216.3 1 0 +github.com/harukasan/go-libwebp/webp/decode.go:223.82,225.29 2 5 +github.com/harukasan/go-libwebp/webp/decode.go:230.2,230.37 1 4 +github.com/harukasan/go-libwebp/webp/decode.go:236.2,238.18 3 3 +github.com/harukasan/go-libwebp/webp/decode.go:225.29,229.3 3 1 +github.com/harukasan/go-libwebp/webp/decode.go:230.37,234.3 3 1 +github.com/harukasan/go-libwebp/webp/encode.go:42.101,46.16 4 11 +github.com/harukasan/go-libwebp/webp/encode.go:49.2,49.10 1 11 +github.com/harukasan/go-libwebp/webp/encode.go:46.16,48.3 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:53.69,55.16 2 1 +github.com/harukasan/go-libwebp/webp/encode.go:59.2,60.49 2 1 +github.com/harukasan/go-libwebp/webp/encode.go:63.12,73.46 8 1 +github.com/harukasan/go-libwebp/webp/encode.go:77.2,77.8 1 1 +github.com/harukasan/go-libwebp/webp/encode.go:55.16,57.3 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:60.49,62.3 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:73.46,75.13 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:81.68,83.16 2 1 +github.com/harukasan/go-libwebp/webp/encode.go:87.2,88.49 2 1 +github.com/harukasan/go-libwebp/webp/encode.go:91.12,101.31 10 1 +github.com/harukasan/go-libwebp/webp/encode.go:106.2,109.46 3 1 +github.com/harukasan/go-libwebp/webp/encode.go:113.2,113.8 1 1 +github.com/harukasan/go-libwebp/webp/encode.go:83.16,85.3 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:88.49,90.3 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:101.31,104.3 2 0 +github.com/harukasan/go-libwebp/webp/encode.go:109.46,111.13 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:117.73,119.96 2 2 +github.com/harukasan/go-libwebp/webp/encode.go:122.12,125.44 3 2 +github.com/harukasan/go-libwebp/webp/encode.go:128.2,128.8 1 2 +github.com/harukasan/go-libwebp/webp/encode.go:119.96,121.3 1 0 +github.com/harukasan/go-libwebp/webp/encode.go:125.44,127.3 1 0 +github.com/harukasan/go-libwebp/webp/yuva_image.go:21.71,25.11 3 2 +github.com/harukasan/go-libwebp/webp/yuva_image.go:55.2,55.8 1 2 +github.com/harukasan/go-libwebp/webp/yuva_image.go:26.2,38.4 2 2 +github.com/harukasan/go-libwebp/webp/yuva_image.go:40.2,52.4 2 0 diff --git a/webp/decode.go b/webp/decode.go new file mode 100644 index 0000000..ef7b227 --- /dev/null +++ b/webp/decode.go @@ -0,0 +1,242 @@ +package webp + +/* +#cgo LDFLAGS: -lwebp +#include +#include + +static VP8StatusCode CheckDecBuffer(const WebPDecBuffer* const buffer); + +*/ +import "C" + +import ( + "errors" + "fmt" + "image" + "unsafe" +) + +// DecoderOptions specifies decoding options +type DecoderOptions struct { + BypassFiltering bool + NoFancyUpsampling bool + Crop image.Rectangle + Scale image.Rectangle + UseThreads bool + DitheringStrength int + Flip bool + AlphaDitheringStrength int +} + +// BitstreamFeatures retrived from data stream. +type BitstreamFeatures struct { + Width int // Image width in pixels + Height int // Image height in pixles + HasAlpha bool // True if data stream contains a alpha channel. + HasAnimation bool // True if data stream is an animation + Format int // Image compression format + NoIncrementalDecoding bool // True if incremental decording is not using +} + +// GetDecoderVersion returns decoder's version number, packed in hexadecimal. +// e.g; v0.4.2 is 0x000402 +func GetDecoderVersion() (v int) { + return int(C.WebPGetDecoderVersion()) +} + +// GetInfo retrives width/height from data bytes. +func GetInfo(data []byte) (width, height int) { + var w, h C.int + C.WebPGetInfo((*C.uint8_t)(&data[0]), (C.size_t)(len(data)), &w, &h) + return int(w), int(h) +} + +// GetFeatures returns features retrived from data stream. +func GetFeatures(data []byte) (f *BitstreamFeatures, err error) { + var cf C.WebPBitstreamFeatures + status := C.WebPGetFeatures((*C.uint8_t)(&data[0]), (C.size_t)(len(data)), &cf) + + if status != C.VP8_STATUS_OK { + return nil, fmt.Errorf("WebPGetFeatures returns unexpected status: %s", statusString(status)) + } + + f = &BitstreamFeatures{ + Width: int(cf.width), // TODO: use Rectangle instaed? + Height: int(cf.height), + HasAlpha: cf.has_alpha > 0, + HasAnimation: cf.has_animation > 0, + Format: int(cf.format), + NoIncrementalDecoding: cf.no_incremental_decoding == 1, + } + return +} + +// DecodeYUVA decodes WebP image into YUV image with alpha channel. +func DecodeYUVA(data []byte, options *DecoderOptions) (img *YUVAImage, err error) { + config, err := initDecoderConfig(options) + if err != nil { + return nil, err + } + + cDataPtr := (*C.uint8_t)(&data[0]) + cDataSize := (C.size_t)(len(data)) + + // Retrive WebP features from data stream + if status := C.WebPGetFeatures(cDataPtr, cDataSize, &config.input); status != C.VP8_STATUS_OK { + return nil, fmt.Errorf("Could not get features from the data stream, return %s", statusString(status)) + } + + outWidth, outHeight := calcOutputSize(config) + buf := (*C.WebPYUVABuffer)(unsafe.Pointer(&config.output.u[0])) + + // Set up output configurations + colorSpace := YUV420 + config.output.colorspace = C.MODE_YUV + if config.input.has_alpha > 0 { + colorSpace = YUV420A + config.output.colorspace = C.MODE_YUVA + } + config.output.is_external_memory = 1 + + // Allocate image and fill into buffer + img = NewYUVAImage(image.Rect(0, 0, outWidth, outHeight), colorSpace) + buf.y = (*C.uint8_t)(&img.Y[0]) + buf.u = (*C.uint8_t)(&img.Cb[0]) + buf.v = (*C.uint8_t)(&img.Cr[0]) + buf.a = nil + buf.y_stride = C.int(img.YStride) + buf.u_stride = C.int(img.CStride) + buf.v_stride = C.int(img.CStride) + buf.a_stride = 0 + buf.y_size = C.size_t(len(img.Y)) + buf.u_size = C.size_t(len(img.Cb)) + buf.v_size = C.size_t(len(img.Cr)) + buf.a_size = 0 + + if config.input.has_alpha > 0 { + buf.a = (*C.uint8_t)(&img.A[0]) + buf.a_stride = C.int(img.AStride) + buf.a_size = C.size_t(len(img.A)) + } + + if status := C.WebPDecode(cDataPtr, cDataSize, config); status != C.VP8_STATUS_OK { + return nil, fmt.Errorf("Could not decode data stream, return %s", statusString(status)) + } + + return +} + +// DecodeRGBA decodes WebP image into RGBA image. +func DecodeRGBA(data []byte, options *DecoderOptions) (img *image.RGBA, err error) { + config, err := initDecoderConfig(options) + if err != nil { + return nil, err + } + + cDataPtr := (*C.uint8_t)(&data[0]) + cDataSize := (C.size_t)(len(data)) + + // Retrive WebP features + if status := C.WebPGetFeatures(cDataPtr, cDataSize, &config.input); status != C.VP8_STATUS_OK { + return nil, fmt.Errorf("Could not get features from the data stream, return %s", statusString(status)) + } + + // Allocate output image + outWidth, outHeight := calcOutputSize(config) + img = image.NewRGBA(image.Rect(0, 0, outWidth, outHeight)) + + // Set up output configurations + config.output.colorspace = C.MODE_RGBA + config.output.is_external_memory = 1 + + // Allocate WebPRGBABuffer and fill in the pointers to output image + buf := (*C.WebPRGBABuffer)(unsafe.Pointer(&config.output.u[0])) + buf.rgba = (*C.uint8_t)(&img.Pix[0]) + buf.stride = C.int(img.Stride) + buf.size = (C.size_t)(len(img.Pix)) + + // Decode + if status := C.WebPDecode(cDataPtr, cDataSize, config); status != C.VP8_STATUS_OK { + return nil, fmt.Errorf("Could not decode data stream, return %s", statusString(status)) + } + + return +} + +// sattusString convert the VP8StatsCode to string. +func statusString(status C.VP8StatusCode) string { + switch status { + case C.VP8_STATUS_OK: + return "VP8_STATUS_OK" + case C.VP8_STATUS_OUT_OF_MEMORY: + return "VP8_STATUS_OUT_OF_MEMORY" + case C.VP8_STATUS_INVALID_PARAM: + return "VP8_STATUS_INVALID_PARAM" + case C.VP8_STATUS_BITSTREAM_ERROR: + return "VP8_STATUS_BITSTREAM_ERROR" + case C.VP8_STATUS_UNSUPPORTED_FEATURE: + return "VP8_STATUS_UNSUPPORTED_FEATURE" + case C.VP8_STATUS_SUSPENDED: + return "VP8_STATUS_SUSPENDED" + case C.VP8_STATUS_USER_ABORT: + return "VP8_STATUS_USER_ABORT" + case C.VP8_STATUS_NOT_ENOUGH_DATA: + return "VP8_STATUS_NOT_ENOUGH_DATA" + } + return "Unexpected Status Code" +} + +// initDecoderConfing initializes a decoder configration and sets up the options. +func initDecoderConfig(options *DecoderOptions) (config *C.WebPDecoderConfig, err error) { + // Initialize decoder config + config = &C.WebPDecoderConfig{} + if C.WebPInitDecoderConfig(config) == 0 { + return nil, errors.New("Could not initialize decoder config") + } + + // Set up decoder options + if options.BypassFiltering { + config.options.bypass_filtering = 1 + } + if options.NoFancyUpsampling { + config.options.no_fancy_upsampling = 1 + } + if options.Crop.Max.X > 0 && options.Crop.Max.Y > 0 { + config.options.use_cropping = 1 + config.options.crop_left = C.int(options.Crop.Min.X) + config.options.crop_top = C.int(options.Crop.Min.Y) + config.options.crop_width = C.int(options.Crop.Dx()) + config.options.crop_height = C.int(options.Crop.Dy()) + } + if options.Scale.Max.X > 0 && options.Scale.Max.Y > 0 { + config.options.use_scaling = 1 + config.options.scaled_width = C.int(options.Scale.Max.X) + config.options.scaled_height = C.int(options.Scale.Max.Y) + } + if options.UseThreads { + config.options.use_threads = 1 + } + config.options.dithering_strength = C.int(options.DitheringStrength) + + return +} + +// calcOutputSize retrives width and height of output image from the decoder configuration. +func calcOutputSize(config *C.WebPDecoderConfig) (width, height int) { + options := config.options + if options.use_scaling > 0 { + width = int(config.options.scaled_width) + height = int(config.options.scaled_height) + return + } + if config.options.use_cropping > 0 { + width = int(config.options.crop_width) + height = int(config.options.crop_height) + return + } + + width = int(config.input.width) + height = int(config.input.height) + return +} diff --git a/webp/encode.go b/webp/encode.go new file mode 100644 index 0000000..4ca42f4 --- /dev/null +++ b/webp/encode.go @@ -0,0 +1,139 @@ +package webp + +/* +#cgo LDFLAGS: -lwebp + +#include +#include + +int writeWebP(uint8_t*, size_t, struct WebPPicture*); + +*/ +import "C" +import ( + "errors" + "fmt" + "image" + "io" + "unsafe" +) + +// Config specifies WebP encoding configuration. +type Config struct { + Preset Preset // Parameters Preset + Lossless bool // True if use lossless encoding + Quality float32 // WebP quality factor, 0-100 + Method int // Quality/Speed trade-off factor, 0=faster / 6=slower-better + TargetSize int // Target size of encoded file in bytes + TargetPSNR float32 // Target PSNR, takes precedence over TargetSize + Segments int // Maximum number of segments to use, 1..4 + SNSStrength int // Strength of spartial noise shaping, 0..100=maximum + FilterStrength int // Strength of filter, 0..100=strength + FilterSharpness int // Sharpness of filter, 0..7=sharpness + FilterType FilterType // Filtering type + Pass int // Number of entropy-analysis passes, 0..100 +} + +type destinationManager struct { + writer io.Writer +} + +//export writeWebP +func writeWebP(data *C.uint8_t, size C.size_t, pic *C.WebPPicture) C.int { + mgr := (*destinationManager)(unsafe.Pointer(pic.custom_ptr)) + bytes := C.GoBytes(unsafe.Pointer(data), C.int(size)) + _, err := mgr.writer.Write(bytes) + if err != nil { + return 0 // TODO: can't pass error message + } + return 1 +} + +// EncodeRGBA encodes and writes image.Image into the writer as WebP. +// Now supports image.RGBA or image.NRGBA. +func EncodeRGBA(w io.Writer, img image.Image, c Config) (err error) { + webpConfig, err := initConfig(c) + if err != nil { + return + } + + var pic C.WebPPicture + if C.WebPPictureInit(&pic) == 0 { + return errors.New("Could not initialize webp picture") + } + pic.use_argb = 1 + + pic.width = C.int(img.Bounds().Dx()) + pic.height = C.int(img.Bounds().Dy()) + + pic.writer = C.WebPWriterFunction(C.writeWebP) + pic.custom_ptr = unsafe.Pointer(&destinationManager{writer: w}) + + switch p := img.(type) { + case *image.RGBA: + C.WebPPictureImportRGBA(&pic, (*C.uint8_t)(&p.Pix[0]), C.int(p.Stride)) + case *image.NRGBA: + C.WebPPictureImportRGBA(&pic, (*C.uint8_t)(&p.Pix[0]), C.int(p.Stride)) + default: + return errors.New("unsupported image type") + } + + defer C.WebPPictureFree(&pic) + + if C.WebPEncode(webpConfig, &pic) == 0 { + return fmt.Errorf("Encoding error: %d", pic.error_code) + } + + return +} + +// EncodeYUVA encodes and writes YUVA Image data into the writer as WebP. +func EncodeYUVA(w io.Writer, img *YUVAImage, c Config) (err error) { + webpConfig, err := initConfig(c) + if err != nil { + return + } + + var pic C.WebPPicture + if C.WebPPictureInit(&pic) == 0 { + return errors.New("Could not initialize webp picture") + } + pic.use_argb = 0 + pic.colorspace = C.WebPEncCSP(img.ColorSpace) + pic.width = C.int(img.Rect.Dx()) + pic.height = C.int(img.Rect.Dy()) + pic.y = (*C.uint8_t)(&img.Y[0]) + pic.u = (*C.uint8_t)(&img.Cb[0]) + pic.v = (*C.uint8_t)(&img.Cr[0]) + pic.y_stride = C.int(img.YStride) + pic.uv_stride = C.int(img.CStride) + + if img.ColorSpace == YUV420A { + pic.a = (*C.uint8_t)(&img.A[0]) + pic.a_stride = C.int(img.AStride) + } + + pic.writer = C.WebPWriterFunction(C.writeWebP) + pic.custom_ptr = unsafe.Pointer(&destinationManager{writer: w}) + + if C.WebPEncode(webpConfig, &pic) == 0 { + return fmt.Errorf("Encoding error: %d", pic.error_code) + } + + return +} + +// initConfig initializes C.WebPConfig with encoding parameters. +func initConfig(c Config) (config *C.WebPConfig, err error) { + config = &C.WebPConfig{} + if C.WebPConfigPreset(config, C.WebPPreset(c.Preset), C.float(c.Quality)) == 0 { + return nil, errors.New("Could not initialize configuration with preset") + } + config.target_size = C.int(c.TargetSize) + config.target_PSNR = C.float(c.TargetPSNR) + + if C.WebPValidateConfig(config) == 0 { + return nil, errors.New("Invalid configuration") + } + return +} diff --git a/webp/webp.go b/webp/webp.go new file mode 100644 index 0000000..78dea82 --- /dev/null +++ b/webp/webp.go @@ -0,0 +1,48 @@ +package webp + +/* +#cgo LDFLAGS: -lwebp + +#include +#include + +*/ +import "C" + +// ColorSpace represents encoding color space in WebP +type ColorSpace int + +const ( + // YUV420 specifies YUV4:2:0 + YUV420 ColorSpace = C.WEBP_YUV420 + // YUV420A specifies YUV4:2:0 with alpha channel + YUV420A ColorSpace = C.WEBP_YUV420A +) + +// Preset corresponds to C.WebPPreset +type Preset int + +const ( + // PresetDefault corresponds to WEBP_PRESET_DEFAULT, for default preset. + PresetDefault Preset = C.WEBP_PRESET_DEFAULT + // PresetPicture corresponds to WEBP_PRESET_PICTURE, for digital picture, like portrait, inner shot + PresetPicture Preset = C.WEBP_PRESET_PICTURE + // PresetPhoto corresponds to WEBP_PRESET_PHOTO, for outdoor photograph, with natural lighting + PresetPhoto Preset = C.WEBP_PRESET_PHOTO + // PresetDrawing corresponds to WEBP_PRESET_DRAWING, for hand or line drawing, with high-contrast details + PresetDrawing Preset = C.WEBP_PRESET_DRAWING + // PresetIcon corresponds to WEBP_PRESET_ICON, for small-sized colorful images + PresetIcon Preset = C.WEBP_PRESET_ICON + // PresetText corresponds to WEBP_PRESET_TEXT, for text-like + PresetText Preset = C.WEBP_PRESET_TEXT +) + +// FilterType corresponds to filter types in compression parameters. +type FilterType int + +const ( + // SimpleFilter (=0, default) + SimpleFilter FilterType = iota + // StrongFilter (=1) + StrongFilter +) diff --git a/webp/webp_test.go b/webp/webp_test.go new file mode 100644 index 0000000..5cc51aa --- /dev/null +++ b/webp/webp_test.go @@ -0,0 +1,236 @@ +package webp_test + +import ( + "bufio" + "image" + "image/png" + "io" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/harukasan/go-libwebp/webp" +) + +// +// Utitlities of input/output example images +// + +// GetExFilePath returns the path of specified example file. +func GetExFilePath(name string) string { + return filepath.Join(os.Getenv("GOPATH"), "src/github.com/harukasan/go-libwebp/examples/images", name) +} + +// OpenExFile opens specified example file +func OpenExFile(name string) (io io.Reader) { + io, err := os.Open(GetExFilePath(name)) + if err != nil { + panic(err) + } + return +} + +// ReadExFile reads and returns data bytes of specified example file. +func ReadExFile(name string) (data []byte) { + data, err := ioutil.ReadFile(GetExFilePath(name)) + if err != nil { + panic(err) + } + return +} + +// CreateExOutFile creates output file into example/out directory. +func CreateExOutFile(name string) (file *os.File) { + // Make output directory + dir := filepath.Join(os.Getenv("GOPATH"), "src/github.com/harukasan/go-libwebp/examples/out") + if err := os.Mkdir(dir, 0755); err != nil && os.IsNotExist(err) { + panic(err) + } + + // Create example output file + file, err := os.Create(filepath.Join(dir, name)) + if err != nil { + panic(err) + } + return +} + +// +// Decode +// + +// Test Get Decoder Version +func TestGetDecoderVersion(t *testing.T) { + v := webp.GetDecoderVersion() + if v < 0 { + t.Errorf("GetDecoderVersion should returns positive version number, got %v\n", v) + } +} + +func TestGetInfo(t *testing.T) { + data := ReadExFile("cosmos.webp") + width, height := webp.GetInfo(data) + + if width != 1024 { + t.Errorf("Expected width: %d, but got %d", 1024, width) + } + if height != 768 { + t.Errorf("Expected height: %d, but got %d", 768, height) + } +} + +func TestGetFeatures(t *testing.T) { + data := ReadExFile("cosmos.webp") + f, err := webp.GetFeatures(data) + if err != nil { + t.Errorf("Got Error: %v", err) + return + } + if got := f.Width; got != 1024 { + t.Errorf("Expected Width: %v, but got %v", 1024, got) + } + if got := f.Height; got != 768 { + t.Errorf("Expected Width: %v, but got %v", 768, got) + } + if got := f.HasAlpha; got != false { + t.Errorf("Expected HasAlpha: %v, but got %v", false, got) + } + if got := f.HasAnimation; got != false { + t.Errorf("Expected HasAlpha: %v, but got %v", false, got) + } + if got := f.Format; got != 1 { + t.Errorf("Expected Format: %v, but got %v", 1, got) + } +} + +func TestDecodeYUV(t *testing.T) { + files := []string{ + "cosmos.webp", + "butterfly.webp", + "kinkaku.webp", + "yellow-rose-3.webp", + } + + for _, file := range files { + data := ReadExFile(file) + options := &webp.DecoderOptions{} + + _, err := webp.DecodeYUVA(data, options) + if err != nil { + t.Errorf("Got Error: %v", err) + return + } + } +} + +func TestDecodeRGBA(t *testing.T) { + files := []string{ + "cosmos.webp", + "butterfly.webp", + "kinkaku.webp", + "yellow-rose-3.webp", + } + + for _, file := range files { + data := ReadExFile(file) + options := &webp.DecoderOptions{} + + _, err := webp.DecodeRGBA(data, options) + if err != nil { + t.Errorf("Got Error: %v", err) + return + } + } +} + +func TestDecodeRGBAWithCropping(t *testing.T) { + data := ReadExFile("cosmos.webp") + crop := image.Rect(100, 100, 300, 200) + + options := &webp.DecoderOptions{ + Crop: crop, + } + + img, err := webp.DecodeRGBA(data, options) + if err != nil { + t.Errorf("Got Error: %v", err) + return + } + if img.Rect.Dx() != crop.Dx() || img.Rect.Dy() != crop.Dy() { + t.Errorf("Decoded image should cropped to %v, but got %v", crop, img.Rect) + } +} + +func TestDecodeRGBAWithScaling(t *testing.T) { + data := ReadExFile("cosmos.webp") + scale := image.Rect(0, 0, 640, 480) + + options := &webp.DecoderOptions{ + Scale: scale, + } + + img, err := webp.DecodeRGBA(data, options) + if err != nil { + t.Errorf("Got Error: %v", err) + return + } + if img.Rect.Dx() != scale.Dx() || img.Rect.Dy() != scale.Dy() { + t.Errorf("Decoded image should scaled to %v, but got %v", scale, img.Rect) + } +} + +// +// Encoding +// + +func TestEncodeRGBA(t *testing.T) { + img, _ := png.Decode(OpenExFile("yellow-rose-3.png")) + + config := webp.Config{ + Preset: webp.PresetDefault, + Quality: 100, + Method: 6, + } + + f := CreateExOutFile("TestEncodeRGBA.webp") + w := bufio.NewWriter(f) + defer func() { + w.Flush() + f.Close() + }() + + if err := webp.EncodeRGBA(w, img, config); err != nil { + t.Errorf("Got Error: %v", err) + return + } +} + +func TestEncodeYUVA(t *testing.T) { + data := ReadExFile("cosmos.webp") + options := &webp.DecoderOptions{} + + img, err := webp.DecodeYUVA(data, options) + if err != nil { + t.Errorf("Got Error: %v in decoding", err) + return + } + + f := CreateExOutFile("TestEncodeYUVA.webp") + w := bufio.NewWriter(f) + defer func() { + w.Flush() + f.Close() + }() + + config := webp.Config{ + Preset: webp.PresetDefault, + Quality: 100, + Method: 6, + } + + if err := webp.EncodeYUVA(w, img, config); err != nil { + t.Errorf("Got Error: %v", err) + return + } +} diff --git a/webp/yuva_image.go b/webp/yuva_image.go new file mode 100644 index 0000000..171665e --- /dev/null +++ b/webp/yuva_image.go @@ -0,0 +1,56 @@ +package webp + +import "image" + +// YUVAImage represents a image of YUV colors with alpha channel image. +// +// We can not insert WebP planer image into image.YCbCr, +// because image.YCbCr is not compatible with ITU-R BT.601, but JFIF/JPEG one. +// +// See: http://en.wikipedia.org/wiki/YCbCr +type YUVAImage struct { + Y, Cb, Cr, A []uint8 + YStride int + CStride int + AStride int + ColorSpace ColorSpace + Rect image.Rectangle +} + +// NewYUVAImage creates and allocates image buffer. +func NewYUVAImage(r image.Rectangle, c ColorSpace) (image *YUVAImage) { + yw, yh := r.Dx(), r.Dx() + cw, ch := ((r.Max.X+1)/2 - r.Min.X/2), ((r.Max.Y+1)/2 - r.Min.Y/2) + + switch c { + case YUV420: + b := make([]byte, yw*yh+2*cw*ch) + image = &YUVAImage{ + Y: b[:yw*yh], + Cb: b[yw*yh+0*cw*ch : yw*yh+1*cw*ch], + Cr: b[yw*yh+1*cw*ch : yw*yh+2*cw*ch], + A: nil, + YStride: yw, + CStride: cw, + AStride: 0, + ColorSpace: c, + Rect: r, + } + + case YUV420A: + b := make([]byte, 2*yw*yh+2*cw*ch) + image = &YUVAImage{ + Y: b[:yw*yh], + Cb: b[yw*yh+0*cw*ch : yw*yh+1*cw*ch], + Cr: b[yw*yh+1*cw*ch : yw*yh+2*cw*ch], + A: b[yw*yh+2*cw*ch:], + YStride: yw, + CStride: cw, + AStride: yw, + ColorSpace: c, + Rect: r, + } + } + + return +}