in src/ports/SkScalerContext_win_dw.cpp [618:1200]
bool SkScalerContext_DW::drawColorV1Paint(SkCanvas& canvas,
IDWritePaintReader& reader,
DWRITE_PAINT_ELEMENT const & element)
{
// Helper to draw the specified number of children.
auto drawChildren = [&](uint32_t childCount) -> bool {
if (childCount != 0) {
DWRITE_PAINT_ELEMENT childElement;
HRB(reader.MoveToFirstChild(&childElement));
this->drawColorV1Paint(canvas, reader, childElement);
for (uint32_t i = 1; i < childCount; i++) {
HRB(reader.MoveToNextSibling(&childElement));
this->drawColorV1Paint(canvas, reader, childElement);
}
HRB(reader.MoveToParent());
}
return true;
};
SkAutoCanvasRestore restoreCanvas(&canvas, true);
switch (element.paintType) {
case DWRITE_PAINT_TYPE_NONE:
return true;
case DWRITE_PAINT_TYPE_LAYERS: {
// A layers paint element has a variable number of children.
return drawChildren(element.paint.layers.childCount);
}
case DWRITE_PAINT_TYPE_SOLID_GLYPH: {
// A solid glyph paint element has no children.
// glyphIndex, color.value, color.paletteEntryIndex, color.alpha, color.colorAttributes
auto const& solidGlyph = element.paint.solidGlyph;
SkPath path;
SkTScopedComPtr<IDWriteGeometrySink> geometryToPath;
HRBM(SkDWriteGeometrySink::Create(&path, &geometryToPath),
"Could not create geometry to path converter.");
UINT16 glyphId = SkTo<UINT16>(solidGlyph.glyphIndex);
{
Exclusive l(maybe_dw_mutex(*this->getDWriteTypeface()));
HRBM(this->getDWriteTypeface()->fDWriteFontFace->GetGlyphRunOutline(
SkScalarToFloat(fTextSizeRender),
&glyphId,
nullptr, //advances
nullptr, //offsets
1, //num glyphs
FALSE, //sideways
FALSE, //rtl
geometryToPath.get()),
"Could not create glyph outline.");
}
path.transform(SkMatrix::Scale(1.0f / fTextSizeRender, 1.0f / fTextSizeRender));
SkPaint skPaint;
skPaint.setColor4f(sk_color_from(solidGlyph.color.value));
skPaint.setAntiAlias(fRenderingMode != DWRITE_RENDERING_MODE_ALIASED);
canvas.drawPath(path, skPaint);
return true;
}
case DWRITE_PAINT_TYPE_SOLID: {
// A solid paint element has no children.
// value, paletteEntryIndex, alphaMultiplier, colorAttributes
SkPaint skPaint;
skPaint.setColor4f(sk_color_from(element.paint.solid.value));
canvas.drawPaint(skPaint);
return true;
}
case DWRITE_PAINT_TYPE_LINEAR_GRADIENT: {
auto const& linearGradient = element.paint.linearGradient;
// A linear gradient paint element has no children.
// x0, y0, x1, y1, x2, y2, extendMode, gradientStopCount, [colorStops]
if (linearGradient.gradientStopCount == 0) {
return true;
}
std::vector<D2D1_GRADIENT_STOP> stops;
stops.resize(linearGradient.gradientStopCount);
// If success stops will be ordered.
HRBM(reader.GetGradientStops(0, stops.size(), stops.data()),
"Could not get linear gradient stops.");
SkPaint skPaint;
if (stops.size() == 1) {
skPaint.setColor4f(sk_color_from(stops[0].color));
canvas.drawPaint(skPaint);
return true;
}
SkPoint linePositions[2] = { {linearGradient.x0, linearGradient.y0},
{linearGradient.x1, linearGradient.y1} };
SkPoint p0 = linePositions[0];
SkPoint p1 = linePositions[1];
SkPoint p2 = SkPoint::Make(linearGradient.x2, linearGradient.y2);
// 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)) {
skPaint.setColor4f(sk_color_from(stops[0].color));
canvas.drawPaint(skPaint);
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 = sk_tile_mode_from(SkTo<D2D1_EXTEND_MODE>(linearGradient.extendMode));
SkScalar colorStopRange = stops.back().position - stops.front().position;
// 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) {
//skPaint.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().position + 1.0f, stops.back().color });
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().position != 0.f)) {
SkVector p0p3 = p3 - p0;
SkVector p0Offset = p0p3;
p0Offset.scale(stops.front().position);
SkVector p1Offset = p0p3;
p1Offset.scale(stops.back().position);
linePositions[0] = p0 + p0Offset;
linePositions[1] = p0 + p1Offset;
SkScalar scaleFactor = 1 / colorStopRange;
SkScalar startOffset = stops.front().position;
for (D2D1_GRADIENT_STOP& stop : stops) {
stop.position = (stop.position - startOffset) * scaleFactor;
}
}
std::unique_ptr<SkColor4f[]> skColors(new SkColor4f[stops.size()]);
std::unique_ptr<SkScalar[]> skStops(new SkScalar[stops.size()]);
for (size_t i = 0; i < stops.size(); ++i) {
skColors[i] = sk_color_from(stops[i].color);
skStops[i] = stops[i].position;
}
sk_sp<SkShader> shader(SkGradientShader::MakeLinear(
linePositions,
skColors.get(), SkColorSpace::MakeSRGB(), skStops.get(), 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.
skPaint.setColor(SK_ColorBLACK);
skPaint.setShader(shader);
canvas.drawPaint(skPaint);
return true;
}
case DWRITE_PAINT_TYPE_RADIAL_GRADIENT: {
auto const& radialGradient = element.paint.radialGradient;
// A radial gradient paint element has no children.
// x0, y0, radius0, x1, y1, radius1, extendMode, gradientStopCount, [colorsStops]
SkPoint start = SkPoint::Make(radialGradient.x0, radialGradient.y0);
SkScalar startRadius = radialGradient.radius0;
SkPoint end = SkPoint::Make(radialGradient.x1, radialGradient.y1);
SkScalar endRadius = radialGradient.radius1;
if (radialGradient.gradientStopCount == 0) {
return true;
}
std::vector<D2D1_GRADIENT_STOP> stops;
stops.resize(radialGradient.gradientStopCount);
// If success stops will be ordered.
HRBM(reader.GetGradientStops(0, stops.size(), stops.data()),
"Could not get radial gradient stops.");
SkPaint skPaint;
if (stops.size() == 1) {
skPaint.setColor4f(sk_color_from(stops[0].color));
canvas.drawPaint(skPaint);
return true;
}
SkScalar colorStopRange = stops.back().position - stops.front().position;
SkTileMode tileMode = sk_tile_mode_from(SkTo<D2D1_EXTEND_MODE>(radialGradient.extendMode));
if (colorStopRange == 0.f) {
if (tileMode != SkTileMode::kClamp) {
//skPaint.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().position + 1.0f, stops.back().color });
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().position != 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().position;
SkVector startOffset = startToEnd;
startOffset.scale(stops.front().position);
SkVector endOffset = startToEnd;
endOffset.scale(stops.back().position);
// 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().position;
startRadius = startRadius + radiusDiff * stops.front().position;
for (auto& stop : stops) {
stop.position = (stop.position - 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) {
//skPaint.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, 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;
stops.erase(stops.begin(), stops.end() - 1);
} else {
start -= startToEnd;
startRadius = -radiusDiff;
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;
}
}
}
std::unique_ptr<SkColor4f[]> skColors(new SkColor4f[stops.size()]);
std::unique_ptr<SkScalar[]> skStops(new SkScalar[stops.size()]);
for (size_t i = 0; i < stops.size(); ++i) {
skColors[i] = sk_color_from(stops[i].color);
skStops[i] = stops[i].position;
}
// An opaque color is needed to ensure the gradient is not modulated by alpha.
skPaint.setColor(SK_ColorBLACK);
skPaint.setShader(SkGradientShader::MakeTwoPointConical(
start, startRadius, end, endRadius,
skColors.get(), SkColorSpace::MakeSRGB(), skStops.get(), stops.size(),
tileMode,
SkGradientShader::Interpolation{
SkGradientShader::Interpolation::InPremul::kNo,
SkGradientShader::Interpolation::ColorSpace::kSRGB,
SkGradientShader::Interpolation::HueMethod::kShorter
},
nullptr));
canvas.drawPaint(skPaint);
return true;
}
case DWRITE_PAINT_TYPE_SWEEP_GRADIENT: {
auto const& sweepGradient = element.paint.sweepGradient;
// A sweep gradient paint element has no children.
// centerX, centerY, startAngle, endAngle, extendMode, gradientStopCount, [colorStops]
if (sweepGradient.gradientStopCount == 0) {
return true;
}
std::vector<D2D1_GRADIENT_STOP> stops;
stops.resize(sweepGradient.gradientStopCount);
// If success stops will be ordered.
HRBM(reader.GetGradientStops(0, stops.size(), stops.data()),
"Could not get sweep gradient stops");
SkPaint skPaint;
if (stops.size() == 1) {
skPaint.setColor4f(sk_color_from(stops[0].color));
canvas.drawPaint(skPaint);
return true;
}
SkPoint center = SkPoint::Make(sweepGradient.centerX, sweepGradient.centerY);
SkScalar startAngle = sweepGradient.startAngle;
SkScalar endAngle = sweepGradient.endAngle;
// OpenType 1.9.1 adds a shift to the angle to ease specification of a 0 to 360
// degree sweep. This appears to already be applied by DW.
//startAngle += 180.0f;
//endAngle += 180.0f;
// An opaque color is needed to ensure the gradient is not modulated by alpha.
skPaint.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 = sk_tile_mode_from(SkTo<D2D1_EXTEND_MODE>(sweepGradient.extendMode));
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.".
//skPaint.setColor(SK_ColorTRANSPARENT);
return true;
}
SkScalar startAngleScaled = startAngle + sectorAngle * stops.front().position;
SkScalar endAngleScaled = startAngle + sectorAngle * stops.back().position;
// 2) Scale stops accordingly to 0 to 1 range.
float colorStopRange = stops.back().position - stops.front().position;
if (colorStopRange == 0.f) {
if (tileMode != SkTileMode::kClamp) {
//skPaint.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().position + 1.0f, stops.back().color });
colorStopRange = 1.0f;
}
}
SkScalar scaleFactor = 1 / colorStopRange;
SkScalar startOffset = stops.front().position;
for (D2D1_GRADIENT_STOP& stop : stops) {
stop.position = (stop.position - 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());
for (auto& stop : stops) {
stop.position = 1.0f - stop.position;
}
}
std::unique_ptr<SkColor4f[]> skColors(new SkColor4f[stops.size()]);
std::unique_ptr<SkScalar[]> skStops(new SkScalar[stops.size()]);
for (size_t i = 0; i < stops.size(); ++i) {
skColors[i] = sk_color_from(stops[i].color);
skStops[i] = stops[i].position;
}
skPaint.setShader(SkGradientShader::MakeSweep(
center.x(), center.y(),
skColors.get(), SkColorSpace::MakeSRGB(), skStops.get(), stops.size(),
tileMode,
startAngleScaled, endAngleScaled,
SkGradientShader::Interpolation{
SkGradientShader::Interpolation::InPremul::kNo,
SkGradientShader::Interpolation::ColorSpace::kSRGB,
SkGradientShader::Interpolation::HueMethod::kShorter
},
nullptr));
canvas.drawPaint(skPaint);
return true;
}
case DWRITE_PAINT_TYPE_GLYPH: {
// A glyph paint element has one child, which is the fill for the glyph shape glyphIndex.
SkPath path;
SkTScopedComPtr<IDWriteGeometrySink> geometryToPath;
HRBM(SkDWriteGeometrySink::Create(&path, &geometryToPath),
"Could not create geometry to path converter.");
UINT16 glyphId = SkTo<UINT16>(element.paint.glyph.glyphIndex);
{
Exclusive l(maybe_dw_mutex(*this->getDWriteTypeface()));
HRBM(this->getDWriteTypeface()->fDWriteFontFace->GetGlyphRunOutline(
SkScalarToFloat(fTextSizeRender),
&glyphId,
nullptr, //advances
nullptr, //offsets
1, //num glyphs
FALSE, //sideways
FALSE, //rtl
geometryToPath.get()),
"Could not create glyph outline.");
}
path.transform(SkMatrix::Scale(1.0f / fTextSizeRender, 1.0f / fTextSizeRender));
canvas.clipPath(path, fRenderingMode != DWRITE_RENDERING_MODE_ALIASED);
drawChildren(1);
return true;
}
case DWRITE_PAINT_TYPE_COLOR_GLYPH: {
auto const& colorGlyph = element.paint.colorGlyph;
// A color glyph paint element has one child, the root of the paint tree for glyphIndex.
// glyphIndex, clipBox
if (D2D_RECT_F_is_empty(colorGlyph.clipBox)) {
// Does not have a clip box
} else {
SkRect r = sk_rect_from(colorGlyph.clipBox);
canvas.clipRect(r, fRenderingMode != DWRITE_RENDERING_MODE_ALIASED);
}
drawChildren(1);
return true;
}
case DWRITE_PAINT_TYPE_TRANSFORM: {
// A transform paint element always has one child, the transformed content.
canvas.concat(sk_matrix_from(element.paint.transform));
drawChildren(1);
return true;
}
case DWRITE_PAINT_TYPE_COMPOSITE: {
// A composite paint element has two children, the source and destination of the operation.
SkPaint blendModePaint;
blendModePaint.setBlendMode(sk_blend_mode_from(element.paint.composite.mode));
SkAutoCanvasRestore acr(&canvas, false);
// Need to visit the second child first and do savelayers, so manually handle children.
DWRITE_PAINT_ELEMENT sourceElement;
DWRITE_PAINT_ELEMENT backdropElement;
HRBM(reader.MoveToFirstChild(&sourceElement), "Could not move to child.");
HRBM(reader.MoveToNextSibling(&backdropElement), "Could not move to sibiling.");
canvas.saveLayer(nullptr, nullptr);
this->drawColorV1Paint(canvas, reader, backdropElement);
HRBM(reader.MoveToParent(), "Could not move to parent.");
HRBM(reader.MoveToFirstChild(&sourceElement), "Could not move to child.");
canvas.saveLayer(nullptr, &blendModePaint);
this->drawColorV1Paint(canvas, reader, sourceElement);
HRBM(reader.MoveToParent(), "Could not move to parent.");
return true;
}
default:
return false;
}
}