Skip to content

Commit 2771555

Browse files
jhaygood86eXpl0it3r
authored andcommitted
CSS engine: sync CSS Color 4/5 color science
Adds the color-space math and color-function resolution from PeachPDF v0.9.6 (upstream cc1a521, 796bb8c, 7cf1855). - ColorSpaceMath: forward/reverse transforms between gamma-encoded sRGB and srgb-linear/display-p3/lab/oklab/xyz-d65/xyz-d50/hsl/hwb/lch/oklch, plus polar hue interpolation (shorter/longer/increasing/decreasing) - Color: FromLab/FromOklab/FromLch/FromOklch factories and Mix(), the premultiplied-alpha color-mix() interpolation - ColorFunctionExtensions: ToResolvedColor(), which resolves a tokenized value through every color function - rgb/hsl/hwb/gray plus lab/oklab/lch/oklch and color-mix() - and MixPaletteColors() for palette-mix() - ColorInterpolationMethodGrammar: the shared `in <space> [<dir> hue]` prelude grammar, so the value converter and the render-time parser agree on which gradient interpolation methods are valid Downgraded to C# 7.3 for the netstandard2.0 leg: collection expressions, switch expressions, property patterns, index-from-end and Math.Clamp. Math.Cbrt is netstandard2.1+, so ColorSpaceMath carries a sign-preserving cube root.
1 parent 58d80b4 commit 2771555

4 files changed

Lines changed: 1066 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
5+
{
6+
/// <summary>
7+
/// The single, shared grammar for a gradient's <c>&lt;color-interpolation-method&gt;</c> prelude
8+
/// (<a href="https://drafts.csswg.org/css-images-4/#color-interpolation-method">CSS Images 4 §3.1</a>):
9+
/// <code>in [ &lt;rectangular-color-space&gt; | &lt;polar-color-space&gt; &lt;hue-interpolation-method&gt;? ]</code>.
10+
/// Both the CSS-OM validator (<see cref="GradientConverter"/>, which drops a malformed prelude at
11+
/// parse time) and the render-time parser classify keywords through this one class, so the two
12+
/// layers cannot disagree about which preludes are valid.
13+
///
14+
/// Only the color spaces the interpolation math actually implements are recognized; the
15+
/// wide-gamut RGB spaces <c>a98-rgb</c>/<c>prophoto-rgb</c>/<c>rec2020</c> are valid CSS but are
16+
/// deliberately rejected here rather than approximated.
17+
/// </summary>
18+
internal static class ColorInterpolationMethodGrammar
19+
{
20+
private const string In = "in";
21+
private const string Hue = "hue";
22+
23+
private static readonly HashSet<string> ColorSpaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
24+
{
25+
"srgb", "srgb-linear", "display-p3", "lab", "oklab", "xyz", "xyz-d65", "xyz-d50",
26+
"hsl", "hwb", "lch", "oklch",
27+
};
28+
29+
private static readonly HashSet<string> PolarColorSpaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
30+
{
31+
"hsl", "hwb", "lch", "oklch",
32+
};
33+
34+
private static readonly HashSet<string> HueDirections = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
35+
{
36+
"shorter", "longer", "increasing", "decreasing",
37+
};
38+
39+
public static bool IsColorSpace(string name)
40+
{
41+
return name != null && ColorSpaces.Contains(name);
42+
}
43+
44+
public static bool IsPolarColorSpace(string name)
45+
{
46+
return name != null && PolarColorSpaces.Contains(name);
47+
}
48+
49+
public static bool IsHueDirection(string name)
50+
{
51+
return name != null && HueDirections.Contains(name);
52+
}
53+
54+
/// <summary>
55+
/// If <paramref name="group"/> (one gradient comma-group's tokens) contains an <c>in</c>
56+
/// interpolation-method prelude, validates it and returns the group with the contiguous
57+
/// <c>in …</c> slice removed as <paramref name="remainder"/> (for the caller to validate as the
58+
/// gradient's own direction/shape/position, since the two combine with <c>||</c> in either order).
59+
/// Returns <see langword="false"/> when an <c>in</c> is present but the method is malformed
60+
/// (unknown/unsupported space, a hue-interpolation method on a rectangular space or without the
61+
/// trailing <c>hue</c>, or <c>in</c> with nothing after it) — the caller drops the declaration.
62+
/// When no <c>in</c> is present, <paramref name="hasInterpolationMethod"/> is
63+
/// <see langword="false"/>, <paramref name="remainder"/> is the group unchanged, and it returns
64+
/// <see langword="true"/>.
65+
/// </summary>
66+
public static bool TryExtractInterpolationMethod(
67+
IEnumerable<Token> group, out List<Token> remainder, out bool hasInterpolationMethod)
68+
{
69+
var tokens = new List<Token>(group);
70+
71+
int inIndex = IndexOfIdent(tokens, 0, In);
72+
if (inIndex < 0)
73+
{
74+
remainder = tokens;
75+
hasInterpolationMethod = false;
76+
return true;
77+
}
78+
79+
hasInterpolationMethod = true;
80+
81+
// in <color-space>
82+
int spaceIndex = NextNonWhitespace(tokens, inIndex + 1);
83+
string spaceName;
84+
if (spaceIndex < 0 || !IsIdent(tokens[spaceIndex], out spaceName) || !IsColorSpace(spaceName))
85+
{
86+
remainder = null;
87+
return false;
88+
}
89+
90+
int lastMethodIndex = spaceIndex;
91+
92+
// A hue-interpolation method (<dir> hue) is only valid after a polar space, and only complete
93+
// with the trailing "hue" keyword.
94+
if (IsPolarColorSpace(spaceName))
95+
{
96+
int dirIndex = NextNonWhitespace(tokens, spaceIndex + 1);
97+
string dir;
98+
if (dirIndex >= 0 && IsIdent(tokens[dirIndex], out dir) && IsHueDirection(dir))
99+
{
100+
int hueIndex = NextNonWhitespace(tokens, dirIndex + 1);
101+
string hueWord;
102+
if (hueIndex < 0 || !IsIdent(tokens[hueIndex], out hueWord) ||
103+
!string.Equals(hueWord, Hue, StringComparison.OrdinalIgnoreCase))
104+
{
105+
remainder = null;
106+
return false;
107+
}
108+
109+
lastMethodIndex = hueIndex;
110+
}
111+
}
112+
113+
remainder = BuildRemainder(tokens, inIndex, lastMethodIndex);
114+
return true;
115+
}
116+
117+
// The group with tokens [firstMethodIndex..lastMethodIndex] removed, then whitespace-trimmed at the
118+
// edges so the leftover direction/shape validates the same as an un-prefixed group would.
119+
private static List<Token> BuildRemainder(List<Token> tokens, int firstMethodIndex, int lastMethodIndex)
120+
{
121+
var remainder = new List<Token>(tokens.Count);
122+
123+
for (int i = 0; i < tokens.Count; i++)
124+
{
125+
if (i >= firstMethodIndex && i <= lastMethodIndex) continue;
126+
remainder.Add(tokens[i]);
127+
}
128+
129+
while (remainder.Count > 0 && remainder[0].Type == TokenType.Whitespace)
130+
remainder.RemoveAt(0);
131+
while (remainder.Count > 0 && remainder[remainder.Count - 1].Type == TokenType.Whitespace)
132+
remainder.RemoveAt(remainder.Count - 1);
133+
134+
return remainder;
135+
}
136+
137+
private static int IndexOfIdent(List<Token> tokens, int start, string data)
138+
{
139+
for (int i = start; i < tokens.Count; i++)
140+
{
141+
string value;
142+
if (IsIdent(tokens[i], out value) &&
143+
string.Equals(value, data, StringComparison.OrdinalIgnoreCase))
144+
return i;
145+
}
146+
147+
return -1;
148+
}
149+
150+
private static int NextNonWhitespace(List<Token> tokens, int start)
151+
{
152+
for (int i = start; i < tokens.Count; i++)
153+
{
154+
if (tokens[i].Type != TokenType.Whitespace) return i;
155+
}
156+
157+
return -1;
158+
}
159+
160+
private static bool IsIdent(Token token, out string data)
161+
{
162+
if (token.Type == TokenType.Ident)
163+
{
164+
data = token.Data;
165+
return true;
166+
}
167+
168+
data = null;
169+
return false;
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)