{"id":47890626,"url":"https://github.com/kanin/fontstack","last_synced_at":"2026-05-03T16:06:29.720Z","repository":{"id":349041845,"uuid":"1200832596","full_name":"Kanin/FontStack","owner":"Kanin","description":"Unicode text rendering for Pillow with automatic per-character font fallback, variable fonts, BiDi/RTL, gradients, outlines, shadows, and emoji.","archived":false,"fork":false,"pushed_at":"2026-05-03T06:00:59.000Z","size":23489,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-03T06:33:37.997Z","etag":null,"topics":["library","pillow","pillow-library","python","text-to-image"],"latest_commit_sha":null,"homepage":"https://fontstack.readthedocs.io/en/latest/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kanin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"Kanin"}},"created_at":"2026-04-03T22:02:15.000Z","updated_at":"2026-05-03T06:00:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Kanin/FontStack","commit_stats":null,"previous_names":["kanin/fontstack"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/Kanin/FontStack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kanin%2FFontStack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kanin%2FFontStack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kanin%2FFontStack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kanin%2FFontStack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kanin","download_url":"https://codeload.github.com/Kanin/FontStack/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kanin%2FFontStack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32575184,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["library","pillow","pillow-library","python","text-to-image"],"created_at":"2026-04-04T03:01:50.747Z","updated_at":"2026-05-03T16:06:29.709Z","avatar_url":"https://github.com/Kanin.png","language":"Python","funding_links":["https://github.com/sponsors/Kanin"],"categories":[],"sub_categories":[],"readme":"# FontStack\n\nUnicode text rendering for Pillow with automatic per-character font fallback, variable fonts, BiDi/RTL, gradients, outlines, shadows, and emoji.\n\n[![PyPI](https://img.shields.io/pypi/v/fontstack)](https://pypi.org/project/fontstack/)\n[![Python 3.11+](https://img.shields.io/pypi/pyversions/fontstack)](https://pypi.org/project/fontstack/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)\n[![Typed](https://img.shields.io/badge/typing-py.typed-informational)](https://peps.python.org/pep-0561/)\n\nPillow's built-in text rendering uses a single font, so any character not covered by that font shows up as a blank box (\"tofu\"). FontStack fixes this by walking an ordered list of fonts per character, the same fallback strategy browsers and operating systems use. It also handles right-to-left scripts, Arabic contextual shaping, variable fonts, TrueType Collections, and emoji.\n\n---\n\n## Features\n\n- Per-character font fallback using fonttools for accurate cmap parsing across TTF, OTF, and collection formats.\n- **Font directory scanning** via `font_dir=` constructor arg or `scan_font_dir()` -- point to a folder of fonts and skip manual `FontConfig` wiring. Fonts are loaded in alphabetical order by filename, so the first file becomes the primary font.\n- RTL/BiDi support via `python-bidi` for Unicode BiDi reordering. Arabic text is reshaped with `arabic-reshaper` before rendering so letters connect correctly under Pillow's BASIC layout engine.\n- Emoji rendered via [Pilmoji](https://github.com/jay3332/pilmoji) / Twemoji with correct baseline alignment across mixed font and emoji runs.\n- **Gradient fills** on text, outlines, and shadows via dash-separated color strings (e.g. `\"red-blue\"`, `\"#FF0000-#00FF00\"`) or the `\"rainbow\"` preset. Gradients are slightly diagonal so multi-line text gets natural color variation per line.\n- **Text outlines** (strokes) with configurable thickness and color, including gradient outlines.\n- **Drop shadows** with configurable color and offset. Shadow shape includes the outline when `stroke_width \u003e 0`. Supports gradient shadow colors.\n- Variable font support: set axes by integer value (`weight=700` sets `wght`) or by named style (`weight=\"Bold\"`). Typed `VariationAxes` for IDE autocomplete on standard axes.\n- TrueType/OpenType Collection support (`.ttc` / `.otc`) via `ttc_index` on `FontConfig`.\n- Two rendering modes: `\"wrap\"` breaks text across lines at a max width; `\"scale\"` shrinks the font to fit, truncating with `...` as a last resort.\n- **Fit mode** (`\"fit\"`) combines both: wraps at `max_width`, then shrinks the font until the block fits within `max_height`, then truncates the last visible line with `...` if necessary. `min_size` sets the floor for both scale and fit modes.\n- Left, center, and right alignment within the text block.\n- **Anchor points** (`anchor=`) on `FontManager.draw()`: choose which corner, edge midpoint, or centre of the text block lands at `position` using PIL-style two-character codes (`\"lt\"`, `\"mm\"`, `\"rb\"`, etc.).\n- LRU caching on both font objects and cmap data; repeated renders with the same stack/size/weight are essentially free.\n- Fully typed: `Literal` on `mode` and `align`, `@overload` signatures that surface `min_size` only when `mode=\"scale\"` or `mode=\"fit\"` and `max_height` only when `mode=\"fit\"`, PEP 561 `py.typed` marker.\n\n---\n\n## Gallery\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/11_multilingual/output.png\" alt=\"Nine languages\" width=\"240\"/\u003e\u003cbr/\u003e\u003cem\u003eNine languages, one stack\u003c/em\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/09_cjk/output.png\" alt=\"CJK fallback\" width=\"420\"/\u003e\u003cbr/\u003e\u003cem\u003eChinese · Japanese · Korean\u003c/em\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/10_scripts/output.png\" alt=\"Indic and RTL scripts\" width=\"420\"/\u003e\u003cbr/\u003e\u003cem\u003eDevanagari · Hebrew · Bengali · Thai\u003c/em\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/02_weights/output.png\" alt=\"Variable font weights\" width=\"420\"/\u003e\u003cbr/\u003e\u003cem\u003eVariable font weight axis (wght 100–900)\u003c/em\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd colspan=\"2\" align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/07_mixed/output.png\" alt=\"Mixed Arabic and Latin\"/\u003e\u003cbr/\u003e\u003cem\u003eMixed Arabic + Latin - BiDi reordering applied automatically\u003c/em\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd colspan=\"2\" align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/12_symbols/output.png\" alt=\"Unicode symbols and fancy text\"/\u003e\u003cbr/\u003e\u003cem\u003eSymbols · Math alphanumerics · Box drawing · Arrows\u003c/em\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd colspan=\"2\" align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/13_fit_mode/output.png\" alt=\"Fit mode - wrap, shrink, truncate\"/\u003e\u003cbr/\u003e\u003cem\u003eFit mode - wrap → shrink → truncate, all four strips share the same bounding box\u003c/em\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/16_gradient/output.png\" alt=\"Gradient fills, outlines, and shadows\" width=\"420\"/\u003e\u003cbr/\u003e\u003cem\u003eGradient fills, gradient outlines, gradient shadows\u003c/em\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/17_combined/output.png\" alt=\"All effects combined\" width=\"420\"/\u003e\u003cbr/\u003e\u003cem\u003eAll effects combined - gradient + outline + shadow\u003c/em\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/14_outline/output.png\" alt=\"Text outlines\" width=\"420\"/\u003e\u003cbr/\u003e\u003cem\u003eText outlines on Latin, Arabic, and mixed scripts\u003c/em\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/Kanin/FontStack/refs/heads/main/examples/15_shadow/output.png\" alt=\"Drop shadows\" width=\"420\"/\u003e\u003cbr/\u003e\u003cem\u003eDrop shadows with emoji silhouettes\u003c/em\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## Installation\n\n```bash\npip install fontstack\n```\n\n\u003e **Note:** FontStack does not bundle fonts. See [Recommended Font Stack](#recommended-font-stack) below for a curated set of free Noto fonts that provide near-complete Unicode coverage.\n\n---\n\n## Quick Start\n\n```python\nfrom fontstack import FontConfig, FontManager\n\nmanager = FontManager(\n    default_stack=[\n        FontConfig(path=\"fonts/NotoSans[wdth,wght].ttf\"),\n        FontConfig(path=\"fonts/NotoSansArabic[wdth,wght].ttf\"),\n    ]\n)\n\n# Or point to a directory and let FontStack discover all fonts\n# (loaded alphabetically by filename - first file = primary font):\n# manager = FontManager(font_dir=\"fonts/\")\n\nfrom PIL import Image\n\nimg = Image.new(\"RGBA\", (800, 100), \"white\")\nmanager.draw(\n    image=img,\n    text=\"Hello مرحبا\",\n    position=(20, 20),\n    size=48,\n    weight=700,\n    fill=(20, 20, 20),\n)\nimg.save(\"output.png\")\n```\n\n---\n\n## Usage\n\n### FontManager\n\n```python\nfrom fontstack import FontConfig, FontManager, VariationAxes\n\n# Option 1: explicit font stack (full control over fallback order)\nmanager = FontManager(\n    default_stack=[\n        # Primary font: Noto Sans variable (Latin, Cyrillic, Greek)\n        FontConfig(path=\"fonts/NotoSans[wdth,wght].ttf\"),\n        # Fallback 1: Noto Sans Arabic (Arabic, Persian, Urdu)\n        FontConfig(path=\"fonts/NotoSansArabic[wdth,wght].ttf\"),\n        # Fallback 2: Noto Sans SC/JP/KR (Simplified Chinese / Japanese / Korean)\n        FontConfig(path=\"fonts/NotoSansSC[wght].ttf\"),\n        FontConfig(path=\"fonts/NotoSansJP[wght].ttf\"),\n        FontConfig(path=\"fonts/NotoSansKR[wght].ttf\"),\n    ]\n)\n\n# Option 2: scan a directory (auto-discovers all .ttf/.otf/.ttc/.otc files)\n# Fonts are loaded in alphabetical order by filename, so the first file\n# becomes the primary font and later files act as fallbacks.\nmanager = FontManager(font_dir=\"fonts/\")\n\nfrom PIL import Image\n\nimg = Image.new(\"RGBA\", (1000, 200), \"white\")\nw, h = manager.draw(\n    image=img,\n    text=\"Hello 世界 مرحبا 🌍\",\n    position=(20, 40),\n    size=48,\n    weight=700,\n    mode=\"wrap\",\n    max_width=960,\n    align=\"center\",\n    fill=(30, 30, 30),\n)\nprint(f\"Rendered {w}×{h} px\")\nimg.save(\"output.png\")\n```\n\n### draw_text\n\nReturns a new `PIL.Image.Image` cropped tightly to the rendered text, no canvas management needed.\n\n```python\nfrom fontstack import FontConfig, draw_text\n\nimg = draw_text(\n    text=\"Hello 世界 مرحبا 🌍\",\n    font_stack=[\n        FontConfig(path=\"fonts/NotoSans[wdth,wght].ttf\"),\n        FontConfig(path=\"fonts/NotoSansArabic[wdth,wght].ttf\"),\n    ],\n    size=48,\n    weight=700,\n    fill=(20, 20, 20),\n    background=\"white\",\n    padding=16,\n)\nimg.save(\"hello.png\")\n```\n\n### Variable font axes\n\n```python\nfrom fontstack import FontConfig, VariationAxes\n\n# Narrow, light weight, slightly slanted\nFontConfig(\n    path=\"fonts/NotoSans[wdth,wght].ttf\",\n    axes=VariationAxes(wght=300.0, wdth=75.0, slnt=-10.0),\n)\n```\n\nStandard axes in `VariationAxes`: `wght` (weight, 100–900), `wdth` (width, 50–200), `ital` (italic, 0–1), `slnt` (slant, degrees), `opsz` (optical size).\n\n### Rendering modes\n\n```python\n# \"wrap\" - word-wrap at max_width, font size unchanged\nmanager.draw(img, long_text, position=(0, 0), size=32,\n                    mode=\"wrap\", max_width=400)\n\n# \"scale\" - shrink font until the full text fits on a single line;\n#             truncates with \"…\" if the text is still too wide at min_size\nmanager.draw(img, long_text, position=(0, 0), size=32,\n                    mode=\"scale\", max_width=400, min_size=10)\n\n# \"fit\" - wrap first, then shrink until the block fits within max_width × max_height;\n#           if the block still overflows at min_size the last visible line is\n#           truncated with \"…\"\nmanager.draw(img, long_text, position=(0, 0), size=32,\n                    mode=\"fit\", max_width=400, max_height=120, min_size=10)\n```\n\n### Anchor points\n\n`anchor` controls which point of the text block is placed at `position`.\nUseful for centering labels, pinning captions to corners, or aligning text\nto UI grid lines without manual offset math.\n\n```\nlt ── mt ── rt\n│           │\nlm    mm    rm\n│           │\nlb ── mb ── rb\n```\n\n```python\n# Centre text on a specific pixel (badge, map pin, etc.)\nmanager.draw(img, \"Hello\", position=(400, 200), size=48, anchor=\"mm\")\n\n# Bottom-right: text ends exactly at a corner\nmanager.draw(img, \"caption\", position=(img.width - 16, img.height - 16),\n                    size=20, anchor=\"rb\")\n\n# Top-center: heading centered at the top edge of a box\nmanager.draw(img, \"Title\", position=(box_cx, box_top), size=32, anchor=\"mt\")\n```\n\n\u003e **Typographic vertical centering:** The middle (`m`) vertical anchor centers\n\u003e on the *cap height* of the text — the region from the visual top of capital\n\u003e letters down to the baseline — rather than on the full rendered bounding box.\n\u003e This means strings with descenders (`g`, `y`, `p`, …) and strings without\n\u003e descenders share the same cap-top position when drawn at the same `y`,\n\u003e producing visually consistent rows (e.g. leaderboard entries, stat lines).\n\u003e Descenders hang below the center point as they do in traditional typography.\n\n### Text effects\n\n```python\n# Gradient fill (diagonal by default so each line gets different hues)\nmanager.draw(img, \"Gradient!\", position=(20, 20), size=64,\n                    fill=\"red-gold-orange\")\n\n# Pure left-to-right gradient (no diagonal)\nmanager.draw(img, \"Gradient!\", position=(20, 20), size=64,\n                    fill=\"red-gold-orange\", gradient_angle=0.0)\n\n# Outline with solid stroke color\nmanager.draw(img, \"Outlined\", position=(20, 120), size=64,\n                    fill=\"white\", stroke_width=3, stroke_fill=\"black\")\n\n# Drop shadow (shadow includes the outline shape when stroke_width \u003e 0)\nmanager.draw(img, \"Shadow\", position=(20, 220), size=64,\n                    fill=\"white\", shadow_color=(0, 0, 0, 120),\n                    shadow_offset=(4, 4))\n\n# Everything at once: gradient fill + gradient outline + gradient shadow\nmanager.draw(img, \"All Effects!\", position=(20, 320), size=64,\n                    fill=\"red-orange-gold\",\n                    stroke_width=3, stroke_fill=\"blue-cyan\",\n                    shadow_color=\"gray-darkgray\", shadow_offset=(3, 3))\n\n# Rainbow preset\nmanager.draw(img, \"Rainbow!\", position=(20, 420), size=64,\n                    fill=\"rainbow\")\n```\n\n### Batch rendering with a shared manager\n\nReusing a `FontManager` across many `draw_text` calls avoids re-parsing cmaps for every image.\n\n```python\nfrom fontstack import FontConfig, FontManager, draw_text\n\nmgr = FontManager(default_stack=[FontConfig(path=\"fonts/NotoSans[wdth,wght].ttf\")])\n\nlabels = [\"First\", \"Second\", \"Third\", ...]\nimages = [draw_text(label, font_stack=[], manager=mgr, size=32) for label in labels]\n```\n\n---\n\n## Recommended Font Stack\n\nAll fonts below are from Google's [Noto](https://fonts.google.com/noto) family, licensed under the [SIL Open Font License 1.1](https://openfontlicense.org/) (free for commercial use).\n\n| # | Font | Scripts covered | Size | Download |\n|---|------|----------------|------|----------|\n| 1 | **Noto Sans** `[wdth,wght].ttf` | Latin, Cyrillic, Greek, Latin Extended | ~1.1 MB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans) |\n| 2 | **Noto Sans Arabic** `[wdth,wght].ttf` | Arabic, Persian, Urdu | ~840 KB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+Arabic) |\n| 3 | **Noto Sans SC** `[wght].ttf` | Simplified Chinese | ~17 MB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+SC) |\n| 4 | **Noto Sans JP** `[wght].ttf` | Japanese | ~9.4 MB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+JP) |\n| 5 | **Noto Sans KR** `[wght].ttf` | Korean | ~10 MB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+KR) |\n| 6 | **Noto Sans Devanagari** `[wdth,wght].ttf` | Hindi, Sanskrit, Marathi, Nepali | ~632 KB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+Devanagari) |\n| 7 | **Noto Sans Hebrew** `[wdth,wght].ttf` | Hebrew, Yiddish | ~110 KB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+Hebrew) |\n| 8 | **Noto Sans Bengali** `[wdth,wght].ttf` | Bengali, Assamese | ~454 KB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+Bengali) |\n| 9 | **Noto Sans Thai** `[wdth,wght].ttf` | Thai | ~214 KB | [Google Fonts](https://fonts.google.com/specimen/Noto+Sans+Thai) |\n\n\u003e Emoji are handled by Pilmoji/Twemoji automatically, no emoji font needed in the stack.\n\n---\n\n## API Reference\n\n### `FontManager(default_stack=None, *, font_dir=None, max_cache=30)`\n\nCreate with an explicit `default_stack` **or** a `font_dir` path (mutually exclusive). When `font_dir` is given, all `.ttf`, `.otf`, `.ttc`, `.otc` files in the directory are scanned and sorted alphabetically by filename (case-insensitive). The first file in that order becomes the primary font; the rest serve as fallbacks.\n\n| Method / Property | Returns | Description |\n|--------|---------|-------------|\n| `draw(image, text, position, ...)` | `tuple[int, int]` | Draw text onto an existing image in-place. Returns `(width, height)` of the rendered bounding box. |\n| `get_font_chain(size, weight, custom_stack)` | `list[FreeTypeFont]` | Return loaded font objects for the given size/weight (LRU-cached). |\n| `default_stack` | `list[FontConfig]` | Read-only copy of the stack (auto-built from `font_dir` if used). |\n| `font_dir` | `str \\| Path \\| None` | The font directory passed at construction, or `None`. |\n\n#### `draw` key parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `size` | `int` | `40` | Starting font size in points. |\n| `weight` | `int \\| str` | `400` | Font weight axis value or named style string (e.g. `700` or `\"Bold\"`). |\n| `mode` | `\"wrap\" \\| \"scale\" \\| \"fit\"` | `\"wrap\"` | Rendering mode. |\n| `max_width` | `int \\| None` | `None` | Maximum line width in pixels. |\n| `max_height` | `int \\| None` | `None` | Maximum block height in pixels (`\"fit\"` mode only). |\n| `min_size` | `int` | `12` | Minimum font size for `\"scale\"` and `\"fit\"` modes. |\n| `align` | `\"left\" \\| \"center\" \\| \"right\"` | `\"left\"` | Horizontal alignment within the text block. |\n| `anchor` | `\"lt\"` … `\"rb\"` | `\"lt\"` | Which point of the text block lands at `position`. Two-char PIL-style code: horizontal (`l`/`m`/`r`) + vertical (`t`/`m`/`b`). |\n| `line_spacing` | `float` | `1.2` | Line-height multiplier (1.0 = tight, 1.5 = loose). |\n| `fill` | `FillType` | `\"black\"` | Text color: color name, RGB/RGBA tuple, palette integer, or gradient string. |\n| `stroke_width` | `int` | `0` | Outline thickness in pixels around each glyph. |\n| `stroke_fill` | `FillType \\| None` | `None` | Outline color. Same value types as `fill`, including gradients. |\n| `shadow_color` | `FillType \\| None` | `None` | Drop-shadow color. Same value types as `fill`, including gradients. |\n| `shadow_offset` | `tuple[int, int]` | `(2, 2)` | Shadow pixel offset `(x, y)`. |\n| `gradient_angle` | `float` | `15.0` | Gradient direction in degrees (0 = left-to-right, 15 = slight diagonal). |\n| `font_stack` | `list[FontConfig] \\| None` | `None` | Per-call font stack override; falls back to `default_stack`. |\n| `emoji_source` | `BaseSource` | `Twemoji` | Pilmoji emoji image source. |\n\n### `draw_text(text, font_stack, ...) -\u003e Image.Image`\n\nConvenience wrapper: creates a `FontManager` (or reuses one via `manager=`), renders `text`, and returns a new `RGBA` image cropped to the result with optional `padding` and `background`. Also accepts `font_dir=` as an alternative to `font_stack`.\n\n### `scan_font_dir(font_dir, *, recursive=False) -\u003e list[FontConfig]`\n\nScan a directory for font files (`.ttf`, `.otf`, `.ttc`, `.otc`) and return a list of `FontConfig` entries sorted alphabetically by filename (case-insensitive). The first entry becomes the primary font when passed to `FontManager`. TTC/OTC collections produce one entry per member font. Useful for inspecting what `font_dir=` will discover, or for building a custom stack from a directory listing.\n\n### `FontConfig`\n\n| Field | Type | Default | Description |\n|-------|------|---------|-------------|\n| `path` | `str` | required | Path to TTF, OTF, TTC, or OTC file. |\n| `axes` | `VariationAxes \\| None` | `None` | Default variable font axis values. |\n| `ttc_index` | `int` | `0` | Index within a TTC/OTC collection. |\n\n### `VariationAxes` (TypedDict, all optional)\n\n`wght` · `wdth` · `ital` · `slnt` · `opsz`\n\n---\n\n## Requirements\n\n- Python 3.11+\n- [Pillow](https://python-pillow.org/) ≥ 12.2\n- [pilmoji](https://github.com/jay3332/pilmoji) ≥ 2.0.5\n- [fonttools](https://github.com/fonttools/fonttools) ≥ 4.62\n- [python-bidi](https://github.com/MeirKriheli/python-bidi) ≥ 0.6.7\n- [arabic-reshaper](https://github.com/mpcabd/python-arabic-reshaper) ≥ 3.0.0\n\n---\n\n## License\n\n[MIT](LICENSE) © 2026 Kanin\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanin%2Ffontstack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkanin%2Ffontstack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanin%2Ffontstack/lists"}