From dbff58a53241e58b4b6a9e2a981cf6d5f94305b4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:15:05 -0700 Subject: [PATCH 1/4] Better Support For Style Alignment Read Order Fix #850 (marked stale many years ago, but now reopened). User had a typo in their script which would have caused problems no matter what. However, it exposed another problem. Style Alignment Read Order was supported only by the Xlsx Reader and Writer, but it could have been supported pretty easily for most other formats. This PR adds support for the following: - Xls (read and write) - Html (write, and read using inline styles). Html reader does not yet process most classes. - Pdf (write). PhpSpreadsheet does not have a Pdf reader. - Ods (write). PhpSpreadsheet does not yet support reading most Ods styles. - Xml (read). PhpSpreadsheet does not have an Xml writer. - Gnumeric (no change). It appears that the Gnumeric product does not support this attribute. - Csv (no change). Csv does not support any styles. - Slk (no change). Slk does not support non-Latin characters, so this attribute doesn't make sense for it. --- src/PhpSpreadsheet/Reader/Html.php | 12 +++ src/PhpSpreadsheet/Reader/Xls.php | 2 + .../Reader/Xml/Style/Alignment.php | 8 ++ src/PhpSpreadsheet/Writer/Html.php | 6 ++ src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 8 +- src/PhpSpreadsheet/Writer/Xls/Xf.php | 3 +- .../Reader/Xml/ReadOrderTest.php | 39 ++++++++ .../Writer/Html/ReadOrderTest.php | 93 +++++++++++++++++++ .../Writer/Ods/ReadOrderTest.php | 52 +++++++++++ .../Writer/Xls/ReadOrderTest.php | 51 ++++++++++ tests/data/Reader/Xml/issue.850.xml | 85 +++++++++++++++++ 11 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xml/ReadOrderTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/ReadOrderTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Ods/ReadOrderTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/ReadOrderTest.php create mode 100644 tests/data/Reader/Xml/issue.850.xml diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index b57d6c1309..cfc3ccceb9 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Fill; @@ -1014,6 +1015,17 @@ private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, a break; + case 'direction': + if ($styleValue === 'rtl') { + $cellStyle->getAlignment() + ->setReadOrder(Alignment::READORDER_RTL); + } elseif ($styleValue === 'ltr') { + $cellStyle->getAlignment() + ->setReadOrder(Alignment::READORDER_LTR); + } + + break; + case 'font-weight': if ($styleValue === 'bold' || $styleValue >= 500) { $cellStyle->getFont()->setBold(true); diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 97c481daaa..3d4cb5e5cb 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -1254,6 +1254,8 @@ protected function readXf(): void break; } + $readOrder = (0xC0 & ord($recordData[8])) >> 6; + $objStyle->getAlignment()->setReadOrder($readOrder); // offset: 9; size: 1; Flags used for attribute groups diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php b/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php index 657decfffe..b4afcdec49 100644 --- a/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php +++ b/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php @@ -54,6 +54,14 @@ public function parseStyle(SimpleXMLElement $styleAttributes): array case 'Indent': $style['alignment']['indent'] = $styleAttributeValue; + break; + case 'ReadingOrder': + if ($styleAttributeValue === 'RightToLeft') { + $style['alignment']['readOrder'] = AlignmentStyles::READORDER_RTL; + } elseif ($styleAttributeValue === 'LeftToRight') { + $style['alignment']['readOrder'] = AlignmentStyles::READORDER_LTR; + } + break; } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 27849bf374..7c3b962d3a 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1126,6 +1126,12 @@ private function createCSSStyleAlignment(Alignment $alignment): array $css['transform'] = "rotate({$rotation}deg)"; } } + $direction = $alignment->getReadOrder(); + if ($direction === Alignment::READORDER_LTR) { + $css['direction'] = 'ltr'; + } elseif ($direction === Alignment::READORDER_RTL) { + $css['direction'] = 'rtl'; + } return $css; } diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index fdb5804984..5c3cf5277d 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -151,6 +151,7 @@ private function writeCellProperties(CellStyle $style): void $vAlign = $style->getAlignment()->getVertical(); $wrap = $style->getAlignment()->getWrapText(); $indent = $style->getAlignment()->getIndent(); + $readOrder = $style->getAlignment()->getReadOrder(); $this->writer->startElement('style:table-cell-properties'); if (!empty($vAlign) || $wrap) { @@ -172,7 +173,7 @@ private function writeCellProperties(CellStyle $style): void $this->writer->endElement(); - if ($hAlign !== '' || !empty($indent)) { + if ($hAlign !== '' || !empty($indent) || $readOrder === Alignment::READORDER_RTL || $readOrder === Alignment::READORDER_LTR) { $this->writer ->startElement('style:paragraph-properties'); if ($hAlign !== '') { @@ -182,6 +183,11 @@ private function writeCellProperties(CellStyle $style): void $indentString = sprintf('%.4f', $indent * self::INDENT_TO_INCHES) . 'in'; $this->writer->writeAttribute('fo:margin-left', $indentString); } + if ($readOrder === Alignment::READORDER_RTL) { + $this->writer->writeAttribute('style:writing-mode', 'rl-tb'); + } elseif ($readOrder === Alignment::READORDER_LTR) { + $this->writer->writeAttribute('style:writing-mode', 'lr-tb'); + } $this->writer->endElement(); } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Xf.php b/src/PhpSpreadsheet/Writer/Xls/Xf.php index 2ab5a4ebca..bda33ed364 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Xf.php +++ b/src/PhpSpreadsheet/Writer/Xls/Xf.php @@ -220,8 +220,9 @@ public function writeXf(): string $header = pack('vv', $record, $length); //BIFF8 options: identation, shrinkToFit and text direction - $biff8_options = $this->style->getAlignment()->getIndent(); + $biff8_options = $this->style->getAlignment()->getIndent() & 15; $biff8_options |= (int) $this->style->getAlignment()->getShrinkToFit() << 4; + $biff8_options |= $this->style->getAlignment()->getReadOrder() << 6; $data = pack('vvvC', $ifnt, $ifmt, $style, $align); $data .= pack('CCC', self::mapTextRotation((int) $this->style->getAlignment()->getTextRotation()), $biff8_options, $used_attrib); diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/ReadOrderTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/ReadOrderTest.php new file mode 100644 index 0000000000..e9a41c024c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xml/ReadOrderTest.php @@ -0,0 +1,39 @@ +load($infile); + + $sheet0 = $robj->setActiveSheetIndex(0); + self::assertSame( + Alignment::READORDER_RTL, + $sheet0->getStyle('A1')->getAlignment()->getReadOrder() + ); + self::assertSame( + Alignment::READORDER_LTR, + $sheet0->getStyle('A2')->getAlignment()->getReadOrder() + ); + self::assertSame( + Alignment::READORDER_CONTEXT, + $sheet0->getStyle('A3')->getAlignment()->getReadOrder() + ); + self::assertSame( + 2, + $sheet0->getStyle('A5')->getAlignment()->getIndent() + ); + $robj->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/ReadOrderTest.php b/tests/PhpSpreadsheetTests/Writer/Html/ReadOrderTest.php new file mode 100644 index 0000000000..ccf68e80de --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/ReadOrderTest.php @@ -0,0 +1,93 @@ +getActiveSheet(); + $sheet->setCellValue('A1', '1-' . 'منصور حسين الناصر'); + $sheet->setCellValue('A2', '1-' . 'منصور حسين الناصر'); + $sheet->setCellValue('A3', '1-' . 'منصور حسين الناصر'); + $sheet->getStyle('A1') + ->getAlignment()->setReadOrder(Alignment::READORDER_RTL); + $sheet->getStyle('A2') + ->getAlignment()->setReadOrder(Alignment::READORDER_LTR); + $sheet->getStyle('A2')->getFont()->setName('Arial'); + $sheet->getStyle('A3')->getFont()->setName('Times New Roman'); + $writer = new HtmlWriter($spreadsheet); + $writer->setUseInlineCss(true); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString( + '' + . '' + . '' + . '' + . '' + . 'disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/ReadOrderTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/ReadOrderTest.php new file mode 100644 index 0000000000..a0b1b20d63 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/ReadOrderTest.php @@ -0,0 +1,51 @@ +getActiveSheet(); + $sheet->setCellValue('A1', '1-' . 'منصور حسين الناصر'); + $sheet->setCellValue('A2', '1-' . 'منصور حسين الناصر'); + $sheet->setCellValue('A3', '1-' . 'منصور حسين الناصر'); + $sheet->getStyle('A1') + ->getAlignment()->setReadOrder(Alignment::READORDER_RTL); + $sheet->getStyle('A2') + ->getAlignment()->setReadOrder(Alignment::READORDER_LTR); + + $sheet->setCellValue('A5', 'hello'); + $spreadsheet->getActiveSheet()->getStyle('A5') + ->getAlignment()->setIndent(2); + + $robj = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + $sheet0 = $robj->setActiveSheetIndex(0); + self::assertSame( + Alignment::READORDER_RTL, + $sheet0->getStyle('A1')->getAlignment()->getReadOrder() + ); + self::assertSame( + Alignment::READORDER_LTR, + $sheet0->getStyle('A2')->getAlignment()->getReadOrder() + ); + self::assertSame( + Alignment::READORDER_CONTEXT, + $sheet0->getStyle('A3')->getAlignment()->getReadOrder() + ); + self::assertSame( + 2, + $sheet0->getStyle('A5')->getAlignment()->getIndent() + ); + $robj->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/Xml/issue.850.xml b/tests/data/Reader/Xml/issue.850.xml new file mode 100644 index 0000000000..314034cb67 --- /dev/null +++ b/tests/data/Reader/Xml/issue.850.xml @@ -0,0 +1,85 @@ + + + + + Untitled Spreadsheet + Unknown Creator + Owen Leibman + 2025-09-19T18:05:38Z + 2025-09-19T18:05:38Z + 16.00 + + + + + + 6510 + 19200 + 32767 + 32767 + False + False + + + + + + + + + + + 1-منصور حسين الناصر + + + 1-منصور حسين الناصر + + + 1-منصور حسين الناصر + + + hello + +
+ + +
+