目前Openxml存在颜色变化属性如下:
参数 | 说明 |
---|---|
Hue | 色调(色相) |
HueModulate | 色调调制,百分比 |
HueOffset | 色调偏移量,角度值 |
Saturation | 饱和度 |
SaturationModulation | 饱和度调制,百分比 |
SaturationOffset | 饱和度偏移量 |
Luminance | 亮度 |
LuminanceModulation | 亮度调制,百分比 |
LuminanceOffset | 亮度偏移量 |
Alpha | Alpha |
AlphaModulation | Alpha 调制,百分比 |
AlphaOffset | Alpha 偏移量 |
Red | 红色 |
RedModulation | 红色调制,百分比 |
RedOffset | 红色偏移量 |
Blue | 蓝色 |
BlueModification | 蓝色调制 |
BlueOffse | 蓝色偏移量,百分比 |
Green | 绿色 |
GreenModification | 绿色调制,百分比 |
GreenOffset | 绿色偏移量 |
Complement | 补充 |
Gamma | 伽玛 |
Gray | 灰色 |
Inverse | 反函数 |
Inverse Gamma | 反函数伽玛 |
Shade | 底纹,百分比 |
Tint | 底纹,百分比 |
其中有关RGB和Hsl的相互转换的公式如下:
RGB转Hsl公式如下:
Hsl转RGB公式如下:
其中涉及到有Hsl计算的有以下九个属性:
那么我们开始写代码:
定义RGB的类:
/// <summary> /// 用 A R G B 表示的颜色 /// </summary> public class ARgbColor { /// <summary> /// 创建 A R G B 颜色 /// </summary> public ARgbColor() { } /// <summary> /// 创建 A R G B 颜色 /// </summary> /// <param name="a"></param> /// <param name="r"></param> /// <param name="g"></param> /// <param name="b"></param> public ARgbColor(byte a, byte r, byte g, byte b) { A = a; R = r; G = g; B = b; } /// <summary> /// 表示透明色 /// </summary> public byte A { set; get; } /// <summary> /// 表示红色 /// </summary> public byte R { set; get; } /// <summary> /// 表示绿色 /// </summary> public byte G { set; get; } /// <summary> /// 表示蓝色 /// </summary> public byte B { set; get; } }
定义颜色变化相关类ColorTransform
,并且定义RGB和Hsl的相互转换逻辑方法:
/// <summary> /// 处理颜色之间的变换,调整,格式转换 /// </summary> public static class ColorTransform { /// <summary> /// 将<see cref="Color" />的数据转换为Hsl /// </summary> /// <param name="color"></param> /// <returns></returns> public static (Degree hue, Percentage sat, Percentage lum, byte alpha) ColorToHsl(Color color) { var max = System.Math.Max(color.R, System.Math.Max(color.G, color.B)); var min = System.Math.Min(color.R, System.Math.Min(color.G, color.B)); var delta = max - min; var l = Percentage.FromDouble((max + min) / 2.0 / 255.0); var h = Degree.FromDouble(0); var s = Percentage.Zero; if (delta > 0) { s = l < Percentage.FromDouble(0.5) ? Percentage.FromDouble((max - min) * 1.0 / (max + min)) : Percentage.FromDouble((max - min) * 1.0 / (2 * 255 - max - min)); if (max == color.R) { h = Degree.FromDouble((0 + (color.G - color.B) * 1.0 / delta) * 60); } else if (max == color.G) { h = Degree.FromDouble((2 + (color.B - color.R) * 1.0 / delta) * 60); } else { h = Degree.FromDouble((4 + (color.R - color.G) * 1.0 / delta) * 60); } } return (h, s, l, color.A); } } /// <summary> /// 将Hsl的数据转换为<see cref="Color" /> /// </summary> /// <param name="hue">色相</param> /// <param name="saturation">饱和度</param> /// <param name="lightness">亮度</param> /// <param name="a">透明度</param> /// <returns></returns> public static Color HslToColor(Degree hue, Percentage saturation, Percentage lightness, byte a = 0xFF) { var color = new Color { A = a }; var hueValue = hue.DoubleValue; var saturationValue = saturation.DoubleValue; var lightnessValue = lightness.DoubleValue; var c = (1 - System.Math.Abs(2 * lightnessValue - 1)) * saturationValue; var x = c * (1 - System.Math.Abs((hueValue / 60) % 2 - 1)); var m = lightnessValue - c / 2; var r = 0d; var g = 0d; var b = 0d; if (hueValue is >= 0 and < 60) { r = c; g = x; b = 0; } if (hueValue is >= 60 and < 120) { r = x; g = c; b = 0; } if (hueValue is >= 120 and < 180) { r = 0; g = c; b = x; } if (hueValue is >= 180 and < 240) { r = 0; g = x; b = c; } if (hueValue is >= 240 and < 300) { r = x; g = 0; b = c; } if (hueValue is >= 300 and < 360) { r = c; g = 0; b = x; } color.R = (byte) ((r + m) * 255); color.G = (byte) ((g + m) * 255); color.B = (byte) ((b + m) * 255); return color; }
然后我们来写真正处理Openxml的Hsl相关属性逻辑:
/// <summary> /// 将<see cref="RgbColorModelHex" />转换为<see cref="Color" /> /// </summary> /// <param name="color"></param> /// <returns></returns> public static Color? ToColor(this RgbColorModelHex color) { if (color.Val is not null) { if (uint.TryParse(color.Val.Value, NumberStyles.HexNumber, null, out var result)) { var solidColor = result.HexToColor(); var modifiedColor = ColorTransform.AppendColorModify(solidColor, color.ChildElements); return modifiedColor; } } return null; } private static Color HexToColor(this uint rgb) { var color = new Color(); const int maxByte = 0xff; color.B = (byte) (rgb & maxByte); color.G = (byte) ((rgb >> 8) & maxByte); color.R = (byte) ((rgb >> 16) & maxByte); color.A = 0xFF; return color; } /// <summary> /// 给颜色叠加转换 /// </summary> /// <param name="color"></param> /// <param name="list"></param> /// <returns></returns> public static Color AppendColorModify(ARgbColor color, OpenXmlElementList list) { var updatedColor = color; foreach (var element in list) { if (element is Hue hue) { updatedColor = HandleHue(updatedColor, hue, null, null); continue; } if (element is HueModulation hueModulation) { updatedColor = HandleHue(updatedColor, null, hueModulation, null); continue; } if (element is HueOffset hueOffset) { updatedColor = HandleHue(updatedColor, null, null, hueOffset); continue; } if (element is Saturation saturation) { updatedColor = HandleSaturation(updatedColor, saturation, null, null); continue; } if (element is SaturationModulation saturationModulation) { updatedColor = HandleSaturation(updatedColor, null, saturationModulation, null); continue; } if (element is SaturationOffset saturationOffset) { updatedColor = HandleSaturation(updatedColor, null, null, saturationOffset); continue; } if (element is Luminance luminance) { updatedColor = HandleLuminance(updatedColor, luminance, null, null); continue; } if (element is LuminanceModulation luminanceModulation) { updatedColor = HandleLuminance(updatedColor, null, luminanceModulation, null); continue; } if (element is LuminanceOffset luminanceOffset) { updatedColor = HandleLuminance(updatedColor, null, null, luminanceOffset); continue; } } private static Color HandleHue(Color color, Hue? hueElement, HueModulation? hueModElement, HueOffset? hueOffsetElement) { if (hueElement is null && hueModElement is null && hueOffsetElement is null) { return color; } var updatedColor = HandleHslCore(color, hueElement: hueElement, hueModElement: hueModElement, hueOffsetElement: hueOffsetElement); return updatedColor; } private static Color HandleSaturation(Color color, Saturation? satElement, SaturationModulation? satModElement, SaturationOffset? satOffsetElement) { if (satElement is null && satModElement is null && satOffsetElement is null) { return color; } var updatedColor = HandleHslCore(color, satElement: satElement, satModElement: satModElement, satOffsetElement: satOffsetElement); return updatedColor; } private static Color HandleLuminance(Color color, Luminance? lumElement, LuminanceModulation? lumModElement, LuminanceOffset? lumOffsetElement) { if (lumElement is null && lumModElement is null && lumOffsetElement is null) { return color; } var updatedColor = HandleHslCore(color, lumElement: lumElement, lumModElement: lumModElement, lumOffsetElement: lumOffsetElement); return updatedColor; } private static Color HandleHslCore(Color color, Hue? hueElement = null, HueModulation? hueModElement = null, HueOffset? hueOffsetElement = null, Saturation? satElement = null, SaturationModulation? satModElement = null, SaturationOffset? satOffsetElement = null, Luminance? lumElement = null, LuminanceModulation? lumModElement = null, LuminanceOffset? lumOffsetElement = null) { if (hueElement is null && hueModElement is null && hueOffsetElement is null && satElement is null && satModElement is null && satOffsetElement is null && lumElement is null && lumModElement is null && lumOffsetElement is null) { return color; } var (hue, sat, lum, alpha) = ColorToHsl(color); var hueElementVal = hueElement?.Val; var hueValue = hueElementVal is not null ? new Angle(hueElementVal).ToDegreeValue() : hue.DoubleValue; var satElementVal = satElement?.Val; var satValue = satElementVal is not null ? new Percentage(satElementVal).DoubleValue : sat.DoubleValue; var lumElementVal = lumElement?.Val; var lumValue = lumElementVal is not null ? new Percentage(lumElementVal).DoubleValue : lum.DoubleValue; var hueModElementVal = hueModElement?.Val; var hueModValue = hueModElementVal is not null && hueModElementVal.HasValue ? new Percentage(hueModElementVal) : Percentage.FromDouble(1); var satModElementVal = satModElement?.Val; var satModValue = satModElementVal is not null && satModElementVal.HasValue ? new Percentage(satModElementVal) : Percentage.FromDouble(1); var lumModElementVal = lumModElement?.Val; var lumModValue = lumModElementVal is not null && lumModElementVal.HasValue ? new Percentage(lumModElementVal) : Percentage.FromDouble(1); var hueOffsetVal = hueOffsetElement?.Val; var hueOffset = hueOffsetVal is not null && hueOffsetVal.HasValue ? new Angle(hueOffsetVal).ToDegreeValue() : new Angle(0).ToDegreeValue(); var saturationOffsetVal = satOffsetElement?.Val; var saturationOffset = saturationOffsetVal is not null && saturationOffsetVal.HasValue ? new Percentage(saturationOffsetVal) : Percentage.Zero; var lumOffsetElementVal = lumOffsetElement?.Val; var lumOffset = lumOffsetElementVal is not null && lumOffsetElementVal.HasValue ? new Percentage(lumOffsetElementVal) : Percentage.Zero; var hueResult = hueValue * hueModValue.DoubleValue + hueOffset; hue = Degree.FromDouble(hueResult); var satResult = satValue * satModValue.DoubleValue + saturationOffset.DoubleValue; sat = Percentage.FromDouble(satResult); sat = sat > Percentage.FromDouble(1) ? Percentage.FromDouble(1) : sat; sat = sat < Percentage.Zero ? Percentage.Zero : sat; var lumResult = lumValue * lumModValue.DoubleValue + lumOffset.DoubleValue; lum = Percentage.FromDouble(lumResult); lum = lum > Percentage.FromDouble(1) ? Percentage.FromDouble(1) : lum; lum = lum < Percentage.Zero ? Percentage.Zero : lum; return HslToColor(hue, sat, lum, alpha); }
涉及到RGB相关的Openxml属性如下:
以下为处理透明度的逻辑代码:
private static Color HandleAlphaModify(Color color, Alpha? alphaElement, AlphaModulation? alphaModulation, AlphaOffset? alphaOffset) { if (alphaElement is null && alphaModulation is null && alphaOffset is null) { return color; } var alphaValue = alphaElement?.Val; var modulationVal = alphaModulation?.Val; var offsetVal = alphaOffset?.Val; var alpha = alphaValue is not null && alphaValue.HasValue ? new Percentage(alphaValue) : Percentage.FromDouble(1); var mod = modulationVal is not null && modulationVal.HasValue ? new Percentage(modulationVal) : Percentage.FromDouble(1); var off = offsetVal is not null && offsetVal.HasValue ? new Percentage(offsetVal) : Percentage.Zero; var alphaResult = alpha.DoubleValue * mod.DoubleValue + off.DoubleValue; color.A = (byte) (color.A * alphaResult); return color; }
以下为处理RGB的红色、蓝色、绿色的逻辑代码:
private static Color HandleRgb(Color color, Red? redElement, Green? greenElement, Blue? blueElement) { if (redElement is null && greenElement is null && blueElement is null) { return color; } var updatedColor = HandleRgbCore(color, redElement: redElement, greenElement: greenElement, blueElement: blueElement); return updatedColor; } private static Color HandleRgbModulation(Color color, RedModulation? redModulationElement, GreenModulation? greenModulationElement, BlueModulation? blueModulationElement) { if (redModulationElement is null && greenModulationElement is null && blueModulationElement is null) { return color; } var updatedColor = HandleRgbCore(color, redModulationElement: redModulationElement, greenModulationElement: greenModulationElement, blueModulationElement: blueModulationElement); return updatedColor; } private static Color HandleRgbOffset(Color color, RedOffset? redOffsetElement, GreenOffset? greenOffsetElement, BlueOffset? blueOffsetElement) { if (redOffsetElement is null && blueOffsetElement is null && greenOffsetElement is null) { return color; } var updatedColor = HandleRgbCore(color, redOffsetElement: redOffsetElement, greenOffsetElement: greenOffsetElement, blueOffsetElement: blueOffsetElement); return updatedColor; } private static Color HandleRgbCore(Color color, Red? redElement = null, Green? greenElement = null, Blue? blueElement = null, RedModulation? redModulationElement = null, GreenModulation? greenModulationElement = null, BlueModulation? blueModulationElement = null, RedOffset? redOffsetElement = null, GreenOffset? greenOffsetElement = null, BlueOffset? blueOffsetElement = null) { if (redElement is null && greenElement is null && blueElement is null && redModulationElement is null && greenModulationElement is null && blueModulationElement is null && redOffsetElement is null && greenOffsetElement is null && blueOffsetElement is null) { return color; } var updatedColor = color; var redModulationValue = redModulationElement?.Val; var redMod = redModulationValue is not null ? new Percentage(redModulationValue) : Percentage.FromDouble(1); var greenModulationValue = greenModulationElement?.Val; var greenMod = greenModulationValue is not null ? new Percentage(greenModulationValue) : Percentage.FromDouble(1); var blueModulationValue = blueModulationElement?.Val; var blueMod = blueModulationValue is not null ? new Percentage(blueModulationValue) : Percentage.FromDouble(1); var redOffsetValue = redOffsetElement?.Val; var redOffset = redOffsetValue is not null ? new Percentage(redOffsetValue) : Percentage.FromDouble(0); var greenOffsetValue = greenOffsetElement?.Val; var greenOffset = greenOffsetValue is not null ? new Percentage(greenOffsetValue) : Percentage.FromDouble(0); var blueOffsetValue = blueOffsetElement?.Val; var blueOffset = blueOffsetValue is not null ? new Percentage(blueOffsetValue) : Percentage.FromDouble(0); var linearR = SRgbToLinearRgb(updatedColor.R / 255.0); var linearG = SRgbToLinearRgb(updatedColor.G / 255.0); var linearB = SRgbToLinearRgb(updatedColor.B / 255.0); var redValue = redElement?.Val; var red = redValue is not null ? new Percentage(redValue).DoubleValue : linearR; var greenValue = greenElement?.Val; var green = greenValue is not null ? new Percentage(greenValue).DoubleValue : linearG; var blueValue = blueElement?.Val; var blue = blueValue is not null ? new Percentage(blueValue).DoubleValue : linearB; var redResult = red * redMod.DoubleValue + redOffset.DoubleValue; var greenResult = green * greenMod.DoubleValue + greenOffset.DoubleValue; var blueResult = blue * blueMod.DoubleValue + blueOffset.DoubleValue; var r = redResult < 0 ? 0 : redResult > 1 ? 1 : redResult; var g = greenResult < 0 ? 0 : greenResult > 1 ? 1 : greenResult; var b = blueResult < 0 ? 0 : blueResult > 1 ? 1 : blueResult; updatedColor.R = (byte) System.Math.Round(255 * LinearRgbToSRgb(r)); updatedColor.G = (byte) System.Math.Round(255 * LinearRgbToSRgb(g)); updatedColor.B = (byte) System.Math.Round(255 * LinearRgbToSRgb(b)); return updatedColor; } /// <summary> /// https://en.wikipedia.org/wiki/SRGB#The_forward_transformation_.28CIE_xyY_or_CIE_XYZ_to_sRGB.29 /// </summary> /// <param name="sRgb"></param> /// <returns></returns> private static double SRgbToLinearRgb(double sRgb) { if (sRgb <= 0.04045) return sRgb / 12.92; return System.Math.Pow((sRgb + 0.055) / 1.055, 2.4); }
以下为处理RGB的反函数的逻辑代码:
private static Color HandleInverse(Color color, Inverse? inverseElement) { var updatedColor = color; if (inverseElement != null) { var linearR = SRgbToLinearRgb(updatedColor.R / 255.0); var linearG = SRgbToLinearRgb(updatedColor.G / 255.0); var linearB = SRgbToLinearRgb(updatedColor.B / 255.0); var r = System.Math.Abs(1.0 - linearR); var g = System.Math.Abs(1.0 - linearG); var b = System.Math.Abs(1.0 - linearB); updatedColor.R = (byte) System.Math.Round(255 * LinearRgbToSRgb(r)); updatedColor.G = (byte) System.Math.Round(255 * LinearRgbToSRgb(g)); updatedColor.B = (byte) System.Math.Round(255 * LinearRgbToSRgb(b)); } return updatedColor; }
以下为处理RGB的补码的逻辑代码:
private static Color HandleComplement(Color color, Complement? complementElement) { var updatedColor = color; if (complementElement != null) { var r = updatedColor.B; var g = updatedColor.R + updatedColor.B - updatedColor.G; var b = updatedColor.R; updatedColor.R = r; updatedColor.G = (byte) g; updatedColor.B = b; } return updatedColor; }
实际上就是显示器的非线性特性让亮度在我们眼中看起来更好, 但是在渲染时反而会因此导致问题. 我们的渲染计算都是在伽马值为 1 的理想线性空间进行的,而显示器的非线性则是伽马值为 2.2计算的即为输入值的pow 2.2,伽马校正的思路就是在颜色被输送到显示器之前, 我们先对其进行 pow 1/2.2 的逆运算以抵消显示器的作用
因此计算伽玛校正的逻辑代码如下:
/// <summary> /// 对于sRGB的伽玛校正,也就是 1/2.2的幂运算 /// </summary> /// <param name="color"></param> /// <param name="gammaElement"></param> /// <returns></returns> private static Color HandleGamma(Color color, Gamma? gammaElement) { var updatedColor = color; if (gammaElement != null) { var r = System.Math.Pow(updatedColor.R / 255.0, 1 / 2.2); var g = System.Math.Pow(updatedColor.G / 255.0, 1 / 2.2); var b = System.Math.Pow(updatedColor.B / 255.0, 1 / 2.2); updatedColor.R = (byte) System.Math.Round(255 * r); updatedColor.G = (byte) System.Math.Round(255 * g); updatedColor.B = (byte) System.Math.Round(255 * b); } return updatedColor; }
而对于反伽玛校正,则其指数为2.2,代码如下:
/// <summary> /// 对于sRGB的反伽玛校正,也就是2.2的幂运算 /// </summary> /// <param name="color"></param> /// <param name="inverseGammaElement"></param> /// <returns></returns> private static Color HandleInverseGamma(Color color, InverseGamma? inverseGammaElement) { var updatedColor = color; if (inverseGammaElement != null) { var r = System.Math.Pow(updatedColor.R / 255.0, 2.2); var g = System.Math.Pow(updatedColor.G / 255.0, 2.2); var b = System.Math.Pow(updatedColor.B / 255.0, 2.2); updatedColor.R = (byte) System.Math.Round(255 * r); updatedColor.G = (byte) System.Math.Round(255 * g); updatedColor.B = (byte) System.Math.Round(255 * b); } return updatedColor; }
不同的RGB空间,灰阶的计算公式有所不同,常见的几种RGB空间的计算灰阶的公式如下:
//简化 sRGB IEC61966-2.1 [gamma=2.20] Gray = (R^2.2 * 0.2126 + G^2.2 * 0.7152 + B^2.2 * 0.0722)^(1/2.2) //Adobe RGB (1998) [gamma=2.20] Gray = (R^2.2 * 0.2973 + G^2.2 * 0.6274 + B^2.2 * 0.0753)^(1/2.2) //Apple RGB [gamma=1.80] Gray = (R^1.8 * 0.2446 + G^1.8 * 0.6720 + B^1.8 * 0.0833)^(1/1.8) //ColorMatch RGB [gamma=1.8] Gray = (R^1.8 * 0.2750 + G^1.8 * 0.6581 + B^1.8 * 0.0670)^(1/1.8) //简化 KODAK DC Series Digital Camera [gamma=2.2] Gray = (R^2.2 * 0.2229 + G^2.2 * 0.7175 + B^2.2 * 0.0595)^(1/2.2)
而我们选择了灰度系数2.2,即伽马值为2.2的sRGB的计算公式,那么逻辑代码如下:
/// <summary> /// 对于sRGB的灰阶计算 /// </summary> /// <param name="color"></param> /// <param name="grayElement"></param> /// <returns></returns> /// sRGB IEC61966-2.1 [gamma=2.20]:sRGB计算灰阶:Gray = (R^2.2 * 0.2126 + G^2.2 * 0.7152 + B^2.2 * 0.0722)^(1/2.2) private static Color HandleGray(Color color, Gray? grayElement) { var updatedColor = color; if (grayElement != null) { var gray = System.Math.Pow( System.Math.Pow(updatedColor.R, 2.2) * 0.2126 + System.Math.Pow(updatedColor.G, 2.2) * 0.7152 + System.Math.Pow(updatedColor.B, 2.2) * 0.0722, 1 / 2.2); var grayResult = (byte) System.Math.Round(gray); updatedColor.R = grayResult; updatedColor.G = grayResult; updatedColor.B = grayResult; } return updatedColor; }