| // Copyright 2019 Joe Drago. All rights reserved. |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| // This is a barebones y4m reader/writer for basic libavif testing. It is NOT comprehensive! |
| |
| #include "y4m.h" |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| static avifBool y4mColorSpaceToFormatAndDepth(const char * formatString, avifPixelFormat * format, int * depth) |
| { |
| if (!strcmp(formatString, "C420jpeg")) { |
| *format = AVIF_PIXEL_FORMAT_YUV420; |
| *depth = 8; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C444p10")) { |
| *format = AVIF_PIXEL_FORMAT_YUV444; |
| *depth = 10; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C422p10")) { |
| *format = AVIF_PIXEL_FORMAT_YUV422; |
| *depth = 10; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C420p10")) { |
| *format = AVIF_PIXEL_FORMAT_YUV420; |
| *depth = 10; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C422p10")) { |
| *format = AVIF_PIXEL_FORMAT_YUV422; |
| *depth = 10; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C444p12")) { |
| *format = AVIF_PIXEL_FORMAT_YUV444; |
| *depth = 12; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C422p12")) { |
| *format = AVIF_PIXEL_FORMAT_YUV422; |
| *depth = 12; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C420p12")) { |
| *format = AVIF_PIXEL_FORMAT_YUV420; |
| *depth = 12; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C422p12")) { |
| *format = AVIF_PIXEL_FORMAT_YUV422; |
| *depth = 12; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C444")) { |
| *format = AVIF_PIXEL_FORMAT_YUV444; |
| *depth = 8; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C422")) { |
| *format = AVIF_PIXEL_FORMAT_YUV422; |
| *depth = 8; |
| return AVIF_TRUE; |
| } |
| if (!strcmp(formatString, "C420")) { |
| *format = AVIF_PIXEL_FORMAT_YUV420; |
| *depth = 8; |
| return AVIF_TRUE; |
| } |
| return AVIF_FALSE; |
| } |
| |
| static avifBool getHeaderString(uint8_t * p, uint8_t * end, char * out, size_t maxChars) |
| { |
| uint8_t * headerEnd = p; |
| while ((*headerEnd != ' ') && (*headerEnd != '\n')) { |
| if (headerEnd >= end) { |
| return AVIF_FALSE; |
| } |
| ++headerEnd; |
| } |
| size_t formatLen = headerEnd - p; |
| if (formatLen > maxChars) { |
| return AVIF_FALSE; |
| } |
| |
| strncpy(out, (const char *)p, formatLen); |
| out[formatLen] = 0; |
| return AVIF_TRUE; |
| } |
| |
| #define ADVANCE(BYTES) \ |
| do { \ |
| p += BYTES; \ |
| if (p >= end) \ |
| goto cleanup; \ |
| } while(0) |
| |
| avifBool y4mRead(avifImage * avif, const char * inputFilename) |
| { |
| FILE * inputFile = fopen(inputFilename, "rb"); |
| if (!inputFile) { |
| fprintf(stderr, "Cannot open file for read: %s\n", inputFilename); |
| return AVIF_FALSE; |
| } |
| fseek(inputFile, 0, SEEK_END); |
| size_t inputFileSize = ftell(inputFile); |
| fseek(inputFile, 0, SEEK_SET); |
| |
| if (inputFileSize < 10) { |
| fprintf(stderr, "File too small: %s\n", inputFilename); |
| fclose(inputFile); |
| return AVIF_FALSE; |
| } |
| |
| avifRWData raw = AVIF_DATA_EMPTY; |
| avifRWDataRealloc(&raw, inputFileSize); |
| if (fread(raw.data, 1, inputFileSize, inputFile) != inputFileSize) { |
| fprintf(stderr, "Failed to read %zu bytes: %s\n", inputFileSize, inputFilename); |
| fclose(inputFile); |
| avifRWDataFree(&raw); |
| return AVIF_FALSE; |
| } |
| |
| avifBool result = AVIF_FALSE; |
| |
| fclose(inputFile); |
| inputFile = NULL; |
| |
| uint8_t * end = raw.data + raw.size; |
| uint8_t * p = raw.data; |
| |
| if (memcmp(p, "YUV4MPEG2 ", 10) != 0) { |
| fprintf(stderr, "Not a y4m file: %s\n", inputFilename); |
| avifRWDataFree(&raw); |
| return AVIF_FALSE; |
| } |
| ADVANCE(10); // skip past header |
| |
| char tmpBuffer[32]; |
| |
| int width = -1; |
| int height = -1; |
| int depth = -1; |
| avifPixelFormat format = AVIF_PIXEL_FORMAT_NONE; |
| avifNclxRangeFlag rangeFlag = AVIF_NCLX_LIMITED_RANGE; |
| while (p != end) { |
| switch (*p) { |
| case 'W': // width |
| width = atoi((const char *)p + 1); |
| break; |
| case 'H': // height |
| height = atoi((const char *)p + 1); |
| break; |
| case 'C': // color space |
| if (!getHeaderString(p, end, tmpBuffer, 31)) { |
| fprintf(stderr, "Bad y4m header: %s\n", inputFilename); |
| goto cleanup; |
| } |
| if (!y4mColorSpaceToFormatAndDepth(tmpBuffer, &format, &depth)) { |
| fprintf(stderr, "Unsupported y4m pixel format: %s\n", inputFilename); |
| goto cleanup; |
| } |
| break; |
| case 'X': |
| if (!getHeaderString(p, end, tmpBuffer, 31)) { |
| fprintf(stderr, "Bad y4m header: %s\n", inputFilename); |
| goto cleanup; |
| } |
| if (!strcmp(tmpBuffer, "XCOLORRANGE=FULL")) { |
| rangeFlag = AVIF_NCLX_FULL_RANGE; |
| } |
| default: |
| break; |
| } |
| |
| // Advance past header section |
| while ((*p != '\n') && (*p != ' ')) { |
| ADVANCE(1); |
| } |
| if (*p == '\n') { |
| // Done with y4m header |
| break; |
| } |
| |
| ADVANCE(1); |
| } |
| |
| if (*p != '\n') { |
| fprintf(stderr, "Truncated y4m header (no newline): %s\n", inputFilename); |
| goto cleanup; |
| } |
| ADVANCE(1); // advance past newline |
| |
| size_t remainingBytes = end - p; |
| if (remainingBytes < 6) { |
| fprintf(stderr, "Truncated y4m (no room for frame header): %s\n", inputFilename); |
| goto cleanup; |
| } |
| if (memcmp(p, "FRAME", 5) != 0) { |
| fprintf(stderr, "Truncated y4m (no frame): %s\n", inputFilename); |
| goto cleanup; |
| } |
| |
| // Advance past frame header |
| // TODO: Parse frame overrides similarly to header parsing above? |
| while (*p != '\n') { |
| ADVANCE(1); |
| } |
| if (*p != '\n') { |
| fprintf(stderr, "Invalid y4m frame header: %s\n", inputFilename); |
| goto cleanup; |
| } |
| ADVANCE(1); // advance past newline |
| |
| if ((width < 1) || (height < 1) || ((depth != 8) && (depth != 10) && (depth != 12)) || (format == AVIF_PIXEL_FORMAT_NONE)) { |
| fprintf(stderr, "Failed to parse y4m header (not enough information): %s\n", inputFilename); |
| goto cleanup; |
| } |
| |
| avifImageFreePlanes(avif, AVIF_PLANES_YUV | AVIF_PLANES_A); |
| avif->width = width; |
| avif->height = height; |
| avif->depth = depth; |
| avif->yuvFormat = format; |
| avif->yuvRange = (uint8_t)rangeFlag; |
| avifImageAllocatePlanes(avif, AVIF_PLANES_YUV); |
| |
| avifPixelFormatInfo info; |
| avifGetPixelFormatInfo(avif->yuvFormat, &info); |
| |
| uint32_t planeBytes[3]; |
| planeBytes[0] = avif->yuvRowBytes[0] * avif->height; |
| planeBytes[1] = avif->yuvRowBytes[1] * (avif->height >> info.chromaShiftY); |
| planeBytes[2] = avif->yuvRowBytes[2] * (avif->height >> info.chromaShiftY); |
| |
| uint32_t bytesNeeded = planeBytes[0] + planeBytes[1] + planeBytes[2]; |
| remainingBytes = end - p; |
| if (bytesNeeded > remainingBytes) { |
| fprintf(stderr, "Not enough bytes in y4m for first frame: %s\n", inputFilename); |
| goto cleanup; |
| } |
| |
| for (int i = 0; i < 3; ++i) { |
| memcpy(avif->yuvPlanes[i], p, planeBytes[i]); |
| p += planeBytes[i]; |
| } |
| |
| result = AVIF_TRUE; |
| cleanup: |
| avifRWDataFree(&raw); |
| return result; |
| } |
| |
| avifBool y4mWrite(avifImage * avif, const char * outputFilename) |
| { |
| avifBool swapUV = AVIF_FALSE; |
| char * y4mHeaderFormat = NULL; |
| switch (avif->depth) { |
| case 8: |
| switch (avif->yuvFormat) { |
| case AVIF_PIXEL_FORMAT_YUV444: |
| y4mHeaderFormat = "C444 XYSCSS=444"; |
| break; |
| case AVIF_PIXEL_FORMAT_YUV422: |
| y4mHeaderFormat = "C422 XYSCSS=422"; |
| break; |
| case AVIF_PIXEL_FORMAT_YUV420: |
| y4mHeaderFormat = "C420jpeg XYSCSS=420JPEG"; |
| break; |
| case AVIF_PIXEL_FORMAT_YV12: |
| y4mHeaderFormat = "C420jpeg XYSCSS=420JPEG"; |
| swapUV = AVIF_TRUE; |
| break; |
| case AVIF_PIXEL_FORMAT_NONE: |
| // will error later; this case is here for warning's sake |
| break; |
| } |
| break; |
| case 10: |
| switch (avif->yuvFormat) { |
| case AVIF_PIXEL_FORMAT_YUV444: |
| y4mHeaderFormat = "C444p10 XYSCSS=444P10"; |
| break; |
| case AVIF_PIXEL_FORMAT_YUV422: |
| y4mHeaderFormat = "C422p10 XYSCSS=422P10"; |
| break; |
| case AVIF_PIXEL_FORMAT_YUV420: |
| y4mHeaderFormat = "C420p10 XYSCSS=420P10"; |
| break; |
| case AVIF_PIXEL_FORMAT_YV12: |
| y4mHeaderFormat = "C422p10 XYSCSS=422P10"; |
| swapUV = AVIF_TRUE; |
| break; |
| case AVIF_PIXEL_FORMAT_NONE: |
| // will error later; this case is here for warning's sake |
| break; |
| } |
| break; |
| case 12: |
| switch (avif->yuvFormat) { |
| case AVIF_PIXEL_FORMAT_YUV444: |
| y4mHeaderFormat = "C444p12 XYSCSS=444P12"; |
| break; |
| case AVIF_PIXEL_FORMAT_YUV422: |
| y4mHeaderFormat = "C422p12 XYSCSS=422P12"; |
| break; |
| case AVIF_PIXEL_FORMAT_YUV420: |
| y4mHeaderFormat = "C420p12 XYSCSS=420P12"; |
| break; |
| case AVIF_PIXEL_FORMAT_YV12: |
| y4mHeaderFormat = "C422p12 XYSCSS=422P12"; |
| swapUV = AVIF_TRUE; |
| break; |
| case AVIF_PIXEL_FORMAT_NONE: |
| // will error later; this case is here for warning's sake |
| break; |
| } |
| break; |
| default: |
| fprintf(stderr, "ERROR: y4mWrite unsupported depth: %d\n", avif->depth); |
| return AVIF_FALSE; |
| } |
| |
| if (y4mHeaderFormat == NULL) { |
| fprintf(stderr, "ERROR: unsupported format\n"); |
| return AVIF_FALSE; |
| } |
| |
| const char * rangeString = "XCOLORRANGE=FULL"; |
| if ((avif->profileFormat == AVIF_PROFILE_FORMAT_NCLX) && !avif->nclx.fullRangeFlag) { |
| rangeString = "XCOLORRANGE=LIMITED"; |
| } |
| |
| avifPixelFormatInfo info; |
| avifGetPixelFormatInfo(avif->yuvFormat, &info); |
| |
| FILE * f = fopen(outputFilename, "wb"); |
| if (!f) { |
| fprintf(stderr, "Cannot open file for write: %s\n", outputFilename); |
| return AVIF_FALSE; |
| } |
| |
| fprintf(f, "YUV4MPEG2 W%d H%d F25:1 Ip A0:0 %s %s\n", avif->width, avif->height, y4mHeaderFormat, rangeString); |
| |
| fprintf(f, "FRAME\n"); |
| uint8_t * planes[3]; |
| uint32_t planeBytes[3]; |
| planes[0] = avif->yuvPlanes[0]; |
| planes[1] = avif->yuvPlanes[1]; |
| planes[2] = avif->yuvPlanes[2]; |
| planeBytes[0] = avif->yuvRowBytes[0] * avif->height; |
| planeBytes[1] = avif->yuvRowBytes[1] * (avif->height >> info.chromaShiftY); |
| planeBytes[2] = avif->yuvRowBytes[2] * (avif->height >> info.chromaShiftY); |
| if (swapUV) { |
| uint8_t * tmpPtr; |
| uint32_t tmp; |
| tmpPtr = planes[1]; |
| tmp = planeBytes[1]; |
| planes[1] = planes[2]; |
| planeBytes[1] = planeBytes[2]; |
| planes[2] = tmpPtr; |
| planeBytes[2] = tmp; |
| } |
| |
| for (int i = 0; i < 3; ++i) { |
| fwrite(planes[i], 1, planeBytes[i], f); |
| } |
| |
| fclose(f); |
| printf("Wrote: %s\n", outputFilename); |
| return AVIF_TRUE; |
| } |