blob: a043af828debb7ef87030e5ad9c8404718b3fd16 [file] [log] [blame]
/*
* 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<SkTypeface> testTypeface = SkTypeface_Make_Fontations(
SkStream::MakeFromFile(testSet.fontFilename.c_str()), SkFontArguments());
sk_sp<SkTypeface> 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<SkShaper> shaper = SkShaper::Make();
shaper->shape(testString.c_str(),
testString.size(),
font,
true,
999999, /* Don't linebreak. */
&textBlobBuilder);
sk_sp<const SkTextBlob> 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<SkSurface> 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<SkTypeface> fReportTypeface;
std::unique_ptr<SkFontArguments::VariationPosition::Coordinate[]> 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