Diff: STRATO-apps/wordpress_03/app/wp-includes/class-avif-info.php
Keine Baseline-Datei – Diff nur gegen leer.
1
-
1
+
<?php
2
+
/**
3
+
* Copyright (c) 2021, Alliance for Open Media. All rights reserved
4
+
*
5
+
* This source code is subject to the terms of the BSD 2 Clause License and
6
+
* the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
7
+
* was not distributed with this source code in the LICENSE file, you can
8
+
* obtain it at www.aomedia.org/license/software. If the Alliance for Open
9
+
* Media Patent License 1.0 was not distributed with this source code in the
10
+
* PATENTS file, you can obtain it at www.aomedia.org/license/patent.
11
+
*
12
+
* Note: this class is from libavifinfo - https://aomedia.googlesource.com/libavifinfo/+/refs/heads/main/avifinfo.php at f509487.
13
+
* It is used as a fallback to parse AVIF files when the server doesn't support AVIF,
14
+
* primarily to identify the width and height of the image.
15
+
*
16
+
* Note PHP 8.2 added native support for AVIF, so this class can be removed when WordPress requires PHP 8.2.
17
+
*/
18
+
19
+
namespace Avifinfo;
20
+
21
+
const FOUND = 0; // Input correctly parsed and information retrieved.
22
+
const NOT_FOUND = 1; // Input correctly parsed but information is missing or elsewhere.
23
+
const TRUNCATED = 2; // Input correctly parsed until missing bytes to continue.
24
+
const ABORTED = 3; // Input correctly parsed until stopped to avoid timeout or crash.
25
+
const INVALID = 4; // Input incorrectly parsed.
26
+
27
+
const MAX_SIZE = 4294967295; // Unlikely to be insufficient to parse AVIF headers.
28
+
const MAX_NUM_BOXES = 4096; // Be reasonable. Avoid timeouts and out-of-memory.
29
+
const MAX_VALUE = 255;
30
+
const MAX_TILES = 16;
31
+
const MAX_PROPS = 32;
32
+
const MAX_FEATURES = 8;
33
+
const UNDEFINED = 0; // Value was not yet parsed.
34
+
35
+
/**
36
+
* Reads an unsigned integer with most significant bits first.
37
+
*
38
+
* @param binary string $input Must be at least $num_bytes-long.
39
+
* @param int $num_bytes Number of parsed bytes.
40
+
* @return int Value.
41
+
*/
42
+
function read_big_endian( $input, $num_bytes ) {
43
+
if ( $num_bytes == 1 ) {
44
+
return unpack( 'C', $input ) [1];
45
+
} else if ( $num_bytes == 2 ) {
46
+
return unpack( 'n', $input ) [1];
47
+
} else if ( $num_bytes == 3 ) {
48
+
$bytes = unpack( 'C3', $input );
49
+
return ( $bytes[1] << 16 ) | ( $bytes[2] << 8 ) | $bytes[3];
50
+
} else { // $num_bytes is 4
51
+
// This might fail to read unsigned values >= 2^31 on 32-bit systems.
52
+
// See https://www.php.net/manual/en/function.unpack.php#106041
53
+
return unpack( 'N', $input ) [1];
54
+
}
55
+
}
56
+
57
+
/**
58
+
* Reads bytes and advances the stream position by the same count.
59
+
*
60
+
* @param stream $handle Bytes will be read from this resource.
61
+
* @param int $num_bytes Number of bytes read. Must be greater than 0.
62
+
* @return binary string|false The raw bytes or false on failure.
63
+
*/
64
+
function read( $handle, $num_bytes ) {
65
+
$data = fread( $handle, $num_bytes );
66
+
return ( $data !== false && strlen( $data ) >= $num_bytes ) ? $data : false;
67
+
}
68
+
69
+
/**
70
+
* Advances the stream position by the given offset.
71
+
*
72
+
* @param stream $handle Bytes will be skipped from this resource.
73
+
* @param int $num_bytes Number of skipped bytes. Can be 0.
74
+
* @return bool True on success or false on failure.
75
+
*/
76
+
// Skips 'num_bytes' from the 'stream'. 'num_bytes' can be zero.
77
+
function skip( $handle, $num_bytes ) {
78
+
return ( fseek( $handle, $num_bytes, SEEK_CUR ) == 0 );
79
+
}
80
+
81
+
//------------------------------------------------------------------------------
82
+
// Features are parsed into temporary property associations.
83
+
84
+
class Tile { // Tile item id <-> parent item id associations.
85
+
public $tile_item_id;
86
+
public $parent_item_id;
87
+
}
88
+
89
+
class Prop { // Property index <-> item id associations.
90
+
public $property_index;
91
+
public $item_id;
92
+
}
93
+
94
+
class Dim_Prop { // Property <-> features associations.
95
+
public $property_index;
96
+
public $width;
97
+
public $height;
98
+
}
99
+
100
+
class Chan_Prop { // Property <-> features associations.
101
+
public $property_index;
102
+
public $bit_depth;
103
+
public $num_channels;
104
+
}
105
+
106
+
class Features {
107
+
public $has_primary_item = false; // True if "pitm" was parsed.
108
+
public $has_alpha = false; // True if an alpha "auxC" was parsed.
109
+
public $primary_item_id;
110
+
public $primary_item_features = array( // Deduced from the data below.
111
+
'width' => UNDEFINED, // In number of pixels.
112
+
'height' => UNDEFINED, // Ignores mirror and rotation.
113
+
'bit_depth' => UNDEFINED, // Likely 8, 10 or 12 bits per channel per pixel.
114
+
'num_channels' => UNDEFINED // Likely 1, 2, 3 or 4 channels:
115
+
// (1 monochrome or 3 colors) + (0 or 1 alpha)
116
+
);
117
+
118
+
public $tiles = array(); // Tile[]
119
+
public $props = array(); // Prop[]
120
+
public $dim_props = array(); // Dim_Prop[]
121
+
public $chan_props = array(); // Chan_Prop[]
122
+
123
+
/**
124
+
* Binds the width, height, bit depth and number of channels from stored internal features.
125
+
*
126
+
* @param int $target_item_id Id of the item whose features will be bound.
127
+
* @param int $tile_depth Maximum recursion to search within tile-parent relations.
128
+
* @return Status FOUND on success or NOT_FOUND on failure.
129
+
*/
130
+
private function get_item_features( $target_item_id, $tile_depth ) {
131
+
foreach ( $this->props as $prop ) {
132
+
if ( $prop->item_id != $target_item_id ) {
133
+
continue;
134
+
}
135
+
136
+
// Retrieve the width and height of the primary item if not already done.
137
+
if ( $target_item_id == $this->primary_item_id &&
138
+
( $this->primary_item_features['width'] == UNDEFINED ||
139
+
$this->primary_item_features['height'] == UNDEFINED ) ) {
140
+
foreach ( $this->dim_props as $dim_prop ) {
141
+
if ( $dim_prop->property_index != $prop->property_index ) {
142
+
continue;
143
+
}
144
+
$this->primary_item_features['width'] = $dim_prop->width;
145
+
$this->primary_item_features['height'] = $dim_prop->height;
146
+
if ( $this->primary_item_features['bit_depth'] != UNDEFINED &&
147
+
$this->primary_item_features['num_channels'] != UNDEFINED ) {
148
+
return FOUND;
149
+
}
150
+
break;
151
+
}
152
+
}
153
+
// Retrieve the bit depth and number of channels of the target item if not
154
+
// already done.
155
+
if ( $this->primary_item_features['bit_depth'] == UNDEFINED ||
156
+
$this->primary_item_features['num_channels'] == UNDEFINED ) {
157
+
foreach ( $this->chan_props as $chan_prop ) {
158
+
if ( $chan_prop->property_index != $prop->property_index ) {
159
+
continue;
160
+
}
161
+
$this->primary_item_features['bit_depth'] = $chan_prop->bit_depth;
162
+
$this->primary_item_features['num_channels'] = $chan_prop->num_channels;
163
+
if ( $this->primary_item_features['width'] != UNDEFINED &&
164
+
$this->primary_item_features['height'] != UNDEFINED ) {
165
+
return FOUND;
166
+
}
167
+
break;
168
+
}
169
+
}
170
+
}
171
+
172
+
// Check for the bit_depth and num_channels in a tile if not yet found.
173
+
if ( $tile_depth < 3 ) {
174
+
foreach ( $this->tiles as $tile ) {
175
+
if ( $tile->parent_item_id != $target_item_id ) {
176
+
continue;
177
+
}
178
+
$status = $this->get_item_features( $tile->tile_item_id, $tile_depth + 1 );
179
+
if ( $status != NOT_FOUND ) {
180
+
return $status;
181
+
}
182
+
}
183
+
}
184
+
return NOT_FOUND;
185
+
}
186
+
187
+
/**
188
+
* Finds the width, height, bit depth and number of channels of the primary item.
189
+
*
190
+
* @return Status FOUND on success or NOT_FOUND on failure.
191
+
*/
192
+
public function get_primary_item_features() {
193
+
// Nothing to do without the primary item ID.
194
+
if ( !$this->has_primary_item ) {
195
+
return NOT_FOUND;
196
+
}
197
+
// Early exit.
198
+
if ( empty( $this->dim_props ) || empty( $this->chan_props ) ) {
199
+
return NOT_FOUND;
200
+
}
201
+
$status = $this->get_item_features( $this->primary_item_id, /*tile_depth=*/ 0 );
202
+
if ( $status != FOUND ) {
203
+
return $status;
204
+
}
205
+
206
+
// "auxC" is parsed before the "ipma" properties so it is known now, if any.
207
+
if ( $this->has_alpha ) {
208
+
++$this->primary_item_features['num_channels'];
209
+
}
210
+
return FOUND;
211
+
}
212
+
}
213
+
214
+
//------------------------------------------------------------------------------
215
+
216
+
class Box {
217
+
public $size; // In bytes.
218
+
public $type; // Four characters.
219
+
public $version; // 0 or actual version if this is a full box.
220
+
public $flags; // 0 or actual value if this is a full box.
221
+
public $content_size; // 'size' minus the header size.
222
+
223
+
/**
224
+
* Reads the box header.
225
+
*
226
+
* @param stream $handle The resource the header will be parsed from.
227
+
* @param int $num_parsed_boxes The total number of parsed boxes. Prevents timeouts.
228
+
* @param int $num_remaining_bytes The number of bytes that should be available from the resource.
229
+
* @return Status FOUND on success or an error on failure.
230
+
*/
231
+
public function parse( $handle, &$num_parsed_boxes, $num_remaining_bytes = MAX_SIZE ) {
232
+
// See ISO/IEC 14496-12:2012(E) 4.2
233
+
$header_size = 8; // box 32b size + 32b type (at least)
234
+
if ( $header_size > $num_remaining_bytes ) {
235
+
return INVALID;
236
+
}
237
+
if ( !( $data = read( $handle, 8 ) ) ) {
238
+
return TRUNCATED;
239
+
}
240
+
$this->size = read_big_endian( $data, 4 );
241
+
$this->type = substr( $data, 4, 4 );
242
+
// 'box->size==1' means 64-bit size should be read after the box type.
243
+
// 'box->size==0' means this box extends to all remaining bytes.
244
+
if ( $this->size == 1 ) {
245
+
$header_size += 8;
246
+
if ( $header_size > $num_remaining_bytes ) {
247
+
return INVALID;
248
+
}
249
+
if ( !( $data = read( $handle, 8 ) ) ) {
250
+
return TRUNCATED;
251
+
}
252
+
// Stop the parsing if any box has a size greater than 4GB.
253
+
if ( read_big_endian( $data, 4 ) != 0 ) {
254
+
return ABORTED;
255
+
}
256
+
// Read the 32 least-significant bits.
257
+
$this->size = read_big_endian( substr( $data, 4, 4 ), 4 );
258
+
} else if ( $this->size == 0 ) {
259
+
$this->size = $num_remaining_bytes;
260
+
}
261
+
if ( $this->size < $header_size ) {
262
+
return INVALID;
263
+
}
264
+
if ( $this->size > $num_remaining_bytes ) {
265
+
return INVALID;
266
+
}
267
+
268
+
$has_fullbox_header = $this->type == 'meta' || $this->type == 'pitm' ||
269
+
$this->type == 'ipma' || $this->type == 'ispe' ||
270
+
$this->type == 'pixi' || $this->type == 'iref' ||
271
+
$this->type == 'auxC';
272
+
if ( $has_fullbox_header ) {
273
+
$header_size += 4;
274
+
}
275
+
if ( $this->size < $header_size ) {
276
+
return INVALID;
277
+
}
278
+
$this->content_size = $this->size - $header_size;
279
+
// Avoid timeouts. The maximum number of parsed boxes is arbitrary.
280
+
++$num_parsed_boxes;
281
+
if ( $num_parsed_boxes >= MAX_NUM_BOXES ) {
282
+
return ABORTED;
283
+
}
284
+
285
+
$this->version = 0;
286
+
$this->flags = 0;
287
+
if ( $has_fullbox_header ) {
288
+
if ( !( $data = read( $handle, 4 ) ) ) {
289
+
return TRUNCATED;
290
+
}
291
+
$this->version = read_big_endian( $data, 1 );
292
+
$this->flags = read_big_endian( substr( $data, 1, 3 ), 3 );
293
+
// See AV1 Image File Format (AVIF) 8.1
294
+
// at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when
295
+
// https://github.com/AOMediaCodec/av1-avif/pull/170 is merged).
296
+
$is_parsable = ( $this->type == 'meta' && $this->version <= 0 ) ||
297
+
( $this->type == 'pitm' && $this->version <= 1 ) ||
298
+
( $this->type == 'ipma' && $this->version <= 1 ) ||
299
+
( $this->type == 'ispe' && $this->version <= 0 ) ||
300
+
( $this->type == 'pixi' && $this->version <= 0 ) ||
301
+
( $this->type == 'iref' && $this->version <= 1 ) ||
302
+
( $this->type == 'auxC' && $this->version <= 0 );
303
+
// Instead of considering this file as invalid, skip unparsable boxes.
304
+
if ( !$is_parsable ) {
305
+
$this->type = 'unknownversion';
306
+
}
307
+
}
308
+
// print_r( $this ); // Uncomment to print all boxes.
309
+
return FOUND;
310
+
}
311
+
}
312
+
313
+
//------------------------------------------------------------------------------
314
+
315
+
class Parser {
316
+
private $handle; // Input stream.
317
+
private $num_parsed_boxes = 0;
318
+
private $data_was_skipped = false;
319
+
public $features;
320
+
321
+
function __construct( $handle ) {
322
+
$this->handle = $handle;
323
+
$this->features = new Features();
324
+
}
325
+
326
+
/**
327
+
* Parses an "ipco" box.
328
+
*
329
+
* "ispe" is used for width and height, "pixi" and "av1C" are used for bit depth
330
+
* and number of channels, and "auxC" is used for alpha.
331
+
*
332
+
* @param stream $handle The resource the box will be parsed from.
333
+
* @param int $num_remaining_bytes The number of bytes that should be available from the resource.
334
+
* @return Status FOUND on success or an error on failure.
335
+
*/
336
+
private function parse_ipco( $num_remaining_bytes ) {
337
+
$box_index = 1; // 1-based index. Used for iterating over properties.
338
+
do {
339
+
$box = new Box();
340
+
$status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
341
+
if ( $status != FOUND ) {
342
+
return $status;
343
+
}
344
+
345
+
if ( $box->type == 'ispe' ) {
346
+
// See ISO/IEC 23008-12:2017(E) 6.5.3.2
347
+
if ( $box->content_size < 8 ) {
348
+
return INVALID;
349
+
}
350
+
if ( !( $data = read( $this->handle, 8 ) ) ) {
351
+
return TRUNCATED;
352
+
}
353
+
$width = read_big_endian( substr( $data, 0, 4 ), 4 );
354
+
$height = read_big_endian( substr( $data, 4, 4 ), 4 );
355
+
if ( $width == 0 || $height == 0 ) {
356
+
return INVALID;
357
+
}
358
+
if ( count( $this->features->dim_props ) <= MAX_FEATURES &&
359
+
$box_index <= MAX_VALUE ) {
360
+
$dim_prop_count = count( $this->features->dim_props );
361
+
$this->features->dim_props[$dim_prop_count] = new Dim_Prop();
362
+
$this->features->dim_props[$dim_prop_count]->property_index = $box_index;
363
+
$this->features->dim_props[$dim_prop_count]->width = $width;
364
+
$this->features->dim_props[$dim_prop_count]->height = $height;
365
+
} else {
366
+
$this->data_was_skipped = true;
367
+
}
368
+
if ( !skip( $this->handle, $box->content_size - 8 ) ) {
369
+
return TRUNCATED;
370
+
}
371
+
} else if ( $box->type == 'pixi' ) {
372
+
// See ISO/IEC 23008-12:2017(E) 6.5.6.2
373
+
if ( $box->content_size < 1 ) {
374
+
return INVALID;
375
+
}
376
+
if ( !( $data = read( $this->handle, 1 ) ) ) {
377
+
return TRUNCATED;
378
+
}
379
+
$num_channels = read_big_endian( $data, 1 );
380
+
if ( $num_channels < 1 ) {
381
+
return INVALID;
382
+
}
383
+
if ( $box->content_size < 1 + $num_channels ) {
384
+
return INVALID;
385
+
}
386
+
if ( !( $data = read( $this->handle, 1 ) ) ) {
387
+
return TRUNCATED;
388
+
}
389
+
$bit_depth = read_big_endian( $data, 1 );
390
+
if ( $bit_depth < 1 ) {
391
+
return INVALID;
392
+
}
393
+
for ( $i = 1; $i < $num_channels; ++$i ) {
394
+
if ( !( $data = read( $this->handle, 1 ) ) ) {
395
+
return TRUNCATED;
396
+
}
397
+
// Bit depth should be the same for all channels.
398
+
if ( read_big_endian( $data, 1 ) != $bit_depth ) {
399
+
return INVALID;
400
+
}
401
+
if ( $i > 32 ) {
402
+
return ABORTED; // Be reasonable.
403
+
}
404
+
}
405
+
if ( count( $this->features->chan_props ) <= MAX_FEATURES &&
406
+
$box_index <= MAX_VALUE && $bit_depth <= MAX_VALUE &&
407
+
$num_channels <= MAX_VALUE ) {
408
+
$chan_prop_count = count( $this->features->chan_props );
409
+
$this->features->chan_props[$chan_prop_count] = new Chan_Prop();
410
+
$this->features->chan_props[$chan_prop_count]->property_index = $box_index;
411
+
$this->features->chan_props[$chan_prop_count]->bit_depth = $bit_depth;
412
+
$this->features->chan_props[$chan_prop_count]->num_channels = $num_channels;
413
+
} else {
414
+
$this->data_was_skipped = true;
415
+
}
416
+
if ( !skip( $this->handle, $box->content_size - ( 1 + $num_channels ) ) ) {
417
+
return TRUNCATED;
418
+
}
419
+
} else if ( $box->type == 'av1C' ) {
420
+
// See AV1 Codec ISO Media File Format Binding 2.3.1
421
+
// at https://aomediacodec.github.io/av1-isobmff/#av1c
422
+
// Only parse the necessary third byte. Assume that the others are valid.
423
+
if ( $box->content_size < 3 ) {
424
+
return INVALID;
425
+
}
426
+
if ( !( $data = read( $this->handle, 3 ) ) ) {
427
+
return TRUNCATED;
428
+
}
429
+
$byte = read_big_endian( substr( $data, 2, 1 ), 1 );
430
+
$high_bitdepth = ( $byte & 0x40 ) != 0;
431
+
$twelve_bit = ( $byte & 0x20 ) != 0;
432
+
$monochrome = ( $byte & 0x10 ) != 0;
433
+
if ( $twelve_bit && !$high_bitdepth ) {
434
+
return INVALID;
435
+
}
436
+
if ( count( $this->features->chan_props ) <= MAX_FEATURES &&
437
+
$box_index <= MAX_VALUE ) {
438
+
$chan_prop_count = count( $this->features->chan_props );
439
+
$this->features->chan_props[$chan_prop_count] = new Chan_Prop();
440
+
$this->features->chan_props[$chan_prop_count]->property_index = $box_index;
441
+
$this->features->chan_props[$chan_prop_count]->bit_depth =
442
+
$high_bitdepth ? $twelve_bit ? 12 : 10 : 8;
443
+
$this->features->chan_props[$chan_prop_count]->num_channels = $monochrome ? 1 : 3;
444
+
} else {
445
+
$this->data_was_skipped = true;
446
+
}
447
+
if ( !skip( $this->handle, $box->content_size - 3 ) ) {
448
+
return TRUNCATED;
449
+
}
450
+
} else if ( $box->type == 'auxC' ) {
451
+
// See AV1 Image File Format (AVIF) 4
452
+
// at https://aomediacodec.github.io/av1-avif/#auxiliary-images
453
+
$kAlphaStr = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0";
454
+
$kAlphaStrLength = 44; // Includes terminating character.
455
+
if ( $box->content_size >= $kAlphaStrLength ) {
456
+
if ( !( $data = read( $this->handle, $kAlphaStrLength ) ) ) {
457
+
return TRUNCATED;
458
+
}
459
+
if ( substr( $data, 0, $kAlphaStrLength ) == $kAlphaStr ) {
460
+
// Note: It is unlikely but it is possible that this alpha plane does
461
+
// not belong to the primary item or a tile. Ignore this issue.
462
+
$this->features->has_alpha = true;
463
+
}
464
+
if ( !skip( $this->handle, $box->content_size - $kAlphaStrLength ) ) {
465
+
return TRUNCATED;
466
+
}
467
+
} else {
468
+
if ( !skip( $this->handle, $box->content_size ) ) {
469
+
return TRUNCATED;
470
+
}
471
+
}
472
+
} else {
473
+
if ( !skip( $this->handle, $box->content_size ) ) {
474
+
return TRUNCATED;
475
+
}
476
+
}
477
+
++$box_index;
478
+
$num_remaining_bytes -= $box->size;
479
+
} while ( $num_remaining_bytes > 0 );
480
+
return NOT_FOUND;
481
+
}
482
+
483
+
/**
484
+
* Parses an "iprp" box.
485
+
*
486
+
* The "ipco" box contain the properties which are linked to items by the "ipma" box.
487
+
*
488
+
* @param stream $handle The resource the box will be parsed from.
489
+
* @param int $num_remaining_bytes The number of bytes that should be available from the resource.
490
+
* @return Status FOUND on success or an error on failure.
491
+
*/
492
+
private function parse_iprp( $num_remaining_bytes ) {
493
+
do {
494
+
$box = new Box();
495
+
$status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
496
+
if ( $status != FOUND ) {
497
+
return $status;
498
+
}
499
+
500
+
if ( $box->type == 'ipco' ) {
501
+
$status = $this->parse_ipco( $box->content_size );
502
+
if ( $status != NOT_FOUND ) {
503
+
return $status;
504
+
}
505
+
} else if ( $box->type == 'ipma' ) {
506
+
// See ISO/IEC 23008-12:2017(E) 9.3.2
507
+
$num_read_bytes = 4;
508
+
if ( $box->content_size < $num_read_bytes ) {
509
+
return INVALID;
510
+
}
511
+
if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) {
512
+
return TRUNCATED;
513
+
}
514
+
$entry_count = read_big_endian( $data, 4 );
515
+
$id_num_bytes = ( $box->version < 1 ) ? 2 : 4;
516
+
$index_num_bytes = ( $box->flags & 1 ) ? 2 : 1;
517
+
$essential_bit_mask = ( $box->flags & 1 ) ? 0x8000 : 0x80;
518
+
519
+
for ( $entry = 0; $entry < $entry_count; ++$entry ) {
520
+
if ( $entry >= MAX_PROPS ||
521
+
count( $this->features->props ) >= MAX_PROPS ) {
522
+
$this->data_was_skipped = true;
523
+
break;
524
+
}
525
+
$num_read_bytes += $id_num_bytes + 1;
526
+
if ( $box->content_size < $num_read_bytes ) {
527
+
return INVALID;
528
+
}
529
+
if ( !( $data = read( $this->handle, $id_num_bytes + 1 ) ) ) {
530
+
return TRUNCATED;
531
+
}
532
+
$item_id = read_big_endian(
533
+
substr( $data, 0, $id_num_bytes ), $id_num_bytes );
534
+
$association_count = read_big_endian(
535
+
substr( $data, $id_num_bytes, 1 ), 1 );
536
+
537
+
for ( $property = 0; $property < $association_count; ++$property ) {
538
+
if ( $property >= MAX_PROPS ||
539
+
count( $this->features->props ) >= MAX_PROPS ) {
540
+
$this->data_was_skipped = true;
541
+
break;
542
+
}
543
+
$num_read_bytes += $index_num_bytes;
544
+
if ( $box->content_size < $num_read_bytes ) {
545
+
return INVALID;
546
+
}
547
+
if ( !( $data = read( $this->handle, $index_num_bytes ) ) ) {
548
+
return TRUNCATED;
549
+
}
550
+
$value = read_big_endian( $data, $index_num_bytes );
551
+
// $essential = ($value & $essential_bit_mask); // Unused.
552
+
$property_index = ( $value & ~$essential_bit_mask );
553
+
if ( $property_index <= MAX_VALUE && $item_id <= MAX_VALUE ) {
554
+
$prop_count = count( $this->features->props );
555
+
$this->features->props[$prop_count] = new Prop();
556
+
$this->features->props[$prop_count]->property_index = $property_index;
557
+
$this->features->props[$prop_count]->item_id = $item_id;
558
+
} else {
559
+
$this->data_was_skipped = true;
560
+
}
561
+
}
562
+
if ( $property < $association_count ) {
563
+
break; // Do not read garbage.
564
+
}
565
+
}
566
+
567
+
// If all features are available now, do not look further.
568
+
$status = $this->features->get_primary_item_features();
569
+
if ( $status != NOT_FOUND ) {
570
+
return $status;
571
+
}
572
+
573
+
// Mostly if 'data_was_skipped'.
574
+
if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) {
575
+
return TRUNCATED;
576
+
}
577
+
} else {
578
+
if ( !skip( $this->handle, $box->content_size ) ) {
579
+
return TRUNCATED;
580
+
}
581
+
}
582
+
$num_remaining_bytes -= $box->size;
583
+
} while ( $num_remaining_bytes > 0 );
584
+
return NOT_FOUND;
585
+
}
586
+
587
+
/**
588
+
* Parses an "iref" box.
589
+
*
590
+
* The "dimg" boxes contain links between tiles and their parent items, which
591
+
* can be used to infer bit depth and number of channels for the primary item
592
+
* when the latter does not have these properties.
593
+
*
594
+
* @param stream $handle The resource the box will be parsed from.
595
+
* @param int $num_remaining_bytes The number of bytes that should be available from the resource.
596
+
* @return Status FOUND on success or an error on failure.
597
+
*/
598
+
private function parse_iref( $num_remaining_bytes ) {
599
+
do {
600
+
$box = new Box();
601
+
$status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
602
+
if ( $status != FOUND ) {
603
+
return $status;
604
+
}
605
+
606
+
if ( $box->type == 'dimg' ) {
607
+
// See ISO/IEC 14496-12:2015(E) 8.11.12.2
608
+
$num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4;
609
+
$num_read_bytes = $num_bytes_per_id + 2;
610
+
if ( $box->content_size < $num_read_bytes ) {
611
+
return INVALID;
612
+
}
613
+
if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) {
614
+
return TRUNCATED;
615
+
}
616
+
$from_item_id = read_big_endian( $data, $num_bytes_per_id );
617
+
$reference_count = read_big_endian( substr( $data, $num_bytes_per_id, 2 ), 2 );
618
+
619
+
for ( $i = 0; $i < $reference_count; ++$i ) {
620
+
if ( $i >= MAX_TILES ) {
621
+
$this->data_was_skipped = true;
622
+
break;
623
+
}
624
+
$num_read_bytes += $num_bytes_per_id;
625
+
if ( $box->content_size < $num_read_bytes ) {
626
+
return INVALID;
627
+
}
628
+
if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) {
629
+
return TRUNCATED;
630
+
}
631
+
$to_item_id = read_big_endian( $data, $num_bytes_per_id );
632
+
$tile_count = count( $this->features->tiles );
633
+
if ( $from_item_id <= MAX_VALUE && $to_item_id <= MAX_VALUE &&
634
+
$tile_count < MAX_TILES ) {
635
+
$this->features->tiles[$tile_count] = new Tile();
636
+
$this->features->tiles[$tile_count]->tile_item_id = $to_item_id;
637
+
$this->features->tiles[$tile_count]->parent_item_id = $from_item_id;
638
+
} else {
639
+
$this->data_was_skipped = true;
640
+
}
641
+
}
642
+
643
+
// If all features are available now, do not look further.
644
+
$status = $this->features->get_primary_item_features();
645
+
if ( $status != NOT_FOUND ) {
646
+
return $status;
647
+
}
648
+
649
+
// Mostly if 'data_was_skipped'.
650
+
if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) {
651
+
return TRUNCATED;
652
+
}
653
+
} else {
654
+
if ( !skip( $this->handle, $box->content_size ) ) {
655
+
return TRUNCATED;
656
+
}
657
+
}
658
+
$num_remaining_bytes -= $box->size;
659
+
} while ( $num_remaining_bytes > 0 );
660
+
return NOT_FOUND;
661
+
}
662
+
663
+
/**
664
+
* Parses a "meta" box.
665
+
*
666
+
* It looks for the primary item ID in the "pitm" box and recurses into other boxes
667
+
* to find its features.
668
+
*
669
+
* @param stream $handle The resource the box will be parsed from.
670
+
* @param int $num_remaining_bytes The number of bytes that should be available from the resource.
671
+
* @return Status FOUND on success or an error on failure.
672
+
*/
673
+
private function parse_meta( $num_remaining_bytes ) {
674
+
do {
675
+
$box = new Box();
676
+
$status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
677
+
if ( $status != FOUND ) {
678
+
return $status;
679
+
}
680
+
681
+
if ( $box->type == 'pitm' ) {
682
+
// See ISO/IEC 14496-12:2015(E) 8.11.4.2
683
+
$num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4;
684
+
if ( $num_bytes_per_id > $num_remaining_bytes ) {
685
+
return INVALID;
686
+
}
687
+
if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) {
688
+
return TRUNCATED;
689
+
}
690
+
$primary_item_id = read_big_endian( $data, $num_bytes_per_id );
691
+
if ( $primary_item_id > MAX_VALUE ) {
692
+
return ABORTED;
693
+
}
694
+
$this->features->has_primary_item = true;
695
+
$this->features->primary_item_id = $primary_item_id;
696
+
if ( !skip( $this->handle, $box->content_size - $num_bytes_per_id ) ) {
697
+
return TRUNCATED;
698
+
}
699
+
} else if ( $box->type == 'iprp' ) {
700
+
$status = $this->parse_iprp( $box->content_size );
701
+
if ( $status != NOT_FOUND ) {
702
+
return $status;
703
+
}
704
+
} else if ( $box->type == 'iref' ) {
705
+
$status = $this->parse_iref( $box->content_size );
706
+
if ( $status != NOT_FOUND ) {
707
+
return $status;
708
+
}
709
+
} else {
710
+
if ( !skip( $this->handle, $box->content_size ) ) {
711
+
return TRUNCATED;
712
+
}
713
+
}
714
+
$num_remaining_bytes -= $box->size;
715
+
} while ( $num_remaining_bytes != 0 );
716
+
// According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one "meta".
717
+
return INVALID;
718
+
}
719
+
720
+
/**
721
+
* Parses a file stream.
722
+
*
723
+
* The file type is checked through the "ftyp" box.
724
+
*
725
+
* @return bool True if the input stream is an AVIF bitstream or false.
726
+
*/
727
+
public function parse_ftyp() {
728
+
$box = new Box();
729
+
$status = $box->parse( $this->handle, $this->num_parsed_boxes );
730
+
if ( $status != FOUND ) {
731
+
return false;
732
+
}
733
+
734
+
if ( $box->type != 'ftyp' ) {
735
+
return false;
736
+
}
737
+
// Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
738
+
if ( $box->content_size < 8 ) {
739
+
return false;
740
+
}
741
+
for ( $i = 0; $i + 4 <= $box->content_size; $i += 4 ) {
742
+
if ( !( $data = read( $this->handle, 4 ) ) ) {
743
+
return false;
744
+
}
745
+
if ( $i == 4 ) {
746
+
continue; // Skip minor_version.
747
+
}
748
+
if ( substr( $data, 0, 4 ) == 'avif' || substr( $data, 0, 4 ) == 'avis' ) {
749
+
return skip( $this->handle, $box->content_size - ( $i + 4 ) );
750
+
}
751
+
if ( $i > 32 * 4 ) {
752
+
return false; // Be reasonable.
753
+
}
754
+
755
+
}
756
+
return false; // No AVIF brand no good.
757
+
}
758
+
759
+
/**
760
+
* Parses a file stream.
761
+
*
762
+
* Features are extracted from the "meta" box.
763
+
*
764
+
* @return bool True if the main features of the primary item were parsed or false.
765
+
*/
766
+
public function parse_file() {
767
+
$box = new Box();
768
+
while ( $box->parse( $this->handle, $this->num_parsed_boxes ) == FOUND ) {
769
+
if ( $box->type === 'meta' ) {
770
+
if ( $this->parse_meta( $box->content_size ) != FOUND ) {
771
+
return false;
772
+
}
773
+
return true;
774
+
}
775
+
if ( !skip( $this->handle, $box->content_size ) ) {
776
+
return false;
777
+
}
778
+
}
779
+
return false; // No "meta" no good.
780
+
}
781
+
}
782
+