diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index b57d6c1309..579f3892da 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); @@ -1083,8 +1095,11 @@ private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, a break; case 'text-indent': + $indentDimension = new CssDimension($styleValueString); + $indent = $indentDimension + ->toUnit(CssDimension::UOM_PIXELS); $cellStyle->getAlignment()->setIndent( - (int) str_replace(['px'], '', $styleValueString) + (int) ($indent / Alignment::INDENT_UNITS_TO_PIXELS) ); break; 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/Style/Alignment.php b/src/PhpSpreadsheet/Style/Alignment.php index d5c42c273a..6ff44e3ef6 100644 --- a/src/PhpSpreadsheet/Style/Alignment.php +++ b/src/PhpSpreadsheet/Style/Alignment.php @@ -92,6 +92,8 @@ class Alignment extends Supervisor const TEXTROTATION_STACK_EXCEL = 255; const TEXTROTATION_STACK_PHPSPREADSHEET = -165; // 90 - 255 + public const INDENT_UNITS_TO_PIXELS = 9; + /** * Horizontal alignment. */ diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 27849bf374..ecf79d8060 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1115,7 +1115,12 @@ private function createCSSStyleAlignment(Alignment $alignment): array if ($textAlign) { $css['text-align'] = $textAlign; if (in_array($textAlign, ['left', 'right'])) { - $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px'; + $css['padding-' . $textAlign] = (string) ($alignment->getIndent() * Alignment::INDENT_UNITS_TO_PIXELS) . 'px'; + } + } else { + $indent = $alignment->getIndent(); + if ($indent !== 0) { + $css['text-indent'] = (string) ($alignment->getIndent() * Alignment::INDENT_UNITS_TO_PIXELS) . 'px'; } } $rotation = $alignment->getTextRotation(); @@ -1126,6 +1131,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; } @@ -1516,7 +1527,6 @@ private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, stri /** @param string|string[] $cssClass */ private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string { - $cellData = ' '; if ($cell instanceof Cell) { $cellData = ''; // Don't know what this does, and no test cases. @@ -1565,13 +1575,21 @@ private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cel } } } else { + $cellData = "$cell"; // Use default borders for empty cell if (is_string($cssClass)) { $cssClass .= ' style0'; } } + /* + * Browsers may remove an entirely empty row. + * An interesting option is to leave an empty cell empty using css. + * td:empty::after{content: "\00a0";} + * This works well in modern browsers. + * Alas, none of our Pdf writers can handle it. + */ - return $cellData; + return (trim($cellData) === '') ? ' ' : $cellData; } private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string 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/Html/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php index a61f901131..6a8f2a6b93 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php @@ -199,7 +199,7 @@ public function testCanApplyAlignment(): void Center valign Center align Center valign - Text indent + Text indent Wraptext '; @@ -220,7 +220,7 @@ public function testCanApplyAlignment(): void self::assertEquals(Alignment::VERTICAL_CENTER, $style->getAlignment()->getVertical()); $style = $firstSheet->getCell('E1')->getStyle(); - self::assertEquals(10, $style->getAlignment()->getIndent()); + self::assertEquals(1, $style->getAlignment()->getIndent()); $style = $firstSheet->getCell('F1')->getStyle(); self::assertTrue($style->getAlignment()->getWrapText()); @@ -302,14 +302,14 @@ public function testTextIndentUseRowspan(): void 2 - Text Indent + Text Indent '; $filename = HtmlHelper::createHtml($html); $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $style = $firstSheet->getCell('C2')->getStyle(); - self::assertEquals(10, $style->getAlignment()->getIndent()); + self::assertEquals(1, $style->getAlignment()->getIndent()); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php index 01c25edafe..c2b8720adc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php @@ -93,7 +93,7 @@ public function testHtml(): void $data = str_replace(["\r", "\n"], '', $writer->generateHtmlAll()); $expected = ' ' // Cell D18 . '  ' - . ' ' + . '  ' . ' Eligible ' . ' Non'; self::assertStringContainsString($expected, $data, 'Cell D18 style'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/ReadOrderTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/ReadOrderTest.php new file mode 100644 index 0000000000..0c7618e5fd --- /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/HtmlColourScaleTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php index 87a9fb6983..036fe33fed 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php @@ -57,7 +57,7 @@ public function testColourScaleHtmlOutput(): void ['G3', 'background-color:#EC926F;">7<', 'cell G3'], ['H3', 'background-color:#E67C73;">8<', 'cell H3'], ['A4', 'background-color:#57BB8A;">1<', 'cell A4'], - ['I4', 'null"><', 'empty cell I4'], + ['I4', 'null"> <', 'empty cell I4'], ['J4', 'background-color:#E67C73;">10<', 'cell J4'], ]; foreach ($expectedMatches as $expected) { diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php index 6044b1d2bb..5ab5b99beb 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlNumberFormatTest.php @@ -151,6 +151,7 @@ public function testFormatValueWithMask(mixed $expectedResult, mixed $val, strin $writer = new Html($spreadsheet); $html = $writer->generateHTMLAll(); + $html = str_replace('> <', '><', $html); // clear empty cells $dom = new DOMDocument(); $dom->loadHTML($html); $body = $dom->getElementsByTagName('body')->item(0); diff --git a/tests/PhpSpreadsheetTests/Writer/Html/ReadOrderTest.php b/tests/PhpSpreadsheetTests/Writer/Html/ReadOrderTest.php new file mode 100644 index 0000000000..9efabbe8f7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/ReadOrderTest.php @@ -0,0 +1,121 @@ +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'); + $sheet->setCellValue('A5', 'hello'); + $sheet->getStyle('A5')->getFont()->setName('Tahoma'); + $sheet->getStyle('A5') + ->getAlignment()->setIndent(2); + $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 + +
+ + +
+