---
title: "Right-to-left and mixed-direction text"
url: https://recursiveai.org/onefont/cookbook/right-to-left-text/
description: "Lay out Arabic, Hebrew and mixed-direction text in C#: resolve the Unicode bidi levels, cut the paragraph into runs, put the runs in display order, and…"
version: "1.3.0"
---

# Right-to-left and mixed-direction text

Lay out Arabic, Hebrew and mixed-direction text in C#: resolve the Unicode bidi levels, cut the paragraph into runs, put the runs in display order, and shape each one in its direction.

Text is stored in logical order, the order it is typed and read. On screen, right-to-left letters run from right to left, and numbers and Latin words inside them keep left-to-right order. The Unicode Bidirectional Algorithm (UAX #9) decides the direction of every character. onefont implements it on Unicode 17 data and passes the official `BidiTest.txt` and `BidiCharacterTest.txt` in full.

```csharp
using OneFont;

var chain = new FontFallbackChain([
    FontFace.Load(File.ReadAllBytes("NotoSans-Regular.ttf")),
    FontFace.Load(File.ReadAllBytes("NotoSansHebrew-Regular.ttf")),
    FontFace.Load(File.ReadAllBytes("NotoSansArabic-Regular.ttf")),
]);

Show("The word שלום means peace.");
Show("في عام 2026 قرأت 12 كتابا.");   // "In 2026 I read 12 books."

void Show(string text)
{
    // 1. Bidi levels, one per UTF-16 unit. Odd levels are right to left.
    byte[] levels = TextShaping.GetBidiLevels(text, TextDirection.Auto, out int paragraphLevel);

    // 2. Cut the paragraph into runs of one level.
    var runs = new List<(int Start, int End, byte Level)>();
    for (int i = 0, start = 0; i <= text.Length; i++)
    {
        if (i == text.Length || levels[i] != levels[start])
        {
            runs.Add((start, i, levels[start]));
            start = i;
        }
    }

    // 3. Display order of the runs (rule L2), from their levels.
    int[] visual = TextShaping.GetVisualOrder(runs.Select(r => r.Level).ToArray());

    // 4. Shape each run in its direction and place the runs left to right.
    Console.WriteLine($"{(paragraphLevel == 1 ? "RTL" : "LTR")} paragraph, {runs.Count} runs, left to right on screen:");
    float x = 0;
    foreach (int r in visual)
    {
        (int start, int end, byte level) = runs[r];
        bool rtl = (level & 1) == 1;
        IReadOnlyList<ShapedCluster> clusters = chain.ShapeRun(text.AsSpan(start, end - start), rightToLeft: rtl);
        float width = clusters.Sum(c => c.Advance * 16f / chain.Faces[c.FaceIndex].UnitsPerEm);
        // Clusters come in logical order: draw a right-to-left run's clusters last to first.
        Console.WriteLine($"  x={x,6:F2}  level {level} {(rtl ? "RTL" : "LTR")}  \"{text[start..end]}\"");
        x += width;
    }
}
```

```text
LTR paragraph, 3 runs, left to right on screen:
  x=  0.00  level 0 LTR  "The word "
  x= 74.51  level 1 RTL  "שלום"
  x=110.14  level 0 LTR  " means peace."
RTL paragraph, 5 runs, left to right on screen:
  x=  0.00  level 1 RTL  " كتابا."
  x= 34.62  level 2 LTR  "12"
  x= 52.93  level 1 RTL  " قرأت "
  x= 94.38  level 2 LTR  "2026"
  x=130.99  level 1 RTL  "في عام "
```

- **Levels.** `GetBidiLevels` returns one embedding level per UTF-16 code unit. Even levels are left to right, odd levels right to left. `TextDirection.Auto` takes the paragraph’s direction from its first strong character, so the English sentence is a left-to-right paragraph and the Arabic one a right-to-left paragraph. `LeftToRight` and `RightToLeft` force it.
- **Runs.** A run is a stretch of one level. In the Arabic sentence the numbers 2026 and 12 are level 2, left to right inside the right-to-left paragraph.
- **Display order.** `GetVisualOrder` applies rule L2 to a list of levels and returns the indexes in display order, left to right. It works on levels per character or per run, as here. In the Arabic paragraph the end of the sentence comes first on the left.
- **Shaping.** Each run is shaped on its own with `rightToLeft` set from its level, so Arabic letters take their joined forms. A run’s clusters come back in logical order: draw a right-to-left run’s clusters last to first.
- **Across fonts.** The runs here go through a `FontFallbackChain`, so English, Hebrew and Arabic each come from a font that has them.

For a paragraph you wrap, run the algorithm once for the paragraph, choose the line breaks, then reorder each line on its own. Rule L1, which resets trailing white space per line, is on the code point API: `TextShaping.ResolveBidiLevels` and `TextShaping.ResetTrailingWhitespace`. Explicit embeddings, overrides and isolates (U+202A to U+202E, U+2066 to U+2069) are honored up to depth 125.

## Mirrored brackets

In right-to-left text, brackets and other paired characters are drawn mirrored, so a logical “(” shows the glyph of “)”. The shaper does this by code point, unless the font has `rtlm` lookups for the script, in which case it leaves mirroring to them.

```csharp
using OneFont;

// In a right-to-left run, "(" should be drawn with the glyph of ")".
foreach (string file in new[] { "DejaVuSans.ttf", "NotoSans-Regular.ttf" })
{
    FontFace face = FontFace.Load(File.ReadAllBytes(file));
    int first = TextShaping.ShapeRun("(\u05D0)", face, rightToLeft: true)[0].Glyphs[0].GlyphId;
    int open = face.GetGlyphIndex('('), close = face.GetGlyphIndex(')');
    Console.WriteLine($"{file}: \"(\" is glyph {open}, \")\" is {close}; shaped right to left: {first}");
}
```

```text
DejaVuSans.ttf: "(" is glyph 11, ")" is 12; shaped right to left: 12
NotoSans-Regular.ttf: "(" is glyph 11, ")" is 12; shaped right to left: 11
```

In 1.3.0 that rule misses fonts whose `rtlm` lookups cover only a few glyphs. Noto Sans’s `rtlm` maps a single math symbol, so its parentheses aren’t mirrored, while DejaVu Sans, which has no `rtlm`, is mirrored by code point. With such a font, swap the mirrored characters yourself before shaping a right-to-left run.

Where onefont stops: it doesn’t split paragraphs, choose line breaks or draw. The rules it follows, and the places it differs from SixLabors.Fonts, are in [Shaping, bidi and segmentation](https://recursiveai.org/onefont/docs/shaping/).
