* @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD * @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE) * @link https://github.com/tecnickcom/tc-lib-pdf * * This file is part of tc-lib-pdf software library. */ // NOTE: local file reads (images, fonts, attachments) are restricted to an allowlist of // trusted paths that covers this package tree, so run the examples in place. To read assets // from other locations, list them in the 'allowedPaths' entry of the fileOptions constructor // parameter (see E047_remote_resources_security.php). // NOTE: run make fonts in the project root to generate the dependencies and example fonts. // autoloader when using Composer require __DIR__ . '/../vendor/autoload.php'; // define fonts directory \define('K_PATH_FONTS', \realpath(__DIR__ . '/../vendor/tecnickcom/tc-lib-pdf-font/target/fonts')); $pdf = new \Com\Tecnick\Pdf\Tcpdf(); $pdf->setCreator('tc-lib-pdf'); $pdf->setAuthor('Nicola Asuni'); $pdf->setSubject('tc-lib-pdf example: 085'); $pdf->setTitle('Bidi Line Break Reading Order'); $pdf->setKeywords('TCPDF tc-lib-pdf example bidi rtl ltr hebrew arabic line break reading order'); $pdf->setPDFFilename('085_bidi_line_break.pdf'); // Every word of a run carries its number, so the reading order can be checked // without reading Hebrew or Arabic: the numbers of an RTL run grow from right to // left on each line, those of an LTR run from left to right, and the numbers // continue from one line to the next. /** * Returns the numbered Hebrew words from $first to $last. */ $hebrew = static function (int $first, int $last): string { $words = []; for ($num = $first; $num <= $last; ++$num) { $words[] = "\u{05DE}\u{05D9}\u{05DC}\u{05D4}" . $num; } return \implode(' ', $words); }; /** * Returns the numbered Arabic words from $first to $last. */ $arabic = static function (int $first, int $last): string { $words = []; for ($num = $first; $num <= $last; ++$num) { $words[] = "\u{0643}\u{0644}\u{0645}\u{0629}" . $num; } return \implode(' ', $words); }; /** * Returns the numbered Latin words from $first to $last. */ $latin = static function (int $first, int $last): string { $words = []; for ($num = $first; $num <= $last; ++$num) { $words[] = 'w' . $num; } return \implode(' ', $words); }; // The metrics used to break the lines come from the current font of the stack, // so each block selects its own font right before it is written. $selectFont = function (string $family = 'dejavusans', string $style = '', float $size = 11) use ($pdf): void { $font = $pdf->font->insert($pdf->pon, $family, $style, $size); $pdf->page->addContent($font['out']); }; // The cell border shows the width the lines are broken to. $cellStyle = [ 'all' => [ 'lineWidth' => 0.3, 'lineCap' => 'butt', 'lineJoin' => 'miter', 'dashArray' => [], 'dashPhase' => 0, 'lineColor' => '#4a5b70', 'fillColor' => '#ffffff', ], ]; $labelx = 8.0; $labelw = 32.0; $posx = 44.0; $width = 80.0; $captionx = $posx + $width + 4.0; $captionw = 210.0 - $captionx - 8.0; /** * Writes a small text block (label or caption) at absolute page coordinates. */ $note = function (string $txt, float $posx, float $posy, float $width, string $halign) use ($pdf, $selectFont): void { $selectFont('helvetica', '', 7); $pdf->page->addContent($pdf->getTextCell( txt: $txt, posx: $posx, posy: $posy, width: $width, valign: \Com\Tecnick\Pdf\TextVAlign::Top, halign: $halign, drawcell: false, )); }; /** * Draws one labelled case with its expected result and returns the ordinate of * the next one. * * @param array $args Named arguments for addTextCell. */ $drawCase = function (string $label, string $expected, float $posy, array $args) use ( $pdf, $selectFont, $note, $cellStyle, $labelx, $labelw, $posx, $width, $captionx, $captionw, ): float { $note($label, $labelx, $posy, $labelw, 'R'); $note($expected, $captionx, $posy, $captionw, 'L'); $selectFont(); $pdf->addTextCell(...\array_merge([ 'posx' => $posx, 'posy' => $posy, 'width' => $width, 'valign' => \Com\Tecnick\Pdf\TextVAlign::Top, 'halign' => \Com\Tecnick\Pdf\TextHAlign::Left, 'styles' => $cellStyle, ], $args)); $bbox = $pdf->getLastCellBBox(); return \max($posy + 18.0, $bbox['y'] + $bbox['h'] + 6.0); }; /** * Writes a page title and returns the ordinate of the first case. */ $drawTitle = function (string $title, ?array $page = null) use ($pdf, $selectFont, $labelx): float { $pdf->addPage($page ?? []); $selectFont('helvetica', 'B', 12); $pdf->page->addContent($pdf->getTextCell( txt: $title, posx: $labelx, posy: 12, width: 190, valign: \Com\Tecnick\Pdf\TextVAlign::Top, halign: \Com\Tecnick\Pdf\TextHAlign::Left, drawcell: false, )); return 24.0; }; // ---------- // Text cells: runs of the opposite direction wrapping over several lines. // ---------- $posy = $drawTitle('Mixed direction text wrapping over several lines'); $posy = $drawCase( 'RTL run in LTR text, 2 lines', 'Line 1: "Start:", then Hebrew words 1, 2, 3, ... read from right to left. ' . 'Line 2: the next Hebrew words, again growing from right to left, then ":end".', $posy, ['txt' => 'Start: ' . $hebrew(1, 10) . ' :end'], ); $posy = $drawCase( 'RTL run in LTR text, 4 lines', 'Each line holds the next Hebrew numbers, growing from right to left; the first line holds word 1.', $posy, ['txt' => 'Start: ' . $hebrew(1, 24) . ' :end'], ); $posy = $drawCase( 'LTR run in RTL text', 'RTL paragraph, right aligned. Line 1: Hebrew word 1 at the right edge, then w1, w2, ... ' . 'read from left to right. The w numbers continue on the next lines.', $posy, [ 'txt' => $hebrew(1, 1) . ' ' . $latin(1, 30) . ' ' . $hebrew(2, 2), 'forcedir' => \Com\Tecnick\Unicode\TextDirection::Rtl, 'halign' => \Com\Tecnick\Pdf\TextHAlign::Right, ], ); $posy = $drawCase( 'Arabic run in LTR text', 'Joined Arabic words 1, 2, 3, ... growing from right to left on each line, ' . 'continuing on the next line. The lam-alef ligature is shaped.', $posy, ['txt' => 'Start: ' . $arabic(1, 12) . " \u{0644}\u{0627} :end"], ); // Every second word of the RTL run is in brackets. A bracket pair that directly // follows the LTR text would resolve to LTR and split the run (UAX #9 rule N0). $bracketed = []; foreach (\explode(' ', $hebrew(1, 14)) as $idx => $word) { $bracketed[] = ($idx % 2) === 1 ? '(' . $word . ')' : $word; } $posy = $drawCase( 'Brackets in an RTL run', 'Every second Hebrew word is in brackets. The numbers grow from right to left on each ' . 'line and continue on the next one; every bracket pair encloses its word.', $posy, ['txt' => 'Start: ' . \implode(' ', $bracketed) . ' :end'], ); $drawCase( 'Two paragraphs', 'Paragraph 1 (LTR): w1, w2, ... Paragraph 2 (RTL, starts with Hebrew): ' . 'Hebrew word 1 at the right end of its first line, then 2, 3, ...', $posy, ['txt' => $latin(1, 16) . "\n" . $hebrew(1, 14)], ); // ---------- // Break opportunities and truncation. // ---------- $posy = $drawTitle('Break opportunities and truncation in mixed direction text'); $pdf->setTexHyphenPatterns(['hyphen' => 'hy3phen', 'phena' => 'phen1a', 'ation' => 'a1tion']); $pdf->enableZeroWidthBreakPoints(true); $posy = $drawCase( 'Hyphens and zero width breaks', 'Comma separated Hebrew words break after a comma, keeping the right to left ' . 'numbering across lines; the comma ending a line is drawn at its left end. ' . '"hyphenation" breaks with a visible hyphen at the end of its line.', $posy, [ 'txt' => 'Start: ' . \str_replace(' ', ',', $hebrew(1, 18)) . ' hyphenation hyphenation hyphenation hyphenation hyphenation', 'width' => 50.0, ], ); $pdf->setTexHyphenPatterns([]); $pdf->enableZeroWidthBreakPoints(false); $posy = $drawCase( 'Truncated RTL text', 'Only the first lines fit: Hebrew words 1, 2, 3, ... from the right, ' . 'and the ellipsis at the left end of the last line. The text is cut by character, ' . 'so the last kept word can be shortened (word 12 cut to "1").', $posy, [ 'txt' => $hebrew(1, 40), 'height' => 12.0, 'forcedir' => \Com\Tecnick\Unicode\TextDirection::Rtl, 'halign' => \Com\Tecnick\Pdf\TextHAlign::Right, 'fit' => \Com\Tecnick\Pdf\TextFitMode::Truncate, ], ); $drawCase( 'Truncated LTR text with an RTL run', 'Only the first lines fit: "Start:", then Hebrew words 1, 2, 3, ... from the right. ' . 'The ellipsis ends the LTR paragraph, so it follows the RTL run at the right end of the ' . 'last line.', $posy, [ 'txt' => 'Start: ' . $hebrew(1, 40), 'height' => 12.0, 'fit' => \Com\Tecnick\Pdf\TextFitMode::Truncate, ], ); // ---------- // HTML: single text fragments, justification and a page split. // ---------- $posy = $drawTitle('HTML paragraphs with mixed direction runs'); /** * Draws one labelled HTML block with its expected result and returns the * ordinate of the next one. */ $drawHtmlCase = function (string $label, string $expected, float $posy, string $html) use ( $pdf, $selectFont, $note, $cellStyle, $labelx, $labelw, $posx, $width, $captionx, $captionw, ): float { $note($label, $labelx, $posy, $labelw, 'R'); $note($expected, $captionx, $posy, $captionw, 'L'); $selectFont(); $pdf->addHTMLCell($html, $posx, $posy, $width, 0, null, $cellStyle); $bbox = $pdf->getLastBBox(); return \max($posy + 18.0, $bbox['y'] + $bbox['h'] + 10.0); }; $posy = $drawHtmlCase( 'LTR paragraph', 'Line 1: "Start:", then Hebrew words 1, 2, 3, ... from the right; the next lines continue the numbering.', $posy, '

Start: ' . $hebrew(1, 16) . ' :end

', ); $posy = $drawHtmlCase( 'Justified, with a following fragment', 'Same order as above, justified line by line; the last line ends with ":end" and a bold "x".', $posy, '

Start: ' . $hebrew(1, 16) . ' :end x

', ); $posy = $drawHtmlCase( 'RTL block with an LTR run', 'Right aligned. Hebrew word 1 at the right edge of line 1, then w1, w2, ... from left ' . 'to right, continuing on the next lines.', $posy, '
' . $hebrew(1, 1) . ' ' . $latin(1, 30) . ' ' . $hebrew(2, 2) . '
', ); $note( 'Page split: the paragraph at the bottom of this page continues on the next page. Line by line the ' . 'Hebrew numbers keep growing from right to left, and the last line on the next page ends with ":end".', $labelx, 255.0, 190.0, 'L', ); $selectFont(); $pdf->addHTMLCell('

Start: ' . $hebrew(1, 40) . ' :end

', $posx, 268.0, $width, 0, null, $cellStyle); // ---------- // Page regions: a paragraph flowing from one column to the next. // ---------- $regiony = 40.0; $regionh = 40.0; $regionw = 80.0; $regiontop = $drawTitle('Mixed direction paragraph flowing across two page regions', [ 'region' => [ ['RX' => 15.0, 'RY' => $regiony, 'RW' => $regionw, 'RH' => $regionh], ['RX' => 115.0, 'RY' => $regiony, 'RW' => $regionw, 'RH' => $regionh], ], ]); $note( 'RTL paragraph: the left column is filled first, then the text continues in the right column. ' . 'The Hebrew numbers and the w numbers continue across the column boundary: none is repeated ' . 'or missing.', $labelx, $regiontop, 190.0, 'L', ); $selectFont(); $pdf->addTextCell( // NOTE: addTextCell() coordinates are relative to the current page region txt: $hebrew(1, 24) . ' ' . $latin(1, 40) . ' ' . $hebrew(25, 54), posx: 0, posy: 0, width: $regionw, valign: \Com\Tecnick\Pdf\TextVAlign::Top, halign: \Com\Tecnick\Pdf\TextHAlign::Right, drawcell: false, forcedir: \Com\Tecnick\Unicode\TextDirection::Rtl, ); // ---------- $rawpdf = $pdf->getOutPDFString(); $pdf->renderPDF(rawpdf: $rawpdf);