Initial commit
This commit is contained in:
commit
f306d71481
|
@ -0,0 +1,7 @@
|
|||
# Temporary avif, libaom and MP4Box files
|
||||
/*.avif
|
||||
/*.obu
|
||||
/*.y4m
|
||||
/*.yuv
|
||||
/*.xml
|
||||
/*.png
|
|
@ -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.
|
|
@ -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
|
|
@ -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).
|
|
@ -0,0 +1,213 @@
|
|||
#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_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;
|
||||
}
|
|
@ -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);
|
|
@ -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 <stdlib.h>
|
||||
// #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
|
||||
}
|
|
@ -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 <src>, --encode=<src> Source filename
|
||||
-o <dst>, --output=<dst> Destination filename
|
||||
-q <qp>, --quality=<qp> Compression level (0..63), [default: 25]
|
||||
-s <spd>, --speed=<spd> 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue