Files

144 lines
3.7 KiB
Go

// Package webpenc encodes WebP pictures in pure Go.
//
// libwebp.go is libwebp compiled to WebAssembly and translated to Go by
// wasm2go, taken unchanged (apart from the package name) from
// github.com/gen2brain/webp; see README.md. Its memory is one Go byte slice
// and every access to it is length-checked, so a fault in libwebp stays
// inside that slice. This package only encodes, and only pixels that Go's own
// decoders produced: it never parses a file someone uploaded. It loads no
// system library and registers no image format.
package webpenc
import (
"errors"
"image"
"io"
)
var (
errMemWrite = errors.New("webp: couldn't hand the picture to the encoder")
errMemRead = errors.New("webp: couldn't read the encoder's output")
errEncode = errors.New("webp: encoding failed")
)
// Options choose between lossy (photographs) and lossless (screenshots and
// drawings). Quality is 1-100 for lossy; for lossless it trades time for size.
type Options struct {
Quality int
Lossless bool
}
// Encode writes m as WebP.
func Encode(w io.Writer, m *image.RGBA, o Options) error {
q := o.Quality
if q <= 0 || q > 100 {
q = 80
}
width, height := m.Rect.Dx(), m.Rect.Dy()
data := m.Pix
if m.Stride != 4*width || len(data) != 4*width*height {
c := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
copy(c.Pix[y*c.Stride:(y+1)*c.Stride], m.Pix[m.PixOffset(m.Rect.Min.X, m.Rect.Min.Y+y):])
}
data = c.Pix
}
// The RGBA here is premultiplied; libwebp wants straight alpha. Only
// pixels that are partly transparent differ.
if !m.Opaque() {
data = unpremultiply(data)
}
mod := newModule()
in := mod.Xmalloc(int32(len(data)))
defer mod.Xfree(in)
if !mod.write(in, data) {
return errMemWrite
}
sizePtr := mod.Xmalloc(8)
defer mod.Xfree(sizePtr)
mod.write(sizePtr, make([]byte, 8)) // size_t is 4 bytes in the module; keep the rest zero
lossless := int32(0)
if o.Lossless {
lossless = 1
}
const method = 4 // libwebp's default balance of speed and size
out := mod.Xencode(in, int32(width), int32(height), sizePtr, 0, int32(q), method, lossless, 0)
defer mod.Xfree(out)
size, ok := mod.readUint64(sizePtr)
if !ok {
return errMemRead
}
if size == 0 {
return errEncode
}
b, ok := mod.read(out, int32(size))
if !ok {
return errMemRead
}
_, err := w.Write(b)
return err
}
func unpremultiply(p []byte) []byte {
out := make([]byte, len(p))
copy(out, p)
for i := 0; i+3 < len(out); i += 4 {
a := uint32(out[i+3])
if a == 0 || a == 255 {
continue
}
for k := 0; k < 3; k++ {
out[i+k] = uint8(min(255, (uint32(out[i+k])*255+a/2)/a))
}
}
return out
}
func (m *module) write(ptr int32, data []byte) bool {
if ptr < 0 || int(ptr)+len(data) > len(m.memory) {
return false
}
copy(m.memory[ptr:], data)
return true
}
func (m *module) read(ptr, size int32) ([]byte, bool) {
if ptr < 0 || size < 0 || int(ptr)+int(size) > len(m.memory) {
return nil, false
}
out := make([]byte, size)
copy(out, m.memory[ptr:ptr+size])
return out, true
}
func (m *module) readUint64(ptr int32) (uint64, bool) {
if ptr < 0 || int(ptr)+8 > len(m.memory) {
return 0, false
}
return load64(m.memory[ptr:]), true
}
func newModule() *module {
mod := newModuleRaw(&wasiHost{})
mod.X_initialize()
return mod
}
// wasiHost answers the module's two system calls; libwebp only writes
// diagnostics, which are dropped.
type wasiHost struct{ mod *module }
func (h *wasiHost) Init(m any) { h.mod = m.(*module) }
func (h *wasiHost) Xfd_close(fd int32) int32 { return 0 }
func (h *wasiHost) Xfd_write(fd, iovs, iovsLen, nwrittenPtr int32) int32 {
mem := h.mod.memory
var written uint32
for i := int32(0); i < iovsLen; i++ {
written += load32(mem[iovs+i*8+4:])
}
store32(mem[nwrittenPtr:], written)
return 0
}