Placeholders implementation + unit tests

Change-Id: I4e87a6927cbd6ef087b72aaa4b2155c1859589b3
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/235677
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
diff --git a/modules/skparagraph/include/Paragraph.h b/modules/skparagraph/include/Paragraph.h
index f9a575a..98e9899 100644
--- a/modules/skparagraph/include/Paragraph.h
+++ b/modules/skparagraph/include/Paragraph.h
@@ -44,6 +44,8 @@
                                                   RectHeightStyle rectHeightStyle,
                                                   RectWidthStyle rectWidthStyle) = 0;
 
+    virtual std::vector<TextBox> GetRectsForPlaceholders() = 0;
+
     // Returns the index of the glyph that corresponds to the provided coordinate,
     // with the top left corner as the origin, and +y direction as down
     virtual PositionWithAffinity getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) = 0;
diff --git a/modules/skparagraph/include/ParagraphBuilder.h b/modules/skparagraph/include/ParagraphBuilder.h
index 388011c..6733a35 100644
--- a/modules/skparagraph/include/ParagraphBuilder.h
+++ b/modules/skparagraph/include/ParagraphBuilder.h
@@ -46,6 +46,11 @@
     // Converts to u16string before adding.
     virtual void addText(const char* text) = 0;
 
+    // Pushes the information required to leave an open space, where Flutter may
+    // draw a custom placeholder into.
+    // Internally, this method adds a single object replacement character (0xFFFC)
+    virtual void addPlaceholder(const PlaceholderStyle& placeholderStyle) = 0;
+
     virtual void setParagraphStyle(const ParagraphStyle& style) = 0;
 
     // Constructs a SkParagraph object that can be used to layout and paint the text to a SkCanvas.
diff --git a/modules/skparagraph/include/TextStyle.h b/modules/skparagraph/include/TextStyle.h
index 449626d..e0a8999 100644
--- a/modules/skparagraph/include/TextStyle.h
+++ b/modules/skparagraph/include/TextStyle.h
@@ -8,6 +8,7 @@
 #include "include/core/SkFontMetrics.h"
 #include "include/core/SkFontStyle.h"
 #include "include/core/SkPaint.h"
+#include "include/core/SkScalar.h"
 #include "modules/skparagraph/include/DartTypes.h"
 #include "modules/skparagraph/include/TextShadow.h"
 
@@ -59,9 +60,67 @@
     }
 };
 
+/// Where to vertically align the placeholder relative to the surrounding text.
+enum class PlaceholderAlignment {
+  /// Match the baseline of the placeholder with the baseline.
+  kBaseline,
+
+  /// Align the bottom edge of the placeholder with the baseline such that the
+  /// placeholder sits on top of the baseline.
+  kAboveBaseline,
+
+  /// Align the top edge of the placeholder with the baseline specified in
+  /// such that the placeholder hangs below the baseline.
+  kBelowBaseline,
+
+  /// Align the top edge of the placeholder with the top edge of the font.
+  /// When the placeholder is very tall, the extra space will hang from
+  /// the top and extend through the bottom of the line.
+  kTop,
+
+  /// Align the bottom edge of the placeholder with the top edge of the font.
+  /// When the placeholder is very tall, the extra space will rise from
+  /// the bottom and extend through the top of the line.
+  kBottom,
+
+  /// Align the middle of the placeholder with the middle of the text. When the
+  /// placeholder is very tall, the extra space will grow equally from
+  /// the top and bottom of the line.
+  kMiddle,
+};
+
+struct PlaceholderStyle {
+    PlaceholderStyle() { }
+    PlaceholderStyle(SkScalar width, SkScalar height, PlaceholderAlignment alignment,
+                     TextBaseline baseline, SkScalar offset)
+            : fWidth(width)
+            , fHeight(height)
+            , fAlignment(alignment)
+            , fBaseline(baseline)
+            , fBaselineOffset(offset) {}
+
+    SkScalar fWidth = 0;
+    SkScalar fHeight = 0;
+
+    PlaceholderAlignment fAlignment;
+
+    TextBaseline fBaseline;
+
+    // Distance from the top edge of the rect to the baseline position. This
+    // baseline will be aligned against the alphabetic baseline of the surrounding
+    // text.
+    //
+    // Positive values drop the baseline lower (positions the rect higher) and
+    // small or negative values will cause the rect to be positioned underneath
+    // the line. When baseline == height, the bottom edge of the rect will rest on
+    // the alphabetic baseline.
+    SkScalar fBaselineOffset = 0;
+};
+
 class TextStyle {
 public:
     TextStyle();
+    TextStyle(const TextStyle& other, bool placeholder);
     ~TextStyle() = default;
 
     bool equals(const TextStyle& other) const;
@@ -143,7 +202,11 @@
 
     void getFontMetrics(SkFontMetrics* metrics) const;
 
+    bool isPlaceholder() const { return fIsPlaceholder; }
+    void setPlaceholder() { fIsPlaceholder = true; }
+
 private:
+
     Decoration fDecoration;
 
     SkFontStyle fFontStyle;
@@ -167,6 +230,7 @@
     std::vector<TextShadow> fTextShadows;
 
     sk_sp<SkTypeface> fTypeface;
+    bool fIsPlaceholder;
 };
 
 typedef size_t TextIndex;
@@ -176,10 +240,13 @@
 
 struct Block {
     Block() : fRange(EMPTY_RANGE), fStyle() { }
-    Block(size_t start, size_t end, const TextStyle& style)
-        : fRange(start, end), fStyle(style) {}
-    Block(TextRange textRange, const TextStyle& style)
-        : fRange(textRange), fStyle(style) {}
+    Block(size_t start, size_t end, const TextStyle& style) : fRange(start, end), fStyle(style) {}
+    Block(TextRange textRange, const TextStyle& style) : fRange(textRange), fStyle(style) {}
+
+    Block(const Block& other) {
+        fRange = other.fRange;
+        fStyle = other.fStyle;
+    }
 
     void add(TextRange tail) {
         SkASSERT(fRange.end == tail.start);
@@ -189,6 +256,35 @@
     TextStyle fStyle;
 };
 
+
+typedef size_t BlockIndex;
+typedef SkRange<size_t> BlockRange;
+const size_t EMPTY_BLOCK = EMPTY_INDEX;
+const SkRange<size_t> EMPTY_BLOCKS = EMPTY_RANGE;
+
+struct Placeholder {
+    Placeholder() : fRange(EMPTY_RANGE), fStyle() {}
+
+    Placeholder(size_t start, size_t end, const PlaceholderStyle& style, BlockRange blocksBefore,
+                TextRange textBefore)
+            : fRange(start, end)
+            , fStyle(style)
+            , fBlocksBefore(blocksBefore)
+            , fTextBefore(textBefore) {}
+
+    Placeholder(const Placeholder& other) {
+        fRange = other.fRange;
+        fStyle = other.fStyle;
+        fBlocksBefore = other.fBlocksBefore;
+        fTextBefore = other.fTextBefore;
+    }
+
+    TextRange fRange;
+    PlaceholderStyle fStyle;
+    BlockRange fBlocksBefore;
+    TextRange fTextBefore;
+};
+
 }  // namespace textlayout
 }  // namespace skia
 
diff --git a/modules/skparagraph/src/FontResolver.cpp b/modules/skparagraph/src/FontResolver.cpp
index d619b9f..de8a9e1 100644
--- a/modules/skparagraph/src/FontResolver.cpp
+++ b/modules/skparagraph/src/FontResolver.cpp
@@ -234,27 +234,31 @@
     fFontCollection = master->fontCollection();
     fStyles = master->styles();
     fText = master->text();
-    fTextRange = TextRange(0, fText.size());
 
-    Block combined;
+    Block combinedBlock;
     for (auto& block : fStyles) {
-        SkASSERT(combined.fRange.empty() ||
-                 combined.fRange.end == block.fRange.start);
+        SkASSERT(combinedBlock.fRange.width() == 0 ||
+                 combinedBlock.fRange.end == block.fRange.start);
 
-        if (!combined.fRange.empty() &&
-                block.fStyle.matchOneAttribute(StyleType::kFont, combined.fStyle)) {
-            combined.add(block.fRange);
-            continue;
+        if (!combinedBlock.fRange.empty()) {
+            if (block.fStyle.matchOneAttribute(StyleType::kFont, combinedBlock.fStyle)) {
+                combinedBlock.add(block.fRange);
+                continue;
+            }
+            // Resolve all characters in the block for this style
+            this->findAllFontsForStyledBlock(combinedBlock.fStyle, combinedBlock.fRange);
         }
 
-        if (!combined.fRange.empty()) {
-            this->findAllFontsForStyledBlock(combined.fStyle, combined.fRange);
+        if (block.fStyle.isPlaceholder()) {
+            fFontMapping.set(block.fRange.start, FontDescr());
+            combinedBlock.fRange = EMPTY_RANGE;
+        } else {
+            combinedBlock.fRange = block.fRange;
+            combinedBlock.fStyle = block.fStyle;
         }
-
-        combined = block;
     }
-    this->findAllFontsForStyledBlock(combined.fStyle, combined.fRange);
 
+    this->findAllFontsForStyledBlock(combinedBlock.fStyle, combinedBlock.fRange);
 
     fFontSwitches.reset();
     FontDescr* prev = nullptr;
@@ -274,9 +278,9 @@
         }
 
         if (*prev == *found) {
-            // Same font
             continue;
         }
+
         fFontSwitches.emplace_back(*prev);
 
         prev = found;
diff --git a/modules/skparagraph/src/FontResolver.h b/modules/skparagraph/src/FontResolver.h
index a935bad..b570b08 100644
--- a/modules/skparagraph/src/FontResolver.h
+++ b/modules/skparagraph/src/FontResolver.h
@@ -16,9 +16,9 @@
 namespace textlayout {
 
 struct FontDescr {
-    FontDescr() {}
+    FontDescr() { }
     FontDescr(SkFont font, SkScalar height)
-            : fFont(font), fHeight(height), fStart(EMPTY_INDEX) {}
+            : fFont(font), fHeight(height), fStart(EMPTY_INDEX) { }
     bool operator==(const FontDescr& a) const {
         return this->fFont == a.fFont && this->fHeight == a.fHeight;
     }
@@ -56,7 +56,6 @@
 
     sk_sp<FontCollection> fFontCollection;
     SkSpan<const char> fText;
-    TextRange fTextRange;
     SkSpan<Block> fStyles;
 
     SkTArray<FontDescr> fFontSwitches;
diff --git a/modules/skparagraph/src/Iterators.h b/modules/skparagraph/src/Iterators.h
index 271b2d4..213fa95 100644
--- a/modules/skparagraph/src/Iterators.h
+++ b/modules/skparagraph/src/Iterators.h
@@ -19,26 +19,22 @@
 class FontIterator final : public SkShaper::FontRunIterator {
 public:
     FontIterator(SkSpan<const char> utf8, FontResolver* fontResolver)
-        : fText(utf8), fCurrentChar(utf8.begin()), fFontResolver(fontResolver) {
-    }
+        : fText(utf8), fCurrentChar(utf8.begin()), fFontResolver(fontResolver) { }
 
     void consume() override {
+
         SkASSERT(fCurrentChar < fText.end());
-        auto found = fFontResolver->findNext(fCurrentChar, &fFont, &fLineHeight);
-        SkASSERT(found);
-        if (!found) {
-            fCurrentChar = fText.end();
-            return;
-        }
+        fFontResolver->findNext(fCurrentChar, &fFont, &fLineHeight);
+
         // Move until we find the first character that cannot be resolved with the current font
         while (++fCurrentChar != fText.end()) {
             SkFont font;
             SkScalar height;
             if (fFontResolver->findNext(fCurrentChar, &font, &height)) {
-                  if (fFont == font && fLineHeight == height) {
-                      continue;
-                  }
-              break;
+                if (fFont == font && fLineHeight == height) {
+                    continue;
+                }
+                break;
             }
         }
     }
@@ -76,7 +72,7 @@
 
         fCurrentChar = fText.begin() + fCurrentStyle->fRange.end;
         fCurrentLocale = fCurrentStyle->fStyle.getLocale();
-        while (++fCurrentStyle != fTextStyles.end()) {
+        while (++fCurrentStyle != fTextStyles.end() && !fCurrentStyle->fStyle.isPlaceholder()) {
             if (fCurrentStyle->fStyle.getLocale() != fCurrentLocale) {
                 break;
             }
@@ -85,7 +81,7 @@
     }
 
     size_t endOfCurrentRun() const override { return fCurrentChar - fText.begin(); }
-    bool atEnd() const override { return fCurrentChar == fText.end(); }
+    bool atEnd() const override { return fCurrentChar >= fText.end(); }
     const char* currentLanguage() const override { return fCurrentLocale.c_str(); }
 
 private:
diff --git a/modules/skparagraph/src/ParagraphBuilderImpl.cpp b/modules/skparagraph/src/ParagraphBuilderImpl.cpp
index 999aeb7a..7301e05 100644
--- a/modules/skparagraph/src/ParagraphBuilderImpl.cpp
+++ b/modules/skparagraph/src/ParagraphBuilderImpl.cpp
@@ -72,15 +72,34 @@
     unicode.setTo((UChar*)text.data());
     std::string str;
     unicode.toUTF8String(str);
-    // SkDebugf("Layout text16: '%s'\n", str.c_str());
     fUtf8.insert(fUtf8.size(), str.c_str());
 }
 
 void ParagraphBuilderImpl::addText(const char* text) {
-    // SkDebugf("Layout text8: '%s'\n", text);
     fUtf8.insert(fUtf8.size(), text);
 }
 
+void ParagraphBuilderImpl::addPlaceholder(const PlaceholderStyle& placeholderStyle) {
+    addPlaceholder(placeholderStyle, false);
+}
+
+void ParagraphBuilderImpl::addPlaceholder(const PlaceholderStyle& placeholderStyle, bool lastOne) {
+    this->endRunIfNeeded();
+
+    BlockRange stylesBefore(fPlaceholders.empty() ? 0 : fPlaceholders.back().fBlocksBefore.end + 1,
+                            fStyledBlocks.size());
+    TextRange textBefore(fPlaceholders.empty() ? 0 : fPlaceholders.back().fRange.end,
+                            fUtf8.size());
+    auto start = fUtf8.size();
+    if (!lastOne) {
+        pushStyle(TextStyle(fTextStyles.top(), true));
+        addText(std::u16string(1ull, 0xFFFC));
+        pop();
+    }
+    auto end = fUtf8.size();
+    fPlaceholders.emplace_back(start, end, placeholderStyle, stylesBefore, textBefore);
+}
+
 void ParagraphBuilderImpl::endRunIfNeeded() {
     if (fStyledBlocks.empty()) {
         return;
@@ -98,8 +117,11 @@
     if (!fUtf8.isEmpty()) {
         this->endRunIfNeeded();
     }
+
+    // Add one fake placeholder with the rest of the text
+    addPlaceholder(PlaceholderStyle(), true);
     return skstd::make_unique<ParagraphImpl>(
-            fUtf8, fParagraphStyle, fStyledBlocks, fFontCollection);
+            fUtf8, fParagraphStyle, fStyledBlocks, fPlaceholders, fFontCollection);
 }
 
 }  // namespace textlayout
diff --git a/modules/skparagraph/src/ParagraphBuilderImpl.h b/modules/skparagraph/src/ParagraphBuilderImpl.h
index f61a117..f034230 100644
--- a/modules/skparagraph/src/ParagraphBuilderImpl.h
+++ b/modules/skparagraph/src/ParagraphBuilderImpl.h
@@ -47,6 +47,8 @@
     // Converts to u16string before adding.
     void addText(const char* text) override;
 
+    void addPlaceholder(const PlaceholderStyle& placeholderStyle) override;
+
     void setParagraphStyle(const ParagraphStyle& style) override;
 
     // Constructs a SkParagraph object that can be used to layout and paint the text to a SkCanvas.
@@ -54,10 +56,12 @@
 
 private:
     void endRunIfNeeded();
+    void addPlaceholder(const PlaceholderStyle& placeholderStyle, bool lastOne);
 
     SkString fUtf8;
     std::stack<TextStyle> fTextStyles;
     SkTArray<Block, true> fStyledBlocks;
+    SkTArray<Placeholder, true> fPlaceholders;
     sk_sp<FontCollection> fFontCollection;
     ParagraphStyle fParagraphStyle;
 };
diff --git a/modules/skparagraph/src/ParagraphCache.cpp b/modules/skparagraph/src/ParagraphCache.cpp
index 465b9bc..80da392 100644
--- a/modules/skparagraph/src/ParagraphCache.cpp
+++ b/modules/skparagraph/src/ParagraphCache.cpp
@@ -58,9 +58,13 @@
         }
     }
     for (auto& ts : key.fTextStyles) {
-        hash = mix(hash, SkGoodHash()(ts.fStyle.getLetterSpacing()));
-        hash = mix(hash, SkGoodHash()(ts.fStyle.getWordSpacing()));
-        hash = mix(hash, SkGoodHash()(ts.fRange));
+        if (!ts.fStyle.isPlaceholder()) {
+            hash = mix(hash, SkGoodHash()(ts.fStyle.getLetterSpacing()));
+            hash = mix(hash, SkGoodHash()(ts.fStyle.getWordSpacing()));
+            hash = mix(hash, SkGoodHash()(ts.fRange));
+        } else {
+            // TODO: cache placeholders
+        }
     }
     hash = mix(hash, SkGoodHash()(key.fText));
     return hash;
@@ -99,17 +103,24 @@
     for (size_t i = 0; i < a.fTextStyles.size(); ++i) {
         auto& tsa = a.fTextStyles[i];
         auto& tsb = b.fTextStyles[i];
-        if (tsa.fStyle.getLetterSpacing() != tsb.fStyle.getLetterSpacing()) {
+        if (!(tsa.fStyle == tsb.fStyle)) {
             return false;
         }
-        if (tsa.fStyle.getWordSpacing() != tsb.fStyle.getWordSpacing()) {
-            return false;
-        }
-        if (tsa.fRange.width() != tsb.fRange.width()) {
-            return false;
-        }
-        if (tsa.fRange.start != tsb.fRange.start) {
-            return false;
+        if (!tsa.fStyle.isPlaceholder()) {
+            if (tsa.fStyle.getLetterSpacing() != tsb.fStyle.getLetterSpacing()) {
+                return false;
+            }
+            if (tsa.fStyle.getWordSpacing() != tsb.fStyle.getWordSpacing()) {
+                return false;
+            }
+            if (tsa.fRange.width() != tsb.fRange.width()) {
+                return false;
+            }
+            if (tsa.fRange.start != tsb.fRange.start) {
+                return false;
+            }
+        } else {
+            // TODO: compare placeholders
         }
     }
 
diff --git a/modules/skparagraph/src/ParagraphImpl.cpp b/modules/skparagraph/src/ParagraphImpl.cpp
index 7e6a3f9..b2eee68 100644
--- a/modules/skparagraph/src/ParagraphImpl.cpp
+++ b/modules/skparagraph/src/ParagraphImpl.cpp
@@ -1,86 +1,16 @@
 // Copyright 2019 Google LLC.
-#include "modules/skparagraph/src/ParagraphImpl.h"
-#include <unicode/brkiter.h>
-#include <unicode/ubidi.h>
-#include <unicode/unistr.h>
-#include <unicode/urename.h>
 #include "include/core/SkBlurTypes.h"
 #include "include/core/SkCanvas.h"
 #include "include/core/SkFontMgr.h"
 #include "include/core/SkPictureRecorder.h"
 #include "modules/skparagraph/src/Iterators.h"
+#include "modules/skparagraph/src/ParagraphImpl.h"
 #include "modules/skparagraph/src/Run.h"
 #include "modules/skparagraph/src/TextWrapper.h"
 #include "src/core/SkSpan.h"
 #include "src/utils/SkUTF.h"
 #include <algorithm>
 
-namespace {
-
-class TextBreaker {
-public:
-    TextBreaker() : fPos(-1) {}
-
-    bool initialize(SkSpan<const char> text, UBreakIteratorType type) {
-        UErrorCode status = U_ZERO_ERROR;
-        fIterator = nullptr;
-        fSize = text.size();
-        UText utf8UText = UTEXT_INITIALIZER;
-        utext_openUTF8(&utf8UText, text.begin(), text.size(), &status);
-        fAutoClose =
-                std::unique_ptr<UText, SkFunctionWrapper<UText*, UText, utext_close>>(&utf8UText);
-        if (U_FAILURE(status)) {
-            SkDebugf("Could not create utf8UText: %s", u_errorName(status));
-            return false;
-        }
-        fIterator.reset(ubrk_open(type, "en", nullptr, 0, &status));
-        if (U_FAILURE(status)) {
-            SkDebugf("Could not create line break iterator: %s", u_errorName(status));
-            SK_ABORT("");
-        }
-
-        ubrk_setUText(fIterator.get(), &utf8UText, &status);
-        if (U_FAILURE(status)) {
-            SkDebugf("Could not setText on break iterator: %s", u_errorName(status));
-            return false;
-        }
-
-        fPos = 0;
-        return true;
-    }
-
-    size_t first() {
-        fPos = ubrk_first(fIterator.get());
-        return eof() ? fSize : fPos;
-    }
-
-    size_t next() {
-        fPos = ubrk_next(fIterator.get());
-        return eof() ? fSize : fPos;
-    }
-
-    size_t preceding(size_t offset) {
-        auto pos = ubrk_preceding(fIterator.get(), offset);
-        return eof() ? 0 : pos;
-    }
-
-    size_t following(size_t offset) {
-        auto pos = ubrk_following(fIterator.get(), offset);
-        return eof() ? fSize : pos;
-    }
-
-    int32_t status() { return ubrk_getRuleStatus(fIterator.get()); }
-
-    bool eof() { return fPos == icu::BreakIterator::DONE; }
-
-private:
-    std::unique_ptr<UText, SkFunctionWrapper<UText*, UText, utext_close>> fAutoClose;
-    std::unique_ptr<UBreakIterator, SkFunctionWrapper<void, UBreakIterator, ubrk_close>> fIterator;
-    int32_t fPos;
-    size_t fSize;
-};
-}  // namespace
-
 namespace skia {
 namespace textlayout {
 
@@ -91,12 +21,43 @@
     return end > begin ? TextRange(begin, end) : EMPTY_TEXT;
 }
 
+bool TextBreaker::initialize(SkSpan<const char> text, UBreakIteratorType type) {
+    UErrorCode status = U_ZERO_ERROR;
+    fIterator = nullptr;
+    fSize = text.size();
+    UText utf8UText = UTEXT_INITIALIZER;
+    utext_openUTF8(&utf8UText, text.begin(), text.size(), &status);
+    fAutoClose =
+            std::unique_ptr<UText, SkFunctionWrapper<UText*, UText, utext_close>>(&utf8UText);
+    if (U_FAILURE(status)) {
+        SkDebugf("Could not create utf8UText: %s", u_errorName(status));
+        return false;
+    }
+    fIterator.reset(ubrk_open(type, "en", nullptr, 0, &status));
+    if (U_FAILURE(status)) {
+        SkDebugf("Could not create line break iterator: %s", u_errorName(status));
+        SK_ABORT("");
+    }
+
+    ubrk_setUText(fIterator.get(), &utf8UText, &status);
+    if (U_FAILURE(status)) {
+        SkDebugf("Could not setText on break iterator: %s", u_errorName(status));
+        return false;
+    }
+
+    fInitialized = true;
+    fPos = 0;
+    return true;
+}
+
 ParagraphImpl::ParagraphImpl(const SkString& text,
                              ParagraphStyle style,
                              SkTArray<Block, true> blocks,
+                             SkTArray<Placeholder, true> placeholders,
                              sk_sp<FontCollection> fonts)
         : Paragraph(std::move(style), std::move(fonts))
         , fTextStyles(std::move(blocks))
+        , fPlaceholders(std::move(placeholders))
         , fText(text)
         , fTextSpan(fText.c_str(), fText.size())
         , fState(kUnknown)
@@ -110,9 +71,11 @@
 ParagraphImpl::ParagraphImpl(const std::u16string& utf16text,
                              ParagraphStyle style,
                              SkTArray<Block, true> blocks,
+                             SkTArray<Placeholder, true> placeholders,
                              sk_sp<FontCollection> fonts)
         : Paragraph(std::move(style), std::move(fonts))
         , fTextStyles(std::move(blocks))
+        , fPlaceholders(std::move(placeholders))
         , fState(kUnknown)
         , fPicture(nullptr)
         , fStrutMetrics(false)
@@ -224,31 +187,42 @@
     for (RunIndex runIndex = 0; runIndex < fRuns.size(); ++runIndex) {
         auto& run = fRuns[runIndex];
         auto runStart = fClusters.size();
-        fClusters.reserve(fClusters.size() + fRuns.size());
-        // Walk through the glyph in the direction of input text
-        run.iterateThroughClustersInTextOrder([runIndex, this](
-                                                      size_t glyphStart,
-                                                      size_t glyphEnd,
-                                                      size_t charStart,
-                                                      size_t charEnd,
-                                                      SkScalar width,
-                                                      SkScalar height) {
-            SkASSERT(charEnd >= charStart);
-            SkSpan<const char> text(fTextSpan.begin() + charStart, charEnd - charStart);
+        if (run.isPlaceholder()) {
+            // There are no glyphs but we want to have one cluster
+            SkSpan<const char> text(fTextSpan.begin() + run.textRange().start, run.textRange().width());
+            if (!fClusters.empty()) {
+                fClusters.back().setBreakType(Cluster::SoftLineBreak);
+            }
+            auto& cluster = fClusters.emplace_back(this, runIndex, 0ul, 0ul, text,
+                                                   run.advance().fX, run.advance().fY);
+            cluster.setBreakType(Cluster::SoftLineBreak);
+        } else {
+            fClusters.reserve(fClusters.size() + fRuns.size());
+            // Walk through the glyph in the direction of input text
+            run.iterateThroughClustersInTextOrder([runIndex, this](size_t glyphStart,
+                                                                   size_t glyphEnd,
+                                                                   size_t charStart,
+                                                                   size_t charEnd,
+                                                                   SkScalar width,
+                                                                   SkScalar height) {
+                SkASSERT(charEnd >= charStart);
+                SkSpan<const char> text(fTextSpan.begin() + charStart, charEnd - charStart);
 
-            auto& cluster = fClusters.emplace_back(this, runIndex, glyphStart, glyphEnd, text, width, height);
-            cluster.setIsWhiteSpaces();
-        });
+                auto& cluster = fClusters.emplace_back(this, runIndex, glyphStart, glyphEnd, text,
+                                                       width, height);
+                cluster.setIsWhiteSpaces();
+            });
+        }
 
         run.setClusterRange(runStart, fClusters.size());
         fMaxIntrinsicWidth += run.advance().fX;
     }
 }
 
-// TODO: we need soft line breaks before for word spacing
 void ParagraphImpl::markLineBreaks() {
 
     // Find all possible (soft) line breaks
+    // This iterator is used only once for a paragraph so we don't have to keep it
     TextBreaker breaker;
     if (!breaker.initialize(fTextSpan, UBRK_LINE)) {
         return;
@@ -277,6 +251,11 @@
     SkScalar shift = 0;
     for (auto& run : fRuns) {
 
+        // Skip placeholder runs
+        if (run.isPlaceholder()) {
+            continue;
+        }
+
         bool soFarWhitespacesOnly = true;
         for (size_t index = 0; index != run.clusterRange().width(); ++index) {
             auto correctIndex = run.leftToRight()
@@ -294,6 +273,8 @@
                 SkASSERT(currentStyle != this->fTextStyles.end());
             }
 
+            SkASSERT(!currentStyle->fStyle.isPlaceholder());
+
             // Process word spacing
             if (currentStyle->fStyle.getWordSpacing() != 0) {
                 if (cluster->isWhitespaces() && cluster->isSoftBreak()) {
@@ -320,10 +301,11 @@
 
     class ShapeHandler final : public SkShaper::RunHandler {
     public:
-        explicit ShapeHandler(ParagraphImpl& paragraph, FontIterator* fontIterator)
+        explicit ShapeHandler(ParagraphImpl& paragraph, size_t firstChar, FontIterator* fontIterator, SkScalar advanceX)
                 : fParagraph(&paragraph)
+                , fFirstChar(firstChar)
                 , fFontIterator(fontIterator)
-                , fAdvance(SkVector::Make(0, 0)) {}
+                , fAdvance(SkVector::Make(advanceX, 0)) {}
 
         SkVector advance() const { return fAdvance; }
 
@@ -337,6 +319,7 @@
         Buffer runBuffer(const RunInfo& info) override {
             auto& run = fParagraph->fRuns.emplace_back(fParagraph,
                                                        info,
+                                                       fFirstChar,
                                                        fFontIterator->currentLineHeight(),
                                                        fParagraph->fRuns.count(),
                                                        fAdvance.fX);
@@ -344,19 +327,23 @@
         }
 
         void commitRunBuffer(const RunInfo&) override {
+
             auto& run = fParagraph->fRuns.back();
             if (run.size() == 0) {
                 fParagraph->fRuns.pop_back();
                 return;
             }
+
             // Carve out the line text out of the entire run text
             fAdvance.fX += run.advance().fX;
             fAdvance.fY = SkMaxScalar(fAdvance.fY, run.advance().fY);
+            run.fPlaceholder = nullptr;
         }
 
         void commitLine() override {}
 
         ParagraphImpl* fParagraph;
+        size_t fFirstChar;
         FontIterator* fFontIterator;
         SkVector fAdvance;
     };
@@ -370,24 +357,37 @@
 
     // Check the font-resolved text against the cache
     if (!fFontCollection->getParagraphCache()->findParagraph(this)) {
-        LangIterator lang(fTextSpan, styles(), paragraphStyle().getTextStyle());
-        FontIterator font(fTextSpan, &fFontResolver);
-        ShapeHandler handler(*this, &font);
-        std::unique_ptr<SkShaper> shaper = SkShaper::MakeShapeDontWrapOrReorder();
-        SkASSERT_RELEASE(shaper != nullptr);
-        auto bidi = SkShaper::MakeIcuBiDiRunIterator(
-                fTextSpan.begin(), fTextSpan.size(),
-                fParagraphStyle.getTextDirection() == TextDirection::kLtr ? (uint8_t)2
-                                                                          : (uint8_t)1);
-        if (bidi == nullptr) {
+        // The text can be broken into many shaping sequences
+        // (by place holders, possibly, by hard line breaks or tabs, too)
+        uint8_t textDirection = fParagraphStyle.getTextDirection() == TextDirection::kLtr  ? 2 : 1;
+        auto limitlessWidth = std::numeric_limits<SkScalar>::max();
+
+        auto result = iterateThroughShapingRegions(
+                [this, textDirection, limitlessWidth]
+                (SkSpan<const char> textSpan, SkSpan<Block> styleSpan, SkScalar& advanceX, size_t start) {
+
+            LangIterator lang(textSpan, styleSpan, paragraphStyle().getTextStyle());
+            FontIterator font(textSpan, &fFontResolver);
+            auto script = SkShaper::MakeHbIcuScriptRunIterator(textSpan.begin(), textSpan.size());
+            auto bidi = SkShaper::MakeIcuBiDiRunIterator(textSpan.begin(), textSpan.size(), textDirection);
+            if (bidi == nullptr) {
+                return false;
+            }
+
+            // Set up the shaper and shape the next
+            ShapeHandler handler(*this, start, &font, advanceX);
+            auto shaper = SkShaper::MakeShapeDontWrapOrReorder();
+            SkASSERT_RELEASE(shaper != nullptr);
+            shaper->shape(textSpan.begin(), textSpan.size(), font, *bidi, *script, lang, limitlessWidth, &handler);
+            advanceX =  handler.advance().fX;
+            return true;
+        });
+        if (!result) {
             return false;
         }
-        auto script = SkShaper::MakeHbIcuScriptRunIterator(fTextSpan.begin(), fTextSpan.size());
-
-        shaper->shape(fTextSpan.begin(), fTextSpan.size(), font, *bidi, *script, lang,
-                      std::numeric_limits<SkScalar>::max(), &handler);
     }
 
+    // Clean the array for justification
     if (fParagraphStyle.getTextAlign() == TextAlign::kJustify) {
         fRunShifts.reset();
         fRunShifts.push_back_n(fRuns.size(), RunShifts());
@@ -399,6 +399,48 @@
     return true;
 }
 
+bool ParagraphImpl::iterateThroughShapingRegions(ShapeVisitor shape) {
+
+    SkScalar advanceX = 0;
+    for (auto& placeholder : fPlaceholders) {
+        // Shape the text
+        if (placeholder.fTextBefore.width() > 0) {
+            // Set up the iterators
+            SkSpan<const char> textSpan(fTextSpan.begin() + placeholder.fTextBefore.start,
+                                        placeholder.fTextBefore.width());
+            SkSpan<Block> styleSpan(fTextStyles.begin() + placeholder.fBlocksBefore.start,
+                                    placeholder.fBlocksBefore.width());
+
+            if (!shape(textSpan, styleSpan, advanceX, placeholder.fTextBefore.start)) {
+                return false;
+            }
+        }
+
+        if (placeholder.fRange.width() == 0) {
+            continue;
+        }
+        // "Shape" the placeholder
+        const SkShaper::RunHandler::RunInfo runInfo = {
+            SkFont(),
+            (uint8_t)2,
+            SkPoint::Make(placeholder.fStyle.fWidth, placeholder.fStyle.fHeight),
+            1,
+            SkShaper::RunHandler::Range(placeholder.fRange.start, placeholder.fRange.width())
+        };
+        auto& run = fRuns.emplace_back(this,
+                                       runInfo,
+                                       0, //placeholder.fRange.start,
+                                       1,
+                                       fRuns.count(),
+                                       advanceX);
+        run.fPositions[0] = { advanceX, 0 };
+        run.fClusterIndexes[0] = 0;
+        run.fPlaceholder = &placeholder.fStyle;
+        advanceX += placeholder.fStyle.fWidth;
+    }
+    return true;
+}
+
 void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) {
 
     TextWrapper textWrapper;
@@ -528,6 +570,7 @@
         return;
     }
 
+    // This breaker gets called only once for a paragraph so we don't have to keep it
     TextBreaker breaker;
     if (!breaker.initialize(fTextSpan, UBRK_CHARACTER)) {
         return;
@@ -628,8 +671,12 @@
 
                 if (rectHeightStyle == RectHeightStyle::kMax) {
                     // TODO: Sort it out with Flutter people
-                    // Mimicking TxtLib: clip.fTop = line.offset().fY + line.roundingDelta();
+                    // Mimicking TxtLib:
+                    //clip.fTop = line.offset().fY + line.roundingDelta();
                     clip.fBottom = line.offset().fY + line.height();
+                    clip.fTop = line.offset().fY +
+                            line.sizes().baseline() - line.getMaxRunMetrics().baseline() +
+                            line.getMaxRunMetrics().delta();
 
                 } else if (rectHeightStyle == RectHeightStyle::kIncludeLineSpacingTop) {
                     if (&line != &fLines.front()) {
@@ -662,7 +709,7 @@
                 // Check if we can merge two boxes
                 bool mergedBoxes = false;
                 if (!results.empty() &&
-                    lastRun != nullptr &&
+                    lastRun != nullptr && lastRun->placeholder() == nullptr && run->placeholder() == nullptr &&
                     lastRun->lineHeight() == run->lineHeight() &&
                     lastRun->font() == run->font()) {
                     auto& lastBox = results.back();
@@ -709,6 +756,30 @@
 
     return results;
 }
+
+std::vector<TextBox> ParagraphImpl::GetRectsForPlaceholders() {
+  std::vector<TextBox> boxes;
+
+  for (auto& line : fLines) {
+      SkScalar runOffset = 0;
+      auto text = line.trimmedText();
+      line.iterateThroughRuns(
+          text,
+          runOffset,
+          false,
+          [&boxes, &line](Run* run, size_t pos, size_t size, TextRange text, SkRect clip,
+                                SkScalar shift, bool clippingNeeded) {
+              if (run->placeholder() == nullptr) {
+                  return true;
+              }
+              clip.offset(line.offset());
+              boxes.emplace_back(clip, run->leftToRight() ? TextDirection::kLtr : TextDirection::kRtl);
+              return true;
+          });
+  }
+
+  return boxes;
+}
 // TODO: Deal with RTL here
 PositionWithAffinity ParagraphImpl::getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) {
 
@@ -804,15 +875,16 @@
 // Finds the first and last glyphs that define a word containing
 // the glyph at index offset.
 // By "glyph" they mean a character index - indicated by Minikin's code
-// TODO: make the breaker a member of ParagraphImpl
 SkRange<size_t> ParagraphImpl::getWordBoundary(unsigned offset) {
-    TextBreaker breaker;
-    if (!breaker.initialize(fTextSpan, UBRK_WORD)) {
-        return {0, 0};
+
+    if (!fWordBreaker.initialized()) {
+        if (!fWordBreaker.initialize(fTextSpan, UBRK_WORD)) {
+            return {0, 0};
+        }
     }
 
-    auto start = breaker.preceding(offset + 1);
-    auto end = breaker.following(start);
+    auto start = fWordBreaker.preceding(offset + 1);
+    auto end = fWordBreaker.following(start);
 
     return { start, end };
 }
diff --git a/modules/skparagraph/src/ParagraphImpl.h b/modules/skparagraph/src/ParagraphImpl.h
index 043e4ee..4c170d2 100644
--- a/modules/skparagraph/src/ParagraphImpl.h
+++ b/modules/skparagraph/src/ParagraphImpl.h
@@ -2,6 +2,10 @@
 #ifndef ParagraphImpl_DEFINED
 #define ParagraphImpl_DEFINED
 
+#include <unicode/brkiter.h>
+#include <unicode/ubidi.h>
+#include <unicode/unistr.h>
+#include <unicode/urename.h>
 #include "include/core/SkPicture.h"
 #include "include/private/SkMutex.h"
 #include "include/private/SkTHash.h"
@@ -38,6 +42,46 @@
     TStyle fStyle;
 };
 
+class TextBreaker {
+public:
+    TextBreaker() : fInitialized(false), fPos(-1) {}
+
+    bool initialize(SkSpan<const char> text, UBreakIteratorType type);
+
+    bool initialized() const { return fInitialized; }
+
+    size_t first() {
+        fPos = ubrk_first(fIterator.get());
+        return eof() ? fSize : fPos;
+    }
+
+    size_t next() {
+        fPos = ubrk_next(fIterator.get());
+        return eof() ? fSize : fPos;
+    }
+
+    size_t preceding(size_t offset) {
+        auto pos = ubrk_preceding(fIterator.get(), offset);
+        return eof() ? 0 : pos;
+    }
+
+    size_t following(size_t offset) {
+        auto pos = ubrk_following(fIterator.get(), offset);
+        return eof() ? fSize : pos;
+    }
+
+    int32_t status() { return ubrk_getRuleStatus(fIterator.get()); }
+
+    bool eof() { return fPos == icu::BreakIterator::DONE; }
+
+private:
+    std::unique_ptr<UText, SkFunctionWrapper<UText*, UText, utext_close>> fAutoClose;
+    std::unique_ptr<UBreakIterator, SkFunctionWrapper<void, UBreakIterator, ubrk_close>> fIterator;
+    bool fInitialized;
+    int32_t fPos;
+    size_t fSize;
+};
+
 class ParagraphImpl final : public Paragraph {
 
 public:
@@ -45,12 +89,14 @@
     ParagraphImpl(const SkString& text,
                   ParagraphStyle style,
                   SkTArray<Block, true> blocks,
+                  SkTArray<Placeholder, true> placeholders,
                   sk_sp<FontCollection> fonts);
 
     ParagraphImpl(const std::u16string& utf16text,
-                    ParagraphStyle style,
-                    SkTArray<Block, true> blocks,
-                    sk_sp<FontCollection> fonts);
+                  ParagraphStyle style,
+                  SkTArray<Block, true> blocks,
+                  SkTArray<Placeholder, true> placeholders,
+                  sk_sp<FontCollection> fonts);
     ~ParagraphImpl() override;
 
     void layout(SkScalar width) override;
@@ -59,6 +105,7 @@
                                           unsigned end,
                                           RectHeightStyle rectHeightStyle,
                                           RectWidthStyle rectWidthStyle) override;
+    std::vector<TextBox> GetRectsForPlaceholders() override;
     PositionWithAffinity getGlyphPositionAtCoordinate(SkScalar dx, SkScalar dy) override;
     SkRange<size_t> getWordBoundary(unsigned offset) override;
     bool didExceedMaxLines() override {
@@ -139,6 +186,10 @@
     void setState(InternalState state);
     sk_sp<SkPicture> getPicture() { return fPicture; }
 
+    using ShapeVisitor =
+            std::function<SkScalar(SkSpan<const char>, SkSpan<Block>, SkScalar&, size_t)>;
+    bool iterateThroughShapingRegions(ShapeVisitor shape);
+
     void resetContext();
     void resolveStrut();
     void resetRunShifts();
@@ -169,6 +220,7 @@
     SkTArray<StyleBlock<std::vector<TextShadow>>> fShadowStyles;
     SkTArray<StyleBlock<Decoration>> fDecorationStyles;
     SkTArray<Block, true> fTextStyles; // TODO: take out only the font stuff
+    SkTArray<Placeholder, true> fPlaceholders;
     SkString fText;
     SkSpan<const char> fTextSpan;
 
@@ -189,6 +241,8 @@
     SkScalar fOldWidth;
     SkScalar fOldHeight;
     SkScalar fMaxWidthWithTrailingSpaces;
+
+    TextBreaker fWordBreaker;
 };
 }  // namespace textlayout
 }  // namespace skia
diff --git a/modules/skparagraph/src/Run.cpp b/modules/skparagraph/src/Run.cpp
index ad93fb1..2f7edc9 100644
--- a/modules/skparagraph/src/Run.cpp
+++ b/modules/skparagraph/src/Run.cpp
@@ -19,12 +19,14 @@
 
 Run::Run(ParagraphImpl* master,
          const SkShaper::RunHandler::RunInfo& info,
+         size_t firstChar,
          SkScalar lineHeight,
          size_t index,
          SkScalar offsetX)
         : fMaster(master)
-        , fTextRange(info.utf8Range.begin(), info.utf8Range.end())
-        , fClusterRange(EMPTY_CLUSTERS) {
+        , fTextRange(firstChar + info.utf8Range.begin(), firstChar + info.utf8Range.end())
+        , fClusterRange(EMPTY_CLUSTERS)
+        , fFirstChar(firstChar) {
     fFont = info.fFont;
     fHeightMultiplier = lineHeight;
     fBidiLevel = info.fBidiLevel;
@@ -125,8 +127,8 @@
 
             visitor(start,
                     glyph,
-                    cluster,
-                    nextCluster,
+                    fFirstChar + cluster,
+                    fFirstChar + nextCluster,
                     this->calculateWidth(start, glyph, glyph == size()),
                     this->calculateHeight());
 
@@ -145,8 +147,8 @@
 
             visitor(start,
                     glyph,
-                    cluster,
-                    nextCluster,
+                    fFirstChar + cluster,
+                    fFirstChar + nextCluster,
                     this->calculateWidth(start, glyph, glyph == 0),
                     this->calculateHeight());
 
@@ -206,6 +208,60 @@
     }
 }
 
+void Run::updateMetrics(LineMetrics* endlineMetrics) {
+
+    // Difference between the placeholder baseline and the line bottom
+    SkScalar baselineAdjustment = 0;
+    switch (fPlaceholder->fBaseline) {
+        case TextBaseline::kAlphabetic:
+            break;
+
+        case TextBaseline::kIdeographic:
+            baselineAdjustment = endlineMetrics->deltaBaselines() / 2;
+            break;
+    }
+
+    auto height = fPlaceholder->fHeight;
+    auto offset = fPlaceholder->fBaselineOffset;
+
+    fFontMetrics.fLeading = 0;
+    switch (fPlaceholder->fAlignment) {
+        case PlaceholderAlignment::kBaseline:
+            fFontMetrics.fAscent = baselineAdjustment - offset;
+            fFontMetrics.fDescent = baselineAdjustment + height - offset;
+            break;
+
+        case PlaceholderAlignment::kAboveBaseline:
+            fFontMetrics.fAscent = baselineAdjustment - height;
+            fFontMetrics.fDescent = baselineAdjustment;
+            break;
+
+        case PlaceholderAlignment::kBelowBaseline:
+            fFontMetrics.fAscent = baselineAdjustment;
+            fFontMetrics.fDescent = baselineAdjustment +height;
+            break;
+
+        case PlaceholderAlignment::kTop:
+            fFontMetrics.fAscent = - endlineMetrics->alphabeticBaseline();
+            fFontMetrics.fDescent = height - endlineMetrics->alphabeticBaseline();
+            break;
+
+        case PlaceholderAlignment::kBottom:
+            fFontMetrics.fDescent = endlineMetrics->deltaBaselines();
+            fFontMetrics.fAscent = - height + endlineMetrics->deltaBaselines();
+            break;
+
+        case PlaceholderAlignment::kMiddle:
+            double mid = (endlineMetrics->ascent() + endlineMetrics->descent())/ 2;
+            fFontMetrics.fDescent = mid + height/2;
+            fFontMetrics.fAscent =  mid - height/2;
+            break;
+    }
+
+    // Make sure the placeholder can fit the line
+    endlineMetrics->add(this);
+}
+
 void Cluster::setIsWhiteSpaces() {
 
     fWhiteSpaces = false;
diff --git a/modules/skparagraph/src/Run.h b/modules/skparagraph/src/Run.h
index b5db00e..62381a1 100644
--- a/modules/skparagraph/src/Run.h
+++ b/modules/skparagraph/src/Run.h
@@ -32,22 +32,19 @@
 typedef size_t CodepointIndex;
 typedef SkRange<CodepointIndex> CodepointRange;
 
-typedef size_t BlockIndex;
-typedef SkRange<size_t> BlockRange;
-const size_t EMPTY_BLOCK = EMPTY_INDEX;
-const SkRange<size_t> EMPTY_BLOCKS = EMPTY_RANGE;
-
 struct RunShifts {
     RunShifts() { }
     RunShifts(size_t count) { fShifts.push_back_n(count, 0.0); }
     SkSTArray<128, SkScalar, true> fShifts;
 };
 
+class LineMetrics;
 class Run {
 public:
     Run() = default;
     Run(ParagraphImpl* master,
         const SkShaper::RunHandler::RunInfo& info,
+        size_t firstChar,
         SkScalar lineHeight,
         size_t index,
         SkScalar shiftX);
@@ -69,8 +66,6 @@
     }
     SkVector offset() const { return fOffset; }
     SkScalar ascent() const { return fFontMetrics.fAscent; }
-    //SkScalar descent() const { return fFontMetrics.fDescent; }
-    //SkScalar leading() const { return fFontMetrics.fLeading; }
     SkScalar correctAscent() const {
 
         if (fHeightMultiplier == 0 || fHeightMultiplier == 1) {
@@ -99,12 +94,16 @@
     bool leftToRight() const { return fBidiLevel % 2 == 0; }
     size_t index() const { return fIndex; }
     SkScalar lineHeight() const { return fHeightMultiplier; }
+    PlaceholderStyle* placeholder() const { return fPlaceholder; }
+    bool isPlaceholder() const { return fPlaceholder != nullptr; }
     size_t clusterIndex(size_t pos) const { return fClusterIndexes[pos]; }
     SkScalar positionX(size_t pos) const;
 
     TextRange textRange() { return fTextRange; }
     ClusterRange clusterRange() { return fClusterRange; }
 
+    void updateMetrics(LineMetrics* endlineMetrics);
+
     void setClusterRange(size_t from, size_t to) { fClusterRange = ClusterRange(from, to); }
     SkRect clip() const {
         return SkRect::MakeXYWH(fOffset.fX, fOffset.fY, fAdvance.fX, fAdvance.fY);
@@ -157,10 +156,12 @@
     SkFont fFont;
     SkFontMetrics fFontMetrics;
     SkScalar fHeightMultiplier;
+    PlaceholderStyle* fPlaceholder;
     size_t fIndex;
     uint8_t fBidiLevel;
     SkVector fAdvance;
     SkVector fOffset;
+    size_t fFirstChar;
     SkShaper::RunHandler::Range fUtf8Range;
     SkSTArray<128, SkGlyphID, false> fGlyphs;
     SkSTArray<128, SkPoint, true> fPositions;
@@ -243,8 +244,6 @@
     size_t startPos() const { return fStart; }
     size_t endPos() const { return fEnd; }
     SkScalar width() const { return fWidth; }
-    SkScalar trimmedWidth() const { return fWidth - fSpacing; }
-    SkScalar lastSpacing() const { return fSpacing; }
     SkScalar height() const { return fHeight; }
     size_t size() const { return fEnd - fStart; }
 
@@ -290,20 +289,25 @@
 public:
 
     LineMetrics() { clean(); }
-    LineMetrics(bool forceStrut) : fForceStrut(forceStrut) { clean(); }
+    LineMetrics(bool forceStrut) {
+        clean();
+        fForceStrut = forceStrut;
+    }
 
-    LineMetrics(SkScalar a, SkScalar d, SkScalar l) : fForceStrut(false) {
+    LineMetrics(SkScalar a, SkScalar d, SkScalar l) {
         fAscent = a;
         fDescent = d;
         fLeading = l;
+        fForceStrut = false;
     }
 
-    LineMetrics(const SkFont& font, bool forceStrut) : fForceStrut(forceStrut) {
+    LineMetrics(const SkFont& font, bool forceStrut) {
         SkFontMetrics metrics;
         font.getMetrics(&metrics);
         fAscent = metrics.fAscent;
         fDescent = metrics.fDescent;
         fLeading = metrics.fLeading;
+        fForceStrut = forceStrut;
     }
 
     void add(Run* run) {
@@ -327,6 +331,7 @@
         fAscent = 0;
         fDescent = 0;
         fLeading = 0;
+        fForceStrut = false;
     }
 
     SkScalar delta() const { return height() - ideographicBaseline(); }
@@ -343,12 +348,16 @@
     SkScalar height() const { return SkScalarRoundToInt(fDescent - fAscent + fLeading); }
     SkScalar alphabeticBaseline() const { return fLeading / 2 - fAscent; }
     SkScalar ideographicBaseline() const { return fDescent - fAscent + fLeading; }
+    SkScalar deltaBaselines() const { return fLeading / 2 + fDescent; }
     SkScalar baseline() const { return fLeading / 2 - fAscent; }
     SkScalar ascent() const { return fAscent; }
     SkScalar descent() const { return fDescent; }
     SkScalar leading() const { return fLeading; }
 
 private:
+
+    friend class TextWrapper;
+
     SkScalar fAscent;
     SkScalar fDescent;
     SkScalar fLeading;
diff --git a/modules/skparagraph/src/TextLine.cpp b/modules/skparagraph/src/TextLine.cpp
index 40d2cbe..1eaef70 100644
--- a/modules/skparagraph/src/TextLine.cpp
+++ b/modules/skparagraph/src/TextLine.cpp
@@ -28,8 +28,6 @@
     return end >= begin ? TextRange(begin, end) : EMPTY_TEXT;
 }
 
-SkTHashMap<SkFont, Run> TextLine::fEllipsisCache;
-
 TextLine::TextLine(ParagraphImpl* master,
                    SkVector offset,
                    SkVector advance,
@@ -63,6 +61,9 @@
 
     for (BlockIndex index = fBlockRange.start; index < fBlockRange.end; ++index) {
         auto b = fMaster->styles().begin() + index;
+        if (b->fStyle.isPlaceholder()) {
+            continue;
+        }
         if (b->fStyle.hasBackground()) {
             fHasBackground = true;
         }
@@ -195,6 +196,9 @@
     return this->iterateThroughRuns(
         textRange, offsetX, false,
         [canvas, paint, shiftDown](Run* run, int32_t pos, size_t size, TextRange, SkRect clip, SkScalar shift, bool clippingNeeded) {
+            if (run->placeholder() != nullptr) {
+                return true;
+            }
             SkTextBlobBuilder builder;
             run->copyTo(builder, SkToU32(pos), size, SkVector::Make(0, shiftDown));
             canvas->save();
@@ -271,9 +275,8 @@
                     continue;
                 }
 
-                SkScalar thickness = style.getDecorationThicknessMultiplier();
-                //
-                SkScalar position = 0;
+                SkScalar thickness = computeDecorationThickness(style);
+                SkScalar position = computeDecorationPosition(style);
                 switch (decoration) {
                     case TextDecoration::kUnderline:
                         position = -run->correctAscent() + thickness;
@@ -286,7 +289,6 @@
                         break;
                     }
                     default:
-                        // TODO: can we actually get here?
                         SkASSERT(false);
                         break;
                 }
@@ -299,7 +301,7 @@
                 SkPaint paint;
                 SkPath path;
                 this->computeDecorationPaint(paint, clip, style, path);
-                paint.setStrokeWidth(thickness);
+                paint.setStrokeWidth(thickness * style.getDecorationThicknessMultiplier());
 
                 switch (style.getDecorationStyle()) {
                     case TextDecorationStyle::kWavy:
@@ -326,6 +328,28 @@
         });
 }
 
+SkScalar TextLine::computeDecorationThickness(const TextStyle& style) const {
+    SkFontMetrics fontMetrics;
+    style.getFontMetrics(&fontMetrics);
+    if ((fontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlineThicknessIsValid_Flag) &&
+         fontMetrics.fUnderlineThickness > 0) {
+        return fontMetrics.fUnderlineThickness;
+    } else {
+        return style.getFontSize() / 14.0f;
+    }
+}
+
+SkScalar TextLine::computeDecorationPosition(const TextStyle& style) const {
+    SkFontMetrics fontMetrics;
+    style.getFontMetrics(&fontMetrics);
+    if ((fontMetrics.fFlags & SkFontMetrics::FontMetricsFlags::kUnderlinePositionIsValid_Flag) &&
+         fontMetrics.fUnderlinePosition > 0) {
+        return fontMetrics.fUnderlinePosition;
+    } else {
+        return style.getFontSize() / 14.0f;
+    }
+}
+
 void TextLine::computeDecorationPaint(SkPaint& paint,
                                       SkRect clip,
                                       const TextStyle& style,
@@ -462,13 +486,9 @@
             }
 
             // Shape the ellipsis
-            Run* cached = fEllipsisCache.find(cluster->font());
-            if (cached == nullptr) {
-                cached = shapeEllipsis(ellipsis, cluster->run());
-            } else {
-                cached->setMaster(fMaster);
-            }
-            fEllipsis = std::make_shared<Run>(*cached);
+            Run* run = shapeEllipsis(ellipsis, cluster->run());
+            run->setMaster(fMaster);
+            fEllipsis = std::make_shared<Run>(*run);
 
             // See if it fits
             if (width + fEllipsis->advance().fX > maxWidth) {
@@ -499,14 +519,14 @@
         void commitRunInfo() override {}
 
         Buffer runBuffer(const RunInfo& info) override {
-            fRun = fEllipsisCache.set(info.fFont,
-                                      Run(nullptr, info, fLineHeight, 0, 0));
+            fRun = new  Run(nullptr, info, 0, fLineHeight, 0, 0);
             return fRun->newRunBuffer();
         }
 
         void commitRunBuffer(const RunInfo& info) override {
             fRun->fAdvance.fX = info.fAdvance.fX;
             fRun->fAdvance.fY = fRun->advance().fY;
+            fRun->fPlaceholder = nullptr;
         }
 
         void commitLine() override {}
@@ -531,6 +551,15 @@
 
     SkASSERT(intersectedSize(run->textRange(), textRange) >= 0);
 
+    if (run->placeholder() != nullptr) {
+        SkASSERT(textRange == run->textRange());
+        pos = 0;
+        size = 1;
+        clippingNeeded = false;
+        return SkRect::MakeXYWH(
+                0, sizes().runTop(run), run->calculateWidth(0, 1, false), run->calculateHeight());
+    }
+
     // Find [start:end] clusters for the text
     bool found;
     ClusterIndex startIndex;
@@ -569,8 +598,6 @@
     clip.fRight -= rightCorrection;
     clippingNeeded = leftCorrection != 0 || rightCorrection != 0;
 
-    // SkDebugf("measureTextInsideOneRun: '%s'[%d:%d]\n", text.begin(), pos, pos + size);
-
     return clip;
 }
 
@@ -730,13 +757,13 @@
             }
         }
 
-        auto* style = &block->fStyle;
-        if (start != EMPTY_INDEX && style->matchOneAttribute(styleType, *prevStyle)) {
+        auto& style = block->fStyle;
+        if (start != EMPTY_INDEX && style.matchOneAttribute(styleType, *prevStyle)) {
             size += intersect.width();
             continue;
         } else if (start == EMPTY_INDEX ) {
             // First time only
-            prevStyle = style;
+            prevStyle = &style;
             size = intersect.width();
             start = intersect.start;
             continue;
@@ -746,7 +773,7 @@
         offsetX += width;
 
         // Start all over again
-        prevStyle = style;
+        prevStyle = &style;
         start = intersect.start;
         size = intersect.width();
     }
diff --git a/modules/skparagraph/src/TextLine.h b/modules/skparagraph/src/TextLine.h
index 91d01b4..b625940 100644
--- a/modules/skparagraph/src/TextLine.h
+++ b/modules/skparagraph/src/TextLine.h
@@ -44,13 +44,11 @@
         return fAdvance.fX + (fEllipsis != nullptr ? fEllipsis->fAdvance.fX : 0);
     }
     SkScalar shift() const { return fShift; }
-    SkScalar widthWithSpaces() const { return fWidthWithSpaces; }
     SkVector offset() const;
 
     SkScalar alphabeticBaseline() const { return fSizes.alphabeticBaseline(); }
     SkScalar ideographicBaseline() const { return fSizes.ideographicBaseline(); }
     SkScalar baseline() const { return fSizes.baseline(); }
-    SkScalar roundingDelta() const { return fSizes.delta(); }
 
     using StyleVisitor = std::function<SkScalar(TextRange textRange, const TextStyle& style,
                                                 SkScalar offsetX)>;
@@ -79,6 +77,9 @@
 
     TextAlign assumedTextAlign() const;
 
+    void setMaxRunMetrics(const LineMetrics& metrics) { fMaxRunMetrics = metrics; }
+    LineMetrics getMaxRunMetrics() const { return fMaxRunMetrics; }
+
 private:
 
     Run* shapeEllipsis(const SkString& ellipsis, Run* run);
@@ -102,6 +103,9 @@
     void computeDecorationPaint(SkPaint& paint, SkRect clip, const TextStyle& style,
                                 SkPath& path) const;
 
+    SkScalar computeDecorationThickness(const TextStyle& style) const;
+    SkScalar computeDecorationPosition(const TextStyle& style) const;
+
     bool contains(const Cluster* cluster) const {
         return fTextRange.contains(cluster->textRange());
     }
@@ -119,13 +123,11 @@
     SkScalar fShift;                    // Left right
     SkScalar fWidthWithSpaces;
     std::shared_ptr<Run> fEllipsis;     // In case the line ends with the ellipsis
-    LineMetrics fSizes;                 // Line metrics as a max of all run metrics
+    LineMetrics fSizes;                 // Line metrics as a max of all run metrics and struts
+    LineMetrics fMaxRunMetrics;         // No struts - need it for GetRectForRange(max height)
     bool fHasBackground;
     bool fHasShadows;
     bool fHasDecorations;
-
-    // TODO: use for ellipsis the common cache
-    static SkTHashMap<SkFont, Run> fEllipsisCache;  // All found so far shapes of ellipsis
 };
 }  // namespace textlayout
 }  // namespace skia
diff --git a/modules/skparagraph/src/TextStyle.cpp b/modules/skparagraph/src/TextStyle.cpp
index 5f82452..0487aa1 100644
--- a/modules/skparagraph/src/TextStyle.cpp
+++ b/modules/skparagraph/src/TextStyle.cpp
@@ -26,9 +26,25 @@
     fHasForeground = false;
     fTextBaseline = TextBaseline::kAlphabetic;
     fLocale = "";
+    fIsPlaceholder = false;
+}
+
+TextStyle:: TextStyle(const TextStyle& other, bool placeholder) {
+    fColor = other.fColor;
+    fDecoration = other.fDecoration;
+    fHasBackground = other.fHasBackground;
+    fHasForeground = other.fHasForeground;
+    fBackground = other.fBackground;
+    fForeground = other.fForeground;
+    fIsPlaceholder = placeholder;
 }
 
 bool TextStyle::equals(const TextStyle& other) const {
+
+    if (fIsPlaceholder || other.fIsPlaceholder) {
+        return false;
+    }
+
     if (fColor != other.fColor) {
         return false;
     }
@@ -124,6 +140,9 @@
 
 void TextStyle::getFontMetrics(SkFontMetrics* metrics) const {
     SkFont font(fTypeface, fFontSize);
+    font.setEdging(SkFont::Edging::kAntiAlias);
+    font.setSubpixel(true);
+    font.setHinting(SkFontHinting::kSlight);
     font.getMetrics(metrics);
     if (fHeightOverride) {
         auto multiplier = fHeight * fFontSize;
diff --git a/modules/skparagraph/src/TextWrapper.cpp b/modules/skparagraph/src/TextWrapper.cpp
index 8792a5a..34760c6 100644
--- a/modules/skparagraph/src/TextWrapper.cpp
+++ b/modules/skparagraph/src/TextWrapper.cpp
@@ -152,6 +152,7 @@
     fEndLine = TextStretch(span.begin(), span.begin(), parent->strutForceHeight());
     auto end = span.end() - 1;
     auto start = span.begin();
+    LineMetrics maxRunMetrics;
     while (fEndLine.endCluster() != end) {
         reset();
 
@@ -176,10 +177,26 @@
                 maxWidth != std::numeric_limits<SkScalar>::max() &&
                 !ellipsisStr.isEmpty();
         // TODO: perform ellipsis work here
+
+        // Deal with placeholder clusters == runs[@size==1]
+        for (auto cluster = fEndLine.startCluster(); cluster <= fEndLine.endCluster(); ++cluster) {
+            if (cluster->run()->placeholder() != nullptr) {
+                SkASSERT(cluster->run()->size() == 1);
+                // Update the placeholder metrics so we can get the placeholder positions later
+                // and the line metrics (to make sure the placeholder fits)
+                cluster->run()->updateMetrics(&fEndLine.metrics());
+            }
+        }
+
+        // Before we update the line metrics with struts,
+        // let's save it for GetRectsForRange(RectHeightStyle::kMax)
+        maxRunMetrics = fEndLine.metrics();
+
         if (parent->strutEnabled()) {
             // Make sure font metrics are not less than the strut
             parent->strutMetrics().updateLineMetrics(fEndLine.metrics());
         }
+
         fMaxIntrinsicWidth = SkMaxScalar(fMaxIntrinsicWidth, fEndLine.width());
         // TODO: keep start/end/break info for text and runs but in a better way that below
         TextRange text(fEndLine.startCluster()->textRange().start, fEndLine.endCluster()->textRange().end + 1);
@@ -199,10 +216,12 @@
                 fEndLine.metrics(),
                 needEllipsis);
 
+        parent->lines().back().setMaxRunMetrics(maxRunMetrics);
+
         // Start a new line
         fHeight += fEndLine.metrics().height();
 
-        if (!fHardLineBreak) {
+        if (!fHardLineBreak || startLine != end) {
             fEndLine.clean();
         }
         fEndLine.startFrom(startLine, pos);
@@ -215,6 +234,7 @@
     }
 
     if (fHardLineBreak) {
+
         // Last character is a line break
         if (parent->strutEnabled()) {
             // Make sure font metrics are not less than the strut
@@ -231,6 +251,7 @@
                 SkVector::Make(0, fEndLine.metrics().height()),
                 fEndLine.metrics(),
                 false);
+        parent->lines().back().setMaxRunMetrics(maxRunMetrics);
     }
 }
 
diff --git a/modules/skparagraph/src/TextWrapper.h b/modules/skparagraph/src/TextWrapper.h
index 78299e9..b94e146 100644
--- a/modules/skparagraph/src/TextWrapper.h
+++ b/modules/skparagraph/src/TextWrapper.h
@@ -52,7 +52,6 @@
         inline LineMetrics& metrics() { return fMetrics; }
         inline size_t startPos() const { return fStart.position(); }
         inline size_t endPos() const { return fEnd.position(); }
-        inline size_t breakPos() const { return fBreak.position(); }
         bool endOfCluster() { return fEnd.position() == fEnd.cluster()->endPos(); }
         bool endOfWord() {
             return endOfCluster() &&
@@ -91,27 +90,15 @@
             fWidth = 0;
         }
 
-        void nextBreakPos(Cluster* endOfClusters) {
-            if (fBreak.position() == fBreak.cluster()->endPos()) {
-                if (fBreak.cluster() < endOfClusters) {
-                    fBreak.move(true);
-                } else {
-                    fBreak.setPosition(0);
-                }
-            } else {
-                fBreak.setPosition(fBreak.cluster()->endPos());
-            }
-        }
-
         void saveBreak() {
             fWidthWithGhostSpaces = fWidth;
             fBreak = fEnd;
         }
 
-        void restoreBreak() { fEnd = fBreak; }
-
         void trim() {
-            fWidth -= (fEnd.cluster()->width() - fEnd.cluster()->trimmedWidth(fEnd.position()));
+            if (fEnd.cluster()->run()->placeholder() == nullptr) {
+                fWidth -= (fEnd.cluster()->width() - fEnd.cluster()->trimmedWidth(fEnd.position()));
+            }
         }
 
         void trim(Cluster* cluster) {
diff --git a/tests/SkParagraphTest.cpp b/tests/SkParagraphTest.cpp
index 71691b8..a9015ec 100644
--- a/tests/SkParagraphTest.cpp
+++ b/tests/SkParagraphTest.cpp
@@ -21,6 +21,7 @@
 SkScalar EPSILON100 = 0.01f;
 SkScalar EPSILON50 = 0.02f;
 SkScalar EPSILON20 = 0.05f;
+SkScalar EPSILON10 = 0.1f;
 SkScalar EPSILON5 = 0.20f;
 SkScalar EPSILON2 = 0.50f;
 
@@ -175,33 +176,749 @@
     }
 }
 
-DEF_TEST(SkParagraph_InlinePlaceholderParagraph, reporter) { SkDebugf("Not implemented yet.\n"); }
+// Checked: DIFF+ (half of the letter spacing before the text???)
+DEF_TEST(SkParagraph_InlinePlaceholderParagraph, reporter) {
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder1(50, 50, PlaceholderAlignment::kBaseline, TextBaseline::kAlphabetic, 0);
+    builder.addPlaceholder(placeholder1);
+    builder.addText(text);
+    builder.addPlaceholder(placeholder1);
+
+    PlaceholderStyle placeholder2(5, 50, PlaceholderAlignment::kBaseline, TextBaseline::kAlphabetic, 50);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2);
+    builder.addText(text);
+    builder.addPlaceholder(placeholder2);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder1);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder1);
+    builder.addText(text);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    auto boxes = paragraph->getRectsForRange(0, 3, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorRED, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+
+    boxes = paragraph->getRectsForRange(0, 3, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorGREEN, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+
+    boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+
+    boxes = paragraph->getRectsForRange(4, 17, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 7);
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.left(), 90.921f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.top(), 50, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.right(), 90.921f + 50 - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.bottom(), 100, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[3].rect.left(), 231.343f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[3].rect.top(), 50, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[3].rect.right(), 231.343f + 50 - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[3].rect.bottom(), 100, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[4].rect.left(), 281.343f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[4].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[4].rect.right(), 281.343f + 5 - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[4].rect.bottom(), 50, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[6].rect.left(), 336.343f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[6].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[6].rect.right(), 336.343f + 5 - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[6].rect.bottom(), 50, EPSILON100));
+}
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
 DEF_TEST(SkParagraph_InlinePlaceholderBaselineParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderBaselineParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder(55, 50, PlaceholderAlignment::kBaseline, TextBaseline::kAlphabetic, 38.347f);
+    builder.addPlaceholder(placeholder);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 90.921f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f + 55 - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 50, EPSILON100));
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    boxes = paragraph->getRectsForRange(5, 6, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 75.324f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 14.226f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 44.694f, EPSILON100));
 }
-DEF_TEST(SkParagraph_InlinePlaceholderAboveBaselineParagraphh, reporter) {
-    SkDebugf("Not implemented yet.\n");
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
+DEF_TEST(SkParagraph_InlinePlaceholderAboveBaselineParagraph, reporter) {
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderAboveBaselineParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder(55, 50, PlaceholderAlignment::kAboveBaseline, TextBaseline::kAlphabetic, 903129.129308f);
+    builder.addPlaceholder(placeholder);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 90.921f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), -0.347f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f + 55 - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 49.652f, EPSILON100));
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    boxes = paragraph->getRectsForRange(5, 6, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 75.324f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 25.531f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 56, EPSILON100));
 }
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
 DEF_TEST(SkParagraph_InlinePlaceholderBelowBaselineParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderBelowBaselineParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder(55, 50, PlaceholderAlignment::kBelowBaseline, TextBaseline::kAlphabetic, 903129.129308f);
+    builder.addPlaceholder(placeholder);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 90.921f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 24, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f + 55 - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 74, EPSILON100));
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    boxes = paragraph->getRectsForRange(5, 6, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 75.324f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), -0.121f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f - 0.5f, EPSILON2));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 30.347f, EPSILON100));
 }
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
 DEF_TEST(SkParagraph_InlinePlaceholderBottomParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderBottomParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder(55, 50, PlaceholderAlignment::kBottom, TextBaseline::kAlphabetic, 0);
+    builder.addPlaceholder(placeholder);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 90.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f + 55 - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 50, EPSILON100));
+
+    boxes = paragraph->getRectsForRange(0, 1, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 0.5f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 19.531f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 16.097f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 50, EPSILON100));
 }
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
 DEF_TEST(SkParagraph_InlinePlaceholderTopParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderTopParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder(55, 50, PlaceholderAlignment::kTop, TextBaseline::kAlphabetic, 0);
+    builder.addPlaceholder(placeholder);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 90.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f + 55 - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 50, EPSILON100));
+
+    boxes = paragraph->getRectsForRange(0, 1, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 0.5f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 16.097f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 30.468f, EPSILON100));
 }
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
 DEF_TEST(SkParagraph_InlinePlaceholderMiddleParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderMiddleParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder(55, 50, PlaceholderAlignment::kMiddle, TextBaseline::kAlphabetic, 0);
+    builder.addPlaceholder(placeholder);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 90.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f + 55 - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 50, EPSILON100));
+
+    boxes = paragraph->getRectsForRange(5, 6, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 75.324f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 9.765f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 40.234f, EPSILON100));
 }
-DEF_TEST(SkParagraph_InlinePlaceholderIdeographicBaselineParagraphh, reporter) {
-    SkDebugf("Not implemented yet.\n");
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
+DEF_TEST(SkParagraph_InlinePlaceholderIdeographicBaselineParagraph, reporter) {
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderIdeographicBaselineParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "給能上目秘使";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Source Han Serif CN")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+    PlaceholderStyle placeholder(55, 50, PlaceholderAlignment::kBaseline, TextBaseline::kIdeographic, 38.347f);
+    builder.addPlaceholder(placeholder);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 162.5f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 162.5f + 55 - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 50, EPSILON100));
+
+    boxes = paragraph->getRectsForRange(5, 6, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 135.5f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 4.703f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 162.5f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 42.065f, EPSILON100));
 }
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
 DEF_TEST(SkParagraph_InlinePlaceholderBreakParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderBreakParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder1(50, 50, PlaceholderAlignment::kBaseline, TextBaseline::kAlphabetic, 50);
+    PlaceholderStyle placeholder2(25, 25, PlaceholderAlignment::kBaseline, TextBaseline::kAlphabetic, 12.5f);
+
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder1);
+    builder.addText(text);
+
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2); // 4 + 1
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2); // 6 + 1
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2); // 7 + 1
+
+    builder.addPlaceholder(placeholder1);
+    builder.addText(text);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2);
+
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder1);
+
+    builder.addText(text);
+
+    builder.addPlaceholder(placeholder2);
+
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth - 100);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kTight;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    auto boxes = paragraph->getRectsForRange(0, 3, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorRED, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+
+    boxes = paragraph->getRectsForRange(175, 176, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorGREEN, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 1);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 31.695f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 218.531f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 47.292f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 249, EPSILON100));
+
+    boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+
+    boxes = paragraph->getRectsForRange(4, 45, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+    REPORTER_ASSERT(reporter, boxes.size() == 30);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 59.726f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 26.378f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 90.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 56.847f, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[11].rect.left(), 606.343f - 0.5f, EPSILON20));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[11].rect.top(), 38, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[11].rect.right(), 631.343f - 0.5f, EPSILON20));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[11].rect.bottom(), 63, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[17].rect.left(), 0.5f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[17].rect.top(), 63.5f, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[17].rect.right(), 50.5f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[17].rect.bottom(), 113.5f, EPSILON100));
 }
+
+// Checked: DIFF+ (half of the letter spacing before the text???)
 DEF_TEST(SkParagraph_InlinePlaceholderGetRectsParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    sk_sp<TestFontCollection> fontCollection = sk_make_sp<TestFontCollection>();
+    TestCanvas canvas("SkParagraph_InlinePlaceholderGetRectsParagraph.png");
+    if (!fontCollection->fontsFound()) return;
+
+    const char* text = "012 34";
+
+    ParagraphStyle paragraph_style;
+    paragraph_style.turnHintingOff();
+    paragraph_style.setMaxLines(14);
+    ParagraphBuilderImpl builder(paragraph_style, fontCollection);
+
+    TextStyle text_style;
+    text_style.setFontFamilies({SkString("Roboto")});
+    text_style.setColor(SK_ColorBLACK);
+    text_style.setFontSize(26);
+    text_style.setWordSpacing(5);
+    text_style.setLetterSpacing(1);
+    text_style.setDecoration(TextDecoration::kUnderline);
+    text_style.setDecorationColor(SK_ColorBLACK);
+    builder.pushStyle(text_style);
+    builder.addText(text);
+
+    PlaceholderStyle placeholder1(50, 50, PlaceholderAlignment::kBaseline, TextBaseline::kAlphabetic, 50);
+    PlaceholderStyle placeholder2(5, 20, PlaceholderAlignment::kBaseline, TextBaseline::kAlphabetic, 10);
+
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2); // 8 + 1
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2); // 5 + 1
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder1); // 8 + 0
+
+    builder.addText(text);
+
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder2); // 1 + 2
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder2); // 1 + 2
+
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);
+    builder.addText(text);  // 11
+
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2);
+    builder.addPlaceholder(placeholder1);
+    builder.addPlaceholder(placeholder2);
+
+    builder.addText(text);
+
+    builder.pop();
+
+    auto paragraph = builder.Build();
+    paragraph->layout(TestCanvasWidth);
+    paragraph->paint(canvas.get(), 0, 0);
+
+    RectHeightStyle rect_height_style = RectHeightStyle::kMax;
+    RectWidthStyle rect_width_style = RectWidthStyle::kTight;
+
+    auto boxes = paragraph->GetRectsForPlaceholders();
+    canvas.drawRects(SK_ColorRED, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 34);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 90.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 140.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 50, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[16].rect.left(), 800.921f - 0.5f, EPSILON20));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[16].rect.top(), 0, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[16].rect.right(), 850.921f - 0.5f, EPSILON20));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[16].rect.bottom(), 50, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[33].rect.left(), 503.382f - 0.5f, EPSILON10));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[33].rect.top(), 160, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[33].rect.right(), 508.382f - 0.5f, EPSILON10));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[33].rect.bottom(), 180, EPSILON100));
+
+    boxes = paragraph->getRectsForRange(30, 50, rect_height_style, rect_width_style);
+    canvas.drawRects(SK_ColorBLUE, boxes);
+
+    REPORTER_ASSERT(reporter, boxes.size() == 8);
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.left(), 216.097f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.top(), 60, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.right(), 290.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[0].rect.bottom(), 120, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.left(), 290.921f - 0.5f, EPSILON20));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.top(), 60, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.right(), 340.921f - 0.5f, EPSILON20));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[1].rect.bottom(), 120, EPSILON100));
+
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[2].rect.left(), 340.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[2].rect.top(), 60, EPSILON100));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[2].rect.right(), 345.921f - 0.5f, EPSILON50));
+    REPORTER_ASSERT(reporter, SkScalarNearlyEqual(boxes[2].rect.bottom(), 120, EPSILON100));
 }
 
 // Checked: NO DIFF
@@ -1016,9 +1733,8 @@
     }
 }
 
-// TODO: Fix decorations
 DEF_TEST(SkParagraph_WavyDecorationParagraph, reporter) {
-    SkDebugf("Not implemented yet.\n");
+    SkDebugf("TODO: Fix decorations\n");
 }
 
 // Checked: NO DIFF
@@ -2694,8 +3410,9 @@
     canvas.drawRects(SK_ColorGREEN, result);
 }
 
-// TODO: Not soon
-DEF_TEST(SkParagraph_HyphenBreakParagraph, reporter) { SkDebugf("Not implemented yet.\n"); }
+DEF_TEST(SkParagraph_HyphenBreakParagraph, reporter) {
+    SkDebugf("Hyphens are not implemented, and will not be implemented soon.\n");
+}
 
 // Checked: DIFF (line breaking)
 DEF_TEST(SkParagraph_RepeatLayoutParagraph, reporter) {
@@ -3569,7 +4286,9 @@
 }
 
 // TODO: Implement font features
-DEF_TEST(SkParagraph_FontFeaturesParagraph, reporter) { SkDebugf("Not implemented yet.\n"); }
+DEF_TEST(SkParagraph_FontFeaturesParagraph, reporter) {
+    SkDebugf("TODO: Font features\n");
+}
 
 // Not in Minikin
 DEF_TEST(SkParagraph_WhitespacesInMultipleFonts, reporter) {