Split Get() into Identity and Features
Usual clients such as php-src begin by identifying the bitstream's
format and only after start to parse it for width, height etc.
Splitting the function in two avoids a stream rewind and/or parsing
the same data twice.
Change-Id: I4fc814e442bebe6f48d94f634c7e86dd07c73867
diff --git a/avifinfo.c b/avifinfo.c
index 88a6e11..5b93d19 100644
--- a/avifinfo.c
+++ b/avifinfo.c
@@ -24,6 +24,13 @@
kInvalid, // Input incorrectly parsed.
} AvifInfoInternalStatus;
+static AvifInfoStatus AvifInfoInternalConvertStatus(AvifInfoInternalStatus s) {
+ return (s == kFound) ? kAvifInfoOk
+ : (s == kNotFound || s == kTruncated) ? kAvifInfoNotEnoughData
+ : (s == kAborted) ? kAvifInfoTooComplex
+ : kAvifInfoInvalidFile;
+}
+
// uint32_t is used everywhere in this file. It is unlikely to be insufficient
// to parse AVIF headers.
#define AVIFINFO_MAX_SIZE UINT32_MAX
@@ -611,8 +618,30 @@
//------------------------------------------------------------------------------
-// Parses a file 'stream'. The file type is checked through the "ftyp" box and
-// 'features' are extracted through the "meta" box.
+// Parses a file 'stream'. The file type is checked through the "ftyp" box.
+static AvifInfoInternalStatus ParseFtyp(AvifInfoInternalStream* stream) {
+ AvifInfoInternalBox box;
+ uint32_t num_parsed_boxes = 0;
+ AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE,
+ &num_parsed_boxes, &box));
+ AVIFINFO_CHECK(!memcmp(box.type, "ftyp", 4), kInvalid);
+ // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
+ AVIFINFO_CHECK(box.content_size >= 8, kInvalid); // major_brand,minor_version
+ for (uint32_t i = 0; i + 4 <= box.content_size; i += 4) {
+ const uint8_t* data;
+ AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data));
+ if (i == 4) continue; // Skip minor_version.
+ if (!memcmp(data, "avif", 4) || !memcmp(data, "avis", 4)) {
+ AVIFINFO_CHECK_FOUND(
+ AvifInfoInternalSkip(stream, box.content_size - (i + 4)));
+ return kFound;
+ }
+ AVIFINFO_CHECK(i <= 32 * 4, kAborted); // Be reasonable.
+ }
+ AVIFINFO_RETURN(kInvalid); // No AVIF brand no good.
+}
+
+// Parses a file 'stream'. 'features' are extracted from the "meta" box.
static AvifInfoInternalStatus ParseFile(AvifInfoInternalStream* stream,
uint32_t* num_parsed_boxes,
AvifInfoInternalFeatures* features) {
@@ -620,28 +649,7 @@
AvifInfoInternalBox box;
AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE,
num_parsed_boxes, &box));
- // The first box must be "ftyp" and no other box at root can be "ftyp".
- AVIFINFO_CHECK((*num_parsed_boxes == 1) == !memcmp(box.type, "ftyp", 4),
- kInvalid);
-
- if (!memcmp(box.type, "ftyp", 4)) {
- // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
- AVIFINFO_CHECK(box.content_size >= 8, kInvalid); // major_brand + version
- uint8_t is_avif = 0;
- for (uint32_t i = 0; i + 4 <= box.content_size; i += 4) {
- const uint8_t* data;
- AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data));
- if (i == 4) continue; // Skip minor_version.
- if (!memcmp(data, "avif", 4) || !memcmp(data, "avis", 4)) {
- is_avif = 1;
- AVIFINFO_CHECK_FOUND(
- AvifInfoInternalSkip(stream, box.content_size - (i + 4)));
- break;
- }
- AVIFINFO_CHECK(i <= 32 * 4, kAborted); // Be reasonable.
- }
- AVIFINFO_CHECK(is_avif, kInvalid);
- } else if (!memcmp(box.type, "meta", 4)) {
+ if (!memcmp(box.type, "meta", 4)) {
return ParseMeta(stream, box.content_size, num_parsed_boxes, features);
} else {
AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size));
@@ -678,22 +686,43 @@
//------------------------------------------------------------------------------
// Fixed-size input public API
-AvifInfoStatus AvifInfoGet(const uint8_t* data, size_t data_size,
- AvifInfoFeatures* features) {
+AvifInfoStatus AvifInfoIdentify(const uint8_t* data, size_t data_size) {
AvifInfoInternalForward stream;
stream.data = data;
stream.data_size = data_size;
// Forward null 'data' as a null 'stream' to handle it the same way.
- return AvifInfoRead((void*)&stream,
- (data == NULL) ? NULL : AvifInfoInternalForwardRead,
- AvifInfoInternalForwardSkip, features);
+ return AvifInfoIdentifyStream(
+ (void*)&stream, (data == NULL) ? NULL : AvifInfoInternalForwardRead,
+ AvifInfoInternalForwardSkip);
+}
+
+AvifInfoStatus AvifInfoGetFeatures(const uint8_t* data, size_t data_size,
+ AvifInfoFeatures* features) {
+ AvifInfoInternalForward stream;
+ stream.data = data;
+ stream.data_size = data_size;
+ return AvifInfoGetFeaturesStream(
+ (void*)&stream, (data == NULL) ? NULL : AvifInfoInternalForwardRead,
+ AvifInfoInternalForwardSkip, features);
}
//------------------------------------------------------------------------------
// Streamed input API
-AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read,
- skip_stream_t skip, AvifInfoFeatures* features) {
+AvifInfoStatus AvifInfoIdentifyStream(void* stream, read_stream_t read,
+ skip_stream_t skip) {
+ if (read == NULL) return kAvifInfoNotEnoughData;
+
+ AvifInfoInternalStream internal_stream;
+ internal_stream.stream = stream;
+ internal_stream.read = read;
+ internal_stream.skip = skip; // Fallbacks to 'read' if null.
+ return AvifInfoInternalConvertStatus(ParseFtyp(&internal_stream));
+}
+
+AvifInfoStatus AvifInfoGetFeaturesStream(void* stream, read_stream_t read,
+ skip_stream_t skip,
+ AvifInfoFeatures* features) {
if (features != NULL) memset(features, 0, sizeof(*features));
if (read == NULL) return kAvifInfoNotEnoughData;
@@ -708,14 +737,9 @@
// Go through all relevant boxes sequentially.
const AvifInfoInternalStatus status =
ParseFile(&internal_stream, &num_parsed_boxes, &internal_features);
-
- if (status == kNotFound) return kAvifInfoNotEnoughData;
- if (status == kTruncated) return kAvifInfoNotEnoughData;
- if (status == kInvalid) return kAvifInfoInvalidFile;
- if (status == kAborted) return kAvifInfoTooComplex;
- if (features != NULL) {
+ if (status == kFound && features != NULL) {
memcpy(features, &internal_features.primary_item_features,
sizeof(*features));
}
- return kAvifInfoOk;
+ return AvifInfoInternalConvertStatus(status);
}
diff --git a/avifinfo.h b/avifinfo.h
index a940ea7..4c898c2 100644
--- a/avifinfo.h
+++ b/avifinfo.h
@@ -45,12 +45,16 @@
// Fixed-size input API
// Use this API if a raw byte array of fixed size is available as input.
-// Parses the AVIF 'data' and extracts its 'features'.
+// Parses the 'data' and returns kAvifInfoOk if it is identified as an AVIF.
+// The file type can be identified in the first 12 bytes of most AVIF files.
+AvifInfoStatus AvifInfoIdentify(const uint8_t* data, size_t data_size);
+
+// Parses the identified AVIF 'data' and extracts its 'features'.
// 'data' can be partial but must point to the beginning of the AVIF file.
// The 'features' can be parsed in the first 450 bytes of most AVIF files.
// 'features' are set to 0 unless kAvifInfoOk is returned.
-AvifInfoStatus AvifInfoGet(const uint8_t* data, size_t data_size,
- AvifInfoFeatures* features);
+AvifInfoStatus AvifInfoGetFeatures(const uint8_t* data, size_t data_size,
+ AvifInfoFeatures* features);
//------------------------------------------------------------------------------
// Streamed input API
@@ -69,11 +73,15 @@
// Maximum number of bytes requested per read. There is no limit per skip.
#define AVIFINFO_MAX_NUM_READ_BYTES 64
-// Same as AvifInfoGet() but takes a 'stream' as input. AvifInfoRead() does not
-// access the 'stream' directly but passes it as is to 'read' and 'skip'.
+// Same as AvifInfo*() but takes a 'stream' as input. AvifInfo*Stream() does
+// not access the 'stream' directly but passes it as is to 'read' and 'skip'.
// 'read' cannot be null. If 'skip' is null, 'read' is called instead.
-AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read,
- skip_stream_t skip, AvifInfoFeatures* features);
+AvifInfoStatus AvifInfoIdentifyStream(void* stream, read_stream_t read,
+ skip_stream_t skip);
+// Can be called right after AvifInfoIdentifyStream() with the same 'stream'.
+AvifInfoStatus AvifInfoGetFeaturesStream(void* stream, read_stream_t read,
+ skip_stream_t skip,
+ AvifInfoFeatures* features);
//------------------------------------------------------------------------------
diff --git a/tests/avifinfo_fuzz.cc b/tests/avifinfo_fuzz.cc
index d4a6526..be39869 100644
--- a/tests/avifinfo_fuzz.cc
+++ b/tests/avifinfo_fuzz.cc
@@ -47,7 +47,8 @@
// Test a random bitstream of random size, whether it is valid or not.
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t data_size) {
- AvifInfoStatus previous_status = kAvifInfoNotEnoughData;
+ AvifInfoStatus previous_status_identity = kAvifInfoNotEnoughData;
+ AvifInfoStatus previous_status_features = kAvifInfoNotEnoughData;
AvifInfoFeatures previous_features = {0};
// Check the consistency of the returned status and features:
@@ -56,22 +57,26 @@
for (size_t size = 0; size < data_size; ++size) {
StreamData stream = {data, size};
AvifInfoFeatures features;
- const AvifInfoStatus status =
- AvifInfoRead(&stream, StreamRead, StreamSkip, &features);
+ const AvifInfoStatus status_identity =
+ AvifInfoIdentifyStream(&stream, StreamRead, StreamSkip);
+ const AvifInfoStatus status_features =
+ AvifInfoGetFeaturesStream(&stream, StreamRead, StreamSkip, &features);
- if (previous_status != kAvifInfoNotEnoughData &&
- status != previous_status) {
+ if ((previous_status_identity != kAvifInfoNotEnoughData &&
+ status_identity != previous_status_identity) ||
+ (previous_status_features != kAvifInfoNotEnoughData &&
+ status_features != previous_status_features)) {
std::abort();
}
- if (status == previous_status) {
+ if (status_features == previous_status_features) {
if (features.width != previous_features.width ||
features.height != previous_features.height ||
features.bit_depth != previous_features.bit_depth ||
features.num_channels != previous_features.num_channels) {
std::abort();
}
- } else if (status == kAvifInfoOk) {
+ } else if (status_features == kAvifInfoOk) {
if (features.width == 0u || features.height == 0u ||
features.bit_depth == 0u || features.num_channels == 0u) {
std::abort();
@@ -83,7 +88,8 @@
}
}
- previous_status = status;
+ previous_status_identity = status_identity;
+ previous_status_features = status_features;
previous_features = features;
}
return 0;
diff --git a/tests/avifinfo_test.cc b/tests/avifinfo_test.cc
index 0ab2ac7..c0cb7c7 100644
--- a/tests/avifinfo_test.cc
+++ b/tests/avifinfo_test.cc
@@ -41,9 +41,10 @@
const Data input = LoadFile("avifinfo_test_1x1.avif");
ASSERT_FALSE(input.empty());
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
- EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u}));
+ EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+ EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u}));
}
TEST(AvifInfoGetTest, NoPixi10b) {
@@ -54,9 +55,10 @@
LoadFile("avifinfo_test_1x1_10b_nopixi_metasize64b_mdatsize0.avif");
ASSERT_FALSE(input.empty());
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
- EXPECT_TRUE(AreEqual(features, {1u, 1u, 10u, 3u}));
+ EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+ EXPECT_TRUE(AreEqual(f, {1u, 1u, 10u, 3u}));
}
TEST(AvifInfoGetTest, EnoughBytes) {
@@ -67,39 +69,28 @@
input.resize(std::search(input.begin(), input.end(), kMdatTag, kMdatTag + 4) -
input.begin());
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
- EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u}));
-}
-
-TEST(AvifInfoGetTest, BigMetaBox) {
- Data input = LoadFile("avifinfo_test_1x1.avif");
- ASSERT_FALSE(input.empty());
- // Change "meta" box size to the size 2^32-1.
- const uint8_t kMetaTag[] = {'m', 'e', 't', 'a'};
- auto meta_tag =
- std::search(input.begin(), input.end(), kMetaTag, kMetaTag + 4);
- meta_tag[-4] = meta_tag[-3] = meta_tag[-2] = meta_tag[-1] = 255;
-
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk);
- EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u}));
+ EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
+ EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u}));
}
TEST(AvifInfoGetTest, Null) {
const Data input = LoadFile("avifinfo_test_1x1.avif");
ASSERT_FALSE(input.empty());
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), nullptr), kAvifInfoOk);
+ EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), nullptr),
+ kAvifInfoOk);
}
//------------------------------------------------------------------------------
// Negative tests
TEST(AvifInfoGetTest, Empty) {
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(nullptr, 0, &features), kAvifInfoNotEnoughData);
- EXPECT_TRUE(AreEqual(features, {0}));
+ EXPECT_EQ(AvifInfoIdentify(nullptr, 0), kAvifInfoNotEnoughData);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(nullptr, 0, &f), kAvifInfoNotEnoughData);
+ EXPECT_TRUE(AreEqual(f, {0}));
}
TEST(AvifInfoGetTest, NotEnoughBytes) {
@@ -110,8 +101,9 @@
input.resize(std::search(input.begin(), input.end(), kIpmaTag, kIpmaTag + 4) -
input.begin());
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features),
+ EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
kAvifInfoNotEnoughData);
}
@@ -122,10 +114,11 @@
const uint8_t kIspeTag[] = {'i', 's', 'p', 'e'};
std::search(input.begin(), input.end(), kIspeTag, kIspeTag + 4)[0] = 'a';
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features),
+ EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
kAvifInfoInvalidFile);
- EXPECT_TRUE(AreEqual(features, {0}));
+ EXPECT_TRUE(AreEqual(f, {0}));
}
TEST(AvifInfoGetTest, MetaBoxIsTooBig) {
@@ -139,10 +132,11 @@
meta_tag[-1] = 1; // 32-bit "1" then 4-char "meta" then 64-bit size.
input.insert(meta_tag + 4, {255, 255, 255, 255, 255, 255, 255, 255});
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features),
+ EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
kAvifInfoTooComplex);
- EXPECT_TRUE(AreEqual(features, {0}));
+ EXPECT_TRUE(AreEqual(f, {0}));
}
TEST(AvifInfoGetTest, TooManyBoxes) {
@@ -156,18 +150,22 @@
input.insert(input.end(), kBox, kBox + kBox[3]);
}
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoGet(reinterpret_cast<uint8_t*>(input.data()),
- input.size() * 4, &features),
+ EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeatures(reinterpret_cast<uint8_t*>(input.data()),
+ input.size() * 4, &f),
kAvifInfoTooComplex);
}
TEST(AvifInfoReadTest, Null) {
- AvifInfoFeatures features;
- EXPECT_EQ(AvifInfoRead(/*stream=*/nullptr, /*read=*/nullptr, /*skip=*/nullptr,
- &features),
+ EXPECT_EQ(AvifInfoIdentifyStream(/*stream=*/nullptr, /*read=*/nullptr,
+ /*skip=*/nullptr),
kAvifInfoNotEnoughData);
- EXPECT_TRUE(AreEqual(features, {0}));
+ AvifInfoFeatures f;
+ EXPECT_EQ(AvifInfoGetFeaturesStream(/*stream=*/nullptr, /*read=*/nullptr,
+ /*skip=*/nullptr, &f),
+ kAvifInfoNotEnoughData);
+ EXPECT_TRUE(AreEqual(f, {0}));
}
//------------------------------------------------------------------------------
diff --git a/tools/avifinfo_tool.cc b/tools/avifinfo_tool.cc
index 0d0dc13..4d4e4ae 100644
--- a/tools/avifinfo_tool.cc
+++ b/tools/avifinfo_tool.cc
@@ -70,8 +70,9 @@
// Parses the AVIF at 'data' of 'data_size' bytes using libavifinfo.
Result ParseAvif(const uint8_t data[], size_t data_size) {
Result result;
- const AvifInfoStatus status = AvifInfoGet(data, data_size, &result.features);
- result.success = (status == kAvifInfoOk);
+ result.success =
+ (AvifInfoIdentify(data, data_size) == kAvifInfoOk &&
+ AvifInfoGetFeatures(data, data_size, &result.features) == kAvifInfoOk);
return result;
}
@@ -88,7 +89,7 @@
size_t max_data_size = data_size;
while (min_data_size < max_data_size) {
const size_t middle = (min_data_size + max_data_size) / 2;
- if (AvifInfoGet(data, middle, nullptr) == kAvifInfoOk) {
+ if (ParseAvif(data, middle).success) {
max_data_size = middle;
} else {
min_data_size = middle + 1;