| // Copyright 2019 Joe Drago. All rights reserved. |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "avif/internal.h" |
| |
| #include <assert.h> |
| #include <math.h> |
| #include <string.h> |
| |
| #if defined(_WIN32) |
| #include <process.h> |
| #include <windows.h> |
| #else |
| #include <pthread.h> |
| #endif |
| |
| struct YUVBlock |
| { |
| float y; |
| float u; |
| float v; |
| }; |
| |
| static avifBool avifGetRGBColorSpaceInfo(const avifRGBImage * rgb, avifRGBColorSpaceInfo * info) |
| { |
| if ((rgb->depth != 8) && (rgb->depth != 10) && (rgb->depth != 12) && (rgb->depth != 16)) { |
| return AVIF_FALSE; |
| } |
| if (rgb->isFloat && rgb->depth != 16) { |
| return AVIF_FALSE; |
| } |
| if (rgb->format == AVIF_RGB_FORMAT_RGB_565 && rgb->depth != 8) { |
| return AVIF_FALSE; |
| } |
| if (rgb->format < AVIF_RGB_FORMAT_RGB || rgb->format >= AVIF_RGB_FORMAT_COUNT) { |
| return AVIF_FALSE; |
| } |
| |
| info->channelBytes = (rgb->depth > 8) ? 2 : 1; |
| info->pixelBytes = avifRGBImagePixelSize(rgb); |
| |
| switch (rgb->format) { |
| case AVIF_RGB_FORMAT_RGB: |
| info->offsetBytesR = info->channelBytes * 0; |
| info->offsetBytesG = info->channelBytes * 1; |
| info->offsetBytesB = info->channelBytes * 2; |
| info->offsetBytesA = 0; |
| break; |
| case AVIF_RGB_FORMAT_RGBA: |
| info->offsetBytesR = info->channelBytes * 0; |
| info->offsetBytesG = info->channelBytes * 1; |
| info->offsetBytesB = info->channelBytes * 2; |
| info->offsetBytesA = info->channelBytes * 3; |
| break; |
| case AVIF_RGB_FORMAT_ARGB: |
| info->offsetBytesA = info->channelBytes * 0; |
| info->offsetBytesR = info->channelBytes * 1; |
| info->offsetBytesG = info->channelBytes * 2; |
| info->offsetBytesB = info->channelBytes * 3; |
| break; |
| case AVIF_RGB_FORMAT_BGR: |
| info->offsetBytesB = info->channelBytes * 0; |
| info->offsetBytesG = info->channelBytes * 1; |
| info->offsetBytesR = info->channelBytes * 2; |
| info->offsetBytesA = 0; |
| break; |
| case AVIF_RGB_FORMAT_BGRA: |
| info->offsetBytesB = info->channelBytes * 0; |
| info->offsetBytesG = info->channelBytes * 1; |
| info->offsetBytesR = info->channelBytes * 2; |
| info->offsetBytesA = info->channelBytes * 3; |
| break; |
| case AVIF_RGB_FORMAT_ABGR: |
| info->offsetBytesA = info->channelBytes * 0; |
| info->offsetBytesB = info->channelBytes * 1; |
| info->offsetBytesG = info->channelBytes * 2; |
| info->offsetBytesR = info->channelBytes * 3; |
| break; |
| case AVIF_RGB_FORMAT_RGB_565: |
| // Since RGB_565 consists of two bytes per RGB pixel, we simply use |
| // the pointer to the red channel to populate the entire pixel value |
| // as a uint16_t. As a result only offsetBytesR is used and the |
| // other offsets are unused. |
| info->offsetBytesR = 0; |
| info->offsetBytesG = 0; |
| info->offsetBytesB = 0; |
| info->offsetBytesA = 0; |
| break; |
| |
| case AVIF_RGB_FORMAT_COUNT: |
| return AVIF_FALSE; |
| } |
| |
| info->maxChannel = (1 << rgb->depth) - 1; |
| info->maxChannelF = (float)info->maxChannel; |
| |
| return AVIF_TRUE; |
| } |
| |
| static avifBool avifGetYUVColorSpaceInfo(const avifImage * image, avifYUVColorSpaceInfo * info) |
| { |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| const avifBool useYCgCo = (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) || |
| (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO); |
| #else |
| const avifBool useYCgCo = AVIF_FALSE; |
| #endif |
| |
| if ((image->depth != 8) && (image->depth != 10) && (image->depth != 12) && (image->depth != 16)) { |
| return AVIF_FALSE; |
| } |
| |
| if (image->yuvFormat <= AVIF_PIXEL_FORMAT_NONE || image->yuvFormat >= AVIF_PIXEL_FORMAT_COUNT) { |
| return AVIF_FALSE; |
| } |
| if (image->yuvRange != AVIF_RANGE_LIMITED && image->yuvRange != AVIF_RANGE_FULL) { |
| return AVIF_FALSE; |
| } |
| |
| // These matrix coefficients values are currently unsupported. Revise this list as more support is added. |
| // |
| // YCgCo performs limited-full range adjustment on R,G,B but the current implementation performs range adjustment |
| // on Y,U,V. So YCgCo with limited range is unsupported. |
| if ((image->matrixCoefficients == 3 /* CICP reserved */) || |
| ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO || useYCgCo) && (image->yuvRange == AVIF_RANGE_LIMITED)) || |
| (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_BT2020_CL) || |
| (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_SMPTE2085) || |
| (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_CL) || |
| (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_ICTCP) || (image->matrixCoefficients >= AVIF_MATRIX_COEFFICIENTS_LAST)) { |
| return AVIF_FALSE; |
| } |
| |
| if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV444) && |
| (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)) { |
| return AVIF_FALSE; |
| } |
| |
| avifGetPixelFormatInfo(image->yuvFormat, &info->formatInfo); |
| avifCalcYUVCoefficients(image, &info->kr, &info->kg, &info->kb); |
| |
| info->channelBytes = (image->depth > 8) ? 2 : 1; |
| |
| info->depth = image->depth; |
| info->range = image->yuvRange; |
| info->maxChannel = (1 << image->depth) - 1; |
| info->biasY = (info->range == AVIF_RANGE_LIMITED) ? (float)(16 << (info->depth - 8)) : 0.0f; |
| info->biasUV = (float)(1 << (info->depth - 1)); |
| info->rangeY = (float)((info->range == AVIF_RANGE_LIMITED) ? (219 << (info->depth - 8)) : info->maxChannel); |
| info->rangeUV = (float)((info->range == AVIF_RANGE_LIMITED) ? (224 << (info->depth - 8)) : info->maxChannel); |
| |
| return AVIF_TRUE; |
| } |
| |
| static avifBool avifPrepareReformatState(const avifImage * image, const avifRGBImage * rgb, avifReformatState * state) |
| { |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| const avifBool useYCgCoRe = (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE); |
| const avifBool useYCgCoRo = (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO); |
| #else |
| const avifBool useYCgCoRe = AVIF_FALSE; |
| const avifBool useYCgCoRo = AVIF_FALSE; |
| #endif |
| if (useYCgCoRe || useYCgCoRo) { |
| const int bitOffset = (useYCgCoRe) ? 2 : 1; |
| if (image->depth - bitOffset != rgb->depth) { |
| return AVIF_FALSE; |
| } |
| } |
| |
| AVIF_CHECK(avifGetRGBColorSpaceInfo(rgb, &state->rgb)); |
| AVIF_CHECK(avifGetYUVColorSpaceInfo(image, &state->yuv)); |
| |
| state->yuv.mode = AVIF_REFORMAT_MODE_YUV_COEFFICIENTS; |
| |
| if (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) { |
| state->yuv.mode = AVIF_REFORMAT_MODE_IDENTITY; |
| } else if (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO) { |
| state->yuv.mode = AVIF_REFORMAT_MODE_YCGCO; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| } else if (useYCgCoRe) { |
| state->yuv.mode = AVIF_REFORMAT_MODE_YCGCO_RE; |
| } else if (useYCgCoRo) { |
| state->yuv.mode = AVIF_REFORMAT_MODE_YCGCO_RO; |
| #endif |
| } |
| |
| if (state->yuv.mode != AVIF_REFORMAT_MODE_YUV_COEFFICIENTS) { |
| state->yuv.kr = 0.0f; |
| state->yuv.kg = 0.0f; |
| state->yuv.kb = 0.0f; |
| } |
| |
| return AVIF_TRUE; |
| } |
| |
| // Formulas 20-31 from https://www.itu.int/rec/T-REC-H.273-201612-I/en |
| static int avifYUVColorSpaceInfoYToUNorm(avifYUVColorSpaceInfo * info, float v) |
| { |
| int unorm = (int)avifRoundf(v * info->rangeY + info->biasY); |
| return AVIF_CLAMP(unorm, 0, info->maxChannel); |
| } |
| |
| static int avifYUVColorSpaceInfoUVToUNorm(avifYUVColorSpaceInfo * info, float v) |
| { |
| int unorm; |
| |
| // YCgCo performs limited-full range adjustment on R,G,B but the current implementation performs range adjustment |
| // on Y,U,V. So YCgCo with limited range is unsupported. |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| assert((info->mode != AVIF_REFORMAT_MODE_YCGCO && info->mode != AVIF_REFORMAT_MODE_YCGCO_RE && info->mode != AVIF_REFORMAT_MODE_YCGCO_RO) || |
| (info->range == AVIF_RANGE_FULL)); |
| #else |
| assert((info->mode != AVIF_REFORMAT_MODE_YCGCO) || (info->range == AVIF_RANGE_FULL)); |
| #endif |
| |
| if (info->mode == AVIF_REFORMAT_MODE_IDENTITY) { |
| unorm = (int)avifRoundf(v * info->rangeY + info->biasY); |
| } else { |
| unorm = (int)avifRoundf(v * info->rangeUV + info->biasUV); |
| } |
| |
| return AVIF_CLAMP(unorm, 0, info->maxChannel); |
| } |
| |
| avifResult avifImageRGBToYUV(avifImage * image, const avifRGBImage * rgb) |
| { |
| if (!rgb->pixels || rgb->format == AVIF_RGB_FORMAT_RGB_565) { |
| return AVIF_RESULT_REFORMAT_FAILED; |
| } |
| |
| avifReformatState state; |
| if (!avifPrepareReformatState(image, rgb, &state)) { |
| return AVIF_RESULT_REFORMAT_FAILED; |
| } |
| |
| if (rgb->isFloat) { |
| return AVIF_RESULT_NOT_IMPLEMENTED; |
| } |
| |
| const avifBool hasAlpha = avifRGBFormatHasAlpha(rgb->format) && !rgb->ignoreAlpha; |
| avifResult allocationResult = avifImageAllocatePlanes(image, hasAlpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV); |
| if (allocationResult != AVIF_RESULT_OK) { |
| return allocationResult; |
| } |
| |
| avifAlphaMultiplyMode alphaMode = AVIF_ALPHA_MULTIPLY_MODE_NO_OP; |
| if (hasAlpha) { |
| if (!rgb->alphaPremultiplied && image->alphaPremultiplied) { |
| alphaMode = AVIF_ALPHA_MULTIPLY_MODE_MULTIPLY; |
| } else if (rgb->alphaPremultiplied && !image->alphaPremultiplied) { |
| alphaMode = AVIF_ALPHA_MULTIPLY_MODE_UNMULTIPLY; |
| } |
| } |
| |
| avifBool converted = AVIF_FALSE; |
| |
| // Try converting with libsharpyuv. |
| if ((rgb->chromaDownsampling == AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV) && (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420)) { |
| const avifResult libSharpYUVResult = avifImageRGBToYUVLibSharpYUV(image, rgb, &state); |
| if (libSharpYUVResult != AVIF_RESULT_OK) { |
| // Return the error if sharpyuv was requested but failed for any reason, including libsharpyuv not being available. |
| return libSharpYUVResult; |
| } |
| converted = AVIF_TRUE; |
| } |
| |
| if (!converted && !rgb->avoidLibYUV && (alphaMode == AVIF_ALPHA_MULTIPLY_MODE_NO_OP)) { |
| avifResult libyuvResult = avifImageRGBToYUVLibYUV(image, rgb); |
| if (libyuvResult == AVIF_RESULT_OK) { |
| converted = AVIF_TRUE; |
| } else if (libyuvResult != AVIF_RESULT_NOT_IMPLEMENTED) { |
| return libyuvResult; |
| } |
| } |
| |
| if (!converted) { |
| const float kr = state.yuv.kr; |
| const float kg = state.yuv.kg; |
| const float kb = state.yuv.kb; |
| |
| struct YUVBlock yuvBlock[2][2]; |
| float rgbPixel[3]; |
| const float rgbMaxChannelF = state.rgb.maxChannelF; |
| uint8_t ** yuvPlanes = image->yuvPlanes; |
| uint32_t * yuvRowBytes = image->yuvRowBytes; |
| for (uint32_t outerJ = 0; outerJ < image->height; outerJ += 2) { |
| for (uint32_t outerI = 0; outerI < image->width; outerI += 2) { |
| int blockW = 2, blockH = 2; |
| if ((outerI + 1) >= image->width) { |
| blockW = 1; |
| } |
| if ((outerJ + 1) >= image->height) { |
| blockH = 1; |
| } |
| |
| // Convert an entire 2x2 block to YUV, and populate any fully sampled channels as we go |
| for (int bJ = 0; bJ < blockH; ++bJ) { |
| for (int bI = 0; bI < blockW; ++bI) { |
| int i = outerI + bI; |
| int j = outerJ + bJ; |
| |
| // Unpack RGB into normalized float |
| if (state.rgb.channelBytes > 1) { |
| rgbPixel[0] = |
| *((uint16_t *)(&rgb->pixels[state.rgb.offsetBytesR + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)])) / |
| rgbMaxChannelF; |
| rgbPixel[1] = |
| *((uint16_t *)(&rgb->pixels[state.rgb.offsetBytesG + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)])) / |
| rgbMaxChannelF; |
| rgbPixel[2] = |
| *((uint16_t *)(&rgb->pixels[state.rgb.offsetBytesB + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)])) / |
| rgbMaxChannelF; |
| } else { |
| rgbPixel[0] = rgb->pixels[state.rgb.offsetBytesR + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)] / |
| rgbMaxChannelF; |
| rgbPixel[1] = rgb->pixels[state.rgb.offsetBytesG + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)] / |
| rgbMaxChannelF; |
| rgbPixel[2] = rgb->pixels[state.rgb.offsetBytesB + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)] / |
| rgbMaxChannelF; |
| } |
| |
| if (alphaMode != AVIF_ALPHA_MULTIPLY_MODE_NO_OP) { |
| float a; |
| if (state.rgb.channelBytes > 1) { |
| a = *((uint16_t *)(&rgb->pixels[state.rgb.offsetBytesA + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)])) / |
| rgbMaxChannelF; |
| } else { |
| a = rgb->pixels[state.rgb.offsetBytesA + (i * state.rgb.pixelBytes) + (j * rgb->rowBytes)] / rgbMaxChannelF; |
| } |
| |
| if (alphaMode == AVIF_ALPHA_MULTIPLY_MODE_MULTIPLY) { |
| if (a == 0) { |
| rgbPixel[0] = 0; |
| rgbPixel[1] = 0; |
| rgbPixel[2] = 0; |
| } else if (a < 1.0f) { |
| rgbPixel[0] *= a; |
| rgbPixel[1] *= a; |
| rgbPixel[2] *= a; |
| } |
| } else { |
| // alphaMode == AVIF_ALPHA_MULTIPLY_MODE_UNMULTIPLY |
| if (a == 0) { |
| rgbPixel[0] = 0; |
| rgbPixel[1] = 0; |
| rgbPixel[2] = 0; |
| } else if (a < 1.0f) { |
| rgbPixel[0] /= a; |
| rgbPixel[1] /= a; |
| rgbPixel[2] /= a; |
| rgbPixel[0] = AVIF_MIN(rgbPixel[0], 1.0f); |
| rgbPixel[1] = AVIF_MIN(rgbPixel[1], 1.0f); |
| rgbPixel[2] = AVIF_MIN(rgbPixel[2], 1.0f); |
| } |
| } |
| } |
| |
| // RGB -> YUV conversion |
| if (state.yuv.mode == AVIF_REFORMAT_MODE_IDENTITY) { |
| // Formulas 41,42,43 from https://www.itu.int/rec/T-REC-H.273-201612-I/en |
| yuvBlock[bI][bJ].y = rgbPixel[1]; // G |
| yuvBlock[bI][bJ].u = rgbPixel[2]; // B |
| yuvBlock[bI][bJ].v = rgbPixel[0]; // R |
| } else if (state.yuv.mode == AVIF_REFORMAT_MODE_YCGCO) { |
| // Formulas 44,45,46 from https://www.itu.int/rec/T-REC-H.273-201612-I/en |
| yuvBlock[bI][bJ].y = 0.5f * rgbPixel[1] + 0.25f * (rgbPixel[0] + rgbPixel[2]); |
| yuvBlock[bI][bJ].u = 0.5f * rgbPixel[1] - 0.25f * (rgbPixel[0] + rgbPixel[2]); |
| yuvBlock[bI][bJ].v = 0.5f * (rgbPixel[0] - rgbPixel[2]); |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| } else if (state.yuv.mode == AVIF_REFORMAT_MODE_YCGCO_RE || state.yuv.mode == AVIF_REFORMAT_MODE_YCGCO_RO) { |
| // Formulas from JVET-U0093. |
| const int R = (int)avifRoundf(AVIF_CLAMP(rgbPixel[0] * rgbMaxChannelF, 0.0f, rgbMaxChannelF)); |
| const int G = (int)avifRoundf(AVIF_CLAMP(rgbPixel[1] * rgbMaxChannelF, 0.0f, rgbMaxChannelF)); |
| const int B = (int)avifRoundf(AVIF_CLAMP(rgbPixel[2] * rgbMaxChannelF, 0.0f, rgbMaxChannelF)); |
| const int Co = R - B; |
| const int t = B + (Co >> 1); |
| const int Cg = G - t; |
| yuvBlock[bI][bJ].y = (t + (Cg >> 1)) / state.yuv.rangeY; |
| yuvBlock[bI][bJ].u = Cg / state.yuv.rangeUV; |
| yuvBlock[bI][bJ].v = Co / state.yuv.rangeUV; |
| #endif |
| } else { |
| float Y = (kr * rgbPixel[0]) + (kg * rgbPixel[1]) + (kb * rgbPixel[2]); |
| yuvBlock[bI][bJ].y = Y; |
| yuvBlock[bI][bJ].u = (rgbPixel[2] - Y) / (2 * (1 - kb)); |
| yuvBlock[bI][bJ].v = (rgbPixel[0] - Y) / (2 * (1 - kr)); |
| } |
| |
| if (state.yuv.channelBytes > 1) { |
| uint16_t * pY = (uint16_t *)&yuvPlanes[AVIF_CHAN_Y][(i * 2) + (j * yuvRowBytes[AVIF_CHAN_Y])]; |
| *pY = (uint16_t)avifYUVColorSpaceInfoYToUNorm(&state.yuv, yuvBlock[bI][bJ].y); |
| if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) { |
| // YUV444, full chroma |
| uint16_t * pU = (uint16_t *)&yuvPlanes[AVIF_CHAN_U][(i * 2) + (j * yuvRowBytes[AVIF_CHAN_U])]; |
| *pU = (uint16_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, yuvBlock[bI][bJ].u); |
| uint16_t * pV = (uint16_t *)&yuvPlanes[AVIF_CHAN_V][(i * 2) + (j * yuvRowBytes[AVIF_CHAN_V])]; |
| *pV = (uint16_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, yuvBlock[bI][bJ].v); |
| } |
| } else { |
| yuvPlanes[AVIF_CHAN_Y][i + (j * yuvRowBytes[AVIF_CHAN_Y])] = |
| (uint8_t)avifYUVColorSpaceInfoYToUNorm(&state.yuv, yuvBlock[bI][bJ].y); |
| if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) { |
| // YUV444, full chroma |
| yuvPlanes[AVIF_CHAN_U][i + (j * yuvRowBytes[AVIF_CHAN_U])] = |
| (uint8_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, yuvBlock[bI][bJ].u); |
| yuvPlanes[AVIF_CHAN_V][i + (j * yuvRowBytes[AVIF_CHAN_V])] = |
| (uint8_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, yuvBlock[bI][bJ].v); |
| } |
| } |
| } |
| } |
| |
| // Populate any subsampled channels with averages from the 2x2 block |
| if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { |
| // Do nothing on chroma planes. |
| } else if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) { |
| // YUV420, average 4 samples (2x2) |
| |
| float sumU = 0.0f; |
| float sumV = 0.0f; |
| for (int bJ = 0; bJ < blockH; ++bJ) { |
| for (int bI = 0; bI < blockW; ++bI) { |
| sumU += yuvBlock[bI][bJ].u; |
| sumV += yuvBlock[bI][bJ].v; |
| } |
| } |
| float totalSamples = (float)(blockW * blockH); |
| float avgU = sumU / totalSamples; |
| float avgV = sumV / totalSamples; |
| |
| const int chromaShiftX = 1; |
| const int chromaShiftY = 1; |
| int uvI = outerI >> chromaShiftX; |
| int uvJ = outerJ >> chromaShiftY; |
| if (state.yuv.channelBytes > 1) { |
| uint16_t * pU = (uint16_t *)&yuvPlanes[AVIF_CHAN_U][(uvI * 2) + (uvJ * yuvRowBytes[AVIF_CHAN_U])]; |
| *pU = (uint16_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgU); |
| uint16_t * pV = (uint16_t *)&yuvPlanes[AVIF_CHAN_V][(uvI * 2) + (uvJ * yuvRowBytes[AVIF_CHAN_V])]; |
| *pV = (uint16_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgV); |
| } else { |
| yuvPlanes[AVIF_CHAN_U][uvI + (uvJ * yuvRowBytes[AVIF_CHAN_U])] = |
| (uint8_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgU); |
| yuvPlanes[AVIF_CHAN_V][uvI + (uvJ * yuvRowBytes[AVIF_CHAN_V])] = |
| (uint8_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgV); |
| } |
| } else if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV422) { |
| // YUV422, average 2 samples (1x2), twice |
| |
| for (int bJ = 0; bJ < blockH; ++bJ) { |
| float sumU = 0.0f; |
| float sumV = 0.0f; |
| for (int bI = 0; bI < blockW; ++bI) { |
| sumU += yuvBlock[bI][bJ].u; |
| sumV += yuvBlock[bI][bJ].v; |
| } |
| float totalSamples = (float)blockW; |
| float avgU = sumU / totalSamples; |
| float avgV = sumV / totalSamples; |
| |
| const int chromaShiftX = 1; |
| int uvI = outerI >> chromaShiftX; |
| int uvJ = outerJ + bJ; |
| if (state.yuv.channelBytes > 1) { |
| uint16_t * pU = (uint16_t *)&yuvPlanes[AVIF_CHAN_U][(uvI * 2) + (uvJ * yuvRowBytes[AVIF_CHAN_U])]; |
| *pU = (uint16_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgU); |
| uint16_t * pV = (uint16_t *)&yuvPlanes[AVIF_CHAN_V][(uvI * 2) + (uvJ * yuvRowBytes[AVIF_CHAN_V])]; |
| *pV = (uint16_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgV); |
| } else { |
| yuvPlanes[AVIF_CHAN_U][uvI + (uvJ * yuvRowBytes[AVIF_CHAN_U])] = |
| (uint8_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgU); |
| yuvPlanes[AVIF_CHAN_V][uvI + (uvJ * yuvRowBytes[AVIF_CHAN_V])] = |
| (uint8_t)avifYUVColorSpaceInfoUVToUNorm(&state.yuv, avgV); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if (image->alphaPlane && image->alphaRowBytes) { |
| avifAlphaParams params; |
| |
| params.width = image->width; |
| params.height = image->height; |
| params.dstDepth = image->depth; |
| params.dstPlane = image->alphaPlane; |
| params.dstRowBytes = image->alphaRowBytes; |
| params.dstOffsetBytes = 0; |
| params.dstPixelBytes = state.yuv.channelBytes; |
| |
| if (avifRGBFormatHasAlpha(rgb->format) && !rgb->ignoreAlpha) { |
| params.srcDepth = rgb->depth; |
| params.srcPlane = rgb->pixels; |
| params.srcRowBytes = rgb->rowBytes; |
| params.srcOffsetBytes = state.rgb.offsetBytesA; |
| params.srcPixelBytes = state.rgb.pixelBytes; |
| |
| avifReformatAlpha(¶ms); |
| } else { |
| // libyuv does not fill alpha when converting from RGB to YUV so |
| // fill it regardless of the value of convertedWithLibYUV. |
| avifFillAlpha(¶ms); |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| // Allocates and fills look-up tables for going from YUV limited/full unorm -> full range RGB FP32. |
| // Review this when implementing YCgCo limited range support. |
| static avifBool avifCreateYUVToRGBLookUpTables(float ** unormFloatTableY, float ** unormFloatTableUV, uint32_t depth, const avifReformatState * state) |
| { |
| const size_t cpCount = (size_t)1 << depth; |
| |
| assert(unormFloatTableY); |
| *unormFloatTableY = avifAlloc(cpCount * sizeof(**unormFloatTableY)); |
| AVIF_CHECK(*unormFloatTableY); |
| for (uint32_t cp = 0; cp < cpCount; ++cp) { |
| (*unormFloatTableY)[cp] = ((float)cp - state->yuv.biasY) / state->yuv.rangeY; |
| } |
| |
| if (unormFloatTableUV) { |
| if (state->yuv.mode == AVIF_REFORMAT_MODE_IDENTITY) { |
| // Just reuse the luma table since the chroma values are the same. |
| *unormFloatTableUV = *unormFloatTableY; |
| } else { |
| *unormFloatTableUV = avifAlloc(cpCount * sizeof(**unormFloatTableUV)); |
| if (!*unormFloatTableUV) { |
| avifFree(*unormFloatTableY); |
| *unormFloatTableY = NULL; |
| return AVIF_FALSE; |
| } |
| for (uint32_t cp = 0; cp < cpCount; ++cp) { |
| (*unormFloatTableUV)[cp] = ((float)cp - state->yuv.biasUV) / state->yuv.rangeUV; |
| } |
| } |
| } |
| return AVIF_TRUE; |
| } |
| |
| // Frees look-up tables allocated with avifCreateYUVToRGBLookUpTables(). |
| static void avifFreeYUVToRGBLookUpTables(float ** unormFloatTableY, float ** unormFloatTableUV) |
| { |
| if (unormFloatTableUV) { |
| if (*unormFloatTableUV != *unormFloatTableY) { |
| avifFree(*unormFloatTableUV); |
| } |
| *unormFloatTableUV = NULL; |
| } |
| |
| avifFree(*unormFloatTableY); |
| *unormFloatTableY = NULL; |
| } |
| |
| #define RGB565(R, G, B) ((uint16_t)(((B) >> 3) | (((G) >> 2) << 5) | (((R) >> 3) << 11))) |
| |
| static void avifStoreRGB8Pixel(avifRGBFormat format, uint8_t R, uint8_t G, uint8_t B, uint8_t * ptrR, uint8_t * ptrG, uint8_t * ptrB) |
| { |
| if (format == AVIF_RGB_FORMAT_RGB_565) { |
| // References for RGB565 color conversion: |
| // * https://docs.microsoft.com/en-us/windows/win32/directshow/working-with-16-bit-rgb |
| // * https://chromium.googlesource.com/libyuv/libyuv/+/9892d70c965678381d2a70a1c9002d1cf136ee78/source/row_common.cc#2362 |
| *(uint16_t *)ptrR = RGB565(R, G, B); |
| return; |
| } |
| *ptrR = R; |
| *ptrG = G; |
| *ptrB = B; |
| } |
| |
| // Note: This function handles alpha (un)multiply. |
| static avifResult avifImageYUVAnyToRGBAnySlow(const avifImage * image, |
| avifRGBImage * rgb, |
| const avifReformatState * state, |
| avifAlphaMultiplyMode alphaMultiplyMode) |
| { |
| // Aliases for some state |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| float * unormFloatTableY = NULL; |
| float * unormFloatTableUV = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| const uint32_t yuvChannelBytes = state->yuv.channelBytes; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| |
| // Aliases for plane data |
| const uint8_t * yPlane = image->yuvPlanes[AVIF_CHAN_Y]; |
| const uint8_t * uPlane = image->yuvPlanes[AVIF_CHAN_U]; |
| const uint8_t * vPlane = image->yuvPlanes[AVIF_CHAN_V]; |
| const uint8_t * aPlane = image->alphaPlane; |
| const uint32_t yRowBytes = image->yuvRowBytes[AVIF_CHAN_Y]; |
| const uint32_t uRowBytes = image->yuvRowBytes[AVIF_CHAN_U]; |
| const uint32_t vRowBytes = image->yuvRowBytes[AVIF_CHAN_V]; |
| const uint32_t aRowBytes = image->alphaRowBytes; |
| |
| // Various observations and limits |
| const avifBool hasColor = (uPlane && vPlane && (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)); |
| const uint16_t yuvMaxChannel = (uint16_t)state->yuv.maxChannel; |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| |
| // If toRGBAlphaMode is active (not no-op), assert that the alpha plane is present. The end of |
| // the avifPrepareReformatState() function should ensure this, but this assert makes it clear |
| // to clang's analyzer. |
| assert((alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_NO_OP) || aPlane); |
| |
| for (uint32_t j = 0; j < image->height; ++j) { |
| // uvJ is used only when hasColor is true. |
| const uint32_t uvJ = hasColor ? (j >> state->yuv.formatInfo.chromaShiftY) : 0; |
| const uint8_t * ptrY8 = &yPlane[j * yRowBytes]; |
| const uint8_t * ptrU8 = uPlane ? &uPlane[(uvJ * uRowBytes)] : NULL; |
| const uint8_t * ptrV8 = vPlane ? &vPlane[(uvJ * vRowBytes)] : NULL; |
| const uint8_t * ptrA8 = aPlane ? &aPlane[j * aRowBytes] : NULL; |
| const uint16_t * ptrY16 = (const uint16_t *)ptrY8; |
| const uint16_t * ptrU16 = (const uint16_t *)ptrU8; |
| const uint16_t * ptrV16 = (const uint16_t *)ptrV8; |
| const uint16_t * ptrA16 = (const uint16_t *)ptrA8; |
| |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| float Y, Cb = 0.5f, Cr = 0.5f; |
| |
| // Calculate Y |
| uint16_t unormY; |
| if (image->depth == 8) { |
| unormY = ptrY8[i]; |
| } else { |
| // clamp incoming data to protect against bad LUT lookups |
| unormY = AVIF_MIN(ptrY16[i], yuvMaxChannel); |
| } |
| Y = unormFloatTableY[unormY]; |
| |
| // Calculate Cb and Cr |
| if (hasColor) { |
| const uint32_t uvI = i >> state->yuv.formatInfo.chromaShiftX; |
| if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) { |
| uint16_t unormU, unormV; |
| |
| if (image->depth == 8) { |
| unormU = ptrU8[uvI]; |
| unormV = ptrV8[uvI]; |
| } else { |
| // clamp incoming data to protect against bad LUT lookups |
| unormU = AVIF_MIN(ptrU16[uvI], yuvMaxChannel); |
| unormV = AVIF_MIN(ptrV16[uvI], yuvMaxChannel); |
| } |
| |
| Cb = unormFloatTableUV[unormU]; |
| Cr = unormFloatTableUV[unormV]; |
| } else { |
| // Upsample to 444: |
| // |
| // * * * * |
| // A B |
| // * 1 2 * |
| // |
| // * 3 4 * |
| // C D |
| // * * * * |
| // |
| // When converting from YUV420 to RGB, for any given "high-resolution" RGB |
| // coordinate (1,2,3,4,*), there are up to four "low-resolution" UV samples |
| // (A,B,C,D) that are "nearest" to the pixel. For RGB pixel #1, A is the closest |
| // UV sample, B and C are "adjacent" to it on the same row and column, and D is |
| // the diagonal. For RGB pixel 3, C is the closest UV sample, A and D are |
| // adjacent, and B is the diagonal. Sometimes the adjacent pixel on the same row |
| // is to the left or right, and sometimes the adjacent pixel on the same column |
| // is up or down. For any edge or corner, there might only be only one or two |
| // samples nearby, so they'll be duplicated. |
| // |
| // The following code attempts to find all four nearest UV samples and put them |
| // in the following unormU and unormV grid as follows: |
| // |
| // unorm[0][0] = closest ( weights: bilinear: 9/16, nearest: 1 ) |
| // unorm[1][0] = adjacent col ( weights: bilinear: 3/16, nearest: 0 ) |
| // unorm[0][1] = adjacent row ( weights: bilinear: 3/16, nearest: 0 ) |
| // unorm[1][1] = diagonal ( weights: bilinear: 1/16, nearest: 0 ) |
| // |
| // It then weights them according to the requested upsampling set in avifRGBImage. |
| |
| uint16_t unormU[2][2], unormV[2][2]; |
| |
| // How many bytes to add to a uint8_t pointer index to get to the adjacent (lesser) sample in a given direction |
| int uAdjCol, vAdjCol, uAdjRow, vAdjRow; |
| if ((i == 0) || ((i == (image->width - 1)) && ((i % 2) != 0))) { |
| uAdjCol = 0; |
| vAdjCol = 0; |
| } else { |
| if ((i % 2) != 0) { |
| uAdjCol = yuvChannelBytes; |
| vAdjCol = yuvChannelBytes; |
| } else { |
| uAdjCol = -1 * yuvChannelBytes; |
| vAdjCol = -1 * yuvChannelBytes; |
| } |
| } |
| |
| // For YUV422, uvJ will always be a fresh value (always corresponds to j), so |
| // we'll simply duplicate the sample as if we were on the top or bottom row and |
| // it'll behave as plain old linear (1D) upsampling, which is all we want. |
| if ((j == 0) || ((j == (image->height - 1)) && ((j % 2) != 0)) || (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV422)) { |
| uAdjRow = 0; |
| vAdjRow = 0; |
| } else { |
| if ((j % 2) != 0) { |
| uAdjRow = (int)uRowBytes; |
| vAdjRow = (int)vRowBytes; |
| } else { |
| uAdjRow = -1 * (int)uRowBytes; |
| vAdjRow = -1 * (int)vRowBytes; |
| } |
| } |
| |
| if (image->depth == 8) { |
| unormU[0][0] = uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes)]; |
| unormV[0][0] = vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes)]; |
| unormU[1][0] = uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes) + uAdjCol]; |
| unormV[1][0] = vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes) + vAdjCol]; |
| unormU[0][1] = uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes) + uAdjRow]; |
| unormV[0][1] = vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes) + vAdjRow]; |
| unormU[1][1] = uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes) + uAdjCol + uAdjRow]; |
| unormV[1][1] = vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes) + vAdjCol + vAdjRow]; |
| } else { |
| unormU[0][0] = *((const uint16_t *)&uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes)]); |
| unormV[0][0] = *((const uint16_t *)&vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes)]); |
| unormU[1][0] = *((const uint16_t *)&uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes) + uAdjCol]); |
| unormV[1][0] = *((const uint16_t *)&vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes) + vAdjCol]); |
| unormU[0][1] = *((const uint16_t *)&uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes) + uAdjRow]); |
| unormV[0][1] = *((const uint16_t *)&vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes) + vAdjRow]); |
| unormU[1][1] = *((const uint16_t *)&uPlane[(uvJ * uRowBytes) + (uvI * yuvChannelBytes) + uAdjCol + uAdjRow]); |
| unormV[1][1] = *((const uint16_t *)&vPlane[(uvJ * vRowBytes) + (uvI * yuvChannelBytes) + vAdjCol + vAdjRow]); |
| |
| // clamp incoming data to protect against bad LUT lookups |
| for (int bJ = 0; bJ < 2; ++bJ) { |
| for (int bI = 0; bI < 2; ++bI) { |
| unormU[bI][bJ] = AVIF_MIN(unormU[bI][bJ], yuvMaxChannel); |
| unormV[bI][bJ] = AVIF_MIN(unormV[bI][bJ], yuvMaxChannel); |
| } |
| } |
| } |
| |
| if ((rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_FASTEST) || |
| (rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_NEAREST)) { |
| // Nearest neighbor; ignore all UVs but the closest one |
| Cb = unormFloatTableUV[unormU[0][0]]; |
| Cr = unormFloatTableUV[unormV[0][0]]; |
| } else { |
| // Bilinear filtering with weights |
| Cb = (unormFloatTableUV[unormU[0][0]] * (9.0f / 16.0f)) + (unormFloatTableUV[unormU[1][0]] * (3.0f / 16.0f)) + |
| (unormFloatTableUV[unormU[0][1]] * (3.0f / 16.0f)) + (unormFloatTableUV[unormU[1][1]] * (1.0f / 16.0f)); |
| Cr = (unormFloatTableUV[unormV[0][0]] * (9.0f / 16.0f)) + (unormFloatTableUV[unormV[1][0]] * (3.0f / 16.0f)) + |
| (unormFloatTableUV[unormV[0][1]] * (3.0f / 16.0f)) + (unormFloatTableUV[unormV[1][1]] * (1.0f / 16.0f)); |
| } |
| } |
| } |
| |
| float R, G, B; |
| if (hasColor) { |
| if (state->yuv.mode == AVIF_REFORMAT_MODE_IDENTITY) { |
| // Identity (GBR): Formulas 41,42,43 from https://www.itu.int/rec/T-REC-H.273-201612-I/en |
| G = Y; |
| B = Cb; |
| R = Cr; |
| } else if (state->yuv.mode == AVIF_REFORMAT_MODE_YCGCO) { |
| // YCgCo: Formulas 47,48,49,50 from https://www.itu.int/rec/T-REC-H.273-201612-I/en |
| const float t = Y - Cb; |
| G = Y + Cb; |
| B = t - Cr; |
| R = t + Cr; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) |
| } else if (state->yuv.mode == AVIF_REFORMAT_MODE_YCGCO_RE || state->yuv.mode == AVIF_REFORMAT_MODE_YCGCO_RO) { |
| const int YY = unormY; |
| const int Cg = (int)avifRoundf(Cb * yuvMaxChannel); |
| const int Co = (int)avifRoundf(Cr * yuvMaxChannel); |
| const int t = YY - (Cg >> 1); |
| G = (float)AVIF_CLAMP(t + Cg, 0, state->rgb.maxChannel); |
| B = (float)AVIF_CLAMP(t - (Co >> 1), 0, state->rgb.maxChannel); |
| R = (float)AVIF_CLAMP(B + Co, 0, state->rgb.maxChannel); |
| G /= rgbMaxChannelF; |
| B /= rgbMaxChannelF; |
| R /= rgbMaxChannelF; |
| #endif |
| } else { |
| // Normal YUV |
| R = Y + (2 * (1 - kr)) * Cr; |
| B = Y + (2 * (1 - kb)) * Cb; |
| G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| } |
| } else { |
| // Monochrome: just populate all channels with luma (state->yuv.mode is irrelevant) |
| R = Y; |
| G = Y; |
| B = Y; |
| } |
| |
| float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| if (alphaMultiplyMode != AVIF_ALPHA_MULTIPLY_MODE_NO_OP) { |
| // Calculate A |
| uint16_t unormA; |
| if (image->depth == 8) { |
| unormA = ptrA8[i]; |
| } else { |
| unormA = AVIF_MIN(ptrA16[i], yuvMaxChannel); |
| } |
| const float A = unormA / ((float)state->yuv.maxChannel); |
| const float Ac = AVIF_CLAMP(A, 0.0f, 1.0f); |
| |
| if (alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_MULTIPLY) { |
| if (Ac == 0.0f) { |
| Rc = 0.0f; |
| Gc = 0.0f; |
| Bc = 0.0f; |
| } else if (Ac < 1.0f) { |
| Rc *= Ac; |
| Gc *= Ac; |
| Bc *= Ac; |
| } |
| } else { |
| // alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_UNMULTIPLY |
| if (Ac == 0.0f) { |
| Rc = 0.0f; |
| Gc = 0.0f; |
| Bc = 0.0f; |
| } else if (Ac < 1.0f) { |
| Rc /= Ac; |
| Gc /= Ac; |
| Bc /= Ac; |
| Rc = AVIF_MIN(Rc, 1.0f); |
| Gc = AVIF_MIN(Gc, 1.0f); |
| Bc = AVIF_MIN(Bc, 1.0f); |
| } |
| } |
| } |
| |
| if (rgb->depth == 8) { |
| avifStoreRGB8Pixel(rgb->format, |
| (uint8_t)(0.5f + (Rc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Gc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Bc * rgbMaxChannelF)), |
| ptrR, |
| ptrG, |
| ptrB); |
| } else { |
| *((uint16_t *)ptrR) = (uint16_t)(0.5f + (Rc * rgbMaxChannelF)); |
| *((uint16_t *)ptrG) = (uint16_t)(0.5f + (Gc * rgbMaxChannelF)); |
| *((uint16_t *)ptrB) = (uint16_t)(0.5f + (Bc * rgbMaxChannelF)); |
| } |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV16ToRGB16Color(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| float * unormFloatTableUV = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const uint16_t yuvMaxChannel = (uint16_t)state->yuv.maxChannel; |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint32_t uvJ = j >> state->yuv.formatInfo.chromaShiftY; |
| const uint16_t * const ptrY = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| const uint16_t * const ptrU = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_U][(uvJ * image->yuvRowBytes[AVIF_CHAN_U])]; |
| const uint16_t * const ptrV = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_V][(uvJ * image->yuvRowBytes[AVIF_CHAN_V])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| uint32_t uvI = i >> state->yuv.formatInfo.chromaShiftX; |
| |
| // clamp incoming data to protect against bad LUT lookups |
| const uint16_t unormY = AVIF_MIN(ptrY[i], yuvMaxChannel); |
| const uint16_t unormU = AVIF_MIN(ptrU[uvI], yuvMaxChannel); |
| const uint16_t unormV = AVIF_MIN(ptrV[uvI], yuvMaxChannel); |
| |
| // Convert unorm to float |
| const float Y = unormFloatTableY[unormY]; |
| const float Cb = unormFloatTableUV[unormU]; |
| const float Cr = unormFloatTableUV[unormV]; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| *((uint16_t *)ptrR) = (uint16_t)(0.5f + (Rc * rgbMaxChannelF)); |
| *((uint16_t *)ptrG) = (uint16_t)(0.5f + (Gc * rgbMaxChannelF)); |
| *((uint16_t *)ptrB) = (uint16_t)(0.5f + (Bc * rgbMaxChannelF)); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV16ToRGB16Mono(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, NULL, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const uint16_t maxChannel = (uint16_t)state->yuv.maxChannel; |
| const float maxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint16_t * const ptrY = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| // clamp incoming data to protect against bad LUT lookups |
| const uint16_t unormY = AVIF_MIN(ptrY[i], maxChannel); |
| |
| // Convert unorm to float |
| const float Y = unormFloatTableY[unormY]; |
| const float Cb = 0.0f; |
| const float Cr = 0.0f; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| *((uint16_t *)ptrR) = (uint16_t)(0.5f + (Rc * maxChannelF)); |
| *((uint16_t *)ptrG) = (uint16_t)(0.5f + (Gc * maxChannelF)); |
| *((uint16_t *)ptrB) = (uint16_t)(0.5f + (Bc * maxChannelF)); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, NULL); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV16ToRGB8Color(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| float * unormFloatTableUV = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const uint16_t yuvMaxChannel = (uint16_t)state->yuv.maxChannel; |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint32_t uvJ = j >> state->yuv.formatInfo.chromaShiftY; |
| const uint16_t * const ptrY = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| const uint16_t * const ptrU = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_U][(uvJ * image->yuvRowBytes[AVIF_CHAN_U])]; |
| const uint16_t * const ptrV = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_V][(uvJ * image->yuvRowBytes[AVIF_CHAN_V])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| uint32_t uvI = i >> state->yuv.formatInfo.chromaShiftX; |
| |
| // clamp incoming data to protect against bad LUT lookups |
| const uint16_t unormY = AVIF_MIN(ptrY[i], yuvMaxChannel); |
| const uint16_t unormU = AVIF_MIN(ptrU[uvI], yuvMaxChannel); |
| const uint16_t unormV = AVIF_MIN(ptrV[uvI], yuvMaxChannel); |
| |
| // Convert unorm to float |
| const float Y = unormFloatTableY[unormY]; |
| const float Cb = unormFloatTableUV[unormU]; |
| const float Cr = unormFloatTableUV[unormV]; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| avifStoreRGB8Pixel(rgb->format, |
| (uint8_t)(0.5f + (Rc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Gc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Bc * rgbMaxChannelF)), |
| ptrR, |
| ptrG, |
| ptrB); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV16ToRGB8Mono(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, NULL, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const uint16_t yuvMaxChannel = (uint16_t)state->yuv.maxChannel; |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint16_t * const ptrY = (uint16_t *)&image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| // clamp incoming data to protect against bad LUT lookups |
| const uint16_t unormY = AVIF_MIN(ptrY[i], yuvMaxChannel); |
| |
| // Convert unorm to float |
| const float Y = unormFloatTableY[unormY]; |
| const float Cb = 0.0f; |
| const float Cr = 0.0f; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| avifStoreRGB8Pixel(rgb->format, |
| (uint8_t)(0.5f + (Rc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Gc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Bc * rgbMaxChannelF)), |
| ptrR, |
| ptrG, |
| ptrB); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, NULL); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV8ToRGB16Color(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| float * unormFloatTableUV = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint32_t uvJ = j >> state->yuv.formatInfo.chromaShiftY; |
| const uint8_t * const ptrY = &image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| const uint8_t * const ptrU = &image->yuvPlanes[AVIF_CHAN_U][(uvJ * image->yuvRowBytes[AVIF_CHAN_U])]; |
| const uint8_t * const ptrV = &image->yuvPlanes[AVIF_CHAN_V][(uvJ * image->yuvRowBytes[AVIF_CHAN_V])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| uint32_t uvI = i >> state->yuv.formatInfo.chromaShiftX; |
| |
| // Convert unorm to float (no clamp necessary, the full uint8_t range is a legal lookup) |
| const float Y = unormFloatTableY[ptrY[i]]; |
| const float Cb = unormFloatTableUV[ptrU[uvI]]; |
| const float Cr = unormFloatTableUV[ptrV[uvI]]; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| *((uint16_t *)ptrR) = (uint16_t)(0.5f + (Rc * rgbMaxChannelF)); |
| *((uint16_t *)ptrG) = (uint16_t)(0.5f + (Gc * rgbMaxChannelF)); |
| *((uint16_t *)ptrB) = (uint16_t)(0.5f + (Bc * rgbMaxChannelF)); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV8ToRGB16Mono(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, NULL, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint8_t * const ptrY = &image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| // Convert unorm to float (no clamp necessary, the full uint8_t range is a legal lookup) |
| const float Y = unormFloatTableY[ptrY[i]]; |
| const float Cb = 0.0f; |
| const float Cr = 0.0f; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| *((uint16_t *)ptrR) = (uint16_t)(0.5f + (Rc * rgbMaxChannelF)); |
| *((uint16_t *)ptrG) = (uint16_t)(0.5f + (Gc * rgbMaxChannelF)); |
| *((uint16_t *)ptrB) = (uint16_t)(0.5f + (Bc * rgbMaxChannelF)); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, NULL); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageIdentity8ToRGB8ColorFullRange(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint8_t * const ptrY = &image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| const uint8_t * const ptrU = &image->yuvPlanes[AVIF_CHAN_U][(j * image->yuvRowBytes[AVIF_CHAN_U])]; |
| const uint8_t * const ptrV = &image->yuvPlanes[AVIF_CHAN_V][(j * image->yuvRowBytes[AVIF_CHAN_V])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| // This is intentionally a per-row conditional instead of a per-pixel |
| // conditional. This makes the "else" path (much more common than the |
| // "if" path) much faster than having a per-pixel branch. |
| if (rgb->format == AVIF_RGB_FORMAT_RGB_565) { |
| for (uint32_t i = 0; i < image->width; ++i) { |
| *(uint16_t *)ptrR = RGB565(ptrV[i], ptrY[i], ptrU[i]); |
| ptrR += rgbPixelBytes; |
| } |
| } else { |
| for (uint32_t i = 0; i < image->width; ++i) { |
| *ptrR = ptrV[i]; |
| *ptrG = ptrY[i]; |
| *ptrB = ptrU[i]; |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV8ToRGB8Color(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| float * unormFloatTableUV = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint32_t uvJ = j >> state->yuv.formatInfo.chromaShiftY; |
| const uint8_t * const ptrY = &image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| const uint8_t * const ptrU = &image->yuvPlanes[AVIF_CHAN_U][(uvJ * image->yuvRowBytes[AVIF_CHAN_U])]; |
| const uint8_t * const ptrV = &image->yuvPlanes[AVIF_CHAN_V][(uvJ * image->yuvRowBytes[AVIF_CHAN_V])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| uint32_t uvI = i >> state->yuv.formatInfo.chromaShiftX; |
| |
| // Convert unorm to float (no clamp necessary, the full uint8_t range is a legal lookup) |
| const float Y = unormFloatTableY[ptrY[i]]; |
| const float Cb = unormFloatTableUV[ptrU[uvI]]; |
| const float Cr = unormFloatTableUV[ptrV[uvI]]; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| avifStoreRGB8Pixel(rgb->format, |
| (uint8_t)(0.5f + (Rc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Gc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Bc * rgbMaxChannelF)), |
| ptrR, |
| ptrG, |
| ptrB); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, &unormFloatTableUV); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUV8ToRGB8Mono(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) |
| { |
| const float kr = state->yuv.kr; |
| const float kg = state->yuv.kg; |
| const float kb = state->yuv.kb; |
| const uint32_t rgbPixelBytes = state->rgb.pixelBytes; |
| float * unormFloatTableY = NULL; |
| AVIF_CHECKERR(avifCreateYUVToRGBLookUpTables(&unormFloatTableY, NULL, image->depth, state), AVIF_RESULT_OUT_OF_MEMORY); |
| |
| const float rgbMaxChannelF = state->rgb.maxChannelF; |
| for (uint32_t j = 0; j < image->height; ++j) { |
| const uint8_t * const ptrY = &image->yuvPlanes[AVIF_CHAN_Y][(j * image->yuvRowBytes[AVIF_CHAN_Y])]; |
| uint8_t * ptrR = &rgb->pixels[state->rgb.offsetBytesR + (j * rgb->rowBytes)]; |
| uint8_t * ptrG = &rgb->pixels[state->rgb.offsetBytesG + (j * rgb->rowBytes)]; |
| uint8_t * ptrB = &rgb->pixels[state->rgb.offsetBytesB + (j * rgb->rowBytes)]; |
| |
| for (uint32_t i = 0; i < image->width; ++i) { |
| // Convert unorm to float (no clamp necessary, the full uint8_t range is a legal lookup) |
| const float Y = unormFloatTableY[ptrY[i]]; |
| const float Cb = 0.0f; |
| const float Cr = 0.0f; |
| |
| const float R = Y + (2 * (1 - kr)) * Cr; |
| const float B = Y + (2 * (1 - kb)) * Cb; |
| const float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); |
| const float Rc = AVIF_CLAMP(R, 0.0f, 1.0f); |
| const float Gc = AVIF_CLAMP(G, 0.0f, 1.0f); |
| const float Bc = AVIF_CLAMP(B, 0.0f, 1.0f); |
| |
| avifStoreRGB8Pixel(rgb->format, |
| (uint8_t)(0.5f + (Rc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Gc * rgbMaxChannelF)), |
| (uint8_t)(0.5f + (Bc * rgbMaxChannelF)), |
| ptrR, |
| ptrG, |
| ptrB); |
| |
| ptrR += rgbPixelBytes; |
| ptrG += rgbPixelBytes; |
| ptrB += rgbPixelBytes; |
| } |
| } |
| avifFreeYUVToRGBLookUpTables(&unormFloatTableY, NULL); |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifRGBImageToF16(avifRGBImage * rgb) |
| { |
| avifResult libyuvResult = AVIF_RESULT_NOT_IMPLEMENTED; |
| if (!rgb->avoidLibYUV) { |
| libyuvResult = avifRGBImageToF16LibYUV(rgb); |
| } |
| if (libyuvResult != AVIF_RESULT_NOT_IMPLEMENTED) { |
| return libyuvResult; |
| } |
| const uint32_t channelCount = avifRGBFormatChannelCount(rgb->format); |
| const float scale = 1.0f / ((1 << rgb->depth) - 1); |
| // This constant comes from libyuv. For details, see here: |
| // https://chromium.googlesource.com/libyuv/libyuv/+/2f87e9a7/source/row_common.cc#3537 |
| const float multiplier = 1.9259299444e-34f * scale; |
| uint16_t * pixelRowBase = (uint16_t *)rgb->pixels; |
| const uint32_t stride = rgb->rowBytes >> 1; |
| for (uint32_t j = 0; j < rgb->height; ++j) { |
| uint16_t * pixel = pixelRowBase; |
| for (uint32_t i = 0; i < rgb->width * channelCount; ++i, ++pixel) { |
| union |
| { |
| float f; |
| uint32_t u32; |
| } f16; |
| f16.f = *pixel * multiplier; |
| *pixel = (uint16_t)(f16.u32 >> 13); |
| } |
| pixelRowBase += stride; |
| } |
| return AVIF_RESULT_OK; |
| } |
| |
| static avifResult avifImageYUVToRGBImpl(const avifImage * image, avifRGBImage * rgb, avifReformatState * state, avifAlphaMultiplyMode alphaMultiplyMode) |
| { |
| avifBool convertedWithLibYUV = AVIF_FALSE; |
| // Reformat alpha, if user asks for it, or (un)multiply processing needs it. |
| avifBool reformatAlpha = avifRGBFormatHasAlpha(rgb->format) && |
| (!rgb->ignoreAlpha || (alphaMultiplyMode != AVIF_ALPHA_MULTIPLY_MODE_NO_OP)); |
| // This value is used only when reformatAlpha is true. |
| avifBool alphaReformattedWithLibYUV = AVIF_FALSE; |
| if (!rgb->avoidLibYUV && ((alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_NO_OP) || avifRGBFormatHasAlpha(rgb->format))) { |
| avifResult libyuvResult = avifImageYUVToRGBLibYUV(image, rgb, reformatAlpha, &alphaReformattedWithLibYUV); |
| if (libyuvResult == AVIF_RESULT_OK) { |
| convertedWithLibYUV = AVIF_TRUE; |
| } else { |
| if (libyuvResult != AVIF_RESULT_NOT_IMPLEMENTED) { |
| return libyuvResult; |
| } |
| } |
| } |
| |
| if (reformatAlpha && !alphaReformattedWithLibYUV) { |
| avifAlphaParams params; |
| |
| params.width = rgb->width; |
| params.height = rgb->height; |
| params.dstDepth = rgb->depth; |
| params.dstPlane = rgb->pixels; |
| params.dstRowBytes = rgb->rowBytes; |
| params.dstOffsetBytes = state->rgb.offsetBytesA; |
| params.dstPixelBytes = state->rgb.pixelBytes; |
| |
| if (image->alphaPlane && image->alphaRowBytes) { |
| params.srcDepth = image->depth; |
| params.srcPlane = image->alphaPlane; |
| params.srcRowBytes = image->alphaRowBytes; |
| params.srcOffsetBytes = 0; |
| params.srcPixelBytes = state->yuv.channelBytes; |
| |
| avifReformatAlpha(¶ms); |
| } else { |
| avifFillAlpha(¶ms); |
| } |
| } |
| |
| if (!convertedWithLibYUV) { |
| // libyuv is either unavailable or unable to perform the specific conversion required here. |
| // Look over the available built-in "fast" routines for YUV->RGB conversion and see if one |
| // fits the current combination, or as a last resort, call avifImageYUVAnyToRGBAnySlow(), |
| // which handles every possibly YUV->RGB combination, but very slowly (in comparison). |
| |
| avifResult convertResult = AVIF_RESULT_NOT_IMPLEMENTED; |
| |
| const avifBool hasColor = |
| (image->yuvRowBytes[AVIF_CHAN_U] && image->yuvRowBytes[AVIF_CHAN_V] && (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)); |
| |
| if ((!hasColor || (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) || |
| ((rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_FASTEST) || (rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_NEAREST))) && |
| (alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_NO_OP || avifRGBFormatHasAlpha(rgb->format))) { |
| // Explanations on the above conditional: |
| // * None of these fast paths currently support bilinear upsampling, so avoid all of them |
| // unless the YUV data isn't subsampled or they explicitly requested AVIF_CHROMA_UPSAMPLING_NEAREST. |
| // * None of these fast paths currently handle alpha (un)multiply, so avoid all of them |
| // if we can't do alpha (un)multiply as a separated post step (destination format doesn't have alpha). |
| |
| if (state->yuv.mode == AVIF_REFORMAT_MODE_IDENTITY) { |
| if ((image->depth == 8) && (rgb->depth == 8) && (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) && |
| (image->yuvRange == AVIF_RANGE_FULL)) { |
| convertResult = avifImageIdentity8ToRGB8ColorFullRange(image, rgb, state); |
| } |
| |
| // TODO: Add more fast paths for identity |
| } else if (state->yuv.mode == AVIF_REFORMAT_MODE_YUV_COEFFICIENTS) { |
| if (image->depth > 8) { |
| // yuv:u16 |
| |
| if (rgb->depth > 8) { |
| // yuv:u16, rgb:u16 |
| |
| if (hasColor) { |
| convertResult = avifImageYUV16ToRGB16Color(image, rgb, state); |
| } else { |
| convertResult = avifImageYUV16ToRGB16Mono(image, rgb, state); |
| } |
| } else { |
| // yuv:u16, rgb:u8 |
| |
| if (hasColor) { |
| convertResult = avifImageYUV16ToRGB8Color(image, rgb, state); |
| } else { |
| convertResult = avifImageYUV16ToRGB8Mono(image, rgb, state); |
| } |
| } |
| } else { |
| // yuv:u8 |
| |
| if (rgb->depth > 8) { |
| // yuv:u8, rgb:u16 |
| |
| if (hasColor) { |
| convertResult = avifImageYUV8ToRGB16Color(image, rgb, state); |
| } else { |
| convertResult = avifImageYUV8ToRGB16Mono(image, rgb, state); |
| } |
| } else { |
| // yuv:u8, rgb:u8 |
| |
| if (hasColor) { |
| convertResult = avifImageYUV8ToRGB8Color(image, rgb, state); |
| } else { |
| convertResult = avifImageYUV8ToRGB8Mono(image, rgb, state); |
| } |
| } |
| } |
| } |
| } |
| |
| if (convertResult == AVIF_RESULT_NOT_IMPLEMENTED) { |
| // If we get here, there is no fast path for this combination. Time to be slow! |
| convertResult = avifImageYUVAnyToRGBAnySlow(image, rgb, state, alphaMultiplyMode); |
| |
| // The slow path also handles alpha (un)multiply, so forget the operation here. |
| alphaMultiplyMode = AVIF_ALPHA_MULTIPLY_MODE_NO_OP; |
| } |
| |
| if (convertResult != AVIF_RESULT_OK) { |
| return convertResult; |
| } |
| } |
| |
| // Process alpha premultiplication, if necessary |
| if (alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_MULTIPLY) { |
| avifResult result = avifRGBImagePremultiplyAlpha(rgb); |
| if (result != AVIF_RESULT_OK) { |
| return result; |
| } |
| } else if (alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_UNMULTIPLY) { |
| avifResult result = avifRGBImageUnpremultiplyAlpha(rgb); |
| if (result != AVIF_RESULT_OK) { |
| return result; |
| } |
| } |
| |
| // Convert pixels to half floats (F16), if necessary. |
| if (rgb->isFloat) { |
| return avifRGBImageToF16(rgb); |
| } |
| |
| return AVIF_RESULT_OK; |
| } |
| |
| typedef struct |
| { |
| #if defined(_WIN32) |
| HANDLE thread; |
| #else |
| pthread_t thread; |
| #endif |
| avifImage image; |
| avifRGBImage rgb; |
| avifReformatState * state; |
| avifAlphaMultiplyMode alphaMultiplyMode; |
| avifResult result; |
| avifBool threadCreated; |
| } YUVToRGBThreadData; |
| |
| #if defined(_WIN32) |
| static unsigned int __stdcall avifImageYUVToRGBThreadWorker(void * arg) |
| #else |
| static void * avifImageYUVToRGBThreadWorker(void * arg) |
| #endif |
| { |
| YUVToRGBThreadData * data = (YUVToRGBThreadData *)arg; |
| data->result = avifImageYUVToRGBImpl(&data->image, &data->rgb, data->state, data->alphaMultiplyMode); |
| #if defined(_WIN32) |
| return 0; |
| #else |
| return NULL; |
| #endif |
| } |
| |
| static avifBool avifCreateYUVToRGBThread(YUVToRGBThreadData * tdata) |
| { |
| #if defined(_WIN32) |
| tdata->thread = (HANDLE)_beginthreadex(/*security=*/NULL, |
| /*stack_size=*/0, |
| &avifImageYUVToRGBThreadWorker, |
| tdata, |
| /*initflag=*/0, |
| /*thrdaddr=*/NULL); |
| return tdata->thread != NULL; |
| #else |
| // TODO: Set the thread name for ease of debugging. |
| return pthread_create(&tdata->thread, NULL, &avifImageYUVToRGBThreadWorker, tdata) == 0; |
| #endif |
| } |
| |
| static avifBool avifJoinYUVToRGBThread(YUVToRGBThreadData * tdata) |
| { |
| #if defined(_WIN32) |
| return WaitForSingleObject(tdata->thread, INFINITE) == WAIT_OBJECT_0 && CloseHandle(tdata->thread) != 0; |
| #else |
| return pthread_join(tdata->thread, NULL) == 0; |
| #endif |
| } |
| |
| avifResult avifImageYUVToRGB(const avifImage * image, avifRGBImage * rgb) |
| { |
| // It is okay for rgb->maxThreads to be equal to zero in order to allow clients to zero initialize the avifRGBImage struct |
| // with memset. |
| if (!image->yuvPlanes[AVIF_CHAN_Y] || rgb->maxThreads < 0) { |
| return AVIF_RESULT_REFORMAT_FAILED; |
| } |
| |
| avifReformatState state; |
| if (!avifPrepareReformatState(image, rgb, &state)) { |
| return AVIF_RESULT_REFORMAT_FAILED; |
| } |
| |
| avifAlphaMultiplyMode alphaMultiplyMode = AVIF_ALPHA_MULTIPLY_MODE_NO_OP; |
| if (image->alphaPlane) { |
| if (!avifRGBFormatHasAlpha(rgb->format) || rgb->ignoreAlpha) { |
| // if we are converting some image with alpha into a format without alpha, we should do 'premultiply alpha' before |
| // discarding alpha plane. This has the same effect of rendering this image on a black background, which makes sense. |
| if (!image->alphaPremultiplied) { |
| alphaMultiplyMode = AVIF_ALPHA_MULTIPLY_MODE_MULTIPLY; |
| } |
| } else { |
| if (!image->alphaPremultiplied && rgb->alphaPremultiplied) { |
| alphaMultiplyMode = AVIF_ALPHA_MULTIPLY_MODE_MULTIPLY; |
| } else if (image->alphaPremultiplied && !rgb->alphaPremultiplied) { |
| alphaMultiplyMode = AVIF_ALPHA_MULTIPLY_MODE_UNMULTIPLY; |
| } |
| } |
| } |
| |
| // In practice, we rarely need more than 8 threads for YUV to RGB conversion. |
| uint32_t jobs = AVIF_CLAMP(rgb->maxThreads, 1, 8); |
| |
| // When yuv format is 420 and chromaUpsampling could be BILINEAR, there is a dependency across the horizontal borders of each |
| // job. So we disallow multithreading in that case. |
| if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420 && (rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_AUTOMATIC || |
| rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_BEST_QUALITY || |
| rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_BILINEAR)) { |
| jobs = 1; |
| } |
| |
| // Each thread worker needs at least 2 Y rows (to account for potential U/V subsampling). |
| if (jobs == 1 || (image->height / 2) < jobs) { |
| return avifImageYUVToRGBImpl(image, rgb, &state, alphaMultiplyMode); |
| } |
| |
| AVIF_ARRAY_DECLARE(YUVToRGBThreadDataArray, YUVToRGBThreadData, threadData); |
| YUVToRGBThreadDataArray tdArray; |
| if (!avifArrayCreate(&tdArray, sizeof(YUVToRGBThreadData), jobs)) { |
| return AVIF_RESULT_OUT_OF_MEMORY; |
| } |
| int rowsPerJob = image->height / jobs; |
| if (rowsPerJob % 2) { |
| ++rowsPerJob; |
| } |
| const int rowsForLastJob = image->height - rowsPerJob * (jobs - 1); |
| int startRow = 0; |
| uint32_t i; |
| for (i = 0; i < jobs; ++i, startRow += rowsPerJob) { |
| YUVToRGBThreadData * tdata = &tdArray.threadData[i]; |
| const avifCropRect rect = { .x = 0, .y = startRow, .width = image->width, .height = (i == jobs - 1) ? rowsForLastJob : rowsPerJob }; |
| if (avifImageSetViewRect(&tdata->image, image, &rect) != AVIF_RESULT_OK) { |
| tdata->result = AVIF_RESULT_REFORMAT_FAILED; |
| break; |
| } |
| |
| tdata->rgb = *rgb; |
| tdata->rgb.pixels += startRow * (size_t)rgb->rowBytes; |
| tdata->rgb.height = tdata->image.height; |
| |
| tdata->state = &state; |
| tdata->alphaMultiplyMode = alphaMultiplyMode; |
| |
| if (i > 0) { |
| tdata->threadCreated = avifCreateYUVToRGBThread(tdata); |
| if (!tdata->threadCreated) { |
| tdata->result = AVIF_RESULT_REFORMAT_FAILED; |
| break; |
| } |
| } |
| } |
| // If above loop ran successfully, Run the first job in the current thread. |
| if (i == jobs) { |
| avifImageYUVToRGBThreadWorker(&tdArray.threadData[0]); |
| } |
| avifResult result = AVIF_RESULT_OK; |
| for (i = 0; i < jobs; ++i) { |
| YUVToRGBThreadData * tdata = &tdArray.threadData[i]; |
| if (tdata->threadCreated && !avifJoinYUVToRGBThread(tdata)) { |
| result = AVIF_RESULT_REFORMAT_FAILED; |
| } |
| if (tdata->result != AVIF_RESULT_OK) { |
| result = tdata->result; |
| } |
| } |
| avifArrayDestroy(&tdArray); |
| return result; |
| } |
| |
| // Limited -> Full |
| // Plan: subtract limited offset, then multiply by ratio of FULLSIZE/LIMITEDSIZE (rounding), then clamp. |
| // RATIO = (FULLY - 0) / (MAXLIMITEDY - MINLIMITEDY) |
| // ----------------------------------------- |
| // ( ( (v - MINLIMITEDY) | subtract limited offset |
| // * FULLY | multiply numerator of ratio |
| // ) + ((MAXLIMITEDY - MINLIMITEDY) / 2) | add 0.5 (half of denominator) to round |
| // ) / (MAXLIMITEDY - MINLIMITEDY) | divide by denominator of ratio |
| // AVIF_CLAMP(v, 0, FULLY) | clamp to full range |
| // ----------------------------------------- |
| #define LIMITED_TO_FULL(MINLIMITEDY, MAXLIMITEDY, FULLY) \ |
| v = (((v - MINLIMITEDY) * FULLY) + ((MAXLIMITEDY - MINLIMITEDY) / 2)) / (MAXLIMITEDY - MINLIMITEDY); \ |
| v = AVIF_CLAMP(v, 0, FULLY) |
| |
| // Full -> Limited |
| // Plan: multiply by ratio of LIMITEDSIZE/FULLSIZE (rounding), then add limited offset, then clamp. |
| // RATIO = (MAXLIMITEDY - MINLIMITEDY) / (FULLY - 0) |
| // ----------------------------------------- |
| // ( ( (v * (MAXLIMITEDY - MINLIMITEDY)) | multiply numerator of ratio |
| // + (FULLY / 2) | add 0.5 (half of denominator) to round |
| // ) / FULLY | divide by denominator of ratio |
| // ) + MINLIMITEDY | add limited offset |
| // AVIF_CLAMP(v, MINLIMITEDY, MAXLIMITEDY) | clamp to limited range |
| // ----------------------------------------- |
| #define FULL_TO_LIMITED(MINLIMITEDY, MAXLIMITEDY, FULLY) \ |
| v = (((v * (MAXLIMITEDY - MINLIMITEDY)) + (FULLY / 2)) / FULLY) + MINLIMITEDY; \ |
| v = AVIF_CLAMP(v, MINLIMITEDY, MAXLIMITEDY) |
| |
| int avifLimitedToFullY(uint32_t depth, int v) |
| { |
| switch (depth) { |
| case 8: |
| LIMITED_TO_FULL(16, 235, 255); |
| break; |
| case 10: |
| LIMITED_TO_FULL(64, 940, 1023); |
| break; |
| case 12: |
| LIMITED_TO_FULL(256, 3760, 4095); |
| break; |
| } |
| return v; |
| } |
| |
| int avifLimitedToFullUV(uint32_t depth, int v) |
| { |
| switch (depth) { |
| case 8: |
| LIMITED_TO_FULL(16, 240, 255); |
| break; |
| case 10: |
| LIMITED_TO_FULL(64, 960, 1023); |
| break; |
| case 12: |
| LIMITED_TO_FULL(256, 3840, 4095); |
| break; |
| } |
| return v; |
| } |
| |
| int avifFullToLimitedY(uint32_t depth, int v) |
| { |
| switch (depth) { |
| case 8: |
| FULL_TO_LIMITED(16, 235, 255); |
| break; |
| case 10: |
| FULL_TO_LIMITED(64, 940, 1023); |
| break; |
| case 12: |
| FULL_TO_LIMITED(256, 3760, 4095); |
| break; |
| } |
| return v; |
| } |
| |
| int avifFullToLimitedUV(uint32_t depth, int v) |
| { |
| switch (depth) { |
| case 8: |
| FULL_TO_LIMITED(16, 240, 255); |
| break; |
| case 10: |
| FULL_TO_LIMITED(64, 960, 1023); |
| break; |
| case 12: |
| FULL_TO_LIMITED(256, 3840, 4095); |
| break; |
| } |
| return v; |
| } |