Skip to content

Commit

Permalink
[mathml] Fix offset of vertical glyph assembly
Browse files Browse the repository at this point in the history
When ShapeResult::CreateForStretchyMathOperator shapes vertical glyph
assemblies, the offset of each part is adjusted by the advance specified
in the corresponding MathGlyphVariantRecord table [1]. Note that this may be slightly different from the calculated ink height of the glyph,
but is faster to get. In any case the baselines of glyphs are used as a
reference when setting an offset in ShapeResults, so this CL changes the
adjustment to use the glyph ink ascent instead of the glyph ink height.

This CL fixes rendering issues for vertical stretchy MathML Operators
for fonts like Cambria Math that use non-zero ink descent for parts in
a glyph assembly.

StretchyOperatorShaperTest.GlyphVariants is tweaked to use an equivalent
stretchy.woff font (glyphs have zero ink descent) and a new test
StretchyOperatorShaperTest.GlyphVariantsCenteredOnBaseline is introduced
for a similar stretchy-centered-on-baseline.woff (glyphs have non-zero
and equal ink ascent/descent).

Finally, a WPT test is added to check that the painting of a vertical
glyph assembly with the two fonts above matches the location of their
bounding boxes.

[1] https://learn.microsoft.com/en-us/typography/opentype/spec/math

Bug: 1409380
Change-Id: I173c1cf461cebe3523e1a10aaf8d7b479bca03ad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4197134
Commit-Queue: Frédéric Wang <fwang@igalia.com>
Reviewed-by: Dominik Röttsches <drott@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1098136}
  • Loading branch information
fred-wang authored and Chromium LUCI CQ committed Jan 27, 2023
1 parent e984b02 commit 5195bd9
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 15 deletions.
Expand Up @@ -20,10 +20,15 @@ class Font;
// contains the following data for horizontal (respectively vertical) operators:
// - Glyph variants: h0, h1, h2, h3 (respectively v0, v1, v2, v3).
// - Glyph parts: non-extender h2 and extender h1 (respectively v2 and v1).
// stretchy.woff and stretchy-centered-on-baseline.woff contain similar stretchy
// constructions for horizontal and vertical arrows only. For the latter, the
// glyphs are centered on the baseline.
// For details, see createSizeVariants() and createStretchy() from
// third_party/blink/web_tests/external/wpt/mathml/tools/operator-dictionary.py
const UChar32 kLeftBraceCodePoint = '{';
const UChar32 kOverBraceCodePoint = 0x23DE;
const UChar32 kVerticalArrow = 0x295C;
const UChar32 kHorizontalArrow = 0x295A;
PLATFORM_EXPORT void retrieveGlyphForStretchyOperators(
const blink::Font operatorsWoff,
Vector<UChar32>& verticalGlyphs,
Expand Down
Expand Up @@ -1630,6 +1630,10 @@ scoped_refptr<ShapeResult> ShapeResult::CreateForStretchyMathOperator(
if (!repetition_count)
continue;
DCHECK(part_index < assembly_parameters.glyph_count);
float glyph_ink_ascent;
if (!is_horizontal_assembly) {
glyph_ink_ascent = -font->PrimaryFont()->BoundsForGlyph(part.glyph).y();
}
for (unsigned repetition_index = 0; repetition_index < repetition_count;
repetition_index++) {
unsigned glyph_index =
Expand All @@ -1644,7 +1648,7 @@ scoped_refptr<ShapeResult> ShapeResult::CreateForStretchyMathOperator(
full_advance};
if (!is_horizontal_assembly) {
GlyphOffset glyph_offset(
0, -assembly_parameters.stretch_size + part.full_advance);
0, -assembly_parameters.stretch_size + glyph_ink_ascent);
run->glyph_data_.SetOffsetAt(glyph_index, glyph_offset);
result->has_vertical_offsets_ |= (glyph_offset.y() != 0);
}
Expand Down
Expand Up @@ -51,15 +51,16 @@ class StretchyOperatorShaperTest : public FontTestBase {
// See blink/web_tests/external/wpt/mathml/tools/operator-dictionary.py and
// blink/renderer/platform/fonts/opentype/open_type_math_test_fonts.h.
TEST_F(StretchyOperatorShaperTest, GlyphVariants) {
Font math = CreateMathFont("operators.woff");
Font math = CreateMathFont("stretchy.woff");

StretchyOperatorShaper vertical_shaper(
kLeftBraceCodePoint, OpenTypeMathStretchData::StretchAxis::Vertical);
kVerticalArrow, OpenTypeMathStretchData::StretchAxis::Vertical);
StretchyOperatorShaper horizontal_shaper(
kOverBraceCodePoint, OpenTypeMathStretchData::StretchAxis::Horizontal);
kHorizontalArrow, OpenTypeMathStretchData::StretchAxis::Horizontal);

auto left_brace = math.PrimaryFont()->GlyphForCharacter(kLeftBraceCodePoint);
auto over_brace = math.PrimaryFont()->GlyphForCharacter(kOverBraceCodePoint);
auto vertical_arrow = math.PrimaryFont()->GlyphForCharacter(kVerticalArrow);
auto horizontal_arrow =
math.PrimaryFont()->GlyphForCharacter(kHorizontalArrow);

// Calculate glyph indices of stretchy operator's parts.
Vector<UChar32> v, h;
Expand All @@ -68,9 +69,9 @@ TEST_F(StretchyOperatorShaperTest, GlyphVariants) {
// Stretch operators to target sizes (in font units) 125, 250, 375, 500, 625,
// 750, 875, 1000, 1125, ..., 3750, 3875, 4000.
//
// Shaper tries glyphs over_brace/left_brace, h0/v0, h1/v1, h2/v2, h3/v3 of
// respective sizes 1000, 1000, 2000, 3000 and 4000. It returns the smallest
// glyph larger than the target size.
// Shaper tries glyphs vertical_arrow/horizontal_arrow, h0/v0, h1/v1, h2/v2,
// h3/v3 of respective sizes 1000, 1000, 2000, 3000 and 4000. It returns the
// smallest glyph larger than the target size.
const unsigned size_count = 4;
const unsigned subdivision = 8;
for (unsigned i = 0; i < size_count; i++) {
Expand Down Expand Up @@ -106,7 +107,7 @@ TEST_F(StretchyOperatorShaperTest, GlyphVariants) {
horizontal_shaper.Shape(&math, target_size);
EXPECT_EQ(TestInfo(result)->NumberOfRunsForTesting(), 1u);
EXPECT_EQ(TestInfo(result)->RunInfoForTesting(0).NumGlyphs(), 1u);
Glyph expected_variant = i ? h[0] + 2 * i : over_brace;
Glyph expected_variant = i ? h[0] + 2 * i : horizontal_arrow;
EXPECT_EQ(TestInfo(result)->GlyphForTesting(0, 0), expected_variant);
EXPECT_NEAR(TestInfo(result)->AdvanceForTesting(0, 0), (i + 1) * 1000,
kSizeError);
Expand All @@ -118,7 +119,7 @@ TEST_F(StretchyOperatorShaperTest, GlyphVariants) {
vertical_shaper.Shape(&math, target_size);
EXPECT_EQ(TestInfo(result)->NumberOfRunsForTesting(), 1u);
EXPECT_EQ(TestInfo(result)->RunInfoForTesting(0).NumGlyphs(), 1u);
Glyph expected_variant = i ? v[0] + 2 * i : left_brace;
Glyph expected_variant = i ? v[0] + 2 * i : vertical_arrow;
EXPECT_EQ(TestInfo(result)->GlyphForTesting(0, 0), expected_variant);
EXPECT_NEAR(TestInfo(result)->AdvanceForTesting(0, 0), (i + 1) * 1000,
kSizeError);
Expand Down Expand Up @@ -258,6 +259,88 @@ TEST_F(StretchyOperatorShaperTest, GlyphVariants) {
}
}

// This test performs similar checks for shaping glyph assemblies to the ones of
// StretchyOperatorShaperTest.GlyphVariants, but the glyphs involved have their
// ink ascents equal to their ink descents. The glyphs used and their advances
// should remain exactly the same. Horizontal assemblies now use the ink
// ascent/descent of the glyphs but vertical assemblies should be normalized to
// a zero ink descent (see crbug.com/1409380).
TEST_F(StretchyOperatorShaperTest, GlyphVariantsCenteredOnBaseline) {
Font math = CreateMathFont("stretchy-centered-on-baseline.woff");

StretchyOperatorShaper vertical_shaper(
kVerticalArrow, OpenTypeMathStretchData::StretchAxis::Vertical);
StretchyOperatorShaper horizontal_shaper(
kHorizontalArrow, OpenTypeMathStretchData::StretchAxis::Horizontal);

// Calculate glyph indices of stretchy operator's parts.
Vector<UChar32> v, h;
retrieveGlyphForStretchyOperators(math, v, h);

unsigned repetition_count = 5;
float overlap = 750;
float target_size = 3000 + repetition_count * (2000 - overlap);

// Metrics of horizontal assembly.
{
StretchyOperatorShaper::Metrics metrics;
horizontal_shaper.Shape(&math, target_size, &metrics);
EXPECT_NEAR(metrics.advance, target_size, kSizeError);
EXPECT_NEAR(metrics.ascent, 500, kSizeError);
EXPECT_FLOAT_EQ(metrics.descent, 500);
}

// Metrics of vertical assembly.
{
StretchyOperatorShaper::Metrics metrics;
vertical_shaper.Shape(&math, target_size, &metrics);
EXPECT_NEAR(metrics.advance, 1000, kSizeError);
EXPECT_NEAR(metrics.ascent, target_size, kSizeError);
EXPECT_FLOAT_EQ(metrics.descent, 0);
}

// Shaping of horizontal assembly.
// From left to right: h2, h1, h1, h1, ...
{
scoped_refptr<ShapeResult> result =
horizontal_shaper.Shape(&math, target_size);

EXPECT_EQ(TestInfo(result)->NumberOfRunsForTesting(), 1u);
EXPECT_EQ(TestInfo(result)->RunInfoForTesting(0).NumGlyphs(),
repetition_count + 1);
EXPECT_EQ(TestInfo(result)->GlyphForTesting(0, 0), h[2]);
EXPECT_NEAR(TestInfo(result)->AdvanceForTesting(0, 0), 3000 - overlap,
kSizeError);
for (unsigned i = 0; i < repetition_count - 1; i++) {
EXPECT_EQ(TestInfo(result)->GlyphForTesting(0, i + 1), h[1]);
EXPECT_NEAR(TestInfo(result)->AdvanceForTesting(0, i + 1), 2000 - overlap,
kSizeError);
}
EXPECT_EQ(TestInfo(result)->GlyphForTesting(0, repetition_count), h[1]);
EXPECT_NEAR(TestInfo(result)->AdvanceForTesting(0, repetition_count), 2000,
kSizeError);
}

// Shaping of vertical assembly.
// From bottom to top: v2, v1, v1, v1, ...
{
scoped_refptr<ShapeResult> result =
vertical_shaper.Shape(&math, target_size);

EXPECT_EQ(TestInfo(result)->NumberOfRunsForTesting(), 1u);
EXPECT_EQ(TestInfo(result)->RunInfoForTesting(0).NumGlyphs(),
repetition_count + 1);
for (unsigned i = 0; i < repetition_count; i++) {
EXPECT_EQ(TestInfo(result)->GlyphForTesting(0, i), v[1]);
EXPECT_NEAR(TestInfo(result)->AdvanceForTesting(0, i), 2000 - overlap,
kSizeError);
}
EXPECT_EQ(TestInfo(result)->GlyphForTesting(0, repetition_count), v[2]);
EXPECT_NEAR(TestInfo(result)->AdvanceForTesting(0, repetition_count), 3000,
kSizeError);
}
}

// See blink/web_tests/external/wpt/mathml/tools/operator-dictionary.py and
// blink/renderer/platform/fonts/opentype/open_type_math_test_fonts.h.
TEST_F(StretchyOperatorShaperTest, NonBMPCodePoint) {
Expand Down
Binary file not shown.
Binary file modified third_party/blink/web_tests/external/wpt/fonts/math/stretchy.woff
Binary file not shown.
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Painting of vertical assembly (reference)</title>
<style>
.container {
font-size: 50px;
position: absolute;
left: 1em;
top: 1em;
padding: 5px;
background: green;
width: 4em;
height: 8em;
}
</style>
<body>
<p>This test passes if you see a green rectangle and no red.</p>
<div class="container">
</div>
</body>
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html class="reftest-wait">
<head>
<meta charset="utf-8">
<title>Painting of vertical assembly</title>
<link rel="match" href="painting-stretchy-operator-001-ref.html">
<link rel="help" href="https://w3c.github.io/mathml-core/#operator-fence-separator-or-accent-mo">
<link rel="help" href="https://crbug.com/1409380">
<meta name="assert" content="Verify that vertical glyph assemblies are painted at the position of their bounding box.">
<script src="/mathml/support/fonts.js"></script>
<style>
.container {
font-size: 50px;
position: absolute;
left: 1em;
top: 1em;
padding: 5px;
background: green;
width: 4em;
height: 8em;
}
mo {
color: green;
background: red;
}
.frame {
position: absolute;
box-sizing: border-box;
border: 2px solid green;
}
@font-face {
font-family: stretchy;
src: url("/fonts/math/stretchy.woff");
}
@font-face {
font-family: stretchy-centered-on-baseline;
src: url("/fonts/math/stretchy-centered-on-baseline.woff");
}
</style>
<script>
function runTests() {
// Add a green frame around mo to avoid antialisasing/rounding issues.
Array.from(document.getElementsByTagName('mo')).forEach(mo => {
let box = mo.getBoundingClientRect();
let div = document.createElement("div");
div.className = 'frame';
div.style.left = `${box.left-1}px`;
div.style.top = `${box.top-1}px`;
div.style.width = `${box.width+1}px`;
div.style.height = `${box.height+1}px`;
document.body.appendChild(div);
});
document.documentElement.classList.remove("reftest-wait");
}
window.addEventListener("load", () => { loadAllFonts().then(runTests); });
</script>
<body>
<p>This test passes if you see a green rectangle and no red.</p>
<div class="container">
<!-- This font uses assembly glyphs with zero ink descent, which is what
Latin Modern Math does for U+007C VERTICAL LINE. -->
<math style="font-family: stretchy">
<mspace height="4em"/>
<mo stretchy="true" symmetric="true">&#x295C;</mo>
</math>
<!-- This font uses assembly glyphs with non-zero ink descent, which is what
Cambria Math does for U+007C VERTICAL LINE. -->
<math style="font-family: stretchy-centered-on-baseline">
<mspace height="4em"/>
<mo stretchy="true" symmetric="true">&#x295C;</mo>
</math>
</div>
</body>
@@ -0,0 +1,43 @@
#!/usr/bin/env python3

from utils import mathfont
import fontforge

# Create a WOFF font with glyphs for all the operator strings.
font = mathfont.create("stretchy-centered-on-baseline", "Copyright (c) 2023 Igalia S.L.")

# Set parameters for stretchy tests.
font.math.MinConnectorOverlap = mathfont.em // 2

# Make sure that underover parameters don't add extra spacing.
font.math.LowerLimitBaselineDropMin = 0
font.math.LowerLimitGapMin = 0
font.math.StretchStackBottomShiftDown = 0
font.math.StretchStackGapAboveMin = 0
font.math.UnderbarVerticalGap = 0
font.math.UnderbarExtraDescender = 0
font.math.UpperLimitBaselineRiseMin = 0
font.math.UpperLimitGapMin = 0
font.math.StretchStackTopShiftUp = 0
font.math.StretchStackGapBelowMin = 0
font.math.OverbarVerticalGap = 0
font.math.AccentBaseHeight = 0
font.math.OverbarExtraAscender = 0

# These two characters will be stretchable in both directions.
horizontalArrow = 0x295A # LEFTWARDS HARPOON WITH BARB UP FROM BAR
verticalArrow = 0x295C # UPWARDS HARPOON WITH BARB RIGHT FROM BAR

mathfont.createSizeVariants(font, aUsePUA = True, aCenterOnBaseline = True)

# Add stretchy vertical and horizontal constructions for the horizontal arrow.
mathfont.createSquareGlyph(font, horizontalArrow)
mathfont.createStretchy(font, horizontalArrow, True)
mathfont.createStretchy(font, horizontalArrow, False)

# Add stretchy vertical and horizontal constructions for the vertical arrow.
mathfont.createSquareGlyph(font, verticalArrow)
mathfont.createStretchy(font, verticalArrow, True)
mathfont.createStretchy(font, verticalArrow, False)

mathfont.save(font)
Expand Up @@ -28,7 +28,7 @@
horizontalArrow = 0x295A # LEFTWARDS HARPOON WITH BARB UP FROM BAR
verticalArrow = 0x295C # UPWARDS HARPOON WITH BARB RIGHT FROM BAR

mathfont.createSizeVariants(font)
mathfont.createSizeVariants(font, aUsePUA = True, aCenterOnBaseline = False)

# Add stretchy vertical and horizontal constructions for the horizontal arrow.
mathfont.createSquareGlyph(font, horizontalArrow)
Expand Down
Expand Up @@ -171,18 +171,24 @@ def createGlyphFromValue(aFont, aCodePoint):
g.width = 5 * em / 2
g.stroke("circular", em / 10, "square", "miter", "cleanup")

def createSizeVariants(aFont, aUsePUA = False):
def createSizeVariants(aFont, aUsePUA = False, aCenterOnBaseline = False):
if aUsePUA:
codePoint = PUA_startCodePoint
else:
codePoint = -1
for size in (0, 1, 2, 3):
g = aFont.createChar(codePoint, "v%d" % size)
drawRectangleGlyph(g, em, (size + 1) * em, 0)
if aCenterOnBaseline:
drawRectangleGlyph(g, em, (size + 1) * em / 2, (size + 1) * em / 2)
else:
drawRectangleGlyph(g, em, (size + 1) * em, 0)
if aUsePUA:
codePoint += 1
g = aFont.createChar(codePoint, "h%d" % size)
drawRectangleGlyph(g, (size + 1) * em, em, 0)
if aCenterOnBaseline:
drawRectangleGlyph(g, (size + 1) * em, em/2, em/2)
else:
drawRectangleGlyph(g, (size + 1) * em, em, 0)
if aUsePUA:
codePoint += 1

Expand Down

0 comments on commit 5195bd9

Please sign in to comment.