1 /*
2 * Copyright 2023 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "gm/gm.h"
9 #include "include/core/SkBitmap.h"
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkColorPriv.h"
12 #include "include/core/SkData.h"
13 #include "include/core/SkFont.h"
14 #include "include/core/SkFontTypes.h"
15 #include "include/core/SkRefCnt.h"
16 #include "include/core/SkStream.h"
17 #include "include/core/SkString.h"
18 #include "include/core/SkSurface.h"
19 #include "include/core/SkTypeface.h"
20 #include "include/ports/SkTypeface_fontations.h"
21 #include "modules/skshaper/include/SkShaper.h"
22 #include "src/ports/SkTypeface_FreeType.h"
23 #include "tools/Resources.h"
24 #include "tools/TestFontDataProvider.h"
25
26 namespace skiagm {
27
28 namespace {
29
30 constexpr int kGmWidth = 1000;
31 constexpr int kMargin = 30;
32 constexpr float kFontSize = 24;
33 constexpr float kLangYIncrementScale = 1.9;
34
35 /** Compare bitmap A and B, in this case originating from text rendering results with FreeType and
36 * Fontations + Skia path rendering, compute individual pixel differences for the rectangles that
37 * must match in size. Produce a highlighted difference bitmap, in which any pixel becomes white for
38 * which a difference was determined. */
comparePixels(const SkPixmap & pixmapA,const SkPixmap & pixmapB,SkBitmap * outPixelDiffBitmap,SkBitmap * outHighlightDiffBitmap)39 void comparePixels(const SkPixmap& pixmapA,
40 const SkPixmap& pixmapB,
41 SkBitmap* outPixelDiffBitmap,
42 SkBitmap* outHighlightDiffBitmap) {
43 if (pixmapA.dimensions() != pixmapB.dimensions()) {
44 return;
45 }
46 if (pixmapA.dimensions() != outPixelDiffBitmap->dimensions()) {
47 return;
48 }
49
50 SkISize dimensions = pixmapA.dimensions();
51 for (int32_t x = 0; x < dimensions.fWidth; x++) {
52 for (int32_t y = 0; y < dimensions.fHeight; y++) {
53 SkColor c0 = pixmapA.getColor(x, y);
54 SkColor c1 = pixmapB.getColor(x, y);
55 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
56 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
57 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
58
59 *(outPixelDiffBitmap->getAddr32(x, y)) =
60 SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
61
62 if (dr != 0 || dg != 0 || db != 0) {
63 *(outHighlightDiffBitmap->getAddr32(x, y)) = SK_ColorWHITE;
64 } else {
65 *(outHighlightDiffBitmap->getAddr32(x, y)) = SK_ColorBLACK;
66 }
67 }
68 }
69 }
70
71 } // namespace
72
73 class FontationsFtCompareGM : public GM {
74 public:
FontationsFtCompareGM(std::string testName,std::string fontNameFilterRegexp,std::string langFilterRegexp,SkFontHinting hintingMode=SkFontHinting::kNone)75 FontationsFtCompareGM(std::string testName,
76 std::string fontNameFilterRegexp,
77 std::string langFilterRegexp,
78 SkFontHinting hintingMode = SkFontHinting::kNone)
79 : fTestDataIterator(fontNameFilterRegexp, langFilterRegexp)
80 , fTestName(testName.c_str())
81 , fHintingMode(hintingMode) {
82 this->setBGColor(SK_ColorWHITE);
83 }
84
85 protected:
getName() const86 SkString getName() const override {
87 SkString testName = SkStringPrintf("fontations_compare_ft_%s", fTestName.c_str());
88 switch (fHintingMode) {
89 case SkFontHinting::kNormal: {
90 testName.append("_hint_normal");
91 break;
92 }
93 case SkFontHinting::kSlight: {
94 testName.append("_hint_slight");
95 break;
96 }
97 case SkFontHinting::kFull: {
98 testName.append("_hint_full");
99 break;
100 }
101 case SkFontHinting::kNone: {
102 testName.append("_hint_none");
103 break;
104 }
105 }
106 return testName;
107 }
108
getISize()109 SkISize getISize() override {
110 TestFontDataProvider::TestSet testSet;
111 fTestDataIterator.rewind();
112 fTestDataIterator.next(&testSet);
113
114 return SkISize::Make(kGmWidth,
115 testSet.langSamples.size() * kFontSize * kLangYIncrementScale + 100);
116 }
117
onDraw(SkCanvas * canvas,SkString * errorMsg)118 DrawResult onDraw(SkCanvas* canvas, SkString* errorMsg) override {
119 SkPaint paint;
120 paint.setColor(SK_ColorBLACK);
121
122 fTestDataIterator.rewind();
123 TestFontDataProvider::TestSet testSet;
124
125 while (fTestDataIterator.next(&testSet)) {
126 sk_sp<SkTypeface> testTypeface = SkTypeface_Make_Fontations(
127 SkStream::MakeFromFile(testSet.fontFilename.c_str()), SkFontArguments());
128 sk_sp<SkTypeface> ftTypeface = SkTypeface_FreeType::MakeFromStream(
129 SkStream::MakeFromFile(testSet.fontFilename.c_str()), SkFontArguments());
130
131 if (!testTypeface || !ftTypeface) {
132 *errorMsg = "Unable to initialize typeface.";
133 return DrawResult::kSkip;
134 }
135
136 auto configureFont = [this](SkFont& font) {
137 font.setSize(kFontSize);
138 font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
139 font.setSubpixel(true);
140 font.setHinting(fHintingMode);
141 };
142
143 SkFont font(testTypeface);
144 configureFont(font);
145
146 SkFont ftFont(ftTypeface);
147 configureFont(ftFont);
148 enum class DrawPhase { Fontations, FreeType, Comparison };
149
150 SkRect maxBounds = SkRect::MakeEmpty();
151 for (auto phase : {DrawPhase::Fontations, DrawPhase::FreeType, DrawPhase::Comparison}) {
152 SkScalar yCoord = kFontSize * 1.5f;
153
154 for (auto& langEntry : testSet.langSamples) {
155 auto shapeAndDrawToCanvas = [canvas, paint, langEntry](const SkFont& font,
156 SkPoint coord) {
157 std::string testString(langEntry.sampleShort.c_str(),
158 langEntry.sampleShort.size());
159 SkTextBlobBuilderRunHandler textBlobBuilder(testString.c_str(), {0, 0});
160 std::unique_ptr<SkShaper> shaper = SkShaper::Make();
161 shaper->shape(testString.c_str(),
162 testString.size(),
163 font,
164 true,
165 999999, /* Don't linebreak. */
166 &textBlobBuilder);
167 sk_sp<const SkTextBlob> blob = textBlobBuilder.makeBlob();
168 canvas->drawTextBlob(blob.get(), coord.x(), coord.y(), paint);
169 return blob->bounds();
170 };
171
172 auto roundToDevicePixels = [canvas](SkPoint& point) {
173 SkMatrix ctm = canvas->getLocalToDeviceAs3x3();
174 SkPoint mapped = ctm.mapPoint(point);
175 SkPoint mappedRounded =
176 SkPoint::Make(roundf(mapped.x()), roundf(mapped.y()));
177 SkMatrix inverse;
178 bool inverseExists = ctm.invert(&inverse);
179 SkASSERT(inverseExists);
180 if (inverseExists) {
181 point = inverse.mapPoint(mappedRounded);
182 }
183 };
184
185 auto fontationsCoord = [yCoord, roundToDevicePixels]() {
186 SkPoint fontationsCoord = SkPoint::Make(kMargin, yCoord);
187 roundToDevicePixels(fontationsCoord);
188 return fontationsCoord;
189 };
190
191 auto freetypeCoord = [yCoord, maxBounds, roundToDevicePixels]() {
192 SkPoint freetypeCoord = SkPoint::Make(
193 2 * kMargin + maxBounds.left() + maxBounds.width(), yCoord);
194 roundToDevicePixels(freetypeCoord);
195 return freetypeCoord;
196 };
197
198 switch (phase) {
199 case DrawPhase::Fontations: {
200 SkRect boundsFontations = shapeAndDrawToCanvas(font, fontationsCoord());
201 /* Determine maximum of column width across all language samples. */
202 boundsFontations.roundOut();
203 maxBounds.join(boundsFontations);
204 break;
205 }
206 case DrawPhase::FreeType: {
207 shapeAndDrawToCanvas(ftFont, freetypeCoord());
208 break;
209 }
210 case DrawPhase::Comparison: {
211 /* Read back pixels from equally sized rectangles from the space in
212 * SkCanvas where Fontations and FreeType sample texts were drawn,
213 * compare them using pixel comparisons similar to SkDiff, draw a
214 * comparison as faint pixel differences, and as an amplified
215 * visualization in which each differing pixel is drawn as white. */
216 SkPoint fontationsOrigin = fontationsCoord();
217 SkPoint freetypeOrigin = freetypeCoord();
218 SkRect fontationsBBox(maxBounds.makeOffset(fontationsOrigin));
219 SkRect freetypeBBox(maxBounds.makeOffset(freetypeOrigin));
220
221 SkMatrix ctm = canvas->getLocalToDeviceAs3x3();
222 ctm.mapRect(&fontationsBBox, fontationsBBox);
223 ctm.mapRect(&freetypeBBox, freetypeBBox);
224
225 SkIRect fontationsIBox(fontationsBBox.roundOut());
226 SkIRect freetypeIBox(freetypeBBox.roundOut());
227
228 SkISize pixelDimensions(fontationsIBox.size());
229 SkImageInfo canvasImageInfo = canvas->imageInfo();
230 SkImageInfo diffImageInfo =
231 SkImageInfo::Make(pixelDimensions,
232 SkColorType::kN32_SkColorType,
233 SkAlphaType::kUnpremul_SkAlphaType);
234
235 SkBitmap diffBitmap, highlightDiffBitmap;
236 diffBitmap.allocPixels(diffImageInfo, 0);
237 highlightDiffBitmap.allocPixels(diffImageInfo, 0);
238
239 // Workaround OveridePaintFilterCanvas limitations
240 // by getting pixel access through peekPixels()
241 // instead of readPixels(). Then use same pixmap to
242 // later write back the comparison results.
243 SkPixmap canvasPixmap;
244 if (!canvas->peekPixels(&canvasPixmap)) {
245 break;
246 }
247
248 SkPixmap fontationsPixmap, freetypePixmap;
249 if (!canvasPixmap.extractSubset(&fontationsPixmap, fontationsIBox) ||
250 !canvasPixmap.extractSubset(&freetypePixmap, freetypeIBox)) {
251 break;
252 }
253
254 comparePixels(fontationsPixmap,
255 freetypePixmap,
256 &diffBitmap,
257 &highlightDiffBitmap);
258
259 /* Place comparison results as two extra columns, shift up to account
260 for placement of rectangles vs. SkTextBlobs (baseline shift). */
261 SkPoint comparisonCoord = ctm.mapPoint(SkPoint::Make(
262 3 * kMargin + maxBounds.width() * 2, yCoord + maxBounds.top()));
263 SkPoint whiteCoord = ctm.mapPoint(SkPoint::Make(
264 4 * kMargin + maxBounds.width() * 3, yCoord + maxBounds.top()));
265
266 SkSurfaceProps canvasSurfaceProps = canvas->getBaseProps();
267 sk_sp<SkSurface> writeBackSurface =
268 SkSurfaces::WrapPixels(canvasPixmap, &canvasSurfaceProps);
269
270 writeBackSurface->writePixels(
271 diffBitmap, comparisonCoord.x(), comparisonCoord.y());
272 writeBackSurface->writePixels(
273 highlightDiffBitmap, whiteCoord.x(), whiteCoord.y());
274 break;
275 }
276 }
277
278 yCoord += font.getSize() * kLangYIncrementScale;
279 }
280 }
281 }
282
283 return DrawResult::kOk;
284 }
285
286 private:
287 using INHERITED = GM;
288
289 TestFontDataProvider fTestDataIterator;
290 SkString fTestName;
291 SkFontHinting fHintingMode;
292 sk_sp<SkTypeface> fReportTypeface;
293 std::unique_ptr<SkFontArguments::VariationPosition::Coordinate[]> fCoordinates;
294 };
295
296 DEF_GM(return new FontationsFtCompareGM(
297 "NotoSans",
298 "Noto Sans",
299 "en_Latn|es_Latn|pt_Latn|id_Latn|ru_Cyrl|fr_Latn|tr_Latn|vi_Latn|de_"
300 "Latn|it_Latn|pl_Latn|nl_Latn|uk_Cyrl|gl_Latn|ro_Latn|cs_Latn|hu_Latn|"
301 "el_Grek|se_Latn|da_Latn|bg_Latn|sk_Latn|fi_Latn|bs_Latn|ca_Latn|no_"
302 "Latn|sr_Latn|sr_Cyrl|lt_Latn|hr_Latn|sl_Latn|uz_Latn|uz_Cyrl|lv_Latn|"
303 "et_Latn|az_Latn|az_Cyrl|la_Latn|tg_Latn|tg_Cyrl|sw_Latn|mn_Cyrl|kk_"
304 "Latn|kk_Cyrl|sq_Latn|af_Latn|ha_Latn|ky_Cyrl"));
305
306 DEF_GM(return new FontationsFtCompareGM(
307 "NotoSans",
308 "Noto Sans",
309 "en_Latn|es_Latn|pt_Latn|id_Latn|ru_Cyrl|fr_Latn|tr_Latn|vi_Latn|de_"
310 "Latn|it_Latn|pl_Latn|nl_Latn|uk_Cyrl|gl_Latn|ro_Latn|cs_Latn|hu_Latn|"
311 "el_Grek|se_Latn|da_Latn|bg_Latn|sk_Latn|fi_Latn|bs_Latn|ca_Latn|no_"
312 "Latn|sr_Latn|sr_Cyrl|lt_Latn|hr_Latn|sl_Latn|uz_Latn|uz_Cyrl|lv_Latn|"
313 "et_Latn|az_Latn|az_Cyrl|la_Latn|tg_Latn|tg_Cyrl|sw_Latn|mn_Cyrl|kk_"
314 "Latn|kk_Cyrl|sq_Latn|af_Latn|ha_Latn|ky_Cyrl",
315 SkFontHinting::kSlight));
316
317 DEF_GM(return new FontationsFtCompareGM(
318 "NotoSans",
319 "Noto Sans",
320 "en_Latn|es_Latn|pt_Latn|id_Latn|ru_Cyrl|fr_Latn|tr_Latn|vi_Latn|de_"
321 "Latn|it_Latn|pl_Latn|nl_Latn|uk_Cyrl|gl_Latn|ro_Latn|cs_Latn|hu_Latn|"
322 "el_Grek|se_Latn|da_Latn|bg_Latn|sk_Latn|fi_Latn|bs_Latn|ca_Latn|no_"
323 "Latn|sr_Latn|sr_Cyrl|lt_Latn|hr_Latn|sl_Latn|uz_Latn|uz_Cyrl|lv_Latn|"
324 "et_Latn|az_Latn|az_Cyrl|la_Latn|tg_Latn|tg_Cyrl|sw_Latn|mn_Cyrl|kk_"
325 "Latn|kk_Cyrl|sq_Latn|af_Latn|ha_Latn|ky_Cyrl",
326 SkFontHinting::kNormal));
327
328 DEF_GM(return new FontationsFtCompareGM("NotoSans_Deva",
329 "Noto Sans Devanagari",
330 "hi_Deva|mr_Deva"));
331
332 DEF_GM(return new FontationsFtCompareGM(
333 "NotoSans_Deva", "Noto Sans Devanagari", "hi_Deva|mr_Deva", SkFontHinting::kSlight));
334
335 DEF_GM(return new FontationsFtCompareGM(
336 "NotoSans_Deva", "Noto Sans Devanagari", "hi_Deva|mr_Deva", SkFontHinting::kNormal));
337
338 DEF_GM(return new FontationsFtCompareGM("NotoSans_ar_Arab",
339 "Noto Sans Arabic",
340 "ar_Arab|uz_Arab|kk_Arab|ky_Arab"));
341
342 DEF_GM(return new FontationsFtCompareGM("NotoSans_ar_Arab",
343 "Noto Sans Arabic",
344 "ar_Arab|uz_Arab|kk_Arab|ky_Arab",
345 SkFontHinting::kSlight));
346
347 DEF_GM(return new FontationsFtCompareGM("NotoSans_ar_Arab",
348 "Noto Sans Arabic",
349 "ar_Arab|uz_Arab|kk_Arab|ky_Arab",
350 SkFontHinting::kNormal));
351
352 DEF_GM(return new FontationsFtCompareGM("NotoSans_Beng", "Noto Sans Bengali", "bn_Beng"));
353
354 DEF_GM(return new FontationsFtCompareGM("NotoSans_Jpan", "Noto Sans JP", "ja_Jpan"));
355
356 DEF_GM(return new FontationsFtCompareGM("NotoSans_Thai", "Noto Sans Thai", "th_Thai"));
357
358 DEF_GM(return new FontationsFtCompareGM("NotoSans_Hans", "Noto Sans SC", "zh_Hans"));
359
360 DEF_GM(return new FontationsFtCompareGM("NotoSans_Hant", "Noto Sans TC", "zh_Hant"));
361
362 DEF_GM(return new FontationsFtCompareGM("NotoSans_Kore", "Noto Sans KR", "ko_Kore"));
363
364 DEF_GM(return new FontationsFtCompareGM("NotoSans_Taml", "Noto Sans Tamil", "ta_Taml"));
365
366 DEF_GM(return new FontationsFtCompareGM("NotoSans_Newa", "Noto Sans Newa", "new_Newa"));
367
368 DEF_GM(return new FontationsFtCompareGM("NotoSans_Knda", "Noto Sans Kannada", "kn_Knda"));
369
370 DEF_GM(return new FontationsFtCompareGM("NotoSans_Tglg", "Noto Sans Tagalog", "fil_Tglg"));
371
372 DEF_GM(return new FontationsFtCompareGM("NotoSans_Telu", "Noto Sans Telugu", "te_Telu"));
373
374 DEF_GM(return new FontationsFtCompareGM("NotoSans_Gujr", "Noto Sans Gujarati", "gu_Gujr"));
375
376 DEF_GM(return new FontationsFtCompareGM("NotoSans_Geor", "Noto Sans Georgian", "ka_Geor"));
377
378 DEF_GM(return new FontationsFtCompareGM("NotoSans_Mlym", "Noto Sans Malayalam", "ml_Mlym"));
379
380 DEF_GM(return new FontationsFtCompareGM("NotoSans_Khmr", "Noto Sans Khmer", "km_Khmr"));
381
382 DEF_GM(return new FontationsFtCompareGM("NotoSans_Sinh", "Noto Sans Sinhala", "si_Sinh"));
383
384 DEF_GM(return new FontationsFtCompareGM("NotoSans_Mymr", "Noto Sans Myanmar", "my_Mymr"));
385
386 DEF_GM(return new FontationsFtCompareGM("NotoSans_Java", "Noto Sans Javanese", "jv_Java"));
387
388 DEF_GM(return new FontationsFtCompareGM("NotoSans_Mong", "Noto Sans Mongolian", "mn_Mong"));
389
390 DEF_GM(return new FontationsFtCompareGM("NotoSans_Armn", "Noto Sans Armenian", "hy_Armn"));
391
392 DEF_GM(return new FontationsFtCompareGM("NotoSans_Elba", "Noto Sans Elbasan", "sq_Elba"));
393
394 DEF_GM(return new FontationsFtCompareGM("NotoSans_Vith", "Noto Sans Vithkuqi", "sq_Vith"));
395
396 DEF_GM(return new FontationsFtCompareGM("NotoSans_Guru", "Noto Sans Gurmukhi", "pa_Guru"));
397
398 } // namespace skiagm
399