目前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;
}