API Reference

Types

class fontstack.FontConfig(path, axes=None, ttc_index=0)

Configuration for a single font entry in a fallback stack.

Parameters:
  • path (str) – Absolute or relative path to a TTF, OTF, TTC, or OTC font file.

  • axes (VariationAxes or None, optional) – Default variable-font axis values applied when loading this font. The weight argument passed to rendering methods always overrides the wght axis specifically. See VariationAxes for the full list of standard axes. Defaults to None.

  • ttc_index (int, optional) – Zero-based index of the desired font within a TrueType Collection (.ttc / .otc) file. Ignored for non-collection files. Defaults to 0.

Examples

Basic font entry:

FontConfig(path="fonts/NotoSans[wdth,wght].ttf")

Variable font with custom default axes:

FontConfig(
    path="fonts/NotoSans[wdth,wght].ttf",
    axes=VariationAxes(wght=300.0, wdth=75.0),
)

Font from a TrueType Collection:

FontConfig(path="fonts/NotoSansCJK.ttc", ttc_index=2)
class fontstack.VariationAxes

OpenType variable-font axis overrides.

Each key is a registered axis tag and the value is the desired position on that axis. Only the axes listed here are set; all others keep their font-defined defaults.

Commonly used axes:

wght

Weight axis (100 = Thin … 400 = Regular … 700 = Bold … 900 = Black).

wdth

Width axis (percentage of normal width, e.g. 75 = Condensed, 100 = Normal).

ital

Italic axis (0 = Upright, 1 = Italic).

slnt

Slant axis (degrees of counter-clockwise slant, e.g. -12).

opsz

Optical-size axis (intended point size for optical compensation).

fontstack.FillType = str | int | tuple[int, int, int] | tuple[int, int, int, int]

Represent a PEP 604 union type

E.g. for int | str

fontstack.RenderMode

alias of Literal[‘wrap’, ‘scale’, ‘fit’]

fontstack.HorizontalAlign

alias of Literal[‘left’, ‘center’, ‘right’]

fontstack.Anchor

alias of Literal[‘lt’, ‘mt’, ‘rt’, ‘lm’, ‘mm’, ‘rm’, ‘lb’, ‘mb’, ‘rb’]

FontManager

class fontstack.FontManager(default_stack=None, max_cache=30, *, font_dir=None)

Manages a prioritized fallback stack of fonts and renders Unicode text onto PIL images, including emoji, Arabic/RTL script, and CJK.

How fallback works

Each character in a string is assigned to the first font in the stack whose cmap contains that codepoint. Emoji are always pinned to the primary (index-0) font because Pilmoji intercepts and rasterises them independently; routing them to a fallback font would corrupt the baseline correction applied during rendering.

TTC support

Font collection files (.ttc, .otc) are supported via the ttc_index key in FontConfig. Each TTC entry is treated as a normal font; the manager reads the cmap of the specified member and loads it through Pillow using the same index.

BiDi / RTL support

All text is automatically processed before rendering. Arabic is reshaped with arabic-reshaper to produce the correct contextual letter forms (initial, medial, final, isolated), then passed through python-bidi for Unicode BiDi reordering. This makes Arabic render correctly under Pillow’s BASIC layout engine.

Caching

Font objects and cmap data are LRU-cached internally. Repeated calls with the same (stack, size, weight) triple are essentially free.

param default_stack:

Ordered list of FontConfig entries. The first item is the primary font; subsequent entries are tried in order when the primary lacks a glyph for a given character. Mutually exclusive with font_dir; at least one of the two must be provided.

param font_dir:

Path to a directory of font files (.ttf, .otf, .ttc, .otc). When given, the directory is scanned via scan_font_dir() and the resulting FontConfig list becomes the default_stack. Mutually exclusive with default_stack.

param max_cache:

Maximum number of (stack, size, weight) entries to keep in the font-object LRU cache. Older entries are evicted when the cache is full. Defaults to 30.

Example

# Explicit font stack
manager = FontManager(
    default_stack=[
        FontConfig(path="fonts/NotoSans.ttf"),
        FontConfig(path="fonts/NotoSansCJK.ttc", ttc_index=0),
        FontConfig(path="fonts/NotoSansArabic.ttf"),
    ]
)

# Or scan from a directory
manager = FontManager(font_dir="fonts/")

img = Image.new("RGBA", (800, 100), "white")
manager.draw(img, "Hello مرحبا 世界", position=(10, 20), size=40)
property default_stack: list[FontConfig]

The fallback font stack supplied at construction time (read-only copy).

property font_dir: str | Path | None

The font directory passed at construction time, or None.

get_font_chain(size, weight=400, custom_stack=None)

Return a list of loaded FreeTypeFont objects for each entry in the active stack at the given size and weight. The result is LRU-cached so subsequent calls with the same arguments are essentially free.

Parameters:
  • size (int) – Point size passed to PIL.ImageFont.truetype().

  • weight (int | str) – Font weight. Can be an integer (e.g. 400, 700) which sets the wght variation axis, or a named style string (e.g. "Bold") passed to set_variation_by_name(). For static (non-variable) fonts the weight parameter is silently ignored.

  • custom_stack (list[FontConfig] | None) – Override the instance’s default_stack for this call only. Useful when rendering a single text element with a bespoke font combination without affecting the manager’s default behaviour.

Returns:

One loaded font object per stack entry, in the same order as the stack.

Return type:

list[ImageFont.FreeTypeFont]

draw(image: ~PIL.Image.Image, text: str, position: tuple[int, int], *, size: int = 40, weight: int | str = 400, mode: ~typing.Literal['wrap'] = 'wrap', max_width: int | None = None, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, anchor: ~typing.Literal['lt', 'mt', 'rt', 'lm', 'mm', 'rm', 'lb', 'mb', 'rb'] = 'lt', font_stack: list[~fontstack.types.FontConfig] | None = None, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>) tuple[int, int]
draw(image: ~PIL.Image.Image, text: str, position: tuple[int, int], *, size: int = 40, weight: int | str = 400, mode: ~typing.Literal['scale'] = 'wrap', max_width: int | None = None, min_size: int = 12, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, anchor: ~typing.Literal['lt', 'mt', 'rt', 'lm', 'mm', 'rm', 'lb', 'mb', 'rb'] = 'lt', font_stack: list[~fontstack.types.FontConfig] | None = None, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>) tuple[int, int]
draw(image: ~PIL.Image.Image, text: str, position: tuple[int, int], *, size: int = 40, weight: int | str = 400, mode: ~typing.Literal['fit'] = 'wrap', max_width: int | None = None, max_height: int | None = None, min_size: int = 12, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, anchor: ~typing.Literal['lt', 'mt', 'rt', 'lm', 'mm', 'rm', 'lb', 'mb', 'rb'] = 'lt', font_stack: list[~fontstack.types.FontConfig] | None = None, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>) tuple[int, int]
draw(image: ~PIL.Image.Image, text: str, position: tuple[int, int], *, size: int = 40, weight: int | str = 400, mode: RenderMode = 'wrap', max_width: int | None = None, max_height: int | None = None, min_size: int = 12, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, anchor: ~typing.Literal['lt', 'mt', 'rt', 'lm', 'mm', 'rm', 'lb', 'mb', 'rb'] = 'lt', font_stack: list[~fontstack.types.FontConfig] | None = None, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>) tuple[int, int]

Draw text onto image with automatic per-character font fallback, BiDi/Arabic support, and emoji rendering, then return the pixel dimensions of the rendered bounding box.

Before any measuring or drawing, text is reshaped and BiDi-reordered by _prepare_bidi(), making mixed-direction strings render correctly in Pillow’s left-to-right model. Text is rendered into a transparent RGBA overlay first, then composited onto image, which prevents anti-aliasing artifacts on non-white backgrounds.

Parameters:
  • image (Image) – The target Image. Modified in place. "RGBA" mode is recommended; other modes composite correctly but may lose transparency.

  • text (str) – The string to render. May contain any Unicode codepoints, emoji sequences, or Arabic/Hebrew/mixed-direction content. BiDi reordering and reshaping are applied automatically.

  • position (tuple[int, int]) – (x, y) pixel coordinate of the visual top-left corner of the text block. Corrects for the font’s internal bounding-box offset so this maps to visible pixels, not FreeType’s internal origin.

  • size (int) – Initial font size in points. In "scale" mode this may be reduced; in "wrap" mode it is used as-is for all lines. Defaults to 40.

  • weight (int | str) – Font weight. Integer (e.g. 700) sets the wght variable axis; a string (e.g. "Bold") calls set_variation_by_name(). Static fonts ignore this silently. Defaults to 400.

  • mode (RenderMode) –

    Rendering mode. One of:

    "wrap"

    Word-wrap the text at max_width. Lines are spaced at size * line_spacing pixels. A single word wider than max_width will overflow without truncation. When max_width is None, the text renders as a single line with no wrapping.

    "scale"

    Shrink the font in 2pt steps down to min_size until the text fits within max_width. If it still overflows, characters are truncated with an ellipsis. When max_width is None, the text renders at full size with no scaling.

    "fit"

    Word-wrap at max_width first (like "wrap"), then shrink the font in 2pt steps until the wrapped text block fits within max_height. If the block still overflows at min_size, lines that do not fit are dropped and the last kept line is truncated with an ellipsis. When max_height is None, this mode behaves identically to "wrap".

    Defaults to "wrap".

  • max_width (int | None) – Maximum pixel width for the text area. None means no constraint. Defaults to None.

  • max_height (int | None) – Maximum pixel height for the text block. Only used in "fit" mode. None means no height constraint. Defaults to None.

  • min_size (int) – Minimum font size (points) used as the floor in "scale" and "fit" modes. Ignored in "wrap" mode. Defaults to 12.

  • align (HorizontalAlign) – Horizontal alignment of lines within the text block. One of "left", "center", or "right". Only affects multi-line "wrap" output; single-line text always starts at position. Defaults to "left".

  • line_spacing (float) – Line height multiplier relative to size. 1.2 means each line’s top is size * 1.2 pixels below the previous line’s top. Increase for scripts with tall diacritics (e.g. Arabic tashkeel). Defaults to 1.2.

  • fill (FillType) – Text color. Accepts a Pillow color name ("red"), an RGB tuple ((255, 0, 0)), an RGBA tuple ((255, 0, 0, 128) for 50% opacity), or an integer for palette/grayscale images. A dash-separated string of colors ("red-blue", "#FF0000-#00FF00-#0000FF") produces a linear gradient across the text (slightly diagonal so multi-line text gets natural color variation per line). The preset "rainbow" expands to "red-orange-yellow-green-blue-indigo-violet". Defaults to "black".

  • stroke_width (int) – Outline thickness in pixels drawn around each glyph. 0 means no outline. Defaults to 0.

  • stroke_fill (TypeAliasForwardRef('FillType') | None) – Outline color. Accepts the same value types as fill, including gradient strings (e.g. "rainbow", "red-blue"). When None and stroke_width > 0, Pillow uses fill as the stroke color. Defaults to None.

  • shadow_color (TypeAliasForwardRef('FillType') | None) – Drop-shadow color. Accepts the same value types as fill, including gradient strings. The shadow shape includes the outline when stroke_width > 0. None disables the shadow. Defaults to None.

  • shadow_offset (tuple[int, int]) – (x, y) pixel offset for the drop shadow relative to the main text position. Positive values shift right and down. Only used when shadow_color is not None. Defaults to (2, 2).

  • gradient_angle (float) – Gradient direction in degrees, clockwise from horizontal. 0 produces a pure left-to-right gradient; the default 15.0 adds a slight diagonal so multi-line text gets natural color variation per line. Only used when fill, stroke_fill, or shadow_color is a gradient string. Defaults to 15.0.

  • font_stack (list[FontConfig] | None) – Override the instance’s default_stack for this call only.

  • anchor (Literal['lt', 'mt', 'rt', 'lm', 'mm', 'rm', 'lb', 'mb', 'rb']) – Two-character PIL-style anchor code specifying which point of the text block is placed at position. The first character selects the horizontal reference (l = left edge, m = horizontal center, r = right edge) and the second selects the vertical reference (t = top edge, m = vertical center, b = bottom edge). Valid values are "lt", "mt", "rt", "lm", "mm", "rm", "lb", "mb", "rb". Defaults to "lt" (top-left), which preserves the historical behavior where position was the top-left corner.

  • emoji_source (type[BaseSource]) – Pilmoji emoji image source class (not instance). Defaults to Twemoji.

Returns:

(width, height) of the tightest bounding box around the actually-drawn pixels, ignoring all font internal padding. Returns (0, 0) if nothing was drawn (e.g. empty string).

Return type:

tuple[int, int]

Raises:
  • ValueError – If mode is not "wrap", "scale", or "fit".

  • ValueError – If align is not "left", "center", or "right".

  • ValueError – If anchor is not one of the nine valid two-character codes.

Parameters:

Convenience functions

fontstack.draw_text(text: str, font_stack: list[~fontstack.types.FontConfig], *, size: int = 40, weight: int | str = 400, mode: ~typing.Literal['wrap'] = 'wrap', max_width: int | None = None, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, background: FillType = (0, 0, 0, 0), padding: int = 0, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>, font_dir: str | ~pathlib.Path | None = None, manager: ~fontstack.manager.FontManager | None = None) Image
fontstack.draw_text(text: str, font_stack: list[~fontstack.types.FontConfig], *, size: int = 40, weight: int | str = 400, mode: ~typing.Literal['scale'] = 'wrap', max_width: int | None = None, min_size: int = 12, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, background: FillType = (0, 0, 0, 0), padding: int = 0, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>, font_dir: str | ~pathlib.Path | None = None, manager: ~fontstack.manager.FontManager | None = None) Image
fontstack.draw_text(text: str, font_stack: list[~fontstack.types.FontConfig], *, size: int = 40, weight: int | str = 400, mode: ~typing.Literal['fit'] = 'wrap', max_width: int | None = None, max_height: int | None = None, min_size: int = 12, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, background: FillType = (0, 0, 0, 0), padding: int = 0, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>, font_dir: str | ~pathlib.Path | None = None, manager: ~fontstack.manager.FontManager | None = None) Image
fontstack.draw_text(text: str, font_stack: list[~fontstack.types.FontConfig], *, size: int = 40, weight: int | str = 400, mode: RenderMode = 'wrap', max_width: int | None = None, max_height: int | None = None, min_size: int = 12, align: HorizontalAlign = 'left', line_spacing: float = 1.2, fill: FillType = 'black', stroke_width: int = 0, stroke_fill: FillType | None = None, shadow_color: FillType | None = None, shadow_offset: tuple[int, int] = (2, 2), gradient_angle: float = 15.0, background: FillType = (0, 0, 0, 0), padding: int = 0, emoji_source: type[~pilmoji.source.BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>, font_dir: str | ~pathlib.Path | None = None, manager: ~fontstack.manager.FontManager | None = None) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: Literal['wrap'] = 'wrap',
max_width: int | None = None,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path | None = None,
manager: FontManager,
) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: Literal['scale'] = 'wrap',
max_width: int | None = None,
min_size: int = 12,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path | None = None,
manager: FontManager,
) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: Literal['fit'] = 'wrap',
max_width: int | None = None,
max_height: int | None = None,
min_size: int = 12,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path | None = None,
manager: FontManager,
) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: RenderMode = 'wrap',
max_width: int | None = None,
max_height: int | None = None,
min_size: int = 12,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path | None = None,
manager: FontManager,
) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: Literal['wrap'] = 'wrap',
max_width: int | None = None,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path,
manager: FontManager | None = None,
) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: Literal['scale'] = 'wrap',
max_width: int | None = None,
min_size: int = 12,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path,
manager: FontManager | None = None,
) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: Literal['fit'] = 'wrap',
max_width: int | None = None,
max_height: int | None = None,
min_size: int = 12,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path,
manager: FontManager | None = None,
) Image
fontstack.draw_text(
text: str,
font_stack: list[FontConfig] | None = None,
*,
size: int = 40,
weight: int | str = 400,
mode: RenderMode = 'wrap',
max_width: int | None = None,
max_height: int | None = None,
min_size: int = 12,
align: HorizontalAlign = 'left',
line_spacing: float = 1.2,
fill: FillType = 'black',
stroke_width: int = 0,
stroke_fill: FillType | None = None,
shadow_color: FillType | None = None,
shadow_offset: tuple[int,
int]=(2,
2),
gradient_angle: float = 15.0,
background: FillType = (0,
0,
0,
0),
padding: int = 0,
emoji_source: type[BaseSource] = <class 'pilmoji.source.TwitterEmojiSource'>,
font_dir: str | Path,
manager: FontManager | None = None,
) Image

Render text and return a new PIL image sized to fit the result.

Creates a FontManager from font_stack (or reuses manager if provided), renders the text onto an oversized transparent canvas, crops the result to the exact pixel bounding box, adds padding on all sides, and composites onto background. When manager is passed, font_stack is ignored and the manager’s caches are reused across calls, which makes this efficient for batch rendering:

mgr = FontManager(default_stack=[FontConfig(path="fonts/NotoSans.ttf")])
images = [draw_text(label, manager=mgr) for label in labels]
Parameters:
  • text (str) – The string to render. BiDi reordering and Arabic reshaping are applied automatically. May contain emoji.

  • font_stack (list[FontConfig] | None) – Ordered list of FontConfig entries defining the fallback font stack. Ignored when manager is provided. May be omitted (None) when font_dir or manager is supplied.

  • font_dir (str | Path | None) – Path to a directory of font files. When given, a FontManager is constructed from the scanned directory via scan_font_dir(). Ignored when manager is provided.

  • size (int) – Initial font size in points. Defaults to 40.

  • weight (int | str) – Font weight - integer (e.g. 700) or named style string (e.g. "Bold"). Defaults to 400.

  • mode (RenderMode) – "wrap" (word-wrap at max_width), "scale" (shrink to fit max_width), or "fit" (wrap then shrink to fit max_height). When max_width is None, all modes render at full size with no constraint. Defaults to "wrap".

  • max_width (int | None) – Maximum pixel width for the text area. None means no constraint. Defaults to None.

  • max_height (int | None) – Maximum pixel height for the text block. Only used in "fit" mode. None means no height constraint. Defaults to None.

  • min_size (int) – Minimum font size floor for "scale" and "fit" modes. Defaults to 12.

  • align (HorizontalAlign) – Horizontal alignment: "left", "center", or "right". Defaults to "left".

  • line_spacing (float) – Line height multiplier relative to size. Defaults to 1.2.

  • fill (FillType) – Text color. Accepts a Pillow color name, RGB tuple, RGBA tuple (e.g. (0, 0, 0, 128) for 50% opacity), or a dash-separated gradient string (e.g. "red-blue", "#FF0000-#00FF00"). Gradients are slightly diagonal so multi-line text gets natural color variation. The preset "rainbow" expands to the seven spectral colors. Defaults to "black".

  • stroke_width (int) – Outline thickness in pixels around each glyph. 0 disables the outline. Defaults to 0.

  • stroke_fill (TypeAliasForwardRef('FillType') | None) – Outline color. Accepts the same value types as fill, including gradient strings (e.g. "rainbow"). None uses fill as the stroke color when stroke_width > 0. Defaults to None.

  • shadow_color (TypeAliasForwardRef('FillType') | None) – Drop-shadow color. Accepts the same value types as fill, including gradient strings. The shadow shape includes the outline when stroke_width > 0. None disables the shadow. Defaults to None.

  • shadow_offset (tuple[int, int]) – (x, y) pixel offset for the drop shadow. Defaults to (2, 2).

  • gradient_angle (float) – Gradient direction in degrees, clockwise from horizontal. 0 produces a pure left-to-right gradient; the default 15.0 adds a slight diagonal so multi-line text gets natural color variation per line. Only used when fill, stroke_fill, or shadow_color is a gradient string. Defaults to 15.0.

  • background (FillType) – Background color of the returned image. Defaults to fully transparent (0, 0, 0, 0). Pass "white" or an RGB/RGBA tuple for an opaque background.

  • padding (int) – Uniform padding in pixels added to all four edges of the cropped result. Prevents glyphs from touching the image border. Defaults to 0.

  • emoji_source (type[BaseSource]) – Pilmoji emoji source class. Defaults to Twemoji.

  • manager (FontManager | None) – An existing FontManager instance to reuse. When provided, font_stack is ignored and no new manager is constructed. Useful for batch rendering where cmap caches should be shared across calls.

Returns:

A new "RGBA" image cropped tightly to the rendered text with padding added on all sides and background filled behind the text.

Return type:

Image.Image

Raises:

ValueError – If none of font_stack, font_dir, or manager provides a usable font configuration.

Example

img = draw_text(
    "Hello مرحبا 世界 🌍",
    font_stack=[
        FontConfig(path="fonts/NotoSans.ttf"),
        FontConfig(path="fonts/NotoSansCJK.ttc", ttc_index=0),
        FontConfig(path="fonts/NotoSansArabic.ttf"),
    ],
    size=48,
    weight=700,
    fill=(20, 20, 20),
    background="white",
    padding=16,
)
img.save("hello.png")
fontstack.scan_font_dir(font_dir, *, recursive=False)

Scan a directory for font files and return a list of FontConfig entries suitable for use as a default_stack.

Recognised extensions: .ttf, .otf, .ttc, .otc. TrueType/OpenType Collection files (.ttc, .otc) produce one FontConfig per member font, each with the appropriate ttc_index. Results are sorted by filename for deterministic fallback order.

Parameters:
  • font_dir (str | Path) – Path to the directory containing font files.

  • recursive (bool) – When True, search subdirectories recursively. Defaults to False.

Returns:

One or more FontConfig entries found in font_dir.

Return type:

list[FontConfig]

Raises: