pyyudit
Cross-platform Python binding to Yudit's Unicode shaping engine, with per-step trace output.
What is this?
pyyudit extracts the shaping engine from the Yudit Unicode editor into a standalone shared library and wraps it with Python ctypes bindings. It provides:
- Indic script shaping — Devanagari, Bengali, Gujarati, Gurmukhi, Oriya, Telugu, Kannada, Malayalam, Sinhala
- Arabic / Syriac joining — isolated, initial, medial and final forms, plus required ligatures (lam-alef, Allah)
- Hebrew, Thaana, N'Ko, Mandaic, Mongolian — right-to-left ordering and joining
- Thai, Lao, Tibetan text processing
- Hangul Jamo processed through
ccmp/ljmo/vjmo/tjmo(no Unicode precomposition) - OpenType GSUB/GPOS feature application, including
Extensionlookups (GSUB type 7 / GPOS type 9) used by Arial, Segoe UI, Noto, … - BiDi (bidirectional text) base-direction detection and RTL visual ordering
- Per-step trace — see every decompose → reorder → gsub → gpos step
How Yudit shapes text
Yudit has two shaping layers, and pyyudit routes text through the right one:
| Layer | Handles | Where |
|---|---|---|
Paragraph layer (SParagraph + SGlyph::setShape) |
Arabic/Syriac joining, Hebrew/RTL ordering, Unicode presentation forms, lam-alef ligature | YuditParagraphShaper |
Script processor (SScriptProcessor) |
Indic reordering (Devanagari … Malayalam, Sinhala), Thai, Lao, Tibetan, Jamo + their GSUB/GPOS | YuditScriptProcessor |
Both paths share the same OpenType engine. Mark positioning (mark, mkmk) and
required/contextual ligatures (rlig, calt) are what Yudit implements; pair
kerning (kern) is not one of them - see Limitations.
Architecture
pyyudit (Python)
├── YuditFont — load font from bytes/path
│ ├── shape() — one-shot shaping
│ └── shape_trace() — shaping with full trace
├── _binding.py — ctypes declarations for C API
├── _loader.py — cross-platform native lib discovery
└── _lib/ — bundled native library (DLL/SO/dylib)
yudit_shaper (C/C++)
├── yudit_shaper.h/cpp — C API wrapper
├── YuditParagraphShaper.h/cpp — paragraph layer (Arabic/Syriac joining, RTL)
├── YuditScriptProcessor.h/cpp — script processor trace (Indic, Thai, Lao, …)
├── stoolkit/ — Yudit core (BiDi, Unicode, glyphs, paragraphs)
├── swindow/ — Font handling (SFontTTF) + SScriptProcessor
└── CMakeLists.txt — build system
Installation
From source (with native build)
# Prerequisites: CMake 3.15+, C++ compiler
git clone https://github.com/Kushim-Jiang/pyyudit.git
cd pyyudit
# Download Yudit source (for the shaping engine)
curl -L https://www.yudit.org/download/yudit-3.1.0.tar.gz | tar xz
# Build the native library
python scripts/build_native.py
# Install the Python package
pip install -e ".[dev]"
Cross-platform wheel
# Build wheel (includes the native library)
pip install hatchling build
python -m build
pip install dist/pyyudit-0.2.0-py3-none-any.whl
Usage
import pyyudit
# One-shot shaping
font_bytes = open("NotoSansDevanagari.ttf", "rb").read()
glyphs = pyyudit.shape(font_bytes, "नमस्ते")
for g in glyphs:
print(f"gid={g.g} cluster={g.cl} adv={g.ax}")
# With full trace
with pyyudit.YuditFont.from_path("NotoSansDevanagari.ttf") as f:
result = f.shape_trace("नमस्ते")
for stage in result.stages:
print(f"{stage.m}: {len(stage.glyphs)} glyphs")
if stage.effective:
print(" (buffer changed)")
# Reusable font object
font = pyyudit.YuditFont.from_path("NotoSansArabic.ttf")
result = font.shape("مرحبا")
print(f"Advance: {result.advance_x} units")
font.close()
Trace stages
The trace captures every step of Yudit's shaping pipeline. Which stages you see depends on the script:
| Stage | Path | Description |
|---|---|---|
input |
both | Original codepoints before shaping |
decompose |
Indic | Unicode decomposition (vowel splits etc.) |
reorder |
Indic | Indic consonant/vowel reordering |
split |
joining | Paragraph split, composition and clustering |
join |
joining | Joining form selected (isolated/initial/medial/final) |
gindex |
both | Glyph index lookup from cmap |
half |
Indic | Half-form substitution (GSUB) |
pres |
Indic | Pre-base substitution (GSUB) |
abvs |
Indic | Above-base substitution (GSUB) |
blws |
Indic | Below-base substitution (GSUB) |
psts |
Indic | Post-base substitution (GSUB) |
haln |
Indic | Halant/virama substitution (GSUB) |
ccmp |
plain + joining | Glyph composition/decomposition (GSUB) |
rlig |
plain + joining | Required ligatures — lam-alef, Allah (GSUB) |
calt |
plain + joining | Contextual alternates (GSUB) |
liga |
joining | Standard ligatures (GSUB) |
abvm |
Indic | Above-base mark positioning (GPOS) |
blwm |
Indic | Below-base mark positioning (GPOS) |
mark |
plain + joining | Mark-to-base positioning (GPOS) |
mkmk |
plain + joining | Mark-to-mark positioning (GPOS) |
kern |
plain + joining | Pair kerning (GPOS type 2) - Yudit's handler only inspects coverage, so this stage never reports a position |
clean |
Indic | Post-GSUB cleanup |
gpos_final |
Indic | Final positioning |
reorder |
joining | Right-to-left visual ordering (emitted when the run is RTL) |
shaped |
both | Final shaped glyph buffer |
Each stage carries the full glyph buffer at that point, so stage.glyphs
is a snapshot you can diff against the previous stage. stage.effective
is True when that step actually changed the buffer — a substitute that found
nothing, or a reorder that was already in order, reports False.
Every GSUB/GPOS feature stage is followed by one group of steps per lookup the feature ran, using the same message vocabulary as the reference trace:
start lookup 114 feature 'half'
skipped lookup 114 feature 'half' because no glyph matches
end lookup 114 feature 'half'
skipped appears only when no glyph matched. Each start/end pair holds the
glyph snapshot as it is after the whole feature ran — the engine does not expose
a buffer per lookup, so the per-lookup steps share the feature's snapshot.
effective sits on the end step and marks the lookup that changed the buffer.
Yudit retries a feature at every position in the run; the repeats are collapsed
so each lookup is listed once per feature.
gholds the Unicode codepoint in the stages before glyph lookup (input,decompose,reorder,split,join) and the glyph index afterwards, which is how the reference trace reports an unmapped buffer.clis the index into the original text.
In the joining path the glyphs are returned in visual order (reversed for RTL runs, like HarfBuzz) and
dxis the pen delta between consecutive output glyphs, sopen += dxreproduces the exact positions.
tests/compare_trace.py lays the trace next to a HarfBuzz trace of the same
text, so the lookup count, the feature sequence and the resulting buffer can be
compared step by step.
Pure-Python helpers
These work without the native library:
from pyyudit import upem_from_ttf, has_table, read_font_bytes
data = read_font_bytes("font.ttf")
upem = upem_from_ttf(data)
has_gsub = has_table(data, "GSUB")
Configuration
# Set explicit library path
pyyudit.configure("/path/to/libyudit_shaper.so")
# Or via environment variable
# export PYYUDIT_LIBRARY_PATH=/path/to/libyudit_shaper.so
# Check availability
print(pyyudit.is_available())
print(pyyudit.library_info())
Supported scripts
Yudit's shaping engine supports these scripts (with OpenType features):
| Script | Tag | GSUB features | GPOS features |
|---|---|---|---|
| Plain (Latin, Greek, Cyrillic, CJK, symbols) | latn hani kana hang |
ccmp isol fina medi init rlig calt | kern mark mkmk |
| Devanagari | deva |
nukt akhn rphf blwf half vatu pres abvs blws psts haln | abvm blwm dist |
| Gujarati | gujr |
nukt akhn rphf blwf half pstf vatu pres abvs blws psts haln | abvm blwm dist |
| Gurmukhi | guru |
nukt blwf pstf vatu pres abvs blws psts haln | abvm blwm dist |
| Oriya | orya |
nukt akhn rphf blwf half pstf vatu pres blws abvs psts haln | abvm blwm dist |
| Tamil | taml |
akhn half pres abvs blws psts haln | abvm blwm dist |
| Telugu | telu |
akhn blwf abvs blws psts haln | abvm blwm dist |
| Kannada | knda |
nukt akhn rphf blwf half pstf vatu pres blws abvs psts haln | abvm blwm dist |
| Malayalam | mlym |
nukt akhn rphf blwf half pstf vatu pres blws abvs psts haln | abvm blwm dist |
| Sinhala | sinh |
nukt akhn rphf blwf half pstf vatu pres blws abvs psts haln | abvm blwm dist |
| Thai | thai |
ccmp | kern mark mkmk |
| Lao | lao |
ccmp blws abvs | kern mark mkmk |
| Tibetan | tibt |
ccmp blws abvs | blwm abvm kern |
| Jamo | jamo |
ccmp ljmo vjmo tjmo | — |
Joining / right-to-left scripts go through the paragraph layer instead:
| Script | Tag | Direction | Shaping |
|---|---|---|---|
| Arabic | arab |
RTL | joining forms via Unicode shape tables, then rlig calt liga |
| Syriac | syrc |
RTL | joining forms, incl. the Alaph end-of-word rules |
| Hebrew | hebr |
RTL | RTL ordering (no joining) |
| Thaana | thaa |
RTL | RTL ordering + joining |
| N'Ko | nko |
RTL | RTL ordering + joining |
| Mandaic | mand |
RTL | RTL ordering + joining |
| Mongolian | mong |
LTR | joining forms |
Kerning
Yudit does not kern. Pair adjustment (kern, GPOS lookup type 2) exists as a
function in the engine but only checks coverage and always returns false, and the
legacy kern table code sits behind #if 0. The kern stage therefore shows up
in the trace as not effective for every font. This is reported as-is rather
than implemented: the point of the trace is to show what Yudit does.
Limitations
- Pair kerning (
kern) and legacykerntables are absent, as described above. - GPOS cursive attachment (
curs, lookup type 3) is not implemented. Fonts that encode Arabic joining adjustments there (Dubai, for example) will miss those deltas. - Chained contextual positioning (lookup type 8) is only partially implemented.
- Hangul jamo are not precomposed: Yudit runs
ljmo/vjmo/tjmoand leaves the jamo as separate glyphs when the font does not carry those features. - Reordering is per run: the base direction is detected with UAX#9 P2/P3 and the run is reversed for RTL. Multi-run BiDi is the caller's job, as it is with HarfBuzz.
face_indexonyudit_font_openis currently ignored and font collections (TTC) are not supported.
License
- pyyudit Python code: MIT
- Yudit shaping engine: GPL-2.0-or-later (Gaspar Sinai)
- See COPYING.TXT for details.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
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 pyyudit-0.2.0-0-py3-none-win_amd64.whl.
File metadata
- Download URL: pyyudit-0.2.0-0-py3-none-win_amd64.whl
- Upload date:
- Size: 445.2 kB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44ab57d03e625b6213bc56fe74732ac643d93c9f5cf8467fa2bdc895aa8c4525
|
|
| MD5 |
44ae0565e2c18bc03e337c7a468a439e
|
|
| BLAKE2b-256 |
3f3496ae761cdb645f8ae545bcfaa27442ab373ca02ea8adbcbac5344d34385c
|
Provenance
The following attestation bundles were made for pyyudit-0.2.0-0-py3-none-win_amd64.whl:
Publisher:
build.yml on Kushim-Jiang/pyyudit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyyudit-0.2.0-0-py3-none-win_amd64.whl -
Subject digest:
44ab57d03e625b6213bc56fe74732ac643d93c9f5cf8467fa2bdc895aa8c4525 - Sigstore transparency entry: 2806865592
- Sigstore integration time:
-
Permalink:
Kushim-Jiang/pyyudit@426c718b277ee4170b1698c9e60ca2dada0ae808 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Kushim-Jiang
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@426c718b277ee4170b1698c9e60ca2dada0ae808 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyyudit-0.2.0-0-py3-none-manylinux_2_17_x86_64.whl.
File metadata
- Download URL: pyyudit-0.2.0-0-py3-none-manylinux_2_17_x86_64.whl
- Upload date:
- Size: 731.7 kB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bff8e2d6f686fc31f3aae59b97d685246a4de2bec967d62bb39536276bcdbed1
|
|
| MD5 |
84510b707716af894eeaa1e66c06b672
|
|
| BLAKE2b-256 |
e911beb95ad5c32069f580fb22b20273147ccc6aeaa6a9af326b4ecc8b9cc8f5
|
Provenance
The following attestation bundles were made for pyyudit-0.2.0-0-py3-none-manylinux_2_17_x86_64.whl:
Publisher:
build.yml on Kushim-Jiang/pyyudit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyyudit-0.2.0-0-py3-none-manylinux_2_17_x86_64.whl -
Subject digest:
bff8e2d6f686fc31f3aae59b97d685246a4de2bec967d62bb39536276bcdbed1 - Sigstore transparency entry: 2806865626
- Sigstore integration time:
-
Permalink:
Kushim-Jiang/pyyudit@426c718b277ee4170b1698c9e60ca2dada0ae808 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Kushim-Jiang
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@426c718b277ee4170b1698c9e60ca2dada0ae808 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyyudit-0.2.0-0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: pyyudit-0.2.0-0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 581.9 kB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
596b6996e2d513a04edbb077fafd0f6bb49e32c8804f1e874e781c3c9936cf08
|
|
| MD5 |
511f52eada2d44d69da977c0f682dc17
|
|
| BLAKE2b-256 |
9037cabedf4ad2c2aea552f5aca1dddeadef6c1dc39db38569bca891036fcacc
|
Provenance
The following attestation bundles were made for pyyudit-0.2.0-0-py3-none-macosx_11_0_arm64.whl:
Publisher:
build.yml on Kushim-Jiang/pyyudit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyyudit-0.2.0-0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
596b6996e2d513a04edbb077fafd0f6bb49e32c8804f1e874e781c3c9936cf08 - Sigstore transparency entry: 2806865660
- Sigstore integration time:
-
Permalink:
Kushim-Jiang/pyyudit@426c718b277ee4170b1698c9e60ca2dada0ae808 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Kushim-Jiang
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@426c718b277ee4170b1698c9e60ca2dada0ae808 -
Trigger Event:
push
-
Statement type: