Skip to content

Right to Left Text

Dragon edited this page Jun 8, 2026 · 3 revisions

Right-to-Left Text

phppdf implements the Unicode Bidirectional Algorithm (UAX #9) so Hebrew - and any other RTL script - renders in the correct visual order on cell() and table text cells. The feature is entirely opt-in: output is byte-identical when no direction is set and no RTL characters are present.

Phase A coverage: Hebrew is fully supported. Arabic reordering is correct but Arabic shaping (contextual letter joining and ligatures) is not yet implemented - Arabic renders as unjoined isolated forms until Phase B. See Limitations below.

The Direction enum

use DragonOfMercy\PhpPdf\Text\Direction;

Direction::LTR  // Left-to-right (the library default)
Direction::RTL  // Right-to-left
Direction::AUTO // Derive from the first strong bidi character in the text (UAX #9 rules P2/P3)

Direction::AUTO scans the text for the first character with a strong bidi type (a Latin letter implies LTR, a Hebrew or Arabic letter implies RTL). If no strong character is found, it falls back to LTR.

Document base direction

Set a document-wide default direction once and every subsequent cell() call inherits it unless overridden:

use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Text\Direction;

$doc = new Document();
$doc->setBaseDirection(Direction::RTL);  // Hebrew-primary document

$page = $doc->addPage();
$page->setFont($doc->registerFontFamily('fonts/OpenSansHebrew-Regular.ttf'), 12);

// This cell inherits RTL from the document, and right-aligns by default.
$page->cell(x: 20, y: 30, w: 170, text: "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"); // Shalom

$doc->baseDirection() returns the currently set direction.

The default is Direction::LTR; you do not need to call setBaseDirection() for LTR-only documents.

Per-cell direction override

Pass direction: to any cell() call to override the document base direction for that cell only:

use DragonOfMercy\PhpPdf\Text\Direction;

// Mixed document (LTR base): one cell rendered RTL.
$page->cell(
    x: 20, y: 30, w: 170,
    text: "\u{05E9}\u{05DC}\u{05D5}\u{05DD}",  // Shalom
    direction: Direction::RTL,
);

// Opposite: RTL document, one cell forced LTR.
$page->cell(
    x: 20, y: 45, w: 170,
    text: 'Price: 99.90 EUR',
    direction: Direction::LTR,
);

Automatic direction detection with Direction::AUTO

Use Direction::AUTO when the text may be RTL or LTR and you want the library to decide:

// Hebrew title - AUTO resolves to RTL because the first strong character is Hebrew.
$page->cell(
    x: 20, y: 30, w: 170,
    text: "\u{05E9}\u{05DC}\u{05D5}\u{05DD} World",  // "Shalom" + Latin
    direction: Direction::AUTO,
);

// Latin phrase - AUTO resolves to LTR because the first strong character is Latin.
$page->cell(
    x: 20, y: 45, w: 170,
    text: 'Hello \u{05E2}\u{05D5}\u{05DC}\u{05DD}',  // "Hello" + "Olam"
    direction: Direction::AUTO,
);

Direction::AUTO can also be set as the document base direction:

$doc->setBaseDirection(Direction::AUTO);
// Each cell's direction is then derived from its own text content.

Default alignment for RTL cells

When a cell's resolved base direction is RTL and no explicit align: is given, the alignment defaults to TextAlign::RIGHT. An explicit align: always takes precedence:

// RTL cell - right-aligned by default.
$page->cell(x: 20, y: 30, w: 170, text: "\u{05E9}\u{05DC}\u{05D5}\u{05DD}", direction: Direction::RTL);

// RTL cell - explicitly centered (overrides the RTL default).
$page->cell(x: 20, y: 45, w: 170, text: "\u{05E9}\u{05DC}\u{05D5}\u{05DD}", direction: Direction::RTL, align: TextAlign::CENTER);

LTR cells continue to default to TextAlign::LEFT.

Table cells

Supply a direction on individual Cell value objects:

use DragonOfMercy\PhpPdf\Table\Cell;
use DragonOfMercy\PhpPdf\Table\Column;
use DragonOfMercy\PhpPdf\Text\Direction;
use DragonOfMercy\PhpPdf\TextAlign;

$columns = [
    Column::of('id',   'ID')->width(20)->align(TextAlign::CENTER),
    Column::of('name', 'Name')->fill(),
    Column::of('hname', 'Hebrew Name')->width(60)->align(TextAlign::RIGHT),
];

$rows = [
    [
        'id'    => '1',
        'name'  => 'Alice',
        'hname' => Cell::of("\u{05D0}\u{05DC}\u{05D9}\u{05E1}")->direction(Direction::RTL),
    ],
    [
        'id'    => '2',
        'name'  => 'Bob',
        'hname' => Cell::of("\u{05D1}\u{05D5}\u{05D1}")->direction(Direction::RTL),
    ],
];

$page->table($columns, $rows, x: 15, y: 30, width: 180);

Cell::direction() only affects the bidi reordering of that individual data cell; it does not change the column alignment. Set column alignment explicitly with Column::align(TextAlign::RIGHT) (as shown above) when you want right-aligned RTL content.

Mixed-direction paragraphs

The bidi algorithm handles mixed LTR/RTL runs within a single line automatically. The base direction controls how RTL and LTR runs are ordered relative to each other at the paragraph level:

// Base direction RTL: the Hebrew word appears to the right of the Latin word on the same line.
$page->cell(
    x: 20, y: 30, w: 170,
    text: "Hello \u{05E9}\u{05DC}\u{05D5}\u{05DD}",
    direction: Direction::RTL,
);

Design note: no page-coordinate flip

phppdf does not flip the page coordinate system for RTL documents (unlike some other PDF libraries). RTL is handled purely at the text-line level: the bidi algorithm reorders characters into visual order and the default alignment shifts to the right. Everything else - page origin, cursor position, image placement, table column order, SVG rendering - is unaffected. This means you can mix RTL text with LTR images, barcodes, and graphics on the same page without any coordinate adjustments.

Limitations (Phase A)

Arabic shaping not yet implemented. Arabic characters are reordered to the correct visual sequence by the bidi algorithm, but the contextual shaping step (choosing the correct glyph form - isolated, initial, medial, or final - and forming ligatures such as lam-alef) is not yet done. Arabic text will render as a sequence of isolated letter forms. Hebrew requires no shaping and is fully correct.

markdown() RTL is deferred. The direction: argument is not yet accepted by Page::markdown() or cell(markdown: true). Markdown rendering follows the document base direction indirectly through the cell() re-dispatch, but RTL-aware layout for Markdown is not yet implemented.

Bidi explicit embedding and isolate controls are out of scope. The Unicode characters LRE, RLE, LRO, RLO, PDF, LRI, RLI, FSI, and PDI are not processed. Phase A implements the implicit bidi algorithm only.

Table default alignment is not auto-flipped. When a table data cell has direction(Direction::RTL), its text is reordered correctly but the column alignment is not automatically changed to RIGHT. Set Column::align(TextAlign::RIGHT) explicitly if you want right-aligned RTL columns.

Per-column-header direction is not configurable. Table header cells follow the document base direction; there is no per-column direction override for headers.

Clone this wiki locally