diff --git a/src/codec_avm.c b/src/codec_avm.c index cff2f3d085..c7a29a0bf2 100644 --- a/src/codec_avm.c +++ b/src/codec_avm.c @@ -144,6 +144,8 @@ static avifBool avmCodecGetNextImage(struct avifCodec * codec, return AVIF_FALSE; } if (codec->internal->image->monochrome) { + // avm does not handle monochrome as of research-v8.0.0. + // This should not happen. yuvFormat = AVIF_PIXEL_FORMAT_YUV400; } @@ -645,7 +647,7 @@ static avifResult avmCodecEncodeImage(avifCodec * codec, cfg->g_threads = AVIF_MIN(encoder->maxThreads, 64); } - // avm does not handle monochrome as of research-v4.0.0. + // avm does not handle monochrome as of research-v8.0.0. // TODO(yguyon): Enable when fixed upstream codec->internal->monochromeEnabled = AVIF_FALSE; diff --git a/src/read.c b/src/read.c index 5c8b68ac55..5fe25b9519 100644 --- a/src/read.c +++ b/src/read.c @@ -1214,13 +1214,19 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item // This is a MinimizedImageBox ('mini'). if (item->miniBoxPixelFormat != avifCodecConfigurationBoxGetFormat(&configProp->u.av1C)) { - avifDiagnosticsPrintf(diag, - "Item ID %u format [%s] specified by MinimizedImageBox does not match %s property format [%s]", - item->id, - avifPixelFormatToString(item->miniBoxPixelFormat), - configPropName, - avifPixelFormatToString(avifCodecConfigurationBoxGetFormat(&configProp->u.av1C))); - return AVIF_RESULT_BMFF_PARSE_FAILED; + if (!memcmp(configPropName, "av2C", 4) && item->miniBoxPixelFormat == AVIF_PIXEL_FORMAT_YUV400 && + avifCodecConfigurationBoxGetFormat(&configProp->u.av1C) == AVIF_PIXEL_FORMAT_YUV420) { + // avm does not handle monochrome as of research-v8.0.0. + // 4:2:0 is used instead. + } else { + avifDiagnosticsPrintf(diag, + "Item ID %u format [%s] specified by MinimizedImageBox does not match %s property format [%s]", + item->id, + avifPixelFormatToString(item->miniBoxPixelFormat), + configPropName, + avifPixelFormatToString(avifCodecConfigurationBoxGetFormat(&configProp->u.av1C))); + return AVIF_RESULT_BMFF_PARSE_FAILED; + } } if (configProp->u.av1C.chromaSamplePosition == /*CSP_UNKNOWN=*/0) { @@ -3779,11 +3785,12 @@ static avifResult avifParseMinimizedImageBox(avifDecoderData * data, AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &codecConfigType[i], 8), AVIF_RESULT_BMFF_PARSE_FAILED); } #if defined(AVIF_CODEC_AVM) - if (!memcmp(infeType, "av02", 4) && !memcmp(codecConfigType, "av2C", 4)) { - return AVIF_RESULT_NOT_IMPLEMENTED; - } -#endif + AVIF_CHECKERR((!memcmp(infeType, "av01", 4) && !memcmp(codecConfigType, "av1C", 4)) || + (!memcmp(infeType, "av02", 4) && !memcmp(codecConfigType, "av2C", 4)), + AVIF_RESULT_BMFF_PARSE_FAILED); +#else AVIF_CHECKERR(!memcmp(infeType, "av01", 4) && !memcmp(codecConfigType, "av1C", 4), AVIF_RESULT_BMFF_PARSE_FAILED); +#endif } else { AVIF_CHECKERR(isAvifAccordingToMinorVersion, AVIF_RESULT_BMFF_PARSE_FAILED); memcpy(infeType, "av01", 4); @@ -6162,6 +6169,37 @@ static avifResult avifDecoderDecodeTiles(avifDecoder * decoder, uint32_t nextIma } } +#if defined(AVIF_CODEC_AVM) + avifDecoderItem * tileItem = NULL; + for (uint32_t itemIndex = 0; itemIndex < decoder->data->meta->items.count; ++itemIndex) { + avifDecoderItem * item = decoder->data->meta->items.item[itemIndex]; + if (avifDecoderItemShouldBeSkipped(item)) { + continue; + } + if (item->id == sample->itemID) { + tileItem = item; + break; + } + } + if (tileItem != NULL) { + const avifProperty * prop = avifPropertyArrayFind(&tileItem->properties, "pixi"); + // Match the decoded image format with the number of planes specified in 'pixi'. + if (prop != NULL && prop->u.pixi.planeCount == 1 && tile->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) { + // Codecs such as avm do not support monochrome so samples were encoded as 4:2:0. + // Ignore the UV planes at decoding. + tile->image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; + if (tile->image->imageOwnsYUVPlanes) { + avifFree(tile->image->yuvPlanes[AVIF_CHAN_U]); + avifFree(tile->image->yuvPlanes[AVIF_CHAN_V]); + } + tile->image->yuvPlanes[AVIF_CHAN_U] = NULL; + tile->image->yuvRowBytes[AVIF_CHAN_U] = 0; + tile->image->yuvPlanes[AVIF_CHAN_V] = NULL; + tile->image->yuvRowBytes[AVIF_CHAN_V] = 0; + } + } +#endif + ++info->decodedTileCount; const avifBool isGrid = (info->grid.rows > 0) && (info->grid.columns > 0); diff --git a/src/write.c b/src/write.c index 7b1afaf8c9..cf4e1c7932 100644 --- a/src/write.c +++ b/src/write.c @@ -2596,7 +2596,19 @@ static avifResult avifEncoderWriteMiniBox(avifEncoder * encoder, avifRWStream * const uint32_t orientationMinus1 = avifImageIrotImirToExifOrientation(image) - 1; - const avifBool hasExplicitCodecTypes = AVIF_FALSE; // 'av01' and 'av1C' known from 'avif' minor_version field of FileTypeBox. + uint8_t infeType[4]; + uint8_t codecConfigType[4]; + avifBool hasExplicitCodecTypes; + if (encoder->codecChoice == AVIF_CODEC_CHOICE_AVM) { + memcpy(infeType, "av02", 4); + memcpy(codecConfigType, "av2C", 4); // Same syntax as 'av1C'. + hasExplicitCodecTypes = AVIF_TRUE; + } else { + memcpy(infeType, "av01", 4); + memcpy(codecConfigType, "av1C", 4); + // 'av01' and 'av1C' are implied by 'avif' minor_version field of FileTypeBox. No need to write them. + hasExplicitCodecTypes = AVIF_FALSE; + } uint32_t smallDimensionsFlag = image->width <= (1 << 7) && image->height <= (1 << 7); const uint32_t codecConfigSize = 4; // 'av1C' always uses 4 bytes. @@ -2676,8 +2688,13 @@ static avifResult avifEncoderWriteMiniBox(avifEncoder * encoder, avifRWStream * if (hasExplicitCodecTypes) { // bit(32) infe_type; + for (int i = 0; i < 4; ++i) { + AVIF_CHECKRES(avifRWStreamWriteBits(s, infeType[i], 8)); + } // bit(32) codec_config_type; - AVIF_ASSERT_OR_RETURN(AVIF_FALSE); + for (int i = 0; i < 4; ++i) { + AVIF_CHECKRES(avifRWStreamWriteBits(s, codecConfigType[i], 8)); + } } // High Dynamic Range properties diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index be9a2162e8..ec4ca2d53c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -328,6 +328,9 @@ endif() if(AVIF_CODEC_AVM_ENABLED) if(AVIF_ENABLE_GTEST) add_avif_gtest(avifavmtest) + if(AVIF_ENABLE_EXPERIMENTAL_MINI) + add_avif_gtest(avifavmminitest) + endif() endif() if(AVIF_BUILD_APPS) diff --git a/tests/gtest/avifavmminitest.cc b/tests/gtest/avifavmminitest.cc new file mode 100644 index 0000000000..5972c53a1c --- /dev/null +++ b/tests/gtest/avifavmminitest.cc @@ -0,0 +1,126 @@ +// Copyright 2024 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 avif { +namespace { + +class AvmMiniTest : public testing::TestWithParam< + std::tuple> {}; + +TEST_P(AvmMiniTest, EncodeDecode) { + 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 bool alpha = std::get<4>(GetParam()); + + ImagePtr image = testutil::CreateImage( + width, height, depth, format, alpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV); + ASSERT_NE(image, nullptr); + testutil::FillImageGradient(image.get()); + + EncoderPtr encoder(avifEncoderCreate()); + ASSERT_NE(encoder, nullptr); + encoder->codecChoice = AVIF_CODEC_CHOICE_AVM; + encoder->headerFormat = AVIF_HEADER_REDUCED; + testutil::AvifRwData encoded; + ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded), + AVIF_RESULT_OK); + + ImagePtr decoded(avifImageCreateEmpty()); + ASSERT_NE(decoded, nullptr); + DecoderPtr decoder(avifDecoderCreate()); + ASSERT_NE(decoder, nullptr); + // No need to set AVIF_CODEC_CHOICE_AVM. The decoder should recognize AV2. + ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data, + encoded.size), + AVIF_RESULT_OK); + + // Verify that the input and decoded images are close. + EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0); + + // Forcing an AV1 decoding codec should fail. + for (avifCodecChoice av1_codec : + {AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_CHOICE_DAV1D, + AVIF_CODEC_CHOICE_LIBGAV1}) { + decoder->codecChoice = av1_codec; + // An error is expected because av1_codec is not enabled or because we are + // trying to decode an AV2 file with an AV1 codec. + ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data, + encoded.size), + avifCodecName(av1_codec, AVIF_CODEC_FLAG_CAN_DECODE) + ? AVIF_RESULT_DECODE_COLOR_FAILED + : AVIF_RESULT_NO_CODEC_AVAILABLE); + } +} + +INSTANTIATE_TEST_SUITE_P(Basic, AvmMiniTest, + Combine(/*width=*/Values(12), /*height=*/Values(34), + /*depth=*/Values(8), + Values(AVIF_PIXEL_FORMAT_YUV400, + AVIF_PIXEL_FORMAT_YUV420, + AVIF_PIXEL_FORMAT_YUV444), + /*alpha=*/Values(false, true))); + +INSTANTIATE_TEST_SUITE_P(Tiny, AvmMiniTest, + Combine(/*width=*/Values(1), /*height=*/Values(1), + /*depth=*/Values(8), + Values(AVIF_PIXEL_FORMAT_YUV444), + /*alpha=*/Values(false))); + +// TODO(yguyon): Implement or fix in avm then test the following combinations. +INSTANTIATE_TEST_SUITE_P(DISABLED_Broken, AvmMiniTest, + Combine(/*width=*/Values(1), /*height=*/Values(34), + /*depth=*/Values(8, 10, 12), + Values(AVIF_PIXEL_FORMAT_YUV400, + AVIF_PIXEL_FORMAT_YUV420, + AVIF_PIXEL_FORMAT_YUV444), + /*alpha=*/Values(true))); + +TEST(AvmMiniTest, Av1StillWorksWhenAvmIsEnabled) { + if (!testutil::Av1EncoderAvailable() || !testutil::Av1DecoderAvailable()) { + GTEST_SKIP() << "AV1 codec unavailable, skip test."; + } + // avm is the only AV2 codec, so the default codec will be an AV1 one. + + ImagePtr image = + testutil::CreateImage(/*width=*/64, /*height=*/64, /*depth=*/8, + AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_ALL); + ASSERT_NE(image, nullptr); + testutil::FillImageGradient(image.get()); + + EncoderPtr encoder(avifEncoderCreate()); + ASSERT_NE(encoder, nullptr); + encoder->headerFormat = AVIF_HEADER_REDUCED; + testutil::AvifRwData encoded; + ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded), + AVIF_RESULT_OK); + + ImagePtr decoded(avifImageCreateEmpty()); + ASSERT_NE(decoded, nullptr); + DecoderPtr decoder(avifDecoderCreate()); + ASSERT_NE(decoder, nullptr); + ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data, + encoded.size), + AVIF_RESULT_OK); + + // Verify that the input and decoded images are close. + EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0); + + // Forcing an AV2 decoding codec should fail. + decoder->codecChoice = AVIF_CODEC_CHOICE_AVM; + ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data, + encoded.size), + AVIF_RESULT_DECODE_COLOR_FAILED); +} + +} // namespace +} // namespace avif diff --git a/tests/gtest/avifavmtest.cc b/tests/gtest/avifavmtest.cc index e3cb5f963c..6597e33d1f 100644 --- a/tests/gtest/avifavmtest.cc +++ b/tests/gtest/avifavmtest.cc @@ -64,9 +64,10 @@ TEST_P(AvmTest, EncodeDecode) { INSTANTIATE_TEST_SUITE_P(Basic, AvmTest, Combine(/*width=*/Values(12), /*height=*/Values(34), /*depth=*/Values(8), - Values(AVIF_PIXEL_FORMAT_YUV420, + Values(AVIF_PIXEL_FORMAT_YUV400, + AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV444), - /*alpha=*/Values(true))); + /*alpha=*/Values(false, true))); INSTANTIATE_TEST_SUITE_P(Tiny, AvmTest, Combine(/*width=*/Values(1), /*height=*/Values(1),