Font features

Change-Id: I8ae44d715fd2fa195c209c8ecb514db3e9644c31
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/259100
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
diff --git a/modules/skparagraph/include/TextStyle.h b/modules/skparagraph/include/TextStyle.h
index 387bf82..88223a5 100644
--- a/modules/skparagraph/include/TextStyle.h
+++ b/modules/skparagraph/include/TextStyle.h
@@ -89,6 +89,16 @@
   kMiddle,
 };
 
+struct FontFeature {
+    FontFeature(const SkString name, int value) : fName(name), fValue(value) { }
+    FontFeature(const FontFeature& other) : fName(other.fName), fValue(other.fValue) { }
+    bool operator==(const FontFeature& other) const {
+        return fName == other.fName && fValue == other.fValue;
+    }
+    SkString fName;
+    int fValue;
+};
+
 struct PlaceholderStyle {
     PlaceholderStyle() { }
     PlaceholderStyle(SkScalar width, SkScalar height, PlaceholderAlignment alignment,
@@ -170,6 +180,13 @@
     void addShadow(TextShadow shadow) { fTextShadows.emplace_back(shadow); }
     void resetShadows() { fTextShadows.clear(); }
 
+    // Font features
+    size_t getFontFeatureNumber() const { return fFontFeatures.size(); }
+    std::vector<FontFeature> getFontFeatures() const { return fFontFeatures; }
+    void addFontFeature(const SkString& fontFeature, int value)
+        { fFontFeatures.emplace_back(fontFeature, value); }
+    void resetFontFeatures() { fFontFeatures.clear(); }
+
     SkScalar getFontSize() const { return fFontSize; }
     void setFontSize(SkScalar size) { fFontSize = size; }
 
@@ -231,6 +248,8 @@
 
     sk_sp<SkTypeface> fTypeface;
     bool fIsPlaceholder;
+
+    std::vector<FontFeature> fFontFeatures;
 };
 
 typedef size_t TextIndex;
diff --git a/modules/skparagraph/src/OneLineShaper.cpp b/modules/skparagraph/src/OneLineShaper.cpp
index f777ac8..39fa231 100644
--- a/modules/skparagraph/src/OneLineShaper.cpp
+++ b/modules/skparagraph/src/OneLineShaper.cpp
@@ -339,6 +339,23 @@
 void OneLineShaper::iterateThroughFontStyles(SkSpan<Block> styleSpan,
                                              const ShapeSingleFontVisitor& visitor) {
     Block combinedBlock;
+    SkTArray<SkShaper::Feature> features;
+
+    auto addFeatures = [&features](const Block& block) {
+        for (auto& ff : block.fStyle.getFontFeatures()) {
+            if (ff.fName.size() != 4) {
+                SkDEBUGF("Incorrect font feature: %s=%d\n", ff.fName.c_str(), ff.fValue);
+                continue;
+            }
+            SkShaper::Feature feature = {
+                SkSetFourByteTag(ff.fName[0], ff.fName[1], ff.fName[2], ff.fName[3]),
+                SkToU32(ff.fValue),
+                block.fRange.start,
+                block.fRange.end
+            };
+            features.emplace_back(feature);
+        }
+    };
 
     for (auto& block : styleSpan) {
         SkASSERT(combinedBlock.fRange.width() == 0 ||
@@ -347,17 +364,20 @@
         if (!combinedBlock.fRange.empty()) {
             if (block.fStyle.matchOneAttribute(StyleType::kFont, combinedBlock.fStyle)) {
                 combinedBlock.add(block.fRange);
+                addFeatures(block);
                 continue;
             }
             // Resolve all characters in the block for this style
-            visitor(combinedBlock);
+            visitor(combinedBlock, features);
         }
 
         combinedBlock.fRange = block.fRange;
         combinedBlock.fStyle = block.fStyle;
+        features.reset();
+        addFeatures(block);
     }
 
-    visitor(combinedBlock);
+    visitor(combinedBlock, features);
 #ifdef SK_DEBUG
     //printState();
 #endif
@@ -462,7 +482,7 @@
 
         iterateThroughFontStyles(styleSpan,
                 [this, &shaper, textDirection, limitlessWidth, &advanceX]
-                (Block block) {
+                (Block block, SkTArray<SkShaper::Feature> features) {
             auto blockSpan = SkSpan<Block>(&block, 1);
 
             // Start from the beginning (hoping that it's a simple case one block - one run)
@@ -499,6 +519,7 @@
                     fCurrentText = unresolvedRange;
                     shaper->shape(unresolvedText.begin(), unresolvedText.size(),
                             fontIter, *bidi,*script, lang,
+                            features.data(), features.size(),
                             limitlessWidth, this);
 
                     // Take off the queue the block we tried to resolved -
diff --git a/modules/skparagraph/src/OneLineShaper.h b/modules/skparagraph/src/OneLineShaper.h
index e25c085..81b34cb 100644
--- a/modules/skparagraph/src/OneLineShaper.h
+++ b/modules/skparagraph/src/OneLineShaper.h
@@ -57,7 +57,7 @@
             std::function<SkScalar(SkSpan<const char>, SkSpan<Block>, SkScalar&, TextIndex)>;
     bool iterateThroughShapingRegions(const ShapeVisitor& shape);
 
-    using ShapeSingleFontVisitor = std::function<void(Block)>;
+    using ShapeSingleFontVisitor = std::function<void(Block, SkTArray<SkShaper::Feature>)>;
     void iterateThroughFontStyles(SkSpan<Block> styleSpan, const ShapeSingleFontVisitor& visitor);
 
     using TypefaceVisitor = std::function<bool(sk_sp<SkTypeface> typeface)>;
diff --git a/modules/skparagraph/src/TextStyle.cpp b/modules/skparagraph/src/TextStyle.cpp
index 4fc619a..5582303 100644
--- a/modules/skparagraph/src/TextStyle.cpp
+++ b/modules/skparagraph/src/TextStyle.cpp
@@ -40,6 +40,7 @@
     fForeground = other.fForeground;
     fHeightOverride = other.fHeightOverride;
     fIsPlaceholder = placeholder;
+    fFontFeatures = other.fFontFeatures;
 }
 
 bool TextStyle::equals(const TextStyle& other) const {
@@ -89,6 +90,14 @@
             return false;
         }
     }
+    if (fFontFeatures.size() != other.fFontFeatures.size()) {
+        return false;
+    }
+    for (size_t i = 0; i < fFontFeatures.size(); ++i) {
+        if (!(fFontFeatures[i] == other.fFontFeatures[i])) {
+            return false;
+        }
+    }
 
     return true;
 }
diff --git a/tests/SkParagraphTest.cpp b/tests/SkParagraphTest.cpp
index ffce0de..395dcae 100644
--- a/tests/SkParagraphTest.cpp
+++ b/tests/SkParagraphTest.cpp
@@ -4482,6 +4482,56 @@
     }
 }
 
+DEF_TEST(SkParagraph_FontFeaturesParagraph, reporter) {
+    sk_sp<ResourceFontCollection> fontCollection = sk_make_sp<ResourceFontCollection>();
+    if (!fontCollection->fontsFound()) return;
+    TestCanvas canvas("SkParagraph_FontFeaturesParagraph.png");
+
+    const char* text = "12ab\n";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontStyle(SkFontStyle::Italic());
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+
+    text_style.addFontFeature(SkString("tnum"), 1);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    text_style.resetFontFeatures();
+    text_style.addFontFeature(SkString("tnum"), 0);
+    text_style.addFontFeature(SkString("pnum"), 1);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    builder.pop();
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+
+    paragraph->paint(canvas.get(), 10.0, 15.0);
+
+    auto impl = static_cast<ParagraphImpl*>(paragraph.get());
+    REPORTER_ASSERT(reporter, paragraph->lineNumber() == 3ull);
+
+    auto& tnum_line = impl->lines()[0];
+    auto& pnum_line = impl->lines()[1];
+
+    REPORTER_ASSERT(reporter, tnum_line.clusters().width() == 4ull);
+    REPORTER_ASSERT(reporter, pnum_line.clusters().width() == 4ull);
+    // Tabular numbers should have equal widths.
+    REPORTER_ASSERT(reporter, impl->clusters()[0].width() == impl->clusters()[1].width());
+    // Proportional numbers should have variable widths.
+    REPORTER_ASSERT(reporter, impl->clusters()[5].width() != impl->clusters()[6].width());
+    // Alphabetic characters should be unaffected.
+    REPORTER_ASSERT(reporter, impl->clusters()[2].width() == impl->clusters()[7].width());
+}
+
 // Not in Minikin
 DEF_TEST(SkParagraph_WhitespacesInMultipleFonts, reporter) {
     sk_sp<ResourceFontCollection> fontCollection = sk_make_sp<ResourceFontCollection>();