diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa0dda1f1..8707067844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The changes are relative to the previous release, unless the baseline is specifi ## [Unreleased] +### Added since 1.4.2 + +* avifenc: add --ignore-alpha flag to discard alpha channel on encode + ## [1.4.2] - 2026-05-26 ### Added since 1.4.1 diff --git a/apps/avifenc.c b/apps/avifenc.c index 4d6106e0a7..e169839dbc 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -59,6 +59,7 @@ typedef struct avifBool ignoreExif; avifBool ignoreXMP; avifBool ignoreColorProfile; + avifBool ignoreAlpha; // These settings are only relevant when compiled with AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION. avifBool qualityGainMapIsConstrained; // true if qualityGainMap explicitly set by the user @@ -256,6 +257,7 @@ static void syntaxLong(void) printf(" --ignore-exif : If the input file contains embedded Exif metadata, ignore it (no-op if absent)\n"); printf(" --ignore-xmp : If the input file contains embedded XMP metadata, ignore it (no-op if absent)\n"); printf(" --ignore-profile,--ignore-icc : If the input file contains an embedded color profile, ignore it (no-op if absent)\n"); + printf(" --ignore-alpha : If the input file contains an alpha channel, ignore it (no-op if absent)\n"); #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) printf(" --ignore-gain-map : If the input file contains an embedded gain map, ignore it (no-op if absent)\n"); printf(" --qgain-map Q : Quality for the gain map in %d..%d where %d is lossless\n", @@ -527,6 +529,7 @@ static avifBool avifInputReadImage(avifInput * input, avifBool ignoreExif, avifBool ignoreXMP, avifBool allowChangingCicp, + avifBool ignoreAlpha, avifBool ignoreGainMap, avifImage * image, const avifInputFileSettings ** settings, @@ -622,6 +625,7 @@ static avifBool avifInputReadImage(avifInput * input, ignoreColorProfile, ignoreExif, ignoreXMP, + ignoreAlpha, ignoreGainMap, UINT32_MAX, dstImage, @@ -677,6 +681,7 @@ static avifBool avifInputReadImage(avifInput * input, ignoreExif, ignoreXMP, allowChangingCicp, + ignoreAlpha, ignoreGainMap, image, settings, @@ -938,6 +943,7 @@ static avifBool avifEncodeRestOfImageSequence(avifEncoder * encoder, /*ignoreExif=*/AVIF_TRUE, /*ignoreXMP=*/AVIF_TRUE, /*allowChangingCicp=*/AVIF_FALSE, + settings->ignoreAlpha, /*ignoreGainMap=*/AVIF_TRUE, nextImage, &nextSettings, @@ -1044,6 +1050,7 @@ static avifBool avifEncodeRestOfLayeredImage(avifEncoder * encoder, /*ignoreExif=*/AVIF_TRUE, /*ignoreXMP=*/AVIF_TRUE, !settings->cicpExplicitlySet, + settings->ignoreAlpha, /*ignoreGainMap=*/AVIF_TRUE, nextImage, &nextSettings, @@ -1443,6 +1450,7 @@ int main(int argc, char * argv[]) settings.ignoreExif = AVIF_FALSE; settings.ignoreXMP = AVIF_FALSE; settings.ignoreColorProfile = AVIF_FALSE; + settings.ignoreAlpha = AVIF_FALSE; settings.ignoreGainMap = AVIF_FALSE; settings.cicpExplicitlySet = AVIF_FALSE; settings.inputFormat = AVIF_APP_FILE_FORMAT_UNKNOWN; @@ -1929,6 +1937,8 @@ int main(int argc, char * argv[]) settings.ignoreXMP = AVIF_TRUE; } else if (!strcmp(arg, "--ignore-profile") || !strcmp(arg, "--ignore-icc")) { settings.ignoreColorProfile = AVIF_TRUE; + } else if (!strcmp(arg, "--ignore-alpha")) { + settings.ignoreAlpha = AVIF_TRUE; #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) } else if (!strcmp(arg, "--ignore-gain-map")) { settings.ignoreGainMap = AVIF_TRUE; @@ -2343,6 +2353,7 @@ int main(int argc, char * argv[]) settings.ignoreExif, settings.ignoreXMP, /*allowChangingCicp=*/!settings.cicpExplicitlySet, + settings.ignoreAlpha, ignoreGainMap, image, /*settings=*/NULL, // Must use the setting for first input @@ -2588,6 +2599,7 @@ int main(int argc, char * argv[]) /*ignoreExif=*/AVIF_TRUE, /*ignoreXMP=*/AVIF_TRUE, /*allowChangingCicp=*/AVIF_FALSE, + settings.ignoreAlpha, settings.ignoreGainMap, cellImage, /*settings=*/NULL, diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc index 2d1b1e8fc2..c127d06455 100644 --- a/apps/avifgainmaputil/imageio.cc +++ b/apps/avifgainmaputil/imageio.cc @@ -238,8 +238,8 @@ avifResult ReadImage(avifImage* image, const std::string& input_filename, input_filename.c_str(), AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */, requested_format, static_cast(requested_depth), AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, ignore_profile, - /*ignoreExif=*/false, /*ignoreXMP=*/false, ignore_gain_map, - AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image, + /*ignoreExif=*/false, /*ignoreXMP=*/false, /*ignoreAlpha=*/false, + ignore_gain_map, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image, /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr); if (file_format == AVIF_APP_FILE_FORMAT_UNKNOWN) { diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c index 6f35a02aac..794e8ef83d 100644 --- a/apps/shared/avifpng.c +++ b/apps/shared/avifpng.c @@ -277,6 +277,7 @@ static avifBool avifPNGReadImpl(FILE * f, avifBool ignoreColorProfile, avifBool ignoreExif, avifBool ignoreXMP, + avifBool ignoreAlpha, uint32_t imageSizeLimit, uint32_t * outPNGDepth) { @@ -524,6 +525,7 @@ static avifBool avifPNGReadImpl(FILE * f, } else if (numChannels == 3) { rgb.format = AVIF_RGB_FORMAT_RGB; } + rgb.ignoreAlpha = ignoreAlpha; if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) { fprintf(stderr, "Conversion to YUV failed: %s (out of memory)\n", inputFilename); goto cleanup; @@ -584,6 +586,7 @@ avifBool avifPNGRead(const char * inputFilename, avifBool ignoreColorProfile, avifBool ignoreExif, avifBool ignoreXMP, + avifBool ignoreAlpha, uint32_t imageSizeLimit, uint32_t * outPNGDepth) { @@ -608,6 +611,7 @@ avifBool avifPNGRead(const char * inputFilename, ignoreColorProfile, ignoreExif, ignoreXMP, + ignoreAlpha, imageSizeLimit, outPNGDepth); diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h index 5292a27476..b50d2b7c2b 100644 --- a/apps/shared/avifpng.h +++ b/apps/shared/avifpng.h @@ -19,6 +19,7 @@ avifBool avifPNGRead(const char * inputFilename, avifBool ignoreColorProfile, avifBool ignoreExif, avifBool ignoreXMP, + avifBool ignoreAlpha, uint32_t imageSizeLimit, uint32_t * outPNGDepth); avifBool avifPNGWrite(const char * outputFilename, diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c index 8578b29d0b..9cc6ead09b 100644 --- a/apps/shared/avifutil.c +++ b/apps/shared/avifutil.c @@ -317,6 +317,7 @@ avifAppFileFormat avifReadImage(const char * filename, avifBool ignoreColorProfile, avifBool ignoreExif, avifBool ignoreXMP, + avifBool ignoreAlpha, avifBool ignoreGainMap, uint32_t imageSizeLimit, avifImage * image, @@ -329,7 +330,7 @@ avifAppFileFormat avifReadImage(const char * filename, } if (inputFormat == AVIF_APP_FILE_FORMAT_Y4M) { - if (!y4mRead(filename, imageSizeLimit, image, sourceTiming, frameIter)) { + if (!y4mRead(filename, ignoreAlpha, imageSizeLimit, image, sourceTiming, frameIter)) { return AVIF_APP_FILE_FORMAT_UNKNOWN; } if (outDepth) { @@ -344,7 +345,7 @@ avifAppFileFormat avifReadImage(const char * filename, *outDepth = 8; } } else if (inputFormat == AVIF_APP_FILE_FORMAT_PNG) { - if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, chromaDownsampling, ignoreColorProfile, ignoreExif, ignoreXMP, imageSizeLimit, outDepth)) { + if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, chromaDownsampling, ignoreColorProfile, ignoreExif, ignoreXMP, ignoreAlpha, imageSizeLimit, outDepth)) { return AVIF_APP_FILE_FORMAT_UNKNOWN; } } else if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h index 6596cfc65b..192bbc7fa8 100644 --- a/apps/shared/avifutil.h +++ b/apps/shared/avifutil.h @@ -95,6 +95,8 @@ struct y4mFrameIterator; // In case of a y4m file, sourceTiming and frameIter can be set. // Returns the format of the file, or AVIF_APP_FILE_FORMAT_UNKNOWN in case of // error. +// 'ignoreAlpha' is only relevant for y4m or png files that have an alpha +// channel. Otherwise it has no effect. // 'ignoreGainMap' is only relevant for jpeg files that have a gain map // and only if AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION is ON // (requires libxml2). Otherwise it has no effect. @@ -108,6 +110,7 @@ avifAppFileFormat avifReadImage(const char * filename, avifBool ignoreColorProfile, avifBool ignoreExif, avifBool ignoreXMP, + avifBool ignoreAlpha, avifBool ignoreGainMap, uint32_t imageSizeLimit, avifImage * image, diff --git a/apps/shared/y4m.c b/apps/shared/y4m.c index 2049b86dcb..9a604ed0bf 100644 --- a/apps/shared/y4m.c +++ b/apps/shared/y4m.c @@ -254,6 +254,7 @@ static avifBool y4mClampSamples(avifImage * avif) } while (0) avifBool y4mRead(const char * inputFilename, + avifBool ignoreAlpha, uint32_t imageSizeLimit, avifImage * avif, avifAppSourceTiming * sourceTiming, @@ -436,6 +437,10 @@ avifBool y4mRead(const char * inputFilename, } } + if (frame.hasAlpha && ignoreAlpha) { + avifImageFreePlanes(avif, AVIF_PLANES_A); + } + // libavif API does not guarantee the absence of undefined behavior if samples exceed the specified avif->depth. // Avoid that by making sure input values are within the correct range. if (y4mClampSamples(avif)) { diff --git a/apps/shared/y4m.h b/apps/shared/y4m.h index a4d837a774..bf9d645672 100644 --- a/apps/shared/y4m.h +++ b/apps/shared/y4m.h @@ -19,6 +19,7 @@ extern "C" { struct y4mFrameIterator; avifBool y4mRead(const char * inputFilename, + avifBool ignoreAlpha, uint32_t imageSizeLimit, avifImage * avif, avifAppSourceTiming * sourceTiming, diff --git a/doc/avifenc.1.md b/doc/avifenc.1.md index b502c18ad0..b1adf17ee4 100644 --- a/doc/avifenc.1.md +++ b/doc/avifenc.1.md @@ -140,6 +140,9 @@ Input format can be either JPEG, PNG or YUV4MPEG2 (Y4M). **\--ignore-profile**, **\--ignore-icc** : If the input file contains an embedded color profile, ignore it (no-op if absent). +**\--ignore-alpha** +: If the input file contains an alpha channel, ignore it (no-op if absent). + **\--ignore-gain-map** : If the input file contains an embedded gain map, ignore it (no-op if absent). diff --git a/tests/gtest/are_images_equal.cc b/tests/gtest/are_images_equal.cc index f6805d5432..66be844a42 100644 --- a/tests/gtest/are_images_equal.cc +++ b/tests/gtest/are_images_equal.cc @@ -95,7 +95,8 @@ int main(int argc, char** argv) { requestedFormat, kRequestedDepth, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile==*/AVIF_FALSE, /*ignoreExif=*/AVIF_FALSE, - /*ignoreXMP=*/AVIF_FALSE, ignore_gain_map, + /*ignoreXMP=*/AVIF_FALSE, /*ignoreAlpha=*/AVIF_FALSE, + ignore_gain_map, /*imageSizeLimit=*/std::numeric_limits::max(), decoded[i].get(), &depth[i], nullptr, nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { diff --git a/tests/gtest/avif_fuzztest_read_image.cc b/tests/gtest/avif_fuzztest_read_image.cc index f04cd18d9e..432e08bac8 100644 --- a/tests/gtest/avif_fuzztest_read_image.cc +++ b/tests/gtest/avif_fuzztest_read_image.cc @@ -80,8 +80,8 @@ void ReadImageFile(const std::string& arbitrary_bytes, const avifAppFileFormat file_format = avifReadImage( file_path.c_str(), AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */, requested_format, requested_depth, chroma_downsampling, - ignore_color_profile, ignore_exif, ignore_xmp, ignore_gain_map, - imageSizeLimit, avif_image.get(), &out_depth, &timing, + ignore_color_profile, ignore_exif, ignore_xmp, /*ignoreAlpha=*/false, + ignore_gain_map, imageSizeLimit, avif_image.get(), &out_depth, &timing, /*frameIter=*/nullptr); if (file_format != AVIF_APP_FILE_FORMAT_UNKNOWN) { diff --git a/tests/gtest/aviflosslesstest.cc b/tests/gtest/aviflosslesstest.cc index c74788da33..1a1b8a6fba 100644 --- a/tests/gtest/aviflosslesstest.cc +++ b/tests/gtest/aviflosslesstest.cc @@ -28,6 +28,7 @@ TEST(LosslessTest, YCGCO_RO) { /*requestedDepth=*/0, /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/false, /*ignoreExif=*/false, /*ignoreXMP=*/false, + /*ignoreAlpha=*/false, /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr); @@ -51,7 +52,7 @@ void ReadImageSimple(const std::string& file_path, avifPixelFormat pixel_format, /*requestedFormat=*/pixel_format, /*requestedDepth=*/0, /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/ignore_icc, /*ignoreExif=*/false, - /*ignoreXMP=*/false, /*ignoreGainMap=*/true, + /*ignoreXMP=*/false, /*ignoreAlpha=*/false, /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr); if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY && @@ -79,7 +80,7 @@ void GetIsGray(const std::string& path, bool& is_gray) { AVIF_PIXEL_FORMAT_NONE, /*requestedDepth=*/0, /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/true, /*ignoreExif=*/true, - /*ignoreXMP=*/true, + /*ignoreXMP=*/true, /*ignoreAlpha=*/false, /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr)); diff --git a/tests/gtest/avifpng16bittest.cc b/tests/gtest/avifpng16bittest.cc index 54720330bc..52a1c8a634 100644 --- a/tests/gtest/avifpng16bittest.cc +++ b/tests/gtest/avifpng16bittest.cc @@ -38,6 +38,7 @@ ImagePtr ReadImageLosslessBitDepth(const std::string& path, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/true, /*ignoreExif=*/true, /*ignoreXMP=*/true, + /*ignoreAlpha=*/false, /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), &output_depth, /*sourceTiming=*/nullptr, diff --git a/tests/gtest/avifreadimagetest.cc b/tests/gtest/avifreadimagetest.cc index c1423fed4a..689741b532 100644 --- a/tests/gtest/avifreadimagetest.cc +++ b/tests/gtest/avifreadimagetest.cc @@ -281,7 +281,7 @@ static avifAppFileFormat avifReadImageForRGB2Gray2RGB(const std::string& path, /*requestedDepth=*/0, /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/ignore_icc, /*ignoreExif=*/false, - /*ignoreXMP=*/false, + /*ignoreXMP=*/false, /*ignoreAlpha=*/false, /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr); @@ -450,7 +450,7 @@ TEST(ImageSizeLimitTest, AllFormats) { filepath.c_str(), AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */, AVIF_PIXEL_FORMAT_NONE, /*requestedDepth=*/0, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/true, - /*ignoreExif=*/true, /*ignoreXMP=*/true, + /*ignoreExif=*/true, /*ignoreXMP=*/true, /*ignoreAlpha=*/false, /*ignoreGainMap=*/true, image_size_limit, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr); diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc index 2ed0adcc23..40730d15e7 100644 --- a/tests/gtest/aviftest_helpers.cc +++ b/tests/gtest/aviftest_helpers.cc @@ -650,8 +650,8 @@ ImagePtr ReadImage(const char* folder_path, const char* file_name, avifReadImage((std::string(folder_path) + file_name).c_str(), AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */, requested_format, requested_depth, chroma_downsampling, - ignore_icc, ignore_exif, ignore_xmp, ignore_gain_map, - AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), + ignore_icc, ignore_exif, ignore_xmp, /*ignoreAlpha=*/false, + ignore_gain_map, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { return nullptr; diff --git a/tests/gtest/avify4mtest.cc b/tests/gtest/avify4mtest.cc index bb9ceec56d..1ff27603e5 100644 --- a/tests/gtest/avify4mtest.cc +++ b/tests/gtest/avify4mtest.cc @@ -50,8 +50,9 @@ TEST_P(Y4mTest, EncodeDecode) { ImagePtr decoded(avifImageCreateEmpty()); ASSERT_NE(decoded, nullptr); - ASSERT_TRUE(y4mRead(file_path.str().c_str(), AVIF_DEFAULT_IMAGE_SIZE_LIMIT, - decoded.get(), /*sourceTiming=*/nullptr, + ASSERT_TRUE(y4mRead(file_path.str().c_str(), /*ignoreAlpha=*/false, + AVIF_DEFAULT_IMAGE_SIZE_LIMIT, decoded.get(), + /*sourceTiming=*/nullptr, /*iter=*/nullptr)); EXPECT_TRUE(testutil::AreImagesEqual(*image, *decoded)); @@ -88,8 +89,9 @@ TEST_P(Y4mTest, OutOfRange) { // that tag along, it is ignored by the compression algorithm. ImagePtr decoded(avifImageCreateEmpty()); ASSERT_NE(decoded, nullptr); - ASSERT_TRUE(y4mRead(file_path.str().c_str(), AVIF_DEFAULT_IMAGE_SIZE_LIMIT, - decoded.get(), /*sourceTiming=*/nullptr, + ASSERT_TRUE(y4mRead(file_path.str().c_str(), /*ignoreAlpha=*/false, + AVIF_DEFAULT_IMAGE_SIZE_LIMIT, decoded.get(), + /*sourceTiming=*/nullptr, /*iter=*/nullptr)); // Pass it through the libavif API to make sure reading a bad y4m does not diff --git a/tests/test_cmd.sh b/tests/test_cmd.sh index b9260ddc3f..0ba0e3a0f7 100755 --- a/tests/test_cmd.sh +++ b/tests/test_cmd.sh @@ -143,6 +143,14 @@ pushd ${TMP_DIR} "${AVIFENC}" --autotiling --tilerowslog2 1 "${INPUT_Y4M}" "${ENCODED_FILE}" && exit 1 "${AVIFENC}" --autotiling --tilecolslog2 2 "${INPUT_Y4M}" "${ENCODED_FILE}" && exit 1 "${AVIFENC}" --autotiling --tilerowslog2 1 --tilecolslog2 2 "${INPUT_Y4M}" "${ENCODED_FILE}" && exit 1 + + # Test --ignore-alpha. + echo "Testing --ignore-alpha" + "${AVIFENC}" -s 10 "${INPUT_PNG}" "${ENCODED_FILE}" > "${OUT_MSG}" + grep " Alpha : Not premultiplied" "${OUT_MSG}" + "${AVIFENC}" -s 10 --ignore-alpha "${INPUT_PNG}" "${ENCODED_FILE}" > "${OUT_MSG}" + grep " Alpha : Absent" "${OUT_MSG}" + grep " Alpha total size: 0 bytes" "${OUT_MSG}" popd exit 0