Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add properties to avifImage #2420

Merged
merged 9 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
y-guyon marked this conversation as resolved.
Show resolved Hide resolved
{
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;
y-guyon marked this conversation as resolved.
Show resolved Hide resolved
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)
{
y-guyon marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -2567,6 +2585,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 @@ -2606,6 +2625,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 @@ -2684,32 +2708,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 @@ -2766,6 +2767,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 @@ -6020,6 +6030,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 @@ -257,7 +257,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));
y-guyon marked this conversation as resolved.
Show resolved Hide resolved
}

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 @@ -22,6 +22,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 @@ -27,15 +28,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
Loading