Initial commit (basic functionality, not feature complete)
diff --git a/src/write.c b/src/write.c
new file mode 100644
index 0000000..a3f0f6f
--- /dev/null
+++ b/src/write.c
@@ -0,0 +1,287 @@
+// Copyright 2019 Joe Drago. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/internal.h"
+
+#include <string.h>
+
+#include "aom/aom_encoder.h"
+#include "aom/aomcx.h"
+
+static avifBool encodeOBU(avifImage * image, avifBool alphaOnly, avifRawData * outputOBU, int quality);
+static avifBool avifImageIsOpaque(avifImage * image);
+
+avifResult avifImageWrite(avifImage * image, avifRawData * output, int quality)
+{
+    avifResult result = AVIF_RESULT_UNKNOWN_ERROR;
+    avifRawData colorOBU = AVIF_RAW_DATA_EMPTY;
+    avifRawData alphaOBU = AVIF_RAW_DATA_EMPTY;
+
+    avifStream s;
+    avifStreamStart(&s, output);
+
+    // -----------------------------------------------------------------------
+    // Reformat pixels, if need be
+
+    avifPixelFormat dstPixelFormat = image->pixelFormat;
+    if (image->pixelFormat == AVIF_PIXEL_FORMAT_RGBA) {
+        // AV1 doesn't support RGB, reformat
+        dstPixelFormat = AVIF_PIXEL_FORMAT_YUV444;
+    }
+
+#if 0 // TODO: implement choice in depth
+    int dstDepth = AVIF_CLAMP(image->depth, 8, 12);
+    if ((dstDepth == 9) || (dstDepth == 11)) {
+        ++dstDepth;
+    }
+#else
+    int dstDepth = 12;
+#endif
+
+    avifImage * pixelImage = image;
+    avifImage * reformattedImage = NULL;
+    if ((image->pixelFormat != dstPixelFormat) || (image->depth != dstDepth)) {
+        reformattedImage = avifImageCreate();
+        avifResult reformatResult = avifImageReformatPixels(image, reformattedImage, dstPixelFormat, dstDepth);
+        if (reformatResult != AVIF_RESULT_OK) {
+            result = reformatResult;
+            goto writeCleanup;
+        }
+        pixelImage = reformattedImage;
+    }
+
+    // -----------------------------------------------------------------------
+    // Encode AV1 OBUs
+
+    if (!encodeOBU(pixelImage, AVIF_FALSE, &colorOBU, quality)) {
+        result = AVIF_RESULT_ENCODE_COLOR_FAILED;
+        goto writeCleanup;
+    }
+
+    // Skip alpha creation on opaque images
+    avifBool hasAlpha = AVIF_FALSE;
+    if (!avifImageIsOpaque(pixelImage)) {
+        if (!encodeOBU(pixelImage, AVIF_TRUE, &alphaOBU, quality)) {
+            result = AVIF_RESULT_ENCODE_ALPHA_FAILED;
+            goto writeCleanup;
+        }
+        hasAlpha = AVIF_TRUE;
+    }
+
+    // -----------------------------------------------------------------------
+    // Write ftyp
+
+    avifBoxMarker ftyp = avifStreamWriteBox(&s, "ftyp", -1, 0);
+    avifStreamWrite(&s, "mif1", 4); // unsigned int(32) major_brand;
+    avifStreamWriteU32(&s, 0);      // unsigned int(32) minor_version;
+    avifStreamWrite(&s, "mif1", 4); // unsigned int(32) compatible_brands[];
+    avifStreamWrite(&s, "avif", 4); // ... compatible_brands[]
+    avifStreamWrite(&s, "miaf", 4); // ... compatible_brands[]
+    avifStreamFinishBox(&s, ftyp);
+
+    // -----------------------------------------------------------------------
+    // Write mdat
+
+    avifBoxMarker mdat = avifStreamWriteBox(&s, "mdat", -1, 0);
+    uint32_t colorOBUOffset = (uint32_t)s.offset;
+    avifStreamWrite(&s, colorOBU.data, colorOBU.size);
+    uint32_t alphaOBUOffset = (uint32_t)s.offset;
+    avifStreamWrite(&s, alphaOBU.data, alphaOBU.size);
+    avifStreamFinishBox(&s, mdat);
+
+    // -----------------------------------------------------------------------
+    // Start meta
+
+    avifBoxMarker meta = avifStreamWriteBox(&s, "meta", 0, 0);
+
+    // -----------------------------------------------------------------------
+    // Write hdlr
+
+    avifBoxMarker hdlr = avifStreamWriteBox(&s, "hdlr", 0, 0);
+    avifStreamWriteU32(&s, 0);         // unsigned int(32) pre_defined = 0;
+    avifStreamWrite(&s, "pict", 4);    // unsigned int(32) handler_type;
+    avifStreamWriteZeros(&s, 12);      // const unsigned int(32)[3] reserved = 0;
+    avifStreamWrite(&s, "libavif", 8); // string name; (writing null terminator)
+    avifStreamFinishBox(&s, hdlr);
+
+    // -----------------------------------------------------------------------
+    // Write pitm
+
+    avifStreamWriteBox(&s, "pitm", 0, sizeof(uint16_t));
+    avifStreamWriteU16(&s, 1); //  unsigned int(16) item_ID;
+
+    // -----------------------------------------------------------------------
+    // Write iloc
+
+    avifBoxMarker iloc = avifStreamWriteBox(&s, "iloc", 0, 0);
+
+    // iloc header
+    uint8_t offsetSizeAndLengthSize = (4 << 4) + (4 << 0); // unsigned int(4) offset_size; unsigned int(4) length_size;
+    avifStreamWrite(&s, &offsetSizeAndLengthSize, 1);      //
+    avifStreamWriteZeros(&s, 1);                           // unsigned int(4) base_offset_size; unsigned int(4) reserved;
+    avifStreamWriteU16(&s, hasAlpha ? 2 : 1);              // unsigned int(16) item_count;
+
+    // Item ID #1 (Color OBU)
+    avifStreamWriteU16(&s, 1);                       // unsigned int(16) item_ID;
+    avifStreamWriteU16(&s, 0);                       // unsigned int(16) data_reference_index;
+    avifStreamWriteU16(&s, 1);                       // unsigned int(16) extent_count;
+    avifStreamWriteU32(&s, colorOBUOffset);          // unsigned int(offset_size*8) extent_offset;
+    avifStreamWriteU32(&s, (uint32_t)colorOBU.size); // unsigned int(length_size*8) extent_length;
+
+    if (hasAlpha) {
+        avifStreamWriteU16(&s, 2);                       // unsigned int(16) item_ID;
+        avifStreamWriteU16(&s, 0);                       // unsigned int(16) data_reference_index;
+        avifStreamWriteU16(&s, 1);                       // unsigned int(16) extent_count;
+        avifStreamWriteU32(&s, alphaOBUOffset);          // unsigned int(offset_size*8) extent_offset;
+        avifStreamWriteU32(&s, (uint32_t)alphaOBU.size); // unsigned int(length_size*8) extent_length;
+    }
+
+    avifStreamFinishBox(&s, iloc);
+
+    // -----------------------------------------------------------------------
+    // Write iinf
+
+    avifBoxMarker iinf = avifStreamWriteBox(&s, "iinf", 0, 0);
+    avifStreamWriteU16(&s, hasAlpha ? 2 : 1); //  unsigned int(16) entry_count;
+
+    avifBoxMarker infe0 = avifStreamWriteBox(&s, "infe", 2, 0);
+    avifStreamWriteU16(&s, 1);       // unsigned int(16) item_ID;
+    avifStreamWriteU16(&s, 0);       // unsigned int(16) item_protection_index;
+    avifStreamWrite(&s, "av01", 4);  // unsigned int(32) item_type;
+    avifStreamWrite(&s, "Color", 6); // string item_name; (writing null terminator)
+    avifStreamFinishBox(&s, infe0);
+    if (hasAlpha) {
+        avifBoxMarker infe1 = avifStreamWriteBox(&s, "infe", 2, 0);
+        avifStreamWriteU16(&s, 2);       // unsigned int(16) item_ID;
+        avifStreamWriteU16(&s, 0);       // unsigned int(16) item_protection_index;
+        avifStreamWrite(&s, "av01", 4);  // unsigned int(32) item_type;
+        avifStreamWrite(&s, "Alpha", 6); // string item_name; (writing null terminator)
+        avifStreamFinishBox(&s, infe1);
+    }
+    avifStreamFinishBox(&s, iinf);
+
+    // -----------------------------------------------------------------------
+    // Write iprp->ipco->ispe
+
+    avifBoxMarker iprp = avifStreamWriteBox(&s, "iprp", -1, 0);
+    avifBoxMarker ipco = avifStreamWriteBox(&s, "ipco", -1, 0);
+    avifBoxMarker ispe = avifStreamWriteBox(&s, "ispe", 0, 0);
+    avifStreamWriteU32(&s, image->width);  // unsigned int(32) image_width;
+    avifStreamWriteU32(&s, image->height); // unsigned int(32) image_height;
+    avifStreamFinishBox(&s, ispe);
+    avifStreamFinishBox(&s, ipco);
+    avifStreamFinishBox(&s, iprp);
+
+    // -----------------------------------------------------------------------
+    // Finish up stream
+
+    avifStreamFinishBox(&s, meta);
+    avifStreamFinishWrite(&s);
+    result = AVIF_RESULT_OK;
+
+    // -----------------------------------------------------------------------
+    // Cleanup
+
+writeCleanup:
+    if (reformattedImage) {
+        avifImageDestroy(reformattedImage);
+    }
+    avifRawDataFree(&colorOBU);
+    avifRawDataFree(&alphaOBU);
+    return result;
+}
+
+static avifBool encodeOBU(avifImage * image, avifBool alphaOnly, avifRawData * outputOBU, int quality)
+{
+    avifBool success = AVIF_FALSE;
+    aom_codec_iface_t * encoder_interface = aom_codec_av1_cx();
+    aom_codec_ctx_t encoder;
+
+    struct aom_codec_enc_cfg cfg;
+    aom_codec_enc_config_default(encoder_interface, &cfg, 0);
+
+    // Profile 2.  8-bit and 10-bit 4:2:2
+    //            12-bit  4:0:0, 4:2:2 and 4:4:4
+    cfg.g_profile = 2;
+    cfg.g_bit_depth = AOM_BITS_12;
+    cfg.g_input_bit_depth = 12;
+    cfg.g_w = image->width;
+    cfg.g_h = image->height;
+    // cfg.g_threads = ...;
+
+    avifBool lossless = (quality == 0) || (quality == 100) || alphaOnly; // alpha is always lossless
+    cfg.rc_min_quantizer = 0;
+    if (lossless) {
+        cfg.rc_max_quantizer = 0;
+    } else {
+        cfg.rc_max_quantizer = quality;
+    }
+
+    aom_codec_enc_init(&encoder, encoder_interface, &cfg, AOM_CODEC_USE_HIGHBITDEPTH);
+    aom_codec_control(&encoder, AV1E_SET_COLOR_RANGE, AOM_CR_FULL_RANGE);
+    if (lossless) {
+        aom_codec_control(&encoder, AV1E_SET_LOSSLESS, 1);
+    }
+
+    aom_image_t * aomImage = aom_img_alloc(NULL, AOM_IMG_FMT_I44416, image->width, image->height, 16);
+    aomImage->range = AOM_CR_FULL_RANGE; // always use full range
+    if (alphaOnly) {
+    }
+
+    if (alphaOnly) {
+        aomImage->monochrome = 1;
+        for (int j = 0; j < image->height; ++j) {
+            for (int i = 0; i < image->width; ++i) {
+                for (int plane = 0; plane < 3; ++plane) {
+                    uint16_t * planeChannel = (uint16_t *)&aomImage->planes[plane][(j * aomImage->stride[plane]) + (2 * i)];
+                    if (plane == 0) {
+                        *planeChannel = image->planes[3][i + (j * image->strides[plane])];
+                    } else {
+                        *planeChannel = 0;
+                    }
+                }
+            }
+        }
+    } else {
+        for (int j = 0; j < image->height; ++j) {
+            for (int i = 0; i < image->width; ++i) {
+                for (int plane = 0; plane < 3; ++plane) {
+                    uint16_t * planeChannel = (uint16_t *)&aomImage->planes[plane][(j * aomImage->stride[plane]) + (2 * i)];
+                    *planeChannel = image->planes[plane][i + (j * image->strides[plane])];
+                }
+            }
+        }
+    }
+
+    aom_codec_encode(&encoder, aomImage, 0, 1, 0);
+    aom_codec_encode(&encoder, NULL, 0, 1, 0); // flush
+
+    aom_codec_iter_t iter = NULL;
+    for (;;) {
+        const aom_codec_cx_pkt_t * pkt = aom_codec_get_cx_data(&encoder, &iter);
+        if (pkt == NULL)
+            break;
+        if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) {
+            avifRawDataSet(outputOBU, pkt->data.frame.buf, pkt->data.frame.sz);
+            success = AVIF_TRUE;
+            break;
+        }
+    }
+
+    aom_img_free(aomImage);
+    aom_codec_destroy(&encoder);
+    return success;
+}
+
+static avifBool avifImageIsOpaque(avifImage * image)
+{
+    int maxChannel = (1 << image->depth) - 1;
+    for (int j = 0; j < image->height; ++j) {
+        for (int i = 0; i < image->width; ++i) {
+            if (image->planes[3][i + (j * image->strides[3])] != maxChannel) {
+                return AVIF_FALSE;
+            }
+        }
+    }
+    return AVIF_TRUE;
+}