Skip to content

Conversation

mattt
Copy link

@mattt mattt commented Sep 29, 2025

This PR adds support for OpenType Font features and font variants using Pango markup attributes.

For example, with the changes in this PR, I can now render proportional and tabular numerals:

Proportional figures (pnum=1; default)

Screenshot 2025-09-29 at 06 06 59

Tabular / monospaced figures (tnum=1)

Screenshot 2025-09-29 at 06 07 10

@mattt
Copy link
Author

mattt commented Sep 29, 2025

Here's an script you can use to generate example outputs:

#!/usr/bin/env python3
"""
Focused test for numeral OpenType features in ManimPango.
"""

import manimpango
import os


def test_numeral_features():
    """Test various numeral-related OpenType features."""
    print("=== Testing Numeral Features ===")
    
    # Text with numbers that should show differences
    numeral_text = "0123456789"
    mixed_text = "Price: $1,234.56 (March 2023)"
    
    numeral_features = {
        "default": None,
        "oldstyle": "onum=1",  # Old-style/text figures
        "lining": "lnum=1",    # Lining figures
        "tabular": "tnum=1",   # Tabular (monospaced) figures
        "proportional": "pnum=1",  # Proportional figures
        "tabular_oldstyle": "tnum=1, onum=1",
        "tabular_lining": "tnum=1, lnum=1"
    }
    
    # Test fonts that are more likely to support numeral features
    fonts_to_test = [
        "Times New Roman",
        "Georgia",
        "SF Pro Text",
        "Avenir",
    ]
    
    for font in fonts_to_test:
        print(f"\nTesting font: {font}")
        
        for feature_name, features in numeral_features.items():
            try:
                filename = f"numerals_{font.replace(' ', '_').lower()}_{feature_name}.svg"
                
                result = manimpango.MarkupUtils.text2svg(
                    text=mixed_text,
                    font=font,
                    slant="NORMAL",
                    weight="NORMAL",
                    size=48,  # Large size to see differences clearly
                    _=None,
                    disable_liga=False,
                    file_name=filename,
                    START_X=10,
                    START_Y=60,
                    width=800,
                    height=100,
                    font_features=features
                )
                print(f"  ✓ Created: {filename} ({feature_name})")
                
            except Exception as e:
                print(f"  ✗ Error with {feature_name}: {e}")
                
        # Also test pure numbers
        try:
            filename = f"pure_numbers_{font.replace(' ', '_').lower()}.svg"
            result = manimpango.MarkupUtils.text2svg(
                text=numeral_text,
                font=font,
                slant="NORMAL",
                weight="NORMAL",
                size=64,
                _=None,
                disable_liga=False,
                file_name=filename,
                START_X=10,
                START_Y=80,
                width=600,
                height=120,
                font_features="onum=1"  # Test old-style figures
            )
            print(f"  ✓ Created: {filename} (pure numbers with onum)")
        except Exception as e:
            print(f"  ✗ Error with pure numbers: {e}")


def compare_numeral_files():
    """Compare file sizes and content for numeral features."""
    print("\n=== Comparing Numeral Files ===")
    
    # Get all numerals files
    numeral_files = [f for f in os.listdir(".") if f.startswith("numerals_") and f.endswith(".svg")]
    
    if not numeral_files:
        print("No numeral files found to compare")
        return
    
    # Group by font
    fonts = {}
    for f in numeral_files:
        parts = f.split("_")
        if len(parts) >= 3:
            font_key = "_".join(parts[1:-1])  # everything except 'numerals' and feature name
            if font_key not in fonts:
                fonts[font_key] = {}
            feature = parts[-1].replace(".svg", "")
            fonts[font_key][feature] = f
    
    for font, files in fonts.items():
        print(f"\nFont: {font}")
        
        # Compare file sizes
        file_sizes = {}
        for feature, filename in files.items():
            try:
                size = os.path.getsize(filename)
                file_sizes[feature] = size
                print(f"  {feature}: {size} bytes")
            except:
                print(f"  {feature}: file not found")
        
        # Look for size differences (indicating different rendering)
        if len(file_sizes) > 1:
            sizes = list(file_sizes.values())
            if len(set(sizes)) > 1:
                print(f"  ✓ Size differences detected - features may be working!")
            else:
                print(f"  ? All files same size - features may not be working")


def test_markup_numerals():
    """Test numeral features using markup."""
    print("\n=== Testing Numerals in Markup ===")
    
    markup_tests = [
        ("Default numbers", "Numbers: 0123456789"),
        ("Old-style figures", "<span font_features='onum=1'>Numbers: 0123456789</span>"),
        ("Tabular figures", "<span font_features='tnum=1'>123.45  678.90  234.56</span>"),
        ("Combined features", "<span font_features='onum=1, tnum=1'>Old+Tab: 0123456789</span>"),
    ]
    
    for name, markup in markup_tests:
        try:
            # Validate markup first
            error = manimpango.MarkupUtils.validate(markup)
            if error:
                print(f"✗ Markup error for '{name}': {error}")
                continue
            
            filename = f"markup_numerals_{name.replace(' ', '_').replace('+', '_').lower()}.svg"
            result = manimpango.MarkupUtils.text2svg(
                text=markup,
                font="Times New Roman",
                slant="NORMAL",
                weight="NORMAL",
                size=36,
                _=None,
                disable_liga=False,
                file_name=filename,
                START_X=10,
                START_Y=50,
                width=600,
                height=80
            )
            print(f"✓ Created: {filename} ({name})")
            
        except Exception as e:
            print(f"✗ Error with '{name}': {e}")


def analyze_svg_numerals():
    """Analyze SVG content specifically for numeral differences."""
    print("\n=== Analyzing SVG Numeral Content ===")
    
    # Find files to compare
    default_file = None
    onum_file = None
    
    for f in os.listdir("."):
        if "numerals_times_new_roman_default.svg" in f:
            default_file = f
        elif "numerals_times_new_roman_oldstyle.svg" in f:
            onum_file = f
    
    if default_file and onum_file:
        try:
            with open(default_file, 'r') as f:
                default_content = f.read()
            with open(onum_file, 'r') as f:
                onum_content = f.read()
            
            # Look for differences in glyph usage
            default_glyphs = set()
            onum_glyphs = set()
            
            # Extract glyph references
            import re
            glyph_pattern = r'xlink:href="#glyph-\d+-(\d+)"'
            
            default_matches = re.findall(glyph_pattern, default_content)
            onum_matches = re.findall(glyph_pattern, onum_content)
            
            print(f"Default file glyphs used: {sorted(set(default_matches))}")
            print(f"Old-style file glyphs used: {sorted(set(onum_matches))}")
            
            if set(default_matches) != set(onum_matches):
                print("✓ Different glyphs used - numeral features are working!")
            else:
                print("? Same glyphs used - numeral features may not be working")
                
            # Check file sizes
            default_size = len(default_content)
            onum_size = len(onum_content)
            print(f"File sizes: default={default_size}, oldstyle={onum_size}")
            
        except Exception as e:
            print(f"Error analyzing files: {e}")
    else:
        print("Could not find both default and oldstyle files to compare")


def main():
    """Run all numeral tests."""
    print("ManimPango Numeral Features Test")
    print("=" * 40)
    
    try:
        test_numeral_features()
        test_markup_numerals()
        compare_numeral_files()
        analyze_svg_numerals()
        
        print("\n" + "=" * 40)
        print("Numeral test completed!")
        print("Check the generated SVG files to see numeral differences.")
        print("Look for files with 'oldstyle', 'tabular' variations.")
        
    except Exception as e:
        print(f"\nError running numeral tests: {e}")


if __name__ == "__main__":
    main()

@naveen521kk
Copy link
Member

Hi @matt, Thanks for your contribution. I'll take a look at this PR later today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants