aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorXe <me@christine.website>2022-09-15 16:22:34 +0000
committerXe <me@christine.website>2022-09-15 16:22:34 +0000
commitc66fbcfc841d90a23131e34226e0ed589c403f32 (patch)
tree7c4f2ef2699deccdda4cb0e2ce0dab979b093c3c /internal
parent4fe84522878290f91a69d8bc0793eacdb6a8af6d (diff)
downloadx-c66fbcfc841d90a23131e34226e0ed589c403f32.tar.xz
x-c66fbcfc841d90a23131e34226e0ed589c403f32.zip
subsume go-avif
Signed-off-by: Xe <me@christine.website>
Diffstat (limited to 'internal')
-rw-r--r--internal/avif/COPYING121
-rw-r--r--internal/avif/README.md117
-rw-r--r--internal/avif/av1.c215
-rw-r--r--internal/avif/av1.h44
-rw-r--r--internal/avif/avif.go201
-rw-r--r--internal/avif/example_test.go42
-rw-r--r--internal/avif/hacker-nest.avifbin0 -> 22522 bytes
-rw-r--r--internal/avif/mp4.go799
8 files changed, 1539 insertions, 0 deletions
diff --git a/internal/avif/COPYING b/internal/avif/COPYING
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/internal/avif/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/internal/avif/README.md b/internal/avif/README.md
new file mode 100644
index 0000000..9c9fef6
--- /dev/null
+++ b/internal/avif/README.md
@@ -0,0 +1,117 @@
+# 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](https://aomediacodec.github.io/av1-avif/))
+encoder for Go using libaom, the [high quality](https://github.com/Kagami/av1-bench)
+AV1 codec.
+
+## Requirements
+
+Make sure libaom is installed. On typical Linux distro just run:
+
+#### Debian (and derivatives):
+```bash
+sudo apt-get install libaom-dev
+```
+
+#### RHEL (and derivatives):
+```bash
+sudo dnf install libaom-devel
+```
+
+## 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"
+)
+
+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 file: %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
+```
+
+Static 64-bit builds for Windows, macOS and Linux are available at
+[releases page](https://github.com/Kagami/go-avif/releases). They include
+latest libaom from git at the moment of build.
+
+## Display
+
+To display resulting AVIF files take a look at software listed
+[here](https://github.com/AOMediaCodec/av1-avif/wiki#demuxers--players). E.g.
+use [avif.js](https://kagami.github.io/avif.js/) web viewer.
+
+## License
+
+go-avif is licensed under [CC0](COPYING).
diff --git a/internal/avif/av1.c b/internal/avif/av1.c
new file mode 100644
index 0000000..24cbeb8
--- /dev/null
+++ b/internal/avif/av1.c
@@ -0,0 +1,215 @@
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <aom/aom_encoder.h>
+#include <aom/aomcx.h>
+#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_FRAME_PARALLEL_DECODING, 0)
+ SET_CODEC_CONTROL(AV1E_SET_TILE_COLUMNS, 1)
+ SET_CODEC_CONTROL(AV1E_SET_TILE_ROWS, 1)
+#ifdef AOM_CTRL_AV1E_SET_ROW_MT
+ SET_CODEC_CONTROL(AV1E_SET_ROW_MT, 1)
+#endif
+
+ 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/internal/avif/av1.h b/internal/avif/av1.h
new file mode 100644
index 0000000..d92e198
--- /dev/null
+++ b/internal/avif/av1.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <stdint.h>
+
+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/internal/avif/avif.go b/internal/avif/avif.go
new file mode 100644
index 0000000..aadcdec
--- /dev/null
+++ b/internal/avif/avif.go
@@ -0,0 +1,201 @@
+// 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
+}
diff --git a/internal/avif/example_test.go b/internal/avif/example_test.go
new file mode 100644
index 0000000..235cbde
--- /dev/null
+++ b/internal/avif/example_test.go
@@ -0,0 +1,42 @@
+package avif_test
+
+import (
+ "image"
+ _ "image/jpeg"
+ "log"
+ "os"
+
+ "within.website/x/internal/avif"
+)
+
+const usageHelp = "Usage: %s src.jpg dst.avif"
+
+func Example() {
+ if len(os.Args) != 3 {
+ log.Fatalf(usageHelp, 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 file: %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/internal/avif/hacker-nest.avif b/internal/avif/hacker-nest.avif
new file mode 100644
index 0000000..b927f01
--- /dev/null
+++ b/internal/avif/hacker-nest.avif
Binary files differ
diff --git a/internal/avif/mp4.go b/internal/avif/mp4.go
new file mode 100644
index 0000000..bfed208
--- /dev/null
+++ b/internal/avif/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{}