CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pilmoji

Emoji renderer for Pillow with Discord emoji support and multiple emoji sources

Pending
Overview
Eval results
Files

text-processing.mddocs/

Text Processing and Utilities

Helper functions and utilities for text analysis, emoji detection, node parsing, and text measurement. These tools enable advanced text layout, custom processing workflows, and integration with existing text processing systems.

Capabilities

Text Parsing and Node System

Convert text strings into structured node representations, separating text, Unicode emojis, and Discord emojis for precise rendering control.

def to_nodes(text: str, /) -> List[List[Node]]:
    """
    Parses text into structured Node objects organized by lines.
    
    Each line becomes a list of Node objects representing text segments,
    Unicode emojis, and Discord emojis. This enables precise control over
    rendering and text processing.
    
    Parameters:
    - text: str - Text to parse (supports multiline strings)
    
    Returns:
    - List[List[Node]] - Nested list where each inner list represents
      a line of Node objects
    
    Example:
    >>> to_nodes("Hello 👋\\nWorld! 🌍")
    [[Node(NodeType.text, 'Hello '), Node(NodeType.emoji, '👋')],
     [Node(NodeType.text, 'World! '), Node(NodeType.emoji, '🌍')]]
    """

Usage Example

from pilmoji.helpers import to_nodes, NodeType

# Parse mixed content
text = """Hello! 👋 Welcome to our app
Support for Discord: <:custom:123456789>
And regular emojis: 🎉 🌟"""

nodes = to_nodes(text)
for line_num, line in enumerate(nodes):
    print(f"Line {line_num + 1}:")
    for node in line:
        if node.type == NodeType.text:
            print(f"  Text: '{node.content}'")
        elif node.type == NodeType.emoji:
            print(f"  Emoji: {node.content}")
        elif node.type == NodeType.discord_emoji:
            print(f"  Discord: ID {node.content}")

Node Data Structure

Structured representation of parsed text segments with type information for precise text processing.

class Node(NamedTuple):
    """Represents a parsed segment of text with type information."""
    
    type: NodeType
    content: str
    
    # Example usage:
    # Node(NodeType.text, "Hello ")
    # Node(NodeType.emoji, "👋")
    # Node(NodeType.discord_emoji, "123456789")

Node Type Enumeration

Enumeration defining the types of text segments that can be parsed from input strings.

class NodeType(Enum):
    """
    Enumeration of node types for text parsing.
    
    Values:
    - text (0): Plain text segment
    - emoji (1): Unicode emoji character
    - discord_emoji (2): Discord custom emoji
    """
    
    text = 0          # Plain text content
    emoji = 1         # Unicode emoji character
    discord_emoji = 2 # Discord custom emoji ID

Text Size Calculation

Standalone function for calculating text dimensions with emoji support, useful for layout planning without creating a Pilmoji instance.

def getsize(
    text: str,
    font: FontT = None,
    *,
    spacing: int = 4,
    emoji_scale_factor: float = 1
) -> Tuple[int, int]:
    """
    Calculate text dimensions including emoji substitutions.
    
    Useful for text layout planning, centering, and UI calculations
    without requiring a Pilmoji instance.
    
    Parameters:
    - text: str - Text to measure
    - font: FontT - Font for measurement (defaults to Pillow's default)
    - spacing: int - Line spacing in pixels (default: 4)
    - emoji_scale_factor: float - Emoji scaling factor (default: 1.0)
    
    Returns:
    - Tuple[int, int] - (width, height) of rendered text
    """

Usage Examples

from pilmoji.helpers import getsize
from PIL import ImageFont

# Basic text measurement
font = ImageFont.truetype('arial.ttf', 16)
text = "Hello! 👋 How are you? 🌟"

width, height = getsize(text, font)
print(f"Text size: {width}x{height}")

# Multiline text measurement
multiline = """Welcome! 🎉
Line 2 with emoji 🌈
Final line 🚀"""

width, height = getsize(multiline, font, spacing=6, emoji_scale_factor=1.2)
print(f"Multiline size: {width}x{height}")

# Layout planning
def center_text_on_image(image, text, font):
    """Helper to center text on an image."""
    img_width, img_height = image.size
    text_width, text_height = getsize(text, font)
    
    x = (img_width - text_width) // 2
    y = (img_height - text_height) // 2
    
    return (x, y)

# Usage in layout
from PIL import Image
image = Image.new('RGB', (400, 200), 'white')
position = center_text_on_image(image, "Centered! 🎯", font)

Emoji Detection Regex

Pre-compiled regular expression for detecting emojis in text, supporting both Unicode emojis and Discord custom emoji format.

EMOJI_REGEX: re.Pattern[str]
"""
Compiled regex pattern for detecting emojis in text.

Matches:
- Unicode emojis (from emoji library)
- Discord custom emojis (<:name:id> and <a:name:id>)

Useful for custom text processing and validation.
"""

Usage Example

from pilmoji.helpers import EMOJI_REGEX
import re

# Find all emojis in text
text = "Hello 👋 Discord: <:smile:123> and <a:wave:456>"
matches = EMOJI_REGEX.findall(text)
print("Found emojis:", matches)

# Split text by emojis
parts = EMOJI_REGEX.split(text)
print("Text parts:", parts)

# Check if text contains emojis
has_emojis = bool(EMOJI_REGEX.search(text))
print("Contains emojis:", has_emojis)

# Replace emojis with placeholders
def replace_emojis(text, placeholder="[EMOJI]"):
    return EMOJI_REGEX.sub(placeholder, text)

clean_text = replace_emojis("Hello! 👋 Welcome 🎉")
print("Clean text:", clean_text)

Advanced Text Processing Workflows

Examples of combining pilmoji utilities for advanced text processing scenarios.

Text Analysis and Statistics

from pilmoji.helpers import to_nodes, NodeType, EMOJI_REGEX
from collections import Counter

def analyze_text(text: str) -> dict:
    """Analyze text for emoji usage and content statistics."""
    
    # Parse into nodes
    nodes = to_nodes(text)
    
    # Flatten nodes and count types
    all_nodes = [node for line in nodes for node in line]
    type_counts = Counter(node.type for node in all_nodes)
    
    # Extract emojis
    unicode_emojis = [node.content for node in all_nodes 
                     if node.type == NodeType.emoji]
    discord_emojis = [node.content for node in all_nodes 
                     if node.type == NodeType.discord_emoji]
    
    return {
        'total_lines': len(nodes),
        'total_nodes': len(all_nodes),
        'text_segments': type_counts[NodeType.text],
        'unicode_emojis': len(unicode_emojis),
        'discord_emojis': len(discord_emojis),
        'unique_unicode_emojis': len(set(unicode_emojis)),
        'emoji_list': unicode_emojis,
        'discord_ids': discord_emojis
    }

# Usage
text = """Welcome! 👋 🎉
We support Discord <:custom:123> and <:other:456>
More emojis: 🌟 🎨 👋"""

stats = analyze_text(text)
print(f"Analysis: {stats}")

Custom Text Renderer with Measurements

from pilmoji.helpers import getsize, to_nodes, NodeType
from PIL import Image, ImageDraw, ImageFont

class CustomTextRenderer:
    """Custom text renderer with advanced measurement capabilities."""
    
    def __init__(self, font):
        self.font = font
    
    def get_text_metrics(self, text: str) -> dict:
        """Get detailed text metrics."""
        nodes = to_nodes(text)
        width, height = getsize(text, self.font)
        
        line_widths = []
        for line in nodes:
            line_text = ''.join(node.content for node in line 
                              if node.type == NodeType.text)
            if line_text:
                line_width, _ = getsize(line_text, self.font)
                line_widths.append(line_width)
        
        return {
            'total_width': width,
            'total_height': height,
            'line_count': len(nodes),
            'max_line_width': max(line_widths) if line_widths else 0,
            'avg_line_width': sum(line_widths) / len(line_widths) if line_widths else 0
        }
    
    def fit_text_to_width(self, text: str, max_width: int) -> str:
        """Truncate text to fit within specified width."""
        if getsize(text, self.font)[0] <= max_width:
            return text
        
        # Binary search for maximum fitting length
        low, high = 0, len(text)
        best_fit = ""
        
        while low <= high:
            mid = (low + high) // 2
            test_text = text[:mid] + "..."
            
            if getsize(test_text, self.font)[0] <= max_width:
                best_fit = test_text
                low = mid + 1
            else:
                high = mid - 1
        
        return best_fit

# Usage
font = ImageFont.load_default()
renderer = CustomTextRenderer(font)

text = "This is a long text with emojis 🎉 that might need truncation 🌟"
metrics = renderer.get_text_metrics(text)
fitted = renderer.fit_text_to_width(text, 200)

print(f"Metrics: {metrics}")
print(f"Fitted text: {fitted}")

Install with Tessl CLI

npx tessl i tessl/pypi-pilmoji

docs

core-rendering.md

emoji-sources.md

index.md

text-processing.md

tile.json