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