Python Imaging Library (Fork) providing comprehensive image processing capabilities for reading, writing, and manipulating images across dozens of formats.
—
TrueType and OpenType font support for rendering text on images with full typography control including size, spacing, alignment, and styling options. The ImageFont module provides comprehensive font loading and text rendering capabilities.
Load and manage different types of fonts.
def load_default(size=None):
"""
Load the default PIL font.
Parameters:
- size (float): Font size (if None, uses default size)
Returns:
FreeTypeFont | ImageFont: Default font object
"""
def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
"""
Load a TrueType or OpenType font from file.
Parameters:
- font (str | bytes | Path): Font filename, bytes, or pathlib.Path
- size (float): Font size in points
- index (int): Font index for font collections (TTC files)
- encoding (str): Character encoding
- layout_engine (int): Text layout engine (LAYOUT_BASIC or LAYOUT_RAQM)
Returns:
FreeTypeFont: Font object
Raises:
OSError: If font file cannot be loaded
"""
def load(filename):
"""
Load a bitmap font from file.
Parameters:
- filename (str): Font filename (.pil format)
Returns:
ImageFont: Bitmap font object
"""
def load_path(filename):
"""
Load a bitmap font, searching in font path.
Parameters:
- filename (str | bytes): Font filename
Returns:
ImageFont: Bitmap font object
"""Font object types for different font formats.
class FreeTypeFont:
"""TrueType/OpenType font support with advanced typography features."""
@property
def family(self) -> str:
"""Font family name."""
@property
def style(self) -> str:
"""Font style name."""
@property
def size(self) -> float:
"""Font size in points."""
def getsize(self, text, direction=None, features=None, language=None, stroke_width=0):
"""
Get the size of given text (deprecated, use getbbox).
Parameters:
- text (str): Text to measure
- direction (str): Text direction ("ltr", "rtl", "ttb")
- features (list): OpenType features
- language (str): Language code
- stroke_width (int): Text stroke width
Returns:
tuple: Text size as (width, height)
"""
def getbbox(self, text, anchor=None, direction=None, features=None, language=None, stroke_width=0, embedded_color=False):
"""
Get the bounding box of given text.
Parameters:
- text (str): Text to measure
- anchor (str): Text anchor point
- direction (str): Text direction
- features (list): OpenType features
- language (str): Language code
- stroke_width (int): Text stroke width
- embedded_color (bool): Whether to use embedded color fonts
Returns:
tuple: Bounding box as (left, top, right, bottom)
"""
def getlength(self, text, direction=None, features=None, language=None, embedded_color=False):
"""
Get the length of given text.
Parameters:
- text (str): Text to measure
- direction (str): Text direction
- features (list): OpenType features
- language (str): Language code
- embedded_color (bool): Whether to use embedded color fonts
Returns:
float: Text length in pixels
"""
def getmask(self, text, mode="", direction=None, features=None, language=None, stroke_width=0, anchor=None, ink=0, start=None):
"""
Create a text mask.
Parameters:
- text (str): Text to render
- mode (str): Mask mode
- direction (str): Text direction
- features (list): OpenType features
- language (str): Language code
- stroke_width (int): Text stroke width
- anchor (str): Text anchor point
- ink (int): Ink parameter for bitmap fonts
- start (tuple): Starting offset
Returns:
Image: Text mask
"""
def font_variant(self, font=None, size=None, index=None, encoding=None, layout_engine=None):
"""
Create a variant of this font with different parameters.
Parameters:
- font (str): Font filename (uses current if None)
- size (float): Font size (uses current if None)
- index (int): Font index (uses current if None)
- encoding (str): Character encoding (uses current if None)
- layout_engine (int): Layout engine (uses current if None)
Returns:
FreeTypeFont: New font variant
"""
def set_variation_by_name(self, name):
"""
Set font variation by name (for variable fonts).
Parameters:
- name (str): Variation name
"""
def set_variation_by_axes(self, axes):
"""
Set font variation by axes (for variable fonts).
Parameters:
- axes (dict): Axis values as {axis_name: value}
"""
def get_variation_names(self):
"""
Get available variation names (for variable fonts).
Returns:
list: Available variation names
"""
def get_variation_axes(self):
"""
Get available variation axes (for variable fonts).
Returns:
list: Available axes information
"""
class ImageFont:
"""Bitmap font support for simple text rendering."""
def getsize(self, text):
"""
Get the size of given text.
Parameters:
- text (str): Text to measure
Returns:
tuple: Text size as (width, height)
"""
def getmask(self, text, mode="", direction=None, features=None, language=None, stroke_width=0, anchor=None, ink=0, start=None):
"""
Create a text mask.
Parameters:
- text (str): Text to render
- mode (str): Mask mode
Returns:
Image: Text mask
"""
class TransposedFont:
"""Font wrapper that applies a transformation to rendered text."""
def __init__(self, font, orientation=None):
"""
Create a transposed font.
Parameters:
- font (FreeTypeFont | ImageFont): Base font
- orientation (int): Text orientation
"""Constants for text layout engines.
LAYOUT_BASIC: int # Basic text layout engine
LAYOUT_RAQM: int # Advanced text layout engine (supports complex scripts)from PIL import Image, ImageDraw, ImageFont
# Create a blank image
img = Image.new("RGB", (400, 200), "white")
draw = ImageDraw.Draw(img)
# Load different fonts
try:
# Load system fonts
title_font = ImageFont.truetype("arial.ttf", 36)
body_font = ImageFont.truetype("arial.ttf", 18)
bold_font = ImageFont.truetype("arialbd.ttf", 24)
except OSError:
# Fallback to default font if system fonts not available
title_font = ImageFont.load_default()
body_font = ImageFont.load_default()
bold_font = ImageFont.load_default()
# Draw text with different fonts
draw.text((200, 50), "Main Title", font=title_font, fill="black", anchor="mm")
draw.text((200, 100), "Subtitle text", font=bold_font, fill="blue", anchor="mm")
draw.text((200, 140), "Body text with default styling", font=body_font, fill="gray", anchor="mm")
img.save("font_example.png")from PIL import Image, ImageDraw, ImageFont
img = Image.new("RGB", (600, 400), "white")
draw = ImageDraw.Draw(img)
# Load font with specific size
font = ImageFont.truetype("times.ttf", 24)
# Text with different anchors
text = "Anchor Demo"
y_positions = [50, 100, 150, 200]
anchors = ["lt", "mt", "rt", "mm"]
anchor_names = ["left-top", "middle-top", "right-top", "middle-middle"]
for y, anchor, name in zip(y_positions, anchors, anchor_names):
# Draw reference line
draw.line([(0, y), (600, y)], fill="lightgray", width=1)
draw.line([(300, y-20), (300, y+20)], fill="lightgray", width=1)
# Draw text with anchor
draw.text((300, y), text, font=font, fill="black", anchor=anchor)
# Label the anchor type
draw.text((10, y), f"{anchor} ({name})", font=ImageFont.load_default(), fill="red")
img.save("anchor_demo.png")from PIL import Image, ImageDraw, ImageFont
# Create image
img = Image.new("RGB", (500, 300), "white")
draw = ImageDraw.Draw(img)
# Load font
font = ImageFont.truetype("arial.ttf", 20)
# Text to measure
text = "Sample text for measurement"
# Get text bounding box
bbox = font.getbbox(text)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
print(f"Text dimensions: {text_width} x {text_height}")
print(f"Bounding box: {bbox}")
# Center text using measurements
img_width, img_height = img.size
x = (img_width - text_width) // 2
y = (img_height - text_height) // 2
# Draw text
draw.text((x, y), text, font=font, fill="black")
# Draw bounding box for visualization
draw.rectangle([x + bbox[0], y + bbox[1], x + bbox[2], y + bbox[3]],
outline="red", width=1)
img.save("text_measurement.png")from PIL import Image, ImageDraw, ImageFont
img = Image.new("RGB", (400, 300), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("arial.ttf", 16)
# Multiline text with different alignments
multiline_text = """This is a sample of multiline text
that demonstrates different
alignment options and
line spacing controls."""
# Left aligned
draw.multiline_text((50, 50), multiline_text, font=font, fill="black",
align="left", spacing=8)
# Center aligned
draw.multiline_text((200, 50), multiline_text, font=font, fill="blue",
align="center", spacing=12)
# Right aligned
draw.multiline_text((350, 50), multiline_text, font=font, fill="green",
align="right", spacing=6)
img.save("multiline_text.png")from PIL import Image, ImageDraw, ImageFont
img = Image.new("RGB", (500, 200), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("arial.ttf", 48)
text = "STYLED TEXT"
# Shadow effect
shadow_offset = 3
draw.text((102 + shadow_offset, 52 + shadow_offset), text, font=font, fill="gray")
draw.text((102, 52), text, font=font, fill="black")
# Outline effect (stroke)
if hasattr(font, 'getbbox'): # Check if FreeType font
draw.text((100, 100), text, font=font, fill="white",
stroke_width=2, stroke_fill="black")
img.save("text_effects.png")from PIL import Image, ImageDraw, ImageFont
img = Image.new("RGB", (400, 400), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("arial.ttf", 24)
# Left-to-right text (default)
draw.text((50, 50), "Left to Right Text", font=font, fill="black", direction="ltr")
# Right-to-left text (for Arabic, Hebrew, etc.)
arabic_text = "النص العربي" # "Arabic text" in Arabic
try:
draw.text((350, 100), arabic_text, font=font, fill="blue",
direction="rtl", anchor="ra")
except:
# Fallback if font doesn't support Arabic
draw.text((350, 100), "RTL Text", font=font, fill="blue",
direction="rtl", anchor="ra")
# Vertical text
draw.text((200, 150), "Vertical", font=font, fill="green", direction="ttb")
img.save("text_directions.png")from PIL import Image, ImageDraw, ImageFont
# Load a variable font (if available)
try:
var_font = ImageFont.truetype("variable_font.ttf", 36)
img = Image.new("RGB", (600, 300), "white")
draw = ImageDraw.Draw(img)
# Get available variations
variations = var_font.get_variation_names()
axes = var_font.get_variation_axes()
print("Available variations:", variations)
print("Available axes:", axes)
# Apply different variations
positions = [(100, 50), (100, 100), (100, 150), (100, 200)]
for i, pos in enumerate(positions):
# Create font variant with different weight
weight = 200 + i * 200 # Vary from 200 to 800
var_font.set_variation_by_axes({"wght": weight})
draw.text(pos, f"Weight {weight}", font=var_font, fill="black")
img.save("variable_font.png")
except OSError:
print("Variable font not available")from PIL import ImageFont
# Load font and inspect properties
font = ImageFont.truetype("arial.ttf", 24)
print(f"Font family: {font.family}")
print(f"Font style: {font.style}")
print(f"Font size: {font.size}")
# Test text measurements
test_text = "Hello, World!"
bbox = font.getbbox(test_text)
length = font.getlength(test_text)
print(f"Text: '{test_text}'")
print(f"Bounding box: {bbox}")
print(f"Text length: {length}")
# Compare with different sizes
sizes = [12, 18, 24, 36, 48]
for size in sizes:
font_size = ImageFont.truetype("arial.ttf", size)
bbox = font_size.getbbox(test_text)
width = bbox[2] - bbox[0]
height = bbox[3] - bbox[1]
print(f"Size {size}: {width} x {height} pixels")from PIL import Image, ImageDraw, ImageFont
def create_text_layout(title, subtitle, body, width=600, height=400):
"""Create a professional text layout with proper spacing."""
img = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(img)
# Load fonts
title_font = ImageFont.truetype("arial.ttf", 32)
subtitle_font = ImageFont.truetype("arial.ttf", 20)
body_font = ImageFont.truetype("arial.ttf", 14)
# Layout parameters
margin = 40
line_spacing = 10
current_y = margin
# Draw title
draw.text((width // 2, current_y), title, font=title_font,
fill="black", anchor="mt")
title_bbox = title_font.getbbox(title)
current_y += (title_bbox[3] - title_bbox[1]) + line_spacing * 2
# Draw subtitle
draw.text((width // 2, current_y), subtitle, font=subtitle_font,
fill="blue", anchor="mt")
subtitle_bbox = subtitle_font.getbbox(subtitle)
current_y += (subtitle_bbox[3] - subtitle_bbox[1]) + line_spacing * 3
# Draw body text (wrapped)
words = body.split()
lines = []
current_line = []
for word in words:
test_line = " ".join(current_line + [word])
test_width = body_font.getlength(test_line)
if test_width <= width - 2 * margin:
current_line.append(word)
else:
if current_line:
lines.append(" ".join(current_line))
current_line = [word]
else:
lines.append(word) # Single word longer than line
if current_line:
lines.append(" ".join(current_line))
# Draw wrapped text
for line in lines:
draw.text((margin, current_y), line, font=body_font, fill="black")
line_bbox = body_font.getbbox(line)
current_y += (line_bbox[3] - line_bbox[1]) + line_spacing
return img
# Create professional layout
title = "Professional Document"
subtitle = "Advanced Typography with Pillow"
body = """This is an example of professional text layout using Python's Pillow library.
The text is properly wrapped, spaced, and formatted to create a clean, readable document.
Multiple font sizes and colors are used to create visual hierarchy."""
layout = create_text_layout(title, subtitle, body)
layout.save("professional_layout.png")Install with Tessl CLI
npx tessl i tessl/pypi-pillow