/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 | } |