Skip to content

Commit d7b4442

Browse files
authored
Merge pull request #4813 from oleibman/odsstyles2
Ods Reader Style Support Part 2 - Alignment, Protection, Default Fill
2 parents d9cd9ac + 62628e0 commit d7b4442

File tree

19 files changed

+497
-86
lines changed

19 files changed

+497
-86
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
4242
- Image Css size in millimeters. [Issue #4800](https://github.com/PHPOffice/PhpSpreadsheet/issues/4800) [PR #4801](https://github.com/PHPOffice/PhpSpreadsheet/pull/4801)
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)
45+
- Ods Reader alignment and cell protection. [PR #4813](https://github.com/PHPOffice/PhpSpreadsheet/pull/4813)
4546

4647
## 2026-01-10 - 5.4.0
4748

docs/references/features-cross-reference.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@
535535
<td style="text-align: center; color: green;">✔</td>
536536
<td style="text-align: center; color: green;">✔</td>
537537
<td style="text-align: center; color: green;">✔</td>
538-
<td style="text-align: center; color: red;"></td>
538+
<td style="text-align: center; color: green;"></td>
539539
<td style="text-align: center; color: green;">✔</td>
540540
<td style="text-align: center;">N/A</td>
541541
<td style="text-align: center; color: red;">✖</td>
@@ -546,7 +546,7 @@
546546
<td style="text-align: center; color: green;">✔</td>
547547
<td style="text-align: center; color: green;">✔</td>
548548
<td style="text-align: center; color: green;">✔</td>
549-
<td style="text-align: center; color: red;"></td>
549+
<td style="text-align: center; color: green;"></td>
550550
<td style="text-align: center; color: green;">✔</td>
551551
<td style="text-align: center;">N/A</td>
552552
<td style="text-align: center; color: red;">✖</td>
@@ -557,7 +557,7 @@
557557
<td style="text-align: center; color: green;">✔</td>
558558
<td style="text-align: center; color: green;">✔</td>
559559
<td style="text-align: center; color: green;">✔</td>
560-
<td style="text-align: center; color: red;"></td>
560+
<td style="text-align: center; color: green;"></td>
561561
<td style="text-align: center; color: green;">✔</td>
562562
<td style="text-align: center;">N/A</td>
563563
<td style="text-align: center; color: red;">✖</td>
@@ -568,7 +568,7 @@
568568
<td style="text-align: center; color: green;">✔</td>
569569
<td style="text-align: center; color: green;">✔</td>
570570
<td style="text-align: center; color: green;">✔</td>
571-
<td style="text-align: center; color: red;"></td>
571+
<td style="text-align: center; color: green;"></td>
572572
<td style="text-align: center; color: green;">✔</td>
573573
<td style="text-align: center;">N/A</td>
574574
<td style="text-align: center; color: red;">✖</td>
@@ -579,7 +579,7 @@
579579
<td style="text-align: center; color: green;">✔</td>
580580
<td style="text-align: center; color: green;">✔</td>
581581
<td style="text-align: center; color: green;">✔</td>
582-
<td style="text-align: center; color: red;"></td>
582+
<td style="text-align: center; color: green;"></td>
583583
<td style="text-align: center; color: green;">✔</td>
584584
<td style="text-align: center;">N/A</td>
585585
<td style="text-align: center; color: red;">✖</td>
@@ -590,7 +590,7 @@
590590
<td style="text-align: center; color: green;">✔</td>
591591
<td style="text-align: center; color: green;">✔</td>
592592
<td style="text-align: center; color: red;">✖</td>
593-
<td style="text-align: center; color: red;"></td>
593+
<td style="text-align: center; color: green;"></td>
594594
<td style="text-align: center; color: green;">✔</td>
595595
<td style="text-align: center;">N/A</td>
596596
<td style="text-align: center; color: red;">✖</td>

samples/templates/OOCalcTest.ods

5.39 KB
Binary file not shown.

src/PhpSpreadsheet/Reader/Ods.php

Lines changed: 172 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
use PhpOffice\PhpSpreadsheet\Shared\File;
2626
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
2727
use PhpOffice\PhpSpreadsheet\Spreadsheet;
28+
use PhpOffice\PhpSpreadsheet\Style\Alignment;
2829
use PhpOffice\PhpSpreadsheet\Style\Fill;
2930
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
31+
use PhpOffice\PhpSpreadsheet\Style\Protection;
3032
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
3133
use Throwable;
3234
use XMLReader;
@@ -146,7 +148,14 @@ public function listWorksheetNames(string $filename): array
146148
/**
147149
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
148150
*
149-
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
151+
* @return array<int, array{
152+
* worksheetName: string,
153+
* lastColumnLetter: string,
154+
* lastColumnIndex: int,
155+
* totalRows: int,
156+
* totalColumns: int,
157+
* sheetState: string
158+
* }>
150159
*/
151160
public function listWorksheetInfo(string $filename): array
152161
{
@@ -262,8 +271,21 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
262271
* fill?:array{
263272
* fillType?: string,
264273
* startColor?: array{rgb: string},
265-
* }
266-
* }> */
274+
* },
275+
* alignment?:array{
276+
* horizontal?: string,
277+
* readOrder?: int,
278+
* shrinkToFit?: bool,
279+
* textRotation?: int,
280+
* vertical?: string,
281+
* wrapText?: bool,
282+
* },
283+
* protection?:array{
284+
* locked?: string,
285+
* hidden?: string,
286+
* },
287+
* }>
288+
*/
267289
private array $allStyles;
268290

269291
/**
@@ -309,6 +331,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
309331
foreach ($automaticStyles as $automaticStyle) {
310332
$styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
311333
if ($styleFamily === 'table-cell') {
334+
$fonts = [];
312335
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
313336
$fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
314337
}
@@ -330,22 +353,24 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
330353
$fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
331354
}
332355
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
333-
$fills = $this->getFillStyles($tableCellProperty, $styleNs, $fontNs);
356+
$fills = $this->getFillStyles($tableCellProperty, $fontNs);
334357
}
335358
if ($styleName !== '') {
336-
$allStyles = [];
337359
if (!empty($fonts)) {
338360
$this->allStyles[$styleName]['font'] = $fonts;
339-
$allStyles['font'] = $fonts;
361+
if (!$defaultStyleSet && $styleName === 'Default') {
362+
$spreadsheet->getDefaultStyle()
363+
->getFont()
364+
->applyFromArray($fonts);
365+
}
340366
}
341367
if (!empty($fills)) {
342368
$this->allStyles[$styleName]['fill'] = $fills;
343-
$allStyles['fill'] = $fills;
344-
}
345-
if (!$defaultStyleSet && $styleName === 'Default' && isset($allStyles['font'])) {
346-
$spreadsheet->getDefaultStyle()
347-
->getFont()
348-
->applyFromArray($allStyles['font']);
369+
if ($styleName === 'Default') {
370+
$spreadsheet->getDefaultStyle()
371+
->getFill()
372+
->applyFromArray($fills);
373+
}
349374
}
350375
}
351376
}
@@ -384,12 +409,19 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
384409
}
385410
}
386411
if ($styleFamily === 'table-cell') {
387-
$fonts = $fills = [];
412+
$fonts = $fills = $alignment1 = $alignment2 = $protection = [];
388413
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'text-properties') as $textProperty) {
389414
$fonts = $this->getFontStyles($textProperty, $styleNs, $fontNs);
390415
}
391416
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
392-
$fills = $this->getFillStyles($tableCellProperty, $styleNs, $fontNs);
417+
$fills = $this->getFillStyles($tableCellProperty, $fontNs);
418+
$protection = $this->getProtectionStyles($tableCellProperty, $styleNs);
419+
}
420+
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'table-cell-properties') as $tableCellProperty) {
421+
$alignment1 = $this->getAlignment1Styles($tableCellProperty, $styleNs, $fontNs);
422+
}
423+
foreach ($automaticStyle->getElementsByTagNameNS($styleNs, 'paragraph-properties') as $paragraphProperty) {
424+
$alignment2 = $this->getAlignment2Styles($paragraphProperty, $styleNs, $fontNs);
393425
}
394426
if ($styleName !== '') {
395427
if (!empty($fonts)) {
@@ -398,6 +430,13 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
398430
if (!empty($fills)) {
399431
$this->allStyles[$styleName]['fill'] = $fills;
400432
}
433+
$alignment = array_merge($alignment1, $alignment2);
434+
if (!empty($alignment)) {
435+
$this->allStyles[$styleName]['alignment'] = $alignment;
436+
}
437+
if (!empty($protection)) {
438+
$this->allStyles[$styleName]['protection'] = $protection;
439+
}
401440
}
402441
}
403442
}
@@ -1340,15 +1379,133 @@ protected function getFontStyles(DOMElement $textProperty, string $styleNs, stri
13401379
* startColor?: array{rgb: string},
13411380
* }
13421381
*/
1343-
protected function getFillStyles(DOMElement $tableCellProperties, string $styleNs, string $fontNs): array
1382+
protected function getFillStyles(DOMElement $tableCellProperties, string $fontNs): array
13441383
{
13451384
$fills = [];
13461385
$temp = $tableCellProperties->getAttributeNs($fontNs, 'background-color');
13471386
if (Preg::isMatch('/^#[a-f0-9]{6}$/i', $temp)) {
13481387
$fills['fillType'] = Fill::FILL_SOLID;
13491388
$fills['startColor'] = ['rgb' => substr($temp, 1)];
1389+
} elseif ($temp === 'transparent') {
1390+
$fills['fillType'] = Fill::FILL_NONE;
13501391
}
13511392

13521393
return $fills;
13531394
}
1395+
1396+
private const MAP_VERTICAL = [
1397+
'top' => Alignment::VERTICAL_TOP,
1398+
'middle' => Alignment::VERTICAL_CENTER,
1399+
'automatic' => Alignment::VERTICAL_JUSTIFY,
1400+
'bottom' => Alignment::VERTICAL_BOTTOM,
1401+
];
1402+
private const MAP_HORIZONTAL = [
1403+
'center' => Alignment::HORIZONTAL_CENTER,
1404+
'end' => Alignment::HORIZONTAL_RIGHT,
1405+
'justify' => Alignment::HORIZONTAL_FILL,
1406+
'start' => Alignment::HORIZONTAL_LEFT,
1407+
];
1408+
1409+
/** @return array{
1410+
* shrinkToFit?: bool,
1411+
* textRotation?: int,
1412+
* vertical?: string,
1413+
* wrapText?: bool,
1414+
* }
1415+
*/
1416+
protected function getAlignment1Styles(DOMElement $tableCellProperties, string $styleNs, string $fontNs): array
1417+
{
1418+
$alignment1 = [];
1419+
$temp = $tableCellProperties->getAttributeNs($styleNs, 'rotation-angle');
1420+
if (is_numeric($temp)) {
1421+
$temp2 = (int) $temp;
1422+
if ($temp2 > 90) {
1423+
$temp2 -= 360;
1424+
}
1425+
if ($temp2 >= -90 && $temp2 <= 90) {
1426+
$alignment1['textRotation'] = (int) $temp2;
1427+
}
1428+
}
1429+
$temp = $tableCellProperties->getAttributeNs($styleNs, 'vertical-align');
1430+
$temp2 = self::MAP_VERTICAL[$temp] ?? '';
1431+
if ($temp2 !== '') {
1432+
$alignment1['vertical'] = $temp2;
1433+
}
1434+
$temp = $tableCellProperties->getAttributeNs($fontNs, 'wrap-option');
1435+
if ($temp === 'wrap') {
1436+
$alignment1['wrapText'] = true;
1437+
} elseif ($temp === 'no-wrap') {
1438+
$alignment1['wrapText'] = false;
1439+
}
1440+
$temp = $tableCellProperties->getAttributeNs($styleNs, 'shrink-to-fit');
1441+
if ($temp === 'true' || $temp === 'false') {
1442+
$alignment1['shrinkToFit'] = $temp === 'true';
1443+
}
1444+
1445+
return $alignment1;
1446+
}
1447+
1448+
/** @return array{
1449+
* horizontal?: string,
1450+
* readOrder?: int,
1451+
* }
1452+
*/
1453+
protected function getAlignment2Styles(DOMElement $paragraphProperties, string $styleNs, string $fontNs): array
1454+
{
1455+
$alignment2 = [];
1456+
$temp = $paragraphProperties->getAttributeNs($fontNs, 'text-align');
1457+
$temp2 = self::MAP_HORIZONTAL[$temp] ?? '';
1458+
if ($temp2 !== '') {
1459+
$alignment2['horizontal'] = $temp2;
1460+
}
1461+
$temp = $paragraphProperties->getAttributeNs($fontNs, 'margin-left') ?: $paragraphProperties->getAttributeNs($fontNs, 'margin-right');
1462+
if (Preg::isMatch('/^\d+([.]\d+)?(cm|in|mm|pt)$/', $temp)) {
1463+
$dimension = new HelperDimension($temp);
1464+
$alignment2['indent'] = (int) round($dimension->toUnit('px') / Alignment::INDENT_UNITS_TO_PIXELS);
1465+
}
1466+
1467+
$temp = $paragraphProperties->getAttributeNs($styleNs, 'writing-mode');
1468+
if ($temp === 'rl-tb') {
1469+
$alignment2['readOrder'] = Alignment::READORDER_RTL;
1470+
} elseif ($temp === 'lr-tb') {
1471+
$alignment2['readOrder'] = Alignment::READORDER_LTR;
1472+
}
1473+
1474+
return $alignment2;
1475+
}
1476+
1477+
/** @return array{
1478+
* locked?: string,
1479+
* hidden?: string,
1480+
* }
1481+
*/
1482+
protected function getProtectionStyles(DOMElement $tableCellProperties, string $styleNs): array
1483+
{
1484+
$protection = [];
1485+
$temp = $tableCellProperties->getAttributeNs($styleNs, 'cell-protect');
1486+
switch ($temp) {
1487+
case 'protected formula-hidden':
1488+
$protection['locked'] = Protection::PROTECTION_PROTECTED;
1489+
$protection['hidden'] = Protection::PROTECTION_PROTECTED;
1490+
1491+
break;
1492+
case 'formula-hidden':
1493+
$protection['locked'] = Protection::PROTECTION_UNPROTECTED;
1494+
$protection['hidden'] = Protection::PROTECTION_PROTECTED;
1495+
1496+
break;
1497+
case 'protected':
1498+
$protection['locked'] = Protection::PROTECTION_PROTECTED;
1499+
$protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
1500+
1501+
break;
1502+
case 'none':
1503+
$protection['locked'] = Protection::PROTECTION_UNPROTECTED;
1504+
$protection['hidden'] = Protection::PROTECTION_UNPROTECTED;
1505+
1506+
break;
1507+
}
1508+
1509+
return $protection;
1510+
}
13541511
}

src/PhpSpreadsheet/Style/Alignment.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ public function setTextRotation(int $angleInDegrees): static
365365
$this->textRotation = $angleInDegrees;
366366
}
367367
} else {
368-
throw new PhpSpreadsheetException('Text rotation should be a value between -90 and 90.');
368+
throw new PhpSpreadsheetException("Text rotation $angleInDegrees should be a value between -90 and 90.");
369369
}
370370

371371
return $this;

0 commit comments

Comments
 (0)