commit f306d71481de51cc8346f1d796e3aed6d75c0e97 Author: Kagami Hiiragi Date: Sun Mar 17 15:13:37 2019 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75be678 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Temporary avif, libaom and MP4Box files +/*.avif +/*.obu +/*.y4m +/*.yuv +/*.xml +/*.png diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7fbaa86 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +# Should contain src/github.com/Kagami/go-avif +export GOPATH = $(PWD)/../../../.. + +all: build +precommit: gofmt-staged + +build: + go get github.com/Kagami/go-avif/... + +gofmt: + go fmt github.com/Kagami/go-avif/... + +gofmt-staged: + ./gofmt-staged.sh + +test: + go test -v diff --git a/README.md b/README.md new file mode 100644 index 0000000..951829e --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# go-avif [![Build Status](https://travis-ci.org/Kagami/go-avif.svg?branch=master)](https://travis-ci.org/Kagami/go-avif) [![GoDoc](https://godoc.org/github.com/Kagami/go-avif?status.svg)](https://godoc.org/github.com/Kagami/go-avif) + +go-avif implements AVIF (AV1 Still Image File Format) encoder for Go using +[libaom](https://aomedia.googlesource.com/aom/), +[the highest quality](https://github.com/Kagami/av1-bench) AV1 codec at the +moment. + +## Requirements + +Make sure libaom is installed. On typical Linux distro just run: + +```bash +sudo apt-get install libaom-dev +``` + +## Usage + +To use go-avif in your Go code: + +```go +import "github.com/Kagami/go-avif" +``` + +To install go-avif in your $GOPATH: + +```bash +go get github.com/Kagami/go-avif +``` + +For further details see [GoDoc documentation](https://godoc.org/github.com/Kagami/go-avif). + +## Example + +```go +package main + +import ( + "image" + _ "image/jpeg" + "log" + "os" + + "github.com/Kagami/go-avif" +) + +// This example shows the basic usage of the package. +func main() { + if len(os.Args) != 3 { + log.Fatalf("Usage: %s src.jpg dst.avif", os.Args[0]) + } + + srcPath := os.Args[1] + src, err := os.Open(srcPath) + if err != nil { + log.Fatalf("Can't open sorce file: %v", err) + } + + dstPath := os.Args[2] + dst, err := os.Create(dstPath) + if err != nil { + log.Fatalf("Can't create destination file: %v", err) + } + + img, _, err := image.Decode(src) + if err != nil { + log.Fatalf("Can't decode source image: %v", err) + } + + err = avif.Encode(dst, img, nil) + if err != nil { + log.Fatalf("Can't encode source image: %v", err) + } + + log.Printf("Encoded AVIF at %s", dstPath) +} +``` + +## CLI + +go-avif comes with handy CLI utility `avif`. It supports encoding of JPEG and +PNG files to AVIF: + +```bash +# Compile and put avif binary to $GOPATH/bin +go get github.com/Kagami/go-avif/... + +# Encode JPEG to AVIF with default settings +avif -e cat.jpg -o kitty.avif + +# Encode PNG with slowest speed +avif -e dog.png -o doggy.avif --best -q 15 + +# Lossless encoding +avif -e pig.png -o piggy.avif --lossless + +# Show help +avif -h +``` + +## License + +go-avif is licensed under [CC0](COPYING). diff --git a/av1.c b/av1.c new file mode 100644 index 0000000..ad6a6b3 --- /dev/null +++ b/av1.c @@ -0,0 +1,213 @@ +#include +#include +#include +#include +#include +#include "av1.h" + +#define SET_CODEC_CONTROL(ctrl, val) \ + {if (aom_codec_control(ctx, ctrl, val)) return AVIF_ERROR_CODEC_INIT;} + +typedef struct { + aom_img_fmt_t fmt; + int dst_c_dec_h; + int dst_c_dec_v; + int bps; + int bytes_per_sample; +} avif_format; + +static avif_format convert_subsampling(const avif_subsampling subsampling) { + avif_format fmt = { 0 }; + switch (subsampling) { + case AVIF_SUBSAMPLING_I420: + fmt.fmt = AOM_IMG_FMT_I420; + fmt.dst_c_dec_h = 2; + fmt.dst_c_dec_v = 2; + fmt.bps = 12; + fmt.bytes_per_sample = 1; + break; + default: + assert(0); + } + return fmt; +} + +// We don't use aom_img_wrap() because it forces padding for odd picture +// sizes (c) libaom/common/y4minput.c +static void convert_frame(const avif_frame *frame, aom_image_t *aom_frame) { + memset(aom_frame, 0, sizeof(*aom_frame)); + avif_format fmt = convert_subsampling(frame->subsampling); + aom_frame->fmt = fmt.fmt; + aom_frame->w = aom_frame->d_w = frame->width; + aom_frame->h = aom_frame->d_h = frame->height; + aom_frame->x_chroma_shift = fmt.dst_c_dec_h >> 1; + aom_frame->y_chroma_shift = fmt.dst_c_dec_v >> 1; + aom_frame->bps = fmt.bps; + int pic_sz = frame->width * frame->height * fmt.bytes_per_sample; + int c_w = (frame->width + fmt.dst_c_dec_h - 1) / fmt.dst_c_dec_h; + c_w *= fmt.bytes_per_sample; + int c_h = (frame->height + fmt.dst_c_dec_v - 1) / fmt.dst_c_dec_v; + int c_sz = c_w * c_h; + aom_frame->stride[AOM_PLANE_Y] = frame->width * fmt.bytes_per_sample; + aom_frame->stride[AOM_PLANE_U] = aom_frame->stride[AOM_PLANE_V] = c_w; + aom_frame->planes[AOM_PLANE_Y] = frame->data; + aom_frame->planes[AOM_PLANE_U] = frame->data + pic_sz; + aom_frame->planes[AOM_PLANE_V] = frame->data + pic_sz + c_sz; +} + +static int get_frame_stats(aom_codec_ctx_t *ctx, + const aom_image_t *frame, + aom_fixed_buf_t *stats) { + if (aom_codec_encode(ctx, frame, 1/*pts*/, 1/*duration*/, 0/*flags*/)) + return AVIF_ERROR_FRAME_ENCODE; + + const aom_codec_cx_pkt_t *pkt = NULL; + aom_codec_iter_t iter = NULL; + int got_pkts = 0; + while ((pkt = aom_codec_get_cx_data(ctx, &iter)) != NULL) { + got_pkts = 1; + if (pkt->kind == AOM_CODEC_STATS_PKT) { + const uint8_t *const pkt_buf = pkt->data.twopass_stats.buf; + const size_t pkt_size = pkt->data.twopass_stats.sz; + stats->buf = realloc(stats->buf, stats->sz + pkt_size); + memcpy((uint8_t *)stats->buf + stats->sz, pkt_buf, pkt_size); + stats->sz += pkt_size; + } + } + return got_pkts; +} + +static int encode_frame(aom_codec_ctx_t *ctx, + const aom_image_t *frame, + avif_buffer *obu) { + if (aom_codec_encode(ctx, frame, 1/*pts*/, 1/*duration*/, 0/*flags*/)) + return AVIF_ERROR_FRAME_ENCODE; + + const aom_codec_cx_pkt_t *pkt = NULL; + aom_codec_iter_t iter = NULL; + int got_pkts = 0; + while ((pkt = aom_codec_get_cx_data(ctx, &iter)) != NULL) { + got_pkts = 1; + if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) { + const uint8_t *const pkt_buf = pkt->data.frame.buf; + const size_t pkt_size = pkt->data.frame.sz; + obu->buf = realloc(obu->buf, obu->sz + pkt_size); + memcpy((uint8_t *)obu->buf + obu->sz, pkt_buf, pkt_size); + obu->sz += pkt_size; + } + } + return got_pkts; +} + +static avif_error init_codec(aom_codec_iface_t *iface, + aom_codec_ctx_t *ctx, + const aom_codec_enc_cfg_t *aom_cfg, + const avif_config *cfg) { + if (aom_codec_enc_init(ctx, iface, aom_cfg, 0)) + return AVIF_ERROR_CODEC_INIT; + + SET_CODEC_CONTROL(AOME_SET_CPUUSED, cfg->speed) + SET_CODEC_CONTROL(AOME_SET_CQ_LEVEL, cfg->quality) + if (cfg->quality == 0) { + SET_CODEC_CONTROL(AV1E_SET_LOSSLESS, 1) + } + SET_CODEC_CONTROL(AV1E_SET_TILE_COLUMNS, 1) + SET_CODEC_CONTROL(AV1E_SET_TILE_ROWS, 1) + SET_CODEC_CONTROL(AV1E_SET_ROW_MT, 1) + SET_CODEC_CONTROL(AV1E_SET_FRAME_PARALLEL_DECODING, 0) + + return AVIF_OK; +} + +static avif_error do_pass1(aom_codec_ctx_t *ctx, + const aom_image_t *frame, + aom_fixed_buf_t *stats) { + avif_error res = AVIF_OK; + + // Calculate frame statistics. + if ((res = get_frame_stats(ctx, frame, stats)) < 0) + goto fail; + + // Flush encoder. + while ((res = get_frame_stats(ctx, NULL, stats)) > 0) + continue; + +fail: + return res < 0 ? res : AVIF_OK; +} + +static avif_error do_pass2(aom_codec_ctx_t *ctx, + const aom_image_t *frame, + avif_buffer *obu) { + avif_error res = AVIF_OK; + + // Encode frame. + if ((res = encode_frame(ctx, frame, obu)) < 0) + goto fail; + + // Flush encoder. + while ((res = encode_frame(ctx, NULL, obu)) > 0) + continue; + +fail: + return res < 0 ? res : AVIF_OK; +} + +avif_error avif_encode_frame(const avif_config *cfg, + const avif_frame *frame, + avif_buffer *obu) { + // Validation. + assert(cfg->threads >= 1); + assert(cfg->speed >= AVIF_MIN_SPEED && cfg->speed <= AVIF_MAX_SPEED); + assert(cfg->quality >= AVIF_MIN_QUALITY && cfg->quality <= AVIF_MAX_QUALITY); + assert(frame->width && frame->height); + + // Prepare image. + aom_image_t aom_frame; + convert_frame(frame, &aom_frame); + + // Setup codec. + avif_error res = AVIF_OK; + aom_codec_ctx_t codec; + aom_fixed_buf_t stats = { NULL, 0 }; + aom_codec_iface_t *iface = aom_codec_av1_cx(); + aom_codec_enc_cfg_t aom_cfg; + if (aom_codec_enc_config_default(iface, &aom_cfg, 0)) { + res = AVIF_ERROR_CODEC_INIT; + goto fail; + } + aom_cfg.g_limit = 1; + aom_cfg.g_w = frame->width; + aom_cfg.g_h = frame->height; + aom_cfg.g_timebase.num = 1; + aom_cfg.g_timebase.den = 24; + aom_cfg.rc_end_usage = AOM_Q; + aom_cfg.g_threads = cfg->threads; + + // Pass 1. + aom_cfg.g_pass = AOM_RC_FIRST_PASS; + if ((res = init_codec(iface, &codec, &aom_cfg, cfg))) + goto fail; + if ((res = do_pass1(&codec, &aom_frame, &stats))) + goto fail; + if (aom_codec_destroy(&codec)) { + res = AVIF_ERROR_CODEC_DESTROY; + goto fail; + } + + // Pass 2. + aom_cfg.g_pass = AOM_RC_LAST_PASS; + aom_cfg.rc_twopass_stats_in = stats; + if ((res = init_codec(iface, &codec, &aom_cfg, cfg))) + goto fail; + if ((res = do_pass2(&codec, &aom_frame, obu))) + goto fail; + if (aom_codec_destroy(&codec)) { + res = AVIF_ERROR_CODEC_DESTROY; + goto fail; + } + +fail: + free(stats.buf); + return res; +} diff --git a/av1.h b/av1.h new file mode 100644 index 0000000..d92e198 --- /dev/null +++ b/av1.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +enum { + AVIF_MIN_SPEED = 0, + AVIF_MAX_SPEED = 8, + AVIF_MIN_QUALITY = 0, + AVIF_MAX_QUALITY = 63, +}; + +typedef enum { + AVIF_OK = 0, + AVIF_ERROR_GENERAL = -1000, + AVIF_ERROR_CODEC_INIT, + AVIF_ERROR_CODEC_DESTROY, + AVIF_ERROR_FRAME_ENCODE, +} avif_error; + +typedef enum { + AVIF_SUBSAMPLING_I420, +} avif_subsampling; + +typedef struct { + int threads; + int speed; + int quality; +} avif_config; + +typedef struct { + uint16_t width; + uint16_t height; + avif_subsampling subsampling; + uint8_t *data; +} avif_frame; + +typedef struct { + void *buf; + size_t sz; +} avif_buffer; + +avif_error avif_encode_frame(const avif_config *cfg, + const avif_frame *frame, + avif_buffer *obu); diff --git a/avif.go b/avif.go new file mode 100644 index 0000000..ec09dc9 --- /dev/null +++ b/avif.go @@ -0,0 +1,188 @@ +// Package avif implements a AVIF image encoder. +// +// AVIF is defined in https://aomediacodec.github.io/av1-avif/ +package avif + +// #cgo CFLAGS: -Wall -O2 -DNDEBUG +// #cgo LDFLAGS: -laom +// #include +// #include "av1.h" +import "C" +import ( + "fmt" + "image" + "io" + "runtime" +) + +type Options struct { + Threads int + Speed int + Quality int + SubsampleRatio *image.YCbCrSubsampleRatio +} + +const ( + MinSpeed = 0 + MaxSpeed = 8 + MinQuality = 0 + MaxQuality = 63 +) + +var ( + DefaultOptions = Options{ + Threads: 0, + Speed: 4, + Quality: 25, + SubsampleRatio: nil, + } +) + +type OptionsError string + +func (e OptionsError) Error() string { + return fmt.Sprintf("options error: %s", string(e)) +} + +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()) +} + +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.SubsampleRatio == nil { + s := image.YCbCrSubsampleRatio420 + o.SubsampleRatio = &s + // if yuvImg, ok := m.(*image.YCbCr); ok { + // o.SubsampleRatio = &yuvImg.SubsampleRatio + // } + } + if o.Threads < 1 { + // 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 +} diff --git a/cmd/avif/main.go b/cmd/avif/main.go new file mode 100644 index 0000000..a1702b0 --- /dev/null +++ b/cmd/avif/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "image" + _ "image/jpeg" + _ "image/png" + "io" + "os" + + "github.com/Kagami/go-avif" + "github.com/docopt/docopt-go" +) + +const VERSION = "0.0.0" +const USAGE = ` +Usage: avif [options] -e src_filename -o dst_filename + +AVIF encoder + +Options: + -h, --help Give this help + -V, --version Display version number + -e , --encode= Source filename + -o , --output= Destination filename + -q , --quality= Compression level (0..63), [default: 25] + -s , --speed= Compression speed (0..8), [default: 4] + --lossless Lossless compression (alias for -q 0) + --best Slowest compression method (alias for -s 0) + --fast Fastest compression method (alias for -s 8) +` + +type config struct { + Encode string + Output string + Quality int + Speed int + Lossless bool + Best bool + Fast bool +} + +func checkErr(err error) { + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func check(cond bool, errStr string) { + if !cond { + fmt.Println(errStr) + os.Exit(1) + } +} + +func main() { + var conf config + opts, err := docopt.ParseArgs(USAGE, nil, VERSION) + checkErr(err) + err = opts.Bind(&conf) + checkErr(err) + check(conf.Quality >= avif.MinQuality && conf.Quality <= avif.MaxQuality, "bad quality (0..63)") + check(conf.Speed >= avif.MinSpeed && conf.Speed <= avif.MaxSpeed, "bad speed (0..8)") + check(!conf.Best || !conf.Fast, "can't use both --best and --fast") + if conf.Lossless { + conf.Quality = 0 + } + if conf.Best { + conf.Speed = 0 + } else if conf.Fast { + conf.Speed = 8 + } + avifOpts := avif.Options{ + Speed: conf.Speed, + Quality: conf.Quality, + } + + var src io.Reader + var dst io.Writer + if conf.Encode == "-" { + src = os.Stdin + } else { + file, err := os.Open(conf.Encode) + checkErr(err) + defer file.Close() + src = file + } + if conf.Output == "-" { + dst = os.Stdout + } else { + file, err := os.Create(conf.Output) + checkErr(err) + defer file.Close() + dst = file + } + + // TODO(Kagami): Accept y4m. + img, _, err := image.Decode(src) + checkErr(err) + + err = avif.Encode(dst, img, &avifOpts) + checkErr(err) +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..8f97738 --- /dev/null +++ b/example_test.go @@ -0,0 +1,41 @@ +package avif_test + +import ( + "image" + _ "image/jpeg" + "log" + "os" + + "github.com/Kagami/go-avif" +) + +// This example shows the basic usage of the package. +func Example_basic() { + if len(os.Args) != 3 { + log.Fatalf("Usage: %s src.jpg dst.avif", os.Args[0]) + } + + srcPath := os.Args[1] + src, err := os.Open(srcPath) + if err != nil { + log.Fatalf("Can't open sorce file: %v", err) + } + + dstPath := os.Args[2] + dst, err := os.Create(dstPath) + if err != nil { + log.Fatalf("Can't create destination file: %v", err) + } + + img, _, err := image.Decode(src) + if err != nil { + log.Fatalf("Can't decode source image: %v", err) + } + + err = avif.Encode(dst, img, nil) + if err != nil { + log.Fatalf("Can't encode source image: %v", err) + } + + log.Printf("Encoded AVIF at %s", dstPath) +} diff --git a/gofmt-staged.sh b/gofmt-staged.sh new file mode 100755 index 0000000..96981c6 --- /dev/null +++ b/gofmt-staged.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# https://github.com/edsrzf/gofmt-git-hook +IFS=' +' +exitcode=0 +for file in `git diff --cached --name-only --diff-filter=ACM | grep '\.go$'` +do + output=`gofmt -w "$file"` + if test -n "$output" + then + # any output is a syntax error + echo >&2 "$output" + exitcode=1 + fi + git add "$file" +done +exit $exitcode diff --git a/mp4.go b/mp4.go new file mode 100644 index 0000000..d36e299 --- /dev/null +++ b/mp4.go @@ -0,0 +1,799 @@ +package avif + +import ( + "encoding/binary" + "image" + "io" +) + +type fourCC [4]byte + +var ( + boxTypeFTYP = fourCC{'f', 't', 'y', 'p'} + boxTypeMDAT = fourCC{'m', 'd', 'a', 't'} + boxTypeMETA = fourCC{'m', 'e', 't', 'a'} + boxTypeHDLR = fourCC{'h', 'd', 'l', 'r'} + boxTypePITM = fourCC{'p', 'i', 't', 'm'} + boxTypeILOC = fourCC{'i', 'l', 'o', 'c'} + boxTypeIINF = fourCC{'i', 'i', 'n', 'f'} + boxTypeINFE = fourCC{'i', 'n', 'f', 'e'} + boxTypeIPRP = fourCC{'i', 'p', 'r', 'p'} + boxTypeIPCO = fourCC{'i', 'p', 'c', 'o'} + boxTypeISPE = fourCC{'i', 's', 'p', 'e'} + boxTypePASP = fourCC{'p', 'a', 's', 'p'} + boxTypeAV1C = fourCC{'a', 'v', '1', 'C'} + boxTypePIXI = fourCC{'p', 'i', 'x', 'i'} + boxTypeIPMA = fourCC{'i', 'p', 'm', 'a'} + + itemTypeMIF1 = fourCC{'m', 'i', 'f', '1'} + itemTypeAVIF = fourCC{'a', 'v', 'i', 'f'} + itemTypeMIAF = fourCC{'m', 'i', 'a', 'f'} + itemTypePICT = fourCC{'p', 'i', 'c', 't'} + itemTypeMIME = fourCC{'m', 'i', 'm', 'e'} + itemTypeURI = fourCC{'u', 'r', 'i', ' '} + itemTypeAV01 = fourCC{'a', 'v', '0', '1'} +) + +func ulen(s string) uint32 { + return uint32(len(s)) +} + +func bflag(b bool, pos uint8) uint8 { + if b { + return 1 << (pos - 1) + } else { + return 0 + } +} + +func writeAll(w io.Writer, writers ...io.WriterTo) (err error) { + for _, wt := range writers { + _, err = wt.WriteTo(w) + if err != nil { + return + } + } + return +} + +func writeBE(w io.Writer, chunks ...interface{}) (err error) { + for _, v := range chunks { + err = binary.Write(w, binary.BigEndian, v) + if err != nil { + return + } + } + return +} + +//---------------------------------------------------------------------- + +type box struct { + size uint32 + typ fourCC +} + +func (b *box) Size() uint32 { + return 8 +} + +func (b *box) WriteTo(w io.Writer) (n int64, err error) { + err = writeBE(w, b.size, b.typ) + return +} + +//---------------------------------------------------------------------- + +type fullBox struct { + box + version uint8 + flags uint32 +} + +func (b *fullBox) Size() uint32 { + return 12 +} + +func (b *fullBox) WriteTo(w io.Writer) (n int64, err error) { + if _, err = b.box.WriteTo(w); err != nil { + return + } + versionAndFlags := (uint32(b.version) << 24) | (b.flags & 0xffffff) + err = writeBE(w, versionAndFlags) + return +} + +//---------------------------------------------------------------------- + +// File Type Box +type boxFTYP struct { + box + majorBrand fourCC + minorVersion uint32 + compatibleBrands []fourCC +} + +func (b *boxFTYP) Size() uint32 { + return b.box.Size() + + 4 /*major_brand*/ + 4 /*minor_version*/ + uint32(len(b.compatibleBrands))*4 +} + +func (b *boxFTYP) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeFTYP + if _, err = b.box.WriteTo(w); err != nil { + return + } + err = writeBE(w, b.majorBrand, b.minorVersion, b.compatibleBrands) + return +} + +//---------------------------------------------------------------------- + +// Media Data Box +type boxMDAT struct { + box + data []byte +} + +func (b *boxMDAT) Size() uint32 { + return b.box.Size() + uint32(len(b.data)) +} + +func (b *boxMDAT) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeMDAT + if _, err = b.box.WriteTo(w); err != nil { + return + } + _, err = w.Write(b.data) + return +} + +//---------------------------------------------------------------------- + +// The Meta box +type boxMETA struct { + fullBox + theHandler boxHDLR + primaryResource boxPITM + itemLocations boxILOC + itemInfos boxIINF + itemProps boxIPRP +} + +func (b *boxMETA) Size() uint32 { + return b.fullBox.Size() + b.theHandler.Size() + b.primaryResource.Size() + + b.itemLocations.Size() + b.itemInfos.Size() + b.itemProps.Size() +} + +func (b *boxMETA) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeMETA + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + err = writeAll(w, &b.theHandler, &b.primaryResource, &b.itemLocations, + &b.itemInfos, &b.itemProps) + return +} + +//---------------------------------------------------------------------- + +// Handler Reference Box +type boxHDLR struct { + fullBox + preDefined uint32 + handlerType fourCC + reserved [3]uint32 + name string +} + +func (b *boxHDLR) Size() uint32 { + return b.fullBox.Size() + + 4 /*pre_defined*/ + 4 /*handler_type*/ + 12 /*reserved*/ + + ulen(b.name) + 1 /*\0*/ +} + +func (b *boxHDLR) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeHDLR + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + err = writeBE(w, b.preDefined, b.handlerType, b.reserved, []byte(b.name), []byte{0}) + return +} + +//---------------------------------------------------------------------- + +// Primary Item Box +type boxPITM struct { + fullBox + itemID uint16 +} + +func (b *boxPITM) Size() uint32 { + return b.fullBox.Size() + 2 /*item_ID*/ +} + +func (b *boxPITM) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypePITM + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + err = writeBE(w, b.itemID) + return +} + +//---------------------------------------------------------------------- + +// The Item Location Box +type boxILOC struct { + fullBox + offsetSize uint8 // 4 bits + lengthSize uint8 // 4 bits + baseOffsetSize uint8 // 4 bits + reserved uint8 // 4 bits + itemCount uint16 + items []boxILOCItem +} + +func (b *boxILOC) Size() uint32 { + size := b.fullBox.Size() + 1 /*offset_size + length_size*/ + + 1 /*base_offset_size + reserved*/ + 2 /*item_count*/ + for _, i := range b.items { + size += 2 /*item_ID*/ + 2 /*data_reference_index*/ + uint32(b.baseOffsetSize) + + 2 /*extent_count*/ + uint32(len(i.extents))*uint32(b.offsetSize+b.lengthSize) + } + return size +} + +func (b *boxILOC) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeILOC + b.itemCount = uint16(len(b.items)) + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + offsetSizeAndLengthSize := (b.offsetSize << 4) | (b.lengthSize & 0xf) + baseOffsetSizeAndReserved := (b.baseOffsetSize << 4) | (b.reserved & 0xf) + err = writeBE(w, offsetSizeAndLengthSize, baseOffsetSizeAndReserved, b.itemCount) + if err != nil { + return + } + for _, i := range b.items { + err = i.write(w, b.baseOffsetSize, b.offsetSize, b.lengthSize) + if err != nil { + return + } + } + return +} + +type boxILOCItem struct { + itemID uint16 + dataReferenceIndex uint16 + baseOffset uint64 // 0, 32 or 64 bits + extentCount uint16 + extents []boxILOCItemExtent +} + +func (i *boxILOCItem) write(w io.Writer, baseOffsetSize, offsetSize, lengthSize uint8) (err error) { + i.extentCount = uint16(len(i.extents)) + var baseOffset interface{} + baseOffset = []byte{} + if baseOffsetSize == 4 { + baseOffset = uint32(i.baseOffset) + } else if baseOffsetSize == 8 { + baseOffset = i.baseOffset + } + err = writeBE(w, i.itemID, i.dataReferenceIndex, baseOffset, i.extentCount) + if err != nil { + return + } + for _, e := range i.extents { + if err = e.write(w, offsetSize, lengthSize); err != nil { + return + } + } + return +} + +type boxILOCItemExtent struct { + extentOffset uint64 // 0, 32 or 64 bits + extentLength uint64 // 0, 32 or 64 bits +} + +func (e *boxILOCItemExtent) write(w io.Writer, offsetSize, lengthSize uint8) (err error) { + var extentOffset interface{} + extentOffset = []byte{} + if offsetSize == 4 { + extentOffset = uint32(e.extentOffset) + } else if offsetSize == 8 { + extentOffset = e.extentOffset + } + var extentLength interface{} + extentLength = []byte{} + if lengthSize == 4 { + extentLength = uint32(e.extentLength) + } else if lengthSize == 8 { + extentLength = e.extentLength + } + err = writeBE(w, extentOffset, extentLength) + return +} + +//---------------------------------------------------------------------- + +// Item Information Box +type boxIINF struct { + fullBox + entryCount uint16 + itemInfos []boxINFEv2 +} + +func (b *boxIINF) Size() uint32 { + size := b.fullBox.Size() + 2 /*entry_count*/ + for _, ie := range b.itemInfos { + size += ie.Size() + } + return size +} + +func (b *boxIINF) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeIINF + b.entryCount = uint16(len(b.itemInfos)) + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + if err = writeBE(w, b.entryCount); err != nil { + return + } + for _, ie := range b.itemInfos { + if _, err = ie.WriteTo(w); err != nil { + return + } + } + return +} + +//---------------------------------------------------------------------- + +// Item Info Entry Box +type boxINFEv2 struct { + fullBox + itemID uint16 + itemProtectionIndex uint16 + itemType fourCC + itemName string + contentType string + contentEncoding string + itemURIType string +} + +func (b *boxINFEv2) Size() uint32 { + size := b.fullBox.Size() + 2 /*item_ID*/ + 2 /*item_protection_index*/ + + 4 /*item_type*/ + ulen(b.itemName) + 1 /*\0*/ + if b.itemType == itemTypeMIME { + size += ulen(b.contentType) + 1 /*\0*/ + ulen(b.contentEncoding) + 1 /*\0*/ + } else if b.itemType == itemTypeURI { + size += ulen(b.itemURIType) + } + return size +} + +func (b *boxINFEv2) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeINFE + b.version = 2 + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + err = writeBE(w, b.itemID, b.itemProtectionIndex, b.itemType, + []byte(b.itemName), []byte{0}) + if err != nil { + return + } + if b.itemType == itemTypeMIME { + // XXX(Kagami): Skip content_encoding if it's empty? + err = writeBE(w, []byte(b.contentType), []byte{0}, []byte(b.contentEncoding), []byte{0}) + } else if b.itemType == itemTypeURI { + // XXX(Kagami): Shouldn't be null-terminated per spec? + err = writeBE(w, []byte(b.itemURIType)) + } + return +} + +//---------------------------------------------------------------------- + +// Item Properties Box +type boxIPRP struct { + box + propertyContainer boxIPCO + association boxIPMA +} + +func (b *boxIPRP) Size() uint32 { + return b.box.Size() + b.propertyContainer.Size() + b.association.Size() +} + +func (b *boxIPRP) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeIPRP + if _, err = b.box.WriteTo(w); err != nil { + return + } + err = writeAll(w, &b.propertyContainer, &b.association) + return +} + +//---------------------------------------------------------------------- + +// Item Property Container Box +type boxIPCO struct { + box + properties []boxIPCOProperty +} + +func (b *boxIPCO) Size() uint32 { + size := b.box.Size() + for _, p := range b.properties { + size += p.Size() + } + return size +} + +func (b *boxIPCO) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeIPCO + if _, err = b.box.WriteTo(w); err != nil { + return + } + for _, p := range b.properties { + if _, err = p.WriteTo(w); err != nil { + return + } + } + return +} + +type boxIPCOProperty interface { + io.WriterTo + Size() uint32 +} + +//---------------------------------------------------------------------- + +// Image spatial extents +type boxISPE struct { + fullBox + imageWidth uint32 + imageHeight uint32 +} + +func (b *boxISPE) Size() uint32 { + return b.fullBox.Size() + 4 /*image_width*/ + 4 /*image_height*/ +} + +func (b *boxISPE) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeISPE + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + err = writeBE(w, b.imageWidth, b.imageHeight) + return +} + +//---------------------------------------------------------------------- + +// Pixel aspect ratio +type boxPASP struct { + box + hSpacing uint32 + vSpacing uint32 +} + +func (b *boxPASP) Size() uint32 { + return b.box.Size() + 4 /*hSpacing*/ + 4 /*vSpacing*/ +} + +func (b *boxPASP) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypePASP + if _, err = b.box.WriteTo(w); err != nil { + return + } + err = writeBE(w, b.hSpacing, b.vSpacing) + return +} + +//---------------------------------------------------------------------- + +// Pixel aspect ratio +type boxAV1C struct { + box + av1Config boxAV1CConfig +} + +func (b *boxAV1C) Size() uint32 { + return b.box.Size() + 1 /*marker + version*/ + 1 /*seq_profile + seq_level_idx_0*/ + + // seq_tier_0 + high_bitdepth + twelve_bit + monochrome + + // chroma_subsampling_x + chroma_subsampling_y + chroma_sample_position + 1 + + // reserved + initial_presentation_delay_present + initial_presentation_delay_minus_one/reserved + 1 + + uint32(len(b.av1Config.configOBUs)) +} + +func (b *boxAV1C) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeAV1C + if _, err = b.box.WriteTo(w); err != nil { + return + } + err = b.av1Config.write(w) + return +} + +type boxAV1CConfig struct { + marker bool + version uint8 // 7 bits + //--- + seqProfile uint8 // 4 bits + seqLevelIdx0 uint8 // 4 bits + //--- + seqTier0 bool + highBitdepth bool + twelveBit bool + monochrome bool + chromaSubsamplingX bool + chromaSubsamplingY bool + chromaSamplePosition uint8 // 2 bits + //--- + reserved uint8 // 3 bits + initialPresentationDelayPresent bool + initialPresentationDelayMinusOne uint8 // 4 bits + reserved2 uint8 // 4 bits + //--- + configOBUs []byte +} + +func (c *boxAV1CConfig) write(w io.Writer) (err error) { + c.marker = true + c.version = 1 + c.reserved = 0 + c.reserved2 = 0 + markerAndVersion := bflag(c.marker, 8) | (c.version & 0x7f) + seqProfileAndSeqLevelIdx0 := (c.seqProfile << 5) | (c.seqLevelIdx0 & 0x1f) + codecParams := bflag(c.seqTier0, 8) | + bflag(c.highBitdepth, 7) | + bflag(c.twelveBit, 6) | + bflag(c.monochrome, 5) | + bflag(c.chromaSubsamplingX, 4) | + bflag(c.chromaSubsamplingY, 3) | + (c.chromaSamplePosition & 3) + presentationParams := (c.reserved << 5) | bflag(c.initialPresentationDelayPresent, 4) + if c.initialPresentationDelayPresent { + presentationParams |= c.initialPresentationDelayMinusOne & 0xf + } else { + presentationParams |= c.reserved2 & 0xf + } + err = writeBE(w, markerAndVersion, seqProfileAndSeqLevelIdx0, codecParams, + presentationParams) + if err != nil { + return + } + _, err = w.Write(c.configOBUs) + return +} + +//---------------------------------------------------------------------- + +// Pixel information +type boxPIXI struct { + fullBox + numChannels uint8 + bitsPerChannel []uint8 +} + +func (b *boxPIXI) Size() uint32 { + return b.fullBox.Size() + 1 /*num_channels*/ + uint32(len(b.bitsPerChannel)) +} + +func (b *boxPIXI) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypePIXI + b.numChannels = uint8(len(b.bitsPerChannel)) + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + if err = writeBE(w, b.numChannels); err != nil { + return + } + for _, bpc := range b.bitsPerChannel { + if err = writeBE(w, bpc); err != nil { + return + } + } + return +} + +//---------------------------------------------------------------------- + +// Item Property Association +type boxIPMA struct { + fullBox + entryCount uint32 + entries []boxIPMAAssociation +} + +func (b *boxIPMA) Size() uint32 { + propSize := 1 + if b.flags&1 == 1 { + propSize = 2 + } + size := b.fullBox.Size() + 4 /*entry_count*/ + for _, a := range b.entries { + size += 2 /*item_ID*/ + 1 /*association_count*/ + + uint32(len(a.props))*uint32(propSize) + } + return size +} + +func (b *boxIPMA) WriteTo(w io.Writer) (n int64, err error) { + b.size = b.Size() + b.typ = boxTypeIPMA + b.entryCount = uint32(len(b.entries)) + if _, err = b.fullBox.WriteTo(w); err != nil { + return + } + if err = writeBE(w, b.entryCount); err != nil { + return + } + for _, a := range b.entries { + if err = a.write(w, b.flags); err != nil { + return + } + } + return +} + +type boxIPMAAssociation struct { + itemID uint16 + associationCount uint8 + props []boxIPMAAssociationProperty +} + +func (a *boxIPMAAssociation) write(w io.Writer, flags uint32) (err error) { + // TODO(Kagami): Make sure slice length isn't overflowed? + a.associationCount = uint8(len(a.props)) + if err = writeBE(w, a.itemID, a.associationCount); err != nil { + return + } + for _, p := range a.props { + if err = p.write(w, flags); err != nil { + return + } + } + return +} + +type boxIPMAAssociationProperty struct { + essential bool + propertyIndex uint16 // 7 or 15 bits +} + +func (p *boxIPMAAssociationProperty) write(w io.Writer, flags uint32) (err error) { + essential := 0 + if p.essential { + essential = 1 + } + if flags&1 == 1 { + v := (p.propertyIndex & 0x7fff) | uint16(essential<<15) + err = writeBE(w, v) + } else { + v := uint8(p.propertyIndex&0x7f) | uint8(essential<<7) + err = writeBE(w, v) + } + return +} + +//---------------------------------------------------------------------- + +func getSubsamplingXY(subsampling image.YCbCrSubsampleRatio) (x bool, y bool) { + switch subsampling { + case image.YCbCrSubsampleRatio420: + return true, true + case image.YCbCrSubsampleRatio422: + return true, false + case image.YCbCrSubsampleRatio444: + return false, false + } + return +} + +func muxFrame(w io.Writer, m image.Image, subsampling image.YCbCrSubsampleRatio, obuData []byte) (err error) { + // TODO(Kagami): Parse params from Sequence Header OBU instead? + rec := m.Bounds() + width := uint32(rec.Max.X - rec.Min.X) + height := uint32(rec.Max.Y - rec.Min.Y) + sx, sy := getSubsamplingXY(subsampling) + + fileData := boxMDAT{data: obuData} + fileType := boxFTYP{ + majorBrand: itemTypeMIF1, + compatibleBrands: []fourCC{itemTypeMIF1, itemTypeAVIF, itemTypeMIAF}, + } + metadata := boxMETA{ + theHandler: boxHDLR{ + handlerType: itemTypePICT, + name: "go-avif v0", + }, + primaryResource: boxPITM{itemID: 1}, + itemLocations: boxILOC{ + // NOTE(Kagami): We predefine location item even while we don't + // know corrent offsets yet in order to fix them in place later. + // It's needed because meta box goes before mdat box therefore + // size of the metadata can't change. We only use baseOffset and + // extentLength so occupy 32-bit storage space for them. They're + // unlikely to overflow (>4GB image is not practical). + lengthSize: 4, + baseOffsetSize: 4, + items: []boxILOCItem{ + boxILOCItem{ + itemID: 1, + extents: []boxILOCItemExtent{{}}, + }, + }, + }, + itemInfos: boxIINF{ + itemInfos: []boxINFEv2{ + boxINFEv2{ + itemID: 1, + itemType: itemTypeAV01, + itemName: "Image", + }, + }, + }, + itemProps: boxIPRP{ + propertyContainer: boxIPCO{ + properties: []boxIPCOProperty{ + &boxISPE{imageWidth: width, imageHeight: height}, + &boxPASP{hSpacing: 1, vSpacing: 1}, + &boxAV1C{ + // Only 8-bit at the moment. + av1Config: boxAV1CConfig{ + chromaSubsamplingX: sx, + chromaSubsamplingY: sy, + }, + }, + &boxPIXI{bitsPerChannel: []uint8{8, 8, 8}}, + }, + }, + association: boxIPMA{ + entries: []boxIPMAAssociation{ + boxIPMAAssociation{ + itemID: 1, + props: []boxIPMAAssociationProperty{ + {false, 1}, // non-essential width/height + {false, 2}, // non-essential aspect ratio + {true, 3}, // essential AV1 config + {true, 4}, // essential bitdepth + }, + }, + }, + }, + }, + } + // Can fix iloc offsets now. + locItem := &metadata.itemLocations.items[0] + locItem.baseOffset = uint64(fileType.Size() + metadata.Size() + fileData.box.Size()) + locItem.extents[0].extentLength = uint64(len(fileData.data)) + + err = writeAll(w, &fileType, &metadata, &fileData) + return +}