/* * Copyright 2023 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "gm/gm.h" #include "include/core/SkBitmap.h" #include "include/core/SkCanvas.h" #include "include/core/SkColorPriv.h" #include "include/core/SkData.h" #include "include/core/SkFont.h" #include "include/core/SkFontTypes.h" #include "include/core/SkRefCnt.h" #include "include/core/SkStream.h" #include "include/core/SkString.h" #include "include/core/SkSurface.h" #include "include/core/SkTypeface.h" #include "include/ports/SkTypeface_fontations.h" #include "modules/skshaper/include/SkShaper.h" #include "src/ports/SkTypeface_FreeType.h" #include "tools/Resources.h" #include "tools/TestFontDataProvider.h" namespace skiagm { namespace { constexpr int kGmWidth = 1000; constexpr int kMargin = 30; constexpr float kFontSize = 24; constexpr float kLangYIncrementScale = 1.9; /** Compare bitmap A and B, in this case originating from text rendering results with FreeType and * Fontations + Skia path rendering, compute individual pixel differences for the rectangles that * must match in size. Produce a highlighted difference bitmap, in which any pixel becomes white for * which a difference was determined. */ void comparePixels(const SkPixmap& pixmapA, const SkPixmap& pixmapB, SkBitmap* outPixelDiffBitmap, SkBitmap* outHighlightDiffBitmap) { if (pixmapA.dimensions() != pixmapB.dimensions()) { return; } if (pixmapA.dimensions() != outPixelDiffBitmap->dimensions()) { return; } SkISize dimensions = pixmapA.dimensions(); for (int32_t x = 0; x < dimensions.fWidth; x++) { for (int32_t y = 0; y < dimensions.fHeight; y++) { SkColor c0 = pixmapA.getColor(x, y); SkColor c1 = pixmapB.getColor(x, y); int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1); int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1); int db = SkGetPackedB32(c0) - SkGetPackedB32(c1); *(outPixelDiffBitmap->getAddr32(x, y)) = SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db)); if (dr != 0 || dg != 0 || db != 0) { *(outHighlightDiffBitmap->getAddr32(x, y)) = SK_ColorWHITE; } else { *(outHighlightDiffBitmap->getAddr32(x, y)) = SK_ColorBLACK; } } } } } // namespace class FontationsFtCompareGM : public GM { public: FontationsFtCompareGM(std::string testName, std::string fontNameFilterRegexp, std::string langFilterRegexp, SkFontHinting hintingMode = SkFontHinting::kNone) : fTestDataIterator(fontNameFilterRegexp, langFilterRegexp) , fTestName(testName.c_str()) , fHintingMode(hintingMode) { this->setBGColor(SK_ColorWHITE); } protected: SkString getName() const override { SkString testName = SkStringPrintf("fontations_compare_ft_%s", fTestName.c_str()); switch (fHintingMode) { case SkFontHinting::kNormal: { testName.append("_hint_normal"); break; } case SkFontHinting::kSlight: { testName.append("_hint_slight"); break; } case SkFontHinting::kFull: { testName.append("_hint_full"); break; } case SkFontHinting::kNone: { testName.append("_hint_none"); break; } } return testName; } SkISize getISize() override { TestFontDataProvider::TestSet testSet; fTestDataIterator.rewind(); fTestDataIterator.next(&testSet); return SkISize::Make(kGmWidth, testSet.langSamples.size() * kFontSize * kLangYIncrementScale + 100); } DrawResult onDraw(SkCanvas* canvas, SkString* errorMsg) override { SkPaint paint; paint.setColor(SK_ColorBLACK); fTestDataIterator.rewind(); TestFontDataProvider::TestSet testSet; while (fTestDataIterator.next(&testSet)) { sk_sp testTypeface = SkTypeface_Make_Fontations( SkStream::MakeFromFile(testSet.fontFilename.c_str()), SkFontArguments()); sk_sp ftTypeface = SkTypeface_FreeType::MakeFromStream( SkStream::MakeFromFile(testSet.fontFilename.c_str()), SkFontArguments()); if (!testTypeface || !ftTypeface) { *errorMsg = "Unable to initialize typeface."; return DrawResult::kSkip; } auto configureFont = [this](SkFont& font) { font.setSize(kFontSize); font.setEdging(SkFont::Edging::kSubpixelAntiAlias); font.setSubpixel(true); font.setHinting(fHintingMode); }; SkFont font(testTypeface); configureFont(font); SkFont ftFont(ftTypeface); configureFont(ftFont); enum class DrawPhase { Fontations, FreeType, Comparison }; SkRect maxBounds = SkRect::MakeEmpty(); for (auto phase : {DrawPhase::Fontations, DrawPhase::FreeType, DrawPhase::Comparison}) { SkScalar yCoord = kFontSize * 1.5f; for (auto& langEntry : testSet.langSamples) { auto shapeAndDrawToCanvas = [canvas, paint, langEntry](const SkFont& font, SkPoint coord) { std::string testString(langEntry.sampleShort.c_str(), langEntry.sampleShort.size()); SkTextBlobBuilderRunHandler textBlobBuilder(testString.c_str(), {0, 0}); std::unique_ptr shaper = SkShaper::Make(); shaper->shape(testString.c_str(), testString.size(), font, true, 999999, /* Don't linebreak. */ &textBlobBuilder); sk_sp blob = textBlobBuilder.makeBlob(); canvas->drawTextBlob(blob.get(), coord.x(), coord.y(), paint); return blob->bounds(); }; auto roundToDevicePixels = [canvas](SkPoint& point) { SkMatrix ctm = canvas->getLocalToDeviceAs3x3(); SkPoint mapped = ctm.mapPoint(point); SkPoint mappedRounded = SkPoint::Make(roundf(mapped.x()), roundf(mapped.y())); SkMatrix inverse; bool inverseExists = ctm.invert(&inverse); SkASSERT(inverseExists); if (inverseExists) { point = inverse.mapPoint(mappedRounded); } }; auto fontationsCoord = [yCoord, roundToDevicePixels]() { SkPoint fontationsCoord = SkPoint::Make(kMargin, yCoord); roundToDevicePixels(fontationsCoord); return fontationsCoord; }; auto freetypeCoord = [yCoord, maxBounds, roundToDevicePixels]() { SkPoint freetypeCoord = SkPoint::Make( 2 * kMargin + maxBounds.left() + maxBounds.width(), yCoord); roundToDevicePixels(freetypeCoord); return freetypeCoord; }; switch (phase) { case DrawPhase::Fontations: { SkRect boundsFontations = shapeAndDrawToCanvas(font, fontationsCoord()); /* Determine maximum of column width across all language samples. */ boundsFontations.roundOut(); maxBounds.join(boundsFontations); break; } case DrawPhase::FreeType: { shapeAndDrawToCanvas(ftFont, freetypeCoord()); break; } case DrawPhase::Comparison: { /* Read back pixels from equally sized rectangles from the space in * SkCanvas where Fontations and FreeType sample texts were drawn, * compare them using pixel comparisons similar to SkDiff, draw a * comparison as faint pixel differences, and as an amplified * visualization in which each differing pixel is drawn as white. */ SkPoint fontationsOrigin = fontationsCoord(); SkPoint freetypeOrigin = freetypeCoord(); SkRect fontationsBBox(maxBounds.makeOffset(fontationsOrigin)); SkRect freetypeBBox(maxBounds.makeOffset(freetypeOrigin)); SkMatrix ctm = canvas->getLocalToDeviceAs3x3(); ctm.mapRect(&fontationsBBox, fontationsBBox); ctm.mapRect(&freetypeBBox, freetypeBBox); SkIRect fontationsIBox(fontationsBBox.roundOut()); SkIRect freetypeIBox(freetypeBBox.roundOut()); SkISize pixelDimensions(fontationsIBox.size()); SkImageInfo canvasImageInfo = canvas->imageInfo(); SkImageInfo diffImageInfo = SkImageInfo::Make(pixelDimensions, SkColorType::kN32_SkColorType, SkAlphaType::kUnpremul_SkAlphaType); SkBitmap diffBitmap, highlightDiffBitmap; diffBitmap.allocPixels(diffImageInfo, 0); highlightDiffBitmap.allocPixels(diffImageInfo, 0); // Workaround OveridePaintFilterCanvas limitations // by getting pixel access through peekPixels() // instead of readPixels(). Then use same pixmap to // later write back the comparison results. SkPixmap canvasPixmap; if (!canvas->peekPixels(&canvasPixmap)) { break; } SkPixmap fontationsPixmap, freetypePixmap; if (!canvasPixmap.extractSubset(&fontationsPixmap, fontationsIBox) || !canvasPixmap.extractSubset(&freetypePixmap, freetypeIBox)) { break; } comparePixels(fontationsPixmap, freetypePixmap, &diffBitmap, &highlightDiffBitmap); /* Place comparison results as two extra columns, shift up to account for placement of rectangles vs. SkTextBlobs (baseline shift). */ SkPoint comparisonCoord = ctm.mapPoint(SkPoint::Make( 3 * kMargin + maxBounds.width() * 2, yCoord + maxBounds.top())); SkPoint whiteCoord = ctm.mapPoint(SkPoint::Make( 4 * kMargin + maxBounds.width() * 3, yCoord + maxBounds.top())); SkSurfaceProps canvasSurfaceProps = canvas->getBaseProps(); sk_sp writeBackSurface = SkSurfaces::WrapPixels(canvasPixmap, &canvasSurfaceProps); writeBackSurface->writePixels( diffBitmap, comparisonCoord.x(), comparisonCoord.y()); writeBackSurface->writePixels( highlightDiffBitmap, whiteCoord.x(), whiteCoord.y()); break; } } yCoord += font.getSize() * kLangYIncrementScale; } } } return DrawResult::kOk; } private: using INHERITED = GM; TestFontDataProvider fTestDataIterator; SkString fTestName; SkFontHinting fHintingMode; sk_sp fReportTypeface; std::unique_ptr fCoordinates; }; DEF_GM(return new FontationsFtCompareGM( "NotoSans", "Noto Sans", "en_Latn|es_Latn|pt_Latn|id_Latn|ru_Cyrl|fr_Latn|tr_Latn|vi_Latn|de_" "Latn|it_Latn|pl_Latn|nl_Latn|uk_Cyrl|gl_Latn|ro_Latn|cs_Latn|hu_Latn|" "el_Grek|se_Latn|da_Latn|bg_Latn|sk_Latn|fi_Latn|bs_Latn|ca_Latn|no_" "Latn|sr_Latn|sr_Cyrl|lt_Latn|hr_Latn|sl_Latn|uz_Latn|uz_Cyrl|lv_Latn|" "et_Latn|az_Latn|az_Cyrl|la_Latn|tg_Latn|tg_Cyrl|sw_Latn|mn_Cyrl|kk_" "Latn|kk_Cyrl|sq_Latn|af_Latn|ha_Latn|ky_Cyrl")); DEF_GM(return new FontationsFtCompareGM( "NotoSans", "Noto Sans", "en_Latn|es_Latn|pt_Latn|id_Latn|ru_Cyrl|fr_Latn|tr_Latn|vi_Latn|de_" "Latn|it_Latn|pl_Latn|nl_Latn|uk_Cyrl|gl_Latn|ro_Latn|cs_Latn|hu_Latn|" "el_Grek|se_Latn|da_Latn|bg_Latn|sk_Latn|fi_Latn|bs_Latn|ca_Latn|no_" "Latn|sr_Latn|sr_Cyrl|lt_Latn|hr_Latn|sl_Latn|uz_Latn|uz_Cyrl|lv_Latn|" "et_Latn|az_Latn|az_Cyrl|la_Latn|tg_Latn|tg_Cyrl|sw_Latn|mn_Cyrl|kk_" "Latn|kk_Cyrl|sq_Latn|af_Latn|ha_Latn|ky_Cyrl", SkFontHinting::kSlight)); DEF_GM(return new FontationsFtCompareGM( "NotoSans", "Noto Sans", "en_Latn|es_Latn|pt_Latn|id_Latn|ru_Cyrl|fr_Latn|tr_Latn|vi_Latn|de_" "Latn|it_Latn|pl_Latn|nl_Latn|uk_Cyrl|gl_Latn|ro_Latn|cs_Latn|hu_Latn|" "el_Grek|se_Latn|da_Latn|bg_Latn|sk_Latn|fi_Latn|bs_Latn|ca_Latn|no_" "Latn|sr_Latn|sr_Cyrl|lt_Latn|hr_Latn|sl_Latn|uz_Latn|uz_Cyrl|lv_Latn|" "et_Latn|az_Latn|az_Cyrl|la_Latn|tg_Latn|tg_Cyrl|sw_Latn|mn_Cyrl|kk_" "Latn|kk_Cyrl|sq_Latn|af_Latn|ha_Latn|ky_Cyrl", SkFontHinting::kNormal)); DEF_GM(return new FontationsFtCompareGM("NotoSans_Deva", "Noto Sans Devanagari", "hi_Deva|mr_Deva")); DEF_GM(return new FontationsFtCompareGM( "NotoSans_Deva", "Noto Sans Devanagari", "hi_Deva|mr_Deva", SkFontHinting::kSlight)); DEF_GM(return new FontationsFtCompareGM( "NotoSans_Deva", "Noto Sans Devanagari", "hi_Deva|mr_Deva", SkFontHinting::kNormal)); DEF_GM(return new FontationsFtCompareGM("NotoSans_ar_Arab", "Noto Sans Arabic", "ar_Arab|uz_Arab|kk_Arab|ky_Arab")); DEF_GM(return new FontationsFtCompareGM("NotoSans_ar_Arab", "Noto Sans Arabic", "ar_Arab|uz_Arab|kk_Arab|ky_Arab", SkFontHinting::kSlight)); DEF_GM(return new FontationsFtCompareGM("NotoSans_ar_Arab", "Noto Sans Arabic", "ar_Arab|uz_Arab|kk_Arab|ky_Arab", SkFontHinting::kNormal)); DEF_GM(return new FontationsFtCompareGM("NotoSans_Beng", "Noto Sans Bengali", "bn_Beng")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Jpan", "Noto Sans JP", "ja_Jpan")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Thai", "Noto Sans Thai", "th_Thai")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Hans", "Noto Sans SC", "zh_Hans")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Hant", "Noto Sans TC", "zh_Hant")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Kore", "Noto Sans KR", "ko_Kore")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Taml", "Noto Sans Tamil", "ta_Taml")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Newa", "Noto Sans Newa", "new_Newa")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Knda", "Noto Sans Kannada", "kn_Knda")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Tglg", "Noto Sans Tagalog", "fil_Tglg")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Telu", "Noto Sans Telugu", "te_Telu")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Gujr", "Noto Sans Gujarati", "gu_Gujr")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Geor", "Noto Sans Georgian", "ka_Geor")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Mlym", "Noto Sans Malayalam", "ml_Mlym")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Khmr", "Noto Sans Khmer", "km_Khmr")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Sinh", "Noto Sans Sinhala", "si_Sinh")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Mymr", "Noto Sans Myanmar", "my_Mymr")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Java", "Noto Sans Javanese", "jv_Java")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Mong", "Noto Sans Mongolian", "mn_Mong")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Armn", "Noto Sans Armenian", "hy_Armn")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Elba", "Noto Sans Elbasan", "sq_Elba")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Vith", "Noto Sans Vithkuqi", "sq_Vith")); DEF_GM(return new FontationsFtCompareGM("NotoSans_Guru", "Noto Sans Gurmukhi", "pa_Guru")); } // namespace skiagm