1 /*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <SkFontMetrics.h>
18 #include <SkRRect.h>
19 #include <SkTextBlob.h>
20
21 #include "../utils/Color.h"
22 #include "Canvas.h"
23 #include "FeatureFlags.h"
24 #include "MinikinUtils.h"
25 #include "Paint.h"
26 #include "Properties.h"
27 #include "RenderNode.h"
28 #include "Typeface.h"
29 #include "hwui/PaintFilter.h"
30 #include "pipeline/skia/SkiaRecordingCanvas.h"
31
32 #ifdef __ANDROID__
33 #include <com_android_graphics_hwui_flags.h>
34 namespace flags = com::android::graphics::hwui::flags;
35 #else
36 namespace flags {
high_contrast_text_small_text_rect()37 constexpr bool high_contrast_text_small_text_rect() {
38 return false;
39 }
high_contrast_text_inner_text_color()40 constexpr bool high_contrast_text_inner_text_color() {
41 return false;
42 }
43 } // namespace flags
44 #endif
45
46 namespace android {
47
48 // These should match the constants in framework/base/core/java/android/text/Layout.java
49 inline constexpr float kHighContrastTextBorderWidth = 4.0f;
50 inline constexpr float kHighContrastTextBorderWidthFactor = 0.2f;
51
drawStroke(SkScalar left,SkScalar right,SkScalar top,SkScalar thickness,const Paint & paint,Canvas * canvas)52 static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness,
53 const Paint& paint, Canvas* canvas) {
54 const SkScalar strokeWidth = fmax(thickness, 1.0f);
55 const SkScalar bottom = top + strokeWidth;
56 canvas->drawRect(left, top, right, bottom, paint);
57 }
58
simplifyPaint(int color,Paint * paint)59 static void simplifyPaint(int color, Paint* paint) {
60 paint->setColor(color);
61 paint->setShader(nullptr);
62 paint->setColorFilter(nullptr);
63 paint->setLooper(nullptr);
64
65 if (flags::high_contrast_text_small_text_rect()) {
66 paint->setStrokeWidth(
67 std::max(kHighContrastTextBorderWidth,
68 kHighContrastTextBorderWidthFactor * paint->getSkFont().getSize()));
69 } else {
70 auto borderWidthFactor = 0.04f;
71 paint->setStrokeWidth(kHighContrastTextBorderWidth +
72 borderWidthFactor * paint->getSkFont().getSize());
73 }
74 paint->setStrokeJoin(SkPaint::kRound_Join);
75 paint->setLooper(nullptr);
76 paint->setBlendMode(SkBlendMode::kSrcOver);
77 }
78
79 namespace {
80
shouldDarkenTextForHighContrast(const uirenderer::Lab & lab)81 static bool shouldDarkenTextForHighContrast(const uirenderer::Lab& lab) {
82 // LINT.IfChange(hct_darken)
83 return lab.L <= 50;
84 // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken)
85 }
86
87 } // namespace
88
adjustHighContrastInnerTextColor(uirenderer::Lab * lab)89 static void adjustHighContrastInnerTextColor(uirenderer::Lab* lab) {
90 bool darken = shouldDarkenTextForHighContrast(*lab);
91 bool isGrayscale = abs(lab->a) < 10 && abs(lab->b) < 10;
92 if (isGrayscale) {
93 // For near-grayscale text we first remove all color.
94 lab->a = lab->b = 0;
95 if (lab->L > 40 && lab->L < 60) {
96 // Text near "middle gray" is pushed to a more contrasty gray.
97 lab->L = darken ? 20 : 80;
98 } else {
99 // Other grayscale text is pushed completely white or black.
100 lab->L = darken ? 0 : 100;
101 }
102 } else {
103 // For color text we ensure the text is bright enough (for light text)
104 // or dark enough (for dark text) to stand out against the background,
105 // without touching the A and B components so we retain color.
106 if (darken && lab->L > 20.f) {
107 lab->L = 20.0f;
108 } else if (!darken && lab->L < 90.f) {
109 lab->L = 90.0f;
110 }
111 }
112 }
113
114 class DrawTextFunctor {
115 public:
116 /**
117 * Creates a Functor to draw the given text layout.
118 *
119 * @param layout
120 * @param canvas
121 * @param paint
122 * @param x
123 * @param y
124 * @param totalAdvance
125 * @param bounds bounds of the text. Only required if high contrast text mode is enabled.
126 */
DrawTextFunctor(const minikin::Layout & layout,Canvas * canvas,const Paint & paint,float x,float y,float totalAdvance)127 DrawTextFunctor(const minikin::Layout& layout, Canvas* canvas, const Paint& paint, float x,
128 float y, float totalAdvance)
129 : layout(layout)
130 , canvas(canvas)
131 , paint(paint)
132 , x(x)
133 , y(y)
134 , totalAdvance(totalAdvance)
135 , underlinePosition(0)
136 , underlineThickness(0) {}
137
operator()138 void operator()(size_t start, size_t end) {
139 auto glyphFunc = [&](uint16_t* text, float* positions) {
140 for (size_t i = start, textIndex = 0, posIndex = 0; i < end; i++) {
141 text[textIndex++] = layout.getGlyphId(i);
142 positions[posIndex++] = x + layout.getX(i);
143 positions[posIndex++] = y + layout.getY(i);
144 }
145 };
146
147 size_t glyphCount = end - start;
148
149 if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) {
150 // high contrast draw path
151 int color = paint.getColor();
152 uirenderer::Lab lab = uirenderer::sRGBToLab(color);
153 bool darken = shouldDarkenTextForHighContrast(lab);
154
155 // outline
156 gDrawTextBlobMode = DrawTextBlobMode::HctOutline;
157 Paint outlinePaint(paint);
158 simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint);
159 outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style);
160 canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
161
162 // inner
163 gDrawTextBlobMode = DrawTextBlobMode::HctInner;
164 Paint innerPaint(paint);
165 if (flags::high_contrast_text_inner_text_color()) {
166 adjustHighContrastInnerTextColor(&lab);
167 simplifyPaint(uirenderer::LabToSRGB(lab, SK_AlphaOPAQUE), &innerPaint);
168 } else {
169 simplifyPaint(darken ? SK_ColorBLACK : SK_ColorWHITE, &innerPaint);
170 }
171 innerPaint.setStyle(SkPaint::kFill_Style);
172 canvas->drawGlyphs(glyphFunc, glyphCount, innerPaint, x, y, totalAdvance);
173 gDrawTextBlobMode = DrawTextBlobMode::Normal;
174 } else {
175 // standard draw path
176 canvas->drawGlyphs(glyphFunc, glyphCount, paint, x, y, totalAdvance);
177 }
178
179 // Extract underline position and thickness.
180 if (paint.isUnderline()) {
181 SkFontMetrics metrics;
182 paint.getSkFont().getMetrics(&metrics);
183 const float textSize = paint.getSkFont().getSize();
184 SkScalar position;
185 if (!metrics.hasUnderlinePosition(&position)) {
186 position = textSize * Paint::kStdUnderline_Top;
187 }
188 SkScalar thickness;
189 if (!metrics.hasUnderlineThickness(&thickness)) {
190 thickness = textSize * Paint::kStdUnderline_Thickness;
191 }
192
193 // If multiple fonts are used, use the most bottom position and most thick stroke
194 // width as the underline position. This follows the CSS standard:
195 // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property
196 // <quote>
197 // The exact position and thickness of line decorations is UA-defined in this level.
198 // However, for underlines and overlines the UA must use a single thickness and
199 // position on each line for the decorations deriving from a single decorating box.
200 // </quote>
201 underlinePosition = std::max(underlinePosition, position);
202 underlineThickness = std::max(underlineThickness, thickness);
203 }
204 }
205
getUnderlinePosition()206 float getUnderlinePosition() const { return underlinePosition; }
getUnderlineThickness()207 float getUnderlineThickness() const { return underlineThickness; }
208
209 private:
210 const minikin::Layout& layout;
211 Canvas* canvas;
212 const Paint& paint;
213 float x;
214 float y;
215 float totalAdvance;
216 float underlinePosition;
217 float underlineThickness;
218 };
219
220 } // namespace android
221