Coverage Report

Created: 2026-05-30 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libavif/apps/shared/y4m.c
Line
Count
Source
1
// Copyright 2019 Joe Drago. All rights reserved.
2
// SPDX-License-Identifier: BSD-2-Clause
3
4
// This is a barebones y4m reader/writer for basic libavif testing. It is NOT comprehensive!
5
6
#include "y4m.h"
7
8
#include <assert.h>
9
#include <inttypes.h>
10
#include <limits.h>
11
#include <stdio.h>
12
#include <stdlib.h>
13
#include <string.h>
14
15
#include "avif/avif.h"
16
#include "avifexif.h"
17
#include "avifutil.h"
18
19
42
#define Y4M_MAX_LINE_SIZE 2048 // Arbitrary limit. Y4M headers should be much smaller than this
20
21
struct y4mFrameIterator
22
{
23
    int width;
24
    int height;
25
    int depth;
26
    avifBool hasAlpha;
27
    avifPixelFormat format;
28
    avifRange range;
29
    avifChromaSamplePosition chromaSamplePosition;
30
    avifAppSourceTiming sourceTiming;
31
32
    FILE * inputFile;
33
    const char * displayFilename;
34
};
35
36
// Sets frame->format, frame->depth, frame->hasAlpha, and frame->chromaSamplePosition.
37
static avifBool y4mColorSpaceParse(const char * formatString, struct y4mFrameIterator * frame)
38
14
{
39
14
    frame->hasAlpha = AVIF_FALSE;
40
14
    frame->chromaSamplePosition = AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN;
41
42
14
    if (!strcmp(formatString, "C420jpeg")) {
43
4
        frame->format = AVIF_PIXEL_FORMAT_YUV420;
44
4
        frame->depth = 8;
45
        // Chroma sample position is center.
46
4
        return AVIF_TRUE;
47
4
    }
48
10
    if (!strcmp(formatString, "C420mpeg2")) {
49
0
        frame->format = AVIF_PIXEL_FORMAT_YUV420;
50
0
        frame->depth = 8;
51
0
        frame->chromaSamplePosition = AVIF_CHROMA_SAMPLE_POSITION_VERTICAL;
52
0
        return AVIF_TRUE;
53
0
    }
54
10
    if (!strcmp(formatString, "C420paldv")) {
55
0
        frame->format = AVIF_PIXEL_FORMAT_YUV420;
56
0
        frame->depth = 8;
57
0
        frame->chromaSamplePosition = AVIF_CHROMA_SAMPLE_POSITION_COLOCATED;
58
0
        return AVIF_TRUE;
59
0
    }
60
10
    if (!strcmp(formatString, "C444p10")) {
61
0
        frame->format = AVIF_PIXEL_FORMAT_YUV444;
62
0
        frame->depth = 10;
63
0
        return AVIF_TRUE;
64
0
    }
65
10
    if (!strcmp(formatString, "C422p10")) {
66
0
        frame->format = AVIF_PIXEL_FORMAT_YUV422;
67
0
        frame->depth = 10;
68
0
        return AVIF_TRUE;
69
0
    }
70
10
    if (!strcmp(formatString, "C420p10")) {
71
0
        frame->format = AVIF_PIXEL_FORMAT_YUV420;
72
0
        frame->depth = 10;
73
0
        return AVIF_TRUE;
74
0
    }
75
10
    if (!strcmp(formatString, "C444p12")) {
76
0
        frame->format = AVIF_PIXEL_FORMAT_YUV444;
77
0
        frame->depth = 12;
78
0
        return AVIF_TRUE;
79
0
    }
80
10
    if (!strcmp(formatString, "C422p12")) {
81
0
        frame->format = AVIF_PIXEL_FORMAT_YUV422;
82
0
        frame->depth = 12;
83
0
        return AVIF_TRUE;
84
0
    }
85
10
    if (!strcmp(formatString, "C420p12")) {
86
0
        frame->format = AVIF_PIXEL_FORMAT_YUV420;
87
0
        frame->depth = 12;
88
0
        return AVIF_TRUE;
89
0
    }
90
10
    if (!strcmp(formatString, "C444")) {
91
7
        frame->format = AVIF_PIXEL_FORMAT_YUV444;
92
7
        frame->depth = 8;
93
7
        return AVIF_TRUE;
94
7
    }
95
3
    if (!strcmp(formatString, "C444alpha")) {
96
0
        frame->format = AVIF_PIXEL_FORMAT_YUV444;
97
0
        frame->depth = 8;
98
0
        frame->hasAlpha = AVIF_TRUE;
99
0
        return AVIF_TRUE;
100
0
    }
101
3
    if (!strcmp(formatString, "C422")) {
102
0
        frame->format = AVIF_PIXEL_FORMAT_YUV422;
103
0
        frame->depth = 8;
104
0
        return AVIF_TRUE;
105
0
    }
106
3
    if (!strcmp(formatString, "C420")) {
107
0
        frame->format = AVIF_PIXEL_FORMAT_YUV420;
108
0
        frame->depth = 8;
109
        // Chroma sample position is center.
110
0
        return AVIF_TRUE;
111
0
    }
112
3
    if (!strcmp(formatString, "Cmono")) {
113
0
        frame->format = AVIF_PIXEL_FORMAT_YUV400;
114
0
        frame->depth = 8;
115
0
        return AVIF_TRUE;
116
0
    }
117
3
    if (!strcmp(formatString, "Cmono10")) {
118
0
        frame->format = AVIF_PIXEL_FORMAT_YUV400;
119
0
        frame->depth = 10;
120
0
        return AVIF_TRUE;
121
0
    }
122
3
    if (!strcmp(formatString, "Cmono12")) {
123
0
        frame->format = AVIF_PIXEL_FORMAT_YUV400;
124
0
        frame->depth = 12;
125
0
        return AVIF_TRUE;
126
0
    }
127
3
    return AVIF_FALSE;
128
3
}
129
130
// Returns an unsigned integer value parsed from [start:end[.
131
// Returns -1 in case of failure.
132
static int y4mReadUnsignedInt(const char * start, const char * end)
133
29
{
134
29
    const char * p = start;
135
29
    int64_t value = 0;
136
98
    while (p < end && *p >= '0' && *p <= '9') {
137
69
        value = value * 10 + (*(p++) - '0');
138
69
        if (value > INT_MAX) {
139
0
            return -1;
140
0
        }
141
69
    }
142
29
    return (p == start) ? -1 : (int)value;
143
29
}
144
145
// Note: this modifies framerateString
146
static avifBool y4mFramerateParse(char * framerateString, avifAppSourceTiming * sourceTiming)
147
15
{
148
15
    if (framerateString[0] != 'F') {
149
0
        return AVIF_FALSE;
150
0
    }
151
15
    ++framerateString; // skip past 'F'
152
153
15
    char * colonLocation = strchr(framerateString, ':');
154
15
    if (!colonLocation) {
155
1
        return AVIF_FALSE;
156
1
    }
157
14
    *colonLocation = 0;
158
14
    ++colonLocation;
159
160
14
    int numerator = atoi(framerateString);
161
14
    int denominator = atoi(colonLocation);
162
14
    if ((numerator < 1) || (denominator < 1)) {
163
0
        return AVIF_FALSE;
164
0
    }
165
166
14
    sourceTiming->timescale = (uint64_t)numerator;
167
14
    sourceTiming->duration = (uint64_t)denominator;
168
14
    return AVIF_TRUE;
169
14
}
170
171
static avifBool getHeaderString(uint8_t * p, uint8_t * end, char * out, size_t maxChars)
172
49
{
173
49
    uint8_t * headerEnd = p;
174
542
    while ((*headerEnd != ' ') && (*headerEnd != '\n')) {
175
493
        if (headerEnd >= end) {
176
0
            return AVIF_FALSE;
177
0
        }
178
493
        ++headerEnd;
179
493
    }
180
49
    size_t formatLen = headerEnd - p;
181
49
    if (formatLen > maxChars) {
182
1
        return AVIF_FALSE;
183
1
    }
184
185
48
    strncpy(out, (const char *)p, formatLen);
186
48
    out[formatLen] = 0;
187
48
    return AVIF_TRUE;
188
49
}
189
190
static int y4mReadLine(FILE * inputFile, avifRWData * raw, const char * displayFilename)
191
26
{
192
26
    static const int maxBytes = Y4M_MAX_LINE_SIZE;
193
26
    int bytesRead = 0;
194
26
    uint8_t * front = raw->data;
195
196
3.27k
    for (;;) {
197
3.27k
        if (fread(front, 1, 1, inputFile) != 1) {
198
0
            fprintf(stderr, "Failed to read line: %s\n", displayFilename);
199
0
            break;
200
0
        }
201
202
3.27k
        ++bytesRead;
203
3.27k
        if (bytesRead >= maxBytes) {
204
1
            break;
205
1
        }
206
207
3.27k
        if (*front == '\n') {
208
25
            return bytesRead;
209
25
        }
210
3.24k
        ++front;
211
3.24k
    }
212
1
    return -1;
213
26
}
214
215
// Limits each sample value to fit into avif->depth bits.
216
// Returns AVIF_TRUE if any sample was clamped this way.
217
static avifBool y4mClampSamples(avifImage * avif)
218
6
{
219
6
    if (!avifImageUsesU16(avif)) {
220
6
        assert(avif->depth == 8);
221
6
        return AVIF_FALSE;
222
6
    }
223
6
    assert(avif->depth < 16); // Otherwise it could be skipped too.
224
225
    // AV1 encoders and decoders do not care whether the samples are full range or limited range
226
    // for the internal computation: it is only passed as an informative tag, so ignore avif->yuvRange.
227
0
    const uint16_t maxSampleValue = (uint16_t)((1u << avif->depth) - 1u);
228
229
0
    avifBool samplesWereClamped = AVIF_FALSE;
230
0
    for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_A; ++plane) {
231
0
        uint32_t planeHeight = avifImagePlaneHeight(avif, plane); // 0 for UV if 4:0:0.
232
0
        uint32_t planeWidth = avifImagePlaneWidth(avif, plane);
233
0
        uint8_t * row = avifImagePlane(avif, plane);
234
0
        uint32_t rowBytes = avifImagePlaneRowBytes(avif, plane);
235
0
        for (uint32_t y = 0; y < planeHeight; ++y) {
236
0
            uint16_t * row16 = (uint16_t *)row;
237
0
            for (uint32_t x = 0; x < planeWidth; ++x) {
238
0
                if (row16[x] > maxSampleValue) {
239
0
                    row16[x] = maxSampleValue;
240
0
                    samplesWereClamped = AVIF_TRUE;
241
0
                }
242
0
            }
243
0
            row += rowBytes;
244
0
        }
245
0
    }
246
0
    return samplesWereClamped;
247
6
}
248
249
#define ADVANCE(BYTES)    \
250
714
    do {                  \
251
714
        p += BYTES;       \
252
714
        if (p >= end)     \
253
714
            goto cleanup; \
254
714
    } while (0)
255
256
avifBool y4mRead(const char * inputFilename,
257
                 avifBool ignoreAlpha,
258
                 uint32_t imageSizeLimit,
259
                 avifImage * avif,
260
                 avifAppSourceTiming * sourceTiming,
261
                 struct y4mFrameIterator ** iter)
262
16
{
263
16
    avifBool result = AVIF_FALSE;
264
265
16
    struct y4mFrameIterator frame;
266
16
    frame.width = -1;
267
16
    frame.height = -1;
268
    // Default to the color space "C420" to match the defaults of aomenc and ffmpeg.
269
16
    frame.depth = 8;
270
16
    frame.hasAlpha = AVIF_FALSE;
271
16
    frame.format = AVIF_PIXEL_FORMAT_YUV420;
272
16
    frame.range = AVIF_RANGE_LIMITED;
273
16
    frame.chromaSamplePosition = AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN;
274
16
    memset(&frame.sourceTiming, 0, sizeof(avifAppSourceTiming));
275
16
    frame.inputFile = NULL;
276
16
    frame.displayFilename = inputFilename;
277
278
16
    avifRWData raw = AVIF_DATA_EMPTY;
279
16
    if (avifRWDataRealloc(&raw, Y4M_MAX_LINE_SIZE) != AVIF_RESULT_OK) {
280
0
        fprintf(stderr, "Out of memory\n");
281
0
        goto cleanup;
282
0
    }
283
284
16
    if (iter && *iter) {
285
        // Continue reading FRAMEs from this y4m stream
286
0
        frame = **iter;
287
16
    } else {
288
        // Open a fresh y4m and read its header
289
290
16
        if (inputFilename) {
291
16
            frame.inputFile = fopen(inputFilename, "rb");
292
16
            if (!frame.inputFile) {
293
0
                fprintf(stderr, "Cannot open file for read: %s\n", inputFilename);
294
0
                goto cleanup;
295
0
            }
296
16
        } else {
297
0
            frame.inputFile = stdin;
298
0
            frame.displayFilename = "(stdin)";
299
0
        }
300
301
16
        int headerBytes = y4mReadLine(frame.inputFile, &raw, frame.displayFilename);
302
16
        if (headerBytes < 0) {
303
0
            fprintf(stderr, "Y4M header too large: %s\n", frame.displayFilename);
304
0
            goto cleanup;
305
0
        }
306
16
        if (headerBytes < 10) {
307
0
            fprintf(stderr, "Y4M header too small: %s\n", frame.displayFilename);
308
0
            goto cleanup;
309
0
        }
310
311
16
        uint8_t * end = raw.data + headerBytes;
312
16
        uint8_t * p = raw.data;
313
314
16
        if (memcmp(p, "YUV4MPEG2 ", 10) != 0) {
315
1
            fprintf(stderr, "Not a y4m file: %s\n", frame.displayFilename);
316
1
            goto cleanup;
317
1
        }
318
15
        ADVANCE(10); // skip past header
319
320
15
        char tmpBuffer[32];
321
322
107
        while (p != end) {
323
107
            switch (*p) {
324
15
                case 'W': // width
325
15
                    frame.width = y4mReadUnsignedInt((const char *)p + 1, (const char *)end);
326
15
                    break;
327
14
                case 'H': // height
328
14
                    frame.height = y4mReadUnsignedInt((const char *)p + 1, (const char *)end);
329
14
                    break;
330
14
                case 'C': // color space
331
14
                    if (!getHeaderString(p, end, tmpBuffer, 31)) {
332
0
                        fprintf(stderr, "Bad y4m header: %s\n", frame.displayFilename);
333
0
                        goto cleanup;
334
0
                    }
335
14
                    if (!y4mColorSpaceParse(tmpBuffer, &frame)) {
336
3
                        fprintf(stderr, "Unsupported y4m pixel format: %s\n", frame.displayFilename);
337
3
                        goto cleanup;
338
3
                    }
339
11
                    break;
340
15
                case 'F': // framerate
341
15
                    if (!getHeaderString(p, end, tmpBuffer, 31)) {
342
0
                        fprintf(stderr, "Bad y4m header: %s\n", frame.displayFilename);
343
0
                        goto cleanup;
344
0
                    }
345
15
                    if (!y4mFramerateParse(tmpBuffer, &frame.sourceTiming)) {
346
1
                        fprintf(stderr, "Unsupported framerate: %s\n", frame.displayFilename);
347
1
                        goto cleanup;
348
1
                    }
349
14
                    break;
350
20
                case 'X':
351
20
                    if (!getHeaderString(p, end, tmpBuffer, 31)) {
352
1
                        fprintf(stderr, "Bad y4m header: %s\n", frame.displayFilename);
353
1
                        goto cleanup;
354
1
                    }
355
19
                    if (!strcmp(tmpBuffer, "XCOLORRANGE=FULL")) {
356
3
                        frame.range = AVIF_RANGE_FULL;
357
3
                    }
358
19
                    break;
359
29
                default:
360
29
                    break;
361
107
            }
362
363
            // Advance past header section
364
709
            while ((*p != '\n') && (*p != ' ')) {
365
607
                ADVANCE(1);
366
607
            }
367
102
            if (*p == '\n') {
368
                // Done with y4m header
369
10
                break;
370
10
            }
371
372
92
            ADVANCE(1);
373
92
        }
374
375
10
        if (*p != '\n') {
376
0
            fprintf(stderr, "Truncated y4m header (no newline): %s\n", frame.displayFilename);
377
0
            goto cleanup;
378
0
        }
379
10
    }
380
381
10
    int frameHeaderBytes = y4mReadLine(frame.inputFile, &raw, frame.displayFilename);
382
10
    if (frameHeaderBytes < 0) {
383
1
        fprintf(stderr, "Y4M frame header too large: %s\n", frame.displayFilename);
384
1
        goto cleanup;
385
1
    }
386
9
    if (frameHeaderBytes < 6) {
387
0
        fprintf(stderr, "Y4M frame header too small: %s\n", frame.displayFilename);
388
0
        goto cleanup;
389
0
    }
390
9
    if (memcmp(raw.data, "FRAME", 5) != 0) {
391
1
        fprintf(stderr, "Truncated y4m (no frame): %s\n", frame.displayFilename);
392
1
        goto cleanup;
393
1
    }
394
395
8
    if ((frame.width < 1) || (frame.height < 1) || ((frame.depth != 8) && (frame.depth != 10) && (frame.depth != 12))) {
396
1
        fprintf(stderr, "Failed to parse y4m header (not enough information): %s\n", frame.displayFilename);
397
1
        goto cleanup;
398
1
    }
399
7
    if ((uint32_t)frame.width > imageSizeLimit / (uint32_t)frame.height) {
400
0
        fprintf(stderr, "Too big y4m dimensions (%d x %d > %u px): %s\n", frame.width, frame.height, imageSizeLimit, frame.displayFilename);
401
0
        goto cleanup;
402
0
    }
403
404
7
    if (sourceTiming) {
405
7
        *sourceTiming = frame.sourceTiming;
406
7
    }
407
408
7
    avifImageFreePlanes(avif, AVIF_PLANES_ALL);
409
7
    avif->width = frame.width;
410
7
    avif->height = frame.height;
411
7
    avif->depth = frame.depth;
412
7
    avif->yuvFormat = frame.format;
413
7
    avif->yuvRange = frame.range;
414
7
    avif->yuvChromaSamplePosition = frame.chromaSamplePosition;
415
7
    avifResult allocationResult = avifImageAllocatePlanes(avif, frame.hasAlpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV);
416
7
    if (allocationResult != AVIF_RESULT_OK) {
417
0
        fprintf(stderr, "Failed to allocate the planes: %s\n", avifResultToString(allocationResult));
418
0
        goto cleanup;
419
0
    }
420
421
33
    for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_A; ++plane) {
422
27
        uint32_t planeHeight = avifImagePlaneHeight(avif, plane); // 0 for A if no alpha and 0 for UV if 4:0:0.
423
27
        uint32_t planeWidthBytes = avifImagePlaneWidth(avif, plane) << (avif->depth > 8);
424
27
        uint8_t * row = avifImagePlane(avif, plane);
425
27
        uint32_t rowBytes = avifImagePlaneRowBytes(avif, plane);
426
2.41k
        for (uint32_t y = 0; y < planeHeight; ++y) {
427
2.38k
            uint32_t bytesRead = (uint32_t)fread(row, 1, planeWidthBytes, frame.inputFile);
428
2.38k
            if (bytesRead != planeWidthBytes) {
429
1
                fprintf(stderr,
430
1
                        "Failed to read y4m row (not enough data, wanted %" PRIu32 ", got %" PRIu32 "): %s\n",
431
1
                        planeWidthBytes,
432
1
                        bytesRead,
433
1
                        frame.displayFilename);
434
1
                goto cleanup;
435
1
            }
436
2.38k
            row += rowBytes;
437
2.38k
        }
438
27
    }
439
440
6
    if (frame.hasAlpha && ignoreAlpha) {
441
0
        avifImageFreePlanes(avif, AVIF_PLANES_A);
442
0
    }
443
444
    // libavif API does not guarantee the absence of undefined behavior if samples exceed the specified avif->depth.
445
    // Avoid that by making sure input values are within the correct range.
446
6
    if (y4mClampSamples(avif)) {
447
0
        fprintf(stderr, "WARNING: some samples were clamped to fit into %u bits per sample\n", avif->depth);
448
0
    }
449
450
6
    result = AVIF_TRUE;
451
16
cleanup:
452
16
    if (iter) {
453
0
        if (*iter) {
454
0
            free(*iter);
455
0
            *iter = NULL;
456
0
        }
457
458
0
        if (result && frame.inputFile) {
459
0
            ungetc(fgetc(frame.inputFile), frame.inputFile); // Kick frame.inputFile to force EOF
460
461
0
            if (!feof(frame.inputFile)) {
462
                // Remember y4m state for next time
463
0
                *iter = malloc(sizeof(struct y4mFrameIterator));
464
0
                if (*iter == NULL) {
465
0
                    fprintf(stderr, "Inter-frame state memory allocation failure\n");
466
0
                    result = AVIF_FALSE;
467
0
                } else {
468
0
                    **iter = frame;
469
0
                }
470
0
            }
471
0
        }
472
0
    }
473
474
16
    if (inputFilename && frame.inputFile && (!iter || !(*iter))) {
475
16
        fclose(frame.inputFile);
476
16
    }
477
16
    avifRWDataFree(&raw);
478
16
    return result;
479
6
}
480
481
avifBool y4mWrite(const char * outputFilename, const avifImage * avif)
482
0
{
483
0
    avifBool hasAlpha = (avif->alphaPlane != NULL) && (avif->alphaRowBytes > 0);
484
0
    avifBool writeAlpha = AVIF_FALSE;
485
0
    char * y4mHeaderFormat = NULL;
486
487
0
    if (hasAlpha && ((avif->depth != 8) || (avif->yuvFormat != AVIF_PIXEL_FORMAT_YUV444))) {
488
0
        fprintf(stderr, "WARNING: writing alpha is currently only supported in 8bpc YUV444, ignoring alpha channel: %s\n", outputFilename);
489
0
    }
490
491
0
    if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
492
0
        avifCropRect cropRect;
493
0
        avifDiagnostics diag;
494
0
        if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) &&
495
0
            (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) {
496
            // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement.
497
0
            fprintf(stderr,
498
0
                    "Warning: Clean Aperture values were ignored, the output image was NOT cropped to rectangle {%u,%u,%u,%u}\n",
499
0
                    cropRect.x,
500
0
                    cropRect.y,
501
0
                    cropRect.width,
502
0
                    cropRect.height);
503
0
        }
504
0
    }
505
0
    if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
506
        // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Rotate the samples.
507
0
        fprintf(stderr,
508
0
                "Warning: Orientation %u was ignored, the output image was NOT rotated or mirrored\n",
509
0
                avifImageGetExifOrientationFromIrotImir(avif));
510
0
    }
511
512
0
    switch (avif->depth) {
513
0
        case 8:
514
0
            switch (avif->yuvFormat) {
515
0
                case AVIF_PIXEL_FORMAT_YUV444:
516
0
                    if (hasAlpha) {
517
0
                        y4mHeaderFormat = "C444alpha XYSCSS=444";
518
0
                        writeAlpha = AVIF_TRUE;
519
0
                    } else {
520
0
                        y4mHeaderFormat = "C444 XYSCSS=444";
521
0
                    }
522
0
                    break;
523
0
                case AVIF_PIXEL_FORMAT_YUV422:
524
0
                    y4mHeaderFormat = "C422 XYSCSS=422";
525
0
                    break;
526
0
                case AVIF_PIXEL_FORMAT_YUV420:
527
0
                    y4mHeaderFormat = "C420jpeg XYSCSS=420JPEG";
528
0
                    break;
529
0
                case AVIF_PIXEL_FORMAT_YUV400:
530
0
                    y4mHeaderFormat = "Cmono XYSCSS=400";
531
0
                    break;
532
0
                case AVIF_PIXEL_FORMAT_NONE:
533
0
                case AVIF_PIXEL_FORMAT_COUNT:
534
                    // will error later; these cases are here for warning's sake
535
0
                    break;
536
0
            }
537
0
            break;
538
0
        case 10:
539
0
            switch (avif->yuvFormat) {
540
0
                case AVIF_PIXEL_FORMAT_YUV444:
541
0
                    y4mHeaderFormat = "C444p10 XYSCSS=444P10";
542
0
                    break;
543
0
                case AVIF_PIXEL_FORMAT_YUV422:
544
0
                    y4mHeaderFormat = "C422p10 XYSCSS=422P10";
545
0
                    break;
546
0
                case AVIF_PIXEL_FORMAT_YUV420:
547
0
                    y4mHeaderFormat = "C420p10 XYSCSS=420P10";
548
0
                    break;
549
0
                case AVIF_PIXEL_FORMAT_YUV400:
550
0
                    y4mHeaderFormat = "Cmono10 XYSCSS=400";
551
0
                    break;
552
0
                case AVIF_PIXEL_FORMAT_NONE:
553
0
                case AVIF_PIXEL_FORMAT_COUNT:
554
                    // will error later; these cases are here for warning's sake
555
0
                    break;
556
0
            }
557
0
            break;
558
0
        case 12:
559
0
            switch (avif->yuvFormat) {
560
0
                case AVIF_PIXEL_FORMAT_YUV444:
561
0
                    y4mHeaderFormat = "C444p12 XYSCSS=444P12";
562
0
                    break;
563
0
                case AVIF_PIXEL_FORMAT_YUV422:
564
0
                    y4mHeaderFormat = "C422p12 XYSCSS=422P12";
565
0
                    break;
566
0
                case AVIF_PIXEL_FORMAT_YUV420:
567
0
                    y4mHeaderFormat = "C420p12 XYSCSS=420P12";
568
0
                    break;
569
0
                case AVIF_PIXEL_FORMAT_YUV400:
570
0
                    y4mHeaderFormat = "Cmono12 XYSCSS=400";
571
0
                    break;
572
0
                case AVIF_PIXEL_FORMAT_NONE:
573
0
                case AVIF_PIXEL_FORMAT_COUNT:
574
                    // will error later; these cases are here for warning's sake
575
0
                    break;
576
0
            }
577
0
            break;
578
0
        default:
579
0
            fprintf(stderr, "ERROR: y4mWrite unsupported depth: %d\n", avif->depth);
580
0
            return AVIF_FALSE;
581
0
    }
582
583
0
    if (y4mHeaderFormat == NULL) {
584
0
        fprintf(stderr, "ERROR: unsupported format\n");
585
0
        return AVIF_FALSE;
586
0
    }
587
588
0
    const char * rangeString = "XCOLORRANGE=FULL";
589
0
    if (avif->yuvRange == AVIF_RANGE_LIMITED) {
590
0
        rangeString = "XCOLORRANGE=LIMITED";
591
0
    }
592
593
0
    FILE * f = fopen(outputFilename, "wb");
594
0
    if (!f) {
595
0
        fprintf(stderr, "Cannot open file for write: %s\n", outputFilename);
596
0
        return AVIF_FALSE;
597
0
    }
598
599
0
    avifBool success = AVIF_TRUE;
600
0
    if (fprintf(f, "YUV4MPEG2 W%d H%d F25:1 Ip A0:0 %s %s\nFRAME\n", avif->width, avif->height, y4mHeaderFormat, rangeString) < 0) {
601
0
        fprintf(stderr, "Cannot write to file: %s\n", outputFilename);
602
0
        success = AVIF_FALSE;
603
0
        goto cleanup;
604
0
    }
605
606
0
    const int lastPlane = writeAlpha ? AVIF_CHAN_A : AVIF_CHAN_V;
607
0
    for (int plane = AVIF_CHAN_Y; plane <= lastPlane; ++plane) {
608
0
        uint32_t planeHeight = avifImagePlaneHeight(avif, plane); // 0 for UV if 4:0:0.
609
0
        uint32_t planeWidthBytes = avifImagePlaneWidth(avif, plane) << (avif->depth > 8);
610
0
        const uint8_t * row = avifImagePlane(avif, plane);
611
0
        uint32_t rowBytes = avifImagePlaneRowBytes(avif, plane);
612
0
        for (uint32_t y = 0; y < planeHeight; ++y) {
613
0
            if (fwrite(row, 1, planeWidthBytes, f) != planeWidthBytes) {
614
0
                fprintf(stderr, "Failed to write %" PRIu32 " bytes: %s\n", planeWidthBytes, outputFilename);
615
0
                success = AVIF_FALSE;
616
0
                goto cleanup;
617
0
            }
618
0
            row += rowBytes;
619
0
        }
620
0
    }
621
622
0
cleanup:
623
0
    fclose(f);
624
0
    if (success) {
625
0
        printf("Wrote Y4M: %s\n", outputFilename);
626
0
    }
627
0
    return success;
628
0
}