Skip to content

Commit 2920b2a

Browse files
authored
Merge pull request #4814 from oleibman/odsstyles3
Ods Reader Style Support Part 3 - Borders
2 parents d7b4442 + 97aa526 commit 2920b2a

File tree

6 files changed

+254
-54
lines changed

6 files changed

+254
-54
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
4343
- Ods improved handling of number formats. [Issue #3961](https://github.com/PHPOffice/PhpSpreadsheet/issues/3961) [Issue #4798](https://github.com/PHPOffice/PhpSpreadsheet/issues/4798) [PR #4806](https://github.com/PHPOffice/PhpSpreadsheet/pull/4806)
4444
- Ods Reader fonts and fills. [Issue #2622](https://github.com/PHPOffice/PhpSpreadsheet/issues/2622) [Issue #1191](https://github.com/PHPOffice/PhpSpreadsheet/issues/1191) [PR #4810](https://github.com/PHPOffice/PhpSpreadsheet/pull/4810)
4545
- Ods Reader alignment and cell protection. [PR #4813](https://github.com/PHPOffice/PhpSpreadsheet/pull/4813)
46+
- Ods Reader borders. [PR #4814](https://github.com/PHPOffice/PhpSpreadsheet/pull/4814)
4647

4748
## 2026-01-10 - 5.4.0
4849

docs/references/features-cross-reference.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@
722722
<td style="text-align: center; color: green;">✔</td>
723723
<td style="text-align: center; color: green;">✔</td>
724724
<td style="text-align: center; color: green;">✔</td>
725-
<td style="text-align: center; color: red;"></td>
725+
<td style="text-align: center; color: green;"></td>
726726
<td style="text-align: center; color: green;">✔</td>
727727
<td style="text-align: center;">N/A</td>
728728
<td style="text-align: center; color: orange;">●</td>
@@ -733,7 +733,7 @@
733733
<td style="text-align: center; color: green;">✔</td>
734734
<td style="text-align: center; color: green;">✔</td>
735735
<td style="text-align: center; color: green;">✔</td>
736-
<td style="text-align: center; color: red;"></td>
736+
<td style="text-align: center; color: green;"></td>
737737
<td style="text-align: center; color: green;">✔</td>
738738
<td style="text-align: center;">N/A</td>
739739
<td style="text-align: center; color: green;">✔</td>
@@ -744,7 +744,7 @@
744744
<td style="text-align: center; color: green;">✔</td>
745745
<td style="text-align: center; color: green;">✔</td>
746746
<td style="text-align: center; color: green;">✔</td>
747-
<td style="text-align: center; color: red;"></td>
747+
<td style="text-align: center; color: green;"></td>
748748
<td style="text-align: center; color: green;">✔</td>
749749
<td style="text-align: center;">N/A</td>
750750
<td style="text-align: center; color: green;">✔</td>
@@ -755,7 +755,7 @@
755755
<td style="text-align: center; color: green;">✔</td>
756756
<td style="text-align: center; color: green;">✔</td>
757757
<td style="text-align: center; color: green;">✔</td>
758-
<td style="text-align: center; color: red;"></td>
758+
<td style="text-align: center; color: green;"></td>
759759
<td style="text-align: center; color: green;">✔</td>
760760
<td style="text-align: center;">N/A</td>
761761
<td style="text-align: center; color: red;">✖</td>
@@ -1480,9 +1480,9 @@
14801480
</tr>
14811481
<tr>
14821482
<td style="padding-left: 3em;">Diagonal</td>
1483-
<td style="text-align: center; color: red;">✖</td>
14841483
<td style="text-align: center; color: green;">✔</td>
1485-
<td style="text-align: center; color: red;">✖</td>
1484+
<td style="text-align: center; color: green;">✔</td>
1485+
<td style="text-align: center; color: green;">✔</td>
14861486
<td style="text-align: center;">N/A</td>
14871487
<td style="text-align: center; color: red;">✖</td>
14881488
<td style="text-align: center; color: red;">✖</td>

samples/templates/OOCalcTest.ods

174 Bytes
Binary file not shown.

src/PhpSpreadsheet/Reader/Ods.php

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
2727
use PhpOffice\PhpSpreadsheet\Spreadsheet;
2828
use PhpOffice\PhpSpreadsheet\Style\Alignment;
29+
use PhpOffice\PhpSpreadsheet\Style\Border;
30+
use PhpOffice\PhpSpreadsheet\Style\Borders;
2931
use PhpOffice\PhpSpreadsheet\Style\Fill;
3032
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
3133
use PhpOffice\PhpSpreadsheet\Style\Protection;
@@ -284,6 +286,14 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
284286
* locked?: string,
285287
* hidden?: string,
286288
* },
289+
* borders?:array{
290+
* bottom?: array{borderStyle:string, color:array{rgb: string}},
291+
* left?: array{borderStyle:string, color:array{rgb: string}},
292+
* right?: array{borderStyle:string, color:array{rgb: string}},
293+
* top?: array{borderStyle:string, color:array{rgb: string}},
294+
* diagonal?: array{borderStyle:string, color:array{rgb: string}},
295+
* diagonalDirection?: int,
296+
* },
287297
* }>
288298
*/
289299
private array $allStyles;
@@ -409,12 +419,13 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
409419
}
410420
}
411421
if ($styleFamily === 'table-cell') {
412-
$fonts = $fills = $alignment1 = $alignment2 = $protection = [];
422+
$fonts = $fills = $alignment1 = $alignment2 = $protection = $borders = [];
413423
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
414424
$fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
415425
}
416426
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
417427
$fills = $this->getFillStyles($tableCellProperty, $fontNs);
428+
$borders = $this->getBorderStyles($tableCellProperty, $fontNs, $styleNs);
418429
$protection = $this->getProtectionStyles($tableCellProperty, $styleNs);
419430
}
420431
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
@@ -437,6 +448,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
437448
if (!empty($protection)) {
438449
$this->allStyles[$styleName]['protection'] = $protection;
439450
}
451+
if (!empty($borders)) {
452+
$this->allStyles[$styleName]['borders'] = $borders;
453+
}
440454
}
441455
}
442456
}
@@ -746,9 +760,34 @@ private function processTableRow(
746760
}
747761
// Fall through to process the cell, with per-column filter checks
748762
}
749-
if ($worksheet !== null && $cellData->hasChildNodes() && isset($this->allStyles[$styleName])) {
750-
$worksheet->getStyle("$columnID$rowID")
763+
if ($worksheet !== null && ($cellData->hasChildNodes() || ($cellData->nextSibling !== null)) && isset($this->allStyles[$styleName])) {
764+
$spannedRange = "$columnID$rowID";
765+
// the following is sufficient for ods,
766+
// and does no harm for xlsx/xls.
767+
$worksheet->getStyle($spannedRange)
751768
->applyFromArray($this->allStyles[$styleName]);
769+
// the rest of this block is needed for xlsx/xls,
770+
// and does no harm for ods.
771+
if (isset($this->allStyles[$styleName]['borders'])) {
772+
$spannedRows = $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
773+
$spannedColumns = $cellData->getAttributeNS($tableNs, 'number-rows-spanned');
774+
$spannedRows = max((int) $spannedRows, 1);
775+
$spannedColumns = max((int) $spannedColumns, 1);
776+
if ($spannedRows > 1 || $spannedColumns > 1) {
777+
$endRow = $rowID + $spannedRows - 1;
778+
$endCol = $columnID;
779+
while ($spannedColumns > 1) {
780+
StringHelper::stringIncrement($endCol);
781+
--$spannedColumns;
782+
}
783+
$spannedRange .= ":$endCol$endRow";
784+
$worksheet->getStyle($spannedRange)
785+
->getBorders()
786+
->applyFromArray(
787+
$this->allStyles[$styleName]['borders']
788+
);
789+
}
790+
}
752791
}
753792

754793
// Initialize variables
@@ -1508,4 +1547,74 @@ protected function getProtectionStyles(DOMElement $tableCellProperties, string $
15081547

15091548
return $protection;
15101549
}
1550+
1551+
private const MAP_BORDER_STYLE = [ // default BORDER_THIN
1552+
'none' => Border::BORDER_NONE,
1553+
'hidden' => Border::BORDER_NONE,
1554+
'dotted' => Border::BORDER_DOTTED,
1555+
'dash-dot' => Border::BORDER_DASHDOT,
1556+
'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1557+
'dashed' => Border::BORDER_DASHED,
1558+
'double' => Border::BORDER_DOUBLE,
1559+
];
1560+
1561+
private const MAP_BORDER_MEDIUM = [
1562+
Border::BORDER_THIN => Border::BORDER_MEDIUM,
1563+
Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
1564+
Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
1565+
Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
1566+
];
1567+
1568+
private const MAP_BORDER_THICK = [
1569+
Border::BORDER_THIN => Border::BORDER_THICK,
1570+
Border::BORDER_DASHDOT => Border::BORDER_MEDIUMDASHDOT,
1571+
Border::BORDER_DASHDOTDOT => Border::BORDER_MEDIUMDASHDOTDOT,
1572+
Border::BORDER_DASHED => Border::BORDER_MEDIUMDASHED,
1573+
];
1574+
1575+
/** @return array{
1576+
* bottom?: array{borderStyle:string, color:array{rgb: string}},
1577+
* top?: array{borderStyle:string, color:array{rgb: string}},
1578+
* left?: array{borderStyle:string, color:array{rgb: string}},
1579+
* right?: array{borderStyle:string, color:array{rgb: string}},
1580+
* diagonal?: array{borderStyle:string, color:array{rgb: string}},
1581+
* diagonalDirection?: int,
1582+
* }
1583+
*/
1584+
protected function getBorderStyles(DOMElement $tableCellProperties, string $fontNs, string $styleNs): array
1585+
{
1586+
$borders = [];
1587+
$temp = $tableCellProperties->getAttributeNs($fontNs, 'border');
1588+
$diagonalIndex = Borders::DIAGONAL_NONE;
1589+
foreach (['bottom', 'left', 'right', 'top', 'diagonal-tl-br', 'diagonal-bl-tr'] as $direction) {
1590+
if (str_starts_with($direction, 'diagonal')) {
1591+
$directionIndex = 'diagonal';
1592+
$temp = $tableCellProperties->getAttributeNs($styleNs, $direction);
1593+
} else {
1594+
$directionIndex = $direction;
1595+
$temp = $tableCellProperties->getAttributeNs($fontNs, "border-$direction");
1596+
}
1597+
if (Preg::isMatch('/^(\d+(?:[.]\d+)?)pt\s+([-\w]+)\s+#([0-9a-fA-F]{6})$/', $temp, $matches)) {
1598+
$style = self::MAP_BORDER_STYLE[$matches[2]] ?? Border::BORDER_THIN;
1599+
$width = (float) $matches[1];
1600+
if ($width >= 2.5) {
1601+
$style = self::MAP_BORDER_THICK[$style] ?? $style;
1602+
} elseif ($width >= 1.75) {
1603+
$style = self::MAP_BORDER_MEDIUM[$style] ?? $style;
1604+
}
1605+
$color = $matches[3];
1606+
$borders[$directionIndex] = ['borderStyle' => $style, 'color' => ['rgb' => $matches[3]]];
1607+
if ($direction === 'diagonal-tl-br') {
1608+
$diagonalIndex = Borders::DIAGONAL_DOWN;
1609+
} elseif ($direction === 'diagonal-bl-tr') {
1610+
$diagonalIndex = ($diagonalIndex === Borders::DIAGONAL_NONE) ? Borders::DIAGONAL_UP : Borders::DIAGONAL_BOTH;
1611+
}
1612+
}
1613+
}
1614+
if ($diagonalIndex !== Borders::DIAGONAL_NONE) {
1615+
$borders['diagonalDirection'] = $diagonalIndex;
1616+
}
1617+
1618+
return $borders; // @phpstan-ignore-line
1619+
}
15111620
}

src/PhpSpreadsheet/Writer/Ods/Cell/Style.php

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ private function writeBordersStyle(Borders $borders): void
9797
$this->writeBorderStyle('left', $borders->getLeft());
9898
$this->writeBorderStyle('right', $borders->getRight());
9999
$this->writeBorderStyle('top', $borders->getTop());
100+
$diagonal = $borders->getDiagonalDirection();
101+
if ($diagonal === Borders::DIAGONAL_DOWN || $diagonal === Borders::DIAGONAL_BOTH) {
102+
$this->writeBorderStyle('style:diagonal-tl-br', $borders->getDiagonal());
103+
}
104+
if ($diagonal === Borders::DIAGONAL_UP || $diagonal === Borders::DIAGONAL_BOTH) {
105+
$this->writeBorderStyle('style:diagonal-bl-tr', $borders->getDiagonal());
106+
}
100107
}
101108

102109
private function writeBorderStyle(string $direction, Border $border): void
@@ -105,64 +112,55 @@ private function writeBorderStyle(string $direction, Border $border): void
105112
return;
106113
}
107114

108-
$this->writer->writeAttribute('fo:border-' . $direction, sprintf(
115+
$attrName = str_starts_with($direction, 'style:') ? $direction : ('fo:border-' . $direction);
116+
$this->writer->writeAttribute($attrName, sprintf(
109117
'%s %s #%s',
110118
$this->mapBorderWidth($border),
111119
$this->mapBorderStyle($border),
112120
$border->getColor()->getRGB(),
113121
));
114122
}
115123

124+
private const MAP_BORDER_WIDTH = [
125+
Border::BORDER_THIN => '0.75pt',
126+
Border::BORDER_DASHED => '0.75pt',
127+
Border::BORDER_DASHDOT => '0.75pt',
128+
Border::BORDER_DASHDOTDOT => '0.75pt',
129+
Border::BORDER_DOTTED => '0.75pt',
130+
Border::BORDER_HAIR => '0.75pt',
131+
// end of thin styles
132+
Border::BORDER_MEDIUM => '1.75pt',
133+
Border::BORDER_MEDIUMDASHED => '1.75pt',
134+
Border::BORDER_MEDIUMDASHDOT => '1.75pt',
135+
Border::BORDER_MEDIUMDASHDOTDOT => '1.75pt',
136+
Border::BORDER_SLANTDASHDOT => '1.75pt',
137+
// end of medium styles
138+
Border::BORDER_DOUBLE => '2.5pt',
139+
Border::BORDER_THICK => '2.5pt',
140+
];
141+
116142
private function mapBorderWidth(Border $border): string
117143
{
118-
switch ($border->getBorderStyle()) {
119-
case Border::BORDER_THIN:
120-
case Border::BORDER_DASHED:
121-
case Border::BORDER_DASHDOT:
122-
case Border::BORDER_DASHDOTDOT:
123-
case Border::BORDER_DOTTED:
124-
case Border::BORDER_HAIR:
125-
return '0.75pt';
126-
case Border::BORDER_MEDIUM:
127-
case Border::BORDER_MEDIUMDASHED:
128-
case Border::BORDER_MEDIUMDASHDOT:
129-
case Border::BORDER_MEDIUMDASHDOTDOT:
130-
case Border::BORDER_SLANTDASHDOT:
131-
return '1.75pt';
132-
case Border::BORDER_DOUBLE:
133-
case Border::BORDER_THICK:
134-
return '2.5pt';
135-
}
136-
137-
return '1pt';
144+
return self::MAP_BORDER_WIDTH[$border->getBorderStyle()] ?? '1pt';
138145
}
139146

147+
private const MAP_BORDER_STYLE = [
148+
Border::BORDER_DOTTED => 'dotted',
149+
Border::BORDER_DASHED => 'dashed',
150+
Border::BORDER_MEDIUMDASHED => 'dashed',
151+
Border::BORDER_DASHDOT => 'dash-dot',
152+
Border::BORDER_MEDIUMDASHDOT => 'dash-dot',
153+
Border::BORDER_DASHDOTDOT => 'dash-dot-dot',
154+
Border::BORDER_MEDIUMDASHDOTDOT => 'dash-dot-dot',
155+
Border::BORDER_SLANTDASHDOT => 'dashed',
156+
Border::BORDER_DOUBLE => 'double',
157+
Border::BORDER_NONE => 'none',
158+
// HAIR, MEDIUM, THICK, THIN fall through to default solid
159+
];
160+
140161
private function mapBorderStyle(Border $border): string
141162
{
142-
switch ($border->getBorderStyle()) {
143-
case Border::BORDER_DOTTED:
144-
case Border::BORDER_MEDIUMDASHDOTDOT:
145-
return Border::BORDER_DOTTED;
146-
147-
case Border::BORDER_DASHED:
148-
case Border::BORDER_DASHDOT:
149-
case Border::BORDER_DASHDOTDOT:
150-
case Border::BORDER_MEDIUMDASHDOT:
151-
case Border::BORDER_MEDIUMDASHED:
152-
case Border::BORDER_SLANTDASHDOT:
153-
return Border::BORDER_DASHED;
154-
155-
case Border::BORDER_DOUBLE:
156-
return Border::BORDER_DOUBLE;
157-
158-
case Border::BORDER_HAIR:
159-
case Border::BORDER_MEDIUM:
160-
case Border::BORDER_THICK:
161-
case Border::BORDER_THIN:
162-
return 'solid';
163-
}
164-
165-
return 'solid';
163+
return self::MAP_BORDER_STYLE[$border->getBorderStyle()] ?? 'solid';
166164
}
167165

168166
// 2d array, 1st index is locked, 2nd is hidden

0 commit comments

Comments
 (0)