Bug 1840075 - Implement support for the OpenType BASE (baselines) table, and use it to back canvas2d TextMetrics attributes and textBaseline alignment. r=gfx-reviewers,lsalzman

Differential Revision: https://phabricator.services.mozilla.com/D181882
This commit is contained in:
Jonathan Kew 2023-06-23 19:17:56 +00:00
parent ca6cc122c2
commit c2251d42ce
24 changed files with 93 additions and 103 deletions

View File

@ -4317,14 +4317,6 @@ TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText(
const nsAString& aRawText, float aX, float aY,
const Optional<double>& aMaxWidth, TextDrawOperation aOp,
ErrorResult& aError) {
// Approximated baselines. In an ideal world, we'd read the baseline info
// directly from the font (where available). Alas we currently lack
// that functionality. These numbers are best guesses and should
// suffice for now. Both are fractions of the em ascent/descent from the
// alphabetic baseline.
const double kHangingBaselineDefault = 0.8; // fraction of ascent
const double kIdeographicBaselineDefault = 0.5; // fraction of descent
gfxFontGroup* currentFontStyle = GetCurrentFontStyle();
if (NS_WARN_IF(!currentFontStyle)) {
aError = NS_ERROR_FAILURE;
@ -4514,12 +4506,20 @@ TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText(
float offsetX = anchorX * totalWidth;
processor.mPt.x -= offsetX;
gfx::ShapedTextFlags runOrientation =
(processor.mTextRunFlags & gfx::ShapedTextFlags::TEXT_ORIENT_MASK);
nsFontMetrics::FontOrientation fontOrientation =
(runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED ||
runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT)
? nsFontMetrics::eVertical
: nsFontMetrics::eHorizontal;
// offset pt.y (or pt.x, for vertical text) based on text baseline
gfxFloat baselineAnchor;
switch (state.textBaseline) {
case CanvasTextBaseline::Hanging:
baselineAnchor = fontMetrics.emAscent * kHangingBaselineDefault;
baselineAnchor = font->GetBaselines(fontOrientation).mHanging;
break;
case CanvasTextBaseline::Top:
baselineAnchor = fontMetrics.emAscent;
@ -4528,10 +4528,10 @@ TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText(
baselineAnchor = (fontMetrics.emAscent - fontMetrics.emDescent) * .5f;
break;
case CanvasTextBaseline::Alphabetic:
baselineAnchor = 0;
baselineAnchor = font->GetBaselines(fontOrientation).mAlphabetic;
break;
case CanvasTextBaseline::Ideographic:
baselineAnchor = -fontMetrics.emDescent * kIdeographicBaselineDefault;
baselineAnchor = font->GetBaselines(fontOrientation).mIdeographic;
break;
case CanvasTextBaseline::Bottom:
baselineAnchor = -fontMetrics.emDescent;
@ -4542,11 +4542,8 @@ TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText(
// We can't query the textRun directly, as it may not have been created yet;
// so instead we check the flags that will be used to initialize it.
gfx::ShapedTextFlags runOrientation =
(processor.mTextRunFlags & gfx::ShapedTextFlags::TEXT_ORIENT_MASK);
if (runOrientation != gfx::ShapedTextFlags::TEXT_ORIENT_HORIZONTAL) {
if (runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED ||
runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT) {
if (fontOrientation == nsFontMetrics::eVertical) {
// Adjust to account for mTextRun being shaped using center baseline
// rather than alphabetic.
baselineAnchor -= (fontMetrics.emAscent - fontMetrics.emDescent) * .5f;
@ -4567,10 +4564,7 @@ TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText(
-processor.mBoundingBox.Y() - baselineAnchor;
double actualBoundingBoxDescent =
processor.mBoundingBox.YMost() + baselineAnchor;
double hangingBaseline =
fontMetrics.emAscent * kHangingBaselineDefault - baselineAnchor;
double ideographicBaseline =
-fontMetrics.emDescent * kIdeographicBaselineDefault - baselineAnchor;
auto baselines = font->GetBaselines(fontOrientation);
return new TextMetrics(
totalWidth, actualBoundingBoxLeft, actualBoundingBoxRight,
fontMetrics.maxAscent - baselineAnchor, // fontBBAscent
@ -4578,9 +4572,9 @@ TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText(
actualBoundingBoxAscent, actualBoundingBoxDescent,
fontMetrics.emAscent - baselineAnchor, // emHeightAscent
-fontMetrics.emDescent - baselineAnchor, // emHeightDescent
hangingBaseline,
-baselineAnchor, // alphabeticBaseline
ideographicBaseline);
baselines.mHanging - baselineAnchor,
baselines.mAlphabetic - baselineAnchor,
baselines.mIdeographic - baselineAnchor);
}
// If we did not actually calculate bounds, set up a simple bounding box

View File

@ -4201,6 +4201,71 @@ void gfxFont::SanitizeMetrics(gfxFont::Metrics* aMetrics,
}
}
gfxFont::Baselines gfxFont::GetBaselines(Orientation aOrientation) {
// Approximated baselines for fonts lacking actual baseline data. These are
// fractions of the em ascent/descent from the alphabetic baseline.
const double kHangingBaselineDefault = 0.8; // fraction of ascent
const double kIdeographicBaselineDefault = -0.5; // fraction of descent
// If no BASE table is present, just return synthetic values immediately.
if (!mFontEntry->HasFontTable(TRUETYPE_TAG('B', 'A', 'S', 'E'))) {
// No baseline table; just synthesize them immediately.
const Metrics& metrics = GetMetrics(aOrientation);
return Baselines{
0.0, // alphabetic
kHangingBaselineDefault * metrics.emAscent, // hanging
kIdeographicBaselineDefault * metrics.emDescent // ideographic
};
}
// Use harfbuzz to try to read the font's baseline metrics.
Baselines result{NAN, NAN, NAN};
hb_font_t* hbFont = gfxHarfBuzzShaper::CreateHBFont(this);
hb_direction_t hbDir = aOrientation == nsFontMetrics::eHorizontal
? HB_DIRECTION_LTR
: HB_DIRECTION_TTB;
hb_position_t position;
unsigned count = 0;
auto Fix2Float = [](hb_position_t f) -> gfxFloat { return f / 65536.0; };
if (hb_ot_layout_get_baseline(hbFont, HB_OT_LAYOUT_BASELINE_TAG_ROMAN, hbDir,
HB_OT_TAG_DEFAULT_SCRIPT,
HB_OT_TAG_DEFAULT_LANGUAGE, &position)) {
result.mAlphabetic = Fix2Float(position);
count++;
}
if (hb_ot_layout_get_baseline(hbFont, HB_OT_LAYOUT_BASELINE_TAG_HANGING,
hbDir, HB_OT_TAG_DEFAULT_SCRIPT,
HB_OT_TAG_DEFAULT_LANGUAGE, &position)) {
result.mHanging = Fix2Float(position);
count++;
}
if (hb_ot_layout_get_baseline(
hbFont, HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_BOTTOM_OR_LEFT, hbDir,
HB_OT_TAG_DEFAULT_SCRIPT, HB_OT_TAG_DEFAULT_LANGUAGE, &position)) {
result.mIdeographic = Fix2Float(position);
count++;
}
hb_font_destroy(hbFont);
// If we successfully read all three, we can return now.
if (count == 3) {
return result;
}
// Synthesize the baselines that we didn't find in the font.
const Metrics& metrics = GetMetrics(aOrientation);
if (isnan(result.mAlphabetic)) {
result.mAlphabetic = 0.0;
}
if (isnan(result.mHanging)) {
result.mHanging = kHangingBaselineDefault * metrics.emAscent;
}
if (isnan(result.mIdeographic)) {
result.mIdeographic = kIdeographicBaselineDefault * metrics.emDescent;
}
return result;
}
// Create a Metrics record to be used for vertical layout. This should never
// fail, as we've already decided this is a valid font. We do not have the
// option of marking it invalid (as can happen if we're unable to read

View File

@ -1624,6 +1624,13 @@ class gfxFont {
return *mVerticalMetrics;
}
struct Baselines {
gfxFloat mAlphabetic;
gfxFloat mHanging;
gfxFloat mIdeographic;
};
Baselines GetBaselines(Orientation aOrientation);
/**
* We let layout specify spacing on either side of any
* character. We need to specify both before and after

View File

@ -134,6 +134,10 @@ class MOZ_STACK_CLASS gfxOTSContext : public ots::OTSContext {
if (aTag == TRUETYPE_TAG('S', 'V', 'G', ' ')) {
return mKeepSVG ? ots::TABLE_ACTION_PASSTHRU : ots::TABLE_ACTION_DROP;
}
// Preserve BASE table; harfbuzz will sanitize it before using.
if (aTag == TRUETYPE_TAG('B', 'A', 'S', 'E')) {
return ots::TABLE_ACTION_PASSTHRU;
}
if (mKeepColorBitmaps && (aTag == TRUETYPE_TAG('C', 'B', 'D', 'T') ||
aTag == TRUETYPE_TAG('C', 'B', 'L', 'C'))) {
return ots::TABLE_ACTION_PASSTHRU;

View File

@ -4200,7 +4200,7 @@
- name: dom.textMetrics.baselines.enabled
type: RelaxedAtomicBool
value: false
value: true
mirror: always
- name: dom.textMetrics.emHeight.enabled

View File

@ -1,5 +0,0 @@
[2d.text.measure.baselines.html]
expected:
if (os == "android") and fission: [TIMEOUT, OK]
[Testing baselines]
expected: FAIL

View File

@ -1,3 +0,0 @@
[2d.text.baseline.default.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +0,0 @@
[2d.text.baseline.invalid.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +0,0 @@
[2d.text.baseline.valid.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +0,0 @@
[2d.text.draw.baseline.alphabetic.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +0,0 @@
[2d.text.draw.baseline.bottom.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,5 +0,0 @@
[2d.text.draw.baseline.hanging.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Canvas test: 2d.text.draw.baseline.hanging]
expected: FAIL

View File

@ -1,5 +0,0 @@
[2d.text.draw.baseline.ideographic.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Canvas test: 2d.text.draw.baseline.ideographic]
expected: FAIL

View File

@ -1,3 +0,0 @@
[2d.text.draw.baseline.middle.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +0,0 @@
[2d.text.draw.baseline.top.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +0,0 @@
[2d.state.saverestore.textBaseline.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +0,0 @@
[2d.text.draw.baseline.hanging.html]
[OffscreenCanvas test: 2d.text.draw.baseline.hanging]
expected: FAIL

View File

@ -1,3 +0,0 @@
[2d.text.draw.baseline.hanging.worker.html]
[2d]
expected: FAIL

View File

@ -1,5 +0,0 @@
[2d.text.draw.baseline.ideographic.html]
expected:
if (os == "android") and fission: TIMEOUT
[OffscreenCanvas test: 2d.text.draw.baseline.ideographic]
expected: FAIL

View File

@ -1,4 +0,0 @@
[2d.text.draw.baseline.ideographic.worker.html]
[2d]
expected: FAIL

View File

@ -1,4 +0,0 @@
[2d.text.measure.baselines.html]
[Testing baselines for OffscreenCanvas]
expected: FAIL

View File

@ -1,4 +0,0 @@
[2d.text.measure.baselines.worker.html]
[Testing baselines for OffscreenCanvas]
expected: FAIL

View File

@ -33,9 +33,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu
[CanvasRenderingContext2D interface: document.createElement("canvas").getContext("2d") must inherit property "getContextAttributes()" with the proper type]
expected: FAIL
[TextMetrics interface: attribute ideographicBaseline]
expected: FAIL
[VideoTrackList interface object length]
expected: FAIL
@ -186,9 +183,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu
[AudioTrack interface: attribute kind]
expected: FAIL
[TextMetrics interface: attribute hangingBaseline]
expected: FAIL
[VideoTrackList interface: existence and properties of interface prototype object]
expected: FAIL
@ -201,9 +195,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu
[VideoTrackList interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[TextMetrics interface: attribute alphabeticBaseline]
expected: FAIL
[VideoTrackList interface: attribute length]
expected: FAIL

View File

@ -2,21 +2,12 @@
[TextMetrics interface: attribute emHeightAscent]
expected: FAIL
[TextMetrics interface: attribute ideographicBaseline]
expected: FAIL
[OffscreenCanvasRenderingContext2D interface: attribute imageSmoothingQuality]
expected: FAIL
[TextMetrics interface: attribute emHeightDescent]
expected: FAIL
[TextMetrics interface: attribute hangingBaseline]
expected: FAIL
[TextMetrics interface: attribute alphabeticBaseline]
expected: FAIL
[ImageData interface: attribute colorSpace]
expected: FAIL