Skip to content

Commit

Permalink
Font Themes
Browse files Browse the repository at this point in the history
It isn't a feature of Excel that I've made any use of, but PR PHPOffice#3476 added better Theme support for colors, and it is relatively easy to add Theme support for Fonts on top of that.

Excel assigns two theme fonts to its spreadsheets, one for Headings (major), and one for Body (minor). If the body theme is Calibri, when you choose a font for a cell in Excel, you can choose 'Calibri (Body)' from the Theme Fonts section at the top of the Font dropdown, or 'Calibri' from the 'All Fonts' section. If you choose the former, the cell will be automatically restyled if you change the Theme Fonts (via Page Layout, Themes, Fonts). The relationship to the theme fonts is recorded in the XML via a `scheme` tag (descending from `font`) whose `val` attribute can be either `major` or `minor`. Accordingly, this PR, in addition to defining the Theme Font properties, adds a `scheme` property, with getter and setter, to Style/Font.

The main benefit of this PR is that you can now load and save a spreadsheet preserving the connections to the Theme Fonts, without having to take any additional action.

A secondary benefit arises from the following difference. Empty cells in Excel will use the spreadsheet's default font name when they are filled in; but, in Google Sheets, they will use the Theme Minor Font name. By setting the `scheme` property in the default style, the resulting spreadsheet will behave the same in both Excel and Google.

I will note that Excel's font themes specify a Latin font, an East Asian font, a Complex Scripts font, and a set of font substitutions for various languages. PhpSpreadsheet will preserve all of these, and allow them to be changed. However, although it is easy to imagine how the non-Latin options might work, I have not yet been able to come up with an example where Excel uses any of them. In particular, if I use a theme font which does not support language X, and I use Language X in a cell bound to the theme, Excel will use a substitution font which does support it, but the font which it uses does not seem to be chosen from the alternatives supplied in the theme.
  • Loading branch information
oleibman committed Mar 28, 2023
1 parent 0e6866d commit f63f157
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 94 deletions.
14 changes: 14 additions & 0 deletions docs/topics/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,20 @@ $spreadsheet->getDefaultStyle()->getFont()->setName('Arial');
$spreadsheet->getDefaultStyle()->getFont()->setSize(8);
```

Excel also offers "theme fonts", with separate font names for major (header) and minor (body) text. PhpSpreadsheet will use the Excel 2007 default (Cambria) for major (default is Calibri Light in Excel 2013+); PhpSpreadsheet default for minor is Calibri, which is used by Excel 2007+. To align the default font name with the minor font name:

```php
$spreadsheet->getTheme()
->setThemeFontName('custom')
->setMinorFontValues('Arial', 'Arial', 'Arial', []);
$spreadsheet->getDefaultStyle()->getFont()->setScheme('minor');
```

All cells bound to the theme fonts (via the `Font::setScheme` method) can be easily changed to a different font in Excel. To do this in PhpSpreadsheet, an additional method call is needed:
```php
$spreadsheet->resetThemeFonts();
```

### Styling cell borders

In PhpSpreadsheet it is easy to apply various borders on a rectangular
Expand Down
30 changes: 30 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,36 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
$theme = new Theme($themeName, $colourSchemeName, $themeColours);
$this->styleReader->setTheme($theme);

$fontScheme = self::getAttributes($xmlTheme->themeElements->fontScheme);
$fontSchemeName = (string) $fontScheme['name'];
$excel->getTheme()->setThemeFontName($fontSchemeName);
$majorFonts = [];
$minorFonts = [];
$fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS);
$majorLatin = self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '';
$majorEastAsian = self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '';
$majorComplexScript = self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '';
$minorLatin = self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '';
$minorEastAsian = self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '';
$minorComplexScript = self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '';

foreach ($fontScheme->majorFont->font as $xmlFont) {
$fontAttributes = self::getAttributes($xmlFont);
$script = (string) ($fontAttributes['script'] ?? '');
if (!empty($script)) {
$majorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
}
}
foreach ($fontScheme->minorFont->font as $xmlFont) {
$fontAttributes = self::getAttributes($xmlFont);
$script = (string) ($fontAttributes['script'] ?? '');
if (!empty($script)) {
$minorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
}
}
$excel->getTheme()->setMajorFontValues($majorLatin, $majorEastAsian, $majorComplexScript, $majorFonts);
$excel->getTheme()->setMinorFontValues($minorLatin, $minorEastAsian, $minorComplexScript, $minorFonts);

break;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx/Styles.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml):
}
}
}
if (isset($fontStyleXml->scheme)) {
$attr = $this->getStyleAttributes($fontStyleXml->scheme);
$fontStyle->setScheme((string) $attr['val']);
}
}

private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void
Expand Down
22 changes: 22 additions & 0 deletions src/PhpSpreadsheet/Spreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -1663,4 +1663,26 @@ public function jsonSerialize(): mixed
{
throw new Exception('Spreadsheet objects cannot be json encoded');
}

public function resetThemeFonts(): void
{
$majorFontLatin = $this->theme->getMajorFontLatin();
$minorFontLatin = $this->theme->getMinorFontLatin();
foreach ($this->cellXfCollection as $cellStyleXf) {
$scheme = $cellStyleXf->getFont()->getScheme();
if ($scheme === 'major') {
$cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
} elseif ($scheme === 'minor') {
$cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
}
}
foreach ($this->cellStyleXfCollection as $cellStyleXf) {
$scheme = $cellStyleXf->getFont()->getScheme();
if ($scheme === 'major') {
$cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
} elseif ($scheme === 'minor') {
$cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
}
}
}
}
39 changes: 34 additions & 5 deletions src/PhpSpreadsheet/Style/Font.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ class Font extends Supervisor
*/
public $colorIndex;

/** @var string */
protected $scheme = '';

/**
* Create a new Font.
*
Expand Down Expand Up @@ -234,6 +237,9 @@ public function applyFromArray(array $styleArray)
if (isset($styleArray['chartColor'])) {
$this->chartColor = $styleArray['chartColor'];
}
if (isset($styleArray['scheme'])) {
$this->setScheme($styleArray['scheme']);
}
}

return $this;
Expand Down Expand Up @@ -281,13 +287,11 @@ public function getComplexScript(): string
}

/**
* Set Name.
* Set Name and turn off Scheme.
*
* @param string $fontname
*
* @return $this
*/
public function setName($fontname)
public function setName($fontname): self
{
if ($fontname == '') {
$fontname = 'Calibri';
Expand All @@ -299,7 +303,7 @@ public function setName($fontname)
$this->name = $fontname;
}

return $this;
return $this->setScheme('');
}

public function setLatin(string $fontname): self
Expand Down Expand Up @@ -784,6 +788,7 @@ public function getHashCode()
$this->underline .
($this->strikethrough ? 't' : 'f') .
$this->color->getHashCode() .
$this->scheme .
implode(
'*',
[
Expand Down Expand Up @@ -812,6 +817,7 @@ protected function exportArray1(): array
$this->exportArray2($exportedArray, 'italic', $this->getItalic());
$this->exportArray2($exportedArray, 'latin', $this->getLatin());
$this->exportArray2($exportedArray, 'name', $this->getName());
$this->exportArray2($exportedArray, 'scheme', $this->getScheme());
$this->exportArray2($exportedArray, 'size', $this->getSize());
$this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough());
$this->exportArray2($exportedArray, 'strikeType', $this->getStrikeType());
Expand All @@ -822,4 +828,27 @@ protected function exportArray1(): array

return $exportedArray;
}

public function getScheme(): string
{
if ($this->isSupervisor) {
return $this->getSharedComponent()->getScheme();
}

return $this->scheme;
}

public function setScheme(string $scheme): self
{
if ($scheme === '' || $scheme === 'major' || $scheme === 'minor') {
if ($this->isSupervisor) {
$styleArray = $this->getStyleArray(['scheme' => $scheme]);
$this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
} else {
$this->scheme = $scheme;
}
}

return $this;
}
}
191 changes: 191 additions & 0 deletions src/PhpSpreadsheet/Theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class Theme
/** @var string */
private $themeColorName = 'Office';

/** @var string */
private $themeFontName = 'Office';

public const COLOR_SCHEME_2013_PLUS_NAME = 'Office 2013+';
public const COLOR_SCHEME_2013_PLUS = [
'dk1' => '000000',
Expand Down Expand Up @@ -42,6 +45,104 @@ class Theme
/** @var string[] */
private $themeColors = self::COLOR_SCHEME_2007_2010;

/** @var string */
private $majorFontLatin = 'Cambria';

/** @var string */
private $majorFontEastAsian = '';

/** @var string */
private $majorFontComplexScript = '';

/** @var string */
private $minorFontLatin = 'Calibri';

/** @var string */
private $minorFontEastAsian = '';

/** @var string */
private $minorFontComplexScript = '';

/**
* Map of Major (header) fonts to write.
*
* @var string[]
*/
private $majorFontSubstitutions = self::FONTS_TIMES_SUBSTITUTIONS;

/**
* Map of Minor (body) fonts to write.
*
* @var string[]
*/
private $minorFontSubstitutions = self::FONTS_ARIAL_SUBSTITUTIONS;

public const FONTS_TIMES_SUBSTITUTIONS = [
'Jpan' => 'MS Pゴシック',
'Hang' => '맑은 고딕',
'Hans' => '宋体',
'Hant' => '新細明體',
'Arab' => 'Times New Roman',
'Hebr' => 'Times New Roman',
'Thai' => 'Tahoma',
'Ethi' => 'Nyala',
'Beng' => 'Vrinda',
'Gujr' => 'Shruti',
'Khmr' => 'MoolBoran',
'Knda' => 'Tunga',
'Guru' => 'Raavi',
'Cans' => 'Euphemia',
'Cher' => 'Plantagenet Cherokee',
'Yiii' => 'Microsoft Yi Baiti',
'Tibt' => 'Microsoft Himalaya',
'Thaa' => 'MV Boli',
'Deva' => 'Mangal',
'Telu' => 'Gautami',
'Taml' => 'Latha',
'Syrc' => 'Estrangelo Edessa',
'Orya' => 'Kalinga',
'Mlym' => 'Kartika',
'Laoo' => 'DokChampa',
'Sinh' => 'Iskoola Pota',
'Mong' => 'Mongolian Baiti',
'Viet' => 'Times New Roman',
'Uigh' => 'Microsoft Uighur',
'Geor' => 'Sylfaen',
];

public const FONTS_ARIAL_SUBSTITUTIONS = [
'Jpan' => 'MS Pゴシック',
'Hang' => '맑은 고딕',
'Hans' => '宋体',
'Hant' => '新細明體',
'Arab' => 'Arial',
'Hebr' => 'Arial',
'Thai' => 'Tahoma',
'Ethi' => 'Nyala',
'Beng' => 'Vrinda',
'Gujr' => 'Shruti',
'Khmr' => 'DaunPenh',
'Knda' => 'Tunga',
'Guru' => 'Raavi',
'Cans' => 'Euphemia',
'Cher' => 'Plantagenet Cherokee',
'Yiii' => 'Microsoft Yi Baiti',
'Tibt' => 'Microsoft Himalaya',
'Thaa' => 'MV Boli',
'Deva' => 'Mangal',
'Telu' => 'Gautami',
'Taml' => 'Latha',
'Syrc' => 'Estrangelo Edessa',
'Orya' => 'Kalinga',
'Mlym' => 'Kartika',
'Laoo' => 'DokChampa',
'Sinh' => 'Iskoola Pota',
'Mong' => 'Mongolian Baiti',
'Viet' => 'Arial',
'Uigh' => 'Microsoft Uighur',
'Geor' => 'Sylfaen',
];

public function getThemeColors(): array
{
return $this->themeColors;
Expand Down Expand Up @@ -73,4 +174,94 @@ public function setThemeColorName(string $name, ?array $themeColors = null): sel

return $this;
}

public function getMajorFontLatin(): string
{
return $this->majorFontLatin;
}

public function getMajorFontEastAsian(): string
{
return $this->majorFontEastAsian;
}

public function getMajorFontComplexScript(): string
{
return $this->majorFontComplexScript;
}

public function getMajorFontSubstitutions(): array
{
return $this->majorFontSubstitutions;
}

public function setMajorFontValues(?string $latin, ?string $eastAsian, ?string $complexScript, ?array $substitutions): self
{
if (!empty($latin)) {
$this->majorFontLatin = $latin;
}
if ($eastAsian !== null) {
$this->majorFontEastAsian = $eastAsian;
}
if ($complexScript !== null) {
$this->majorFontComplexScript = $complexScript;
}
if ($substitutions !== null) {
$this->majorFontSubstitutions = $substitutions;
}

return $this;
}

public function getMinorFontLatin(): string
{
return $this->minorFontLatin;
}

public function getMinorFontEastAsian(): string
{
return $this->minorFontEastAsian;
}

public function getMinorFontComplexScript(): string
{
return $this->minorFontComplexScript;
}

public function getMinorFontSubstitutions(): array
{
return $this->minorFontSubstitutions;
}

public function setMinorFontValues(?string $latin, ?string $eastAsian, ?string $complexScript, ?array $substitutions): self
{
if (!empty($latin)) {
$this->minorFontLatin = $latin;
}
if ($eastAsian !== null) {
$this->minorFontEastAsian = $eastAsian;
}
if ($complexScript !== null) {
$this->minorFontComplexScript = $complexScript;
}
if ($substitutions !== null) {
$this->minorFontSubstitutions = $substitutions;
}

return $this;
}

public function getThemeFontName(): string
{
return $this->themeFontName;
}

public function setThemeFontName(?string $name): self
{
if (!empty($name)) {
$this->themeFontName = $name;
}

return $this;
}
}
Loading

0 comments on commit f63f157

Please sign in to comment.