diff --git a/README.REDIST.BINS b/README.REDIST.BINS index 64123ab004953..19e6a02b18269 100644 --- a/README.REDIST.BINS +++ b/README.REDIST.BINS @@ -15,6 +15,7 @@ 15. ext/phar/zip.c portion extracted from libzip 16. libbcmath (ext/bcmath) see ext/bcmath/libbcmath/LICENSE 17. ext/mbstring/ucgendat portions based on the ucgendat.c from the OpenLDAP +18. avifinfo (ext/standard/libavifinfo) see ext/standard/libavifinfo/LICENSE 3. pcre2lib (ext/pcre) @@ -591,7 +592,7 @@ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -16. ext/mbstring/ucgendat portions based on the ucgendat.c from the OpenLDAP +17. ext/mbstring/ucgendat portions based on the ucgendat.c from the OpenLDAP The OpenLDAP Public License Version 2.8, 17 August 2003 diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index 5299006892797..9ca92c610b9ed 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -460,7 +460,7 @@ PHP_NEW_EXTENSION(standard, array.c base64.c basic_functions.c browscap.c crc32. http_fopen_wrapper.c php_fopen_wrapper.c credits.c css.c \ var_unserializer.c ftok.c sha1.c user_filters.c uuencode.c \ filters.c proc_open.c streamsfuncs.c http.c password.c \ - random.c net.c hrtime.c crc32_x86.c,,, + random.c net.c hrtime.c crc32_x86.c libavifinfo/avifinfo.c,,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) PHP_ADD_MAKEFILE_FRAGMENT diff --git a/ext/standard/config.w32 b/ext/standard/config.w32 index 3de43d04c067e..1813210849344 100644 --- a/ext/standard/config.w32 +++ b/ext/standard/config.w32 @@ -37,6 +37,7 @@ EXTENSION("standard", "array.c base64.c basic_functions.c browscap.c \ user_filters.c uuencode.c filters.c proc_open.c password.c \ streamsfuncs.c http.c flock_compat.c random.c hrtime.c", false /* never shared */, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1'); +ADD_SOURCES("ext/standard/libavifinfo", "avifinfo.c", "standard"); PHP_STANDARD = "yes"; ADD_MAKEFILE_FRAGMENT(); PHP_INSTALL_HEADERS("", "ext/standard"); diff --git a/ext/standard/image.c b/ext/standard/image.c index 85ecda2f3d70b..a614521ae1091 100644 --- a/ext/standard/image.c +++ b/ext/standard/image.c @@ -22,6 +22,7 @@ #endif #include "fopen_wrappers.h" #include "ext/standard/fsock.h" +#include "libavifinfo/avifinfo.h" #if HAVE_UNISTD_H #include #endif @@ -1155,95 +1156,76 @@ static struct gfxinfo *php_handle_webp(php_stream * stream) } /* }}} */ -/* {{{ php_handle_avif - * There's no simple way to get this information - so, for now, this is unsupported. - * Simply return 0 for everything. - */ -static struct gfxinfo *php_handle_avif(php_stream * stream) { - return ecalloc(1, sizeof(struct gfxinfo)); -} -/* }}} */ - -/* {{{ php_ntohl - * Convert a big-endian network uint32 to host order - - * which may be either little-endian or big-endian. - * Thanks to Rob Pike via Joe Drago: - * https://commandcenter.blogspot.nl/2012/04/byte-order-fallacy.html - */ -static uint32_t php_ntohl(uint32_t val) { - uint8_t data[4]; - - memcpy(&data, &val, sizeof(data)); - return ((uint32_t)data[3] << 0) | - ((uint32_t)data[2] << 8) | - ((uint32_t)data[1] << 16) | - ((uint32_t)data[0] << 24); -} -/* }}} */ - -/* {{{ php_is_image_avif - * detect whether an image is of type AVIF - * - * An AVIF image will start off a header "box". - * This starts with with a four-byte integer containing the number of bytes in the filetype box. - * This must be followed by the string "ftyp". - * Next comes a four-byte string indicating the "major brand". - * If that's "avif" or "avis", this is an AVIF image. - * Next, there's a four-byte "minor version" field, which we can ignore. - * Next comes an array of four-byte strings containing "compatible brands". - * These extend to the end of the box. - * If any of the compatible brands is "avif" or "avis", then this is an AVIF image. - * Otherwise, well, it's not. - * For more, see https://mpeg.chiariglione.org/standards/mpeg-4/iso-base-media-file-format/text-isoiec-14496-12-5th-edition - */ -bool php_is_image_avif(php_stream * stream) { - uint32_t header_size_reversed, header_size, i; - char box_type[4], brand[4]; +/* {{{ User struct and stream read/skip implementations for libavifinfo API */ +struct php_avif_stream { + php_stream* stream; + uint8_t buffer[AVIFINFO_MAX_NUM_READ_BYTES]; +}; - ZEND_ASSERT(stream != NULL); +static const uint8_t* php_avif_stream_read(void* stream, size_t num_bytes) { + struct php_avif_stream* avif_stream = (struct php_avif_stream*)stream; - if (php_stream_read(stream, (char *) &header_size_reversed, 4) != 4) { - return 0; + if (avif_stream == NULL || avif_stream->stream == NULL) { + return NULL; } - - header_size = php_ntohl(header_size_reversed); - - /* If the box type isn't "ftyp", it can't be an AVIF image. */ - if (php_stream_read(stream, box_type, 4) != 4) { - return 0; + if (php_stream_read(avif_stream->stream, (char*)avif_stream->buffer, num_bytes) != num_bytes) { + avif_stream->stream = NULL; /* fail further calls */ + return NULL; } + return avif_stream->buffer; +} - if (memcmp(box_type, "ftyp", 4)) { - return 0; - } - - /* If the major brand is "avif" or "avis", it's an AVIF image. */ - if (php_stream_read(stream, brand, 4) != 4) { - return 0; - } +static void php_avif_stream_skip(void* stream, size_t num_bytes) { + struct php_avif_stream* avif_stream = (struct php_avif_stream*)stream; - if (!memcmp(brand, "avif", 4) || !memcmp(brand, "avis", 4)) { - return 1; + if (avif_stream == NULL || avif_stream->stream == NULL) { + return; } - - /* Skip the next four bytes, which are the "minor version". */ - if (php_stream_read(stream, brand, 4) != 4) { - return 0; + if (php_stream_seek(avif_stream->stream, num_bytes, SEEK_CUR)) { + avif_stream->stream = NULL; /* fail further calls */ } +} +/* }}} */ - /* Look for "avif" or "avis" in any member of compatible_brands[], to the end of the header. - Note we've already read four groups of four bytes. */ +/* {{{ php_handle_avif + * Parse AVIF features + * + * The stream must be positioned at the beginning of a box, so it does not + * matter whether the "ftyp" box was already read by php_is_image_avif() or not. + * It will read bytes from the stream until features are found or the file is + * declared as invalid. Around 450 bytes are usually enough. + * Transforms such as mirror and rotation are not applied on width and height. + */ +static struct gfxinfo *php_handle_avif(php_stream * stream) { + struct gfxinfo* result = NULL; + AvifInfoFeatures features; + struct php_avif_stream avif_stream; + avif_stream.stream = stream; + + if (AvifInfoGetFeaturesStream(&avif_stream, php_avif_stream_read, php_avif_stream_skip, &features) == kAvifInfoOk) { + result = (struct gfxinfo*)ecalloc(1, sizeof(struct gfxinfo)); + result->width = features.width; + result->height = features.height; + result->bits = features.bit_depth; + result->channels = features.num_channels; + } + return result; +} +/* }}} */ - for (i = 16; i < header_size; i += 4) { - if (php_stream_read(stream, brand, 4) != 4) { - return 0; - } +/* {{{ php_is_image_avif + * Detect whether an image is of type AVIF + * + * Only the first "ftyp" box is read. + * For a valid file, 12 bytes are usually read, but more might be necessary. + */ +bool php_is_image_avif(php_stream* stream) { + struct php_avif_stream avif_stream; + avif_stream.stream = stream; - if (!memcmp(brand, "avif", 4) || !memcmp(brand, "avis", 4)) { - return 1; - } + if (AvifInfoIdentifyStream(&avif_stream, php_avif_stream_read, php_avif_stream_skip) == kAvifInfoOk) { + return 1; } - return 0; } /* }}} */ diff --git a/ext/standard/libavifinfo/LICENSE b/ext/standard/libavifinfo/LICENSE new file mode 100644 index 0000000000000..330b6b87bb6e8 --- /dev/null +++ b/ext/standard/libavifinfo/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2021, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/ext/standard/libavifinfo/PATENTS b/ext/standard/libavifinfo/PATENTS new file mode 100644 index 0000000000000..826644b635355 --- /dev/null +++ b/ext/standard/libavifinfo/PATENTS @@ -0,0 +1,107 @@ +Alliance for Open Media Patent License 1.0 + +1. License Terms. + +1.1. Patent License. Subject to the terms and conditions of this License, each + Licensor, on behalf of itself and successors in interest and assigns, + grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, + no-charge, royalty-free, irrevocable (except as expressly stated in this + License) patent license to its Necessary Claims to make, use, sell, offer + for sale, import or distribute any Implementation. + +1.2. Conditions. + +1.2.1. Availability. As a condition to the grant of rights to Licensee to make, + sell, offer for sale, import or distribute an Implementation under + Section 1.1, Licensee must make its Necessary Claims available under + this License, and must reproduce this License with any Implementation + as follows: + + a. For distribution in source code, by including this License in the + root directory of the source code with its Implementation. + + b. For distribution in any other form (including binary, object form, + and/or hardware description code (e.g., HDL, RTL, Gate Level Netlist, + GDSII, etc.)), by including this License in the documentation, legal + notices, and/or other written materials provided with the + Implementation. + +1.2.2. Additional Conditions. This license is directly from Licensor to + Licensee. Licensee acknowledges as a condition of benefiting from it + that no rights from Licensor are received from suppliers, distributors, + or otherwise in connection with this License. + +1.3. Defensive Termination. If any Licensee, its Affiliates, or its agents + initiates patent litigation or files, maintains, or voluntarily + participates in a lawsuit against another entity or any person asserting + that any Implementation infringes Necessary Claims, any patent licenses + granted under this License directly to the Licensee are immediately + terminated as of the date of the initiation of action unless 1) that suit + was in response to a corresponding suit regarding an Implementation first + brought against an initiating entity, or 2) that suit was brought to + enforce the terms of this License (including intervention in a third-party + action by a Licensee). + +1.4. Disclaimers. The Reference Implementation and Specification are provided + "AS IS" and without warranty. The entire risk as to implementing or + otherwise using the Reference Implementation or Specification is assumed + by the implementer and user. Licensor expressly disclaims any warranties + (express, implied, or otherwise), including implied warranties of + merchantability, non-infringement, fitness for a particular purpose, or + title, related to the material. IN NO EVENT WILL LICENSOR BE LIABLE TO + ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, + INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF + ACTION OF ANY KIND WITH RESPECT TO THIS LICENSE, WHETHER BASED ON BREACH + OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR + NOT THE OTHER PARTRY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +2. Definitions. + +2.1. Affiliate. "Affiliate" means an entity that directly or indirectly + Controls, is Controlled by, or is under common Control of that party. + +2.2. Control. "Control" means direct or indirect control of more than 50% of + the voting power to elect directors of that corporation, or for any other + entity, the power to direct management of such entity. + +2.3. Decoder. "Decoder" means any decoder that conforms fully with all + non-optional portions of the Specification. + +2.4. Encoder. "Encoder" means any encoder that produces a bitstream that can + be decoded by a Decoder only to the extent it produces such a bitstream. + +2.5. Final Deliverable. "Final Deliverable" means the final version of a + deliverable approved by the Alliance for Open Media as a Final + Deliverable. + +2.6. Implementation. "Implementation" means any implementation, including the + Reference Implementation, that is an Encoder and/or a Decoder. An + Implementation also includes components of an Implementation only to the + extent they are used as part of an Implementation. + +2.7. License. "License" means this license. + +2.8. Licensee. "Licensee" means any person or entity who exercises patent + rights granted under this License. + +2.9. Licensor. "Licensor" means (i) any Licensee that makes, sells, offers + for sale, imports or distributes any Implementation, or (ii) a person + or entity that has a licensing obligation to the Implementation as a + result of its membership and/or participation in the Alliance for Open + Media working group that developed the Specification. + +2.10. Necessary Claims. "Necessary Claims" means all claims of patents or + patent applications, (a) that currently or at any time in the future, + are owned or controlled by the Licensor, and (b) (i) would be an + Essential Claim as defined by the W3C Policy as of February 5, 2004 + (https://www.w3.org/Consortium/Patent-Policy-20040205/#def-essential) + as if the Specification was a W3C Recommendation; or (ii) are infringed + by the Reference Implementation. + +2.11. Reference Implementation. "Reference Implementation" means an Encoder + and/or Decoder released by the Alliance for Open Media as a Final + Deliverable. + +2.12. Specification. "Specification" means the specification designated by + the Alliance for Open Media as a Final Deliverable for which this + License was issued. diff --git a/ext/standard/libavifinfo/README.md b/ext/standard/libavifinfo/README.md new file mode 100644 index 0000000000000..a08c29067f4b9 --- /dev/null +++ b/ext/standard/libavifinfo/README.md @@ -0,0 +1,11 @@ +# AVIF-info + +There is no compact, reliable way to determine the size of an AVIF image. A +standalone C snippet called +[libavifinfo](https://aomedia.googlesource.com/libavifinfo) was created to +partially parse an AVIF payload and to extract the width, height, bit depth and +channel count without depending on the full libavif library. + +`avifinfo.h`, `avifinfo.c`, `LICENSE` and `PATENTS` were copied verbatim from: \ +https://aomedia.googlesource.com/libavifinfo/+/96f34d945ac7dac229feddfa94dbae66e202b838 \ +They can easily be kept up-to-date the same way. diff --git a/ext/standard/libavifinfo/avifinfo.c b/ext/standard/libavifinfo/avifinfo.c new file mode 100644 index 0000000000000..5b93d1997be33 --- /dev/null +++ b/ext/standard/libavifinfo/avifinfo.c @@ -0,0 +1,745 @@ +// Copyright (c) 2021, Alliance for Open Media. All rights reserved +// +// This source code is subject to the terms of the BSD 2 Clause License and +// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License +// was not distributed with this source code in the LICENSE file, you can +// obtain it at www.aomedia.org/license/software. If the Alliance for Open +// Media Patent License 1.0 was not distributed with this source code in the +// PATENTS file, you can obtain it at www.aomedia.org/license/patent. + +#include "avifinfo.h" + +#include +#include +#include + +//------------------------------------------------------------------------------ + +// Status returned when reading the content of a box (or file). +typedef enum { + kFound, // Input correctly parsed and information retrieved. + kNotFound, // Input correctly parsed but information is missing or elsewhere. + kTruncated, // Input correctly parsed until missing bytes to continue. + kAborted, // Input correctly parsed until stopped to avoid timeout or crash. + 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 +// AvifInfoInternalFeatures uses uint8_t to store values and the number of +// values is clamped to 32 to limit the stack size. +#define AVIFINFO_MAX_VALUE UINT8_MAX +#define AVIFINFO_UNDEFINED 0 +// Maximum number of stored associations. Past that, they are skipped. +#define AVIFINFO_MAX_TILES 16 +#define AVIFINFO_MAX_PROPS 32 +#define AVIFINFO_MAX_FEATURES 8 + +// Reads an unsigned integer from 'input' with most significant bits first. +// 'input' must be at least 'num_bytes'-long. +static uint32_t AvifInfoInternalReadBigEndian(const uint8_t* input, + uint32_t num_bytes) { + uint32_t value = 0; + for (uint32_t i = 0; i < num_bytes; ++i) { + value = (value << 8) | input[i]; + } + return value; +} + +//------------------------------------------------------------------------------ +// Convenience macros. + +#if defined(AVIFINFO_LOG_ERROR) // Toggle to log encountered issues. +static void AvifInfoInternalLogError(const char* file, int line, + AvifInfoInternalStatus status) { + const char* kStr[] = {"Found", "NotFound", "Truncated", "Invalid", "Aborted"}; + fprintf(stderr, " %s:%d: %s\n", file, line, kStr[status]); + // Set a breakpoint here to catch the first detected issue. +} +#define AVIFINFO_RETURN(check_status) \ + do { \ + const AvifInfoInternalStatus status_checked = (check_status); \ + if (status_checked != kFound && status_checked != kNotFound) { \ + AvifInfoInternalLogError(__FILE__, __LINE__, status_checked); \ + } \ + return status_checked; \ + } while (0) +#else +#define AVIFINFO_RETURN(check_status) \ + do { \ + return (check_status); \ + } while (0) +#endif + +#define AVIFINFO_CHECK(check_condition, check_status) \ + do { \ + if (!(check_condition)) AVIFINFO_RETURN(check_status); \ + } while (0) +#define AVIFINFO_CHECK_STATUS_IS(check_status, expected_status) \ + do { \ + const AvifInfoInternalStatus status_returned = (check_status); \ + AVIFINFO_CHECK(status_returned == (expected_status), status_returned); \ + } while (0) +#define AVIFINFO_CHECK_FOUND(check_status) \ + AVIFINFO_CHECK_STATUS_IS((check_status), kFound) +#define AVIFINFO_CHECK_NOT_FOUND(check_status) \ + AVIFINFO_CHECK_STATUS_IS((check_status), kNotFound) + +//------------------------------------------------------------------------------ +// Streamed input struct and helper functions. + +typedef struct { + void* stream; // User-defined data. + read_stream_t read; // Used to fetch more bytes from the 'stream'. + skip_stream_t skip; // Used to advance the position in the 'stream'. + // Fallback to 'read' if 'skip' is null. +} AvifInfoInternalStream; + +// Reads 'num_bytes' from the 'stream'. They are available at '*data'. +// 'num_bytes' must be greater than zero. +static AvifInfoInternalStatus AvifInfoInternalRead( + AvifInfoInternalStream* stream, uint32_t num_bytes, const uint8_t** data) { + *data = stream->read(stream->stream, num_bytes); + AVIFINFO_CHECK(*data != NULL, kTruncated); + return kFound; +} + +// Skips 'num_bytes' from the 'stream'. 'num_bytes' can be zero. +static AvifInfoInternalStatus AvifInfoInternalSkip( + AvifInfoInternalStream* stream, uint32_t num_bytes) { + // Avoid a call to the user-defined function for nothing. + if (num_bytes > 0) { + if (stream->skip == NULL) { + const uint8_t* unused; + while (num_bytes > AVIFINFO_MAX_NUM_READ_BYTES) { + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, AVIFINFO_MAX_NUM_READ_BYTES, &unused)); + num_bytes -= AVIFINFO_MAX_NUM_READ_BYTES; + } + return AvifInfoInternalRead(stream, num_bytes, &unused); + } + stream->skip(stream->stream, num_bytes); + } + return kFound; +} + +//------------------------------------------------------------------------------ +// Features are parsed into temporary property associations. + +typedef struct { + uint8_t tile_item_id; + uint8_t parent_item_id; +} AvifInfoInternalTile; // Tile item id <-> parent item id associations. + +typedef struct { + uint8_t property_index; + uint8_t item_id; +} AvifInfoInternalProp; // Property index <-> item id associations. + +typedef struct { + uint8_t property_index; + uint32_t width, height; +} AvifInfoInternalDimProp; // Property <-> features associations. + +typedef struct { + uint8_t property_index; + uint8_t bit_depth, num_channels; +} AvifInfoInternalChanProp; // Property <-> features associations. + +typedef struct { + uint8_t has_primary_item; // True if "pitm" was parsed. + uint8_t has_alpha; // True if an alpha "auxC" was parsed. + uint8_t primary_item_id; + AvifInfoFeatures primary_item_features; // Deduced from the data below. + uint8_t data_was_skipped; // True if some loops/indices were skipped. + + uint8_t num_tiles; + AvifInfoInternalTile tiles[AVIFINFO_MAX_TILES]; + uint8_t num_props; + AvifInfoInternalProp props[AVIFINFO_MAX_PROPS]; + uint8_t num_dim_props; + AvifInfoInternalDimProp dim_props[AVIFINFO_MAX_FEATURES]; + uint8_t num_chan_props; + AvifInfoInternalChanProp chan_props[AVIFINFO_MAX_FEATURES]; +} AvifInfoInternalFeatures; + +// Generates the features of a given 'target_item_id' from internal features. +static AvifInfoInternalStatus AvifInfoInternalGetItemFeatures( + AvifInfoInternalFeatures* f, uint32_t target_item_id, uint32_t tile_depth) { + for (uint32_t prop_item = 0; prop_item < f->num_props; ++prop_item) { + if (f->props[prop_item].item_id != target_item_id) continue; + const uint32_t property_index = f->props[prop_item].property_index; + + // Retrieve the width and height of the primary item if not already done. + if (target_item_id == f->primary_item_id && + (f->primary_item_features.width == AVIFINFO_UNDEFINED || + f->primary_item_features.height == AVIFINFO_UNDEFINED)) { + for (uint32_t i = 0; i < f->num_dim_props; ++i) { + if (f->dim_props[i].property_index != property_index) continue; + f->primary_item_features.width = f->dim_props[i].width; + f->primary_item_features.height = f->dim_props[i].height; + if (f->primary_item_features.bit_depth != AVIFINFO_UNDEFINED && + f->primary_item_features.num_channels != AVIFINFO_UNDEFINED) { + return kFound; + } + break; + } + } + // Retrieve the bit depth and number of channels of the target item if not + // already done. + if (f->primary_item_features.bit_depth == AVIFINFO_UNDEFINED || + f->primary_item_features.num_channels == AVIFINFO_UNDEFINED) { + for (uint32_t i = 0; i < f->num_chan_props; ++i) { + if (f->chan_props[i].property_index != property_index) continue; + f->primary_item_features.bit_depth = f->chan_props[i].bit_depth; + f->primary_item_features.num_channels = f->chan_props[i].num_channels; + if (f->primary_item_features.width != AVIFINFO_UNDEFINED && + f->primary_item_features.height != AVIFINFO_UNDEFINED) { + return kFound; + } + break; + } + } + } + + // Check for the bit_depth and num_channels in a tile if not yet found. + for (uint32_t tile = 0; tile < f->num_tiles && tile_depth < 3; ++tile) { + if (f->tiles[tile].parent_item_id != target_item_id) continue; + AVIFINFO_CHECK_NOT_FOUND(AvifInfoInternalGetItemFeatures( + f, f->tiles[tile].tile_item_id, tile_depth + 1)); + } + AVIFINFO_RETURN(kNotFound); +} + +// Generates the 'f->primary_item_features' from the AvifInfoInternalFeatures. +// Returns kNotFound if there is not enough information. +static AvifInfoInternalStatus AvifInfoInternalGetPrimaryItemFeatures( + AvifInfoInternalFeatures* f) { + // Nothing to do without the primary item ID. + AVIFINFO_CHECK(f->has_primary_item, kNotFound); + // Early exit. + AVIFINFO_CHECK(f->num_dim_props > 0 && f->num_chan_props, kNotFound); + AVIFINFO_CHECK_FOUND( + AvifInfoInternalGetItemFeatures(f, f->primary_item_id, /*tile_depth=*/0)); + + // "auxC" is parsed before the "ipma" properties so it is known now, if any. + if (f->has_alpha) ++f->primary_item_features.num_channels; + return kFound; +} + +//------------------------------------------------------------------------------ +// Box header parsing and various size checks. + +typedef struct { + uint32_t size; // In bytes. + uint8_t type[4]; // Four characters. + uint32_t version; // 0 or actual version if this is a full box. + uint32_t flags; // 0 or actual value if this is a full box. + uint32_t content_size; // 'size' minus the header size. +} AvifInfoInternalBox; + +// Reads the header of a 'box' starting at the beginning of a 'stream'. +// 'num_remaining_bytes' is the remaining size of the container of the 'box' +// (either the file size itself or the content size of the parent of the 'box'). +static AvifInfoInternalStatus AvifInfoInternalParseBox( + AvifInfoInternalStream* stream, uint32_t num_remaining_bytes, + uint32_t* num_parsed_boxes, AvifInfoInternalBox* box) { + const uint8_t* data; + // See ISO/IEC 14496-12:2012(E) 4.2 + uint32_t box_header_size = 8; // box 32b size + 32b type (at least) + AVIFINFO_CHECK(box_header_size <= num_remaining_bytes, kInvalid); + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 8, &data)); + box->size = AvifInfoInternalReadBigEndian(data, sizeof(uint32_t)); + memcpy(box->type, data + 4, 4); + // 'box->size==1' means 64-bit size should be read after the box type. + // 'box->size==0' means this box extends to all remaining bytes. + if (box->size == 1) { + box_header_size += 8; + AVIFINFO_CHECK(box_header_size <= num_remaining_bytes, kInvalid); + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 8, &data)); + // Stop the parsing if any box has a size greater than 4GB. + AVIFINFO_CHECK(AvifInfoInternalReadBigEndian(data, sizeof(uint32_t)) == 0, + kAborted); + // Read the 32 least-significant bits. + box->size = AvifInfoInternalReadBigEndian(data + 4, sizeof(uint32_t)); + } else if (box->size == 0) { + box->size = num_remaining_bytes; + } + AVIFINFO_CHECK(box->size >= box_header_size, kInvalid); + AVIFINFO_CHECK(box->size <= num_remaining_bytes, kInvalid); + + const int has_fullbox_header = + !memcmp(box->type, "meta", 4) || !memcmp(box->type, "pitm", 4) || + !memcmp(box->type, "ipma", 4) || !memcmp(box->type, "ispe", 4) || + !memcmp(box->type, "pixi", 4) || !memcmp(box->type, "iref", 4) || + !memcmp(box->type, "auxC", 4); + if (has_fullbox_header) box_header_size += 4; + AVIFINFO_CHECK(box->size >= box_header_size, kInvalid); + box->content_size = box->size - box_header_size; + // Avoid timeouts. The maximum number of parsed boxes is arbitrary. + ++*num_parsed_boxes; + AVIFINFO_CHECK(*num_parsed_boxes < 4096, kAborted); + + box->version = 0; + box->flags = 0; + if (has_fullbox_header) { + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data)); + box->version = AvifInfoInternalReadBigEndian(data, 1); + box->flags = AvifInfoInternalReadBigEndian(data + 1, 3); + // See AV1 Image File Format (AVIF) 8.1 + // at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when + // https://github.com/AOMediaCodec/av1-avif/pull/170 is merged). + uint32_t is_parsable = 1; + if (!memcmp(box->type, "meta", 4)) is_parsable = (box->version <= 0); + if (!memcmp(box->type, "pitm", 4)) is_parsable = (box->version <= 1); + if (!memcmp(box->type, "ipma", 4)) is_parsable = (box->version <= 1); + if (!memcmp(box->type, "ispe", 4)) is_parsable = (box->version <= 0); + if (!memcmp(box->type, "pixi", 4)) is_parsable = (box->version <= 0); + if (!memcmp(box->type, "iref", 4)) is_parsable = (box->version <= 1); + if (!memcmp(box->type, "auxC", 4)) is_parsable = (box->version <= 0); + // Instead of considering this file as invalid, skip unparsable boxes. + if (!is_parsable) memcpy(box->type, "\0skp", 4); // \0 so not a valid type + } + return kFound; +} + +//------------------------------------------------------------------------------ + +// Parses a 'stream' of an "ipco" box into 'features'. +// "ispe" is used for width and height, "pixi" and "av1C" are used for bit depth +// and number of channels, and "auxC" is used for alpha. +static AvifInfoInternalStatus ParseIpco(AvifInfoInternalStream* stream, + uint32_t num_remaining_bytes, + uint32_t* num_parsed_boxes, + AvifInfoInternalFeatures* features) { + uint32_t box_index = 1; // 1-based index. Used for iterating over properties. + do { + AvifInfoInternalBox box; + AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes, + num_parsed_boxes, &box)); + + if (!memcmp(box.type, "ispe", 4)) { + // See ISO/IEC 23008-12:2017(E) 6.5.3.2 + const uint8_t* data; + AVIFINFO_CHECK(box.content_size >= 8, kInvalid); + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 8, &data)); + const uint32_t width = AvifInfoInternalReadBigEndian(data + 0, 4); + const uint32_t height = AvifInfoInternalReadBigEndian(data + 4, 4); + AVIFINFO_CHECK(width != 0 && height != 0, kInvalid); + if (features->num_dim_props < AVIFINFO_MAX_FEATURES && + box_index <= AVIFINFO_MAX_VALUE) { + features->dim_props[features->num_dim_props].property_index = box_index; + features->dim_props[features->num_dim_props].width = width; + features->dim_props[features->num_dim_props].height = height; + ++features->num_dim_props; + } else { + features->data_was_skipped = 1; + } + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size - 8)); + } else if (!memcmp(box.type, "pixi", 4)) { + // See ISO/IEC 23008-12:2017(E) 6.5.6.2 + const uint8_t* data; + AVIFINFO_CHECK(box.content_size >= 1, kInvalid); + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 1, &data)); + const uint32_t num_channels = AvifInfoInternalReadBigEndian(data + 0, 1); + AVIFINFO_CHECK(num_channels >= 1, kInvalid); + AVIFINFO_CHECK(box.content_size >= 1 + num_channels, kInvalid); + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 1, &data)); + const uint32_t bit_depth = AvifInfoInternalReadBigEndian(data, 1); + AVIFINFO_CHECK(bit_depth >= 1, kInvalid); + for (uint32_t i = 1; i < num_channels; ++i) { + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 1, &data)); + // Bit depth should be the same for all channels. + AVIFINFO_CHECK(AvifInfoInternalReadBigEndian(data, 1) == bit_depth, + kInvalid); + AVIFINFO_CHECK(i <= 32, kAborted); // Be reasonable. + } + if (features->num_chan_props < AVIFINFO_MAX_FEATURES && + box_index <= AVIFINFO_MAX_VALUE && bit_depth <= AVIFINFO_MAX_VALUE && + num_channels <= AVIFINFO_MAX_VALUE) { + features->chan_props[features->num_chan_props].property_index = + box_index; + features->chan_props[features->num_chan_props].bit_depth = bit_depth; + features->chan_props[features->num_chan_props].num_channels = + num_channels; + ++features->num_chan_props; + } else { + features->data_was_skipped = 1; + } + AVIFINFO_CHECK_FOUND( + AvifInfoInternalSkip(stream, box.content_size - (1 + num_channels))); + } else if (!memcmp(box.type, "av1C", 4)) { + // See AV1 Codec ISO Media File Format Binding 2.3.1 + // at https://aomediacodec.github.io/av1-isobmff/#av1c + // Only parse the necessary third byte. Assume that the others are valid. + const uint8_t* data; + AVIFINFO_CHECK(box.content_size >= 3, kInvalid); + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 3, &data)); + const int high_bitdepth = (data[2] & 0x40) != 0; + const int twelve_bit = (data[2] & 0x20) != 0; + const int monochrome = (data[2] & 0x10) != 0; + if (twelve_bit) { + AVIFINFO_CHECK(high_bitdepth, kInvalid); + } + if (features->num_chan_props < AVIFINFO_MAX_FEATURES && + box_index <= AVIFINFO_MAX_VALUE) { + features->chan_props[features->num_chan_props].property_index = + box_index; + features->chan_props[features->num_chan_props].bit_depth = + high_bitdepth ? twelve_bit ? 12 : 10 : 8; + features->chan_props[features->num_chan_props].num_channels = + monochrome ? 1 : 3; + ++features->num_chan_props; + } else { + features->data_was_skipped = 1; + } + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size - 3)); + } else if (!memcmp(box.type, "auxC", 4)) { + // See AV1 Image File Format (AVIF) 4 + // at https://aomediacodec.github.io/av1-avif/#auxiliary-images + const char* kAlphaStr = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; + const uint32_t kAlphaStrLength = 44; // Includes terminating character. + if (box.content_size >= kAlphaStrLength) { + const uint8_t* data; + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, kAlphaStrLength, &data)); + const char* const aux_type = (const char*)data; + if (strcmp(aux_type, kAlphaStr) == 0) { + // Note: It is unlikely but it is possible that this alpha plane does + // not belong to the primary item or a tile. Ignore this issue. + features->has_alpha = 1; + } + AVIFINFO_CHECK_FOUND( + AvifInfoInternalSkip(stream, box.content_size - kAlphaStrLength)); + } else { + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); + } + } else { + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); + } + ++box_index; + num_remaining_bytes -= box.size; + } while (num_remaining_bytes > 0); + AVIFINFO_RETURN(kNotFound); +} + +// Parses a 'stream' of an "iprp" box into 'features'. The "ipco" box contain +// the properties which are linked to items by the "ipma" box. +static AvifInfoInternalStatus ParseIprp(AvifInfoInternalStream* stream, + uint32_t num_remaining_bytes, + uint32_t* num_parsed_boxes, + AvifInfoInternalFeatures* features) { + do { + AvifInfoInternalBox box; + AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes, + num_parsed_boxes, &box)); + + if (!memcmp(box.type, "ipco", 4)) { + AVIFINFO_CHECK_NOT_FOUND( + ParseIpco(stream, box.content_size, num_parsed_boxes, features)); + } else if (!memcmp(box.type, "ipma", 4)) { + // See ISO/IEC 23008-12:2017(E) 9.3.2 + uint32_t num_read_bytes = 4; + const uint8_t* data; + AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid); + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data)); + const uint32_t entry_count = AvifInfoInternalReadBigEndian(data, 4); + const uint32_t id_num_bytes = (box.version < 1) ? 2 : 4; + const uint32_t index_num_bytes = (box.flags & 1) ? 2 : 1; + const uint32_t essential_bit_mask = (box.flags & 1) ? 0x8000 : 0x80; + + for (uint32_t entry = 0; entry < entry_count; ++entry) { + if (entry >= AVIFINFO_MAX_PROPS || + features->num_props >= AVIFINFO_MAX_PROPS) { + features->data_was_skipped = 1; + break; + } + num_read_bytes += id_num_bytes + 1; + AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid); + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, id_num_bytes + 1, &data)); + const uint32_t item_id = + AvifInfoInternalReadBigEndian(data, id_num_bytes); + const uint32_t association_count = + AvifInfoInternalReadBigEndian(data + id_num_bytes, 1); + + uint32_t property; + for (property = 0; property < association_count; ++property) { + if (property >= AVIFINFO_MAX_PROPS || + features->num_props >= AVIFINFO_MAX_PROPS) { + features->data_was_skipped = 1; + break; + } + num_read_bytes += index_num_bytes; + AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid); + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, index_num_bytes, &data)); + const uint32_t value = + AvifInfoInternalReadBigEndian(data, index_num_bytes); + // const int essential = (value & essential_bit_mask); // Unused. + const uint32_t property_index = (value & ~essential_bit_mask); + if (property_index <= AVIFINFO_MAX_VALUE && + item_id <= AVIFINFO_MAX_VALUE) { + features->props[features->num_props].property_index = + property_index; + features->props[features->num_props].item_id = item_id; + ++features->num_props; + } else { + features->data_was_skipped = 1; + } + } + if (property < association_count) break; // Do not read garbage. + } + + // If all features are available now, do not look further. + AVIFINFO_CHECK_NOT_FOUND( + AvifInfoInternalGetPrimaryItemFeatures(features)); + + AVIFINFO_CHECK_FOUND( + AvifInfoInternalSkip(stream, box.content_size - num_read_bytes)); + } else { + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); + } + num_remaining_bytes -= box.size; + } while (num_remaining_bytes != 0); + AVIFINFO_RETURN(kNotFound); +} + +//------------------------------------------------------------------------------ + +// Parses a 'stream' of an "iref" box into 'features'. +// The "dimg" boxes contain links between tiles and their parent items, which +// can be used to infer bit depth and number of channels for the primary item +// when the latter does not have these properties. +static AvifInfoInternalStatus ParseIref(AvifInfoInternalStream* stream, + uint32_t num_remaining_bytes, + uint32_t* num_parsed_boxes, + AvifInfoInternalFeatures* features) { + do { + AvifInfoInternalBox box; + AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes, + num_parsed_boxes, &box)); + + if (!memcmp(box.type, "dimg", 4)) { + // See ISO/IEC 14496-12:2015(E) 8.11.12.2 + const uint32_t num_bytes_per_id = (box.version == 0) ? 2 : 4; + uint32_t num_read_bytes = num_bytes_per_id + 2; + const uint8_t* data; + AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid); + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, num_bytes_per_id + 2, &data)); + const uint32_t from_item_id = + AvifInfoInternalReadBigEndian(data, num_bytes_per_id); + const uint32_t reference_count = + AvifInfoInternalReadBigEndian(data + num_bytes_per_id, 2); + + for (uint32_t i = 0; i < reference_count; ++i) { + if (i >= AVIFINFO_MAX_TILES) { + features->data_was_skipped = 1; + break; + } + num_read_bytes += num_bytes_per_id; + AVIFINFO_CHECK(box.content_size >= num_read_bytes, kInvalid); + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, num_bytes_per_id, &data)); + const uint32_t to_item_id = + AvifInfoInternalReadBigEndian(data, num_bytes_per_id); + if (from_item_id <= AVIFINFO_MAX_VALUE && + to_item_id <= AVIFINFO_MAX_VALUE && + features->num_tiles < AVIFINFO_MAX_TILES) { + features->tiles[features->num_tiles].tile_item_id = to_item_id; + features->tiles[features->num_tiles].parent_item_id = from_item_id; + ++features->num_tiles; + } else { + features->data_was_skipped = 1; + } + } + + // If all features are available now, do not look further. + AVIFINFO_CHECK_NOT_FOUND( + AvifInfoInternalGetPrimaryItemFeatures(features)); + } else { + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); + } + num_remaining_bytes -= box.size; + } while (num_remaining_bytes > 0); + AVIFINFO_RETURN(kNotFound); +} + +//------------------------------------------------------------------------------ + +// Parses a 'stream' of a "meta" box. It looks for the primary item ID in the +// "pitm" box and recurses into other boxes to find its 'features'. +static AvifInfoInternalStatus ParseMeta(AvifInfoInternalStream* stream, + uint32_t num_remaining_bytes, + uint32_t* num_parsed_boxes, + AvifInfoInternalFeatures* features) { + do { + AvifInfoInternalBox box; + AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, num_remaining_bytes, + num_parsed_boxes, &box)); + + if (!memcmp(box.type, "pitm", 4)) { + // See ISO/IEC 14496-12:2015(E) 8.11.4.2 + const uint32_t num_bytes_per_id = (box.version == 0) ? 2 : 4; + const uint8_t* data; + AVIFINFO_CHECK(num_bytes_per_id <= num_remaining_bytes, kInvalid); + AVIFINFO_CHECK_FOUND( + AvifInfoInternalRead(stream, num_bytes_per_id, &data)); + const uint32_t primary_item_id = + AvifInfoInternalReadBigEndian(data, num_bytes_per_id); + AVIFINFO_CHECK(primary_item_id <= AVIFINFO_MAX_VALUE, kAborted); + features->has_primary_item = 1; + features->primary_item_id = primary_item_id; + AVIFINFO_CHECK_FOUND( + AvifInfoInternalSkip(stream, box.content_size - num_bytes_per_id)); + } else if (!memcmp(box.type, "iprp", 4)) { + AVIFINFO_CHECK_NOT_FOUND( + ParseIprp(stream, box.content_size, num_parsed_boxes, features)); + } else if (!memcmp(box.type, "iref", 4)) { + AVIFINFO_CHECK_NOT_FOUND( + ParseIref(stream, box.content_size, num_parsed_boxes, features)); + } else { + AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); + } + num_remaining_bytes -= box.size; + } while (num_remaining_bytes != 0); + // According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one "meta". + AVIFINFO_RETURN(features->data_was_skipped ? kAborted : kInvalid); +} + +//------------------------------------------------------------------------------ + +// 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) { + while (1) { + AvifInfoInternalBox box; + AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE, + num_parsed_boxes, &box)); + 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)); + } + } + AVIFINFO_RETURN(kInvalid); // No "meta" no good. +} + +//------------------------------------------------------------------------------ +// Helpers for converting the fixed-size input public API to the streamed one. + +typedef struct { + const uint8_t* data; + size_t data_size; +} AvifInfoInternalForward; + +static const uint8_t* AvifInfoInternalForwardRead(void* stream, + size_t num_bytes) { + AvifInfoInternalForward* forward = (AvifInfoInternalForward*)stream; + if (num_bytes > forward->data_size) return NULL; + const uint8_t* data = forward->data; + forward->data += num_bytes; + forward->data_size -= num_bytes; + return data; +} + +static void AvifInfoInternalForwardSkip(void* stream, size_t num_bytes) { + AvifInfoInternalForward* forward = (AvifInfoInternalForward*)stream; + if (num_bytes > forward->data_size) num_bytes = forward->data_size; + forward->data += num_bytes; + forward->data_size -= num_bytes; +} + +//------------------------------------------------------------------------------ +// Fixed-size input public API + +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 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 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; + + AvifInfoInternalStream internal_stream; + internal_stream.stream = stream; + internal_stream.read = read; + internal_stream.skip = skip; // Fallbacks to 'read' if null. + uint32_t num_parsed_boxes = 0; + AvifInfoInternalFeatures internal_features; + memset(&internal_features, AVIFINFO_UNDEFINED, sizeof(internal_features)); + + // Go through all relevant boxes sequentially. + const AvifInfoInternalStatus status = + ParseFile(&internal_stream, &num_parsed_boxes, &internal_features); + if (status == kFound && features != NULL) { + memcpy(features, &internal_features.primary_item_features, + sizeof(*features)); + } + return AvifInfoInternalConvertStatus(status); +} diff --git a/ext/standard/libavifinfo/avifinfo.h b/ext/standard/libavifinfo/avifinfo.h new file mode 100644 index 0000000000000..4c898c2efcc5c --- /dev/null +++ b/ext/standard/libavifinfo/avifinfo.h @@ -0,0 +1,92 @@ +// Copyright (c) 2021, Alliance for Open Media. All rights reserved +// +// This source code is subject to the terms of the BSD 2 Clause License and +// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License +// was not distributed with this source code in the LICENSE file, you can +// obtain it at www.aomedia.org/license/software. If the Alliance for Open +// Media Patent License 1.0 was not distributed with this source code in the +// PATENTS file, you can obtain it at www.aomedia.org/license/patent. + +#ifndef AVIFINFO_H_ +#define AVIFINFO_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +//------------------------------------------------------------------------------ + +typedef enum { + kAvifInfoOk, // The file was correctly parsed and the requested + // information was extracted. It is not guaranteed + // that the input bitstream is a valid complete + // AVIF file. + kAvifInfoNotEnoughData, // The input bitstream was correctly parsed until + // now but bytes are missing. The request should be + // repeated with more input bytes. + kAvifInfoTooComplex, // The input bitstream was correctly parsed until + // now but it is too complex. The parsing was + // stopped to avoid any timeout or crash. + kAvifInfoInvalidFile, // The input bitstream is not a valid AVIF file, + // truncated or not. +} AvifInfoStatus; + +typedef struct { + uint32_t width, height; // In number of pixels. Ignores mirror and rotation. + uint32_t bit_depth; // Likely 8, 10 or 12 bits per channel per pixel. + uint32_t num_channels; // Likely 1, 2, 3 or 4 channels: + // (1 monochrome or 3 colors) + (0 or 1 alpha) +} AvifInfoFeatures; + +//------------------------------------------------------------------------------ +// Fixed-size input API +// Use this API if a raw byte array of fixed size is available as input. + +// 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 AvifInfoGetFeatures(const uint8_t* data, size_t data_size, + AvifInfoFeatures* features); + +//------------------------------------------------------------------------------ +// Streamed input API +// Use this API if the input bytes must be fetched and/or if the AVIF payload +// size is unknown. Implement the two function signatures below and pass them to +// AvifInfoRead*() with a 'stream', which can be anything (file, struct etc.). + +// Reads 'num_bytes' from the 'stream'. +// The position in the 'stream' must be advanced by 'num_bytes'. +// Returns a pointer to the 'num_bytes' or null if it cannot be fulfilled. +// The returned data must remain valid until the next read. +typedef const uint8_t* (*read_stream_t)(void* stream, size_t num_bytes); +// Advances the position in the 'stream' by 'num_bytes'. +typedef void (*skip_stream_t)(void* stream, size_t num_bytes); + +// Maximum number of bytes requested per read. There is no limit per skip. +#define AVIFINFO_MAX_NUM_READ_BYTES 64 + +// 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 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); + +//------------------------------------------------------------------------------ + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // AVIFINFO_H_ diff --git a/ext/standard/tests/image/getimagesize.phpt b/ext/standard/tests/image/getimagesize.phpt index 0a1002d2934a1..472be1d25e366 100644 --- a/ext/standard/tests/image/getimagesize.phpt +++ b/ext/standard/tests/image/getimagesize.phpt @@ -70,15 +70,19 @@ array(17) { string(9) "image/bmp" } ["test1pix.avif"]=> - array(5) { + array(7) { [0]=> - int(0) + int(102) [1]=> - int(0) + int(121) [2]=> int(19) [3]=> - string(20) "width="0" height="0"" + string(24) "width="102" height="121"" + ["bits"]=> + int(8) + ["channels"]=> + int(4) ["mime"]=> string(10) "image/avif" }