Skip to content

Commit

Permalink
Add properties to avifImage (#2420)
Browse files Browse the repository at this point in the history
Allows for custom ItemProperty and ItemFullProperty access from the
libavif reading API.
Add avifpropertytest.
Fuzz avifImage::properties.
Add CHANGELOG entry.
  • Loading branch information
y-guyon authored Nov 15, 2024
1 parent a9ac378 commit 13d784e
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 34 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The changes are relative to the previous release, unless the baseline is specifi

## [Unreleased]

### Added since 1.1.1
* Add the properties and numProperties fields to avifImage. They are filled by
the avifDecoder instance with the properties unrecognized by libavif.

### Changed since 1.1.1
* avifenc: Allow large images to be encoded.
* Fix empty CMAKE_CXX_FLAGS_RELEASE if -DAVIF_CODEC_AOM=LOCAL -DAVIF_LIBYUV=OFF
Expand Down
19 changes: 19 additions & 0 deletions include/avif/avif.h
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,19 @@ typedef enum avifSampleTransformRecipe
} avifSampleTransformRecipe;
#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM

// ---------------------------------------------------------------------------
// Opaque image item properties

// This struct represents an opaque ItemProperty (Box) or ItemFullProperty (FullBox) in ISO/IEC 14496-12.
typedef struct avifImageItemProperty
{
uint8_t boxtype[4]; // boxtype as defined in ISO/IEC 14496-12.
uint8_t usertype[16]; // Universally Unique IDentifier as defined in IETF RFC 4122 and ISO/IEC 9834-8.
// Used only when boxtype is "uuid".
avifRWData boxPayload; // BoxPayload as defined in ISO/IEC 14496-12.
// Starts with the version (1 byte) and flags (3 bytes) fields in case of a FullBox.
} avifImageItemProperty;

// ---------------------------------------------------------------------------
// avifImage

Expand Down Expand Up @@ -809,6 +822,12 @@ typedef struct avifImage

// Version 1.0.0 ends here. Add any new members after this line.

// Other properties attached to this image item (primary or gainmap).
// At decoding: Forwarded here as opaque byte sequences by the avifDecoder.
// At encoding: Ignored.
avifImageItemProperty * properties; // NULL only if numProperties is 0.
size_t numProperties;

#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
// Gain map image and metadata. NULL if no gain map is present.
// Owned by the avifImage and gets freed when calling avifImageDestroy().
Expand Down
8 changes: 8 additions & 0 deletions include/avif/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ void avifImageCopyNoAlloc(avifImage * dstImage, const avifImage * srcImage);
// Ignores the gainMap field (which exists only if AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP is defined).
void avifImageCopySamples(avifImage * dstImage, const avifImage * srcImage, avifPlanesFlags planes);

// Appends an opaque image item property.
AVIF_API avifResult avifImagePushProperty(avifImage * image,
const uint8_t boxtype[4],
const uint8_t usertype[16],
const uint8_t * boxPayload,
size_t boxPayloadSize);

// ---------------------------------------------------------------------------

#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM)
Expand Down Expand Up @@ -644,6 +651,7 @@ typedef struct avifBoxHeader
size_t size;

uint8_t type[4];
uint8_t usertype[16]; // Unused unless |type| is "uuid".
} avifBoxHeader;

typedef struct avifROStream
Expand Down
56 changes: 56 additions & 0 deletions src/avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,31 @@ void avifImageCopySamples(avifImage * dstImage, const avifImage * srcImage, avif
}
}

static avifResult avifImageCopyProperties(avifImage * dstImage, const avifImage * srcImage)
{
for (size_t i = 0; i < dstImage->numProperties; ++i) {
avifRWDataFree(&dstImage->properties[i].boxPayload);
}
avifFree(dstImage->properties);
dstImage->properties = NULL;
dstImage->numProperties = 0;

if (srcImage->numProperties != 0) {
dstImage->properties = (avifImageItemProperty *)avifAlloc(srcImage->numProperties * sizeof(srcImage->properties[0]));
AVIF_CHECKERR(dstImage->properties != NULL, AVIF_RESULT_OUT_OF_MEMORY);
memset(dstImage->properties, 0, srcImage->numProperties * sizeof(srcImage->properties[0]));
dstImage->numProperties = srcImage->numProperties;
for (size_t i = 0; i < srcImage->numProperties; ++i) {
memcpy(dstImage->properties[i].boxtype, srcImage->properties[i].boxtype, sizeof(srcImage->properties[i].boxtype));
memcpy(dstImage->properties[i].usertype, srcImage->properties[i].usertype, sizeof(srcImage->properties[i].usertype));
AVIF_CHECKRES(avifRWDataSet(&dstImage->properties[i].boxPayload,
srcImage->properties[i].boxPayload.data,
srcImage->properties[i].boxPayload.size));
}
}
return AVIF_RESULT_OK;
}

avifResult avifImageCopy(avifImage * dstImage, const avifImage * srcImage, avifPlanesFlags planes)
{
avifImageFreePlanes(dstImage, AVIF_PLANES_ALL);
Expand All @@ -238,6 +263,8 @@ avifResult avifImageCopy(avifImage * dstImage, const avifImage * srcImage, avifP
AVIF_CHECKRES(avifRWDataSet(&dstImage->exif, srcImage->exif.data, srcImage->exif.size));
AVIF_CHECKRES(avifImageSetMetadataXMP(dstImage, srcImage->xmp.data, srcImage->xmp.size));

AVIF_CHECKRES(avifImageCopyProperties(dstImage, srcImage));

if ((planes & AVIF_PLANES_YUV) && srcImage->yuvPlanes[AVIF_CHAN_Y]) {
if ((srcImage->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) &&
(!srcImage->yuvPlanes[AVIF_CHAN_U] || !srcImage->yuvPlanes[AVIF_CHAN_V])) {
Expand Down Expand Up @@ -343,6 +370,12 @@ void avifImageDestroy(avifImage * image)
avifRWDataFree(&image->icc);
avifRWDataFree(&image->exif);
avifRWDataFree(&image->xmp);
for (size_t i = 0; i < image->numProperties; ++i) {
avifRWDataFree(&image->properties[i].boxPayload);
}
avifFree(image->properties);
image->properties = NULL;
image->numProperties = 0;
avifFree(image);
}

Expand All @@ -356,6 +389,29 @@ avifResult avifImageSetMetadataXMP(avifImage * image, const uint8_t * xmp, size_
return avifRWDataSet(&image->xmp, xmp, xmpSize);
}

avifResult avifImagePushProperty(avifImage * image, const uint8_t boxtype[4], const uint8_t usertype[16], const uint8_t * boxPayload, size_t boxPayloadSize)
{
AVIF_CHECKERR(image->numProperties < SIZE_MAX / sizeof(avifImageItemProperty), AVIF_RESULT_INVALID_ARGUMENT);
// Shallow copy the current properties.
const size_t numProperties = image->numProperties + 1;
avifImageItemProperty * const properties = (avifImageItemProperty *)avifAlloc(numProperties * sizeof(properties[0]));
AVIF_CHECKERR(properties != NULL, AVIF_RESULT_OUT_OF_MEMORY);
if (image->numProperties != 0) {
memcpy(properties, image->properties, image->numProperties * sizeof(properties[0]));
}
// Free the old array and replace it by the new one.
avifFree(image->properties);
image->properties = properties;
image->numProperties = numProperties;
// Set the new property.
avifImageItemProperty * const property = &image->properties[image->numProperties - 1];
memset(property, 0, sizeof(*property));
memcpy(property->boxtype, boxtype, sizeof(property->boxtype));
memcpy(property->usertype, usertype, sizeof(property->usertype));
AVIF_CHECKRES(avifRWDataSet(&property->boxPayload, boxPayload, boxPayloadSize));
return AVIF_RESULT_OK;
}

avifResult avifImageAllocatePlanes(avifImage * image, avifPlanesFlags planes)
{
if (image->width == 0 || image->height == 0) {
Expand Down
82 changes: 52 additions & 30 deletions src/read.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ typedef struct avifAV1LayeredImageIndexingProperty
uint32_t layerSize[3];
} avifAV1LayeredImageIndexingProperty;

typedef struct avifOpaqueProperty
{
uint8_t usertype[16]; // Same as in avifImageItemProperty.
avifRWData boxPayload; // Same as in avifImageItemProperty.
} avifOpaqueProperty;

// ---------------------------------------------------------------------------
// Top-level structures

Expand All @@ -148,6 +154,7 @@ struct avifMeta;
typedef struct avifProperty
{
uint8_t type[4];
avifBool isOpaque;
union
{
avifImageSpatialExtents ispe;
Expand All @@ -163,6 +170,7 @@ typedef struct avifProperty
avifLayerSelectorProperty lsel;
avifAV1LayeredImageIndexingProperty a1lx;
avifContentLightLevelInformationBox clli;
avifOpaqueProperty opaque;
} u;
} avifProperty;
AVIF_ARRAY_DECLARE(avifPropertyArray, avifProperty, prop);
Expand Down Expand Up @@ -298,12 +306,22 @@ static avifSampleTable * avifSampleTableCreate(void)
return sampleTable;
}

static void avifPropertyArrayDestroy(avifPropertyArray * array)
{
for (size_t i = 0; i < array->count; ++i) {
if (array->prop[i].isOpaque) {
avifRWDataFree(&array->prop[i].u.opaque.boxPayload);
}
}
avifArrayDestroy(array);
}

static void avifSampleTableDestroy(avifSampleTable * sampleTable)
{
avifArrayDestroy(&sampleTable->chunks);
for (uint32_t i = 0; i < sampleTable->sampleDescriptions.count; ++i) {
avifSampleDescription * description = &sampleTable->sampleDescriptions.description[i];
avifArrayDestroy(&description->properties);
avifPropertyArrayDestroy(&description->properties);
}
avifArrayDestroy(&sampleTable->sampleDescriptions);
avifArrayDestroy(&sampleTable->sampleToChunks);
Expand Down Expand Up @@ -769,15 +787,15 @@ static void avifMetaDestroy(avifMeta * meta)
{
for (uint32_t i = 0; i < meta->items.count; ++i) {
avifDecoderItem * item = meta->items.item[i];
avifArrayDestroy(&item->properties);
avifPropertyArrayDestroy(&item->properties);
avifArrayDestroy(&item->extents);
if (item->ownsMergedExtents) {
avifRWDataFree(&item->mergedExtents);
}
avifFree(item);
}
avifArrayDestroy(&meta->items);
avifArrayDestroy(&meta->properties);
avifPropertyArrayDestroy(&meta->properties);
avifRWDataFree(&meta->idat);
#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM)
avifArrayDestroy(&meta->sampleTransformExpression);
Expand Down Expand Up @@ -823,7 +841,7 @@ static avifResult avifMetaFindOrCreateItem(avifMeta * meta, uint32_t itemID, avi
return AVIF_RESULT_OUT_OF_MEMORY;
}
if (!avifArrayCreate(&(*item)->extents, sizeof(avifExtent), 1)) {
avifArrayDestroy(&(*item)->properties);
avifPropertyArrayDestroy(&(*item)->properties);
avifFree(*item);
*item = NULL;
avifArrayPop(&meta->items);
Expand Down Expand Up @@ -2614,6 +2632,7 @@ static avifResult avifParseItemPropertyContainerBox(avifPropertyArray * properti
avifProperty * prop = (avifProperty *)avifArrayPush(properties);
AVIF_CHECKERR(prop != NULL, AVIF_RESULT_OUT_OF_MEMORY);
memcpy(prop->type, header.type, 4);
prop->isOpaque = AVIF_FALSE;
if (!memcmp(header.type, "ispe", 4)) {
AVIF_CHECKERR(avifParseImageSpatialExtentsProperty(prop, avifROStreamCurrent(&s), header.size, diag),
AVIF_RESULT_BMFF_PARSE_FAILED);
Expand Down Expand Up @@ -2653,6 +2672,11 @@ static avifResult avifParseItemPropertyContainerBox(avifPropertyArray * properti
AVIF_RESULT_BMFF_PARSE_FAILED);
} else if (!memcmp(header.type, "clli", 4)) {
AVIF_CHECKRES(avifParseContentLightLevelInformationBox(prop, avifROStreamCurrent(&s), header.size, diag));
} else {
prop->isOpaque = AVIF_TRUE;
memset(&prop->u.opaque, 0, sizeof(prop->u.opaque));
memcpy(prop->u.opaque.usertype, header.usertype, sizeof(prop->u.opaque.usertype));
AVIF_CHECKRES(avifRWDataSet(&prop->u.opaque.boxPayload, avifROStreamCurrent(&s), header.size));
}

AVIF_CHECKERR(avifROStreamSkip(&s, header.size), AVIF_RESULT_BMFF_PARSE_FAILED);
Expand Down Expand Up @@ -2731,32 +2755,9 @@ static avifResult avifParseItemPropertyAssociation(avifMeta * meta, const uint8_
// Copy property to item
const avifProperty * srcProp = &meta->properties.prop[propertyIndex];

static const char * supportedTypes[] = {
"ispe",
"auxC",
"colr",
"av1C",
#if defined(AVIF_CODEC_AVM)
"av2C",
#endif
"pasp",
"clap",
"irot",
"imir",
"pixi",
"a1op",
"lsel",
"a1lx",
"clli"
};
size_t supportedTypesCount = sizeof(supportedTypes) / sizeof(supportedTypes[0]);
avifBool supportedType = AVIF_FALSE;
for (size_t i = 0; i < supportedTypesCount; ++i) {
if (!memcmp(srcProp->type, supportedTypes[i], 4)) {
supportedType = AVIF_TRUE;
break;
}
}
// Some properties are supported and parsed by libavif.
// Other properties are forwarded to the user as opaque blobs.
const avifBool supportedType = !srcProp->isOpaque;
if (supportedType) {
if (essential) {
// Verify that it is legal for this property to be flagged as essential. Any
Expand Down Expand Up @@ -2813,6 +2814,15 @@ static avifResult avifParseItemPropertyAssociation(avifMeta * meta, const uint8_
// Make a note to ignore this item later.
item->hasUnsupportedEssentialProperty = AVIF_TRUE;
}

// Will be forwarded to the user through avifImage::properties.
avifProperty * dstProp = (avifProperty *)avifArrayPush(&item->properties);
AVIF_CHECKERR(dstProp != NULL, AVIF_RESULT_OUT_OF_MEMORY);
dstProp->isOpaque = AVIF_TRUE;
memcpy(dstProp->type, srcProp->type, sizeof(dstProp->type));
memcpy(dstProp->u.opaque.usertype, srcProp->u.opaque.usertype, sizeof(dstProp->u.opaque.usertype));
AVIF_CHECKRES(
avifRWDataSet(&dstProp->u.opaque.boxPayload, srcProp->u.opaque.boxPayload.data, srcProp->u.opaque.boxPayload.size));
}
}
}
Expand Down Expand Up @@ -6062,6 +6072,18 @@ avifResult avifDecoderReset(avifDecoder * decoder)

AVIF_CHECKRES(avifReadCodecConfigProperty(decoder->image, colorProperties, colorCodecType));

// Expose as raw bytes all other properties that libavif does not care about.
for (size_t i = 0; i < colorProperties->count; ++i) {
const avifProperty * property = &colorProperties->prop[i];
if (property->isOpaque) {
AVIF_CHECKRES(avifImagePushProperty(decoder->image,
property->type,
property->u.opaque.usertype,
property->u.opaque.boxPayload.data,
property->u.opaque.boxPayload.size));
}
}

return AVIF_RESULT_OK;
}

Expand Down
4 changes: 3 additions & 1 deletion src/stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ avifBool avifROStreamReadBoxHeaderPartial(avifROStream * stream, avifBoxHeader *
}

if (!memcmp(header->type, "uuid", 4)) {
AVIF_CHECK(avifROStreamSkip(stream, 16)); // unsigned int(8) usertype[16] = extended_type;
AVIF_CHECK(avifROStreamRead(stream, header->usertype, 16)); // unsigned int(8) usertype[16] = extended_type;
} else {
memset(header->usertype, 0, sizeof(header->usertype));
}

size_t bytesRead = stream->offset - startOffset;
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ if(AVIF_ENABLE_GTEST)
add_avif_gtest(avifopaquetest)
add_avif_gtest_with_data(avifpng16bittest)
add_avif_gtest_with_data(avifprogressivetest)
add_avif_gtest_with_data(avifpropertytest)
add_avif_gtest(avifrangetest)
add_avif_gtest_with_data(avifreadimagetest)
add_avif_internal_gtest(avifrgbtest)
Expand Down
9 changes: 9 additions & 0 deletions tests/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ chunk. Since the PNG specification version 1.2 says "the tRNS chunk [...] must
follow the PLTE chunk, if any", libpng considers the tRNS chunk as invalid and
ignores it.

### File [circle_custom_properties.avif](circle_custom_properties.avif)

![](circle_custom_properties.avif)

License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)

Source: `avifenc circle-trns-after-plte.png` with custom properties added in
`avifRWStreamWriteProperties()`: FullBox `1234`, Box `abcd` and Box `uuid`.

### File [draw_points.png](draw_points.png)

![](draw_points.png)
Expand Down
Binary file added tests/data/circle_custom_properties.avif
Binary file not shown.
16 changes: 13 additions & 3 deletions tests/gtest/avif_fuzztest_dec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Decodes an arbitrary sequence of bytes.

#include <algorithm>
#include <cstdint>

#include "avif/avif.h"
Expand All @@ -25,15 +26,24 @@ void Decode(const std::string& arbitrary_bytes, bool is_persistent,
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);

avifIO* const io = avifIOCreateMemoryReader(
reinterpret_cast<const uint8_t*>(arbitrary_bytes.data()),
arbitrary_bytes.size());
const uint8_t* data =
reinterpret_cast<const uint8_t*>(arbitrary_bytes.data());
avifIO* const io = avifIOCreateMemoryReader(data, arbitrary_bytes.size());
if (io == nullptr) return;
// The Chrome's avifIO object is not persistent.
io->persistent = is_persistent;
avifDecoderSetIO(decoder.get(), io);

if (avifDecoderParse(decoder.get()) != AVIF_RESULT_OK) return;

for (size_t i = 0; i < decoder->image->numProperties; ++i) {
const avifRWData& box_payload = decoder->image->properties[i].boxPayload;
// Each custom property should be found as is in the input bitstream.
EXPECT_NE(std::search(data, data + arbitrary_bytes.size(), box_payload.data,
box_payload.data + box_payload.size),
data + arbitrary_bytes.size());
}

while (avifDecoderNextImage(decoder.get()) == AVIF_RESULT_OK) {
EXPECT_GT(decoder->image->width, 0u);
EXPECT_GT(decoder->image->height, 0u);
Expand Down
Loading

0 comments on commit 13d784e

Please sign in to comment.