diff --git a/include/avif/avif.h b/include/avif/avif.h index f926149186..d4419f276c 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -1288,6 +1288,9 @@ typedef struct avifScalingMode // a combination of settings are tweaked to simulate this speed range. // * Extra layer count: [0 - (AVIF_MAX_AV1_LAYER_COUNT-1)]. Non-zero value indicates a layered // (progressive) image. +// * Width and height: width and height of encoded image. Default value 0 means infer from first frame. +// For grid image, this is the size of one cell. Value must not be smaller than the largest frame +// to be encoded. // * Some encoder settings can be changed after encoding starts. Changes will take effect in the next // call to avifEncoderAddImage(). typedef struct avifEncoder @@ -1334,6 +1337,9 @@ typedef struct avifEncoder // Defaults to AVIF_HEADER_FULL avifHeaderFormat headerFormat; + uint32_t width; + uint32_t height; + #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) int qualityGainMap; // changeable encoder setting #endif diff --git a/include/avif/internal.h b/include/avif/internal.h index 64b9696e45..5933ba6737 100644 --- a/include/avif/internal.h +++ b/include/avif/internal.h @@ -400,7 +400,7 @@ typedef avifBool (*avifCodecGetNextImageFunc)(struct avifCodec * codec, typedef avifResult (*avifCodecEncodeImageFunc)(struct avifCodec * codec, avifEncoder * encoder, const avifImage * image, - avifBool alpha, + avifItemCategory category, int tileRowsLog2, int tileColsLog2, int quantizer, diff --git a/src/codec_aom.c b/src/codec_aom.c index 6bf2ea7ccc..87b447a447 100644 --- a/src/codec_aom.c +++ b/src/codec_aom.c @@ -547,7 +547,7 @@ static avifBool aomCodecEncodeFinish(avifCodec * codec, avifCodecEncodeOutput * static avifResult aomCodecEncodeImage(avifCodec * codec, avifEncoder * encoder, const avifImage * image, - avifBool alpha, + avifItemCategory category, int tileRowsLog2, int tileColsLog2, int quantizer, @@ -558,6 +558,13 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, { struct aom_codec_enc_cfg * cfg = &codec->internal->cfg; avifBool quantizerUpdated = AVIF_FALSE; + avifBool alpha = category == AVIF_ITEM_ALPHA; +#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + avifBool gainMap = category == AVIF_ITEM_GAIN_MAP; +#else + avifBool gainMap = AVIF_FALSE; +#endif + const int aomVersion = aom_codec_version(); // For encoder->scalingMode.horizontal and encoder->scalingMode.vertical to take effect in AOM // encoder, config should be applied for each frame, so we don't care about changes on these @@ -601,7 +608,6 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, // aom_codec.h says: aom_codec_version() == (major<<16 | minor<<8 | patch) static const int aomVersion_2_0_0 = (2 << 16); - const int aomVersion = aom_codec_version(); if ((aomVersion < aomVersion_2_0_0) && (image->depth > 8)) { // Due to a known issue with libaom v1.0.0-errata1-avif, 10bpc and // 12bpc image encodes will call the wrong variant of @@ -697,6 +703,12 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, cfg->g_w = image->width; cfg->g_h = image->height; + // gain map has its own dimension + if (!gainMap) { + cfg->g_forced_max_frame_width = encoder->width; + cfg->g_forced_max_frame_height = encoder->height; + } + // Detect the libaom v3.6.0 bug described in // https://crbug.com/aomedia/2871#c12. See the changes to // av1/encoder/encoder.c in @@ -747,6 +759,11 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, if (disableLaggedOutput) { cfg->g_lag_in_frames = 0; } + if ((encoder->width || encoder->height) && (cfg->g_lag_in_frames > 1)) { + // libaom does not allow changing frame dimension if + // g_lag_in_frames > 1. + cfg->g_lag_in_frames = 1; + } if (encoder->maxThreads > 1) { cfg->g_threads = encoder->maxThreads; } @@ -877,9 +894,33 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, } } else { avifBool dimensionsChanged = AVIF_FALSE; - if ((cfg->g_w != image->width) || (cfg->g_h != image->height)) { - // We are not ready for dimension change for now. - return AVIF_RESULT_NOT_IMPLEMENTED; + if (!gainMap && ((cfg->g_w != image->width) || (cfg->g_h != image->height))) { + static const int aomVersion_3_6_0 = (3 << 16) + (6 << 8); + if (aomVersion < aomVersion_3_6_0) { + // Due to a bug in libaom before v3.6.0 encoding 10bpc and 12bpc images + // with changing dimension will crash the encoder. + if (image->depth > 8) { + avifDiagnosticsPrintf(codec->diag, + "Detected libaom bug with high bitdepth images and changing dimension. Upgrade to libaom v3.6.0 or later."); + return AVIF_RESULT_INCOMPATIBLE_IMAGE; + } + + // There exists a bug in libaom's buffer allocation logic before v3.6.0 + // where it allocates buffers based on g_w and g_h of first frame instead of + // g_forced_max_frame_width and g_forced_max_frame_height, so encoding frames + // of increasing size will crash the encoder. + // + // This check is stricter than it needs to be, but we don't track the size of + // first image but only the last successful encoded one. + if ((cfg->g_w < image->width) || (cfg->g_h < image->height)) { + avifDiagnosticsPrintf(codec->diag, + "Detected libaom bug with increasing encoding dimension. Upgrade to libaom v3.6.0 or later."); + return AVIF_RESULT_INCOMPATIBLE_IMAGE; + } + } + cfg->g_w = image->width; + cfg->g_h = image->height; + dimensionsChanged = AVIF_TRUE; } if (alpha) { if (encoderChanges & (AVIF_ENCODER_CHANGE_MIN_QUANTIZER_ALPHA | AVIF_ENCODER_CHANGE_MAX_QUANTIZER_ALPHA)) { diff --git a/src/codec_avm.c b/src/codec_avm.c index 8f41bb0d44..bd3f5f3ae5 100644 --- a/src/codec_avm.c +++ b/src/codec_avm.c @@ -530,7 +530,7 @@ static avifBool avmCodecEncodeFinish(avifCodec * codec, avifCodecEncodeOutput * static avifResult avmCodecEncodeImage(avifCodec * codec, avifEncoder * encoder, const avifImage * image, - avifBool alpha, + avifItemCategory category, int tileRowsLog2, int tileColsLog2, int quantizer, @@ -541,6 +541,7 @@ static avifResult avmCodecEncodeImage(avifCodec * codec, { struct aom_codec_enc_cfg * cfg = &codec->internal->cfg; avifBool quantizerUpdated = AVIF_FALSE; + avifBool alpha = category == AVIF_ITEM_ALPHA; // For encoder->scalingMode.horizontal and encoder->scalingMode.vertical to take effect in AV2 // encoder, config should be applied for each frame, so we don't care about changes on these diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c index 91dbad211a..73a102dc1e 100644 --- a/src/codec_rav1e.c +++ b/src/codec_rav1e.c @@ -52,7 +52,7 @@ static avifBool rav1eSupports400(void) static avifResult rav1eCodecEncodeImage(avifCodec * codec, avifEncoder * encoder, const avifImage * image, - avifBool alpha, + avifItemCategory category, int tileRowsLog2, int tileColsLog2, int quantizer, @@ -61,6 +61,8 @@ static avifResult rav1eCodecEncodeImage(avifCodec * codec, uint32_t addImageFlags, avifCodecEncodeOutput * output) { + avifBool alpha = category == AVIF_ITEM_ALPHA; + // rav1e does not support changing encoder settings. if (encoderChanges) { return AVIF_RESULT_NOT_IMPLEMENTED; @@ -82,6 +84,11 @@ static avifResult rav1eCodecEncodeImage(avifCodec * codec, // rav1e does not support disabling lagged output. See https://github.com/xiph/rav1e/issues/2267. Ignore this setting. (void)disableLaggedOutput; + // rav1e does not support overriding maximum frame width/height in sequence header + if (encoder->width || encoder->height) { + return AVIF_RESULT_NOT_IMPLEMENTED; + } + avifResult result = AVIF_RESULT_UNKNOWN_ERROR; RaConfig * rav1eConfig = NULL; diff --git a/src/codec_svt.c b/src/codec_svt.c index 059adc14b2..5e745b764b 100644 --- a/src/codec_svt.c +++ b/src/codec_svt.c @@ -45,7 +45,7 @@ static avifResult dequeue_frame(avifCodec * codec, avifCodecEncodeOutput * outpu static avifResult svtCodecEncodeImage(avifCodec * codec, avifEncoder * encoder, const avifImage * image, - avifBool alpha, + avifItemCategory category, int tileRowsLog2, int tileColsLog2, int quantizer, @@ -54,6 +54,8 @@ static avifResult svtCodecEncodeImage(avifCodec * codec, uint32_t addImageFlags, avifCodecEncodeOutput * output) { + avifBool alpha = category == AVIF_ITEM_ALPHA; + // SVT-AV1 does not support changing encoder settings. if (encoderChanges) { return AVIF_RESULT_NOT_IMPLEMENTED; @@ -128,6 +130,8 @@ static avifResult svtCodecEncodeImage(avifCodec * codec, svt_config->source_width = image->width; svt_config->source_height = image->height; + svt_config->forced_max_frame_width = encoder->width; + svt_config->forced_max_frame_height = encoder->height; svt_config->logical_processors = encoder->maxThreads; svt_config->enable_adaptive_quantization = 2; // disable 2-pass diff --git a/src/write.c b/src/write.c index 0a9cdec61c..02b67d40f6 100644 --- a/src/write.c +++ b/src/write.c @@ -446,6 +446,8 @@ avifEncoder * avifEncoderCreate(void) encoder->maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY; encoder->minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY; encoder->maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY; + encoder->width = 0; + encoder->height = 0; encoder->tileRowsLog2 = 0; encoder->tileColsLog2 = 0; encoder->autoTiling = AVIF_FALSE; @@ -490,6 +492,8 @@ static void avifEncoderBackupSettings(avifEncoder * encoder) lastEncoder->timescale = encoder->timescale; lastEncoder->repetitionCount = encoder->repetitionCount; lastEncoder->extraLayerCount = encoder->extraLayerCount; + lastEncoder->width = encoder->width; + lastEncoder->height = encoder->height; lastEncoder->minQuantizer = encoder->minQuantizer; lastEncoder->maxQuantizer = encoder->maxQuantizer; lastEncoder->minQuantizerAlpha = encoder->minQuantizerAlpha; @@ -517,7 +521,8 @@ static avifBool avifEncoderDetectChanges(const avifEncoder * encoder, avifEncode if ((lastEncoder->codecChoice != encoder->codecChoice) || (lastEncoder->maxThreads != encoder->maxThreads) || (lastEncoder->speed != encoder->speed) || (lastEncoder->keyframeInterval != encoder->keyframeInterval) || (lastEncoder->timescale != encoder->timescale) || (lastEncoder->repetitionCount != encoder->repetitionCount) || - (lastEncoder->extraLayerCount != encoder->extraLayerCount)) { + (lastEncoder->extraLayerCount != encoder->extraLayerCount) || (lastEncoder->width != encoder->width) || + (lastEncoder->height != encoder->height)) { return AVIF_FALSE; } @@ -1277,6 +1282,10 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, return AVIF_RESULT_NO_CONTENT; } + if ((encoder->width && (encoder->width < firstCell->width)) || (encoder->height && (encoder->height < firstCell->height))) { + return AVIF_RESULT_INCOMPATIBLE_IMAGE; + } + AVIF_CHECKRES(avifValidateGrid(gridCols, gridRows, cellImages, /*validateGainMap=*/AVIF_FALSE, &encoder->diag)); #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) @@ -1592,7 +1601,7 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder, avifResult encodeResult = item->codec->encodeImage(item->codec, encoder, cellImage, - item->itemCategory == AVIF_ITEM_ALPHA, + item->itemCategory, encoder->data->tileRowsLog2, encoder->data->tileColsLog2, quantizer, @@ -2158,6 +2167,9 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) #endif // AVIF_ENABLE_EXPERIMENTAL_AVIR const avifImage * imageMetadata = encoder->data->imageMetadata; + const uint32_t cellWidth = encoder->width ? encoder->width : imageMetadata->width; + const uint32_t cellHeight = encoder->height ? encoder->height : imageMetadata->height; + // The epoch for creation_time and modification_time is midnight, Jan. 1, // 1904, in UTC time. Add the number of seconds between that epoch and the // Unix epoch. @@ -2398,14 +2410,16 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) } const avifImage * itemMetadata = imageMetadata; + uint32_t imageWidth = cellWidth; + uint32_t imageHeight = cellHeight; #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) if (item->itemCategory == AVIF_ITEM_GAIN_MAP) { itemMetadata = itemMetadata->gainMap.image; assert(itemMetadata); + imageWidth = itemMetadata->width; + imageHeight = itemMetadata->height; } #endif - uint32_t imageWidth = itemMetadata->width; - uint32_t imageHeight = itemMetadata->height; if (isGrid) { imageWidth = item->gridWidth; imageHeight = item->gridHeight; @@ -2646,8 +2660,8 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) AVIF_CHECKRES(avifRWStreamWriteU16(&s, 0)); // template int(16) volume = {if track_is_audio 0x0100 else 0}; AVIF_CHECKRES(avifRWStreamWriteU16(&s, 0)); // const unsigned int(16) reserved = 0; AVIF_CHECKRES(avifRWStreamWrite(&s, unityMatrix, sizeof(unityMatrix))); // template int(32)[9] matrix= // { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }; - AVIF_CHECKRES(avifRWStreamWriteU32(&s, imageMetadata->width << 16)); // unsigned int(32) width; - AVIF_CHECKRES(avifRWStreamWriteU32(&s, imageMetadata->height << 16)); // unsigned int(32) height; + AVIF_CHECKRES(avifRWStreamWriteU32(&s, cellWidth << 16)); // unsigned int(32) width; + AVIF_CHECKRES(avifRWStreamWriteU32(&s, cellHeight << 16)); // unsigned int(32) height; avifRWStreamFinishBox(&s, tkhd); if (item->irefToID != 0) { @@ -2729,16 +2743,16 @@ avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) AVIF_CHECKRES(avifRWStreamWriteU32(&s, 1)); // unsigned int(32) entry_count; avifBoxMarker imageItem; AVIF_CHECKRES(avifRWStreamWriteBox(&s, encoder->data->imageItemType, AVIF_BOX_SIZE_TBD, &imageItem)); - AVIF_CHECKRES(avifRWStreamWriteZeros(&s, 6)); // const unsigned int(8)[6] reserved = 0; - AVIF_CHECKRES(avifRWStreamWriteU16(&s, 1)); // unsigned int(16) data_reference_index; - AVIF_CHECKRES(avifRWStreamWriteU16(&s, 0)); // unsigned int(16) pre_defined = 0; - AVIF_CHECKRES(avifRWStreamWriteU16(&s, 0)); // const unsigned int(16) reserved = 0; - AVIF_CHECKRES(avifRWStreamWriteZeros(&s, sizeof(uint32_t) * 3)); // unsigned int(32)[3] pre_defined = 0; - AVIF_CHECKRES(avifRWStreamWriteU16(&s, (uint16_t)imageMetadata->width)); // unsigned int(16) width; - AVIF_CHECKRES(avifRWStreamWriteU16(&s, (uint16_t)imageMetadata->height)); // unsigned int(16) height; - AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0x00480000)); // template unsigned int(32) horizresolution - AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0x00480000)); // template unsigned int(32) vertresolution - AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0)); // const unsigned int(32) reserved = 0; + AVIF_CHECKRES(avifRWStreamWriteZeros(&s, 6)); // const unsigned int(8)[6] reserved = 0; + AVIF_CHECKRES(avifRWStreamWriteU16(&s, 1)); // unsigned int(16) data_reference_index; + AVIF_CHECKRES(avifRWStreamWriteU16(&s, 0)); // unsigned int(16) pre_defined = 0; + AVIF_CHECKRES(avifRWStreamWriteU16(&s, 0)); // const unsigned int(16) reserved = 0; + AVIF_CHECKRES(avifRWStreamWriteZeros(&s, sizeof(uint32_t) * 3)); // unsigned int(32)[3] pre_defined = 0; + AVIF_CHECKRES(avifRWStreamWriteU16(&s, (uint16_t)cellWidth)); // unsigned int(16) width; + AVIF_CHECKRES(avifRWStreamWriteU16(&s, (uint16_t)cellHeight)); // unsigned int(16) height; + AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0x00480000)); // template unsigned int(32) horizresolution + AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0x00480000)); // template unsigned int(32) vertresolution + AVIF_CHECKRES(avifRWStreamWriteU32(&s, 0)); // const unsigned int(32) reserved = 0; AVIF_CHECKRES(avifRWStreamWriteU16(&s, 1)); // template unsigned int(16) frame_count = 1; AVIF_CHECKRES(avifRWStreamWriteChars(&s, "\012AOM Coding", 11)); // string[32] compressorname; AVIF_CHECKRES(avifRWStreamWriteZeros(&s, 32 - 11)); // diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d17076da55..edb9bc6ec0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -102,6 +102,11 @@ if(AVIF_ENABLE_GTEST) target_include_directories(avifbasictest PRIVATE ${GTEST_INCLUDE_DIRS}) add_test(NAME avifbasictest COMMAND avifbasictest) + add_executable(avifchangedimensiontest gtest/avifchangedimensiontest.cc) + target_link_libraries(avifchangedimensiontest aviftest_helpers ${GTEST_BOTH_LIBRARIES}) + target_include_directories(avifchangedimensiontest PRIVATE ${GTEST_INCLUDE_DIRS}) + add_test(NAME avifchangedimensiontest COMMAND avifchangedimensiontest) + add_executable(avifchangesettingtest gtest/avifchangesettingtest.cc) target_link_libraries(avifchangesettingtest aviftest_helpers ${GTEST_BOTH_LIBRARIES}) target_include_directories(avifchangesettingtest PRIVATE ${GTEST_INCLUDE_DIRS}) @@ -192,9 +197,9 @@ if(AVIF_ENABLE_GTEST) add_test(NAME avifpng16bittest COMMAND avifpng16bittest ${CMAKE_CURRENT_SOURCE_DIR}/data/) add_executable(avifprogressivetest gtest/avifprogressivetest.cc) - target_link_libraries(avifprogressivetest aviftest_helpers ${GTEST_BOTH_LIBRARIES}) + target_link_libraries(avifprogressivetest aviftest_helpers ${GTEST_LIBRARIES}) target_include_directories(avifprogressivetest PRIVATE ${GTEST_INCLUDE_DIRS}) - add_test(NAME avifprogressivetest COMMAND avifprogressivetest) + add_test(NAME avifprogressivetest COMMAND avifprogressivetest ${CMAKE_CURRENT_SOURCE_DIR}/data/) add_executable(avifreadimagetest gtest/avifreadimagetest.cc) target_link_libraries(avifreadimagetest aviftest_helpers ${GTEST_LIBRARIES}) diff --git a/tests/data/dog_1080p.jpg b/tests/data/dog_1080p.jpg new file mode 100644 index 0000000000..65b8d8aa36 Binary files /dev/null and b/tests/data/dog_1080p.jpg differ diff --git a/tests/data/dog_blur_1080p.jpg b/tests/data/dog_blur_1080p.jpg new file mode 100644 index 0000000000..bbb7d618e4 Binary files /dev/null and b/tests/data/dog_blur_1080p.jpg differ diff --git a/tests/data/dog_blur_540p.jpg b/tests/data/dog_blur_540p.jpg new file mode 100644 index 0000000000..da93c413a3 Binary files /dev/null and b/tests/data/dog_blur_540p.jpg differ diff --git a/tests/gtest/avifchangedimensiontest.cc b/tests/gtest/avifchangedimensiontest.cc new file mode 100644 index 0000000000..9ba15404b5 --- /dev/null +++ b/tests/gtest/avifchangedimensiontest.cc @@ -0,0 +1,174 @@ +// Copyright 2022 Yuan Tong. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#include +#include +#include +#include + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +using testing::Bool; +using testing::Combine; +using testing::Values; + +namespace libavif { +namespace { + +class ChangeDimensionTest + : public testing::TestWithParam> { +}; + +TEST_P(ChangeDimensionTest, EncodeDecode) { + if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) == + nullptr) { + GTEST_SKIP() << "Codec unavailable, skip test."; + } + + if (avifLibYUVVersion() == 0) { + GTEST_SKIP() << "libyuv unavailable, skip test."; + } + + const uint32_t size_first = std::get<0>(GetParam()); + const uint32_t size_second = std::get<1>(GetParam()); + const int speed = std::get<2>(GetParam()); + const int depth = std::get<3>(GetParam()); + const int maxThreads = std::get<4>(GetParam()); + const bool tiling = std::get<5>(GetParam()); + const std::string end_usage = std::get<6>(GetParam()); + const std::string tune = std::get<7>(GetParam()); + const bool denoise = std::get<8>(GetParam()); + + char versionBuffer[256]; + avifCodecVersions(versionBuffer); + std::string version_code = + std::find(versionBuffer, versionBuffer + 256, ':') + 1; + int version[3]{}; + for (int i = 0; i < 3; ++i) { + ASSERT_FALSE(version_code.empty()); + size_t parsed = 0; + version[i] = std::stoi(version_code, &parsed); + ASSERT_NE(parsed, size_t(0)); + if (i != 2) { + ASSERT_GT(version_code.size(), parsed + 1); + version_code = version_code.substr(parsed + 1); + } + } + + bool will_fail = (std::make_tuple(version[0], version[1], version[2]) < + std::make_tuple(3, 6, 0)) && + ((size_first < size_second) || (depth > 8)); + + uint32_t size_display = std::max(size_first, size_second); + + testutil::AvifImagePtr first = testutil::CreateImage( + int(size_first), int(size_first), depth, AVIF_PIXEL_FORMAT_YUV420, + AVIF_PLANES_YUV, AVIF_RANGE_FULL); + ASSERT_NE(first, nullptr); + testutil::FillImageGradient(first.get()); + + testutil::AvifImagePtr second = testutil::CreateImage( + int(size_second), int(size_second), depth, AVIF_PIXEL_FORMAT_YUV420, + AVIF_PLANES_YUV, AVIF_RANGE_FULL); + ASSERT_NE(second, nullptr); + testutil::FillImageGradient(second.get()); + + testutil::AvifRwData encodedAvif; + + // Encode + { + testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy); + ASSERT_NE(encoder, nullptr); + encoder->codecChoice = AVIF_CODEC_CHOICE_AOM; + encoder->speed = speed; + encoder->maxThreads = maxThreads; + encoder->timescale = 1; + encoder->minQuantizer = 20; + encoder->maxQuantizer = 40; + encoder->tileRowsLog2 = (tiling ? 1 : 0); + encoder->width = size_display; + encoder->height = size_display; + + avifEncoderSetCodecSpecificOption(encoder.get(), "end-usage", + end_usage.c_str()); + if (end_usage == "q") { + encoder->quality = 50; + } + avifEncoderSetCodecSpecificOption(encoder.get(), "tune", tune.c_str()); + if (denoise) { + avifEncoderSetCodecSpecificOption(encoder.get(), "denoise-noise-level", + "25"); + } + + ASSERT_EQ(avifEncoderAddImage(encoder.get(), first.get(), 1, 0), + AVIF_RESULT_OK); + + if (will_fail) { + ASSERT_EQ(avifEncoderAddImage(encoder.get(), second.get(), 1, 0), + AVIF_RESULT_INCOMPATIBLE_IMAGE); + return; + } + + ASSERT_EQ(avifEncoderAddImage(encoder.get(), second.get(), 1, 0), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder.get(), &encodedAvif), AVIF_RESULT_OK); + } + + // Decode + { + testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy); + ASSERT_NE(decoder, nullptr); + + avifDecoderSetIOMemory(decoder.get(), encodedAvif.data, encodedAvif.size); + ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); + ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); + // libavif scales frames automatically. + ASSERT_EQ(decoder->image->width, size_display); + ASSERT_EQ(decoder->image->height, size_display); + ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); + ASSERT_EQ(decoder->image->width, size_display); + ASSERT_EQ(decoder->image->height, size_display); + } +} + +INSTANTIATE_TEST_SUITE_P(AOMDecreasing, ChangeDimensionTest, + Combine(/*size_first*/ Values(128), + /*size_second*/ Values(64), + /*speed=*/Values(6, 10), + /*depth=*/Values(8, 10), + /*maxThreads*/ Values(1), + /*tiling*/ Bool(), + /*end_usage=*/Values("q", "cbr"), + /*tune=*/Values("ssim", "psnr"), + /*denoise=*/Bool())); + +INSTANTIATE_TEST_SUITE_P(AOMIncreasing, ChangeDimensionTest, + Combine(/*size_first*/ Values(64), + /*size_second*/ Values(128), + /*speed=*/Values(6, 10), + /*depth=*/Values(8, 10), + /*maxThreads*/ Values(1), + /*tiling*/ Bool(), + /*end_usage=*/Values("q", "cbr"), + /*tune=*/Values("ssim", "psnr"), + /*denoise=*/Bool())); + +INSTANTIATE_TEST_SUITE_P(AOMIncreasingMultiThread, ChangeDimensionTest, + Combine(/*size_first*/ Values(256), + /*size_second*/ Values(512), + /*speed=*/Values(6, 10), + /*depth=*/Values(8, 10), + /*maxThreads*/ Values(8), + /*tiling*/ Values(true), + /*end_usage=*/Values("q"), + /*tune=*/Values("ssim"), + /*denoise=*/Values(true))); + +} // namespace +} // namespace libavif diff --git a/tests/gtest/avifprogressivetest.cc b/tests/gtest/avifprogressivetest.cc index 5d72c360b6..62ff6d2bf1 100644 --- a/tests/gtest/avifprogressivetest.cc +++ b/tests/gtest/avifprogressivetest.cc @@ -2,12 +2,18 @@ // SPDX-License-Identifier: BSD-2-Clause #include "avif/avif.h" +#include "avifjpeg.h" #include "aviftest_helpers.h" #include "gtest/gtest.h" namespace libavif { namespace { +//------------------------------------------------------------------------------ + +// Used to pass the data folder path to the GoogleTest suites. +const char* data_path = nullptr; + class ProgressiveTest : public testing::Test { protected: static constexpr uint32_t kImageSize = 256; @@ -169,5 +175,121 @@ TEST_F(ProgressiveTest, TooFewLayers) { AVIF_RESULT_INVALID_ARGUMENT); } +TEST_F(ProgressiveTest, DimensionChangeLargeImageMultiThread) { + encoder_->speed = 6; + encoder_->maxThreads = 2; + encoder_->extraLayerCount = 1; + + image_ = testutil::CreateImage(1920, 1080, 8, AVIF_PIXEL_FORMAT_YUV420, + AVIF_PLANES_YUV, AVIF_RANGE_FULL); + + encoder_->scalingMode = {{1, 2}, {1, 2}}; + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + encoder_->scalingMode = {{1, 1}, {1, 1}}; + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(1920, 1080); +} + +TEST_F(ProgressiveTest, DimensionChangeLargeImageSlowSpeedDifferentImage) { + encoder_->speed = 2; + encoder_->maxThreads = 1; + encoder_->extraLayerCount = 1; + + auto layer1 = testutil::ReadImage( + data_path, "dog_blur_1080p.jpg", AVIF_PIXEL_FORMAT_YUV420, 8, + AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, false, false, false); + auto layer2 = testutil::ReadImage( + data_path, "dog_1080p.jpg", AVIF_PIXEL_FORMAT_YUV420, 8, + AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, false, false, false); + + encoder_->scalingMode = {{1, 2}, {1, 2}}; + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), layer1.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + encoder_->scalingMode = {{1, 1}, {1, 1}}; + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), layer2.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(1920, 1080); +} + +TEST_F(ProgressiveTest, DimensionChangeExternalLargeImageMultiThread) { + encoder_->speed = 3; + encoder_->maxThreads = 2; + encoder_->extraLayerCount = 1; + encoder_->width = 1920; + encoder_->height = 1080; + + image_ = testutil::CreateImage(960, 540, 8, AVIF_PIXEL_FORMAT_YUV420, + AVIF_PLANES_YUV, AVIF_RANGE_FULL); + testutil::AvifImagePtr image2 = + testutil::CreateImage(1920, 1080, 8, AVIF_PIXEL_FORMAT_YUV420, + AVIF_PLANES_YUV, AVIF_RANGE_FULL); + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image2.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(1920, 1080); +} + +TEST_F(ProgressiveTest, + DimensionChangeExternalLargeImageSlowSpeedDifferentImage) { + encoder_->speed = 2; + encoder_->maxThreads = 1; + encoder_->extraLayerCount = 1; + encoder_->width = 1920; + encoder_->height = 1080; + + auto layer1 = testutil::ReadImage( + data_path, "dog_blur_540p.jpg", AVIF_PIXEL_FORMAT_YUV420, 8, + AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, false, false, false); + auto layer2 = testutil::ReadImage( + data_path, "dog_1080p.jpg", AVIF_PIXEL_FORMAT_YUV420, 8, + AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, false, false, false); + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), layer1.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderAddImage(encoder_.get(), layer2.get(), 1, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_OK); + + ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK); + + TestDecode(1920, 1080); +} + } // namespace } // namespace libavif + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + if (argc != 2) { + std::cerr << "There must be exactly one argument containing the path to " + "the test data folder" + << std::endl; + return 1; + } + libavif::data_path = argv[1]; + return RUN_ALL_TESTS(); +}