From f306d71481de51cc8346f1d796e3aed6d75c0e97 Mon Sep 17 00:00:00 2001 From: Kagami Hiiragi Date: Sun, 17 Mar 2019 15:13:37 +0300 Subject: [PATCH] Initial commit --- .gitignore | 7 + COPYING | 121 +++++++ Makefile | 17 + README.md | 102 ++++++ av1.c | 213 +++++++++++++ av1.h | 44 +++ avif.go | 188 +++++++++++ cmd/avif/main.go | 104 ++++++ example_test.go | 41 +++ gofmt-staged.sh | 18 ++ mp4.go | 799 +++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1654 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 Makefile create mode 100644 README.md create mode 100644 av1.c create mode 100644 av1.h create mode 100644 avif.go create mode 100644 cmd/avif/main.go create mode 100644 example_test.go create mode 100755 gofmt-staged.sh create mode 100644 mp4.go 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 +}