diff --git a/CHANGELOG.md b/CHANGELOG.md index 32636083ad..76f226a82d 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.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 diff --git a/include/avif/avif.h b/include/avif/avif.h index f3d87a8ea9..ceda2b67d5 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -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 @@ -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(). diff --git a/include/avif/internal.h b/include/avif/internal.h index c044716ab6..26e7e50689 100644 --- a/include/avif/internal.h +++ b/include/avif/internal.h @@ -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) @@ -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 diff --git a/src/avif.c b/src/avif.c index a81885800f..93c46033e6 100644 --- a/src/avif.c +++ b/src/avif.c @@ -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); @@ -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])) { @@ -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); } @@ -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) { diff --git a/src/read.c b/src/read.c index 5c8b68ac55..178d2666c5 100644 --- a/src/read.c +++ b/src/read.c @@ -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 @@ -148,6 +154,7 @@ struct avifMeta; typedef struct avifProperty { uint8_t type[4]; + avifBool isOpaque; union { avifImageSpatialExtents ispe; @@ -163,6 +170,7 @@ typedef struct avifProperty avifLayerSelectorProperty lsel; avifAV1LayeredImageIndexingProperty a1lx; avifContentLightLevelInformationBox clli; + avifOpaqueProperty opaque; } u; } avifProperty; AVIF_ARRAY_DECLARE(avifPropertyArray, avifProperty, prop); @@ -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); @@ -769,7 +787,7 @@ 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); @@ -777,7 +795,7 @@ static void avifMetaDestroy(avifMeta * meta) avifFree(item); } avifArrayDestroy(&meta->items); - avifArrayDestroy(&meta->properties); + avifPropertyArrayDestroy(&meta->properties); avifRWDataFree(&meta->idat); #if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM) avifArrayDestroy(&meta->sampleTransformExpression); @@ -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); @@ -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); @@ -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); @@ -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 @@ -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)); } } } @@ -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; } diff --git a/src/stream.c b/src/stream.c index f220fed127..a3265d5da2 100644 --- a/src/stream.c +++ b/src/stream.c @@ -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)); } size_t bytesRead = stream->offset - startOffset; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 702739c099..6dc12e0625 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/data/README.md b/tests/data/README.md index d424f072a6..d095fe27f8 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -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) diff --git a/tests/data/circle_custom_properties.avif b/tests/data/circle_custom_properties.avif new file mode 100644 index 0000000000..0f29c78c38 Binary files /dev/null and b/tests/data/circle_custom_properties.avif differ diff --git a/tests/gtest/avif_fuzztest_dec.cc b/tests/gtest/avif_fuzztest_dec.cc index 2aeeef516a..b35df1523c 100644 --- a/tests/gtest/avif_fuzztest_dec.cc +++ b/tests/gtest/avif_fuzztest_dec.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Decodes an arbitrary sequence of bytes. +#include #include #include "avif/avif.h" @@ -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(arbitrary_bytes.data()), - arbitrary_bytes.size()); + const uint8_t* data = + reinterpret_cast(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); diff --git a/tests/gtest/avifpropertytest.cc b/tests/gtest/avifpropertytest.cc new file mode 100644 index 0000000000..107fa2e671 --- /dev/null +++ b/tests/gtest/avifpropertytest.cc @@ -0,0 +1,63 @@ +// Copyright 2024 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include +#include +#include +#include + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +namespace avif { +namespace { + +// Used to pass the data folder path to the GoogleTest suites. +const char* data_path = nullptr; + +//------------------------------------------------------------------------------ + +TEST(AvifPropertyTest, Parse) { + const std::string path = + std::string(data_path) + "circle_custom_properties.avif"; + DecoderPtr decoder(avifDecoderCreate()); + ASSERT_NE(decoder, nullptr); + ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), path.c_str()), AVIF_RESULT_OK); + ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); + ASSERT_EQ(decoder->image->numProperties, 3u); + + const avifImageItemProperty& p1234 = decoder->image->properties[0]; + EXPECT_EQ(std::string(p1234.boxtype, p1234.boxtype + 4), "1234"); + EXPECT_EQ(std::vector(p1234.boxPayload.data, + p1234.boxPayload.data + p1234.boxPayload.size), + std::vector({/*version*/ 0, /*flags*/ 0, 0, 0, + /*FullBoxPayload*/ 1, 2, 3, 4})); + + const avifImageItemProperty& abcd = decoder->image->properties[1]; + EXPECT_EQ(std::string(abcd.boxtype, abcd.boxtype + 4), "abcd"); + EXPECT_EQ(std::string(reinterpret_cast(abcd.boxPayload.data)), + "abcd"); + + const avifImageItemProperty& uuid = decoder->image->properties[2]; + EXPECT_EQ(std::string(uuid.boxtype, uuid.boxtype + 4), "uuid"); + EXPECT_EQ(std::string(uuid.usertype, uuid.usertype + 16), "extended_type 16"); + EXPECT_EQ(uuid.boxPayload.size, 0); +} + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace avif + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + if (argc != 2) { + std::cerr << "There must be exactly one argument containing the path to " + "the test data folder" + << std::endl; + return 1; + } + avif::data_path = argv[1]; + return RUN_ALL_TESTS(); +}