Skip to content

Commit 7e38073

Browse files
authored
Reconcile Differences between Css and Excel For Cell Alignment (PHPOffice#3048)
This PR expands on PR PHPOffice#2195 from @nkjackzhang. That PR has been stalled for some time awaiting requested fixes. Those fixes are part of this PR, and additional tests and samples are added. The original request was to handle `vertical-align:middle` in Css (Excel uses `center`). This PR does its best to also handle vertical alignment Excel values not found in Css - `justify` (as `middle`) and `distributed` (as `middle`). It likewises handles valid Css values not found in Excel (`baseline`, `sub`, and `text-bottom` as `bottom`; `super` and `text-top` as `top`; `middle` as `center`). It also handles horizontal alignment Excel values not found in Css - `center-continuous` as `center` and `distributed` as `justify`; I couldn't think of a reasonable equivalent for `fill`, so it is ignored. The values assigned for vertical and horizontal alignment are now lower-cased (special handling required for `centerContinuous`).
1 parent 420a63b commit 7e38073

File tree

6 files changed

+273
-39
lines changed

6 files changed

+273
-39
lines changed

samples/Basic/49_alignment.php

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
4+
use PhpOffice\PhpSpreadsheet\Style\Alignment;
5+
6+
require __DIR__ . '/../Header.php';
7+
8+
$helper->log('Create new Spreadsheet object');
9+
$spreadsheet = new Spreadsheet();
10+
$spreadsheet->getProperties()->setTitle('Alignment');
11+
$sheet = $spreadsheet->getActiveSheet();
12+
$hi = 'Hi There';
13+
$ju = 'This is a longer than normal sentence';
14+
$sheet->fromArray([
15+
['', 'default', 'bottom', 'top', 'center', 'justify', 'distributed'],
16+
['default', $hi, $hi, $hi, $hi, $hi, $hi],
17+
['left', $hi, $hi, $hi, $hi, $hi, $hi],
18+
['right', $hi, $hi, $hi, $hi, $hi, $hi],
19+
['center', $hi, $hi, $hi, $hi, $hi, $hi],
20+
['justify', $ju, $ju, $ju, $ju, $ju, $ju],
21+
['distributed', $ju, $ju, $ju, $ju, $ju, $ju],
22+
]);
23+
$sheet->getColumnDimension('B')->setWidth(20);
24+
$sheet->getColumnDimension('C')->setWidth(20);
25+
$sheet->getColumnDimension('D')->setWidth(20);
26+
$sheet->getColumnDimension('E')->setWidth(20);
27+
$sheet->getColumnDimension('F')->setWidth(20);
28+
$sheet->getColumnDimension('G')->setWidth(20);
29+
$sheet->getRowDimension(2)->setRowHeight(30);
30+
$sheet->getRowDimension(3)->setRowHeight(30);
31+
$sheet->getRowDimension(4)->setRowHeight(30);
32+
$sheet->getRowDimension(5)->setRowHeight(30);
33+
$sheet->getRowDimension(6)->setRowHeight(40);
34+
$sheet->getRowDimension(7)->setRowHeight(40);
35+
$minRow = 2;
36+
$maxRow = 7;
37+
$minCol = 'B';
38+
$maxCol = 'g';
39+
$sheet->getStyle("C$minRow:C$maxRow")
40+
->getAlignment()
41+
->setVertical(Alignment::VERTICAL_BOTTOM);
42+
$sheet->getStyle("D$minRow:D$maxRow")
43+
->getAlignment()
44+
->setVertical(Alignment::VERTICAL_TOP);
45+
$sheet->getStyle("E$minRow:E$maxRow")
46+
->getAlignment()
47+
->setVertical(Alignment::VERTICAL_CENTER);
48+
$sheet->getStyle("F$minRow:F$maxRow")
49+
->getAlignment()
50+
->setVertical(Alignment::VERTICAL_JUSTIFY);
51+
$sheet->getStyle("G$minRow:G$maxRow")
52+
->getAlignment()
53+
->setVertical(Alignment::VERTICAL_DISTRIBUTED);
54+
$sheet->getStyle("{$minCol}3:{$maxCol}3")
55+
->getAlignment()
56+
->setHorizontal(Alignment::HORIZONTAL_LEFT);
57+
$sheet->getStyle("{$minCol}4:{$maxCol}4")
58+
->getAlignment()
59+
->setHorizontal(Alignment::HORIZONTAL_RIGHT);
60+
$sheet->getStyle("{$minCol}5:{$maxCol}5")
61+
->getAlignment()
62+
->setHorizontal(Alignment::HORIZONTAL_CENTER);
63+
$sheet->getStyle("{$minCol}6:{$maxCol}6")
64+
->getAlignment()
65+
->setHorizontal(Alignment::HORIZONTAL_JUSTIFY);
66+
$sheet->getStyle("{$minCol}7:{$maxCol}7")
67+
->getAlignment()
68+
->setHorizontal(Alignment::HORIZONTAL_DISTRIBUTED);
69+
70+
$sheet->getCell('A9')->setValue('Center Continuous A9-C9');
71+
$sheet->getStyle('A9:C9')
72+
->getAlignment()
73+
->setHorizontal(Alignment::HORIZONTAL_CENTER_CONTINUOUS);
74+
$sheet->getCell('A10')->setValue('Fill');
75+
$sheet->getStyle('A10')
76+
->getAlignment()
77+
->setHorizontal(Alignment::HORIZONTAL_FILL);
78+
$sheet->setSelectedCells('A1');
79+
80+
$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Html', 'Xls']);

src/PhpSpreadsheet/Style/Alignment.php

+64-5
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,73 @@ class Alignment extends Supervisor
1515
const HORIZONTAL_JUSTIFY = 'justify';
1616
const HORIZONTAL_FILL = 'fill';
1717
const HORIZONTAL_DISTRIBUTED = 'distributed'; // Excel2007 only
18+
private const HORIZONTAL_CENTER_CONTINUOUS_LC = 'centercontinuous';
19+
// Mapping for horizontal alignment
20+
const HORIZONTAL_ALIGNMENT_FOR_XLSX = [
21+
self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT,
22+
self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT,
23+
self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER,
24+
self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER_CONTINUOUS,
25+
self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY,
26+
self::HORIZONTAL_FILL => self::HORIZONTAL_FILL,
27+
self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_DISTRIBUTED,
28+
];
29+
// Mapping for horizontal alignment CSS
30+
const HORIZONTAL_ALIGNMENT_FOR_HTML = [
31+
self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT,
32+
self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT,
33+
self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER,
34+
self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER,
35+
self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY,
36+
//self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, // no reasonable equivalent for fill
37+
self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_JUSTIFY,
38+
];
1839

1940
// Vertical alignment styles
2041
const VERTICAL_BOTTOM = 'bottom';
2142
const VERTICAL_TOP = 'top';
2243
const VERTICAL_CENTER = 'center';
2344
const VERTICAL_JUSTIFY = 'justify';
2445
const VERTICAL_DISTRIBUTED = 'distributed'; // Excel2007 only
46+
// Vertical alignment CSS
47+
private const VERTICAL_BASELINE = 'baseline';
48+
private const VERTICAL_MIDDLE = 'middle';
49+
private const VERTICAL_SUB = 'sub';
50+
private const VERTICAL_SUPER = 'super';
51+
private const VERTICAL_TEXT_BOTTOM = 'text-bottom';
52+
private const VERTICAL_TEXT_TOP = 'text-top';
53+
54+
// Mapping for vertical alignment
55+
const VERTICAL_ALIGNMENT_FOR_XLSX = [
56+
self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM,
57+
self::VERTICAL_TOP => self::VERTICAL_TOP,
58+
self::VERTICAL_CENTER => self::VERTICAL_CENTER,
59+
self::VERTICAL_JUSTIFY => self::VERTICAL_JUSTIFY,
60+
self::VERTICAL_DISTRIBUTED => self::VERTICAL_DISTRIBUTED,
61+
// css settings that arent't in sync with Excel
62+
self::VERTICAL_BASELINE => self::VERTICAL_BOTTOM,
63+
self::VERTICAL_MIDDLE => self::VERTICAL_CENTER,
64+
self::VERTICAL_SUB => self::VERTICAL_BOTTOM,
65+
self::VERTICAL_SUPER => self::VERTICAL_TOP,
66+
self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_BOTTOM,
67+
self::VERTICAL_TEXT_TOP => self::VERTICAL_TOP,
68+
];
69+
70+
// Mapping for vertical alignment for Html
71+
const VERTICAL_ALIGNMENT_FOR_HTML = [
72+
self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM,
73+
self::VERTICAL_TOP => self::VERTICAL_TOP,
74+
self::VERTICAL_CENTER => self::VERTICAL_MIDDLE,
75+
self::VERTICAL_JUSTIFY => self::VERTICAL_MIDDLE,
76+
self::VERTICAL_DISTRIBUTED => self::VERTICAL_MIDDLE,
77+
// css settings that arent't in sync with Excel
78+
self::VERTICAL_BASELINE => self::VERTICAL_BASELINE,
79+
self::VERTICAL_MIDDLE => self::VERTICAL_MIDDLE,
80+
self::VERTICAL_SUB => self::VERTICAL_SUB,
81+
self::VERTICAL_SUPER => self::VERTICAL_SUPER,
82+
self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_TEXT_BOTTOM,
83+
self::VERTICAL_TEXT_TOP => self::VERTICAL_TEXT_TOP,
84+
];
2585

2686
// Read order
2787
const READORDER_CONTEXT = 0;
@@ -202,8 +262,9 @@ public function getHorizontal()
202262
*/
203263
public function setHorizontal(string $horizontalAlignment)
204264
{
205-
if ($horizontalAlignment == '') {
206-
$horizontalAlignment = self::HORIZONTAL_GENERAL;
265+
$horizontalAlignment = strtolower($horizontalAlignment);
266+
if ($horizontalAlignment === self::HORIZONTAL_CENTER_CONTINUOUS_LC) {
267+
$horizontalAlignment = self::HORIZONTAL_CENTER_CONTINUOUS;
207268
}
208269

209270
if ($this->isSupervisor) {
@@ -239,9 +300,7 @@ public function getVertical()
239300
*/
240301
public function setVertical($verticalAlignment)
241302
{
242-
if ($verticalAlignment == '') {
243-
$verticalAlignment = self::VERTICAL_BOTTOM;
244-
}
303+
$verticalAlignment = strtolower($verticalAlignment);
245304

246305
if ($this->isSupervisor) {
247306
$styleArray = $this->getStyleArray(['vertical' => $verticalAlignment]);

src/PhpSpreadsheet/Writer/Html.php

+6-18
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,6 @@ public function setEditHtmlCallback(?callable $callback): void
230230
$this->editHtmlCallback = $callback;
231231
}
232232

233-
const VALIGN_ARR = [
234-
Alignment::VERTICAL_BOTTOM => 'bottom',
235-
Alignment::VERTICAL_TOP => 'top',
236-
Alignment::VERTICAL_CENTER => 'middle',
237-
Alignment::VERTICAL_JUSTIFY => 'middle',
238-
];
239-
240233
/**
241234
* Map VAlign.
242235
*
@@ -246,17 +239,9 @@ public function setEditHtmlCallback(?callable $callback): void
246239
*/
247240
private function mapVAlign($vAlign)
248241
{
249-
return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline';
242+
return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? '';
250243
}
251244

252-
const HALIGN_ARR = [
253-
Alignment::HORIZONTAL_LEFT => 'left',
254-
Alignment::HORIZONTAL_RIGHT => 'right',
255-
Alignment::HORIZONTAL_CENTER => 'center',
256-
Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center',
257-
Alignment::HORIZONTAL_JUSTIFY => 'justify',
258-
];
259-
260245
/**
261246
* Map HAlign.
262247
*
@@ -266,7 +251,7 @@ private function mapVAlign($vAlign)
266251
*/
267252
private function mapHAlign($hAlign)
268253
{
269-
return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : '';
254+
return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? '';
270255
}
271256

272257
const BORDER_ARR = [
@@ -988,7 +973,10 @@ private function createCSSStyleAlignment(Alignment $alignment)
988973
$css = [];
989974

990975
// Create CSS
991-
$css['vertical-align'] = $this->mapVAlign($alignment->getVertical() ?? '');
976+
$verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
977+
if ($verticalAlign) {
978+
$css['vertical-align'] = $verticalAlign;
979+
}
992980
$textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
993981
if ($textAlign) {
994982
$css['text-align'] = $textAlign;

src/PhpSpreadsheet/Writer/Xlsx/Style.php

+15-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
66
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
77
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheet\Style\Alignment;
89
use PhpOffice\PhpSpreadsheet\Style\Border;
910
use PhpOffice\PhpSpreadsheet\Style\Borders;
1011
use PhpOffice\PhpSpreadsheet\Style\Conditional;
@@ -403,8 +404,14 @@ private function writeCellStyleXf(XMLWriter $objWriter, \PhpOffice\PhpSpreadshee
403404

404405
// alignment
405406
$objWriter->startElement('alignment');
406-
$objWriter->writeAttribute('horizontal', (string) $style->getAlignment()->getHorizontal());
407-
$objWriter->writeAttribute('vertical', (string) $style->getAlignment()->getVertical());
407+
$vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? '';
408+
$horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? '';
409+
if ($horizontal !== '') {
410+
$objWriter->writeAttribute('horizontal', $horizontal);
411+
}
412+
if ($vertical !== '') {
413+
$objWriter->writeAttribute('vertical', $vertical);
414+
}
408415

409416
$textRotation = 0;
410417
if ($style->getAlignment()->getTextRotation() >= 0) {
@@ -459,11 +466,13 @@ private function writeCellStyleDxf(XMLWriter $objWriter, \PhpOffice\PhpSpreadshe
459466

460467
// alignment
461468
$objWriter->startElement('alignment');
462-
if ($style->getAlignment()->getHorizontal() !== null) {
463-
$objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal());
469+
$horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? '';
470+
if ($horizontal) {
471+
$objWriter->writeAttribute('horizontal', $horizontal);
464472
}
465-
if ($style->getAlignment()->getVertical() !== null) {
466-
$objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical());
473+
$vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? '';
474+
if ($vertical) {
475+
$objWriter->writeAttribute('vertical', $vertical);
467476
}
468477

469478
if ($style->getAlignment()->getTextRotation() !== null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Style;
4+
5+
use PhpOffice\PhpSpreadsheet\Shared\File;
6+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
7+
use PhpOffice\PhpSpreadsheet\Style\Alignment;
8+
use PhpOffice\PhpSpreadsheet\Writer\Html;
9+
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
10+
use PHPUnit\Framework\TestCase;
11+
use ZipArchive;
12+
13+
class AlignmentMiddleTest extends TestCase
14+
{
15+
/** @var ?Spreadsheet */
16+
private $spreadsheet;
17+
18+
/** @var string */
19+
private $outputFileName = '';
20+
21+
protected function tearDown(): void
22+
{
23+
if ($this->spreadsheet !== null) {
24+
$this->spreadsheet->disconnectWorksheets();
25+
$this->spreadsheet = null;
26+
}
27+
if ($this->outputFileName !== '') {
28+
unlink($this->outputFileName);
29+
$this->outputFileName = '';
30+
}
31+
}
32+
33+
public function testCenterWriteHtml(): void
34+
{
35+
// Html Writer changes vertical align center to middle
36+
$this->spreadsheet = new Spreadsheet();
37+
$sheet = $this->spreadsheet->getActiveSheet();
38+
$sheet->getCell('A1')->setValue('Cell1');
39+
$sheet->getStyle('A1')
40+
->getAlignment()
41+
->setVertical(Alignment::VERTICAL_CENTER);
42+
$writer = new HTML($this->spreadsheet);
43+
$html = $writer->generateHtmlAll();
44+
self::assertStringContainsString('vertical-align:middle', $html);
45+
self::assertStringNotContainsString('vertical-align:center', $html);
46+
}
47+
48+
public function testCenterWriteXlsx(): void
49+
{
50+
// Xlsx Writer uses vertical align center unchanged
51+
$this->spreadsheet = new Spreadsheet();
52+
$sheet = $this->spreadsheet->getActiveSheet();
53+
$sheet->getCell('A1')->setValue('Cell1');
54+
$sheet->getStyle('A1')
55+
->getAlignment()
56+
->setVertical(Alignment::VERTICAL_CENTER);
57+
$this->outputFileName = File::temporaryFilename();
58+
$writer = new Xlsx($this->spreadsheet);
59+
$writer->save($this->outputFileName);
60+
$zip = new ZipArchive();
61+
$zip->open($this->outputFileName);
62+
$html = $zip->getFromName('xl/styles.xml');
63+
$zip->close();
64+
self::assertStringContainsString('vertical="center"', $html);
65+
self::assertStringNotContainsString('vertical="middle"', $html);
66+
}
67+
68+
public function testCenterWriteXlsx2(): void
69+
{
70+
// Xlsx Writer changes vertical align middle to center
71+
$this->spreadsheet = new Spreadsheet();
72+
$sheet = $this->spreadsheet->getActiveSheet();
73+
$sheet->getCell('A1')->setValue('Cell1');
74+
$sheet->getStyle('A1')
75+
->getAlignment()
76+
->setVertical('middle');
77+
$this->outputFileName = File::temporaryFilename();
78+
$writer = new Xlsx($this->spreadsheet);
79+
$writer->save($this->outputFileName);
80+
$zip = new ZipArchive();
81+
$zip->open($this->outputFileName);
82+
$html = $zip->getFromName('xl/styles.xml');
83+
$zip->close();
84+
self::assertStringContainsString('vertical="center"', $html);
85+
self::assertStringNotContainsString('vertical="middle"', $html);
86+
}
87+
}

0 commit comments

Comments
 (0)