diff options
| author | Xe <me@christine.website> | 2022-09-15 16:22:34 +0000 |
|---|---|---|
| committer | Xe <me@christine.website> | 2022-09-15 16:22:34 +0000 |
| commit | c66fbcfc841d90a23131e34226e0ed589c403f32 (patch) | |
| tree | 7c4f2ef2699deccdda4cb0e2ce0dab979b093c3c /internal | |
| parent | 4fe84522878290f91a69d8bc0793eacdb6a8af6d (diff) | |
| download | x-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/COPYING | 121 | ||||
| -rw-r--r-- | internal/avif/README.md | 117 | ||||
| -rw-r--r-- | internal/avif/av1.c | 215 | ||||
| -rw-r--r-- | internal/avif/av1.h | 44 | ||||
| -rw-r--r-- | internal/avif/avif.go | 201 | ||||
| -rw-r--r-- | internal/avif/example_test.go | 42 | ||||
| -rw-r--r-- | internal/avif/hacker-nest.avif | bin | 0 -> 22522 bytes | |||
| -rw-r--r-- | internal/avif/mp4.go | 799 |
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 [](https://travis-ci.org/Kagami/go-avif) [](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 Binary files differnew file mode 100644 index 0000000..b927f01 --- /dev/null +++ b/internal/avif/hacker-nest.avif 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{} |
