1 #ifdef HAVE_CONFIG_H
2 #include "config.h"
3 #endif
4
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <limits.h>
9 #include <math.h>
10
11 #include "gd.h"
12 #include "gd_errors.h"
13 #include "gdhelpers.h"
14 #include "gd_intern.h"
15
16 #ifdef HAVE_LIBAVIF
17 #include <avif/avif.h>
18
19 /*
20 Define defaults for encoding images:
21 CHROMA_SUBSAMPLING_DEFAULT: 4:2:0 is commonly used for Chroma subsampling.
22 CHROMA_SUBAMPLING_HIGH_QUALITY: Use 4:4:4, or no subsampling, when a sufficient high quality is requested.
23 SUBAMPLING_HIGH_QUALITY_THRESHOLD: At or above this value, use CHROMA_SUBAMPLING_HIGH_QUALITY
24 QUANTIZER_DEFAULT:
25 We need more testing to really know what quantizer settings are optimal,
26 but teams at Google have been using maximum=30 as a starting point.
27 QUALITY_DEFAULT: following gd conventions, -1 indicates the default.
28 SPEED_DEFAULT:
29 AVIF_SPEED_DEFAULT is simply the default encoding speed of the AV1 codec.
30 This could be as slow as 0. So we use 6, which is currently considered to be a fine default.
31 */
32
33 #define CHROMA_SUBSAMPLING_DEFAULT AVIF_PIXEL_FORMAT_YUV420
34 #define CHROMA_SUBAMPLING_HIGH_QUALITY AVIF_PIXEL_FORMAT_YUV444
35 #define HIGH_QUALITY_SUBSAMPLING_THRESHOLD 90
36 #define QUANTIZER_DEFAULT 30
37 #define QUALITY_DEFAULT -1
38 #define SPEED_DEFAULT 6
39
40 // This initial size for the gdIOCtx is standard among GD image conversion functions.
41 #define NEW_DYNAMIC_CTX_SIZE 2048
42
43 // Our quality param ranges from 0 to 100.
44 // To calculate quality, we convert from AVIF's quantizer scale, which runs from 63 to 0.
45 #define MAX_QUALITY 100
46
47 // These constants are for computing the number of tiles and threads to use during encoding.
48 // Maximum threads are from libavif/contrib/gkd-pixbuf/loader.c.
49 #define MIN_TILE_AREA (512 * 512)
50 #define MAX_TILES 8
51 #define MAX_THREADS 64
52
53 /*** Macros ***/
54
55 /*
56 From gd_png.c:
57 convert the 7-bit alpha channel to an 8-bit alpha channel.
58 We do a little bit-flipping magic, repeating the MSB
59 as the LSB, to ensure that 0 maps to 0 and
60 127 maps to 255. We also have to invert to match
61 PNG's convention in which 255 is opaque.
62 */
63 #define alpha7BitTo8Bit(alpha7Bit) \
64 (alpha7Bit == 127 ? \
65 0 : \
66 255 - ((alpha7Bit << 1) + (alpha7Bit >> 6)))
67
68 #define alpha8BitTo7Bit(alpha8Bit) (gdAlphaMax - (alpha8Bit >> 1))
69
70
71 /*** Helper functions ***/
72
73 /* Convert the quality param we expose to the quantity params used by libavif.
74 The *Quantizer* params values can range from 0 to 63, with 0 = highest quality and 63 = worst.
75 We make the scale 0-100, and we reverse this, so that 0 = worst quality and 100 = highest.
76
77 Values below 0 are set to 0, and values below MAX_QUALITY are set to MAX_QUALITY.
78 */
quality2Quantizer(int quality)79 static int quality2Quantizer(int quality) {
80 int clampedQuality = CLAMP(quality, 0, MAX_QUALITY);
81
82 float scaleFactor = (float) AVIF_QUANTIZER_WORST_QUALITY / (float) MAX_QUALITY;
83
84 return round(scaleFactor * (MAX_QUALITY - clampedQuality));
85 }
86
87 /*
88 As of February 2021, this algorithm reflects the latest research on how many tiles
89 and threads to include for a given image size.
90 This is subject to change as research continues.
91
92 Returns false if there was an error, true if all was well.
93 */
setEncoderTilesAndThreads(avifEncoder * encoder,avifRGBImage * rgb)94 static avifBool setEncoderTilesAndThreads(avifEncoder *encoder, avifRGBImage *rgb) {
95 int imageArea, tiles, tilesLog2, encoderTiles;
96
97 // _gdImageAvifCtx(), the calling function, checks this operation for overflow
98 imageArea = rgb->width * rgb->height;
99
100 tiles = (int) ceil((double) imageArea / MIN_TILE_AREA);
101 tiles = MIN(tiles, MAX_TILES);
102 tiles = MIN(tiles, MAX_THREADS);
103
104 // The number of tiles in any dimension will always be a power of 2. We can only specify log(2)tiles.
105
106 tilesLog2 = floor(log2(tiles));
107
108 // If the image's width is greater than the height, use more tile columns
109 // than tile rows to make the tile size close to a square.
110
111 if (rgb->width >= rgb->height) {
112 encoder->tileRowsLog2 = tilesLog2 / 2;
113 encoder->tileColsLog2 = tilesLog2 - encoder->tileRowsLog2;
114 } else {
115 encoder->tileColsLog2 = tilesLog2 / 2;
116 encoder->tileRowsLog2 = tilesLog2 - encoder->tileColsLog2;
117 }
118
119 // It's good to have one thread per tile.
120 encoderTiles = (1 << encoder->tileRowsLog2) * (1 << encoder->tileColsLog2);
121 encoder->maxThreads = encoderTiles;
122
123 return AVIF_TRUE;
124 }
125
126 /*
127 We can handle AVIF images whose color profile is sRGB, or whose color profile isn't set.
128 */
isAvifSrgbImage(avifImage * avifIm)129 static avifBool isAvifSrgbImage(avifImage *avifIm) {
130 return
131 (avifIm->colorPrimaries == AVIF_COLOR_PRIMARIES_BT709 ||
132 avifIm->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) &&
133 (avifIm->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SRGB ||
134 avifIm->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED)
135 ;
136 }
137
138 /*
139 Check the result from an Avif function to see if it's an error.
140 If so, decode the error and output it, and return true.
141 Otherwise, return false.
142 */
isAvifError(avifResult result,const char * msg)143 static avifBool isAvifError(avifResult result, const char *msg) {
144 if (result != AVIF_RESULT_OK) {
145 gd_error("avif error - %s: %s\n", msg, avifResultToString(result));
146 return AVIF_TRUE;
147 }
148
149 return AVIF_FALSE;
150 }
151
152
153 /*
154 <readfromCtx> implements the avifIOReadFunc interface by calling the relevant functions
155 in the gdIOCtx. Our logic is inspired by avifIOMemoryReaderRead() and avifIOFileReaderRead().
156 We don't know whether we're reading from a file or from memory. We don't have to know,
157 since we rely on the helper functions in the gdIOCtx.
158 We assume we've stashed the gdIOCtx in io->data, as we do in createAvifIOFromCtx().
159
160 We ignore readFlags, just as the avifIO*ReaderRead() functions do.
161
162 If there's a problem, this returns an avifResult error.
163 If things go well, return AVIF_RESULT_OK.
164 Of course these AVIF codes shouldn't be returned by any top-level GD function.
165 */
readFromCtx(avifIO * io,uint32_t readFlags,uint64_t offset,size_t size,avifROData * out)166 static avifResult readFromCtx(avifIO *io, uint32_t readFlags, uint64_t offset, size_t size, avifROData *out)
167 {
168 void *dataBuf = NULL;
169 gdIOCtx *ctx = (gdIOCtx *) io->data;
170
171 // readFlags is unsupported
172 if (readFlags != 0) {
173 return AVIF_RESULT_IO_ERROR;
174 }
175
176 // TODO: if we set sizeHint, this will be more efficient.
177
178 if (offset > INT_MAX || size > INT_MAX)
179 return AVIF_RESULT_IO_ERROR;
180
181 // Try to seek offset bytes forward. If we pass the end of the buffer, throw an error.
182 if (!ctx->seek(ctx, (int) offset))
183 return AVIF_RESULT_IO_ERROR;
184
185 dataBuf = avifAlloc(size);
186 if (!dataBuf) {
187 gd_error("avif error - couldn't allocate memory");
188 return AVIF_RESULT_UNKNOWN_ERROR;
189 }
190
191 // Read the number of bytes requested.
192 // If getBuf() returns a negative value, that means there was an error.
193 int charsRead = ctx->getBuf(ctx, dataBuf, (int) size);
194 if (charsRead < 0) {
195 avifFree(dataBuf);
196 return AVIF_RESULT_IO_ERROR;
197 }
198
199 out->data = dataBuf;
200 out->size = charsRead;
201 return AVIF_RESULT_OK;
202 }
203
204 // avif.h says this is optional, but it seemed easy to implement.
destroyAvifIO(struct avifIO * io)205 static void destroyAvifIO(struct avifIO *io) {
206 gdFree(io);
207 }
208
209 /* Set up an avifIO object.
210 The functions in the gdIOCtx struct may point either to a file or a memory buffer.
211 To us, that's immaterial.
212 Our task is simply to assign avifIO functions to the proper functions from gdIOCtx.
213 The destroy function needs to destroy the avifIO object and anything else it uses.
214
215 Returns NULL if memory for the object can't be allocated.
216 */
217
218 // TODO: can we get sizeHint somehow?
createAvifIOFromCtx(gdIOCtx * ctx)219 static avifIO *createAvifIOFromCtx(gdIOCtx *ctx) {
220 avifIO *io;
221
222 io = gdMalloc(sizeof(*io));
223 if (io == NULL)
224 return NULL;
225
226 // TODO: setting persistent=FALSE is safe, but it's less efficient. Is it necessary?
227 io->persistent = AVIF_FALSE;
228 io->read = readFromCtx;
229 io->write = NULL; // this function is currently unused; see avif.h
230 io->destroy = destroyAvifIO;
231 io->sizeHint = 0; // sadly, we don't get this information from the gdIOCtx.
232 io->data = ctx;
233
234 return io;
235 }
236
237
238 /*** Decoding functions ***/
239
240 /*
241 Function: gdImageCreateFromAvif
242
243 <gdImageCreateFromAvif> is called to load truecolor images from
244 AVIF format files. Invoke <gdImageCreateFromAvif> with an
245 already opened pointer to a file containing the desired
246 image. <gdImageCreateFromAvif> returns a <gdImagePtr> to the new
247 truecolor image, or NULL if unable to load the image (most often
248 because the file is corrupt or does not contain a AVIF
249 image). <gdImageCreateFromAvif> does not close the file.
250
251 This function creates a gdIOCtx struct from the file pointer it's passed.
252 And then it relies on <gdImageCreateFromAvifCtx> to do the real decoding work.
253 If the file contains an image sequence, we simply read the first one, discarding the rest.
254
255 Variants:
256
257 <gdImageCreateFromAvifPtr> creates an image from AVIF data
258 already in memory.
259
260 <gdImageCreateFromAvifCtx> reads data from the function
261 pointers in a <gdIOCtx> structure.
262
263 Parameters:
264
265 infile - pointer to the input file
266
267 Returns:
268
269 A pointer to the new truecolor image. This will need to be
270 destroyed with <gdImageDestroy> once it is no longer needed.
271
272 On error, returns 0.
273 */
gdImageCreateFromAvif(FILE * infile)274 gdImagePtr gdImageCreateFromAvif(FILE *infile)
275 {
276 gdImagePtr im;
277 gdIOCtx *ctx = gdNewFileCtx(infile);
278
279 if (!ctx)
280 return NULL;
281
282 im = gdImageCreateFromAvifCtx(ctx);
283 ctx->gd_free(ctx);
284
285 return im;
286 }
287
288 /*
289 Function: gdImageCreateFromAvifPtr
290
291 See <gdImageCreateFromAvif>.
292
293 Parameters:
294
295 size - size of Avif data in bytes.
296 data - pointer to Avif data.
297 */
gdImageCreateFromAvifPtr(int size,void * data)298 gdImagePtr gdImageCreateFromAvifPtr(int size, void *data)
299 {
300 gdImagePtr im;
301 gdIOCtx *ctx = gdNewDynamicCtxEx(size, data, 0);
302
303 if (!ctx)
304 return 0;
305
306 im = gdImageCreateFromAvifCtx(ctx);
307 ctx->gd_free(ctx);
308
309 return im;
310 }
311
312 /*
313 Function: gdImageCreateFromAvifCtx
314
315 See <gdImageCreateFromAvif>.
316
317 Additional details: the AVIF library comes with functions to create an IO object from
318 a file and from a memory pointer. Of course, it doesn't have a way to create an IO object
319 from a gdIOCtx. So, here, we use our own helper function, <createAvifIOfromCtx>.
320
321 Otherwise, we create the image by calling AVIF library functions in order:
322 * avifDecoderCreate(), to create the decoder
323 * avifDecoderSetIO(), to tell libavif how to read from our data structure
324 * avifDecoderParse(), to parse the image
325 * avifDecoderNextImage(), to read the first image from the decoder
326 * avifRGBImageSetDefaults(), to create the avifRGBImage
327 * avifRGBImageAllocatePixels(), to allocate memory for the pixels
328 * avifImageYUVToRGB(), to convert YUV to RGB
329
330 Finally, we create a new gd image and copy over the pixel data.
331
332 Parameters:
333
334 ctx - a gdIOCtx struct
335 */
gdImageCreateFromAvifCtx(gdIOCtx * ctx)336 gdImagePtr gdImageCreateFromAvifCtx (gdIOCtx *ctx)
337 {
338 uint32_t x, y;
339 gdImage *im = NULL;
340 avifResult result;
341 avifIO *io;
342 avifDecoder *decoder;
343 avifRGBImage rgb;
344
345 // this lets us know that memory hasn't been allocated yet for the pixels
346 rgb.pixels = NULL;
347
348 decoder = avifDecoderCreate();
349
350 // Check if libavif version is >= 0.9.1
351 // If so, allow the PixelInformationProperty ('pixi') to be missing in AV1 image
352 // items. libheif v1.11.0 or older does not add the 'pixi' item property to
353 // AV1 image items. (This issue has been corrected in libheif v1.12.0.)
354
355 #if AVIF_VERSION >= 90100
356 decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED;
357 #endif
358
359 io = createAvifIOFromCtx(ctx);
360 if (!io) {
361 gd_error("avif error - Could not allocate memory");
362 goto cleanup;
363 }
364
365 avifDecoderSetIO(decoder, io);
366
367 result = avifDecoderParse(decoder);
368 if (isAvifError(result, "Could not parse image"))
369 goto cleanup;
370
371 // Note again that, for an image sequence, we read only the first image, ignoring the rest.
372 result = avifDecoderNextImage(decoder);
373 if (isAvifError(result, "Could not decode image"))
374 goto cleanup;
375
376 if (!isAvifSrgbImage(decoder->image))
377 gd_error_ex(GD_NOTICE, "Image's color profile is not sRGB");
378
379 // Set up the avifRGBImage, and convert it from YUV to an 8-bit RGB image.
380 // (While AVIF image pixel depth can be 8, 10, or 12 bits, GD truecolor images are 8-bit.)
381 avifRGBImageSetDefaults(&rgb, decoder->image);
382 rgb.depth = 8;
383 avifRGBImageAllocatePixels(&rgb);
384
385 result = avifImageYUVToRGB(decoder->image, &rgb);
386 if (isAvifError(result, "Conversion from YUV to RGB failed"))
387 goto cleanup;
388
389 im = gdImageCreateTrueColor(decoder->image->width, decoder->image->height);
390 if (!im) {
391 gd_error("avif error - Could not create GD truecolor image");
392 goto cleanup;
393 }
394
395 im->saveAlphaFlag = 1;
396
397 // Read the pixels from the AVIF image and copy them into the GD image.
398
399 uint8_t *p = rgb.pixels;
400
401 for (y = 0; y < decoder->image->height; y++) {
402 for (x = 0; x < decoder->image->width; x++) {
403 uint8_t r = *(p++);
404 uint8_t g = *(p++);
405 uint8_t b = *(p++);
406 uint8_t a = alpha8BitTo7Bit(*(p++));
407 im->tpixels[y][x] = gdTrueColorAlpha(r, g, b, a);
408 }
409 }
410
411 cleanup:
412 // if io has been allocated, this frees it
413 avifDecoderDestroy(decoder);
414
415 if (rgb.pixels)
416 avifRGBImageFreePixels(&rgb);
417
418 return im;
419 }
420
421
422 /*** Encoding functions ***/
423
424 /*
425 Function: gdImageAvifEx
426
427 <gdImageAvifEx> outputs the specified image to the specified file in
428 AVIF format. The file must be open for writing. Under MSDOS and
429 all versions of Windows, it is important to use "wb" as opposed to
430 simply "w" as the mode when opening the file, and under Unix there
431 is no penalty for doing so. <gdImageAvifEx> does not close the file;
432 your code must do so.
433
434 Variants:
435
436 <gdImageAvifEx> writes the image to a file, encoding with the default quality and speed.
437
438 <gdImageAvifPtrEx> stores the image in RAM.
439
440 <gdImageAvifPtr> stores the image in RAM, encoding with the default quality and speed.
441
442 <gdImageAvifCtx> stores the image using a <gdIOCtx> struct.
443
444 Parameters:
445
446 im - The image to save.
447 outFile - The FILE pointer to write to.
448 quality - Compression quality (0-100). 0 is lowest-quality, 100 is highest.
449 speed - The speed of compression (0-10). 0 is slowest, 10 is fastest.
450
451 Notes on parameters:
452 quality - If quality = -1, we use a default quality as defined in QUALITY_DEFAULT.
453 For information on how we convert this quality to libavif's quantity param, see <quality2Quantizer>.
454
455 speed - At slower speeds, encoding may be quite slow. Use judiciously.
456
457 Qualities or speeds that are lower than the minimum value get clamped to the minimum value,
458 and qualities or speeds that are lower than the maximum value get clamped to the maxmum value.
459 Note that AVIF_SPEED_DEFAULT is -1. If we ever set SPEED_DEFAULT = AVIF_SPEED_DEFAULT,
460 we'd want to add a conditional to ensure that value doesn't get clamped.
461
462
463 Returns:
464
465 * for <gdImageAvifEx>, <gdImageAvif>, and <gdImageAvifCtx>, nothing.
466 * for <gdImageAvifPtrEx> and <gdImageAvifPtr>, a pointer to the image in memory.
467 */
468
469 /*
470 If we're passed the QUALITY_DEFAULT of -1, set the quantizer params to QUANTIZER_DEFAULT.
471 */
gdImageAvifCtx(gdImagePtr im,gdIOCtx * outfile,int quality,int speed)472 void gdImageAvifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, int speed)
473 {
474 avifResult result;
475 avifRGBImage rgb;
476 avifRWData avifOutput = AVIF_DATA_EMPTY;
477 avifBool lossless = quality == 100;
478 avifEncoder *encoder = NULL;
479
480 uint32_t val;
481 uint8_t *p;
482 uint32_t x, y;
483
484 if (im == NULL)
485 return;
486
487 if (!gdImageTrueColor(im)) {
488 gd_error("avif error - avif doesn't support palette images");
489 return;
490 }
491
492 if (!gdImageSX(im) || !gdImageSY(im)) {
493 gd_error("avif error - image dimensions must not be zero");
494 return;
495 }
496
497 if (overflow2(gdImageSX(im), gdImageSY(im))) {
498 gd_error("avif error - image dimensions are too large");
499 return;
500 }
501
502 speed = CLAMP(speed, AVIF_SPEED_SLOWEST, AVIF_SPEED_FASTEST);
503
504 avifPixelFormat subsampling = quality >= HIGH_QUALITY_SUBSAMPLING_THRESHOLD ?
505 CHROMA_SUBAMPLING_HIGH_QUALITY : CHROMA_SUBSAMPLING_DEFAULT;
506
507 // Create the AVIF image.
508 // Set the ICC to sRGB, as that's what gd supports right now.
509 // Note that MATRIX_COEFFICIENTS_IDENTITY enables lossless conversion from RGB to YUV.
510
511 avifImage *avifIm = avifImageCreate(gdImageSX(im), gdImageSY(im), 8, subsampling);
512
513 avifIm->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709;
514 avifIm->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
515 avifIm->matrixCoefficients = lossless ? AVIF_MATRIX_COEFFICIENTS_IDENTITY : AVIF_MATRIX_COEFFICIENTS_BT709;
516
517 avifRGBImageSetDefaults(&rgb, avifIm);
518 // this allocates memory, and sets rgb.rowBytes and rgb.pixels.
519 avifRGBImageAllocatePixels(&rgb);
520
521 // Parse RGB data from the GD image, and copy it into the AVIF RGB image.
522 // Convert 7-bit GD alpha channel values to 8-bit AVIF values.
523
524 p = rgb.pixels;
525 for (y = 0; y < rgb.height; y++) {
526 for (x = 0; x < rgb.width; x++) {
527 val = im->tpixels[y][x];
528
529 *(p++) = gdTrueColorGetRed(val);
530 *(p++) = gdTrueColorGetGreen(val);
531 *(p++) = gdTrueColorGetBlue(val);
532 *(p++) = alpha7BitTo8Bit(gdTrueColorGetAlpha(val));
533 }
534 }
535
536 // Convert the RGB image to YUV.
537
538 result = avifImageRGBToYUV(avifIm, &rgb);
539 if (isAvifError(result, "Could not convert image to YUV"))
540 goto cleanup;
541
542 // Encode the image in AVIF format.
543
544 encoder = avifEncoderCreate();
545 int quantizerQuality = quality == QUALITY_DEFAULT ?
546 QUANTIZER_DEFAULT : quality2Quantizer(quality);
547
548 encoder->minQuantizer = quantizerQuality;
549 encoder->maxQuantizer = quantizerQuality;
550 encoder->minQuantizerAlpha = quantizerQuality;
551 encoder->maxQuantizerAlpha = quantizerQuality;
552 encoder->speed = speed;
553
554 if (!setEncoderTilesAndThreads(encoder, &rgb))
555 goto cleanup;
556
557 //TODO: is there a reason to use timeSscales != 1?
558 result = avifEncoderAddImage(encoder, avifIm, 1, AVIF_ADD_IMAGE_FLAG_SINGLE);
559 if (isAvifError(result, "Could not encode image"))
560 goto cleanup;
561
562 result = avifEncoderFinish(encoder, &avifOutput);
563 if (isAvifError(result, "Could not finish encoding"))
564 goto cleanup;
565
566 // Write the AVIF image bytes to the GD ctx.
567
568 gdPutBuf(avifOutput.data, avifOutput.size, outfile);
569
570 cleanup:
571 if (rgb.pixels)
572 avifRGBImageFreePixels(&rgb);
573
574 if (encoder)
575 avifEncoderDestroy(encoder);
576
577 if (avifOutput.data)
578 avifRWDataFree(&avifOutput);
579 }
580
gdImageAvifEx(gdImagePtr im,FILE * outFile,int quality,int speed)581 void gdImageAvifEx(gdImagePtr im, FILE *outFile, int quality, int speed)
582 {
583 gdIOCtx *out = gdNewFileCtx(outFile);
584
585 if (out != NULL) {
586 gdImageAvifCtx(im, out, quality, speed);
587 out->gd_free(out);
588 }
589 }
590
gdImageAvif(gdImagePtr im,FILE * outFile)591 void gdImageAvif(gdImagePtr im, FILE *outFile)
592 {
593 gdImageAvifEx(im, outFile, QUALITY_DEFAULT, SPEED_DEFAULT);
594 }
595
gdImageAvifPtrEx(gdImagePtr im,int * size,int quality,int speed)596 void * gdImageAvifPtrEx(gdImagePtr im, int *size, int quality, int speed)
597 {
598 void *rv;
599 gdIOCtx *out = gdNewDynamicCtx(NEW_DYNAMIC_CTX_SIZE, NULL);
600
601 if (out == NULL) {
602 return NULL;
603 }
604
605 gdImageAvifCtx(im, out, quality, speed);
606 rv = gdDPExtractData(out, size);
607
608 out->gd_free(out);
609 return rv;
610 }
611
gdImageAvifPtr(gdImagePtr im,int * size)612 void * gdImageAvifPtr(gdImagePtr im, int *size)
613 {
614 return gdImageAvifPtrEx(im, size, QUALITY_DEFAULT, AVIF_SPEED_DEFAULT);
615 }
616
617 #endif /* HAVE_LIBAVIF */
618