diff --git a/.github/workflows/ci-unix-static.yml b/.github/workflows/ci-unix-static.yml index 46670ebcbb..7c7a376ac1 100644 --- a/.github/workflows/ci-unix-static.yml +++ b/.github/workflows/ci-unix-static.yml @@ -97,6 +97,7 @@ jobs: -DAVIF_BUILD_EXAMPLES=ON -DAVIF_BUILD_APPS=ON -DAVIF_BUILD_TESTS=ON -DAVIF_ENABLE_GTEST=ON -DAVIF_LOCAL_GTEST=ON -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON + -DAVIF_ENABLE_EXPERIMENTAL_AVIR=ON - name: Build libavif (ninja) working-directory: ./build run: ninja diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 9c372490c5..137c99881c 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -111,6 +111,7 @@ jobs: -DAVIF_BUILD_EXAMPLES=ON -DAVIF_BUILD_APPS=ON -DAVIF_BUILD_TESTS=ON -DAVIF_ENABLE_GTEST=ON -DAVIF_LOCAL_GTEST=ON -DAVIF_ENABLE_EXPERIMENTAL_YCGCO_R=ON + -DAVIF_ENABLE_EXPERIMENTAL_AVIR=ON - name: Build libavif (ninja) working-directory: ./build run: ninja diff --git a/CMakeLists.txt b/CMakeLists.txt index aa89c87281..5f76d1d6a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ option(BUILD_SHARED_LIBS "Build shared avif library" ON) option(AVIF_ENABLE_WERROR "Treat all compiler warnings as errors" ON) option(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R "Enable experimental YCgCo-R matrix code" OFF) +option(AVIF_ENABLE_EXPERIMENTAL_AVIR "Enable experimental reduced header" OFF) option(AVIF_CODEC_AOM "Use the AOM codec for encoding/decoding (see AVIF_CODEC_AOM_DECODE/AVIF_CODEC_AOM_ENCODE)" OFF) option(AVIF_CODEC_DAV1D "Use the dav1d codec for decoding" OFF) @@ -262,6 +263,10 @@ if(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) endif() +if(AVIF_ENABLE_EXPERIMENTAL_AVIR) + add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_AVIR) +endif() + set(AVIF_SRCS src/alpha.c src/avif.c diff --git a/apps/avifenc.c b/apps/avifenc.c index ec9fca91b5..b35c1e6788 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -97,6 +97,9 @@ static void syntaxLong(void) printf(" -j,--jobs J : Number of jobs (worker threads, default: 1. Use \"all\" to use all available cores)\n"); printf(" --no-overwrite : Never overwrite existing output file\n"); printf(" -o,--output FILENAME : Instead of using the last filename given as output, use this filename\n"); +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + printf(" --avir : Minimize header if possible\n"); +#endif printf(" -l,--lossless : Set all defaults to encode losslessly, and emit warnings when settings/input don't allow for it\n"); printf(" -d,--depth D : Output depth [8,10,12]. (JPEG/PNG only; For y4m or stdin, depth is retained)\n"); printf(" -y,--yuv FORMAT : Output format [default=auto, 444, 422, 420, 400]. Ignored for y4m or stdin (y4m format is retained)\n"); @@ -669,6 +672,7 @@ typedef struct avifBool autoTiling; avifBool progressive; int speed; + avifEncoderHeaderStrategy headerStrategy; int paspCount; uint32_t paspValues[8]; // only the first two are used @@ -888,6 +892,7 @@ static avifBool avifEncodeImagesFixedQuality(const avifSettings * settings, encoder->autoTiling = settings->autoTiling; encoder->codecChoice = settings->codecChoice; encoder->speed = settings->speed; + encoder->headerStrategy = settings->headerStrategy; encoder->timescale = settings->outputTiming.timescale; encoder->keyframeInterval = settings->keyframeInterval; encoder->repetitionCount = settings->repetitionCount; @@ -1107,6 +1112,7 @@ int main(int argc, char * argv[]) settings.autoTiling = AVIF_FALSE; settings.progressive = AVIF_FALSE; settings.speed = 6; + settings.headerStrategy = AVIF_ENCODER_FULL_HEADER; settings.repetitionCount = AVIF_REPETITION_COUNT_INFINITE; settings.keyframeInterval = 0; settings.ignoreExif = AVIF_FALSE; @@ -1177,6 +1183,10 @@ int main(int argc, char * argv[]) } else if (!strcmp(arg, "-o") || !strcmp(arg, "--output")) { NEXTARG(); outputFilename = arg; +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + } else if (!strcmp(arg, "--avir")) { + settings.headerStrategy = AVIF_ENCODER_MINIMIZE_HEADER; +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) { NEXTARG(); input.requestedDepth = atoi(arg); diff --git a/include/avif/avif.h b/include/avif/avif.h index d2caef9353..7d4e4c7c6d 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -176,6 +176,20 @@ typedef enum avifResult AVIF_API const char * avifResultToString(avifResult result); +// --------------------------------------------------------------------------- +// avifEncoderHeaderStrategy + +typedef enum avifEncoderHeaderStrategy +{ + // Encodes as "avif" brand with a MetaBox and all its required boxes for maximum compatibility. + AVIF_ENCODER_FULL_HEADER, +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + // Encodes as "avir" brand with a CondensedImageBox to reduce the encoded file size. + // WARNING: Experimental feature. Produces files that are incompatible with older decoders. + AVIF_ENCODER_MINIMIZE_HEADER, +#endif +} avifEncoderHeaderStrategy; + // --------------------------------------------------------------------------- // avifROData/avifRWData: Generic raw memory storage @@ -1138,6 +1152,9 @@ typedef struct avifScalingMode // call to avifEncoderAddImage(). typedef struct avifEncoder { + // Defaults to AVIF_ENCODER_FULL_HEADER + avifEncoderHeaderStrategy headerStrategy; + // Defaults to AVIF_CODEC_CHOICE_AUTO: Preference determined by order in availableCodecs table (avif.c) avifCodecChoice codecChoice; diff --git a/include/avif/internal.h b/include/avif/internal.h index e58c3e8e77..0e3f3b15cd 100644 --- a/include/avif/internal.h +++ b/include/avif/internal.h @@ -453,6 +453,14 @@ typedef struct avifROStream // Index of the next byte in the raw stream. size_t offset; +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + // Index of the currently selected partial byte. Must be lower than offset. + // Only meaningful if numUsedBitsInPartialByte is not zero. + size_t offsetOfPartialByte; + // Number of bits already used in the currently selected partial byte. + size_t numUsedBitsInPartialByte; +#endif + // Error information, if any. avifDiagnostics * diag; const char * diagContext; @@ -473,6 +481,10 @@ avifBool avifROStreamReadU32(avifROStream * stream, uint32_t * v); avifBool avifROStreamReadU32Endianness(avifROStream * stream, uint32_t * v, avifBool littleEndian); avifBool avifROStreamReadUX8(avifROStream * stream, uint64_t * v, uint64_t factor); // Reads a factor*8 sized uint, saves in v avifBool avifROStreamReadU64(avifROStream * stream, uint64_t * v); +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) +avifBool avifROStreamReadBits(avifROStream * stream, uint32_t * v, size_t bitCount); +avifBool avifROStreamReadVarInt(avifROStream * stream, uint32_t * v); +#endif avifBool avifROStreamReadString(avifROStream * stream, char * output, size_t outputSize); avifBool avifROStreamReadBoxHeader(avifROStream * stream, avifBoxHeader * header); // This fails if the size reported by the header cannot fit in the stream avifBool avifROStreamReadBoxHeaderPartial(avifROStream * stream, avifBoxHeader * header); // This doesn't require that the full box can fit in the stream @@ -485,6 +497,14 @@ typedef struct avifRWStream // Index of the next byte in the raw stream. size_t offset; + +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + // Index of the currently selected partial byte. Must be lower than offset. + // Only meaningful if numUsedBitsInPartialByte is not zero. + size_t offsetOfPartialByte; + // Number of bits already used in the currently selected partial byte. + size_t numUsedBitsInPartialByte; +#endif } avifRWStream; uint8_t * avifRWStreamCurrent(avifRWStream * stream); @@ -503,6 +523,10 @@ void avifRWStreamWriteU16(avifRWStream * stream, uint16_t v); void avifRWStreamWriteU32(avifRWStream * stream, uint32_t v); void avifRWStreamWriteU64(avifRWStream * stream, uint64_t v); void avifRWStreamWriteZeros(avifRWStream * stream, size_t byteCount); +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) +void avifRWStreamWriteBits(avifRWStream * stream, uint32_t v, size_t bitCount); +void avifRWStreamWriteVarInt(avifRWStream * stream, uint32_t v); +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR // This is to make it clear that the box size is currently unknown, and will be determined later (with a call to avifRWStreamFinishBox) #define AVIF_BOX_SIZE_TBD 0 diff --git a/src/read.c b/src/read.c index 9fba9e09ba..819d1957ee 100644 --- a/src/read.c +++ b/src/read.c @@ -202,6 +202,9 @@ typedef struct avifDecoderItem avifBool hasUnsupportedEssentialProperty; // If true, this item cites a property flagged as 'essential' that libavif doesn't support (yet). Ignore the item, if so. avifBool ipmaSeen; // if true, this item already received a property association avifBool progressive; // if true, this item has progressive layers (a1lx), but does not select a specific layer (the layer_id value in lsel is set to 0xFFFF) +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + avifPixelFormat coniPixelFormat; // Set from the CondensedImageBox, if present (AVIF_PIXEL_FORMAT_NONE otherwise) +#endif } avifDecoderItem; AVIF_ARRAY_DECLARE(avifDecoderItemArray, avifDecoderItem, item); @@ -732,6 +735,12 @@ typedef struct avifMeta // AVIF, this should point at an image item containing color planes, and all other items // are ignored unless they refer to this item in some way (alpha plane, EXIF/XMP metadata). uint32_t primaryItemID; + +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + // If true, the fields above were extracted from a CondensedImageBox. It implies some + // constraints to its optional extendedMeta field, such as forbidden item IDs, properties etc. + avifBool fromConi; +#endif } avifMeta; static void avifMetaDestroy(avifMeta * meta); @@ -1155,6 +1164,19 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item } } +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + if ((item->coniPixelFormat != AVIF_PIXEL_FORMAT_NONE) && + (item->coniPixelFormat != avifCodecConfigurationBoxGetFormat(&configProp->u.av1C))) { + avifDiagnosticsPrintf(diag, + "Item ID %u format [%s] specified by coni box does not match %s property format [%s]", + item->id, + avifPixelFormatToString(item->coniPixelFormat), + configPropName, + avifPixelFormatToString(avifCodecConfigurationBoxGetFormat(&configProp->u.av1C))); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR + if (strictFlags & AVIF_STRICT_CLAP_VALID) { const avifProperty * clapProp = avifPropertyArrayFind(&item->properties, "clap"); if (clapProp) { @@ -1571,9 +1593,15 @@ static avifResult avifDecoderFindMetadata(avifDecoder * decoder, avifMeta * meta // Advance past Annex A.2.1's header BEGIN_STREAM(exifBoxStream, exifContents.data, exifContents.size, &decoder->diag, "Exif header"); - uint32_t exifTiffHeaderOffset; - AVIF_CHECKERR(avifROStreamReadU32(&exifBoxStream, &exifTiffHeaderOffset), - AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) exif_tiff_header_offset; +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + if (!meta->fromConi) +#endif + { + // The CondensedImageBox does not signal the exifTiffHeaderOffset. + uint32_t exifTiffHeaderOffset; + AVIF_CHECKERR(avifROStreamReadU32(&exifBoxStream, &exifTiffHeaderOffset), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(32) exif_tiff_header_offset; + } AVIF_CHECKRES(avifRWDataSet(&image->exif, avifROStreamCurrent(&exifBoxStream), avifROStreamRemainingBytes(&exifBoxStream))); } else if (!decoder->ignoreXMP && !memcmp(item->type, "mime", 4) && @@ -1676,6 +1704,14 @@ static avifBool avifParseItemLocationBox(avifMeta * meta, const uint8_t * raw, s } else { AVIF_CHECK(avifROStreamReadU32(&s, &itemID)); // unsigned int(32) item_ID; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + if (meta->fromConi && itemID < 5) { + // This is the extendedMeta field of an enclosing CondensedImageBox. + // Item IDs 1 to 4 are reserved and already defined, including their locations. + avifDiagnosticsPrintf(diag, "%s: Box[iloc] has a forbidden item ID [%u]", s.diagContext, itemID); + return AVIF_FALSE; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR avifDecoderItem * item = avifMetaFindItem(meta, itemID); if (!item) { @@ -1700,6 +1736,14 @@ static avifBool avifParseItemLocationBox(avifMeta * meta, const uint8_t * raw, s return AVIF_FALSE; } if (constructionMethod == 1) { +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + if (meta->fromConi) { + // This is the extendedMeta field of an enclosing CondensedImageBox. + // It may not contain an ItemDataBox. + avifDiagnosticsPrintf(diag, "%s: Box[iloc] has a forbidden construction method [%u]", s.diagContext, constructionMethod); + return AVIF_FALSE; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR item->idatStored = AVIF_TRUE; } } @@ -2101,6 +2145,14 @@ static avifBool avifParseItemPropertyContainerBox(avifPropertyArray * properties static avifBool avifParseItemPropertyAssociation(avifMeta * meta, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag, uint32_t * outVersionAndFlags) { +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + if (meta->fromConi) { + // The CondensedImageBox will always create 8 item properties, so to refer to the + // first property in the ItemPropertyContainerBox of its extendedMeta field, use index 9. + assert(meta->properties.count >= 8); + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR + // NOTE: If this function ever adds support for versions other than [0,1] or flags other than // [0,1], please increase the value of MAX_IPMA_VERSION_AND_FLAGS_SEEN accordingly. @@ -2408,6 +2460,15 @@ static avifBool avifParseItemInfoEntry(avifMeta * meta, const uint8_t * raw, siz memset(&contentType, 0, sizeof(contentType)); } +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + if (meta->fromConi && itemID < 5) { + // This is the extendedMeta field of an enclosing CondensedImageBox. + // itemID 1 is reserved for the primary color item, 2 for the alpha auxiliary item, + // 3 for the Exif metadata item and 4 for the XMP metadata item. + avifDiagnosticsPrintf(diag, "%s: Box[infe] of type %.4s has a forbidden item ID [%u]", s.diagContext, itemType, itemID); + return AVIF_FALSE; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR avifDecoderItem * item = avifMetaFindItem(meta, itemID); if (!item) { avifDiagnosticsPrintf(diag, "%s: Box[infe] with item_type %.4s has an invalid item ID [%u]", s.diagContext, itemType, itemID); @@ -3077,6 +3138,350 @@ static avifBool avifParseMovieBox(avifDecoderData * data, uint64_t rawOffset, co return AVIF_TRUE; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) +static avifBool avifParseExtendedMeta(avifMeta * meta, uint64_t rawOffset, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) +{ + BEGIN_STREAM(s, raw, rawLen, diag, "extendedMeta"); + // The extendedMeta field has no size and box type because these are already set by the CondensedImageBox. + + uint32_t uniqueBoxFlags = 0; + while (avifROStreamHasBytesLeft(&s, 1)) { + avifBoxHeader header; + AVIF_CHECK(avifROStreamReadBoxHeader(&s, &header)); + + if (!memcmp(header.type, "hdlr", 4) || !memcmp(header.type, "dinf", 4) || !memcmp(header.type, "idat", 4) || + !memcmp(header.type, "pitm", 4)) { + avifDiagnosticsPrintf(diag, "Box[coni] shall have no Box[%.4s] in its extendedMeta field", (const char *)header.type); + return AVIF_FALSE; + } else if (!memcmp(header.type, "iinf", 4)) { + AVIF_CHECK(uniqueBoxSeen(&uniqueBoxFlags, 5, "coni", "iinf", diag)); + AVIF_CHECK(avifParseItemInfoBox(meta, avifROStreamCurrent(&s), header.size, diag)); + } else if (!memcmp(header.type, "iloc", 4)) { + AVIF_CHECK(uniqueBoxSeen(&uniqueBoxFlags, 1, "coni", "iloc", diag)); + AVIF_CHECK(avifParseItemLocationBox(meta, avifROStreamCurrent(&s), header.size, diag)); + } else if (!memcmp(header.type, "iprp", 4)) { + AVIF_CHECK(uniqueBoxSeen(&uniqueBoxFlags, 4, "coni", "iprp", diag)); + AVIF_CHECK(avifParseItemPropertiesBox(meta, rawOffset + avifROStreamOffset(&s), avifROStreamCurrent(&s), header.size, diag)); + } else if (!memcmp(header.type, "iref", 4)) { + AVIF_CHECK(uniqueBoxSeen(&uniqueBoxFlags, 6, "coni", "iref", diag)); + AVIF_CHECK(avifParseItemReferenceBox(meta, avifROStreamCurrent(&s), header.size, diag)); + } + + AVIF_CHECK(avifROStreamSkip(&s, header.size)); + } + return AVIF_TRUE; +} + +static avifProperty * avifMetaCreateProperty(avifMeta * meta, const char * propertyType) +{ + avifProperty * metaProperty = avifArrayPushPtr(&meta->properties); + AVIF_CHECK(metaProperty); + memcpy(metaProperty->type, propertyType, 4); + return metaProperty; +} + +static avifProperty * avifDecoderItemAddProperty(avifDecoderItem * item, const avifProperty * metaProperty) +{ + avifProperty * itemProperty = avifArrayPushPtr(&item->properties); + AVIF_CHECK(itemProperty); + *itemProperty = *metaProperty; + return itemProperty; +} + +static avifResult avifParseCondensedImageBox(avifMeta * meta, uint64_t rawOffset, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) +{ + // Experimental box. + BEGIN_STREAM(s, raw, rawLen, diag, "Box[coni]"); + + meta->fromConi = AVIF_TRUE; + + // Read the bit fields. + + uint32_t version; + AVIF_CHECKERR(avifROStreamReadBits(&s, &version, 2), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(2) version; + AVIF_CHECKERR(version == 0, AVIF_RESULT_NOT_IMPLEMENTED); + + uint32_t width, height; + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &width), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) width_minus_one; + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &height), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) height_minus_one; + ++width; + ++height; + + uint32_t isFloat; + AVIF_CHECKERR(avifROStreamReadBits(&s, &isFloat, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) is_float; + AVIF_CHECKERR(!isFloat, AVIF_RESULT_NOT_IMPLEMENTED); + uint32_t bitDepth; + AVIF_CHECKERR(avifROStreamReadBits(&s, &bitDepth, 4), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) bit_depth_minus_one; + ++bitDepth; + AVIF_CHECKERR((bitDepth == 8) || (bitDepth == 10) || (bitDepth == 12), AVIF_RESULT_UNSUPPORTED_DEPTH); + uint32_t isMonochrome; + AVIF_CHECKERR(avifROStreamReadBits(&s, &isMonochrome, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) is_monochrome; + uint32_t isSubsampled = AVIF_FALSE; + if (!isMonochrome) { + AVIF_CHECKERR(avifROStreamReadBits(&s, &isSubsampled, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) is_subsampled; + } + uint32_t fullRange; + AVIF_CHECKERR(avifROStreamReadBits(&s, &fullRange, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) full_range; + uint32_t colorType; + AVIF_CHECKERR(avifROStreamReadBits(&s, &colorType, 2), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(2) color_type; + uint32_t colorPrimaries; + uint32_t transferCharacteristics; + uint32_t matrixCoefficients; + uint32_t iccDataSize; + if (colorType == 3) { + colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + AVIF_CHECKERR(avifROStreamReadBits(&s, &matrixCoefficients, 8), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(8) matrix_coefficients; + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &iccDataSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) icc_data_size; + ++iccDataSize; + } else if (colorType == 0) { + // sRGB + colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + iccDataSize = 0; + } else { + const uint32_t numBitsPerComponent = (colorType == 1) ? 5 : 8; + AVIF_CHECKERR(avifROStreamReadBits(&s, &colorPrimaries, numBitsPerComponent), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(5/8) color_primaries; + AVIF_CHECKERR(avifROStreamReadBits(&s, &transferCharacteristics, numBitsPerComponent), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(5/8) transfer_characteristics; + AVIF_CHECKERR(avifROStreamReadBits(&s, &matrixCoefficients, numBitsPerComponent), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(5/8) matrix_coefficients; + iccDataSize = 0; + } + + uint32_t hasExplicitCodecTypes; + AVIF_CHECKERR(avifROStreamReadBits(&s, &hasExplicitCodecTypes, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) has_explicit_codec_types; + AVIF_CHECKERR(!hasExplicitCodecTypes, AVIF_RESULT_NOT_IMPLEMENTED); + uint32_t colorItemCodecConfigSize, colorItemDataSize; + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &colorItemCodecConfigSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) main_item_codec_config_size; + AVIF_CHECKERR(colorItemCodecConfigSize == 4, AVIF_RESULT_NOT_IMPLEMENTED); + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &colorItemDataSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) main_item_data_size; + ++colorItemDataSize; + + uint32_t hasAlpha; + uint32_t alphaIsPremultiplied = AVIF_FALSE; + uint32_t alphaItemDataSize = 0; + AVIF_CHECKERR(avifROStreamReadBits(&s, &hasAlpha, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) has_alpha; + if (hasAlpha) { + AVIF_CHECKERR(avifROStreamReadBits(&s, &alphaIsPremultiplied, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) alpha_is_premultiplied; + uint32_t alphaItemCodecConfigSize; + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &alphaItemCodecConfigSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) alpha_item_codec_config_size; + AVIF_CHECKERR(alphaItemCodecConfigSize == 4, AVIF_RESULT_NOT_IMPLEMENTED); + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &alphaItemDataSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) alpha_item_data_size; + } + + uint32_t extendedMetaSize, exifSize, xmpSize; + AVIF_CHECKERR(avifROStreamReadBits(&s, &extendedMetaSize, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) has_extended_meta; + if (extendedMetaSize) { + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &extendedMetaSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) extended_meta_size; + ++extendedMetaSize; + } + AVIF_CHECKERR(avifROStreamReadBits(&s, &exifSize, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) has_exif; + if (exifSize) { + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &exifSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) exif_data_size; + ++exifSize; + } + AVIF_CHECKERR(avifROStreamReadBits(&s, &xmpSize, 1), AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(1) has_xmp; + if (xmpSize) { + AVIF_CHECKERR(avifROStreamReadVarInt(&s, &xmpSize), AVIF_RESULT_BMFF_PARSE_FAILED); // varint(32) xmp_data_size; + ++xmpSize; + } + + // Padding to align with whole bytes if necessary. + if (s.numUsedBitsInPartialByte) { + uint32_t padding; + AVIF_CHECKERR(avifROStreamReadBits(&s, &padding, 8 - s.numUsedBitsInPartialByte), AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKERR(!padding, AVIF_RESULT_BMFF_PARSE_FAILED); // Only accept zeros as padding. + } + + avifCodecConfigurationBox alphaCodecConfig = { 0 }; + if (hasAlpha) { + AVIF_CHECKERR(avifParseCodecConfiguration(&s, &alphaCodecConfig, "coni", diag), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(8) alpha_item_codec_config[]; + } + avifCodecConfigurationBox colorCodecConfig; + AVIF_CHECKERR(avifParseCodecConfiguration(&s, &colorCodecConfig, "coni", diag), + AVIF_RESULT_BMFF_PARSE_FAILED); // unsigned int(8) main_item_codec_config[]; + + // Store and update the offset for the following item extents and properties. + // The extendedMeta field is parsed after creating the items defined by the CondensedImageBox + // so the stream s cannot be used for keeping track of the position. + size_t offset = (size_t)rawOffset + avifROStreamOffset(&s) + (size_t)extendedMetaSize; + + // Create the items and properties generated by the CondensedImageBox. + // The CondensedImageBox always creates 8 properties for specification easiness. + // Use FreeSpaceBoxes as no-op placeholder properties when necessary. + // There is no need to use placeholder items because item IDs do not have to + // be contiguous, whereas property indices shall be 1, 2, 3, 4, 5 etc. + + meta->primaryItemID = 1; + avifDecoderItem * colorItem = avifMetaFindItem(meta, meta->primaryItemID); + AVIF_CHECKERR(colorItem, AVIF_RESULT_OUT_OF_MEMORY); + memcpy(colorItem->type, "av01", 4); + colorItem->width = width; + colorItem->height = height; + colorItem->coniPixelFormat = isMonochrome ? AVIF_PIXEL_FORMAT_YUV400 + : isSubsampled ? AVIF_PIXEL_FORMAT_YUV420 + : AVIF_PIXEL_FORMAT_YUV444; + + avifDecoderItem * alphaItem = NULL; + if (hasAlpha) { + alphaItem = avifMetaFindItem(meta, /*itemID=*/2); + AVIF_CHECKERR(alphaItem, AVIF_RESULT_OUT_OF_MEMORY); + memcpy(alphaItem->type, "av01", 4); + alphaItem->width = width; + alphaItem->height = height; + alphaItem->coniPixelFormat = AVIF_PIXEL_FORMAT_YUV400; + } + + // Property with fixed index 1. + avifProperty * colorCodecConfigProp = avifMetaCreateProperty(meta, "av1C"); + AVIF_CHECKERR(colorCodecConfigProp, AVIF_RESULT_OUT_OF_MEMORY); + colorCodecConfigProp->u.av1C = colorCodecConfig; + AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, colorCodecConfigProp), AVIF_RESULT_OUT_OF_MEMORY); + + // Property with fixed index 2. + avifProperty * ispeProp = avifMetaCreateProperty(meta, "ispe"); + AVIF_CHECKERR(ispeProp, AVIF_RESULT_OUT_OF_MEMORY); + ispeProp->u.ispe.width = width; + ispeProp->u.ispe.height = height; + AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, ispeProp), AVIF_RESULT_OUT_OF_MEMORY); + + // Property with fixed index 3. + avifProperty * pixiProp = avifMetaCreateProperty(meta, "pixi"); + AVIF_CHECKERR(pixiProp, AVIF_RESULT_OUT_OF_MEMORY); + pixiProp->u.pixi.planeCount = isMonochrome ? 1 : 3; + for (uint8_t plane = 0; plane < pixiProp->u.pixi.planeCount; ++plane) { + pixiProp->u.pixi.planeDepths[plane] = (uint8_t)bitDepth; + } + AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, pixiProp), AVIF_RESULT_OUT_OF_MEMORY); + + // Property with fixed index 4. + avifProperty * colrPropNCLX = avifMetaCreateProperty(meta, "colr"); + AVIF_CHECKERR(colrPropNCLX, AVIF_RESULT_OUT_OF_MEMORY); + colrPropNCLX->u.colr.hasNCLX = AVIF_TRUE; // colour_type "nclx" + colrPropNCLX->u.colr.colorPrimaries = (avifColorPrimaries)colorPrimaries; + colrPropNCLX->u.colr.transferCharacteristics = (avifTransferCharacteristics)transferCharacteristics; + colrPropNCLX->u.colr.matrixCoefficients = (avifMatrixCoefficients)matrixCoefficients; + colrPropNCLX->u.colr.range = fullRange ? AVIF_RANGE_FULL : AVIF_RANGE_LIMITED; + AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, colrPropNCLX), AVIF_RESULT_OUT_OF_MEMORY); + + // Property with fixed index 5. + if (iccDataSize) { + avifProperty * colrPropICC = avifMetaCreateProperty(meta, "colr"); + AVIF_CHECKERR(colrPropICC, AVIF_RESULT_OUT_OF_MEMORY); + colrPropICC->u.colr.hasICC = AVIF_TRUE; // colour_type "rICC" or "prof" + colrPropICC->u.colr.iccOffset = offset; + colrPropICC->u.colr.iccSize = iccDataSize; + offset += iccDataSize; + AVIF_CHECKERR(avifDecoderItemAddProperty(colorItem, colrPropICC), AVIF_RESULT_OUT_OF_MEMORY); + } else { + AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); // Placeholder. + } + + if (hasAlpha) { + // Property with fixed index 6. + avifProperty * alphaCodecConfigProp = avifMetaCreateProperty(meta, "av1C"); + AVIF_CHECKERR(alphaCodecConfigProp, AVIF_RESULT_OUT_OF_MEMORY); + alphaCodecConfigProp->u.av1C = alphaCodecConfig; + AVIF_CHECKERR(avifDecoderItemAddProperty(alphaItem, alphaCodecConfigProp), AVIF_RESULT_OUT_OF_MEMORY); + + // Property with fixed index 7. + alphaItem->auxForID = colorItem->id; + colorItem->premByID = alphaIsPremultiplied; + avifProperty * alphaAuxProp = avifMetaCreateProperty(meta, "auxC"); + AVIF_CHECKERR(alphaAuxProp, AVIF_RESULT_OUT_OF_MEMORY); + strcpy(alphaAuxProp->u.auxC.auxType, AVIF_URN_ALPHA0); + AVIF_CHECKERR(avifDecoderItemAddProperty(alphaItem, alphaAuxProp), AVIF_RESULT_OUT_OF_MEMORY); + + // Property with fixed index 2 (reused). + AVIF_CHECKERR(avifDecoderItemAddProperty(alphaItem, ispeProp), AVIF_RESULT_OUT_OF_MEMORY); + + // Property with fixed index 8. + avifProperty * alphaPixiProp = avifMetaCreateProperty(meta, "pixi"); + AVIF_CHECKERR(alphaPixiProp, AVIF_RESULT_OUT_OF_MEMORY); + memcpy(alphaPixiProp->type, "pixi", 4); + alphaPixiProp->u.pixi.planeCount = 1; + alphaPixiProp->u.pixi.planeDepths[0] = (uint8_t)bitDepth; + AVIF_CHECKERR(avifDecoderItemAddProperty(alphaItem, alphaPixiProp), AVIF_RESULT_OUT_OF_MEMORY); + } else { + // Placeholders 6, 7 and 8. + AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); + AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); + AVIF_CHECKERR(avifMetaCreateProperty(meta, "skip"), AVIF_RESULT_OUT_OF_MEMORY); + } + + // Extents. + + if (hasAlpha) { + avifExtent * alphaExtent = (avifExtent *)avifArrayPushPtr(&alphaItem->extents); + AVIF_CHECKERR(alphaExtent, AVIF_RESULT_OUT_OF_MEMORY); + alphaExtent->offset = offset; + alphaExtent->size = alphaItemDataSize; + offset += alphaItemDataSize; + alphaItem->size = alphaItemDataSize; + } + + avifExtent * colorExtent = (avifExtent *)avifArrayPushPtr(&colorItem->extents); + AVIF_CHECKERR(colorExtent, AVIF_RESULT_OUT_OF_MEMORY); + colorExtent->offset = offset; + colorExtent->size = colorItemDataSize; + offset += colorItemDataSize; + colorItem->size = colorItemDataSize; + + if (exifSize) { + avifDecoderItem * exifItem = avifMetaFindItem(meta, /*itemID=*/3); + AVIF_CHECKERR(exifItem, AVIF_RESULT_OUT_OF_MEMORY); + memcpy(exifItem->type, "Exif", 4); + exifItem->descForID = colorItem->id; + colorItem->premByID = alphaIsPremultiplied; + + avifExtent * exifExtent = (avifExtent *)avifArrayPushPtr(&exifItem->extents); + AVIF_CHECKERR(exifExtent, AVIF_RESULT_OUT_OF_MEMORY); + exifExtent->offset = offset; + exifExtent->size = exifSize; // Does not include unsigned int(32) exif_tiff_header_offset; + offset += exifSize; + exifItem->size = exifExtent->size; + } + + if (xmpSize) { + avifDecoderItem * xmpItem = avifMetaFindItem(meta, /*itemID=*/4); + AVIF_CHECKERR(xmpItem, AVIF_RESULT_OUT_OF_MEMORY); + memcpy(xmpItem->type, "mime", 4); + memcpy(xmpItem->contentType.contentType, xmpContentType, xmpContentTypeSize); + xmpItem->descForID = colorItem->id; + colorItem->premByID = alphaIsPremultiplied; + + avifExtent * xmpExtent = (avifExtent *)avifArrayPushPtr(&xmpItem->extents); + AVIF_CHECKERR(xmpExtent, AVIF_RESULT_OUT_OF_MEMORY); + xmpExtent->offset = offset; + xmpExtent->size = xmpSize; + offset += xmpSize; + xmpItem->size = xmpSize; + } + + // Complete the generated virtual MetaBox with the ExtendedMetaBox items and properties. + // The ExtendedMetaBox may reuse items and properties created above so it must be parsed last. + + if (extendedMetaSize) { + AVIF_CHECKERR(avifParseExtendedMeta(meta, rawOffset + avifROStreamOffset(&s), avifROStreamCurrent(&s), extendedMetaSize, diag), + AVIF_RESULT_BMFF_PARSE_FAILED); + } + + // Check that the stream s and the variable offset match. + AVIF_CHECKERR(avifROStreamSkip(&s, extendedMetaSize), AVIF_RESULT_TRUNCATED_DATA); + AVIF_CHECKERR(avifROStreamSkip(&s, iccDataSize), AVIF_RESULT_TRUNCATED_DATA); + AVIF_CHECKERR(avifROStreamSkip(&s, alphaItemDataSize), AVIF_RESULT_TRUNCATED_DATA); + AVIF_CHECKERR(avifROStreamSkip(&s, colorItemDataSize), AVIF_RESULT_TRUNCATED_DATA); + AVIF_CHECKERR(avifROStreamSkip(&s, exifSize), AVIF_RESULT_TRUNCATED_DATA); + AVIF_CHECKERR(avifROStreamSkip(&s, xmpSize), AVIF_RESULT_TRUNCATED_DATA); + assert(rawOffset + avifROStreamOffset(&s) == offset); + return AVIF_RESULT_OK; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR + static avifBool avifParseFileTypeBox(avifFileType * ftyp, const uint8_t * raw, size_t rawLen, avifDiagnostics * diag) { BEGIN_STREAM(s, raw, rawLen, diag, "Box[ftyp]"); @@ -3110,8 +3515,10 @@ static avifResult avifParse(avifDecoder * decoder) avifBool ftypSeen = AVIF_FALSE; avifBool metaSeen = AVIF_FALSE; avifBool moovSeen = AVIF_FALSE; + avifBool coniSeen = AVIF_FALSE; avifBool needsMeta = AVIF_FALSE; avifBool needsMoov = AVIF_FALSE; + avifBool needsConi = AVIF_FALSE; for (;;) { // Read just enough to get the next box header (a max of 32 bytes) @@ -3141,7 +3548,8 @@ static avifResult avifParse(avifDecoder * decoder) avifROData boxContents = AVIF_DATA_EMPTY; // TODO: reorg this code to only do these memcmps once each - if (!memcmp(header.type, "ftyp", 4) || !memcmp(header.type, "meta", 4) || !memcmp(header.type, "moov", 4)) { + if (!memcmp(header.type, "ftyp", 4) || !memcmp(header.type, "meta", 4) || !memcmp(header.type, "moov", 4) || + !memcmp(header.type, "coni", 4)) { boxOffset = parseOffset; readResult = decoder->io->read(decoder->io, 0, parseOffset, header.size, &boxContents); if (readResult != AVIF_RESULT_OK) { @@ -3167,6 +3575,12 @@ static avifResult avifParse(avifDecoder * decoder) memcpy(data->majorBrand, ftyp.majorBrand, 4); // Remember the major brand for future AVIF_DECODER_SOURCE_AUTO decisions needsMeta = avifFileTypeHasBrand(&ftyp, "avif"); needsMoov = avifFileTypeHasBrand(&ftyp, "avis"); +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + needsConi = avifFileTypeHasBrand(&ftyp, "avir"); + if (needsConi && (needsMeta || needsMoov)) { + return AVIF_RESULT_INVALID_FTYP; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR } else if (!memcmp(header.type, "meta", 4)) { AVIF_CHECKERR(!metaSeen, AVIF_RESULT_BMFF_PARSE_FAILED); AVIF_CHECKERR(avifParseMetaBox(data->meta, boxOffset, boxContents.data, boxContents.size, data->diag), @@ -3178,18 +3592,31 @@ static avifResult avifParse(avifDecoder * decoder) AVIF_RESULT_BMFF_PARSE_FAILED); moovSeen = AVIF_TRUE; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + else if (!memcmp(header.type, "coni", 4)) { + AVIF_CHECKERR(!metaSeen && !moovSeen, AVIF_RESULT_BMFF_PARSE_FAILED); + AVIF_CHECKRES(avifParseCondensedImageBox(data->meta, boxOffset, boxContents.data, boxContents.size, data->diag)); + coniSeen = AVIF_TRUE; + } + + if (ftypSeen && !needsConi && coniSeen) { + // The 'coni' box should be ignored if there is no 'avir' brand, but libavif allows reading them in any order. + return AVIF_RESULT_NOT_IMPLEMENTED; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR // See if there is enough information to consider Parse() a success and early-out: // * If the brand 'avif' is present, require a meta box // * If the brand 'avis' is present, require a moov box - if (ftypSeen && (!needsMeta || metaSeen) && (!needsMoov || moovSeen)) { + // * If the brand 'avir' is present, require a coni box + if (ftypSeen && (!needsMeta || metaSeen) && (!needsMoov || moovSeen) && (!needsConi || coniSeen)) { return AVIF_RESULT_OK; } } if (!ftypSeen) { return AVIF_RESULT_INVALID_FTYP; } - if ((needsMeta && !metaSeen) || (needsMoov && !moovSeen)) { + if ((needsMeta && !metaSeen) || (needsMoov && !moovSeen) || (needsConi && !coniSeen)) { return AVIF_RESULT_TRUNCATED_DATA; } return AVIF_RESULT_OK; @@ -3214,7 +3641,11 @@ static avifBool avifFileTypeHasBrand(avifFileType * ftyp, const char * brand) static avifBool avifFileTypeIsCompatible(avifFileType * ftyp) { - return avifFileTypeHasBrand(ftyp, "avif") || avifFileTypeHasBrand(ftyp, "avis"); + return avifFileTypeHasBrand(ftyp, "avif") || avifFileTypeHasBrand(ftyp, "avis") +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + || avifFileTypeHasBrand(ftyp, "avir") +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR + ; } avifBool avifPeekCompatibleFileType(const avifROData * input) diff --git a/src/stream.c b/src/stream.c index a4e9d39bdd..612d1c9926 100644 --- a/src/stream.c +++ b/src/stream.c @@ -21,6 +21,10 @@ void avifROStreamStart(avifROStream * stream, avifROData * raw, avifDiagnostics { stream->raw = raw; stream->offset = 0; +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + stream->offsetOfPartialByte = 0; + stream->numUsedBitsInPartialByte = 0; +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR stream->diag = diag; stream->diagContext = diagContext; @@ -45,6 +49,9 @@ size_t avifROStreamOffset(const avifROStream * stream) void avifROStreamSetOffset(avifROStream * stream, size_t offset) { +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + assert(stream->numUsedBitsInPartialByte == 0); +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR stream->offset = offset; if (stream->offset > stream->raw->size) { stream->offset = stream->raw->size; @@ -137,6 +144,65 @@ avifBool avifROStreamReadU64(avifROStream * stream, uint64_t * v) return AVIF_TRUE; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) +avifBool avifROStreamReadBits(avifROStream * stream, uint32_t * v, size_t bitCount) +{ + *v = 0; + while (bitCount) { + if (stream->numUsedBitsInPartialByte == 0) { + stream->offsetOfPartialByte = stream->offset; + AVIF_CHECK(avifROStreamSkip(stream, sizeof(uint8_t))); + } + const uint8_t * packedBits = stream->raw->data + stream->offsetOfPartialByte; + + const size_t numBits = AVIF_MIN(bitCount, 8 - stream->numUsedBitsInPartialByte); + stream->numUsedBitsInPartialByte += numBits; + bitCount -= numBits; + // The stream bits are packed starting with the most significant bit of the first input byte. + // This way, packed bits can be found in the same order in the bit stream. + const uint32_t bits = (*packedBits >> (8 - stream->numUsedBitsInPartialByte)) & ((1 << numBits) - 1); + // The value bits are ordered from the most significant bit to the least significant bit (big-endian). + *v |= bits << bitCount; + + if (stream->numUsedBitsInPartialByte == 8) { + // Start a new partial byte the next time a bit is needed. + stream->numUsedBitsInPartialByte = 0; + } + } + return AVIF_TRUE; +} + +// Based on https://sqlite.org/src4/doc/trunk/www/varint.wiki. +avifBool avifROStreamReadVarInt(avifROStream * stream, uint32_t * v) +{ + uint32_t a[5]; + AVIF_CHECK(avifROStreamReadBits(stream, &a[0], 8)); + if (a[0] <= 240) { + *v = a[0]; + } else { + AVIF_CHECK(avifROStreamReadBits(stream, &a[1], 8)); + if (a[0] <= 248) { + *v = 240 + 256 * (a[0] - 241) + a[1]; + } else { + AVIF_CHECK(avifROStreamReadBits(stream, &a[2], 8)); + if (a[0] == 249) { + *v = 2288 + 256 * a[1] + a[2]; + } else { + AVIF_CHECK(avifROStreamReadBits(stream, &a[3], 8)); + if (a[0] == 250) { + *v = (a[3] << 16) | (a[2] << 8) | a[1]; + } else { + // TODO(yguyon): Use values of a[0] in range [252-255] (avoid pessimization). + AVIF_CHECK(avifROStreamReadBits(stream, &a[4], 8)); + *v = (a[4] << 24) | (a[3] << 16) | (a[2] << 8) | a[1]; + } + } + } + } + return AVIF_TRUE; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR + avifBool avifROStreamReadString(avifROStream * stream, char * output, size_t outputSize) { // Check for the presence of a null terminator in the stream. @@ -250,6 +316,10 @@ void avifRWStreamStart(avifRWStream * stream, avifRWData * raw) { stream->raw = raw; stream->offset = 0; +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + stream->offsetOfPartialByte = 0; + stream->numUsedBitsInPartialByte = 0; +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR } size_t avifRWStreamOffset(const avifRWStream * stream) @@ -372,3 +442,59 @@ void avifRWStreamWriteZeros(avifRWStream * stream, size_t byteCount) } stream->offset += byteCount; } + +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) +void avifRWStreamWriteBits(avifRWStream * stream, uint32_t v, size_t bitCount) +{ + assert(v >> bitCount == 0); + while (bitCount) { + if (stream->numUsedBitsInPartialByte == 0) { + makeRoom(stream, 1); + stream->offsetOfPartialByte = stream->offset; + stream->raw->data[stream->offsetOfPartialByte] = 0; + stream->offset += 1; + } + uint8_t * packedBits = stream->raw->data + stream->offsetOfPartialByte; + + const size_t numBits = AVIF_MIN(bitCount, 8 - stream->numUsedBitsInPartialByte); + stream->numUsedBitsInPartialByte += numBits; + bitCount -= numBits; + // Order the input bits from the most significant bit to the least significant bit (big-endian). + const uint32_t bits = (v >> bitCount) & ((1 << numBits) - 1); + // Pack bits starting with the most significant bit of the first output byte. + // This way, packed bits can be found in the same order in the bit stream. + *packedBits |= bits << (8 - stream->numUsedBitsInPartialByte); + + if (stream->numUsedBitsInPartialByte == 8) { + // Start a new partial byte the next time a bit is needed. + stream->numUsedBitsInPartialByte = 0; + } + } +} + +// Based on https://sqlite.org/src4/doc/trunk/www/varint.wiki. +void avifRWStreamWriteVarInt(avifRWStream * stream, uint32_t v) +{ + if (v <= 240) { + avifRWStreamWriteBits(stream, v, 8); + } else if (v <= 2287) { + avifRWStreamWriteBits(stream, (v - 240) / 256 + 241, 8); + avifRWStreamWriteBits(stream, (v - 240) % 256, 8); + } else if (v <= 67823) { + avifRWStreamWriteBits(stream, 249, 8); + avifRWStreamWriteBits(stream, (v - 2288) / 256, 8); + avifRWStreamWriteBits(stream, (v - 2288) % 256, 8); + } else if (v <= 16777215) { + avifRWStreamWriteBits(stream, 250, 8); + avifRWStreamWriteBits(stream, (v >> 0) & 0xff, 8); + avifRWStreamWriteBits(stream, (v >> 8) & 0xff, 8); + avifRWStreamWriteBits(stream, (v >> 16) & 0xff, 8); + } else { + avifRWStreamWriteBits(stream, 251, 8); + avifRWStreamWriteBits(stream, (v >> 0) & 0xff, 8); + avifRWStreamWriteBits(stream, (v >> 8) & 0xff, 8); + avifRWStreamWriteBits(stream, (v >> 16) & 0xff, 8); + avifRWStreamWriteBits(stream, (v >> 24) & 0xff, 8); + } +} +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR diff --git a/src/write.c b/src/write.c index 1974872422..d4039f595a 100644 --- a/src/write.c +++ b/src/write.c @@ -36,6 +36,7 @@ static const size_t alphaURNSize = sizeof(alphaURN); static const char xmpContentType[] = AVIF_CONTENT_TYPE_XMP; static const size_t xmpContentTypeSize = sizeof(xmpContentType); +static void writeCodecConfig(avifRWStream * s, const avifCodecConfigurationBox * cfg); static void writeConfigBox(avifRWStream * s, const avifCodecConfigurationBox * cfg, const char * configPropName); // --------------------------------------------------------------------------- @@ -417,6 +418,7 @@ avifEncoder * avifEncoderCreate(void) return NULL; } memset(encoder, 0, sizeof(avifEncoder)); + encoder->headerStrategy = AVIF_ENCODER_FULL_HEADER; encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO; encoder->maxThreads = 1; encoder->speed = AVIF_SPEED_DEFAULT; @@ -538,6 +540,7 @@ static avifBool avifEncoderDetectChanges(const avifEncoder * encoder, avifEncode } // Subset of avifEncoderWriteColorProperties() for the properties clli, pasp, clap, irot, imir. +// Also used by the extendedMeta field of the CondensedImageBox. static void avifEncoderWriteExtendedColorProperties(avifRWStream * dedupStream, avifRWStream * outputStream, const avifImage * imageMetadata, @@ -1354,6 +1357,255 @@ static size_t avifEncoderFindExistingChunk(avifRWStream * s, size_t mdatStartOff return 0; } +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) +// Writes the extendedMeta field of the CondensedImageBox. +static void avifImageWriteExtendedMeta(const avifImage * imageMetadata, avifRWStream * stream) +{ + // The ExtendedMetaBox has no size and box type because these are already set by the CondensedImageBox. + + avifBoxMarker iprp = avifRWStreamWriteBox(stream, "iprp", AVIF_BOX_SIZE_TBD); + { + // ItemPropertyContainerBox + + avifBoxMarker ipco = avifRWStreamWriteBox(stream, "ipco", AVIF_BOX_SIZE_TBD); + { + // No need for dedup because there is only one property of each type and for a single item. + avifEncoderWriteExtendedColorProperties(stream, stream, imageMetadata, /*ipma=*/NULL, /*dedup=*/NULL); + } + avifRWStreamFinishBox(stream, ipco); + + // ItemPropertyAssociationBox + + avifBoxMarker ipma = avifRWStreamWriteFullBox(stream, "ipma", AVIF_BOX_SIZE_TBD, 0, 0); + { + // Same order as in avifEncoderWriteExtendedColorProperties(). + const uint8_t numNonEssentialProperties = ((imageMetadata->clli.maxCLL || imageMetadata->clli.maxPALL) ? 1 : 0) + + ((imageMetadata->transformFlags & AVIF_TRANSFORM_PASP) ? 1 : 0); + const uint8_t numEssentialProperties = ((imageMetadata->transformFlags & AVIF_TRANSFORM_CLAP) ? 1 : 0) + + ((imageMetadata->transformFlags & AVIF_TRANSFORM_IROT) ? 1 : 0) + + ((imageMetadata->transformFlags & AVIF_TRANSFORM_IMIR) ? 1 : 0); + const uint8_t ipmaCount = numNonEssentialProperties + numEssentialProperties; + assert(ipmaCount >= 1); + + // Only add properties to the primary item. + avifRWStreamWriteU32(stream, 1); // unsigned int(32) entry_count; + { + // Primary item ID is defined as 1 by the CondensedImageBox. + avifRWStreamWriteU16(stream, 1); // unsigned int(16) item_ID; + avifRWStreamWriteU8(stream, ipmaCount); // unsigned int(8) association_count; + for (uint8_t i = 0; i < ipmaCount; ++i) { + avifRWStreamWriteBits(stream, (i >= numNonEssentialProperties) ? 1 : 0, /*bitCount=*/1); // bit(1) essential; + // The CondensedImageBox will always create 8 item properties, so to refer to the + // first property in the ItemPropertyContainerBox above, use index 9. + avifRWStreamWriteBits(stream, 9 + i, /*bitCount=*/7); // unsigned int(7) property_index; + } + } + } + avifRWStreamFinishBox(stream, ipma); + } + avifRWStreamFinishBox(stream, iprp); +} + +// Returns true if the image can be encoded with a CondensedImageBox instead of a full regular MetaBox. +static avifBool avifEncoderIsCondensedImageBoxCompatible(const avifEncoder * encoder) +{ + // The CondensedImageBox ("avir" brand) only supports non-layered, still images. + if (encoder->extraLayerCount || (encoder->data->frames.count != 1)) { + return AVIF_FALSE; + } + + // Only 4:4:4, 4:2:0 and 4:0:0 are supported by a CondensedImageBox. + if ((encoder->data->imageMetadata->yuvFormat != AVIF_PIXEL_FORMAT_YUV444) && + (encoder->data->imageMetadata->yuvFormat != AVIF_PIXEL_FORMAT_YUV420) && + (encoder->data->imageMetadata->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)) { + return AVIF_FALSE; + } + + const avifEncoderItem * colorItem = NULL; + for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { + avifEncoderItem * item = &encoder->data->items.item[itemIndex]; + + // Grids are not supported by a CondensedImageBox. + if (item->gridCols || item->gridRows) { + return AVIF_FALSE; + } + + if (item->id == encoder->data->primaryItemID) { + assert(!colorItem); + colorItem = item; + continue; // The primary item can be stored in the CondensedImageBox. + } + if (item->itemCategory == AVIF_ITEM_ALPHA && item->irefToID == encoder->data->primaryItemID) { + continue; // The alpha auxiliary item can be stored in the CondensedImageBox. + } + if (!memcmp(item->type, "mime", 4) && !memcmp(item->infeName, "XMP", item->infeNameSize)) { + assert(item->metadataPayload.size == encoder->data->imageMetadata->xmp.size); + continue; // XMP metadata can be stored in the CondensedImageBox. + } + if (!memcmp(item->type, "Exif", 4) && !memcmp(item->infeName, "Exif", item->infeNameSize)) { + assert(item->metadataPayload.size == encoder->data->imageMetadata->exif.size + 4); + const uint32_t exif_tiff_header_offset = *(uint32_t *)item->metadataPayload.data; + if (exif_tiff_header_offset != 0) { + return AVIF_FALSE; + } + continue; // Exif metadata can be stored in the CondensedImageBox if exif_tiff_header_offset is 0. + } + + // Items besides the colorItem, the alphaItem and Exif/XMP/ICC + // metadata are not directly supported by the CondensedImageBox. + // Store them in its inner extendedMeta field instead. + // TODO(yguyon): Implement comment above instead of fallbacking to regular AVIF + // (or drop the comment above if there is no other item type). + return AVIF_FALSE; + } + // A primary item is necessary. + if (!colorItem) { + return AVIF_FALSE; + } + return AVIF_TRUE; +} + +static avifResult avifEncoderWriteFileTypeBoxAndCondensedImageBox(avifEncoder * encoder, avifRWData * output) +{ + avifRWStream s; + avifRWStreamStart(&s, output); + + // FileTypeBox + + avifBoxMarker ftyp = avifRWStreamWriteBox(&s, "ftyp", AVIF_BOX_SIZE_TBD); + avifRWStreamWriteChars(&s, "avir", 4); // unsigned int(32) major_brand; + avifRWStreamWriteU32(&s, 0); // unsigned int(32) minor_version; + avifRWStreamWriteChars(&s, "avir", 4); // unsigned int(32) compatible_brands[]; + avifRWStreamFinishBox(&s, ftyp); + + // CondensedImageBox + + const avifEncoderItem * colorItem = NULL; + const avifEncoderItem * alphaItem = NULL; + for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) { + avifEncoderItem * item = &encoder->data->items.item[itemIndex]; + if (item->id == encoder->data->primaryItemID) { + colorItem = item; + assert(colorItem->encodeOutput->samples.count == 1); + } else if (item->itemCategory == AVIF_ITEM_ALPHA && item->irefToID == encoder->data->primaryItemID) { + assert(!alphaItem); + alphaItem = item; + assert(alphaItem->encodeOutput->samples.count == 1); + } + } + + const avifRWData * colorData = &colorItem->encodeOutput->samples.sample[0].data; + const avifRWData * alphaData = alphaItem ? &alphaItem->encodeOutput->samples.sample[0].data : NULL; + + const avifImage * const image = encoder->data->imageMetadata; + + const avifBool isMonochrome = image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400; + const avifBool isSubsampled = image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420; + const avifBool fullRange = image->yuvRange == AVIF_RANGE_FULL; + + avifBoxMarker coni = avifRWStreamWriteBox(&s, "coni", AVIF_BOX_SIZE_TBD); + avifRWStreamWriteBits(&s, 0, 2); // unsigned int(2) version; + + avifRWStreamWriteVarInt(&s, image->width - 1); // varint(32) width_minus_one; + avifRWStreamWriteVarInt(&s, image->height - 1); // varint(32) height_minus_one; + + avifRWStreamWriteBits(&s, 0, 1); // unsigned int(1) is_float; + avifRWStreamWriteBits(&s, image->depth - 1, 4); // unsigned int(4) bit_depth_minus_one; + avifRWStreamWriteBits(&s, isMonochrome, 1); // unsigned int(1) is_monochrome; + if (!isMonochrome) { + avifRWStreamWriteBits(&s, isSubsampled, 1); // unsigned int(1) is_subsampled; + } + avifRWStreamWriteBits(&s, fullRange, 1); // unsigned int(1) full_range; + if (image->icc.size) { + avifRWStreamWriteBits(&s, 3, 2); // unsigned int(2) color_type; + avifRWStreamWriteBits(&s, image->matrixCoefficients, 8); // unsigned int(8) matrix_coefficients; + avifRWStreamWriteVarInt(&s, (uint32_t)image->icc.size - 1); // varint(32) icc_data_size; + } else { + if (((image->colorPrimaries == AVIF_COLOR_PRIMARIES_BT709) && (image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SRGB) && + (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_BT601)) || + ((image->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) && + (image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) && + (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED))) { + avifRWStreamWriteBits(&s, 0, 2); // unsigned int(2) color_type; + } else { + const avifBool eachFitsOn5bits = (image->colorPrimaries >> 5 == 0) && (image->transferCharacteristics >> 5 == 0) && + (image->matrixCoefficients >> 5 == 0); + const uint32_t numBitsPerComponent = eachFitsOn5bits ? 5 : 8; + avifRWStreamWriteBits(&s, eachFitsOn5bits ? 1 : 2, 2); // unsigned int(2) color_type; + avifRWStreamWriteBits(&s, image->colorPrimaries, numBitsPerComponent); // unsigned int(5) color_primaries; + avifRWStreamWriteBits(&s, image->transferCharacteristics, numBitsPerComponent); // unsigned int(5) transfer_characteristics; + avifRWStreamWriteBits(&s, image->matrixCoefficients, numBitsPerComponent); // unsigned int(5) matrix_coefficients; + } + } + + avifRWStreamWriteBits(&s, 0, 1); // unsigned int(1) has_explicit_codec_types; + avifRWStreamWriteVarInt(&s, 4); // varint(32) main_item_codec_config_size; + assert(colorData->size >= 1); + avifRWStreamWriteVarInt(&s, (uint32_t)colorData->size - 1); // varint(32) main_item_data_size; + + avifRWStreamWriteBits(&s, alphaItem ? 1 : 0, 1); // unsigned int(1) has_alpha; + if (alphaItem) { + avifRWStreamWriteBits(&s, image->alphaPremultiplied, 1); // unsigned int(1) alpha_is_premultiplied; + avifRWStreamWriteVarInt(&s, 4); // varint(32) alpha_item_codec_config_size; + avifRWStreamWriteVarInt(&s, (uint32_t)alphaData->size); // varint(32) alpha_item_data_size; + } + + avifRWData extendedMeta = AVIF_DATA_EMPTY; + if (image->clli.maxCLL || image->clli.maxPALL || (image->transformFlags != AVIF_TRANSFORM_NONE)) { + avifRWStream extendedMetaStream; + avifRWStreamStart(&extendedMetaStream, &extendedMeta); + avifImageWriteExtendedMeta(image, &extendedMetaStream); + avifRWStreamFinishWrite(&extendedMetaStream); + } + avifRWStreamWriteBits(&s, extendedMeta.size ? 1 : 0, 1); // unsigned int(1) has_extended_meta; + if (extendedMeta.size) { + avifRWStreamWriteVarInt(&s, (uint32_t)extendedMeta.size - 1); // varint(32) extended_meta_size; + } + + avifRWStreamWriteBits(&s, image->exif.size ? 1 : 0, 1); // unsigned int(1) has_exif; + if (image->exif.size) { + avifRWStreamWriteVarInt(&s, (uint32_t)image->exif.size - 1); // varint(32) exif_data_size; + } + avifRWStreamWriteBits(&s, image->xmp.size ? 1 : 0, 1); // unsigned int(1) has_xmp; + if (image->xmp.size) { + avifRWStreamWriteVarInt(&s, (uint32_t)image->xmp.size - 1); // varint(32) xmp_data_size; + } + + // Padding to align with whole bytes if necessary. + if (s.numUsedBitsInPartialByte) { + avifRWStreamWriteBits(&s, 0, 8 - s.numUsedBitsInPartialByte); + } + + if (alphaItem) { + writeCodecConfig(&s, &alphaItem->av1C); // unsigned int(8) alpha_item_codec_config[]; + } + writeCodecConfig(&s, &colorItem->av1C); // unsigned int(8) main_item_codec_config[]; + + if (extendedMeta.size) { + avifRWStreamWrite(&s, extendedMeta.data, extendedMeta.size); + } + + if (image->icc.size) { + avifRWStreamWrite(&s, image->icc.data, image->icc.size); // unsigned int(8) icc_data[]; + } + if (alphaItem) { + avifRWStreamWrite(&s, alphaData->data, alphaData->size); // unsigned int(8) alpha_data[]; + } + avifRWStreamWrite(&s, colorData->data, colorData->size); // unsigned int(8) main_data[]; + if (image->exif.size) { + avifRWStreamWrite(&s, image->exif.data, image->exif.size); // unsigned int(8) exif_data[]; + } + if (image->xmp.size) { + avifRWStreamWrite(&s, image->xmp.data, image->xmp.size); // unsigned int(8) xmp_data[]; + } + avifRWStreamFinishBox(&s, coni); + + avifRWStreamFinishWrite(&s); + avifRWDataFree(&extendedMeta); + return AVIF_RESULT_OK; +} +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR + avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) { avifDiagnosticsClearError(&encoder->diag); @@ -1414,6 +1666,16 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) // Unix epoch. uint64_t now = (uint64_t)time(NULL) + 2082844800; + // ----------------------------------------------------------------------- + // Decide whether to go for a CondensedImageBox or a full regular MetaBox. + +#if defined(AVIF_ENABLE_EXPERIMENTAL_AVIR) + if ((encoder->headerStrategy == AVIF_ENCODER_MINIMIZE_HEADER) && avifEncoderIsCondensedImageBoxCompatible(encoder)) { + AVIF_CHECKRES(avifEncoderWriteFileTypeBoxAndCondensedImageBox(encoder, output)); + return AVIF_RESULT_OK; + } +#endif // AVIF_ENABLE_EXPERIMENTAL_AVIR + avifRWStream s; avifRWStreamStart(&s, output); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 391bc01c56..4b2bc04329 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -105,6 +105,13 @@ if(AVIF_ENABLE_GTEST) target_include_directories(avifcodectest PRIVATE ${GTEST_INCLUDE_DIRS}) add_test(NAME avifcodectest COMMAND avifcodectest) + if(AVIF_ENABLE_EXPERIMENTAL_AVIR) + add_executable(avifconitest gtest/avifconitest.cc) + target_link_libraries(avifconitest aviftest_helpers ${GTEST_BOTH_LIBRARIES}) + target_include_directories(avifconitest PRIVATE ${GTEST_INCLUDE_DIRS}) + add_test(NAME avifconitest COMMAND avifconitest) + endif() + add_executable(avifdecodetest gtest/avifdecodetest.cc) target_link_libraries(avifdecodetest aviftest_helpers ${GTEST_LIBRARIES}) target_include_directories(avifdecodetest PRIVATE ${GTEST_INCLUDE_DIRS}) @@ -285,6 +292,9 @@ if(AVIF_CODEC_AVM) avifallocationtest avifbasictest avifchangesettingtest avifcllitest avifgridapitest avifincrtest avifiostatstest avifmetadatatest avifprogressivetest avify4mtest PROPERTIES DISABLED True ) + if(AVIF_ENABLE_EXPERIMENTAL_AVIR) + set_tests_properties(avifconitest PROPERTIES DISABLED True) + endif() endif() if(AVIF_BUILD_APPS) diff --git a/tests/gtest/avifconitest.cc b/tests/gtest/avifconitest.cc new file mode 100644 index 0000000000..4c2c63e4b8 --- /dev/null +++ b/tests/gtest/avifconitest.cc @@ -0,0 +1,151 @@ +// Copyright 2023 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +using ::testing::Combine; +using ::testing::Values; + +namespace libavif { +namespace { + +//------------------------------------------------------------------------------ + +class AvifCondensedImageBoxTest + : public testing::TestWithParam> {}; + +TEST_P(AvifCondensedImageBoxTest, SimpleOpaque) { + const int width = std::get<0>(GetParam()); + const int height = std::get<1>(GetParam()); + const int depth = std::get<2>(GetParam()); + const avifPixelFormat format = std::get<3>(GetParam()); + const avifPlanesFlags planes = std::get<4>(GetParam()); + const avifRange range = std::get<5>(GetParam()); + const bool create_icc = std::get<6>(GetParam()); + const bool create_exif = std::get<7>(GetParam()); + const bool create_xmp = std::get<8>(GetParam()); + const bool create_clli = std::get<9>(GetParam()); + const avifTransformFlags create_transform_flags = std::get<10>(GetParam()); + + testutil::AvifImagePtr image = + testutil::CreateImage(width, height, depth, format, planes, range); + ASSERT_NE(image, nullptr); + testutil::FillImageGradient(image.get()); // The pixels do not matter. + if (create_icc) { + avifImageSetProfileICC(image.get(), testutil::kSampleIcc.data(), + testutil::kSampleIcc.size()); + } + if (create_exif) { + avifImageSetMetadataExif(image.get(), testutil::kSampleExif.data(), + testutil::kSampleExif.size()); + } + if (create_xmp) { + avifImageSetMetadataXMP(image.get(), testutil::kSampleXmp.data(), + testutil::kSampleXmp.size()); + } + if (create_clli) { + image->clli.maxCLL = 1; + image->clli.maxPALL = 2; + } + image->transformFlags = create_transform_flags; + if (create_transform_flags & AVIF_TRANSFORM_PASP) { + image->pasp.hSpacing = 1; + image->pasp.vSpacing = 2; + } + if (create_transform_flags & AVIF_TRANSFORM_CLAP) { + const avifCropRect rect{image->width / 4, image->height / 4, + std::min(1u, image->width / 2), + std::min(1u, image->height / 2)}; + ASSERT_TRUE(avifCleanApertureBoxConvertCropRect( + &image->clap, &rect, image->width, image->height, image->yuvFormat, + /*diag=*/nullptr)); + } + if (create_transform_flags & AVIF_TRANSFORM_IROT) { + image->irot.angle = 2; + } + if (create_transform_flags & AVIF_TRANSFORM_IMIR) { + image->imir.mode = 1; + } + + // Encode. + testutil::AvifRwData encoded_coni; + testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy); + ASSERT_NE(encoder, nullptr); + encoder->speed = AVIF_SPEED_FASTEST; + encoder->headerStrategy = AVIF_ENCODER_MINIMIZE_HEADER; + ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded_coni), + AVIF_RESULT_OK); + + // Decode. + const testutil::AvifImagePtr decoded_coni = + testutil::Decode(encoded_coni.data, encoded_coni.size); + ASSERT_NE(decoded_coni, nullptr); + + // Compare. + testutil::AvifRwData encoded_meta = + testutil::Encode(image.get(), encoder->speed); + ASSERT_NE(encoded_meta.data, nullptr); + // At least 200 bytes should be saved. + EXPECT_LT(encoded_coni.size, encoded_meta.size - 200); + + const testutil::AvifImagePtr decoded_meta = + testutil::Decode(encoded_meta.data, encoded_meta.size); + ASSERT_NE(decoded_meta, nullptr); + + // Only the container changed. The pixels, features and metadata should be + // identical. + EXPECT_TRUE( + testutil::AreImagesEqual(*decoded_meta.get(), *decoded_coni.get())); +} + +INSTANTIATE_TEST_SUITE_P(OnePixel, AvifCondensedImageBoxTest, + Combine(/*width=*/Values(1), /*height=*/Values(1), + /*depth=*/Values(8), + Values(AVIF_PIXEL_FORMAT_YUV444), + Values(AVIF_PLANES_YUV, AVIF_PLANES_ALL), + Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL), + /*create_icc=*/Values(false, true), + /*create_exif=*/Values(false, true), + /*create_xmp=*/Values(false, true), + /*create_clli=*/Values(false), + Values(AVIF_TRANSFORM_NONE))); + +INSTANTIATE_TEST_SUITE_P( + DepthsSubsamplings, AvifCondensedImageBoxTest, + Combine(/*width=*/Values(12), /*height=*/Values(34), + /*depth=*/Values(8, 10, 12), + Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV420, + AVIF_PIXEL_FORMAT_YUV400), + Values(AVIF_PLANES_ALL), Values(AVIF_RANGE_FULL), + /*create_icc=*/Values(false), /*create_exif=*/Values(false), + /*create_xmp=*/Values(false), /*create_clli=*/Values(false), + Values(AVIF_TRANSFORM_NONE))); + +INSTANTIATE_TEST_SUITE_P( + Dimensions, AvifCondensedImageBoxTest, + Combine(/*width=*/Values(127), /*height=*/Values(200), /*depth=*/Values(8), + Values(AVIF_PIXEL_FORMAT_YUV444), Values(AVIF_PLANES_ALL), + Values(AVIF_RANGE_FULL), /*create_icc=*/Values(true), + /*create_exif=*/Values(true), /*create_xmp=*/Values(true), + /*create_clli=*/Values(false), Values(AVIF_TRANSFORM_NONE))); + +INSTANTIATE_TEST_SUITE_P( + ExtendedMetaBox, AvifCondensedImageBoxTest, + Combine(/*width=*/Values(16), /*height=*/Values(24), /*depth=*/Values(8), + Values(AVIF_PIXEL_FORMAT_YUV444), Values(AVIF_PLANES_ALL), + Values(AVIF_RANGE_FULL), /*create_icc=*/Values(true), + /*create_exif=*/Values(true), /*create_xmp=*/Values(true), + /*create_clli=*/Values(false, true), + Values(AVIF_TRANSFORM_NONE, + AVIF_TRANSFORM_PASP | AVIF_TRANSFORM_CLAP | + AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR))); + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace libavif