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 +