diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index d484420301..b344c35d9a 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -618,6 +618,18 @@ def font_size(self, font_val: float) -> None: def _gen_chars(self) -> VGroup: chars = self.get_group_class()() submobjects_char_index = 0 + non_space_chars = sum(1 for char in self.text if not char.isspace()) + if non_space_chars != len(self.submobjects): + raise ValueError( + f"Text {self.original_text!r} produced {len(self.submobjects)} " + f"glyph(s) for {non_space_chars} non-space character(s) with " + "disable_ligatures=True. This usually means the chosen font " + "implements some of its ligatures through an OpenType feature " + "that isn't disabled (e.g. 'calt', used for programming " + "ligatures like '<=' or '->' by fonts such as Fira Code), so " + "characters and glyphs no longer correspond one-to-one. Try a " + "different font to work around this." + ) for char_index in range(len(self.text)): if self.text[char_index].isspace(): space = Dot(radius=0, fill_opacity=0, stroke_opacity=0) diff --git a/tests/module/mobject/text/test_text_mobject.py b/tests/module/mobject/text/test_text_mobject.py index 2e656b90ae..d44cb7b40e 100644 --- a/tests/module/mobject/text/test_text_mobject.py +++ b/tests/module/mobject/text/test_text_mobject.py @@ -3,6 +3,8 @@ from contextlib import redirect_stdout from io import StringIO +import pytest + from manim.mobject.text.text_mobject import MarkupText, Text @@ -32,3 +34,19 @@ def warning_printed(font: str, **kwargs) -> bool: # check random string (should be warning) assert warning_printed("Manim!" * 3, warn_missing_font=True) + + +def test_gen_chars_raises_clear_error_on_glyph_mismatch(): + """``_gen_chars`` should raise a clear, actionable error instead of an + opaque ``IndexError`` when the number of rendered glyphs doesn't match + the number of non-space characters. This happens when a font implements + some of its ligatures (e.g. programming ligatures like ``<=``) through + an OpenType feature that ``disable_ligatures`` doesn't disable, such as + ``calt`` (see issue #3237). This test simulates that mismatch directly, + without depending on any particular font being installed. + """ + text = Text("ab", disable_ligatures=True) + # Simulate a font that merged "ab" into a single ligature glyph. + text.submobjects = text.submobjects[:1] + with pytest.raises(ValueError, match="glyph"): + text._gen_chars()