Add a function to convert gain map metadata double values to fractions. (#1546)
Continued fractions (https://en.wikipedia.org/wiki/Continued_fraction) are used to find the best fractional approximation of a double value.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c893668..378cbf6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -295,6 +295,9 @@
src/utils.c
src/write.c
)
+if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+ list(APPEND AVIF_SRCS src/gainmap.c)
+endif()
# Only applicable to macOS. In GitHub CI's macos-latest os image, this prevents using the libpng
# and libjpeg headers from /Library/Frameworks/Mono.framework/Headers instead of
@@ -709,6 +712,9 @@
endif()
if(NOT SKIP_INSTALL_HEADERS AND NOT SKIP_INSTALL_ALL)
install(FILES include/avif/avif.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/avif")
+ if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+ install(FILES include/avif/gainmap.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/avif")
+ endif()
endif()
# ---------------------------------------------------------------------------------------
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 41d908b..1365f2e 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -522,7 +522,8 @@
// avifGainMap
// Gain Maps are a HIGHLY EXPERIMENTAL FEATURE. The format might still change and
// images containing a gain map encoded with the current version of libavif might
-// not decode with a feature future version of libavif. Use are your own risk.
+// not decode with a future version of libavif. The API is not guaranteed
+// to be stable, and might even be removed in the future. Use are your own risk.
// This is based on ISO/IEC JTC 1/SC 29/WG 3 m64379
// This product includes Gain Map technology under license by Adobe.
diff --git a/include/avif/gainmap.h b/include/avif/gainmap.h
new file mode 100644
index 0000000..b3b0376
--- /dev/null
+++ b/include/avif/gainmap.h
@@ -0,0 +1,44 @@
+// Copyright 2023 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+// Utilities to deal with gain maps.
+// This API is experimental and may change or be removed in the future.
+
+#ifndef AVIF_GAINMAP_H
+#define AVIF_GAINMAP_H
+
+#include "avif/avif.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+
+// Same as avifGainMapMetadata, but with fields of type double instead of uint32_t fractions.
+// Use avifGainMapMetadataDoubleToFractions() to convert this to a avifGainMapMetadata.
+// See avifGainMapMetadata in avif.h for detailed descriptions of fields.
+typedef struct avifGainMapMetadataDouble
+{
+ double gainMapMin[3];
+ double gainMapMax[3];
+ double gainMapGamma[3];
+ double offsetSdr[3];
+ double offsetHdr[3];
+ double hdrCapacityMin;
+ double hdrCapacityMax;
+ avifBool baseRenditionIsHDR;
+} avifGainMapMetadataDouble;
+
+// Converts a avifGainMapMetadataDouble to avifGainMapMetadata by converting double values
+// to the closest uint32_t fractions.
+// Returns AVIF_FALSE if some field values are < 0 or > UINT32_MAX.
+avifBool avifGainMapMetadataDoubleToFractions(avifGainMapMetadata * dst, const avifGainMapMetadataDouble * src);
+
+#endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // ifndef AVIF_GAINMAP_H
diff --git a/include/avif/internal.h b/include/avif/internal.h
index a70b9d6..4d36701 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -86,6 +86,10 @@
avifBool avifFractionAdd(avifFraction a, avifFraction b, avifFraction * result);
avifBool avifFractionSub(avifFraction a, avifFraction b, avifFraction * result);
+// Creates a uint32 fraction that is approximately equal to 'v'.
+// Returns AVIF_FALSE if 'v' is < 0 or > UINT32_MAX or NaN.
+avifBool avifToUnsignedFraction(double v, uint32_t * numerator, uint32_t * denominator);
+
void avifImageSetDefaults(avifImage * image);
// Copies all fields that do not need to be freed/allocated from srcImage to dstImage.
void avifImageCopyNoAlloc(avifImage * dstImage, const avifImage * srcImage);
diff --git a/src/gainmap.c b/src/gainmap.c
new file mode 100644
index 0000000..4d0200c
--- /dev/null
+++ b/src/gainmap.c
@@ -0,0 +1,25 @@
+// Copyright 2023 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/gainmap.h"
+#include "avif/avif.h"
+#include "avif/internal.h"
+
+#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+
+avifBool avifGainMapMetadataDoubleToFractions(avifGainMapMetadata * dst, const avifGainMapMetadataDouble * src)
+{
+ for (int i = 0; i < 3; ++i) {
+ AVIF_CHECK(avifToUnsignedFraction(src->gainMapMin[i], &dst->gainMapMinN[i], &dst->gainMapMinD[i]));
+ AVIF_CHECK(avifToUnsignedFraction(src->gainMapMax[i], &dst->gainMapMaxN[i], &dst->gainMapMaxD[i]));
+ AVIF_CHECK(avifToUnsignedFraction(src->gainMapGamma[i], &dst->gainMapGammaN[i], &dst->gainMapGammaD[i]));
+ AVIF_CHECK(avifToUnsignedFraction(src->offsetSdr[i], &dst->offsetSdrN[i], &dst->offsetSdrD[i]));
+ AVIF_CHECK(avifToUnsignedFraction(src->offsetHdr[i], &dst->offsetHdrN[i], &dst->offsetHdrD[i]));
+ }
+ AVIF_CHECK(avifToUnsignedFraction(src->hdrCapacityMin, &dst->hdrCapacityMinN, &dst->hdrCapacityMinD));
+ AVIF_CHECK(avifToUnsignedFraction(src->hdrCapacityMax, &dst->hdrCapacityMaxN, &dst->hdrCapacityMaxD));
+ dst->baseRenditionIsHDR = src->baseRenditionIsHDR;
+ return AVIF_TRUE;
+}
+
+#endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP
diff --git a/src/utils.c b/src/utils.c
index deb12bc..64e823b 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -4,6 +4,7 @@
#include "avif/internal.h"
#include <assert.h>
+#include <float.h>
#include <math.h>
#include <string.h>
@@ -234,3 +235,48 @@
avifFractionSimplify(result);
return AVIF_TRUE;
}
+
+avifBool avifToUnsignedFraction(double v, uint32_t * numerator, uint32_t * denominator)
+{
+ if (isnan(v) || v < 0 || v > UINT32_MAX) {
+ return AVIF_FALSE;
+ }
+
+ // Maximum denominator: makes sure that both the numerator and denominator are <= UINT32_MAX.
+ const uint64_t maxD = (v <= 1) ? UINT32_MAX : (uint64_t)floor(UINT32_MAX / v);
+
+ // Find the best approximation of v as a fraction using continued fractions, see
+ // https://en.wikipedia.org/wiki/Continued_fraction
+ *denominator = 1;
+ uint32_t previousD = 0;
+ double currentV = v - floor(v);
+ int iter = 0;
+ // Set a maximum number of iterations to be safe. Most numbers should
+ // converge in less than ~20 iterations.
+ // The golden ratio is the worst case and takes 39 iterations.
+ const int maxIter = 39;
+ while (iter < maxIter) {
+ const double numeratorDouble = (double)(*denominator) * v;
+ assert(numeratorDouble <= UINT32_MAX);
+ *numerator = (uint32_t)round(numeratorDouble);
+ if (fabs(numeratorDouble - (*numerator)) == 0.0) {
+ return AVIF_TRUE;
+ }
+ currentV = 1.0 / currentV;
+ const double newD = previousD + floor(currentV) * (*denominator);
+ if (newD > maxD) {
+ // This is the best we can do with a denominator <= max_d.
+ return AVIF_TRUE;
+ }
+ previousD = *denominator;
+ assert(newD <= UINT32_MAX);
+ *denominator = (uint32_t)newD;
+ currentV -= floor(currentV);
+ ++iter;
+ }
+ // Maximum number of iterations reached, return what we've found.
+ // For max_iter >= 39 we shouldn't get here. max_iter can be set
+ // to a lower value to speed up the algorithm if needed.
+ *numerator = (uint32_t)round((double)(*denominator) * v);
+ return AVIF_TRUE;
+}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1722cee..4af8636 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -209,6 +209,11 @@
target_include_directories(aviftilingtest PRIVATE ${GTEST_INCLUDE_DIRS})
add_test(NAME aviftilingtest COMMAND aviftilingtest)
+ add_executable(avifutilstest gtest/avifutilstest.cc)
+ target_link_libraries(avifutilstest avif_internal ${GTEST_BOTH_LIBRARIES})
+ target_include_directories(avifutilstest PRIVATE ${GTEST_INCLUDE_DIRS})
+ add_test(NAME avifutilstest COMMAND avifutilstest)
+
add_executable(avify4mtest gtest/avify4mtest.cc)
target_link_libraries(avify4mtest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
target_include_directories(avify4mtest PRIVATE ${GTEST_INCLUDE_DIRS})
diff --git a/tests/gtest/avifgainmaptest.cc b/tests/gtest/avifgainmaptest.cc
index 77f31bb..62e5450 100644
--- a/tests/gtest/avifgainmaptest.cc
+++ b/tests/gtest/avifgainmaptest.cc
@@ -1,9 +1,11 @@
// Copyright 2023 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
+#include <cmath>
#include <fstream>
#include "avif/avif.h"
+#include "avif/gainmap.h"
#include "avif/internal.h"
#include "aviftest_helpers.h"
#include "gtest/gtest.h"
@@ -485,5 +487,60 @@
CheckGainMapMetadataMatches(decoded->gainMap.metadata, avifGainMapMetadata());
}
+#define EXPECT_FRACTION_NEAR(numerator, denominator, expected) \
+ EXPECT_NEAR(std::abs((double)numerator / denominator), expected, \
+ expected * 0.001);
+
+TEST(GainMapTest, Convert) {
+ avifGainMapMetadataDouble metadata_double = {};
+ metadata_double.gainMapMin[0] = 1.0;
+ metadata_double.gainMapMin[1] = 1.1;
+ metadata_double.gainMapMin[2] = 1.2;
+ metadata_double.gainMapMax[0] = 10.0;
+ metadata_double.gainMapMax[1] = 10.1;
+ metadata_double.gainMapMax[2] = 10.2;
+ metadata_double.gainMapGamma[0] = 1.0;
+ metadata_double.gainMapGamma[1] = 1.0;
+ metadata_double.gainMapGamma[2] = 1.2;
+ metadata_double.offsetSdr[0] = 1.0 / 32.0;
+ metadata_double.offsetSdr[1] = 1.0 / 64.0;
+ metadata_double.offsetSdr[2] = 1.0 / 128.0;
+ metadata_double.offsetHdr[0] = 0.004564;
+ metadata_double.offsetHdr[1] = 0.0;
+ metadata_double.hdrCapacityMin = 1.0;
+ metadata_double.hdrCapacityMax = 10.0;
+ metadata_double.baseRenditionIsHDR = AVIF_TRUE;
+
+ avifGainMapMetadata metadata = {};
+ ASSERT_TRUE(
+ avifGainMapMetadataDoubleToFractions(&metadata, &metadata_double));
+
+ for (int i = 0; i < 3; ++i) {
+ EXPECT_FRACTION_NEAR(metadata.gainMapMinN[i], metadata.gainMapMinD[i],
+ metadata_double.gainMapMin[i]);
+ EXPECT_FRACTION_NEAR(metadata.gainMapMaxN[i], metadata.gainMapMaxD[i],
+ metadata_double.gainMapMax[i]);
+ EXPECT_FRACTION_NEAR(metadata.gainMapGammaN[i], metadata.gainMapGammaD[i],
+ metadata_double.gainMapGamma[i]);
+ EXPECT_FRACTION_NEAR(metadata.offsetSdrN[i], metadata.offsetSdrD[i],
+ metadata_double.offsetSdr[i]);
+ EXPECT_FRACTION_NEAR(metadata.offsetHdrN[i], metadata.offsetHdrD[i],
+ metadata_double.offsetHdr[i]);
+ }
+ EXPECT_FRACTION_NEAR(metadata.hdrCapacityMinN, metadata.hdrCapacityMinD,
+ metadata_double.hdrCapacityMin);
+ EXPECT_FRACTION_NEAR(metadata.hdrCapacityMaxN, metadata.hdrCapacityMaxD,
+ metadata_double.hdrCapacityMax);
+ EXPECT_EQ(metadata.baseRenditionIsHDR, metadata_double.baseRenditionIsHDR);
+}
+
+TEST(GainMapTest, Invalid) {
+ avifGainMapMetadataDouble metadata_double = {};
+ metadata_double.gainMapGamma[0] = -42; // A negative value is invalid!
+ avifGainMapMetadata metadata = {};
+ ASSERT_FALSE(
+ avifGainMapMetadataDoubleToFractions(&metadata, &metadata_double));
+}
+
} // namespace
} // namespace libavif
diff --git a/tests/gtest/avifutilstest.cc b/tests/gtest/avifutilstest.cc
new file mode 100644
index 0000000..fe342f4
--- /dev/null
+++ b/tests/gtest/avifutilstest.cc
@@ -0,0 +1,129 @@
+// Copyright 2023 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <cmath>
+
+#include "avif/avif.h"
+#include "avif/internal.h"
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+namespace libavif {
+namespace {
+
+// Converts a double value to a fraction, and checks that the difference
+// between numerator/denominator and v is below relative_tolerance.
+void TestRoundTrip(double v, double relative_tolerance) {
+ uint32_t numerator, denominator;
+ ASSERT_TRUE(avifToUnsignedFraction(v, &numerator, &denominator)) << v;
+ const double reconstructed = (double)numerator / denominator;
+ const double tolerance = v * relative_tolerance;
+ EXPECT_NEAR(reconstructed, v, tolerance)
+ << "numerator " << (double)numerator << " denominator "
+ << (double)denominator;
+}
+
+constexpr double kLotsOfDecimals = 0.14159265358979323846;
+
+TEST(ToFractionUTest, RoundTrip) {
+ // Whole numbers and simple fractions should match perfectly.
+ constexpr double kPerfectTolerance = 0.0;
+ TestRoundTrip(0.0, kPerfectTolerance);
+ TestRoundTrip(1.0, kPerfectTolerance);
+ TestRoundTrip(42.0, kPerfectTolerance);
+ TestRoundTrip(102356.0, kPerfectTolerance);
+ TestRoundTrip(102356456.0f, kPerfectTolerance);
+ TestRoundTrip(UINT32_MAX / 2.0, kPerfectTolerance);
+ TestRoundTrip((double)UINT32_MAX - 1.0, kPerfectTolerance);
+ TestRoundTrip((double)UINT32_MAX, kPerfectTolerance);
+ TestRoundTrip(0.123, kPerfectTolerance);
+ TestRoundTrip(1.0 / 3.0, kPerfectTolerance);
+ TestRoundTrip(1.0 / 4.0, kPerfectTolerance);
+ TestRoundTrip(3.0 / 23.0, kPerfectTolerance);
+ TestRoundTrip(1253456.456, kPerfectTolerance);
+ TestRoundTrip(8598533.9, kPerfectTolerance);
+
+ // // Numbers with a lot of decimals or very large/small can show a small
+ // error.
+ constexpr double kSmallTolerance = 1e-9;
+ TestRoundTrip(0.0123456, kSmallTolerance);
+ TestRoundTrip(3 + kLotsOfDecimals, kSmallTolerance);
+ TestRoundTrip(sqrt(2.0), kSmallTolerance);
+ TestRoundTrip(exp(1.0), kSmallTolerance);
+ TestRoundTrip(exp(10.0), kSmallTolerance);
+ TestRoundTrip(exp(15.0), kSmallTolerance);
+ // The golden ratio, the irrational number that is the "most difficult" to
+ // approximate rationally according to Wikipedia.
+ const double kGoldenRatio = (1.0 + std::sqrt(5.0)) / 2.0;
+ TestRoundTrip(kGoldenRatio, kSmallTolerance); // Golden ratio.
+ TestRoundTrip(((double)UINT32_MAX) - 0.5, kSmallTolerance);
+ // Note that values smaller than this might have a larger relative error
+ // (e.g. 1.0e-10).
+ TestRoundTrip(4.2e-10, kSmallTolerance);
+}
+
+// Tests the max difference between the fraction-ified value and the original
+// value, for a subset of values between 0.0 and UINT32_MAX.
+TEST(ToFractionUTest, MaxDifference) {
+ double max_error = 0;
+ double max_error_v = 0;
+ double max_relative_error = 0;
+ double max_relative_error_v = 0;
+ for (uint64_t i = 0; i < UINT32_MAX; i += 1000) {
+ const double v = i + kLotsOfDecimals;
+ uint32_t numerator, denominator;
+ ASSERT_TRUE(avifToUnsignedFraction(v, &numerator, &denominator)) << v;
+ const double reconstructed = (double)numerator / denominator;
+ const double error = abs(reconstructed - v);
+ const double relative_error = error / v;
+ if (error > max_error) {
+ max_error = error;
+ max_error_v = v;
+ }
+ if (relative_error > max_relative_error) {
+ max_relative_error = relative_error;
+ max_relative_error_v = v;
+ }
+ }
+ EXPECT_LE(max_error, 0.5f) << max_error_v;
+ EXPECT_LT(max_relative_error, 1e-9) << max_relative_error_v;
+}
+
+// Tests the max difference between the fraction-ified value and the original
+// value, for a subset of values between 0 and 1.0/UINT32_MAX.
+TEST(ToFractionUTest, MaxDifferenceSmall) {
+ double max_error = 0;
+ double max_error_v = 0;
+ double max_relative_error = 0;
+ double max_relative_error_v = 0;
+ for (uint64_t i = 1; i < UINT32_MAX; i += 1000) {
+ const double v = 1.0 / (i + kLotsOfDecimals);
+ uint32_t numerator, denominator;
+ ASSERT_TRUE(avifToUnsignedFraction(v, &numerator, &denominator)) << v;
+ const double reconstructed = (double)numerator / denominator;
+ const double error = abs(reconstructed - v);
+ const double relative_error = error / v;
+ if (error > max_error) {
+ max_error = error;
+ max_error_v = v;
+ }
+ if (relative_error > max_relative_error) {
+ max_relative_error = relative_error;
+ max_relative_error_v = v;
+ }
+ }
+ EXPECT_LE(max_error, 1e-10) << max_error_v;
+ EXPECT_LT(max_relative_error, 1e-5) << max_relative_error_v;
+}
+
+TEST(ToFractionUTest, BadValues) {
+ uint32_t numerator, denominator;
+ // Negative value.
+ EXPECT_FALSE(avifToUnsignedFraction(-0.1, &numerator, &denominator));
+ // Too large.
+ EXPECT_FALSE(avifToUnsignedFraction(((double)UINT32_MAX) + 1.0, &numerator,
+ &denominator));
+}
+
+} // namespace
+} // namespace libavif