Commit 04ec8880 authored by Koji Ishii's avatar Koji Ishii Committed by Commit Bot

Use NGBidiParagraph for Canvas measureText

The Canvas [text-preparation-algorithm] requires to run bidi
algorithm using |TextDirection|, without |ComputedStyle|.

In LayoutNG, |NGBidiParagraph| splits the text into bidi runs
and compute bidi levels. This patch changes |NGBidiParagraph|
so that Canvas code can use it without |ComputedStyle|.

This patch changes |TextMetrics| to use |NGBidiParagraph| to
resolve bidi levels, and measur each run.

[text-preparation-algorithm]: https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm

Bug: 1010893
Change-Id: I46d8e5e3fd9e76910e86a5be71f9d7247911fbb1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2196086Reviewed-by: default avatarFernando Serboncini <fserb@chromium.org>
Commit-Queue: Koji Ishii <kojii@chromium.org>
Cr-Commit-Position: refs/heads/master@{#771602}
parent 11753154
......@@ -1231,6 +1231,7 @@ jumbo_source_set("unit_tests") {
"layout/ng/geometry/ng_box_strut_test.cc",
"layout/ng/geometry/ng_static_position_test.cc",
"layout/ng/inline/layout_ng_text_test.cc",
"layout/ng/inline/ng_bidi_paragraph_test.cc",
"layout/ng/inline/ng_caret_position_test.cc",
"layout/ng/inline/ng_fragment_item_test.cc",
"layout/ng/inline/ng_inline_cursor_test.cc",
......
......@@ -4,6 +4,7 @@
#include "third_party/blink/renderer/core/html/canvas/text_metrics.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_baselines.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_bidi_paragraph.h"
#include "third_party/blink/renderer/platform/fonts/character_range.h"
namespace blink {
......@@ -63,28 +64,54 @@ void TextMetrics::Update(const Font& font,
if (!font_data)
return;
// TODO(kojii): Need to figure out the desired behavior of |advances| when
// bidi reorder occurs.
TextRun text_run(
text, /* xpos */ 0, /* expansion */ 0,
TextRun::kAllowTrailingExpansion | TextRun::kForbidLeadingExpansion,
direction, false);
text_run.SetNormalizeSpace(true);
const FontMetrics& font_metrics = font_data->GetFontMetrics();
advances_ = font.IndividualCharacterAdvances(text_run);
// x direction
// Run bidi algorithm on the given text. Step 5 of:
// https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm
FloatRect glyph_bounds;
double real_width = font.Width(text_run, nullptr, &glyph_bounds);
String text16 = text;
text16.Ensure16Bit();
NGBidiParagraph bidi;
bidi.SetParagraph(text16, direction);
NGBidiParagraph::Runs runs;
bidi.GetLogicalRuns(text16, &runs);
float xpos = 0;
for (const auto& run : runs) {
// Measure each run.
TextRun text_run(
StringView(text, run.start, run.Length()), xpos, /* expansion */ 0,
TextRun::kAllowTrailingExpansion | TextRun::kForbidLeadingExpansion,
run.Direction(), /* directional_override */ false);
text_run.SetNormalizeSpace(true);
FloatRect run_glyph_bounds;
float run_width = font.Width(text_run, nullptr, &run_glyph_bounds);
// Accumulate the position and the glyph bounding box.
run_glyph_bounds.Move(xpos, 0);
glyph_bounds.Unite(run_glyph_bounds);
xpos += run_width;
}
double real_width = xpos;
#if DCHECK_IS_ON()
// This DCHECK is for limited time only; to use |glyph_bounds| instead of
// |BoundingBox| and make sure they are compatible.
FloatRect bbox = font.BoundingBox(text_run);
// |GetCharacterRange|, the underlying function of |BoundingBox|, clamps
// negative |MaxY| to 0. This is unintentional, and that we are not copying
// the behavior.
DCHECK_EQ(bbox.Y(), std::min(glyph_bounds.Y(), .0f));
DCHECK_EQ(bbox.MaxY(), std::max(glyph_bounds.MaxY(), .0f));
DCHECK_EQ(bbox.Width(), real_width);
if (runs.size() == 1 && direction == runs[0].Direction()) {
FloatRect bbox = font.BoundingBox(text_run);
// |GetCharacterRange|, the underlying function of |BoundingBox|, clamps
// negative |MaxY| to 0. This is unintentional, and that we are not copying
// the behavior.
DCHECK_EQ(bbox.Y(), std::min(glyph_bounds.Y(), .0f));
DCHECK_EQ(bbox.MaxY(), std::max(glyph_bounds.MaxY(), .0f));
DCHECK_EQ(bbox.Width(), real_width);
}
#endif
width_ = real_width;
......@@ -99,6 +126,7 @@ void TextMetrics::Update(const Font& font,
actual_bounding_box_right_ = glyph_bounds.MaxX() - dx;
// y direction
const FontMetrics& font_metrics = font_data->GetFontMetrics();
const float ascent = font_metrics.FloatAscent();
const float descent = font_metrics.FloatDescent();
const float baseline_y = GetFontBaseline(baseline, *font_data);
......
......@@ -15,17 +15,23 @@ NGBidiParagraph::~NGBidiParagraph() {
bool NGBidiParagraph::SetParagraph(const String& text,
const ComputedStyle& block_style) {
if (UNLIKELY(block_style.GetUnicodeBidi() == UnicodeBidi::kPlaintext))
return SetParagraph(text, base::nullopt);
return SetParagraph(text, block_style.Direction());
}
bool NGBidiParagraph::SetParagraph(
const String& text,
base::Optional<TextDirection> base_direction) {
DCHECK(!ubidi_);
ubidi_ = ubidi_open();
bool use_heuristic_base_direction =
block_style.GetUnicodeBidi() == UnicodeBidi::kPlaintext;
UBiDiLevel para_level;
if (use_heuristic_base_direction) {
para_level = UBIDI_DEFAULT_LTR;
} else {
base_direction_ = block_style.Direction();
if (base_direction) {
base_direction_ = *base_direction;
para_level = IsLtr(base_direction_) ? UBIDI_LTR : UBIDI_RTL;
} else {
para_level = UBIDI_DEFAULT_LTR;
}
ICUError error;
......@@ -38,7 +44,7 @@ bool NGBidiParagraph::SetParagraph(const String& text,
return false;
}
if (use_heuristic_base_direction)
if (!base_direction)
base_direction_ = DirectionFromLevel(ubidi_getParaLevel(ubidi_));
return true;
......@@ -60,6 +66,17 @@ unsigned NGBidiParagraph::GetLogicalRun(unsigned start,
return end;
}
void NGBidiParagraph::GetLogicalRuns(const String& text, Runs* runs) const {
DCHECK(runs->IsEmpty());
for (unsigned start = 0; start < text.length();) {
UBiDiLevel level;
unsigned end = GetLogicalRun(start, &level);
DCHECK_GT(end, start);
runs->emplace_back(start, end, level);
start = end;
}
}
void NGBidiParagraph::IndicesInVisualOrder(
const Vector<UBiDiLevel, 32>& levels,
Vector<int32_t, 32>* indices_in_visual_order_out) {
......
......@@ -5,6 +5,8 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_BIDI_PARAGRAPH_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_BIDI_PARAGRAPH_H_
#include "base/optional.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/text/text_direction.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
#include "third_party/blink/renderer/platform/wtf/forward.h"
......@@ -23,7 +25,7 @@ class ComputedStyle;
// UAX#9 and create logical runs.
// http://unicode.org/reports/tr9/
// It can also create visual runs once lines breaks are determined.
class NGBidiParagraph {
class CORE_EXPORT NGBidiParagraph {
STACK_ALLOCATED();
public:
......@@ -35,6 +37,8 @@ class NGBidiParagraph {
// Returns false on failure. Nothing other than the destructor should be
// called.
bool SetParagraph(const String&, const ComputedStyle&);
bool SetParagraph(const String&,
base::Optional<TextDirection> base_direction);
// @return the entire text is unidirectional.
bool IsUnidirectional() const {
......@@ -52,6 +56,30 @@ class NGBidiParagraph {
// http://unicode.org/reports/tr9/#The_Paragraph_Level
static TextDirection BaseDirectionForString(const StringView&);
struct Run {
Run(unsigned start, unsigned end, UBiDiLevel level)
: start(start), end(end), level(level) {
DCHECK_GT(end, start);
}
unsigned Length() const { return end - start; }
TextDirection Direction() const { return DirectionFromLevel(level); }
bool operator==(const Run& other) const {
return start == other.start && end == other.end && level == other.level;
}
unsigned start;
unsigned end;
UBiDiLevel level;
};
using Runs = Vector<Run, 32>;
// Get a list of |Run| in the logical order (before bidi reorder.)
// |text| must be the same one as |SetParagraph|.
// This is higher-level API for |GetLogicalRun|.
void GetLogicalRuns(const String& text, Runs* runs) const;
// Returns the end offset of a logical run that starts from the |start|
// offset.
unsigned GetLogicalRun(unsigned start, UBiDiLevel*) const;
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/layout/ng/inline/ng_bidi_paragraph.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
using testing::ElementsAre;
TEST(NGBidiParagraph, SetParagraphHeuristicLtr) {
String text(u"abc");
NGBidiParagraph bidi;
bidi.SetParagraph(text, base::nullopt);
EXPECT_EQ(bidi.BaseDirection(), TextDirection::kLtr);
}
TEST(NGBidiParagraph, SetParagraphHeuristicRtl) {
String text(u"\u05D0\u05D1\u05D2");
NGBidiParagraph bidi;
bidi.SetParagraph(text, base::nullopt);
EXPECT_EQ(bidi.BaseDirection(), TextDirection::kRtl);
}
TEST(NGBidiParagraph, GetLogicalRuns) {
String text(u"\u05D0\u05D1\u05D2 abc \u05D3\u05D4\u05D5");
NGBidiParagraph bidi;
bidi.SetParagraph(text, TextDirection::kRtl);
NGBidiParagraph::Runs runs;
bidi.GetLogicalRuns(text, &runs);
EXPECT_THAT(runs, ElementsAre(NGBidiParagraph::Run(0, 4, 1),
NGBidiParagraph::Run(4, 7, 2),
NGBidiParagraph::Run(7, 11, 1)));
}
} // namespace blink
<!DOCTYPE html>
<meta charset="utf-8">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
test(() => {
let canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
let ctx = canvas.getContext('2d');
let text = "اضربببببببببببببببببب";
ctx.direction="ltr";
let width_ltr = ctx.measureText(text).width;
ctx.direction="rtl";
let width_rtl = ctx.measureText(text).width;
assert_equals(width_ltr, width_rtl);
});
</script>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment