From 21e4fb523a2c1bf3a4330283f4140f5a979a97eb Mon Sep 17 00:00:00 2001 From: Nicholas Hayes <0xC0000054@users.noreply.github.com> Date: Sat, 5 Nov 2022 14:01:13 -0600 Subject: [PATCH] Add preliminary support for loading HDR images The Netflix HDR sample images are loading correctly. Currently the HDR support is limited to SMPTE ST 2084 (BT.2100 PQ). It is unclear how the HDR support should work with premultiplied alpha images. I filed an issue in the av1-avif repository for clarification on that: https://github.com/AOMediaCodec/av1-avif/issues/197 --- src/common/AvifFormat.cpp | 2 + src/common/AvifFormat.h | 2 + src/common/AvifFormat.r | 2 +- src/common/ColorProfileGeneration.cpp | 71 ++++- src/common/ColorTransfer.cpp | 99 +++++++ src/common/ColorTransfer.h | 36 +++ src/common/Read.cpp | 139 +++++++--- src/common/ReadHeifImage.cpp | 378 ++++++++++++++++++++++++++ src/common/ReadHeifImage.h | 12 + src/common/Utilities.cpp | 3 + src/common/YUVDecode.h | 43 ++- src/common/YuvDecode.cpp | 191 +++++++++++++ vs/AvifFormat.vcxproj | 2 + vs/AvifFormat.vcxproj.filters | 6 + 14 files changed, 951 insertions(+), 35 deletions(-) create mode 100644 src/common/ColorTransfer.cpp create mode 100644 src/common/ColorTransfer.h diff --git a/src/common/AvifFormat.cpp b/src/common/AvifFormat.cpp index d8328e9..ca9c2e2 100644 --- a/src/common/AvifFormat.cpp +++ b/src/common/AvifFormat.cpp @@ -77,7 +77,9 @@ namespace { globals->context = nullptr; globals->imageHandle = nullptr; + globals->imageHandleNclxProfile = nullptr; globals->image = nullptr; + globals->imageHandleProfileType = heif_color_profile_type_not_present; globals->saveOptions.quality = 85; globals->saveOptions.chromaSubsampling = ChromaSubsampling::Yuv422; globals->saveOptions.compressionSpeed = CompressionSpeed::Default; diff --git a/src/common/AvifFormat.h b/src/common/AvifFormat.h index a3bbe84..38dfa44 100644 --- a/src/common/AvifFormat.h +++ b/src/common/AvifFormat.h @@ -63,7 +63,9 @@ struct Globals { heif_context* context; heif_image_handle* imageHandle; + heif_color_profile_nclx* imageHandleNclxProfile; heif_image* image; + heif_color_profile_type imageHandleProfileType; SaveUIOptions saveOptions; bool libheifInitialized; diff --git a/src/common/AvifFormat.r b/src/common/AvifFormat.r index 96ec144..8490f3a 100644 --- a/src/common/AvifFormat.r +++ b/src/common/AvifFormat.r @@ -95,7 +95,7 @@ resource 'PiPL' (ResourceID, plugInName " PiPL", purgeable) noLABColor }, - EnableInfo { "in (PSHOP_ImageMode, RGBMode, RGB48Mode, GrayScaleMode, Gray16Mode)" }, + EnableInfo { "in (PSHOP_ImageMode, RGBMode, RGB48Mode, GrayScaleMode, Gray16Mode, RGB96Mode, Gray32Mode)" }, PlugInMaxSize { 1073741824, 1073741824 }, diff --git a/src/common/ColorProfileGeneration.cpp b/src/common/ColorProfileGeneration.cpp index f38e4a9..43f4e93 100644 --- a/src/common/ColorProfileGeneration.cpp +++ b/src/common/ColorProfileGeneration.cpp @@ -345,7 +345,10 @@ void SetIccProfileFromNclx(FormatRecord* formatRecord, const heif_color_profile_ toneCurve.reset(cmsBuildParametricToneCurve(context.get(), 4, Parameters)); description = L"Grayscale (sRGB TRC)"; } - else if (transferCharacteristics == heif_transfer_characteristic_ITU_R_BT_709_5) + else if (transferCharacteristics == heif_transfer_characteristic_ITU_R_BT_709_5 || + transferCharacteristics == heif_transfer_characteristic_ITU_R_BT_601_6 || + transferCharacteristics == heif_transfer_characteristic_ITU_R_BT_2020_2_10bit || + transferCharacteristics == heif_transfer_characteristic_ITU_R_BT_2020_2_12bit) { cmsFloat64Number Parameters[5] { @@ -357,7 +360,26 @@ void SetIccProfileFromNclx(FormatRecord* formatRecord, const heif_color_profile_ }; toneCurve.reset(cmsBuildParametricToneCurve(context.get(), 4, Parameters)); - description = L"Grayscale (Rec. 709 TRC)"; + + switch (primaries) + { + case heif_color_primaries_ITU_R_BT_2020_2_and_2100_0: + description = L"Grayscale (BT. 2020)"; + break; + case heif_color_primaries_ITU_R_BT_709_5: + case heif_color_primaries_ITU_R_BT_470_6_System_M: + case heif_color_primaries_ITU_R_BT_470_6_System_B_G: + case heif_color_primaries_ITU_R_BT_601_6: + case heif_color_primaries_SMPTE_240M: + case heif_color_primaries_generic_film: + case heif_color_primaries_SMPTE_ST_428_1: + case heif_color_primaries_SMPTE_RP_431_2: + case heif_color_primaries_SMPTE_EG_432_1: + case heif_color_primaries_EBU_Tech_3213_E: + default: + description = L"Grayscale (Rec. 709 TRC)"; + break; + } } if (toneCurve && description != nullptr) @@ -437,6 +459,51 @@ void SetIccProfileFromNclx(FormatRecord* formatRecord, const heif_color_profile_ description); } } + else if (primaries == heif_color_primaries_ITU_R_BT_2020_2_and_2100_0) + { + const cmsCIExyY whitepoint = { 0.3127, 0.3290, 1.0f }; // D65 + const cmsCIExyYTRIPLE rgbPrimaries = + { + { 0.708, 0.292, 1.0 }, + { 0.170, 0.797, 1.0 }, + { 0.131, 0.046, 1.0 } + }; + + ScopedLcmsToneCurve toneCurve; + const wchar_t* description = nullptr; + + if (linear) + { + toneCurve.reset(cmsBuildGamma(context.get(), 1.0)); + description = L"BT. 2020 (Linear RGB Profile)"; + } + else if (transferCharacteristics == heif_transfer_characteristic_ITU_R_BT_2020_2_10bit || + transferCharacteristics == heif_transfer_characteristic_ITU_R_BT_2020_2_12bit) + { + // BT. 2020 uses the same transfer curve as Rec. 709. + cmsFloat64Number Parameters[5] + { + 1.0 / 0.45, + 1.0 / 1.099296826809442, + 1.0 - 1 / 1.099296826809442, + 1.0 / 4.5, + 4.5 * 0.018053968510807, + }; + + toneCurve.reset(cmsBuildParametricToneCurve(context.get(), 4, Parameters)); + description = L"BT. 2020"; + } + + if (toneCurve && description != nullptr) + { + profile = BuildRGBProfile( + context.get(), + &whitepoint, + &rgbPrimaries, + toneCurve.get(), + description); + } + } if (profile) { diff --git a/src/common/ColorTransfer.cpp b/src/common/ColorTransfer.cpp new file mode 100644 index 0000000..c2f459c --- /dev/null +++ b/src/common/ColorTransfer.cpp @@ -0,0 +1,99 @@ +/* + * This file is part of avif-format, an AV1 Image (AVIF) file format + * plug-in for Adobe Photoshop(R). + * + * Copyright (c) 2021, 2022 Nicholas Hayes + * + * avif-format is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * avif-format is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with avif-format. If not, see . + */ + +#include "ColorTransfer.h" +#include + +namespace +{ + // The sRGB reference viewing environment has a maximum luminance level of 80 nits + // See the 'Screen luminance level' value in the sRGB reference viewing environment table + // https://en.wikipedia.org/wiki/SRGB#Viewing_environment + constexpr float srgbMaxLuminanceLevel = 80.0f; + // PQ (SMPTE ST 2084) has a maximum luminance level of 10000 nits + // https://en.wikipedia.org/wiki/Perceptual_quantizer + constexpr float pqMaxLuminanceLevel = 10000; + + inline float LinearToPQ(float normalizedLinearValue) + { + // These constant values are taken from the Perceptual quantizer article on Wikipedia + // https://en.wikipedia.org/wiki/Perceptual_quantizer + constexpr float m1 = 2610.0f / 16384.0f; + constexpr float m2 = 2523.0f / 4096.0f * 128.0f; + constexpr float c1 = 3424.0f / 4096.0f; // c3 - c2 + 1 + constexpr float c2 = 2413.0f / 4096.0f * 32.0f; + constexpr float c3 = 2392.0f / 4096.0f * 32.0f; + + if (normalizedLinearValue < 0.0f) + { + return 0.0f; + } + + const float x = powf(normalizedLinearValue, m1); + const float pq = powf((c1 + c2 * x) / (1.0f + c3 * x), m2); + + return pq; + } + + inline float PQToLinear(float value) + { + // These constant values are taken from the Perceptual quantizer article on Wikipedia + // https://en.wikipedia.org/wiki/Perceptual_quantizer + constexpr float m1 = 2610.0f / 16384.0f; + constexpr float m2 = 2523.0f / 4096.0f * 128.0f; + constexpr float c1 = 3424.0f / 4096.0f; // c3 - c2 + 1 + constexpr float c2 = 2413.0f / 4096.0f * 32.0f; + constexpr float c3 = 2392.0f / 4096.0f * 32.0f; + + if (value < 0.0f) + { + return 0.0f; + } + + const float x = powf(value, 1.0f / m2); + const float normalizedLinear = powf(std::max(x - c1, 0.0f) / (c2 - c3 * x), 1.0f / m1); + + // We have to adjust for the difference in the maximum luminance level between + // PQ and sRGB, otherwise the image is too dark. + return normalizedLinear * (pqMaxLuminanceLevel / srgbMaxLuminanceLevel); + } +} + +float TransferCurveToLinear(float value, ColorTransferCurveType curveType) +{ + switch (curveType) + { + case ColorTransferCurveType::PQ: + return PQToLinear(value); + default: + throw std::runtime_error("Unsupported color transfer curve type."); + } +} + +float LinearToTransferCurve(float value, ColorTransferCurveType curveType) +{ + switch (curveType) + { + case ColorTransferCurveType::PQ: + return LinearToPQ(value); + default: + throw std::runtime_error("Unsupported color transfer curve type."); + } +} diff --git a/src/common/ColorTransfer.h b/src/common/ColorTransfer.h new file mode 100644 index 0000000..84fc5f2 --- /dev/null +++ b/src/common/ColorTransfer.h @@ -0,0 +1,36 @@ +/* + * This file is part of avif-format, an AV1 Image (AVIF) file format + * plug-in for Adobe Photoshop(R). + * + * Copyright (c) 2021, 2022 Nicholas Hayes + * + * avif-format is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * avif-format is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with avif-format. If not, see . + */ + +#ifndef COLORTRANSFER_H +#define COLORTRANSFER_H + +#include +#include + +enum class ColorTransferCurveType +{ + PQ +}; + +float TransferCurveToLinear(float value, ColorTransferCurveType curveType); + +float LinearToTransferCurve(float value, ColorTransferCurveType curveType); + +#endif // !COLORTRANSFER_H diff --git a/src/common/Read.cpp b/src/common/Read.cpp index 95f4504..432969c 100644 --- a/src/common/Read.cpp +++ b/src/common/Read.cpp @@ -169,6 +169,18 @@ namespace return alphaState; } + + bool IsHDRImage(const heif_color_profile_nclx* nclx) + { + bool result = false; + + if (nclx != nullptr) + { + result = nclx->transfer_characteristics == heif_transfer_characteristic_ITU_R_BT_2100_0_PQ; + } + + return result; + } } OSErr DoReadPrepare(FormatRecordPtr formatRecord) @@ -245,6 +257,15 @@ OSErr DoReadStart(FormatRecordPtr formatRecord, Globals* globals) formatRecord->imageSize.v = static_cast(height); } + const heif_color_profile_type imageHandleProfileType = heif_image_handle_get_color_profile_type(primaryImage.get()); + + ScopedHeifNclxProfile imageHandleNclxProfile; + + if (imageHandleProfileType == heif_color_profile_type_nclx) + { + imageHandleNclxProfile = GetNclxColorProfile(primaryImage.get()); + } + ScopedHeifImage image = DecodeImage(primaryImage.get(), heif_colorspace_undefined, heif_chroma_undefined); const heif_colorspace colorSpace = heif_image_get_colorspace(image.get()); @@ -265,8 +286,16 @@ OSErr DoReadStart(FormatRecordPtr formatRecord, Globals* globals) break; case 10: case 12: - formatRecord->imageMode = plugInModeGray16; - formatRecord->depth = 16; + if (IsHDRImage(imageHandleNclxProfile.get())) + { + formatRecord->imageMode = plugInModeGrayScale; + formatRecord->depth = 32; + } + else + { + formatRecord->imageMode = plugInModeGray16; + formatRecord->depth = 16; + } break; default: throw OSErrException(formatCannotRead); @@ -288,8 +317,16 @@ OSErr DoReadStart(FormatRecordPtr formatRecord, Globals* globals) break; case 10: case 12: - formatRecord->imageMode = plugInModeRGB48; - formatRecord->depth = 16; + if (IsHDRImage(imageHandleNclxProfile.get())) + { + formatRecord->imageMode = plugInModeRGBColor; + formatRecord->depth = 32; + } + else + { + formatRecord->imageMode = plugInModeRGB48; + formatRecord->depth = 16; + } break; default: throw OSErrException(formatCannotRead); @@ -311,8 +348,16 @@ OSErr DoReadStart(FormatRecordPtr formatRecord, Globals* globals) break; case 10: case 12: - formatRecord->imageMode = plugInModeRGB48; - formatRecord->depth = 16; + if (IsHDRImage(imageHandleNclxProfile.get())) + { + formatRecord->imageMode = plugInModeRGBColor; + formatRecord->depth = 32; + } + else + { + formatRecord->imageMode = plugInModeRGB48; + formatRecord->depth = 16; + } break; default: throw OSErrException(formatCannotRead); @@ -334,7 +379,9 @@ OSErr DoReadStart(FormatRecordPtr formatRecord, Globals* globals) // The image data and meta-data will be set in DoReadContinue. globals->context = context.release(); globals->imageHandle = primaryImage.release(); + globals->imageHandleNclxProfile = imageHandleNclxProfile.release(); globals->image = image.release(); + globals->imageHandleProfileType = imageHandleProfileType; } catch (const std::bad_alloc&) { @@ -368,26 +415,56 @@ OSErr DoReadContinue(FormatRecordPtr formatRecord, Globals* globals) try { - ScopedHeifNclxProfile imageNclxProfile = GetNclxColorProfile(globals->image); + ScopedHeifNclxProfile imageNclxProfile; + + const heif_color_profile_nclx* nclxProfile = globals->imageHandleNclxProfile; + + if (nclxProfile == nullptr) + { + // If the image handle does not have color information from a NCLX 'colr' box + // try to get the color information from the image bitstream. + imageNclxProfile = GetNclxColorProfile(globals->image); + if (imageNclxProfile) + { + nclxProfile = imageNclxProfile.get(); + } + } const AlphaState alphaState = GetAlphaState(globals->imageHandle); - switch (formatRecord->imageMode) + if (IsMonochromeImage(formatRecord)) + { + switch (formatRecord->depth) + { + case 8: + ReadHeifImageGrayEightBit(globals->image, alphaState, nclxProfile, formatRecord); + break; + case 16: + ReadHeifImageGraySixteenBit(globals->image, alphaState, nclxProfile, formatRecord); + break; + case 32: + ReadHeifImageGrayThirtyTwoBit(globals->image, alphaState, nclxProfile, formatRecord); + break; + default: + throw std::runtime_error("Unsupported host bit depth"); + } + } + else { - case plugInModeGrayScale: - ReadHeifImageGrayEightBit(globals->image, alphaState, imageNclxProfile.get(), formatRecord); - break; - case plugInModeGray16: - ReadHeifImageGraySixteenBit(globals->image, alphaState, imageNclxProfile.get(), formatRecord); - break; - case plugInModeRGBColor: - ReadHeifImageRGBEightBit(globals->image, alphaState, imageNclxProfile.get(), formatRecord); - break; - case plugInModeRGB48: - ReadHeifImageRGBSixteenBit(globals->image, alphaState, imageNclxProfile.get(), formatRecord); - break; - default: - throw OSErrException(formatCannotRead); + switch (formatRecord->depth) + { + case 8: + ReadHeifImageRGBEightBit(globals->image, alphaState, nclxProfile, formatRecord); + break; + case 16: + ReadHeifImageRGBSixteenBit(globals->image, alphaState, nclxProfile, formatRecord); + break; + case 32: + ReadHeifImageRGBThirtyTwoBit(globals->image, alphaState, nclxProfile, formatRecord); + break; + default: + throw std::runtime_error("Unsupported host bit depth"); + } } SetRect(formatRecord, 0, 0, 0, 0); @@ -403,7 +480,7 @@ OSErr DoReadContinue(FormatRecordPtr formatRecord, Globals* globals) if (formatRecord->canUseICCProfiles) { - const heif_color_profile_type imageHandleProfileType = heif_image_handle_get_color_profile_type(globals->imageHandle); + const heif_color_profile_type imageHandleProfileType = globals->imageHandleProfileType; if (imageHandleProfileType == heif_color_profile_type_prof || imageHandleProfileType == heif_color_profile_type_rICC) @@ -412,15 +489,9 @@ OSErr DoReadContinue(FormatRecordPtr formatRecord, Globals* globals) } else { - if (imageNclxProfile) - { - SetIccProfileFromNclx(formatRecord, imageNclxProfile.get()); - } - else if (imageHandleProfileType == heif_color_profile_type_nclx) + if (nclxProfile != nullptr) { - ScopedHeifNclxProfile imageHandleNclxProfile = GetNclxColorProfile(globals->imageHandle); - - SetIccProfileFromNclx(formatRecord, imageHandleNclxProfile.get()); + SetIccProfileFromNclx(formatRecord, nclxProfile); } } } @@ -466,6 +537,12 @@ OSErr DoReadFinish(Globals* globals) globals->imageHandle = nullptr; } + if (globals->imageHandleNclxProfile != nullptr) + { + heif_nclx_color_profile_free(globals->imageHandleNclxProfile); + globals->imageHandleNclxProfile = nullptr; + } + if (globals->context != nullptr) { heif_context_free(globals->context); diff --git a/src/common/ReadHeifImage.cpp b/src/common/ReadHeifImage.cpp index c5e0db9..2a9b441 100644 --- a/src/common/ReadHeifImage.cpp +++ b/src/common/ReadHeifImage.cpp @@ -19,10 +19,12 @@ */ #include "ReadHeifImage.h" +#include "ColorTransfer.h" #include "PremultipliedAlpha.h" #include "ScopedBufferSuite.h" #include "Utilities.h" #include "YUVDecode.h" +#include namespace { @@ -284,6 +286,125 @@ namespace } } } + + void ReadHeifImageYUVThirtyTwoBit( + const heif_image* image, + AlphaState alphaState, + const heif_color_profile_nclx* nclxProfile, + FormatRecordPtr formatRecord, + ColorTransferCurveType curveType) + { + const heif_chroma chroma = heif_image_get_chroma_format(image); + + const int lumaBitsPerPixel = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + + if (heif_image_get_bits_per_pixel_range(image, heif_channel_Cb) != lumaBitsPerPixel || + heif_image_get_bits_per_pixel_range(image, heif_channel_Cr) != lumaBitsPerPixel) + { + throw std::runtime_error("The chroma channel bit depth does not match the main image."); + } + + const VPoint imageSize = GetImageSize(formatRecord); + const bool hasAlpha = alphaState != AlphaState::None; + + SetupFormatRecord(formatRecord, imageSize); + + int yPlaneStride; + const uint8_t* yPlaneScan0 = heif_image_get_plane_readonly(image, heif_channel_Y, &yPlaneStride); + + int cbPlaneStride; + const uint8_t* cbPlaneScan0 = heif_image_get_plane_readonly(image, heif_channel_Cb, &cbPlaneStride); + + int crPlaneStride; + const uint8_t* crPlaneScan0 = heif_image_get_plane_readonly(image, heif_channel_Cr, &crPlaneStride); + + ScopedBufferSuiteBuffer buffer(formatRecord->bufferProcs, formatRecord->rowBytes); + + formatRecord->data = buffer.Lock(); + + const int32 left = 0; + const int32 right = imageSize.h; + + YUVCoefficiants yuvCoefficiants{}; + + GetYUVCoefficiants(nclxProfile, yuvCoefficiants); + + const YUVLookupTables tables(nclxProfile, lumaBitsPerPixel, false, hasAlpha); + + int32 xChromaShift, yChromaShift; + + GetChromaShift(chroma, xChromaShift, yChromaShift); + + if (hasAlpha) + { + if (heif_image_get_bits_per_pixel_range(image, heif_channel_Alpha) != lumaBitsPerPixel) + { + throw std::runtime_error("The alpha channel bit depth does not match the main image channels."); + } + + int alphaStride; + const uint8_t* alphaScan0 = heif_image_get_plane_readonly(image, heif_channel_Alpha, &alphaStride); + const bool alphaPremultiplied = alphaState == AlphaState::Premultiplied; + + for (int32 y = 0; y < imageSize.v; y++) + { + const int32 uvJ = y >> yChromaShift; + const uint16_t* srcY = reinterpret_cast(yPlaneScan0 + (static_cast(y) * yPlaneStride)); + const uint16_t* srcCb = reinterpret_cast(cbPlaneScan0 + (static_cast(uvJ) * cbPlaneStride)); + const uint16_t* srcCr = reinterpret_cast(crPlaneScan0 + (static_cast(uvJ) * crPlaneStride)); + + const uint16_t* srcAlpha = reinterpret_cast(alphaScan0 + (static_cast(y) * alphaStride)); + float* dst = static_cast(formatRecord->data); + + DecodeYUV16RowToRGBA32(srcY, srcCb, srcCr, srcAlpha, alphaPremultiplied, + dst, imageSize.h, xChromaShift, yuvCoefficiants, tables, curveType); + + const int32 top = y; + const int32 bottom = y + 1; + + SetRect(formatRecord, top, left, bottom, right); + + OSErrException::ThrowIfError(formatRecord->advanceState()); + } + } + else + { + for (int32 y = 0; y < imageSize.v; y++) + { + const int32 uvJ = y >> yChromaShift; + const uint16_t* srcY = reinterpret_cast(yPlaneScan0 + (static_cast(y) * yPlaneStride)); + const uint16_t* srcCb = reinterpret_cast(cbPlaneScan0 + (static_cast(uvJ) * cbPlaneStride)); + const uint16_t* srcCr = reinterpret_cast(crPlaneScan0 + (static_cast(uvJ) * crPlaneStride)); + + float* dst = static_cast(formatRecord->data); + + DecodeYUV16RowToRGB32(srcY, srcCb, srcCr, dst, imageSize.h, xChromaShift, + yuvCoefficiants, tables, curveType); + + const int32 top = y; + const int32 bottom = y + 1; + + SetRect(formatRecord, top, left, bottom, right); + + OSErrException::ThrowIfError(formatRecord->advanceState()); + } + } + } + + ::std::vector BuildUnormToFloatLookupTable(int bitDepth) + { + const size_t count = static_cast(1) << bitDepth; + const float maxValue = static_cast(count - 1); + + ::std::vector table(count); + + for (size_t i = 0; i < count; i++) + { + table[i] = static_cast(i) / maxValue; + } + + return table; + } } void ReadHeifImageGrayEightBit( @@ -730,3 +851,260 @@ void ReadHeifImageRGBSixteenBit( } } } + +void ReadHeifImageGrayThirtyTwoBit( + const heif_image* image, + AlphaState alphaState, + const heif_color_profile_nclx* nclxProfile, + FormatRecordPtr formatRecord) +{ + if (nclxProfile == nullptr) + { + throw std::runtime_error("The nclxProfile is null."); + } + + const VPoint imageSize = GetImageSize(formatRecord); + const bool hasAlpha = alphaState != AlphaState::None; + + SetupFormatRecord(formatRecord, imageSize); + + const int lumaBitsPerPixel = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + + int grayStride; + const uint8_t* grayScan0 = heif_image_get_plane_readonly(image, heif_channel_Y, &grayStride); + + ScopedBufferSuiteBuffer buffer(formatRecord->bufferProcs, formatRecord->rowBytes); + + formatRecord->data = buffer.Lock(); + + const int32 left = 0; + const int32 right = imageSize.h; + + const YUVLookupTables tables(nclxProfile, lumaBitsPerPixel, true, hasAlpha); + ColorTransferCurveType curveType = ColorTransferCurveType::PQ; + + if (nclxProfile->transfer_characteristics != heif_transfer_characteristic_ITU_R_BT_2100_0_PQ) + { + throw std::runtime_error("Unsupported NCLX transfer characteristic."); + } + + if (hasAlpha) + { + if (heif_image_get_bits_per_pixel_range(image, heif_channel_Alpha) != lumaBitsPerPixel) + { + throw std::runtime_error("The alpha channel bit depth does not match the main image channels."); + } + + int alphaStride; + const uint8_t* alphaScan0 = heif_image_get_plane_readonly(image, heif_channel_Alpha, &alphaStride); + const bool alphaPremultiplied = alphaState == AlphaState::Premultiplied; + + for (int32 y = 0; y < imageSize.v; y++) + { + const uint16_t* srcGray = reinterpret_cast(grayScan0 + (static_cast(y) * grayStride)); + const uint16_t* srcAlpha = reinterpret_cast(alphaScan0 + (static_cast(y) * alphaStride)); + float* dst = static_cast(formatRecord->data); + + DecodeY16RowToGrayAlpha32(srcGray, srcAlpha, alphaPremultiplied, dst, imageSize.h, tables, curveType); + + const int32 top = y; + const int32 bottom = y + 1; + + SetRect(formatRecord, top, left, bottom, right); + + OSErrException::ThrowIfError(formatRecord->advanceState()); + } + } + else + { + for (int32 y = 0; y < imageSize.v; y++) + { + const uint16_t* srcGray = reinterpret_cast(grayScan0 + (static_cast(y) * grayStride)); + float* dst = static_cast(formatRecord->data); + + DecodeY16RowToGray32(srcGray, dst, imageSize.h, tables, curveType); + + const int32 top = y; + const int32 bottom = y + 1; + + SetRect(formatRecord, top, left, bottom, right); + + OSErrException::ThrowIfError(formatRecord->advanceState()); + } + } +} + +void ReadHeifImageRGBThirtyTwoBit( + const heif_image* image, + AlphaState alphaState, + const heif_color_profile_nclx* nclxProfile, + FormatRecordPtr formatRecord) +{ + if (nclxProfile == nullptr) + { + throw std::runtime_error("The nclxProfile is null."); + } + + ColorTransferCurveType curveType = ColorTransferCurveType::PQ; + + if (nclxProfile->transfer_characteristics != heif_transfer_characteristic_ITU_R_BT_2100_0_PQ) + { + throw std::runtime_error("Unsupported NCLX transfer characteristic."); + } + + const heif_colorspace colorspace = heif_image_get_colorspace(image); + + // The image color space can be either YCbCr or RGB. + if (colorspace == heif_colorspace_YCbCr) + { + ReadHeifImageYUVThirtyTwoBit(image, alphaState, nclxProfile, formatRecord, curveType); + return; + } + else if (colorspace != heif_colorspace_RGB) + { + throw std::runtime_error("Unsupported image color space, expected RGB."); + } + + const VPoint imageSize = GetImageSize(formatRecord); + const bool hasAlpha = alphaState != AlphaState::None; + + const int redBitsPerPixel = heif_image_get_bits_per_pixel_range(image, heif_channel_R); + + if (heif_image_get_bits_per_pixel_range(image, heif_channel_G) != redBitsPerPixel || + heif_image_get_bits_per_pixel_range(image, heif_channel_B) != redBitsPerPixel) + { + throw std::runtime_error("The color channel bit depths do not match."); + } + + SetupFormatRecord(formatRecord, imageSize); + + int rPlaneStride; + const uint8_t* rPlaneScan0 = heif_image_get_plane_readonly(image, heif_channel_R, &rPlaneStride); + + int gPlaneStride; + const uint8_t* gPlaneScan0 = heif_image_get_plane_readonly(image, heif_channel_G, &gPlaneStride); + + int bPlaneStride; + const uint8_t* bPlaneScan0 = heif_image_get_plane_readonly(image, heif_channel_B, &bPlaneStride); + + ScopedBufferSuiteBuffer buffer(formatRecord->bufferProcs, formatRecord->rowBytes); + + formatRecord->data = buffer.Lock(); + + const int32 left = 0; + const int32 right = imageSize.h; + + const ::std::vector unormToFloatTable = BuildUnormToFloatLookupTable(redBitsPerPixel); + + if (hasAlpha) + { + if (heif_image_get_bits_per_pixel_range(image, heif_channel_Alpha) != redBitsPerPixel) + { + throw std::runtime_error("The alpha channel bit depth does not match the main image channels."); + } + + int alphaStride; + const uint8_t* alphaScan0 = heif_image_get_plane_readonly(image, heif_channel_Alpha, &alphaStride); + const bool alphaPremultiplied = alphaState == AlphaState::Premultiplied; + + const uint16_t rgbMaxValue = (1 << redBitsPerPixel) - 1; + + for (int32 y = 0; y < imageSize.v; y++) + { + const uint16_t* srcR = reinterpret_cast(rPlaneScan0 + (static_cast(y) * rPlaneStride)); + const uint16_t* srcG = reinterpret_cast(gPlaneScan0 + (static_cast(y) * gPlaneStride)); + const uint16_t* srcB = reinterpret_cast(bPlaneScan0 + (static_cast(y) * bPlaneStride)); + const uint16_t* srcAlpha = reinterpret_cast(alphaScan0 + (static_cast(y) * alphaStride)); + + float* dst = static_cast(formatRecord->data); + + for (int32 x = 0; x < imageSize.h; x++) + { + uint16_t unormR = *srcR; + uint16_t unormG = *srcG; + uint16_t unormB = *srcB; + const uint16_t unormA = *srcAlpha; + + if (alphaPremultiplied) + { + if (unormA < rgbMaxValue) + { + if (unormA == 0) + { + unormR = 0; + unormG = 0; + unormB = 0; + } + else + { + unormR = UnpremultiplyColor(unormR, unormA, rgbMaxValue); + unormG = UnpremultiplyColor(unormG, unormA, rgbMaxValue); + unormB = UnpremultiplyColor(unormB, unormA, rgbMaxValue); + } + } + } + + const float r = unormToFloatTable[unormR]; + const float g = unormToFloatTable[unormG]; + const float b = unormToFloatTable[unormB]; + const float a = unormToFloatTable[unormA]; + + dst[0] = TransferCurveToLinear(r, curveType); + dst[1] = TransferCurveToLinear(g, curveType); + dst[2] = TransferCurveToLinear(b, curveType); + dst[3] = a; + + srcR++; + srcG++; + srcB++; + srcAlpha++; + dst += 4; + } + + const int32 top = y; + const int32 bottom = y + 1; + + SetRect(formatRecord, top, left, bottom, right); + + OSErrException::ThrowIfError(formatRecord->advanceState()); + } + } + else + { + for (int32 y = 0; y < imageSize.v; y++) + { + const uint16_t* srcR = reinterpret_cast(rPlaneScan0 + (static_cast(y) * rPlaneStride)); + const uint16_t* srcG = reinterpret_cast(gPlaneScan0 + (static_cast(y) * gPlaneStride)); + const uint16_t* srcB = reinterpret_cast(bPlaneScan0 + (static_cast(y) * bPlaneStride)); + + float* dst = static_cast(formatRecord->data); + + for (int32 x = 0; x < imageSize.h; x++) + { + const uint16_t unormR = *srcR; + const uint16_t unormG = *srcG; + const uint16_t unormB = *srcB; + + const float r = unormToFloatTable[unormR]; + const float g = unormToFloatTable[unormG]; + const float b = unormToFloatTable[unormB]; + + dst[0] = TransferCurveToLinear(r, curveType); + dst[1] = TransferCurveToLinear(g, curveType); + dst[2] = TransferCurveToLinear(b, curveType); + + srcR++; + srcG++; + srcB++; + dst += 3; + } + + const int32 top = y; + const int32 bottom = y + 1; + + SetRect(formatRecord, top, left, bottom, right); + + OSErrException::ThrowIfError(formatRecord->advanceState()); + } + } +} diff --git a/src/common/ReadHeifImage.h b/src/common/ReadHeifImage.h index 89e22e7..06b5de1 100644 --- a/src/common/ReadHeifImage.h +++ b/src/common/ReadHeifImage.h @@ -48,4 +48,16 @@ void ReadHeifImageRGBSixteenBit( const heif_color_profile_nclx* nclxProfile, FormatRecordPtr formatRecord); +void ReadHeifImageGrayThirtyTwoBit( + const heif_image* image, + AlphaState alphaState, + const heif_color_profile_nclx* nclxProfile, + FormatRecordPtr formatRecord); + +void ReadHeifImageRGBThirtyTwoBit( + const heif_image* image, + AlphaState alphaState, + const heif_color_profile_nclx* nclxProfile, + FormatRecordPtr formatRecord); + #endif // !READHEIFIMAGE_H diff --git a/src/common/Utilities.cpp b/src/common/Utilities.cpp index f030e7e..95b8a70 100644 --- a/src/common/Utilities.cpp +++ b/src/common/Utilities.cpp @@ -421,9 +421,11 @@ bool HasAlphaChannel(const FormatRecordPtr formatRecord) { case plugInModeGrayScale: case plugInModeGray16: + case plugInModeGray32: return formatRecord->planes == 2; case plugInModeRGBColor: case plugInModeRGB48: + case plugInModeRGB96: return formatRecord->planes == 4; default: return false; @@ -436,6 +438,7 @@ bool IsMonochromeImage(const FormatRecordPtr formatRecord) { case plugInModeGrayScale: case plugInModeGray16: + case plugInModeGray32: return true; default: return false; diff --git a/src/common/YUVDecode.h b/src/common/YUVDecode.h index f9c15fc..70f8641 100644 --- a/src/common/YUVDecode.h +++ b/src/common/YUVDecode.h @@ -21,6 +21,7 @@ #ifndef YUVDECODE_H #define YUVDECODE_H +#include "ColorTransfer.h" #include "YUVCoefficiants.h" #include "YUVLookupTables.h" @@ -46,12 +47,28 @@ void DecodeY16RowToGray16( void DecodeY16RowToGrayAlpha16( const uint16_t* yPlane, - const uint16_t* alphaPlane, + const uint16_t* alphaPlane, bool alphaPremultiplied, uint16_t* grayaRow, int32 rowWidth, const YUVLookupTables& tables); +void DecodeY16RowToGray32( + const uint16_t* yPlane, + float* grayRow, + int32 rowWidth, + const YUVLookupTables& tables, + ColorTransferCurveType curveType); + +void DecodeY16RowToGrayAlpha32( + const uint16_t* yPlane, + const uint16_t* alphaPlane, + bool alphaPremultiplied, + float* grayaRow, + int32 rowWidth, + const YUVLookupTables& tables, + ColorTransferCurveType curveType); + void DecodeYUV8RowToRGB8( const uint8_t* yPlane, const uint8_t* uPlane, @@ -96,4 +113,28 @@ void DecodeYUV16RowToRGBA16( const YUVCoefficiants& yuvCoefficiants, const YUVLookupTables& tables); +void DecodeYUV16RowToRGB32( + const uint16_t* yPlane, + const uint16_t* uPlane, + const uint16_t* vPlane, + float* rgbRow, + int32 rowWidth, + int32 xChromaShift, + const YUVCoefficiants& yuvCoefficiants, + const YUVLookupTables& tables, + ColorTransferCurveType curveType); + +void DecodeYUV16RowToRGBA32( + const uint16_t* yPlane, + const uint16_t* uPlane, + const uint16_t* vPlane, + const uint16_t* alphaPlane, + bool alphaPremultiplied, + float* rgbaRow, + int32 rowWidth, + int32 xChromaShift, + const YUVCoefficiants& yuvCoefficiants, + const YUVLookupTables& tables, + ColorTransferCurveType curveType); + #endif // !YUVDECODE_H diff --git a/src/common/YuvDecode.cpp b/src/common/YuvDecode.cpp index 6e6cc61..adde480 100644 --- a/src/common/YuvDecode.cpp +++ b/src/common/YuvDecode.cpp @@ -45,6 +45,7 @@ */ #include "YUVDecode.h" +#include "ColorTransfer.h" #include "PremultipliedAlpha.h" #include #include @@ -190,6 +191,76 @@ void DecodeY16RowToGrayAlpha16( } } +void DecodeY16RowToGray32( + const uint16_t* yPlane, + float* grayRow, + int32 rowWidth, + const YUVLookupTables& tables, + ColorTransferCurveType curveType) +{ + float* dstPtr = grayRow; + + const uint16_t yuvMaxChannel = static_cast(tables.yuvMaxChannel); + + for (int32 x = 0; x < rowWidth; ++x) + { + // Unpack Y into unorm + const uint16_t unormY = std::min(yPlane[x], yuvMaxChannel); + + // Convert unorm to float + const float Y = tables.unormFloatTableY[unormY]; + + dstPtr[0] = TransferCurveToLinear(Y, curveType); + + dstPtr++; + } +} + +void DecodeY16RowToGrayAlpha32( + const uint16_t* yPlane, + const uint16_t* alphaPlane, + bool alphaPremultiplied, + float* grayaRow, + int32 rowWidth, + const YUVLookupTables& tables, + ColorTransferCurveType curveType) +{ + float* dstPtr = grayaRow; + + const uint16_t yuvMaxChannel = static_cast(tables.yuvMaxChannel); + + for (int32 x = 0; x < rowWidth; ++x) + { + // Unpack Y into unorm + uint16_t unormY = std::min(yPlane[x], yuvMaxChannel); + const uint16_t unormA = std::min(alphaPlane[x], yuvMaxChannel); + + if (alphaPremultiplied) + { + if (unormA < tables.yuvMaxChannel) + { + if (unormA == 0) + { + unormY = 0; + } + else + { + unormY = UnpremultiplyColor(unormY, unormA, yuvMaxChannel); + } + } + } + + // Convert unorm to float + const float Y = tables.unormFloatTableY[unormY]; + const float A = tables.unormFloatTableAlpha[unormA]; + + dstPtr[0] = TransferCurveToLinear(Y, curveType); + dstPtr[1] = A; + + dstPtr += 2; + } +} + void DecodeYUV8RowToRGB8( const uint8_t* yPlane, const uint8_t* uPlane, @@ -429,3 +500,123 @@ void DecodeYUV16RowToRGBA16( dstPtr += 4; } } + +void DecodeYUV16RowToRGB32( + const uint16_t* yPlane, + const uint16_t* uPlane, + const uint16_t* vPlane, + float* rgbRow, + int32 rowWidth, + int32 xChromaShift, + const YUVCoefficiants& yuvCoefficiants, + const YUVLookupTables& tables, + ColorTransferCurveType curveType) +{ + const float kr = yuvCoefficiants.kr; + const float kg = yuvCoefficiants.kg; + const float kb = yuvCoefficiants.kb; + + const uint16_t yuvMaxChannel = static_cast(tables.yuvMaxChannel); + + float* dstPtr = rgbRow; + + for (int32 x = 0; x < rowWidth; ++x) + { + // Unpack YUV into unorm + const int32_t uvI = x >> xChromaShift; + const uint16_t unormY = std::min(yPlane[x], yuvMaxChannel); + const uint16_t unormU = std::min(uPlane[uvI], yuvMaxChannel); + const uint16_t unormV = std::min(vPlane[uvI], yuvMaxChannel); + + // Convert unorm to float + const float Y = tables.unormFloatTableY[unormY]; + const float Cb = tables.unormFloatTableUV[unormU]; + const float Cr = tables.unormFloatTableUV[unormV]; + + float R = Y + (2 * (1 - kr)) * Cr; + float B = Y + (2 * (1 - kb)) * Cb; + float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); + + R = std::clamp(R, 0.0f, 1.0f); + G = std::clamp(G, 0.0f, 1.0f); + B = std::clamp(B, 0.0f, 1.0f); + + dstPtr[0] = TransferCurveToLinear(R, curveType); + dstPtr[1] = TransferCurveToLinear(G, curveType); + dstPtr[2] = TransferCurveToLinear(B, curveType); + + dstPtr += 3; + } +} + +void DecodeYUV16RowToRGBA32( + const uint16_t* yPlane, + const uint16_t* uPlane, + const uint16_t* vPlane, + const uint16_t* alphaPlane, + bool alphaPremultiplied, + float* rgbaRow, + int32 rowWidth, + int32 xChromaShift, + const YUVCoefficiants& yuvCoefficiants, + const YUVLookupTables& tables, + ColorTransferCurveType curveType) +{ + const float kr = yuvCoefficiants.kr; + const float kg = yuvCoefficiants.kg; + const float kb = yuvCoefficiants.kb; + + const uint16_t yuvMaxChannel = static_cast(tables.yuvMaxChannel); + + float* dstPtr = rgbaRow; + + for (int32 x = 0; x < rowWidth; ++x) + { + // Unpack YUV into unorm + const int32_t uvI = x >> xChromaShift; + const uint16_t unormY = std::min(yPlane[x], yuvMaxChannel); + const uint16_t unormU = std::min(uPlane[uvI], yuvMaxChannel); + const uint16_t unormV = std::min(vPlane[uvI], yuvMaxChannel); + const uint16_t unormA = std::min(alphaPlane[x], yuvMaxChannel); + + // Convert unorm to float + const float Y = tables.unormFloatTableY[unormY]; + const float Cb = tables.unormFloatTableUV[unormU]; + const float Cr = tables.unormFloatTableUV[unormV]; + + float R = Y + (2 * (1 - kr)) * Cr; + float B = Y + (2 * (1 - kb)) * Cb; + float G = Y - ((2 * ((kr * (1 - kr) * Cr) + (kb * (1 - kb) * Cb))) / kg); + + R = std::clamp(R, 0.0f, 1.0f); + G = std::clamp(G, 0.0f, 1.0f); + B = std::clamp(B, 0.0f, 1.0f); + const float A = tables.unormFloatTableAlpha[unormA]; + + if (alphaPremultiplied) + { + if (unormA < tables.yuvMaxChannel) + { + if (unormA == 0) + { + R = 0; + G = 0; + B = 0; + } + else + { + R = UnpremultiplyColor(R, A, 1.0f); + G = UnpremultiplyColor(G, A, 1.0f); + B = UnpremultiplyColor(B, A, 1.0f); + } + } + } + + dstPtr[0] = TransferCurveToLinear(R, curveType); + dstPtr[1] = TransferCurveToLinear(G, curveType); + dstPtr[2] = TransferCurveToLinear(B, curveType); + dstPtr[3] = A; + + dstPtr += 4; + } +} diff --git a/vs/AvifFormat.vcxproj b/vs/AvifFormat.vcxproj index 928550c..d038a3f 100644 --- a/vs/AvifFormat.vcxproj +++ b/vs/AvifFormat.vcxproj @@ -332,6 +332,7 @@ + @@ -358,6 +359,7 @@ + diff --git a/vs/AvifFormat.vcxproj.filters b/vs/AvifFormat.vcxproj.filters index 7a619b3..00b9243 100644 --- a/vs/AvifFormat.vcxproj.filters +++ b/vs/AvifFormat.vcxproj.filters @@ -93,6 +93,9 @@ Header Files + + Header Files + @@ -170,6 +173,9 @@ Source Files + + Source Files +