From 4816bb440c31dedbce1530c3fb458dfb306b6c20 Mon Sep 17 00:00:00 2001 From: Javier Carreira <140205325+SupernovaIa@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:02:15 +0200 Subject: [PATCH] fix(text): raise a clear error instead of IndexError on glyph/char mismatch Text._gen_chars() assumed disable_ligatures=True guarantees one glyph per non-space character, but that only holds if the font's ligatures are all implemented via the 'liga'/'dlig'/'clig'/'hlig' OpenType features that ManimPango disables. Fonts like Fira Code implement programming ligatures (e.g. '<=', '->') via 'calt' instead, which isn't covered, so Pango still merges characters into a single glyph and _gen_chars() walks off the end of self.submobjects with an opaque IndexError (e.g. via Code(..., paragraph_config={"font": "Fira Code"}), see issue #3237). This adds an upfront check that raises a clear ValueError explaining the likely cause and a workaround, instead of the bare IndexError. Note this does not fix the underlying issue -- the actual root cause (ManimPango not disabling 'calt') lives in a separate repo (ManimPango) and would need to be addressed there for Code/Text to render correctly with fonts like Fira Code. Refs #3237 --- manim/mobject/text/text_mobject.py | 12 ++++++++++++ tests/module/mobject/text/test_text_mobject.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) 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()