avif/avif.go

202 lines
5.4 KiB
Go

// Package avif implements a AVIF image encoder.
//
// The AVIF specification is at https://aomediacodec.github.io/av1-avif/.
package avif
// #cgo CFLAGS: -Wall -O2 -DNDEBUG
// #cgo LDFLAGS: -laom
// #include <stdlib.h>
// #include "av1.h"
import "C"
import (
"fmt"
"image"
"io"
"runtime"
)
// Encoder constants.
const (
MinThreads = 1
MaxThreads = 64
MinSpeed = 0
MaxSpeed = 8
MinQuality = 0
MaxQuality = 63
)
// Options are the encoding parameters. Threads ranges from MinThreads
// to MaxThreads, 0 means use all available cores. Speed ranges from
// MinSpeed to MaxSpeed. Quality ranges from MinQuality to MaxQuality,
// lower is better, 0 means lossless encoding. SubsampleRatio specifies
// subsampling of the encoded image, nil means 4:2:0.
type Options struct {
Threads int
Speed int
Quality int
SubsampleRatio *image.YCbCrSubsampleRatio
}
// DefaultOptions defines default encoder config.
var DefaultOptions = Options{
Threads: 0,
Speed: 4,
Quality: 25,
SubsampleRatio: nil,
}
// An OptionsError reports that the passed options are not valid.
type OptionsError string
func (e OptionsError) Error() string {
return fmt.Sprintf("options error: %s", string(e))
}
// An EncoderError reports that the encoder error has occured.
type EncoderError int
func (e EncoderError) ToString() string {
switch e {
case C.AVIF_ERROR_GENERAL:
return "general error"
case C.AVIF_ERROR_CODEC_INIT:
return "codec init error"
case C.AVIF_ERROR_CODEC_DESTROY:
return "codec destroy error"
case C.AVIF_ERROR_FRAME_ENCODE:
return "frame encode error"
default:
return "unknown error"
}
}
func (e EncoderError) Error() string {
return fmt.Sprintf("encoder error: %s", e.ToString())
}
// A MuxerError reports that the muxer error has occured.
type MuxerError string
func (e MuxerError) Error() string {
return fmt.Sprintf("muxer error: %s", string(e))
}
// RGB to BT.709 YCbCr limited range.
// https://web.archive.org/web/20180421030430/http://www.equasys.de/colorconversion.html
// TODO(Kagami): Use fixed point, don't calc chroma values for skipped pixels.
func rgb2yuv(r16, g16, b16 uint32) (uint8, uint8, uint8) {
r, g, b := float32(r16)/256, float32(g16)/256, float32(b16)/256
y := 0.183*r + 0.614*g + 0.062*b + 16
cb := -0.101*r - 0.339*g + 0.439*b + 128
cr := 0.439*r - 0.399*g - 0.040*b + 128
return uint8(y), uint8(cb), uint8(cr)
}
// Encode writes the Image m to w in AVIF format with the given options.
// Default parameters are used if a nil *Options is passed.
//
// NOTE: Image pixels are converted to RGBA first using standard Go
// library. This is no-op for PNG images and does the right thing for
// JPEG since they are normally stored as BT.601 full range with some
// chroma subsampling. Then pixels are converted to BT.709 limited range
// with specified chroma subsampling.
//
// Alpha channel and monochrome are not supported at the moment. Only
// 4:2:0 8-bit images are supported at the moment.
func Encode(w io.Writer, m image.Image, o *Options) error {
// TODO(Kagami): More subsamplings, 10/12 bitdepth, monochrome, alpha.
// TODO(Kagami): Allow to pass BT.709 YCbCr without extra conversions.
if o == nil {
o2 := DefaultOptions
o = &o2
} else {
o2 := *o
o = &o2
}
if o.Threads == 0 {
o.Threads = runtime.NumCPU()
if o.Threads > MaxThreads {
o.Threads = MaxThreads
}
}
if o.SubsampleRatio == nil {
s := image.YCbCrSubsampleRatio420
o.SubsampleRatio = &s
// if yuvImg, ok := m.(*image.YCbCr); ok {
// o.SubsampleRatio = &yuvImg.SubsampleRatio
// }
}
if o.Threads < MinThreads || o.Threads > MaxThreads {
return OptionsError("bad threads number")
}
if o.Speed < MinSpeed || o.Speed > MaxSpeed {
return OptionsError("bad speed value")
}
if o.Quality < MinQuality || o.Quality > MaxQuality {
return OptionsError("bad quality value")
}
if *o.SubsampleRatio != image.YCbCrSubsampleRatio420 {
return OptionsError("unsupported subsampling")
}
if m.Bounds().Empty() {
return OptionsError("empty image")
}
rec := m.Bounds()
width := rec.Max.X - rec.Min.X
height := rec.Max.Y - rec.Min.Y
ySize := width * height
uSize := ((width + 1) / 2) * ((height + 1) / 2)
dataSize := ySize + uSize*2
// Can't pass normal slice inside a struct, see
// https://github.com/golang/go/issues/14210
dataPtr := C.malloc(C.size_t(dataSize))
defer C.free(dataPtr)
data := (*[1 << 30]byte)(dataPtr)[:dataSize:dataSize]
yPos := 0
uPos := ySize
for j := rec.Min.Y; j < rec.Max.Y; j++ {
for i := rec.Min.X; i < rec.Max.X; i++ {
r16, g16, b16, _ := m.At(i, j).RGBA()
y, u, v := rgb2yuv(r16, g16, b16)
data[yPos] = y
yPos++
// TODO(Kagami): Resample chroma planes with some better filter.
if (i-rec.Min.X)&1 == 0 && (j-rec.Min.Y)&1 == 0 {
data[uPos] = u
data[uPos+uSize] = v
uPos++
}
}
}
cfg := C.avif_config{
threads: C.int(o.Threads),
speed: C.int(o.Speed),
quality: C.int(o.Quality),
}
frame := C.avif_frame{
width: C.uint16_t(width),
height: C.uint16_t(height),
subsampling: C.AVIF_SUBSAMPLING_I420,
data: (*C.uint8_t)(dataPtr),
}
obu := C.avif_buffer{
buf: nil,
sz: 0,
}
defer C.free(obu.buf)
// TODO(Kagami): Error description.
if eErr := C.avif_encode_frame(&cfg, &frame, &obu); eErr != 0 {
return EncoderError(eErr)
}
obuData := (*[1 << 30]byte)(obu.buf)[:obu.sz:obu.sz]
if mErr := muxFrame(w, m, *o.SubsampleRatio, obuData); mErr != nil {
return MuxerError(mErr.Error())
}
return nil
}