in src/ports/SkFontHost_FreeType_common.cpp [603:1078]
bool colrv1_configure_skpaint(FT_Face face,
const SkSpan<SkColor>& palette,
const SkColor foregroundColor,
const FT_COLR_Paint& colrPaint,
SkPaint* paint) {
auto fetchColorStops = [&face, &palette, &foregroundColor](
const FT_ColorStopIterator& colorStopIterator,
std::vector<SkScalar>& stops,
std::vector<SkColor4f>& colors) -> bool {
const FT_UInt colorStopCount = colorStopIterator.num_color_stops;
if (colorStopCount == 0) {
return false;
}
// 5.7.11.2.4 ColorIndex, ColorStop and ColorLine
// "Applications shall apply the colorStops in increasing stopOffset order."
struct ColorStop {
SkScalar pos;
SkColor4f color;
};
std::vector<ColorStop> colorStopsSorted;
colorStopsSorted.resize(colorStopCount);
FT_ColorStop ftStop;
FT_ColorStopIterator mutable_color_stop_iterator = colorStopIterator;
while (FT_Get_Colorline_Stops(face, &ftStop, &mutable_color_stop_iterator)) {
FT_UInt index = mutable_color_stop_iterator.current_color_stop - 1;
ColorStop& skStop = colorStopsSorted[index];
skStop.pos = ftStop.stop_offset / kColorStopShift;
FT_UInt16& palette_index = ftStop.color.palette_index;
if (palette_index == kForegroundColorPaletteIndex) {
skStop.color = SkColor4f::FromColor(foregroundColor);
} else if (palette_index >= palette.size()) {
return false;
} else {
skStop.color = SkColor4f::FromColor(palette[palette_index]);
}
skStop.color.fA *= SkColrV1AlphaToFloat(ftStop.color.alpha);
}
std::stable_sort(colorStopsSorted.begin(), colorStopsSorted.end(),
[](const ColorStop& a, const ColorStop& b) { return a.pos < b.pos; });
stops.resize(colorStopCount);
colors.resize(colorStopCount);
for (size_t i = 0; i < colorStopCount; ++i) {
stops[i] = colorStopsSorted[i].pos;
colors[i] = colorStopsSorted[i].color;
}
return true;
};
switch (colrPaint.format) {
case FT_COLR_PAINTFORMAT_SOLID: {
FT_PaintSolid solid = colrPaint.u.solid;
// Dont' draw anything with this color if the palette index is out of bounds.
SkColor4f color = SkColors::kTransparent;
if (solid.color.palette_index == kForegroundColorPaletteIndex) {
color = SkColor4f::FromColor(foregroundColor);
} else if (solid.color.palette_index >= palette.size()) {
return false;
} else {
color = SkColor4f::FromColor(palette[solid.color.palette_index]);
}
color.fA *= SkColrV1AlphaToFloat(solid.color.alpha);
paint->setShader(nullptr);
paint->setColor(color);
return true;
}
case FT_COLR_PAINTFORMAT_LINEAR_GRADIENT: {
const FT_PaintLinearGradient& linearGradient = colrPaint.u.linear_gradient;
std::vector<SkScalar> stops;
std::vector<SkColor4f> colors;
if (!fetchColorStops(linearGradient.colorline.color_stop_iterator, stops, colors)) {
return false;
}
if (stops.size() == 1) {
paint->setColor(colors[0]);
return true;
}
SkPoint linePositions[2] = {SkPoint::Make( SkFixedToScalar(linearGradient.p0.x),
-SkFixedToScalar(linearGradient.p0.y)),
SkPoint::Make( SkFixedToScalar(linearGradient.p1.x),
-SkFixedToScalar(linearGradient.p1.y))};
SkPoint p0 = linePositions[0];
SkPoint p1 = linePositions[1];
SkPoint p2 = SkPoint::Make( SkFixedToScalar(linearGradient.p2.x),
-SkFixedToScalar(linearGradient.p2.y));
// If p0p1 or p0p2 are degenerate probably nothing should be drawn.
// If p0p1 and p0p2 are parallel then one side is the first color and the other side is
// the last color, depending on the direction.
// For now, just use the first color.
if (p1 == p0 || p2 == p0 || !SkPoint::CrossProduct(p1 - p0, p2 - p0)) {
paint->setColor(colors[0]);
return true;
}
// Follow implementation note in nanoemoji:
// https://github.com/googlefonts/nanoemoji/blob/0ac6e7bb4d8202db692574d8530a9b643f1b3b3c/src/nanoemoji/svg.py#L188
// to compute a new gradient end point P3 as the orthogonal
// projection of the vector from p0 to p1 onto a line perpendicular
// to line p0p2 and passing through p0.
SkVector perpendicularToP2P0 = (p2 - p0);
perpendicularToP2P0 = SkPoint::Make( perpendicularToP2P0.y(),
-perpendicularToP2P0.x());
SkVector p3 = p0 + SkVectorProjection((p1 - p0), perpendicularToP2P0);
linePositions[1] = p3;
// Project/scale points according to stop extrema along p0p3 line,
// p3 being the result of the projection above, then scale stops to
// to [0, 1] range so that repeat modes work. The Skia linear
// gradient shader performs the repeat modes over the 0 to 1 range,
// that's why we need to scale the stops to within that range.
SkTileMode tileMode = ToSkTileMode(linearGradient.colorline.extend);
SkScalar colorStopRange = stops.back() - stops.front();
// If the color stops are all at the same offset position, repeat and reflect modes
// become meaningless.
if (colorStopRange == 0.f) {
if (tileMode != SkTileMode::kClamp) {
paint->setColor(SK_ColorTRANSPARENT);
return true;
} else {
// Insert duplicated fake color stop in pad case at +1.0f to enable the projection
// of circles for an originally 0-length color stop range. Adding this stop will
// paint the equivalent gradient, because: All font specified color stops are in the
// same spot, mode is pad, so everything before this spot is painted with the first
// color, everything after this spot is painted with the last color. Not adding this
// stop will skip the projection and result in specifying non-normalized color stops
// to the shader.
stops.push_back(stops.back() + 1.0f);
colors.push_back(colors.back());
colorStopRange = 1.0f;
}
}
SkASSERT(colorStopRange != 0.f);
// If the colorStopRange is 0 at this point, the default behavior of the shader is to
// clamp to 1 color stops that are above 1, clamp to 0 for color stops that are below 0,
// and repeat the outer color stops at 0 and 1 if the color stops are inside the
// range. That will result in the correct rendering.
if ((colorStopRange != 1 || stops.front() != 0.f)) {
SkVector p0p3 = p3 - p0;
SkVector p0Offset = p0p3;
p0Offset.scale(stops.front());
SkVector p1Offset = p0p3;
p1Offset.scale(stops.back());
linePositions[0] = p0 + p0Offset;
linePositions[1] = p0 + p1Offset;
SkScalar scaleFactor = 1 / colorStopRange;
SkScalar startOffset = stops.front();
for (SkScalar& stop : stops) {
stop = (stop - startOffset) * scaleFactor;
}
}
sk_sp<SkShader> shader(SkGradientShader::MakeLinear(
linePositions,
colors.data(), SkColorSpace::MakeSRGB(), stops.data(), stops.size(),
tileMode,
SkGradientShader::Interpolation{
SkGradientShader::Interpolation::InPremul::kNo,
SkGradientShader::Interpolation::ColorSpace::kSRGB,
SkGradientShader::Interpolation::HueMethod::kShorter
},
nullptr));
SkASSERT(shader);
// An opaque color is needed to ensure the gradient is not modulated by alpha.
paint->setColor(SK_ColorBLACK);
paint->setShader(shader);
return true;
}
case FT_COLR_PAINTFORMAT_RADIAL_GRADIENT: {
const FT_PaintRadialGradient& radialGradient = colrPaint.u.radial_gradient;
SkPoint start = SkPoint::Make( SkFixedToScalar(radialGradient.c0.x),
-SkFixedToScalar(radialGradient.c0.y));
SkScalar startRadius = SkFixedToScalar(radialGradient.r0);
SkPoint end = SkPoint::Make( SkFixedToScalar(radialGradient.c1.x),
-SkFixedToScalar(radialGradient.c1.y));
SkScalar endRadius = SkFixedToScalar(radialGradient.r1);
std::vector<SkScalar> stops;
std::vector<SkColor4f> colors;
if (!fetchColorStops(radialGradient.colorline.color_stop_iterator, stops, colors)) {
return false;
}
if (stops.size() == 1) {
paint->setColor(colors[0]);
return true;
}
SkScalar colorStopRange = stops.back() - stops.front();
SkTileMode tileMode = ToSkTileMode(radialGradient.colorline.extend);
if (colorStopRange == 0.f) {
if (tileMode != SkTileMode::kClamp) {
paint->setColor(SK_ColorTRANSPARENT);
return true;
} else {
// Insert duplicated fake color stop in pad case at +1.0f to enable the projection
// of circles for an originally 0-length color stop range. Adding this stop will
// paint the equivalent gradient, because: All font specified color stops are in the
// same spot, mode is pad, so everything before this spot is painted with the first
// color, everything after this spot is painted with the last color. Not adding this
// stop will skip the projection and result in specifying non-normalized color stops
// to the shader.
stops.push_back(stops.back() + 1.0f);
colors.push_back(colors.back());
colorStopRange = 1.0f;
}
}
SkASSERT(colorStopRange != 0.f);
// If the colorStopRange is 0 at this point, the default behavior of the shader is to
// clamp to 1 color stops that are above 1, clamp to 0 for color stops that are below 0,
// and repeat the outer color stops at 0 and 1 if the color stops are inside the
// range. That will result in the correct rendering.
if (colorStopRange != 1 || stops.front() != 0.f) {
// For the Skia two-point caonical shader to understand the
// COLRv1 color stops we need to scale stops to 0 to 1 range and
// interpolate new centers and radii. Otherwise the shader
// clamps stops outside the range to 0 and 1 (larger interval)
// or repeats the outer stops at 0 and 1 if the (smaller
// interval).
SkVector startToEnd = end - start;
SkScalar radiusDiff = endRadius - startRadius;
SkScalar scaleFactor = 1 / colorStopRange;
SkScalar stopsStartOffset = stops.front();
SkVector startOffset = startToEnd;
startOffset.scale(stops.front());
SkVector endOffset = startToEnd;
endOffset.scale(stops.back());
// The order of the following computations is important in order to avoid
// overwriting start or startRadius before the second reassignment.
end = start + endOffset;
start = start + startOffset;
endRadius = startRadius + radiusDiff * stops.back();
startRadius = startRadius + radiusDiff * stops.front();
for (auto& stop : stops) {
stop = (stop - stopsStartOffset) * scaleFactor;
}
}
// For negative radii, interpolation is needed to prepare parameters suitable
// for invoking the shader. Implementation below as resolution discussed in
// https://github.com/googlefonts/colr-gradients-spec/issues/367.
// Truncate to manually interpolated color for tile mode clamp, otherwise
// calculate positive projected circles.
if (startRadius < 0 || endRadius < 0) {
if (startRadius == endRadius && startRadius < 0) {
paint->setColor(SK_ColorTRANSPARENT);
return true;
}
if (tileMode == SkTileMode::kClamp) {
SkVector startToEnd = end - start;
SkScalar radiusDiff = endRadius - startRadius;
SkScalar zeroRadiusStop = 0.f;
TruncateStops truncateSide = TruncateStart;
if (startRadius < 0) {
truncateSide = TruncateStart;
// Compute color stop position where radius is = 0. After the scaling
// of stop positions to the normal 0,1 range that we have done above,
// the size of the radius as a function of the color stops is: r(x) = r0
// + x*(r1-r0) Solving this function for r(x) = 0, we get: x = -r0 /
// (r1-r0)
zeroRadiusStop = -startRadius / (endRadius - startRadius);
startRadius = 0.f;
SkVector startEndDiff = end - start;
startEndDiff.scale(zeroRadiusStop);
start = start + startEndDiff;
}
if (endRadius < 0) {
truncateSide = TruncateEnd;
zeroRadiusStop = -startRadius / (endRadius - startRadius);
endRadius = 0.f;
SkVector startEndDiff = end - start;
startEndDiff.scale(1 - zeroRadiusStop);
end = end - startEndDiff;
}
if (!(startRadius == 0 && endRadius == 0)) {
truncateToStopInterpolating(
zeroRadiusStop, colors, stops, truncateSide);
} else {
// If both radii have become negative and where clamped to 0, we need to
// produce a single color cone, otherwise the shader colors the whole
// plane in a single color when two radii are specified as 0.
if (radiusDiff > 0) {
end = start + startToEnd;
endRadius = radiusDiff;
colors.erase(colors.begin(), colors.end() - 1);
stops.erase(stops.begin(), stops.end() - 1);
} else {
start -= startToEnd;
startRadius = -radiusDiff;
colors.erase(colors.begin() + 1, colors.end());
stops.erase(stops.begin() + 1, stops.end());
}
}
} else {
if (startRadius < 0 || endRadius < 0) {
auto roundIntegerMultiple = [](SkScalar factorZeroCrossing,
SkTileMode tileMode) {
int roundedMultiple = factorZeroCrossing > 0
? ceilf(factorZeroCrossing)
: floorf(factorZeroCrossing) - 1;
if (tileMode == SkTileMode::kMirror && roundedMultiple % 2 != 0) {
roundedMultiple += roundedMultiple < 0 ? -1 : 1;
}
return roundedMultiple;
};
SkVector startToEnd = end - start;
SkScalar radiusDiff = endRadius - startRadius;
SkScalar factorZeroCrossing = (startRadius / (startRadius - endRadius));
bool inRange = 0.f <= factorZeroCrossing && factorZeroCrossing <= 1.0f;
SkScalar direction = inRange && radiusDiff < 0 ? -1.0f : 1.0f;
SkScalar circleProjectionFactor =
roundIntegerMultiple(factorZeroCrossing * direction, tileMode);
startToEnd.scale(circleProjectionFactor);
startRadius += circleProjectionFactor * radiusDiff;
endRadius += circleProjectionFactor * radiusDiff;
start += startToEnd;
end += startToEnd;
}
}
}
// An opaque color is needed to ensure the gradient is not modulated by alpha.
paint->setColor(SK_ColorBLACK);
paint->setShader(SkGradientShader::MakeTwoPointConical(
start, startRadius, end, endRadius,
colors.data(), SkColorSpace::MakeSRGB(), stops.data(), stops.size(),
tileMode,
SkGradientShader::Interpolation{
SkGradientShader::Interpolation::InPremul::kNo,
SkGradientShader::Interpolation::ColorSpace::kSRGB,
SkGradientShader::Interpolation::HueMethod::kShorter
},
nullptr));
return true;
}
case FT_COLR_PAINTFORMAT_SWEEP_GRADIENT: {
const FT_PaintSweepGradient& sweepGradient = colrPaint.u.sweep_gradient;
SkPoint center = SkPoint::Make( SkFixedToScalar(sweepGradient.center.x),
-SkFixedToScalar(sweepGradient.center.y));
SkScalar startAngle = SkFixedToScalar(sweepGradient.start_angle * 180.0f);
SkScalar endAngle = SkFixedToScalar(sweepGradient.end_angle * 180.0f);
// OpenType 1.9.1 adds a shift to the angle to ease specification of a 0 to 360
// degree sweep.
startAngle += 180.0f;
endAngle += 180.0f;
std::vector<SkScalar> stops;
std::vector<SkColor4f> colors;
if (!fetchColorStops(sweepGradient.colorline.color_stop_iterator, stops, colors)) {
return false;
}
if (stops.size() == 1) {
paint->setColor(colors[0]);
return true;
}
// An opaque color is needed to ensure the gradient is not modulated by alpha.
paint->setColor(SK_ColorBLACK);
// New (Var)SweepGradient implementation compliant with OpenType 1.9.1 from here.
// The shader expects stops from 0 to 1, so we need to account for
// minimum and maximum stop positions being different from 0 and
// 1. We do that by scaling minimum and maximum stop positions to
// the 0 to 1 interval and scaling the angles inverse proportionally.
// 1) Scale angles to their equivalent positions if stops were from 0 to 1.
SkScalar sectorAngle = endAngle - startAngle;
SkTileMode tileMode = ToSkTileMode(sweepGradient.colorline.extend);
if (sectorAngle == 0 && tileMode != SkTileMode::kClamp) {
// "If the ColorLine's extend mode is reflect or repeat and start and end angle
// are equal, nothing is drawn.".
paint->setColor(SK_ColorTRANSPARENT);
return true;
}
SkScalar startAngleScaled = startAngle + sectorAngle * stops.front();
SkScalar endAngleScaled = startAngle + sectorAngle * stops.back();
// 2) Scale stops accordingly to 0 to 1 range.
float colorStopRange = stops.back() - stops.front();
if (colorStopRange == 0.f) {
if (tileMode != SkTileMode::kClamp) {
paint->setColor(SK_ColorTRANSPARENT);
return true;
} else {
// Insert duplicated fake color stop in pad case at +1.0f to feed the shader correct
// values and enable painting a pad sweep gradient with two colors. Adding this stop
// will paint the equivalent gradient, because: All font specified color stops are
// in the same spot, mode is pad, so everything before this spot is painted with the
// first color, everything after this spot is painted with the last color. Not
// adding this stop will skip the projection and result in specifying non-normalized
// color stops to the shader.
stops.push_back(stops.back() + 1.0f);
colors.push_back(colors.back());
colorStopRange = 1.0f;
}
}
SkScalar scaleFactor = 1 / colorStopRange;
SkScalar startOffset = stops.front();
for (SkScalar& stop : stops) {
stop = (stop - startOffset) * scaleFactor;
}
/* https://docs.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients
* "The angles are expressed in counter-clockwise degrees from
* the direction of the positive x-axis on the design
* grid. [...] The color line progresses from the start angle
* to the end angle in the counter-clockwise direction;" -
* Convert angles and stops from counter-clockwise to clockwise
* for the shader if the gradient is not already reversed due to
* start angle being larger than end angle. */
startAngleScaled = 360.f - startAngleScaled;
endAngleScaled = 360.f - endAngleScaled;
if (startAngleScaled >= endAngleScaled) {
std::swap(startAngleScaled, endAngleScaled);
std::reverse(stops.begin(), stops.end());
std::reverse(colors.begin(), colors.end());
for (auto& stop : stops) {
stop = 1.0f - stop;
}
}
paint->setShader(SkGradientShader::MakeSweep(
center.x(), center.y(),
colors.data(), SkColorSpace::MakeSRGB(), stops.data(), stops.size(),
tileMode,
startAngleScaled, endAngleScaled,
SkGradientShader::Interpolation{
SkGradientShader::Interpolation::InPremul::kNo,
SkGradientShader::Interpolation::ColorSpace::kSRGB,
SkGradientShader::Interpolation::HueMethod::kShorter
},
nullptr));
return true;
}
default: {
SkASSERT(false);
return false;
}
}
SkUNREACHABLE;
}