From 98f464b41f1867cf9fdd0bf574b3598d29915f13 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 14 Jul 2025 00:22:54 -0700 Subject: [PATCH] Writer Html/Pdf Support RTL Alignment of Table Fix #1104, which went stale over 6 years ago, and is now reopened. This PR addresses the alignment of the table, not alignment of text. Support is added for the following: - Html. Full support. - Mpdf. Full support, except that, when mixed LTR and RTL worksheets are output, all tables will be on the left of the page (but will be properly aligned). - Tcpdf. Full support when all worksheets being output are RTL; no support when mixed LTR and RTL worksheets are output. - Dompdf. No support. --- .../{21f_Drawing_mpdf.php => 21f_Drawing.php} | 13 +-- samples/Pdf/21g_Direction.php | 28 ++++++ samples/Pdf/21h_DirectionMultiple.php | 36 ++++++++ src/PhpSpreadsheet/Reader/Html.php | 3 + src/PhpSpreadsheet/Writer/Html.php | 81 ++++++++++++++-- src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php | 24 ++++- .../Reader/Html/DirectionTest.php | 92 +++++++++++++++++++ .../Writer/Html/DirectionTest.php | 86 +++++++++++++++++ 8 files changed, 339 insertions(+), 24 deletions(-) rename samples/Pdf/{21f_Drawing_mpdf.php => 21f_Drawing.php} (78%) create mode 100644 samples/Pdf/21g_Direction.php create mode 100644 samples/Pdf/21h_DirectionMultiple.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/DirectionTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/DirectionTest.php diff --git a/samples/Pdf/21f_Drawing_mpdf.php b/samples/Pdf/21f_Drawing.php similarity index 78% rename from samples/Pdf/21f_Drawing_mpdf.php rename to samples/Pdf/21f_Drawing.php index 357b4690a..489914c3a 100644 --- a/samples/Pdf/21f_Drawing_mpdf.php +++ b/samples/Pdf/21f_Drawing.php @@ -2,12 +2,10 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; -use PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf; require __DIR__ . '/../Header.php'; -/** @var PhpOffice\PhpSpreadsheet\Helper\Sample $helper */ -require_once __DIR__ . '/Mpdf2.php'; +/** @var PhpOffice\PhpSpreadsheet\Helper\Sample $helper */ $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); @@ -41,11 +39,6 @@ $helper->log('Merge drawing cells for Pdf'); $spreadsheet->mergeDrawingCellsForPdf(); $helper->log('Write to Mpdf'); -$writer = new Mpdf($spreadsheet); -$filename = $helper->getFileName(__FILE__, 'pdf'); -$writer->save($filename); -$helper->log("Saved $filename"); -if (PHP_SAPI !== 'cli') { - echo 'Download ' . basename($filename) . '
'; -} +$helper->write($spreadsheet, __FILE__, ['Mpdf']); + $spreadsheet->disconnectWorksheets(); diff --git a/samples/Pdf/21g_Direction.php b/samples/Pdf/21g_Direction.php new file mode 100644 index 000000000..180fad1a3 --- /dev/null +++ b/samples/Pdf/21g_Direction.php @@ -0,0 +1,28 @@ +getActiveSheet(); + +$sheet1 = $spreadsheet->getActiveSheet(); +$sheet2 = $spreadsheet->createSheet(); +$sheet3 = $spreadsheet->createSheet(); +$cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], +]; +$sheet1->fromArray($cells); +$sheet1->setRightToLeft(true); +$sheet2->fromArray($cells); +$sheet3->fromArray($cells); +$sheet3->setRightToLeft(true); + +$helper->log('Write to html, mpdf, tcpdf'); +// Save +$helper->write($spreadsheet, __FILE__, ['Html', 'Mpdf', 'Tcpdf']); + +$spreadsheet->disconnectWorksheets(); diff --git a/samples/Pdf/21h_DirectionMultiple.php b/samples/Pdf/21h_DirectionMultiple.php new file mode 100644 index 000000000..894dfa3f6 --- /dev/null +++ b/samples/Pdf/21h_DirectionMultiple.php @@ -0,0 +1,36 @@ +getActiveSheet(); + +$sheet1 = $spreadsheet->getActiveSheet(); +$sheet2 = $spreadsheet->createSheet(); +$sheet3 = $spreadsheet->createSheet(); +$cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], +]; +$sheet1->fromArray($cells); +$sheet1->setRightToLeft(true); +$sheet2->fromArray($cells); +$sheet3->fromArray($cells); +$sheet3->setRightToLeft(true); + +$helper->log('Write to html, mpdf'); +// Save +$helper->write( + $spreadsheet, + __FILE__, + ['Html', 'Mpdf'], + writerCallback: function (HtmlWriter $writer): void { + $writer->writeAllSheets(); + } +); + +$spreadsheet->disconnectWorksheets(); diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index af831cf46..e63c05327 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -532,6 +532,9 @@ class Html extends BaseReader $sheet->setShowGridlines(in_array('gridlines', $classes, true)); $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true)); } + if ('rtl' === ($attributeArray['dir'] ?? '')) { + $sheet->setRightToLeft(true); + } $this->currentColumn = 'A'; $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray); $column = $this->setTableStartColumn($column); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 8d7e45b0b..c79b5737a 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -158,6 +158,10 @@ class Html extends BaseWriter private string $getFalse = 'FALSE'; + protected bool $rtlSheets = false; + + protected bool $ltrSheets = false; + /** * Create a new HTML. */ @@ -186,11 +190,31 @@ class Html extends BaseWriter $this->maybeCloseFileHandle(); } + protected function checkRtlAndLtr(): void + { + $this->rtlSheets = false; + $this->ltrSheets = false; + if ($this->sheetIndex === null) { + foreach ($this->spreadsheet->getAllSheets() as $sheet) { + if ($sheet->getRightToLeft()) { + $this->rtlSheets = true; + } else { + $this->ltrSheets = true; + } + } + } else { + if ($this->spreadsheet->getSheet($this->sheetIndex)->getRightToLeft()) { + $this->rtlSheets = true; + } + } + } + /** * Save Spreadsheet as html to variable. */ public function generateHtmlAll(): string { + $this->checkRtlAndLtr(); $sheets = $this->generateSheetPrep(); foreach ($sheets as $sheet) { $sheet->calculateArrays($this->preCalculateFormulas); @@ -369,7 +393,8 @@ class Html extends BaseWriter // Construct HTML $properties = $this->spreadsheet->getProperties(); $html = '' . PHP_EOL; - $html .= '' . PHP_EOL; + $rtl = ($this->rtlSheets && !$this->ltrSheets) ? " dir='rtl'" : ''; + $html .= '' . PHP_EOL; $html .= ' ' . PHP_EOL; $html .= ' ' . PHP_EOL; $html .= ' ' . PHP_EOL; @@ -1013,6 +1038,9 @@ class Html extends BaseWriter // .s {} $css['.s']['text-align'] = 'left'; // STRING + $css['.floatright']['float'] = 'right'; + $css['.floatleft']['float'] = 'left'; + // Calculate cell style hashes foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style); @@ -1221,21 +1249,52 @@ class Html extends BaseWriter return $html; } + private function getDir(Worksheet $worksheet): string + { + if ($worksheet->getRightToLeft()) { + return " dir='rtl'"; + } + if ($this->rtlSheets) { + return " dir='ltr'"; + } + + return ''; + } + + private function getFloat(Worksheet $worksheet): string + { + $float = ''; + if ($worksheet->getRightToLeft()) { + if ($this->ltrSheets) { + $float = ' floatright'; + } + } else { + if ($this->rtlSheets) { + $float = ' floatleft'; + } + } + + return $float; + } + private function generateTableTagInline(Worksheet $worksheet, string $id): string { $style = isset($this->cssStyles['table']) ? $this->assembleCSS($this->cssStyles['table']) : ''; - + $rtl = $this->getDir($worksheet); + $float = $this->getFloat($worksheet); $prntgrid = $worksheet->getPrintGridlines(); $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines(); if ($viewgrid && $prntgrid) { - $html = " " . PHP_EOL; + $html = "
" . PHP_EOL; } elseif ($viewgrid) { - $html = "
" . PHP_EOL; + $html = "
" . PHP_EOL; } elseif ($prntgrid) { - $html = "
" . PHP_EOL; + $html = "
" . PHP_EOL; + } elseif ($float === '') { + $html = "
" . PHP_EOL; } else { - $html = "
" . PHP_EOL; + $html = "
" . PHP_EOL; } return $html; @@ -1244,9 +1303,11 @@ class Html extends BaseWriter private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void { if (!$this->useInlineCss) { + $rtl = $this->getDir($worksheet); + $float = $this->getFloat($worksheet); $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : ''; - $html .= "
" . PHP_EOL; + $html .= "
" . PHP_EOL; } else { $html .= $this->generateTableTagInline($worksheet, $id); } @@ -1265,10 +1326,12 @@ class Html extends BaseWriter // Construct HTML $html = ''; $id = $showid ? "id='sheet$sheetIndex'" : ''; + $clear = ($this->rtlSheets && $this->ltrSheets) ? '; clear:both' : ''; + if ($showid) { - $html .= "
" . PHP_EOL; + $html .= "
" . PHP_EOL; } else { - $html .= "
" . PHP_EOL; + $html .= "
" . PHP_EOL; } $this->generateTableTag($worksheet, $id, $html, $sheetIndex); diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index 540882708..fe5cfe25c 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -67,14 +67,28 @@ class Tcpdf extends Pdf // Set the appropriate font $pdf->SetFont($this->getFont()); + $this->checkRtlAndLtr(); + if ($this->rtlSheets && !$this->ltrSheets) { + $pdf->setRTL(true); + } $pdf->writeHTML($this->generateHTMLAll()); // Document info - $pdf->SetTitle($this->spreadsheet->getProperties()->getTitle()); - $pdf->SetAuthor($this->spreadsheet->getProperties()->getCreator()); - $pdf->SetSubject($this->spreadsheet->getProperties()->getSubject()); - $pdf->SetKeywords($this->spreadsheet->getProperties()->getKeywords()); - $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); + $pdf->SetTitle( + $this->spreadsheet->getProperties()->getTitle() + ); + $pdf->SetAuthor( + $this->spreadsheet->getProperties()->getCreator() + ); + $pdf->SetSubject( + $this->spreadsheet->getProperties()->getSubject() + ); + $pdf->SetKeywords( + $this->spreadsheet->getProperties()->getKeywords() + ); + $pdf->SetCreator( + $this->spreadsheet->getProperties()->getCreator() + ); // Write to file fwrite($fileHandle, $pdf->output('', 'S')); diff --git a/tests/PhpSpreadsheetTests/Reader/Html/DirectionTest.php b/tests/PhpSpreadsheetTests/Reader/Html/DirectionTest.php new file mode 100644 index 000000000..5021a54f3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/DirectionTest.php @@ -0,0 +1,92 @@ +", + '
', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
a1b1c1
a2b2c2
', + ]; + $html = implode("\n", $inlines); + $reader = new HtmlReader(); + $spreadsheet = $reader->loadFromString($html); + $sheet = $spreadsheet->getActiveSheet(); + self::assertTrue($sheet->getRightToLeft()); + self::assertSame('a1', $sheet->getCell('A1')->getValue()); + self::assertSame('c2', $sheet->getCell('C2')->getValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function testLtr(): void + { + $inlines = [ + "", + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
a1b1c1
a2b2c2
', + ]; + $html = implode("\n", $inlines); + $reader = new HtmlReader(); + $spreadsheet = $reader->loadFromString($html); + $sheet = $spreadsheet->getActiveSheet(); + self::assertFalse($sheet->getRightToLeft()); + self::assertSame('a1', $sheet->getCell('A1')->getValue()); + self::assertSame('c2', $sheet->getCell('C2')->getValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function testDefault(): void + { + $inlines = [ + "", + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
a1b1c1
a2b2c2
', + ]; + $html = implode("\n", $inlines); + $reader = new HtmlReader(); + $spreadsheet = $reader->loadFromString($html); + $sheet = $spreadsheet->getActiveSheet(); + self::assertFalse($sheet->getRightToLeft()); + self::assertSame('a1', $sheet->getCell('A1')->getValue()); + self::assertSame('c2', $sheet->getCell('C2')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/DirectionTest.php b/tests/PhpSpreadsheetTests/Writer/Html/DirectionTest.php new file mode 100644 index 000000000..9a406bc32 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/DirectionTest.php @@ -0,0 +1,86 @@ +getActiveSheet(); + $sheet1->setRightToLeft(true); + $sheet2 = $spreadsheet->createSheet(); + $sheet3 = $spreadsheet->createSheet(); + $sheet3->setRightToLeft(true); + $cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ]; + $sheet1->fromArray($cells); + $sheet2->fromArray($cells); + $sheet3->fromArray($cells); + $writer = new Html($spreadsheet); + $writer->writeAllSheets(); + $html = $writer->generateHTMLall(); + $rtlCount = substr_count($html, "dir='rtl'"); + self::assertSame(2, $rtlCount); + $ltrCount = substr_count($html, "dir='ltr'"); + self::assertSame(1, $ltrCount); + $spreadsheet->disconnectWorksheets(); + } + + public function testNoRtl(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + $sheet3 = $spreadsheet->createSheet(); + $cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ]; + $sheet1->fromArray($cells); + $sheet2->fromArray($cells); + $sheet3->fromArray($cells); + $writer = new Html($spreadsheet); + $writer->writeAllSheets(); + $html = $writer->generateHTMLall(); + $rtlCount = substr_count($html, "dir='rtl'"); + self::assertSame(0, $rtlCount); + $ltrCount = substr_count($html, "dir='ltr'"); + self::assertSame(0, $ltrCount); + $spreadsheet->disconnectWorksheets(); + } + + public function testOnlyRtl(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + $sheet3 = $spreadsheet->createSheet(); + $cells = [ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ]; + $sheet1->fromArray($cells); + $sheet1->setRightToLeft(true); + $sheet2->fromArray($cells); + $sheet2->setRightToLeft(true); + $sheet3->fromArray($cells); + $sheet3->setRightToLeft(true); + $writer = new Html($spreadsheet); + $writer->writeAllSheets(); + $html = $writer->generateHTMLall(); + $rtlCount = substr_count($html, "dir='rtl'"); + self::assertSame(4, $rtlCount, '3 sheets plus html tag'); + $ltrCount = substr_count($html, "dir='ltr'"); + self::assertSame(0, $ltrCount); + $spreadsheet->disconnectWorksheets(); + } +}