Emoji renderer for Pillow with Discord emoji support and multiple emoji sources
—
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.
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, '🌍')]]
"""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}")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")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 IDStandalone 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
"""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)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.
"""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)Examples of combining pilmoji utilities for advanced text processing scenarios.
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}")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