-
Notifications
You must be signed in to change notification settings - Fork 0
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.
Coverage: Hebrew is fully supported. Arabic cursive shaping is also supported: letters are joined using the correct contextual presentation forms and the mandatory lam-alef ligatures are formed automatically. See Arabic shaping for the font requirement and usage.
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.
Set a document-wide default direction once and every subsequent cell() call inherits it unless overridden:
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font;
use DragonOfMercy\PhpPdf\Text\Direction;
$doc = new Document();
$doc->setBaseDirection(Direction::RTL); // Hebrew-primary document
// Register a family alias; reference it with Font::custom().
$doc->registerFontFamily('hebrew', regular: 'fonts/OpenSansHebrew-Regular.ttf');
$page = $doc->addPage();
$page->setFont(Font::custom('hebrew'), 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.
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,
);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.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.
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.
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,
);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.
Arabic is a cursive script: the correct glyph form for each letter (isolated, initial, medial, or final) depends on its neighbors, and certain letter pairs (lam + alef) must fuse into a mandatory ligature. phppdf performs this shaping step in pure PHP before the bidi reordering pass.
- Each Arabic letter is replaced by its correct contextual presentation form (isolated / initial / medial / final) according to the Unicode Arabic Presentation Forms block.
- The four mandatory lam-alef ligatures are formed automatically: lam (U+0644) followed by plain alef (U+0627), alef madda (U+0622), alef hamza above (U+0623), or alef hamza below (U+0625). Each fuses into its isolated or final ligature form depending on whether a preceding letter joins the lam.
The shaping substitutions use the legacy Arabic Presentation Forms-A and Presentation Forms-B blocks (U+FB50-U+FDFF and U+FE70-U+FEFF). The font's cmap must contain these code points. Fonts that serve Arabic solely through OpenType GSUB lookups and omit the presentation-form block will render incorrect or missing glyphs.
Fonts known to work: GNU FreeSerif, DejaVu Sans, Tahoma.
Register an Arabic-capable font and set the direction. The shaping runs automatically:
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font;
use DragonOfMercy\PhpPdf\Text\Direction;
$doc = new Document();
$doc->setBaseDirection(Direction::RTL);
// Register a font whose cmap contains the Arabic presentation forms.
// The first argument is the family alias; reference it with Font::custom().
$doc->registerFontFamily('arabic', regular: __DIR__ . '/fonts/FreeSerif.ttf');
$page = $doc->addPage();
$page->setFont(Font::custom('arabic'), 14);
// "marhaban" - letters join and lam-alef ligatures form automatically.
$page->cell(x: 20, y: 30, w: 170, text: "\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}");To override direction on a single cell in an otherwise LTR document:
$page->cell(
x: 20, y: 30, w: 170,
text: "\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}",
direction: Direction::RTL,
);Arabic shaping also runs in table cells. Use Cell::direction() exactly as with Hebrew:
use DragonOfMercy\PhpPdf\Table\Cell;
use DragonOfMercy\PhpPdf\Text\Direction;
$rows = [
['id' => '1', 'name' => Cell::of("\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}")->direction(Direction::RTL)],
];ZWJ / ZWNJ are not specially handled. The Zero Width Joiner (U+200D) and Zero Width Non-Joiner (U+200C) are passed through to the output unchanged; phppdf does not use them to force or break a cursive join. Shaping that depends on explicit joiner control is out of scope.
RTL support is available in Page::markdown() and cell(markdown: true). Bidi reordering and Arabic shaping apply block by block: each paragraph, heading, list item, and blockquote is shaped and reordered independently.
Pass direction: to Page::markdown() or use Document::setBaseDirection() for a document-wide default:
use DragonOfMercy\PhpPdf\Document;
use DragonOfMercy\PhpPdf\Font;
use DragonOfMercy\PhpPdf\Text\Direction;
$doc = new Document();
// Register a font whose cmap contains the Arabic and Hebrew presentation forms.
$doc->registerFontFamily('freeserif', regular: __DIR__ . '/fonts/FreeSerif.ttf');
// Set the document base direction (all markdown() and cell() calls inherit this).
$doc->setBaseDirection(Direction::RTL);
$page = $doc->addPage();
$page->setFont(Font::custom('freeserif'), 13);
// Document base direction is RTL - the block is right-aligned and bidi-reordered.
$page->markdown(
"## \u{05E9}\u{05DC}\u{05D5}\u{05DD}\n\n" . // Hebrew heading "Shalom"
"- \u{05E4}\u{05E8}\u{05D9}\u{05D8}\u{05D9}\n" . // Hebrew list item "Friti"
"- \u{05E2}\u{05D5}\u{05DC}\u{05DD}\n", // Hebrew list item "Olam"
);To override direction for a single markdown() call in an otherwise LTR document, pass direction: inline:
$page->markdown(
$hebrewContent,
direction: Direction::RTL,
);The same direction: argument is available on cell(markdown: true):
$page->cell(
x: 20, y: 30, w: 170,
text: "## \u{05E9}\u{05DC}\u{05D5}\u{05DD}",
markdown: true,
direction: Direction::RTL,
);When direction: is Direction::AUTO (or the document base direction is AUTO), each Markdown block detects its own base direction independently from its first strong bidi character. A heading with a Hebrew first letter resolves to RTL; a paragraph starting with Latin resolves to LTR. This is useful for mixed-language documents where content may alternate between RTL and LTR:
$doc->setBaseDirection(Direction::AUTO);
// Each paragraph / heading / list item / blockquote auto-detects its direction.For a block whose resolved base direction is RTL:
- The block text is bidi-reordered and Arabic-shaped (same as
cell()). - The block is right-aligned by default. An explicit alignment set via
MarkdownStyletakes precedence. - Unordered list markers (bullets) and ordered list numbers are placed on the right side of the indent.
- Blockquote vertical bars are drawn on the right margin of the quoted block.
- Inline code spans and fenced / indented code blocks are always rendered LTR regardless of the surrounding block direction.
Arabic joining does not cross an inline-style boundary. Each styled run (bold, italic, code span, link text) is shaped independently. An Arabic letter at the end of a bold span and a letter at the start of the following plain-text span will not join even if they would in continuous text.
Tables, thematic rules, and images are not mirrored. Markdown table columns retain their left-to-right order; thematic breaks (---) and inline images are not flipped.
Tagged-PDF reading order for reordered RTL Markdown is not specially handled. When enableTagging() or enablePdfUA() is active, the structure tree reflects the logical (pre-bidi) source order; the visual reordering is not reflected in the tag tree.
Bidi explicit embedding and isolate controls are out of scope. Characters such as LRE, RLI, and FSI are not processed in Markdown blocks.
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. Only the implicit bidi algorithm is implemented.
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.
MIT licensed. Source on GitHub - if phppdf helps you, you can buy me a coffee.