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 @@
+",
+ '
',
+ '',
+ '| a1 | ',
+ 'b1 | ',
+ 'c1 | ',
+ '
',
+ '',
+ '| a2 | ',
+ 'b2 | ',
+ 'c2 | ',
+ '
',
+ '
',
+ ];
+ $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 = [
+ "",
+ '',
+ '',
+ '| a1 | ',
+ 'b1 | ',
+ 'c1 | ',
+ '
',
+ '',
+ '| a2 | ',
+ 'b2 | ',
+ 'c2 | ',
+ '
',
+ '
',
+ ];
+ $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 = [
+ "",
+ '',
+ '',
+ '| a1 | ',
+ 'b1 | ',
+ 'c1 | ',
+ '
',
+ '',
+ '| a2 | ',
+ 'b2 | ',
+ 'c2 | ',
+ '
',
+ '
',
+ ];
+ $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();
+ }
+}