Skip to content

Commit c35cc94

Browse files
committed
Hyperlink Styles
Fix #1632. Excel automatically supplies a style for cells which contain hyperlinks (e.g. underlined, and blue text changing to purple after the link has been followed). PhpSpreadsheet cannot handle the style automatically. The user can, with some effort, specify a style for the cell which mimics Excel's choice (except for the color change after following). Examining a sheet with a hyperlink created through Excel, it appears that Excel creates 3 different entries in styles.xml, one for cellStyleXfs, one for cellXfs, and one for cellStyles. It is difficult for me to figure out how they interrelate. This is especially so since PhpSpreadsheet outputs only 1 entry (for the default style) for each of cellStyles and cellStyleXfs. However, it appears that only the cellXfs entry is required, and, when it specifies a font whose color specifies `theme="10"` rather than an rgb value, the style works as expected. In order to implement this, it is necessary to add a `theme` property, with setter and getter, to Style/Color. There are 12 possible values for theme, 0-11 representing 0=dk1 1=lt1 2=dk2 3=lt2 4-9=accent1-6 10=hlink 11=folHlink. This PR is mainly to allow the use of hlink, but the others are also usable if a use case arises for them. If a theme is set for Color, Xlsx Writer will use the theme rather than rgb when generating the color xml. Other writers will continue to use rgb rather than theme, so there is a use case for setting both if you want to generate both Xlsx and some other format. The `theme` property will, for now, be ignored except for Font. There is probably a case to be made for using it for Fill, and maybe for Border and other areas that I haven't yet considered. I will wait for someone to make that case before adding those. In order to make it as easy as possible to use this, a method `setHyperlinkTheme` is added to both Style/Color and Style/Font. The one in Color sets `theme` to the appropriate value. The one in Font calls the one in Color, and also sets `underline` on (this will be honored by other writers in addition to Xlsx). Samples which use hyperlinks are updated to use `setHyperlinkTheme`. So is `Reader\Xlsx\HyperlinkTest`, with appropriate tests added.
1 parent 85a9a39 commit c35cc94

File tree

11 files changed

+154
-41
lines changed

11 files changed

+154
-41
lines changed

samples/Basic/02_Types.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,11 @@
142142
->getHyperlink()
143143
->setUrl('https://github.com/PHPOffice/PhpSpreadsheet')
144144
->setTooltip('Navigate to PhpSpreadsheet website');
145+
$spreadsheet->getActiveSheet()->getStyle('C17')->getFont()->setHyperlinkTheme();
145146

146147
$spreadsheet->getActiveSheet()
147148
->setCellValue('C18', '=HYPERLINK("mailto:abc@def.com","abc@def.com")');
149+
$spreadsheet->getActiveSheet()->getStyle('C18')->getFont()->setHyperlinkTheme();
148150

149151
$spreadsheet->getActiveSheet()
150152
->setCellValue('A20', 'String')

samples/templates/sampleSpreadsheet.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,18 @@
248248

249249
// Add a hyperlink to the sheet
250250
$helper->log('Add a hyperlink to an external website');
251-
$spreadsheet->getActiveSheet()->setCellValue('E26', 'www.phpexcel.net');
251+
$spreadsheet->getActiveSheet()->setCellValue('E26', 'www.example.com');
252252
$spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setUrl('https://www.example.com');
253253
$spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setTooltip('Navigate to website');
254254
$spreadsheet->getActiveSheet()->getStyle('E26')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
255+
$spreadsheet->getActiveSheet()->getStyle('E26')->getFont()->setHyperlinkTheme();
255256

256257
$helper->log('Add a hyperlink to another cell on a different worksheet within the workbook');
257258
$spreadsheet->getActiveSheet()->setCellValue('E27', 'Terms and conditions');
258259
$spreadsheet->getActiveSheet()->getCell('E27')->getHyperlink()->setUrl("sheet://'Terms and conditions'!A1");
259260
$spreadsheet->getActiveSheet()->getCell('E27')->getHyperlink()->setTooltip('Review terms and conditions');
260261
$spreadsheet->getActiveSheet()->getStyle('E27')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
262+
$spreadsheet->getActiveSheet()->getStyle('E27')->getFont()->setHyperlinkTheme();
261263

262264
// Add a drawing to the worksheet
263265
$helper->log('Add a drawing to the worksheet');

samples/templates/sampleSpreadsheet2.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,18 @@
248248

249249
// Add a hyperlink to the sheet
250250
$helper->log('Add a hyperlink to an external website');
251-
$spreadsheet->getActiveSheet()->setCellValue('E26', 'www.phpexcel.net');
251+
$spreadsheet->getActiveSheet()->setCellValue('E26', 'www.example.com');
252252
$spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setUrl('https://www.example.com');
253253
$spreadsheet->getActiveSheet()->getCell('E26')->getHyperlink()->setTooltip('Navigate to website');
254254
$spreadsheet->getActiveSheet()->getStyle('E26')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
255+
$spreadsheet->getActiveSheet()->getStyle('E26')->getFont()->setHyperlinkTheme();
255256

256257
$helper->log('Add a hyperlink to another cell on a different worksheet within the workbook');
257258
$spreadsheet->getActiveSheet()->setCellValue('E27', 'Terms and conditions');
258259
$spreadsheet->getActiveSheet()->getCell('E27')->getHyperlink()->setUrl("sheet://'Terms and conditions'!A1");
259260
$spreadsheet->getActiveSheet()->getCell('E27')->getHyperlink()->setTooltip('Review terms and conditions');
260261
$spreadsheet->getActiveSheet()->getStyle('E27')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
262+
$spreadsheet->getActiveSheet()->getStyle('E27')->getFont()->setHyperlinkTheme();
261263

262264
// Add a drawing to the worksheet
263265
$helper->log('Add a drawing to the worksheet');

src/PhpSpreadsheet/Reader/Xlsx/Styles.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml):
104104
$attr = $this->getStyleAttributes($fontStyleXml->strike);
105105
$fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val']));
106106
}
107-
$fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color));
107+
$theme = $this->readColorTheme($fontStyleXml->color);
108+
if ($theme >= 0) {
109+
$fontStyle->getColor()->setTheme($theme);
110+
}
111+
$fontStyle->getColor()
112+
->setARGB(
113+
$this->readColor($fontStyleXml->color)
114+
);
108115

109116
if (isset($fontStyleXml->u)) {
110117
$attr = $this->getStyleAttributes($fontStyleXml->u);
@@ -398,6 +405,13 @@ public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style):
398405
}
399406
}
400407

408+
public function readColorTheme(SimpleXMLElement $color): int
409+
{
410+
$attr = $this->getStyleAttributes($color);
411+
412+
return isset($attr['theme']) ? (int) $attr['theme'] : -1;
413+
}
414+
401415
public function readColor(SimpleXMLElement $color, bool $background = false): string
402416
{
403417
$attr = $this->getStyleAttributes($color);

src/PhpSpreadsheet/Style/Color.php

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Style;
44

5+
use PhpOffice\PhpSpreadsheet\Theme;
6+
57
class Color extends Supervisor
68
{
79
const NAMED_COLORS = [
@@ -111,6 +113,8 @@ class Color extends Supervisor
111113

112114
private bool $hasChanged = false;
113115

116+
private int $theme = -1;
117+
114118
/**
115119
* Create a new Color.
116120
*
@@ -176,21 +180,28 @@ public function getStyleArray(array $array): array
176180
* $spreadsheet->getActiveSheet()->getStyle('B2')->getFont()->getColor()->applyFromArray(['rgb' => '808080']);
177181
* </code>
178182
*
179-
* @param array{rgb?: string, argb?: string} $styleArray Array containing style information
183+
* @param array{rgb?: string, argb?: string, theme?: int} $styleArray Array containing style information
180184
*
181185
* @return $this
182186
*/
183187
public function applyFromArray(array $styleArray): static
184188
{
185189
if ($this->isSupervisor) {
186-
$this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray));
190+
$this->getActiveSheet()
191+
->getStyle($this->getSelectedCells())
192+
->applyFromArray(
193+
$this->getStyleArray($styleArray)
194+
);
187195
} else {
188196
if (isset($styleArray['rgb'])) {
189197
$this->setRGB($styleArray['rgb']);
190198
}
191199
if (isset($styleArray['argb'])) {
192200
$this->setARGB($styleArray['argb']);
193201
}
202+
if (isset($styleArray['theme'])) {
203+
$this->setTheme($styleArray['theme']);
204+
}
194205
}
195206

196207
return $this;
@@ -402,6 +413,7 @@ public function getHashCode(): string
402413

403414
return md5(
404415
$this->argb
416+
. (string) $this->theme
405417
. __CLASS__
406418
);
407419
}
@@ -411,6 +423,8 @@ protected function exportArray1(): array
411423
{
412424
$exportedArray = [];
413425
$this->exportArray2($exportedArray, 'argb', $this->getARGB());
426+
$theme = $this->getTheme();
427+
$this->exportArray2($exportedArray, 'theme', $this->getTheme());
414428

415429
return $exportedArray;
416430
}
@@ -423,4 +437,34 @@ public function getHasChanged(): bool
423437

424438
return $this->hasChanged;
425439
}
440+
441+
public function getTheme(): int
442+
{
443+
if ($this->isSupervisor) {
444+
return $this->getSharedComponent()->getTheme();
445+
}
446+
447+
return $this->theme;
448+
}
449+
450+
public function setTheme(int $theme): self
451+
{
452+
$this->hasChanged = true;
453+
454+
if ($this->isSupervisor) {
455+
$styleArray = $this->getStyleArray(['theme' => $theme]);
456+
$this->getActiveSheet()
457+
->getStyle($this->getSelectedCells())
458+
->applyFromArray($styleArray);
459+
} else {
460+
$this->theme = $theme;
461+
}
462+
463+
return $this;
464+
}
465+
466+
public function setHyperlinkTheme(): self
467+
{
468+
return $this->setTheme(Theme::HYPERLINK_THEME);
469+
}
426470
}

src/PhpSpreadsheet/Style/Font.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,10 @@ public function applyFromArray(array $styleArray): static
206206
$this->setStrikethrough($styleArray['strikethrough']);
207207
}
208208
if (isset($styleArray['color'])) {
209-
$this->getColor()->applyFromArray($styleArray['color']);
209+
/** @var array{rgb?: string, argb?: string, theme?: int} */
210+
$temp = $styleArray['color'];
211+
$this->getColor()
212+
->applyFromArray($temp);
210213
}
211214
if (isset($styleArray['size'])) {
212215
$this->setSize($styleArray['size']);
@@ -398,9 +401,6 @@ public function getBold(): ?bool
398401
*/
399402
public function setBold(bool $bold): static
400403
{
401-
if ($bold == '') {
402-
$bold = false;
403-
}
404404
if ($this->isSupervisor) {
405405
$styleArray = $this->getStyleArray(['bold' => $bold]);
406406
$this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
@@ -430,9 +430,6 @@ public function getItalic(): ?bool
430430
*/
431431
public function setItalic(bool $italic): static
432432
{
433-
if ($italic == '') {
434-
$italic = false;
435-
}
436433
if ($this->isSupervisor) {
437434
$styleArray = $this->getStyleArray(['italic' => $italic]);
438435
$this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
@@ -669,10 +666,6 @@ public function getStrikethrough(): ?bool
669666
*/
670667
public function setStrikethrough(bool $strikethru): static
671668
{
672-
if ($strikethru == '') {
673-
$strikethru = false;
674-
}
675-
676669
if ($this->isSupervisor) {
677670
$styleArray = $this->getStyleArray(['strikethrough' => $strikethru]);
678671
$this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
@@ -830,6 +823,14 @@ public function getCap(): ?string
830823
return $this->cap;
831824
}
832825

826+
public function setHyperlinkTheme(): self
827+
{
828+
$this->color->setHyperlinkTheme();
829+
$this->setUnderline(self::UNDERLINE_SINGLE);
830+
831+
return $this;
832+
}
833+
833834
/**
834835
* Implement PHP __clone to create a deep clone, not just a shallow copy.
835836
*/

src/PhpSpreadsheet/Theme.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class Theme
88

99
private string $themeFontName = 'Office';
1010

11+
public const HYPERLINK_THEME = 10;
1112
public const COLOR_SCHEME_2013_PLUS_NAME = 'Office 2013+';
1213
public const COLOR_SCHEME_2013_PLUS = [
1314
'dk1' => '000000',

src/PhpSpreadsheet/Writer/Xlsx/Style.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,12 @@ private function writeFont(XMLWriter $objWriter, Font $font): void
347347
}
348348

349349
// Foreground color
350-
if ($font->getColor()->getARGB() !== null) {
350+
if ($font->getColor()->getTheme() >= 0) {
351+
$this->startFont($objWriter, $fontStarted);
352+
$objWriter->startElement('color');
353+
$objWriter->writeAttribute('theme', (string) $font->getColor()->getTheme());
354+
$objWriter->endElement();
355+
} elseif ($font->getColor()->getARGB() !== null) {
351356
$this->startFont($objWriter, $fontStarted);
352357
$objWriter->startElement('color');
353358
$objWriter->writeAttribute('rgb', $font->getColor()->getARGB());

tests/PhpSpreadsheetTests/Functional/CommentsTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ public function testComments(string $format): void
4848
self::assertEquals($comment, $commentClone);
4949
self::assertNotSame($comment, $commentClone);
5050
if ($format === 'Xlsx') {
51-
self::assertEquals('feb0c24b880a8130262dadf801f85e94', $comment->getHashCode());
52-
self::assertEquals(Alignment::HORIZONTAL_GENERAL, $comment->getAlignment());
51+
self::assertSame('bc7bcec8f676a333dae65c945cf8dace', $comment->getHashCode(), 'changed due to addition of theme to fillColor');
52+
self::assertSame(Alignment::HORIZONTAL_GENERAL, $comment->getAlignment());
5353
$comment->setAlignment(Alignment::HORIZONTAL_RIGHT);
54-
self::assertEquals(Alignment::HORIZONTAL_RIGHT, $comment->getAlignment());
54+
self::assertSame(Alignment::HORIZONTAL_RIGHT, $comment->getAlignment());
5555
}
5656
$spreadsheet->disconnectWorksheets();
5757
$reloadedSpreadsheet->disconnectWorksheets();

tests/PhpSpreadsheetTests/Reader/Xlsx/HyperlinkTest.php

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,73 @@ public function testReadAndWriteHyperlinks(): void
1919
$sheet1->getCell('B1')->setValue('this is b1');
2020
$spreadsheet->addNamedRange(new NamedRange('namedb1', $sheet1, '$B$1'));
2121
$sheet1->setCellValue('A2', 'link to same sheet');
22-
$sheet1->getCell('A2')->getHyperlink()->setUrl("sheet://'Sheet One'!A1");
22+
$sheet1->getCell('A2')->getHyperlink()
23+
->setUrl("sheet://'Sheet One'!A1");
24+
$sheet1->getStyle('A2')->getFont()->setHyperlinkTheme();
2325
$sheet1->setCellValue('A3', 'link to defined name');
24-
$sheet1->getCell('A3')->getHyperlink()->setUrl('sheet://namedb1');
26+
$sheet1->getCell('A3')->getHyperlink()
27+
->setUrl('sheet://namedb1');
28+
$sheet1->getStyle('A3')->getFont()->setHyperlinkTheme();
2529

2630
$sheet2 = $spreadsheet->createSheet();
2731
$sheet2->setTitle('Sheet Two');
2832
$sheet2->setCellValue('A2', 'link to other sheet');
29-
$sheet2->getCell('A2')->getHyperlink()->setUrl("sheet://'Sheet One'!A1");
33+
$sheet2->getCell('A2')->getHyperlink()
34+
->setUrl("sheet://'Sheet One'!A1");
35+
$sheet2->getStyle('A2')->getFont()->setHyperlinkTheme();
3036
$sheet2->setCellValue('A3', 'external link');
31-
$sheet2->getCell('A3')->getHyperlink()->setUrl('https://www.example.com');
37+
$sheet2->getCell('A3')->getHyperlink()
38+
->setUrl('https://www.example.com');
39+
$sheet2->getStyle('A3')->getFont()->setHyperlinkTheme();
3240
$sheet2->setCellValue('A4', 'external link with anchor');
33-
$sheet2->getCell('A4')->getHyperlink()->setUrl('https://www.example.com#anchor');
41+
$sheet2->getCell('A4')->getHyperlink()
42+
->setUrl('https://www.example.com#anchor');
3443
$sheet2->getCell('A4')->getHyperlink()->setTooltip('go to anchor tag on example.com');
44+
$sheet2->getStyle('A4')->getFont()->setHyperlinkTheme();
3545

3646
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
3747
$spreadsheet->disconnectWorksheets();
3848
$rsheet1 = $reloadedSpreadsheet->getSheet(0);
3949
self::assertSame('link to same sheet', $rsheet1->getCell('A2')->getValue());
4050
self::assertSame("sheet://'Sheet One'!A1", $rsheet1->getCell('A2')->getHyperlink()->getUrl());
51+
4152
self::assertSame('link to defined name', $rsheet1->getCell('A3')->getValue());
4253
self::assertSame('sheet://namedb1', $rsheet1->getCell('A3')->getHyperlink()->getUrl());
4354

4455
$rsheet2 = $reloadedSpreadsheet->getSheet(1);
4556
self::assertSame('link to other sheet', $rsheet2->getCell('A2')->getValue());
4657
self::assertSame("sheet://'Sheet One'!A1", $rsheet2->getCell('A2')->getHyperlink()->getUrl());
58+
4759
self::assertSame('external link', $rsheet2->getCell('A3')->getValue());
4860
self::assertSame('https://www.example.com', $rsheet2->getCell('A3')->getHyperlink()->getUrl());
61+
4962
self::assertSame('https://www.example.com#anchor', $rsheet2->getCell('A4')->getHyperlink()->getUrl());
5063
self::assertSame('external link with anchor', $rsheet2->getCell('A4')->getValue());
5164
self::assertSame('go to anchor tag on example.com', $rsheet2->getCell('A4')->getHyperlink()->getToolTip());
65+
66+
$testCells = [
67+
[0, 'A2'],
68+
[0, 'A3'],
69+
[0, 'A2'],
70+
[1, 'A3'],
71+
[1, 'A4'],
72+
];
73+
foreach ($testCells as $sheetAndCell) {
74+
[$sheetIndex, $cell] = $sheetAndCell;
75+
$rsheet = $reloadedSpreadsheet->getSheet($sheetIndex);
76+
self::assertSame(
77+
10,
78+
$rsheet->getStyle($cell)
79+
->getFont()->getColor()->getTheme(),
80+
"theme sheet $sheetIndex cell $cell"
81+
);
82+
self::assertSame(
83+
'single',
84+
$rsheet->getStyle('A2')->getFont()->getUnderline(),
85+
"underline sheet $sheetIndex cell $cell"
86+
);
87+
}
88+
5289
$reloadedSpreadsheet->disconnectWorksheets();
5390
}
5491
}

0 commit comments

Comments
 (0)