Closed Bug 2038422 Opened 19 days ago Closed 14 days ago

MVS (U+180E) is rendered with zero advance in Mongolian text, even when the font's GSUB rule produces a visible mvs.narrow / mvs.wide glyph

Categories

(Core :: Layout: Text and Fonts, defect, P3)

Firefox 152
defect

Tracking

()

RESOLVED FIXED
152 Branch
Tracking Status
firefox152 --- fixed

People

(Reporter: satsrag, Assigned: jfkthame)

References

Details

Attachments

(3 files)

User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36

Steps to reproduce:

  1. Open the attached HTML file (firefox-mvs-bug-report.html) in Firefox 150.
  2. Wait for Noto Sans Mongolian to load via Google Fonts CDN.
  3. Look at the two test rows at 200px font size:
    • ᠠᠨ᠎ᠠ (U+1820 U+1828 U+180E U+1820)
    • ᠠᠨ᠎ᠢ (U+1820 U+1828 U+180E U+1822)
  4. Compare the JavaScript-measured widths and the verdict at the bottom.
  5. Open the same HTML file in Chrome to verify the bug is Firefox-specific (Chrome renders it correctly).

Actual results:

The MVS (U+180E) glyph is stripped to zero advance, breaking Mongolian narrow-vowel typography:

Input Predicted (px) Firefox actual (px) Diff (px / font-units)
ᠠᠨ᠎ᠠ 336.60 325.60 -11 px / -55 fu (= mvs.narrow advance, exactly)
ᠠᠨ᠎ᠢ 404.20 352.20 -52 px / -260 fu (= mvs.wide advance, exactly)

The diffs match the MVS glyph's advance exactly. Surrounding letters are shaped correctly (ᠨ takes N.fina.mvs, vowels take Aa.isol / I.isol) — only the MVS glyph itself is removed.

Chrome (also using HarfBuzz, same machine, same font) renders the same HTML correctly with the proper visible MVS spacing → the bug is Firefox-specific post-processing, not HarfBuzz or the font.

Root cause

In gfx/thebes/gfxHarfBuzzShaper.cpp (~line 1742, after the hb_shape call):

// Check for default-ignorable char that didn't get filtered, combined,                                                                                                                                         
// etc by the shaping process, and remove from the run.                                                                                                                                                           
// (This may be done within harfbuzz eventually.)                                                                                                                                                                 
if (glyphsInClump == 1 && baseCharIndex + 1 == endCharIndex &&                                                                                                                                                    
    aShapedText->FilterIfIgnorable(aOffset + baseCharIndex,                                                                                                                                                       
                                   aText[baseCharIndex])) {                                                                                                                                                       
  glyphStart = glyphEnd;                                                                                                                                                                                          
  charStart = charEnd;                                                                                                                                                                                            
  continue;  // glyph silently dropped                                                                                                                                                                          
}                                                                                                                                                                                                                 
                                                          
And gfx/thebes/gfxFont.cpp::FilterIfIgnorable (~line 848):                                                                                                                                                        
                                                          
bool gfxShapedText::FilterIfIgnorable(uint32_t aIndex, uint32_t aCh) {                                                                                                                                            
  if (IsIgnorable(aCh)) {                                                                                                                                                                                         
    // Exception only for Letter category (Hangul fillers, bug 1238243)
    if (GetGenCategory(aCh) == nsUGenCategory::kLetter && /* … */) {                                                                                                                                              
      return false;                                                                                                                                                                                               
    }                                                                                                                                                                                                             
    g.SetComplex(/* … */).SetMissing();   // becomes zero-width                                                                                                                                                   
    return true;                                                                                                                                                                                                  
  }
  return false;                                                                                                                                                                                                   
}                                                         
                                                                                                                                                                                                                  
MVS is gc=Cf and Default_Ignorable_Code_Point=Yes, so it falls past the Letter exception (added in bug 1238243 for Hangul fillers) and is unconditionally hidden. But the font's GSUB deliberately produces       
mvs.narrow=55 / mvs.wide=260 glyphs with non-zero advances — Firefox is overriding the font's intent.                                                                                                             
                                                                                                                                                                                                                  
Cross-verified that HarfBuzz itself does NOT strip the glyph: hb-shape returns the visible mvs.narrow / mvs.wide glyphs identically across HarfBuzz 8.3.0 (Ubuntu 24.04), 13.1.1 (Firefox-bundled, built from     
source for testing), and 14.2.0 (current Homebrew).


Expected results:

```markdown
The MVS glyph should render with its visible advance, as produced by the font's GSUB rule and as returned by HarfBuzz:                                                                                            
                                                                                                                                                                                                                  
| Input | Glyph sequence (advance, em=1000) | Total fu | Expected px @ 200pt |                                                                                                                                    
|-------|------------------------------------|----------|---------------------|                                                                                                                                   
| ᠠᠨ᠎ᠠ   | AA.init=786 \| N.fina.mvs=427 \| **mvs.narrow=55** \| Aa.isol=415 | 1683 | 336.6 px |                                                                                                                   
| ᠠᠨ᠎ᠢ   | AA.init=786 \| A.fina=427 \| **mvs.wide=260** \| I.isol=548       | 2021 | 404.2 px |                                                                                                                   
                                                                                                                                                                                                                  
Verified by `hb-shape --script=Mong` against the same font.                                                                                                                                                       
                                                                                                                                                                                                                  
Chrome renders these widths correctly.                                                                                                                                                                            
                                                          
## Suggested fix                                                                                                                                                                                                  
                                                          
Either:

1. **Skip the filter when HarfBuzz produced a non-zero advance** — i.e. the font deliberately gave the codepoint a visible glyph; trust the font.                                                                 
2. **Add a context-aware exception** for default-ignorable Cf codepoints used as visible-width markers in their primary script (MVS in `mong`). Analogous to the Hangul-filler exception added in bug 1238243.
                                                                                                                                                                                                                  
Option 1 is preferable — it's font-driven rather than hard-coding script knowledge into the shaper.                                                                                                               
                                                                                                                                                                                                                  
## Affected users                                                                                                                                                                                                 
                                                          
Mongolian (traditional script) typography in Firefox is broken anywhere MVS is used, which includes most multi-syllable words ending in vowels with gender-suffix patterns. Visible effect: the "narrow vowel     
space" gap disappears, characters appear too tightly packed.
                                                                                                                                                                                                                  
The same Firefox post-processing affects any other default-ignorable Cf codepoint used as a visible-width marker by a font's GSUB.

User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36

Steps to reproduce:

  1. Open the attached HTML file (firefox-mvs-bug-report.html) in Firefox 150.
  2. Wait for Noto Sans Mongolian to load via Google Fonts CDN.
  3. Look at the two test rows at 200px font size:
    • ᠠᠨ᠎ᠠ (U+1820 U+1828 U+180E U+1820)
    • ᠠᠨ᠎ᠢ (U+1820 U+1828 U+180E U+1822) ─
  4. Compare the JavaScript-measured widths and the verdict at the bottom.
  5. Open the same HTML file in Chrome to verify the bug is Firefox-specific (Chrome renders it correctly).

Actual results:

The MVS (U+180E) glyph is stripped to zero advance, breaking Mongolian narrow-vowel typography:

Input Predicted (px) Firefox actual (px) Diff (px / font-units)
ᠠᠨ᠎ᠠ 336.60 325.60 -11 px / -55 fu (= mvs.narrow advance, exactly)
ᠠᠨ᠎ᠢ 404.20 352.20 -52 px / -260 fu (= mvs.wide advance, exactly)

The diffs match the MVS glyph's advance exactly. Surrounding letters are shaped correctly (ᠨ takes N.fina.mvs, vowels take Aa.isol / I.isol) — only the MVS glyph itself is removed.

Chrome (also using HarfBuzz, same machine, same font) renders the same HTML correctly with the proper visible MVS spacing → the bug is Firefox-specific post-processing, not HarfBuzz or the font.

Root cause

In gfx/thebes/gfxHarfBuzzShaper.cpp (~line 1742, after the hb_shape call):

// Check for default-ignorable char that didn't get filtered, combined,                                                                                                                                         
// etc by the shaping process, and remove from the run.                                                                                                                                                           
// (This may be done within harfbuzz eventually.)                                                                                                                                                                 
if (glyphsInClump == 1 && baseCharIndex + 1 == endCharIndex &&                                                                                                                                                    
    aShapedText->FilterIfIgnorable(aOffset + baseCharIndex,                                                                                                                                                       
                                   aText[baseCharIndex])) {                                                                                                                                                       
  glyphStart = glyphEnd;                                                                                                                                                                                          
  charStart = charEnd;                                                                                                                                                                                            
  continue;  // glyph silently dropped                                                                                                                                                                          
}  

And gfx/thebes/gfxFont.cpp::FilterIfIgnorable (~line 848):

bool gfxShapedText::FilterIfIgnorable(uint32_t aIndex, uint32_t aCh) {                                                                                                                                            
  if (IsIgnorable(aCh)) {                                                                                                                                                                                         
    // Exception only for Letter category (Hangul fillers, bug 1238243)
    if (GetGenCategory(aCh) == nsUGenCategory::kLetter && /* … */) {                                                                                                                                              
      return false;                                                                                                                                                                                               
    }                                                                                                                                                                                                             
    g.SetComplex(/* … */).SetMissing();   // becomes zero-width                                                                                                                                                   
    return true;                                                                                                                                                                                                  
  }
  return false;                                                                                                                                                                                                   
}                                                         

MVS is gc=Cf and Default_Ignorable_Code_Point=Yes, so it falls past the Letter exception (added in bug 1238243 for Hangul fillers) and is unconditionally hidden. But the font's GSUB deliberately produces
mvs.narrow=55 / mvs.wide=260 glyphs with non-zero advances — Firefox is overriding the font's intent.

Cross-verified that HarfBuzz itself does NOT strip the glyph: hb-shape returns the visible mvs.narrow / mvs.wide glyphs identically across HarfBuzz 8.3.0 (Ubuntu 24.04), 13.1.1 (Firefox-bundled, built from
source for testing), and 14.2.0 (current Homebrew).

Box 3: What should have happened? (expected results)

The MVS glyph should render with its visible advance, as produced by the font's GSUB rule and as returned by HarfBuzz:

Input Glyph sequence (advance, em=1000) Total fu Expected px @ 200pt
ᠠᠨ᠎ᠠ AA.init=786 | N.fina.mvs=427 | mvs.narrow=55 | Aa.isol=415 1683 336.6 px
ᠠᠨ᠎ᠢ AA.init=786 | A.fina=427 | mvs.wide=260 | I.isol=548 2021 404.2 px

Verified by hb-shape --script=Mong against the same font.

Chrome renders these widths correctly.

Suggested fix

Either:

  1. Skip the filter when HarfBuzz produced a non-zero advance — i.e. the font deliberately gave the codepoint a visible glyph; trust the font.
  2. Add a context-aware exception for default-ignorable Cf codepoints used as visible-width markers in their primary script (MVS in mong). Analogous to the Hangul-filler exception added in bug 1238243.

Option 1 is preferable — it's font-driven rather than hard-coding script knowledge into the shaper.

Affected users

Mongolian (traditional script) typography in Firefox is broken anywhere MVS is used, which includes most multi-syllable words ending in vowels with gender-suffix patterns. Visible effect: the "narrow vowel
space" gap disappears, characters appear too tightly packed.

The same Firefox post-processing affects any other default-ignorable Cf codepoint used as a visible-width marker by a font's GSUB.

The Bugbug bot thinks this bug should belong to the 'Core::Layout: Text and Fonts' component, and is moving the bug to that component. Please correct in case you think the bot is wrong.

Component: Untriaged → Layout: Text and Fonts
Product: Firefox → Core
Severity: -- → S3
Status: UNCONFIRMED → NEW
Ever confirmed: true
Flags: needinfo?(jfkthame)
Priority: -- → P3
See Also: → 1238243

Thanks for the report and analysis. For now, at least, I propose to do a patch along the lines of the suggested option 2, more narrowly targeted than option 1, because I'm concerned there are fonts where many of the default-ignorable controls, etc, are given "placeholder" glyphs whose display is not normally desired. So I think option 2 is a lower-risk approach to this issue.

Flags: needinfo?(jfkthame)
Assignee: nobody → jfkthame
Status: NEW → ASSIGNED

Thank you all

Pushed by jkew@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/ca65d2394ba4 https://hg.mozilla.org/integration/autoland/rev/ade22f647347 Don't filter out Mongolian Vowel Separator (or other similar format chars) if the font explicitly wants to render them. r=layout-reviewers,emilio https://github.com/mozilla-firefox/firefox/commit/e1b8ebbf687f https://hg.mozilla.org/integration/autoland/rev/88a3f724c5dd Add a test for Mongolian shaping with context-dependent MVS glyph. r=layout-reviewers,emilio

Created web-platform-tests PR https://github.com/web-platform-tests/wpt/pull/59849 for changes under testing/web-platform/tests

Regressions: 2039301
Pushed by csabou@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/f77ff0012776 https://hg.mozilla.org/integration/autoland/rev/f27f3f9cff2d Revert "Bug 2038422 - Add a test for Mongolian shaping with context-dependent MVS glyph. r=layout-reviewers,emilio" for causing wpt failures on mvs-shaping.html.

Backed out for causing wpt failures on mvs-shaping.html.

Push with failures

Failure log

Backout link

Flags: needinfo?(jfkthame)

Ugh, looks like our Linux machines are using a non-linear hinting/rasterization mode, which results in discrepancies a bit larger than a pixel once multiple glyphs are involved. I guess we can either annotate the test as failing on Linux, or relax the comparison to allow more than just accumulated floating-point error; instead it's accumulated glyph-advance rounding.

Flags: needinfo?(jfkthame)

Upstream PR was closed without merging

Pushed by jkew@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/48b368801573 https://hg.mozilla.org/integration/autoland/rev/6035eb8121d7 Don't filter out Mongolian Vowel Separator (or other similar format chars) if the font explicitly wants to render them. r=layout-reviewers,emilio https://github.com/mozilla-firefox/firefox/commit/49ec41dc5983 https://hg.mozilla.org/integration/autoland/rev/15f06a5ffa4b Add a test for Mongolian shaping with context-dependent MVS glyph. r=layout-reviewers,emilio
Status: ASSIGNED → RESOLVED
Closed: 14 days ago
Resolution: --- → FIXED
Target Milestone: --- → 152 Branch

Upstream PR merged by moz-wptsync-bot

QA Whiteboard: [qa-triage-done-c153/b152]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: