Parmoji
Description
Parmoji is a Pillow-based emoji rendering library (unicode + Discord custom emoji) with pluggable image sources (HTTP CDN and local font), LRU in-memory caching, and optional on-disk caching using XDG locations. It’s extracted from the par-term-emu project and packaged as a standalone library for reuse.
Technology
- Python 3.11+
- Pillow
- httpx (primary HTTP backend) and requests (selectable alternate backend)
Prerequisites
- Python 3.11 or higher
- uv package manager (recommended)
Features
- Unicode and Discord emoji
- Multi-line rendering with alignment and anchors
- Fine control over emoji size/position per draw call
- Multiple built-in emoji sources (Twemoji, Apple, Google, etc.)
- LRU in-memory cache and optional disk cache (XDG)
- PEP 561 typed (
py.typedships; inline types are visible to pyright/mypy) - Selectable HTTP backend (httpx default; switch to
requestsper source viaHTTP_BACKEND)
Installation
uv add parmoji
Update
uv add parmoji -U
Quickstart
from parmoji import Parmoji
from parmoji.source import TwitterEmojiSource
from PIL import Image, ImageFont
text = "Hello 👋 from Parmoji 😎"
img = Image.new("RGBA", (480, 120), (255, 255, 255, 255))
font = ImageFont.load_default()
with Parmoji(img, source=TwitterEmojiSource, cache=True) as p:
p.text((10, 20), text, fill=(0, 0, 0), font=font)
img.save("parmoji_example.png")
Emoji Sources and Caching
- Default source is
Twemoji(Twitter-style). Swap viaParmoji(image, source=AppleEmojiSource). - Disk cache: construct sources with
disk_cache=Trueto persist assets. - Cache location:
$XDG_CACHE_HOME/par-term/parmoji/<SourceClass>/(or~/.cache/par-term/parmoji/<SourceClass>/). - Clear failed CDN retries:
source.clear_failed_cache().
Built-in sources
Parmoji ships twelve HTTP CDN styles (all routed through https://emojicdn.elk.sh/) plus one offline source. CDN sources are imported from parmoji.source; the offline source from parmoji.local_source.
| Class | STYLE |
|---|---|
TwitterEmojiSource |
twitter |
AppleEmojiSource |
apple |
GoogleEmojiSource |
google |
MicrosoftEmojiSource |
microsoft |
SamsungEmojiSource |
samsung |
WhatsAppEmojiSource |
whatsapp |
FacebookEmojiSource |
facebook |
MessengerEmojiSource |
messenger |
JoyPixelsEmojiSource |
joypixels |
OpenmojiEmojiSource |
openmoji |
EmojidexEmojiSource |
emojidex |
MozillaEmojiSource |
mozilla |
LocalFontSource |
(renders from system fonts; no network) |
Aliases: Twemoji and TwemojiEmojiSource are TwitterEmojiSource; Openmoji is OpenmojiEmojiSource; FacebookMessengerEmojiSource is MessengerEmojiSource; DiscordEmojiSource is the preferred public name for DiscordEmojiSourceMixin (every CDN style above already subclasses it, so Discord custom-emoji support is built in). TwitterEmojiSource is also the emoji set Discord uses.
Local font source (offline rendering)
For no-network or system-font rendering, use LocalFontSource. It renders emoji from locally installed fonts (Apple Color Emoji on macOS, Segoe UI Emoji on Windows, Noto Color Emoji on Linux by default) and needs no HTTP backend. It supports the same disk_cache and get_emoji(..., tight=, margin=) API as the CDN sources.
from parmoji.local_source import LocalFontSource
src = LocalFontSource(disk_cache=True) # renders using a detected system emoji font
get_emojialways crops to the glyph's alpha bounding box (there is no CDN safe-zone to remove);tight/margindrive the crop padding and are honored, with results cached under a derived key.- Discord custom emoji are not supported by
LocalFontSource:get_discord_emoji(...)always returnsNone. Use a CDN source if you need Discord emoji. - Disk-cache I/O is shared with the CDN sources via
BaseSource._cache_get/_cache_put, so cache behavior is consistent across source kinds.
Tight Cropping (remove Twemoji safe-zone)
Some emoji sets (notably Twemoji) include transparent padding around glyphs. To have the visible emoji fill the cell area (useful for multi-cell flags), request a tightly cropped asset directly from the source:
from parmoji.source import TwitterEmojiSource
src = TwitterEmojiSource(disk_cache=True)
stream = src.get_emoji("🇺🇸", tight=True, margin=1) # crop to alpha bbox + 1px margin
- Cropped variants are cached on disk with a derived key, so subsequent calls don’t repeat work.
- You can enable tight cropping by default via environment:
PARMOJI_TIGHT=1to enablePARMOJI_TIGHT_MARGIN=2to set a default margin
Tuning HTTP behavior
httpx is the default backend for every CDN source; requests is an alternate, selected per subclass via the HTTP_BACKEND ClassVar. Both are hard dependencies (no urllib fallback).
from parmoji.source import TwitterEmojiSource
class RequestsTwemoji(TwitterEmojiSource):
HTTP_BACKEND = "requests"
TIMEOUT = 5.0 # per-request timeout (seconds)
MAX_RETRIES = 5 # attempt count
RETRY_BACKOFF = 0.5 # base for exponential backoff
The full set of public override knobs lives on HTTPBasedSource: REQUEST_KWARGS, TIMEOUT, MAX_RETRIES, RETRY_BACKOFF, HTTP_BACKEND, MAX_RESPONSE_BYTES, ALLOWED_SCHEMES, ALLOWED_HOSTS, and TRUST_ENV. The last three are security guards — ALLOWED_HOSTS is re-checked against the post-redirect URL, and any mirror/failover host you point a subclass at must be added there.
Architecture
For a high-level system design, components, rendering flow, and caching details, see the Architecture overview:
Development
make setup # uv lock + uv sync
make checkall # lint + format + typecheck + test
make test # run tests
make package-all # build wheel + sdist
- Pre-commit:
pre-commit install(thenpre-commit run --all-files) - Type checking:
uv run pyright - Lint/format:
uv run ruff check --fix src/ testsanduv run ruff format src/ tests
CI / Releases
- Build & test on push:
.github/workflows/build.yml - Publish to TestPyPI (manual):
.github/workflows/publish-dev.yml - Publish to PyPI (manual):
.github/workflows/publish.yml(trusted publishing) - GitHub Release (manual):
.github/workflows/release.yml
Limitations / Known Issues
- Discord custom emoji require network access. Discord emoji are fetched from
cdn.discordapp.comvia an HTTP source;LocalFontSource.get_discord_emoji(...)always returnsNone. - Tight cropping exists to remove CDN safe-zone padding. Some sets (notably Twemoji) ship glyphs with transparent padding;
get_emoji(..., tight=True, margin=N)trims to the alpha bounding box so the visible glyph fills its cell (useful for multi-cell flags).LocalFontSourcealready renders tightly cropped and usestight/marginonly for crop padding. - Thread safety. A single
Parmojiinstance is not safe for concurrenttext()calls across threads. Create one instance per thread, or serialize access. - Single upstream CDN, no automatic failover. All twelve built-in styles route through one host (
emojicdn.elk.sh); a host outage takes down every style. SubclassEmojiCDNSourcewith a customBASE_EMOJI_CDN_URL(and add the host toHTTPBasedSource.ALLOWED_HOSTS) to point at a mirror.
What's New
For the full versioned changelog, see docs/RELEASE_NOTES.md.
- 2.1.0 — Audit remediation: PEP 561
py.typedshipped; animated Discord emoji (.gif) andParmoji.open()httpx-restore fixes; HTTP-backend hardening (decompression-bomb guards, host allowlist); bounded, thread-safebytescaches; configurableHTTP_BACKEND;stroke_width-awaregetsize;DiscordEmojiSourcealias. - 2.0.8 — Updated dependencies and ensure Python 3.13 compatibility
- 2.0.7 — Tight-cropping support for CDN sources (Twemoji, etc.):
get_emoji(..., tight=True, margin=1)trims Twemoji's transparent safe‑zone.- Cropped variants are cached with a derived key.
- Env toggles:
PARMOJI_TIGHT=1,PARMOJI_TIGHT_MARGIN=2.
License
MIT — see LICENSE.
Acknowledgements
Originally based on the Pilmoji project by jay3332; heavily refactored and optimized.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file parmoji-2.1.0.tar.gz.
File metadata
- Download URL: parmoji-2.1.0.tar.gz
- Upload date:
- Size: 34.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f90416c5eafa39548dafebf24f118d2fbf2d4561939d97af55f5e5a0eea48dc8
|
|
| MD5 |
de90290e2aed02ac670e79c764299bb3
|
|
| BLAKE2b-256 |
cbc7e7f7c5551a394fea1f47bc30832f6e986b6fa9c3adf6bf488938e8c2cc50
|
Provenance
The following attestation bundles were made for parmoji-2.1.0.tar.gz:
Publisher:
publish.yml on paulrobello/parmoji
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
parmoji-2.1.0.tar.gz -
Subject digest:
f90416c5eafa39548dafebf24f118d2fbf2d4561939d97af55f5e5a0eea48dc8 - Sigstore transparency entry: 2298307682
- Sigstore integration time:
-
Permalink:
paulrobello/parmoji@943f245df788187556fd4d31976b253218d4abd5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/paulrobello
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@943f245df788187556fd4d31976b253218d4abd5 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file parmoji-2.1.0-py3-none-any.whl.
File metadata
- Download URL: parmoji-2.1.0-py3-none-any.whl
- Upload date:
- Size: 37.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74a8bc81cb913d336d4c11dd65046bb5645ad97e7ece953df142d218dcfcd804
|
|
| MD5 |
da2096e182262d50b3be536509cac368
|
|
| BLAKE2b-256 |
ad4298f55ee53f50b7aa3ed991f4a43a4949099865abc6150890f473f50f8cb4
|
Provenance
The following attestation bundles were made for parmoji-2.1.0-py3-none-any.whl:
Publisher:
publish.yml on paulrobello/parmoji
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
parmoji-2.1.0-py3-none-any.whl -
Subject digest:
74a8bc81cb913d336d4c11dd65046bb5645ad97e7ece953df142d218dcfcd804 - Sigstore transparency entry: 2298307697
- Sigstore integration time:
-
Permalink:
paulrobello/parmoji@943f245df788187556fd4d31976b253218d4abd5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/paulrobello
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@943f245df788187556fd4d31976b253218d4abd5 -
Trigger Event:
workflow_dispatch
-
Statement type: